[IMP] website: configurator for automatic website generation
On website app installation and on new website creation a configurator is launched. The purpose of this configurator is to generate a website that meet the user's needs. The configurator is composed of 4 steps: 1) Business description: the user is asked to describe its need with its website purpose (dropdown), its industry (autocomplete search) and its objective (dropdown). 2) Logo and palette selection: the user must select a color palette for its website. He can also upload its logo. In this case color palettes recommendations are generated based on the logo's colors. 3) Features selection: the user select the pages and applications he needs. 4) Theme selection: three themes are recommended to the user based on its industry. This screen display a preview of these three themes. task-id: 2451965 ENT PR: odoo/enterprise#16949 UPG PR: odoo/upgrade#2316 closes odoo/odoo#67537 Signed-off-by: Sébastien Mottet <smottet@users.noreply.github.com>
This commit is contained in:
@@ -87,7 +87,7 @@ class BaseDocumentLayout(models.TransientModel):
|
||||
wizard_for_image = wizard.with_context(bin_size=False)
|
||||
else:
|
||||
wizard_for_image = wizard
|
||||
wizard.logo_primary_color, wizard.logo_secondary_color = wizard_for_image._parse_logo_colors()
|
||||
wizard.logo_primary_color, wizard.logo_secondary_color = wizard.extract_image_primary_secondary_colors(wizard_for_image.logo)
|
||||
|
||||
@api.depends('report_layout_id', 'logo', 'font', 'primary_color', 'secondary_color', 'report_header', 'report_footer')
|
||||
def _compute_preview(self):
|
||||
@@ -150,7 +150,8 @@ class BaseDocumentLayout(models.TransientModel):
|
||||
if wizard.logo_secondary_color:
|
||||
wizard.secondary_color = wizard.logo_secondary_color
|
||||
|
||||
def _parse_logo_colors(self, logo=None, white_threshold=225):
|
||||
@api.model
|
||||
def extract_image_primary_secondary_colors(self, logo, white_threshold=225):
|
||||
"""
|
||||
Identifies dominant colors
|
||||
|
||||
@@ -158,16 +159,13 @@ class BaseDocumentLayout(models.TransientModel):
|
||||
transparent colors and white-ish colors, then calls the averaging
|
||||
method twice to evaluate both primary and secondary colors.
|
||||
|
||||
:param logo: alternate logo to process
|
||||
:param logo: logo to process
|
||||
:param white_threshold: arbitrary value defining the maximum value a color can reach
|
||||
|
||||
:return colors: hex values of primary and secondary colors
|
||||
"""
|
||||
self.ensure_one()
|
||||
logo = logo or self.logo
|
||||
if not logo:
|
||||
return False, False
|
||||
|
||||
# The "===" gives different base64 encoding a correct padding
|
||||
logo += b'===' if type(logo) == bytes else '==='
|
||||
try:
|
||||
|
||||
@@ -54,8 +54,7 @@ class TestBaseDocumentLayoutHelpers(TransactionCase):
|
||||
if not fname_split[0] in _file_cache:
|
||||
with Image.open(os.path.join(dir_path, fname), 'r') as img:
|
||||
base64_img = image_to_base64(img, 'PNG')
|
||||
primary, secondary = self.env['base.document.layout'].create(
|
||||
{})._parse_logo_colors(base64_img)
|
||||
primary, secondary = self.env['base.document.layout'].extract_image_primary_secondary_colors(base64_img)
|
||||
_img = frozendict({
|
||||
'img': base64_img,
|
||||
'colors': {
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
'website/static/src/xml/website.backend.xml',
|
||||
'website/static/src/xml/website_widget.xml',
|
||||
'website/static/src/xml/theme_preview.xml',
|
||||
'website/static/src/components/configurator/configurator.xml',
|
||||
],
|
||||
'website.test_bundle': [
|
||||
'/web/static/lib/qweb/qweb2.js',
|
||||
@@ -211,5 +212,15 @@
|
||||
'http://test.external.link/javascript2.js',
|
||||
'http://test.external.link/style2.css',
|
||||
],
|
||||
'website.website_configurator_assets_js': [
|
||||
'/web/static/lib/owl/owl.js',
|
||||
'/website/static/src/components/configurator/configurator.js',
|
||||
],
|
||||
'website.website_configurator_assets_scss': [
|
||||
('include', 'web.assets_frontend'),
|
||||
'/website/static/src/scss/configurator.scss',
|
||||
'/website/static/src/scss/website.theme_install.scss',
|
||||
'/website/static/src/scss/website.wysiwyg.scss'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +223,16 @@ class Website(Home):
|
||||
}
|
||||
return request.render('website.website_info', values)
|
||||
|
||||
@http.route(['/website/configurator/<int:step>'], type='http', auth="user", website=True)
|
||||
def website_configurator(self, step=1, **kwargs):
|
||||
if not request.env.user.has_group('website.group_website_designer'):
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
website_id = request.env['website'].get_current_website()
|
||||
if website_id.configurator_done is False:
|
||||
return request.render('website.website_configurator', {'lang': request.env.user.lang})
|
||||
else:
|
||||
return request.redirect('/')
|
||||
|
||||
@http.route(['/website/social/<string:social>'], type='http', auth="public", website=True, sitemap=False)
|
||||
def social(self, social, **kwargs):
|
||||
url = getattr(request.website, 'social_%s' % social, False)
|
||||
|
||||
@@ -22,7 +22,14 @@
|
||||
<field name="bundle">web.assets_frontend</field>
|
||||
<field name="glob">website/static/src/scss/compatibility/bs3_for_12_0.scss</field>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
</record>
|
||||
|
||||
<record id="website.configurator_tour" model="ir.asset">
|
||||
<field name="name">website.configurator_tour</field>
|
||||
<field name="bundle">website.assets_editor</field>
|
||||
<field name="glob">website/static/src/js/tours/configurator_tour.js</field>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,72 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- split between ir_ui_view & website_page record to keep external_id on ir_ui_view -->
|
||||
<record id="homepage" model="ir.ui.view">
|
||||
<field name="name">Home</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">website.homepage</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Home" priority="29" t-name="website.homepage">
|
||||
<t t-call="website.layout">
|
||||
<t t-set="pageName" t-value="'homepage'"/>
|
||||
<div id="wrap" class="oe_structure oe_empty"/>
|
||||
</t>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Group multi website -->
|
||||
<record id="group_multi_website" model="res.groups">
|
||||
<field name="name">Multi-website</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="contactus" model="ir.ui.view">
|
||||
<field name="name">Contact Us</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">website.contactus</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Contact Us" t-name="website.contactus">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<!-- Action Jump on website action -->
|
||||
<record id="action_website" model="ir.actions.act_url">
|
||||
<field name="name">Website</field>
|
||||
<field name="url">/</field>
|
||||
<field name="target">self</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Template for home and contactus -->
|
||||
<template id="website.homepage" name="Home">
|
||||
<t t-call="website.layout">
|
||||
<t t-set="pageName" t-value="'homepage'"/>
|
||||
<div id="wrap" class="oe_structure oe_empty"/>
|
||||
</t>
|
||||
</template>
|
||||
<template id="website.contactus" name="Contact Us">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<div class="oe_structure">
|
||||
<section class="s_title parallax s_parallax_is_fixed bg-black-50 pt24 pb24" data-vcss="001" data-snippet="s_title" data-scroll-background-ratio="1">
|
||||
<span class="s_parallax_bg oe_img_bg" style="background-image: url('/web/image/website.s_banner_default_image'); background-position: 50% 0;"/>
|
||||
<div class="o_we_bg_filter bg-black-50"/>
|
||||
<div class="container">
|
||||
<h1>Contact us</h1>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="container mt-4 mb-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-xl-6 mr-lg-auto">
|
||||
<div class="oe_structure">
|
||||
<section class="s_title parallax s_parallax_is_fixed bg-black-50 pt24 pb24" data-vcss="001" data-snippet="s_title" data-scroll-background-ratio="1">
|
||||
<span class="s_parallax_bg oe_img_bg" style="background-image: url('/web/image/website.s_banner_default_image'); background-position: 50% 0;"/>
|
||||
<div class="o_we_bg_filter bg-black-50"/>
|
||||
<section class="s_text_block" data-snippet="s_text_block">
|
||||
<div class="container">
|
||||
<h1>Contact us</h1>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<p>
|
||||
Contact us about anything related to our company or services.<br/>
|
||||
We'll do our best to get back to you as soon as possible.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="container mt-4 mb-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-xl-6 mr-lg-auto">
|
||||
<div class="oe_structure">
|
||||
<section class="s_text_block" data-snippet="s_text_block">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<p>
|
||||
Contact us about anything related to our company or services.<br/>
|
||||
We'll do our best to get back to you as soon as possible.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="text-center my-3" name="mail_button">
|
||||
<a role="button" t-attf-href="mailto:{{ res_company.email }}" class="btn btn-primary" id="o_contact_mail">Send us an email</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 mt-4 mt-lg-0">
|
||||
<t t-call="website.company_description"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center my-3" name="mail_button">
|
||||
<a role="button" t-attf-href="mailto:{{ res_company.email }}" class="btn btn-primary" id="o_contact_mail">Send us an email</a>
|
||||
</div>
|
||||
<div class="oe_structure mt-2"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
<div class="col-lg-4 mt-4 mt-lg-0">
|
||||
<t t-call="website.company_description"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_structure mt-2"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Template for cookie (optional) -->
|
||||
<template id="cookie_policy" name="Cookie Policy">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="oe_structure">
|
||||
@@ -193,7 +191,177 @@
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Template configurator (optional) -->
|
||||
<template id="website.aboutus" name="About Us">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<div class="oe_structure">
|
||||
<section class="s_title parallax s_parallax_is_fixed bg-black-50 pt24 pb24" data-vcss="001" data-snippet="s_title" data-scroll-background-ratio="1">
|
||||
<span class="s_parallax_bg oe_img_bg" style="background-image: url('/web/image/website.s_banner_default_image'); background-position: 50% 0;"/>
|
||||
<div class="o_we_bg_filter bg-black-50"/>
|
||||
<div class="container">
|
||||
<h1>About us</h1>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="website.our_services" name="Our Servicesw">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<div class="oe_structure">
|
||||
<section class="s_title parallax s_parallax_is_fixed bg-black-50 pt24 pb24" data-vcss="001" data-snippet="s_title" data-scroll-background-ratio="1">
|
||||
<span class="s_parallax_bg oe_img_bg" style="background-image: url('/web/image/website.s_banner_default_image'); background-position: 50% 0;"/>
|
||||
<div class="o_we_bg_filter bg-black-50"/>
|
||||
<div class="container">
|
||||
<h1>Our Services</h1>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="pricing" name="Pricing">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<div class="oe_structure">
|
||||
<section class="s_title parallax s_parallax_is_fixed bg-black-50 pt24 pb24" data-vcss="001" data-snippet="s_title" data-scroll-background-ratio="1">
|
||||
<span class="s_parallax_bg oe_img_bg" style="background-image: url('/web/image/website.s_banner_default_image'); background-position: 50% 0;"/>
|
||||
<div class="o_we_bg_filter bg-black-50"/>
|
||||
<div class="container">
|
||||
<h1>Pricing</h1>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="privacy_policy" name="Privacy Policy">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<div class="oe_structure">
|
||||
<section class="s_title parallax s_parallax_is_fixed bg-black-50 pt24 pb24" data-vcss="001" data-snippet="s_title" data-scroll-background-ratio="1">
|
||||
<span class="s_parallax_bg oe_img_bg" style="background-image: url('/web/image/website.s_banner_default_image'); background-position: 50% 0;"/>
|
||||
<div class="o_we_bg_filter bg-black-50"/>
|
||||
<div class="container">
|
||||
<h1>Privacy Policy</h1>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Configurator page features -->
|
||||
<record id="feature_page_about_us" model="website.configurator.feature">
|
||||
<field name="name">About Us</field>
|
||||
<field name="description">Info and stats about your company</field>
|
||||
<field name="iap_page_code">about_us</field>
|
||||
<field name="sequence">1</field>
|
||||
<field name="page_view_id" ref="aboutus"/>
|
||||
<field name="icon">fa-building</field>
|
||||
</record>
|
||||
<record id="feature_page_our_services" model="website.configurator.feature">
|
||||
<field name="name">Our Services</field>
|
||||
<field name="description">Description of your services offer</field>
|
||||
<field name="iap_page_code">our_services</field>
|
||||
<field name="sequence">3</field>
|
||||
<field name="page_view_id" ref="our_services"/>
|
||||
<field name="icon">fa-handshake-o</field>
|
||||
</record>
|
||||
<record id="feature_page_pricing" model="website.configurator.feature">
|
||||
<field name="name">Pricing</field>
|
||||
<field name="description">Design to drive conversion</field>
|
||||
<field name="iap_page_code">pricing</field>
|
||||
<field name="sequence">4</field>
|
||||
<field name="page_view_id" ref="pricing"/>
|
||||
<field name="icon">fa-random</field>
|
||||
</record>
|
||||
<record id="feature_page_privacy_policy" model="website.configurator.feature">
|
||||
<field name="name">Privacy Policy</field>
|
||||
<field name="description">Explain how you protect privacy</field>
|
||||
<field name="iap_page_code">privacy_policy</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="page_view_id" ref="privacy_policy"/>
|
||||
<field name="icon">fa-gavel</field>
|
||||
</record>
|
||||
|
||||
<!-- Configurator apps features -->
|
||||
<record id="feature_module_news" model="website.configurator.feature">
|
||||
<field name="name">News</field>
|
||||
<field name="description">Perfect for news and useful content</field>
|
||||
<field name="sequence">6</field>
|
||||
<field name="website_types_preselection">blog</field>
|
||||
<field name="module_id" ref="base.module_website_blog"/>
|
||||
<field name="icon">fa-rss</field>
|
||||
</record>
|
||||
<record id="feature_module_success_stories" model="website.configurator.feature">
|
||||
<field name="name">Success Stories</field>
|
||||
<field name="description">Share your best case studies</field>
|
||||
<field name="sequence">7</field>
|
||||
<field name="module_id" ref="base.module_website_blog"/>
|
||||
<field name="icon">fa-star</field>
|
||||
</record>
|
||||
<record id="feature_module_career" model="website.configurator.feature">
|
||||
<field name="name">Career</field>
|
||||
<field name="description">Publish jobs offer and let people apply</field>
|
||||
<field name="sequence">8</field>
|
||||
<field name="module_id" ref="base.module_website_hr_recruitment"/>
|
||||
<field name="icon">fa-address-card</field>
|
||||
</record>
|
||||
<record id="feature_module_shop" model="website.configurator.feature">
|
||||
<field name="name">Shop</field>
|
||||
<field name="description">Sell more with an eCommerce</field>
|
||||
<field name="sequence">9</field>
|
||||
<field name="website_types_preselection">online_store</field>
|
||||
<field name="module_id" ref="base.module_website_sale"/>
|
||||
<field name="icon">fa-shopping-cart</field>
|
||||
</record>
|
||||
<record id="feature_module_event" model="website.configurator.feature">
|
||||
<field name="name">Events</field>
|
||||
<field name="description">Publish on-site and online events</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="website_types_preselection">event</field>
|
||||
<field name="module_id" ref="base.module_website_event_sale"/>
|
||||
<field name="icon">fa-ticket</field>
|
||||
</record>
|
||||
<record id="feature_module_forum" model="website.configurator.feature">
|
||||
<field name="name">Forum</field>
|
||||
<field name="description">Give visitors the information they need</field>
|
||||
<field name="sequence">11</field>
|
||||
<field name="module_id" ref="base.module_website_forum"/>
|
||||
<field name="icon">fa-users</field>
|
||||
</record>
|
||||
<record id="feature_module_live_chat" model="website.configurator.feature">
|
||||
<field name="name">Live Chat</field>
|
||||
<field name="description">Chat with visitors to improve traction</field>
|
||||
<field name="sequence">12</field>
|
||||
<field name="module_id" ref="base.module_website_livechat"/>
|
||||
<field name="icon">fa-comments</field>
|
||||
</record>
|
||||
<record id="feature_module_elearning" model="website.configurator.feature">
|
||||
<field name="name">eLearning</field>
|
||||
<field name="description">Share videos & presentations, publicly or for a fee</field>
|
||||
<field name="sequence">13</field>
|
||||
<field name="website_types_preselection">elearning</field>
|
||||
<field name="module_id" ref="base.module_website_slides"/>
|
||||
<field name="icon">fa-graduation-cap</field>
|
||||
</record>
|
||||
<record id="feature_module_stores_locator" model="website.configurator.feature">
|
||||
<field name="name">Stores Locator</field>
|
||||
<field name="description">A map and a listing of your stores</field>
|
||||
<field name="sequence">15</field>
|
||||
<field name="module_id" ref="base.module_website_google_map"/>
|
||||
<field name="icon">fa-map-marker</field>
|
||||
</record>
|
||||
|
||||
<data noupdate="1">
|
||||
<!-- Default pagese -->
|
||||
<record id="homepage_page" model="website.page">
|
||||
<field name="is_published">True</field>
|
||||
<field name="url">/</field>
|
||||
@@ -207,6 +375,7 @@
|
||||
<field name="cache_key_expr">('cached' if not request.params else None,)</field>
|
||||
<field name="track">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Default Menu to store module menus for new website -->
|
||||
<record id="main_menu" model="website.menu">
|
||||
<field name="name">Default Main Menu</field>
|
||||
@@ -227,6 +396,7 @@
|
||||
<field name="sequence" type="int">60</field>
|
||||
</record>
|
||||
|
||||
<!-- Default Website -->
|
||||
<record id="default_website" model="website">
|
||||
<field name="name">My Website</field>
|
||||
<field name="domain"></field>
|
||||
@@ -235,22 +405,6 @@
|
||||
<!-- Correct homepage will be set during bootstraping -->
|
||||
</record>
|
||||
|
||||
<!-- Open website on install -->
|
||||
<record id="action_website" model="ir.actions.act_url">
|
||||
<field name="name">Website</field>
|
||||
<field name="url">/</field>
|
||||
<field name="target">self</field>
|
||||
</record>
|
||||
<record id="action_website_edit" model="ir.actions.act_url">
|
||||
<field name="name">Website Edit Mode</field>
|
||||
<field name="url">/?enable_editor=1</field>
|
||||
<field name="target">self</field>
|
||||
</record>
|
||||
<record id="base.open_menu" model="ir.actions.todo">
|
||||
<field name="action_id" ref="action_website_edit"/>
|
||||
<field name="state">open</field>
|
||||
</record>
|
||||
|
||||
<!-- Pre loaded images -->
|
||||
<record id="website.business_conference" model="ir.attachment">
|
||||
<field name="public" eval="True"/>
|
||||
@@ -714,10 +868,4 @@
|
||||
</record>
|
||||
|
||||
</data>
|
||||
<data>
|
||||
<record id="group_multi_website" model="res.groups">
|
||||
<field name="name">Multi-website</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -25,3 +25,4 @@ from . import res_lang
|
||||
from . import theme_models
|
||||
from . import website_visitor
|
||||
from . import website_snippet_filter
|
||||
from . import website_configurator_feature
|
||||
|
||||
@@ -60,18 +60,3 @@ class ServerAction(models.Model):
|
||||
"""
|
||||
res = super(ServerAction, self)._run_action_code_multi(eval_context)
|
||||
return eval_context.get('response', res)
|
||||
|
||||
|
||||
class IrActionsTodo(models.Model):
|
||||
_name = 'ir.actions.todo'
|
||||
_inherit = 'ir.actions.todo'
|
||||
|
||||
def action_launch(self):
|
||||
res = super().action_launch() # do ensure_one()
|
||||
|
||||
if self.id == self.env.ref('website.theme_install_todo').id:
|
||||
# Pick a theme consume all ir.actions.todo by default (due to lower sequence).
|
||||
# Once done, we re-enable the main ir.act.todo: open_menu
|
||||
self.env.ref('base.open_menu').action_open()
|
||||
|
||||
return res
|
||||
|
||||
@@ -22,19 +22,6 @@ class Company(models.Model):
|
||||
partner = self.sudo().partner_id
|
||||
return partner and partner.google_map_link(zoom) or None
|
||||
|
||||
def _compute_website_theme_onboarding_done(self):
|
||||
""" The step is marked as done if one theme is installed. """
|
||||
# we need the same domain as the existing action
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("website.theme_install_kanban_action")
|
||||
domain = literal_eval(action['domain'])
|
||||
domain.append(('state', '=', 'installed'))
|
||||
installed_themes_count = self.env['ir.module.module'].sudo().search_count(domain)
|
||||
for record in self:
|
||||
record.website_theme_onboarding_done = (installed_themes_count > 0)
|
||||
|
||||
website_theme_onboarding_done = fields.Boolean("Onboarding website theme step done",
|
||||
compute='_compute_website_theme_onboarding_done')
|
||||
|
||||
def _get_public_user(self):
|
||||
self.ensure_one()
|
||||
# We need sudo to be able to see public users from others companies too
|
||||
|
||||
@@ -5,6 +5,7 @@ import base64
|
||||
import inspect
|
||||
import logging
|
||||
import hashlib
|
||||
import requests
|
||||
import re
|
||||
|
||||
|
||||
@@ -16,11 +17,13 @@ from odoo import api, fields, models, tools
|
||||
from odoo.addons.http_routing.models.ir_http import slugify, _guess_mimetype, url_for
|
||||
from odoo.addons.website.models.ir_http import sitemap_qs2dom
|
||||
from odoo.addons.portal.controllers.portal import pager
|
||||
from odoo.addons.iap.tools import iap_tools
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.http import request
|
||||
from odoo.modules.module import get_resource_path
|
||||
from odoo.osv.expression import FALSE_DOMAIN
|
||||
from odoo.tools.translate import _
|
||||
from odoo.tools import pycompat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -35,6 +38,7 @@ DEFAULT_CDN_FILTERS = [
|
||||
"^/website/image/",
|
||||
]
|
||||
|
||||
DEFAULT_ENDPOINT = 'https://website.api.odoo.com'
|
||||
|
||||
class Website(models.Model):
|
||||
|
||||
@@ -63,6 +67,7 @@ class Website(models.Model):
|
||||
default_lang_id = fields.Many2one('res.lang', string="Default Language", default=_default_language, required=True)
|
||||
auto_redirect_lang = fields.Boolean('Autoredirect Language', default=True, help="Should users be redirected to their browser's language")
|
||||
cookies_bar = fields.Boolean('Cookies Bar', help="Display a customizable cookies bar on your website.")
|
||||
configurator_done = fields.Boolean(help='True if configurator has been completed or ignored')
|
||||
|
||||
def _default_social_facebook(self):
|
||||
return self.env.ref('base.main_company').social_facebook
|
||||
@@ -241,10 +246,138 @@ class Website(models.Model):
|
||||
attachments_to_unlink.unlink()
|
||||
return super(Website, self).unlink()
|
||||
|
||||
def create_and_redirect_to_theme(self):
|
||||
def create_and_redirect_configurator(self):
|
||||
self._force()
|
||||
action = self.env.ref('website.theme_install_kanban_action')
|
||||
return action.read()[0]
|
||||
action = self.env['ir.actions.actions']._for_xml_id('website.start_configurator_act_url')
|
||||
return action
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Configurator
|
||||
# ----------------------------------------------------------
|
||||
def _website_api_rpc(self, route, params):
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
website_api_endpoint = IrConfigParameter.get_param('website.website_api_endpoint', DEFAULT_ENDPOINT)
|
||||
endpoint = website_api_endpoint + route
|
||||
return iap_tools.iap_jsonrpc(endpoint, params=params)
|
||||
|
||||
@api.model
|
||||
def configurator_init(self):
|
||||
r = dict()
|
||||
company_id = self.get_current_website().company_id
|
||||
r['features'] = self.env['website.configurator.feature'].search_read([], ['name', 'description', 'type', 'icon', 'website_types_preselection'])
|
||||
r['logo'] = False
|
||||
if company_id.logo and company_id.logo != company_id._get_logo():
|
||||
r['logo'] = company_id.logo.decode('utf-8')
|
||||
r['industries'] = self._website_api_rpc('/api/website/1/configurator/industries', {'lang': self.env.user.lang})['industries']
|
||||
return r
|
||||
|
||||
@api.model
|
||||
def configurator_recommended_themes(self, industry_name, palette):
|
||||
domain = [('name', '=like', 'theme%'), ('name', 'not in', ['theme_default', 'theme_common'])]
|
||||
client_themes = request.env['ir.module.module'].search(domain).mapped('name')
|
||||
params = {
|
||||
'industry_name': industry_name,
|
||||
'palette': palette,
|
||||
'client_themes': client_themes,
|
||||
}
|
||||
return self._website_api_rpc('/api/website/1/configurator/recommended_themes', params)
|
||||
|
||||
@api.model
|
||||
def configurator_skip(self):
|
||||
self.configurator_done = True
|
||||
|
||||
@api.model
|
||||
def configurator_apply(self, **kwargs):
|
||||
def set_colors(selected_palette):
|
||||
url = '/website/static/src/scss/options/user_values.scss'
|
||||
custo = {'color-palettes-name': "'%s'" % selected_palette}
|
||||
self.env['web_editor.assets'].make_scss_customization(url, custo)
|
||||
|
||||
def set_features(selected_features):
|
||||
feature_ids = self.env['website.configurator.feature'].browse(selected_features)
|
||||
pages_views = {}
|
||||
modules = self.env['ir.module.module']
|
||||
for feature_id in feature_ids:
|
||||
if feature_id.type == 'app' and feature_id.module_id and feature_id.module_id.state != 'installed':
|
||||
modules += feature_id.module_id
|
||||
if feature_id.type == 'page' and feature_id.page_view_id:
|
||||
result = self.env['website'].new_page(name=feature_id.name, add_menu=True, template=feature_id.page_view_id.key)
|
||||
pages_views[feature_id.iap_page_code] = result['view_id']
|
||||
|
||||
modules._button_immediate_function(lambda self: self.button_install())
|
||||
return pages_views
|
||||
|
||||
def configure_page(page_code, snippet_list, pages_views):
|
||||
if page_code == 'homepage':
|
||||
page_view_id = website.homepage_id.view_id
|
||||
else:
|
||||
page_view_id = self.env['ir.ui.view'].browse(pages_views[page_code])
|
||||
rendered_snippets = []
|
||||
for snippet in snippet_list:
|
||||
try:
|
||||
view_id = self.env['website'].with_context(website_id=website.id).viewref(snippet)
|
||||
if view_id:
|
||||
rendered_snippet = pycompat.to_text(view_id._render())
|
||||
rendered_snippets.append(rendered_snippet)
|
||||
except ValueError as e:
|
||||
logger.warning(e)
|
||||
page_view_id.save(value=''.join(rendered_snippets), xpath="(//div[hasclass('oe_structure')])[last()]")
|
||||
|
||||
def set_images(images):
|
||||
for image in images:
|
||||
try:
|
||||
response = requests.get(image['url'], timeout=3)
|
||||
response.raise_for_status()
|
||||
self.env['ir.attachment'].create({
|
||||
'name': image['name'],
|
||||
'website_id': website.id,
|
||||
'key': image['name'],
|
||||
'type': 'binary',
|
||||
'raw': response.content,
|
||||
})
|
||||
except requests.HTTPError:
|
||||
logger.warning("Failed to download image '%s'", image['url'])
|
||||
|
||||
website = self.get_current_website()
|
||||
|
||||
url = self.env['ir.module.module'].search([('name', '=', kwargs['theme_name'])]).button_choose_theme()
|
||||
|
||||
website.configurator_done = True
|
||||
|
||||
# Enable tour
|
||||
tour_asset_id = self.env['ir.asset']._get_related_assets([('name', '=', 'website.configurator_tour')])
|
||||
tour_asset_id.sudo().write({'active': True})
|
||||
|
||||
# logo was generated as base64 url
|
||||
logo = kwargs.get('logo')
|
||||
if logo:
|
||||
website.logo = logo.split(',', 1)[1]
|
||||
|
||||
# palette
|
||||
palette = kwargs.get('selected_palette')
|
||||
if palette:
|
||||
set_colors(palette)
|
||||
|
||||
# modules
|
||||
pages_views = set_features(kwargs.get('selected_features'))
|
||||
|
||||
# Load suggestion from iap for selected pages
|
||||
params = {
|
||||
'theme': kwargs.get('theme_name'),
|
||||
'industry': kwargs.get('industry'),
|
||||
'pages': list(pages_views.keys()),
|
||||
}
|
||||
custom_resources = self._website_api_rpc('/api/website/1/configurator/custom_resources', params)
|
||||
|
||||
# Update pages
|
||||
pages = custom_resources.get('pages', {})
|
||||
for page_code, snippet_list in pages.items():
|
||||
configure_page(page_code, snippet_list, pages_views)
|
||||
|
||||
images = custom_resources.get('images', [])
|
||||
set_images(images)
|
||||
|
||||
return url
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Page Management
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class WebsiteConfiguratorFeature(models.Model):
|
||||
|
||||
_name = 'website.configurator.feature'
|
||||
_description = 'Website Configurator Feature'
|
||||
_order = 'sequence'
|
||||
|
||||
sequence = fields.Integer()
|
||||
name = fields.Char(translate=True)
|
||||
description = fields.Char(translate=True)
|
||||
icon = fields.Char()
|
||||
iap_page_code = fields.Char(help='Page code used to tell IAP website_service for which page a snippet list should be generated')
|
||||
website_types_preselection = fields.Char(help='Comma-separated list of website type for which this feature should be pre-selected')
|
||||
type = fields.Selection([('page', "Page"), ('app', "App")], compute='_compute_type')
|
||||
page_view_id = fields.Many2one('ir.ui.view', ondelete='cascade')
|
||||
module_id = fields.Many2one('ir.module.module', ondelete='cascade')
|
||||
|
||||
@api.depends('module_id', 'page_view_id')
|
||||
def _compute_type(self):
|
||||
for record in self:
|
||||
record.type = 'page' if record.page_view_id else 'app'
|
||||
|
||||
@api.constrains('module_id', 'page_view_id')
|
||||
def _check_module_xor_page_view(self):
|
||||
if bool(self.module_id) == bool(self.page_view_id):
|
||||
raise ValidationError(_("One and only one of the two fields 'page_view_id' and 'module_id' should be set"))
|
||||
@@ -24,3 +24,4 @@ access_theme_website_page,access_theme_website_page,model_theme_website_page,bas
|
||||
access_website_robots,access.website.robots,model_website_robots,website.group_website_designer,1,1,1,0
|
||||
access_website_dynamic_filter_public,access_website_dynamic_filter,model_website_snippet_filter,,0,0,0,0
|
||||
access_website_dynamic_filter_system,access_website_dynamic_filter,model_website_snippet_filter,base.group_system,1,1,1,1
|
||||
access_website_configurator_feature_designer,access_website_configurator_feature_designer,model_website_configurator_feature,website.group_website_designer,1,0,0,0
|
||||
|
||||
|
@@ -0,0 +1,515 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import rpc from 'web.rpc';
|
||||
import utils from 'web.utils';
|
||||
import weUtils from 'web_editor.utils';
|
||||
import session from 'web.session';
|
||||
import {_t, _lt} from 'web.core';
|
||||
|
||||
const {Component, Store, mount, QWeb} = owl;
|
||||
const {useDispatch, useStore, useGetters, useRef} = owl.hooks;
|
||||
const {Router, RouteComponent} = owl.router;
|
||||
const {whenReady} = owl.utils;
|
||||
|
||||
const MAX_PALETTES = 16;
|
||||
|
||||
const WEBSITE_TYPES = {
|
||||
1: {id: 1, label: _lt("a business website"), name: 'business'},
|
||||
2: {id: 2, label: _lt("an online store"), name: 'online_store'},
|
||||
3: {id: 3, label: _lt("a blog"), name: 'blog'},
|
||||
4: {id: 4, label: _lt("an event website"), name: 'event'},
|
||||
5: {id: 5, label: _lt("an elearning platform"), name: 'elearning'}
|
||||
};
|
||||
|
||||
const WEBSITE_PURPOSES = {
|
||||
1: {id: 1, label: _lt("get leads"), name: 'get_leads'},
|
||||
2: {id: 2, label: _lt("develop the brand"), name: 'develop_brand'},
|
||||
3: {id: 3, label: _lt("sell more"), name: 'sell_more'},
|
||||
4: {id: 4, label: _lt("inform customers"), name: 'inform_customers'}
|
||||
};
|
||||
|
||||
const hex2lab = (hex) => {
|
||||
let r = parseInt(hex.substring(1, 3), 16) / 255, g = parseInt(hex.substring(3, 5), 16) / 255, b = parseInt(hex.substring(5, 7), 16) / 255, x, y, z;
|
||||
[r, g, b] = [r, g, b].map((x) => (x > 0.04045) ? Math.pow((x + 0.055) / 1.055, 2.4) : x / 12.92);
|
||||
x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
|
||||
y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
|
||||
z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
|
||||
[x, y, z] = [x, y, z].map((x) => (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116);
|
||||
return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)];
|
||||
};
|
||||
|
||||
const deltaE = (hexA, hexB) => {
|
||||
let labA = hex2lab(hexA), labB = hex2lab(hexB);
|
||||
let deltaL = labA[0] - labB[0], deltaA = labA[1] - labB[1], deltaB = labA[2] - labB[2];
|
||||
let c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]), c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);
|
||||
let deltaC = c1 - c2;
|
||||
let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
|
||||
deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
|
||||
let sc = 1.0 + 0.045 * c1, sh = 1.0 + 0.015 * c1;
|
||||
let deltaLKlsl = deltaL / (1.0), deltaCkcsc = deltaC / (sc), deltaHkhsh = deltaH / (sh);
|
||||
let i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
|
||||
return i < 0 ? 0 : Math.sqrt(i);
|
||||
};
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Components
|
||||
//---------------------------------------------------------
|
||||
|
||||
class SkipButton extends Component {
|
||||
async skip() {
|
||||
await skipConfigurator();
|
||||
}
|
||||
}
|
||||
|
||||
SkipButton.template = 'website.Configurator.SkipButton';
|
||||
|
||||
class WelcomeScreen extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.dispatch = useDispatch();
|
||||
}
|
||||
|
||||
goToDescription() {
|
||||
this.env.router.navigate({to: 'CONFIGURATOR_DESCRIPTION_SCREEN'});
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(WelcomeScreen, {
|
||||
components: {SkipButton},
|
||||
template: 'website.Configurator.WelcomeScreen',
|
||||
});
|
||||
|
||||
class DescriptionScreen extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.industrySelection = useRef('industrySelection');
|
||||
this.state = useStore((state) => state);
|
||||
this.labelToName = {};
|
||||
this.getters = useGetters();
|
||||
this.dispatch = useDispatch();
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.dispatch('selectWebsitePurpose', undefined);
|
||||
this.dispatch('selectIndustry', undefined);
|
||||
$(this.industrySelection.el).autocomplete({
|
||||
appendTo: '.o_configurator_industry_wrapper',
|
||||
delay: 400,
|
||||
minLength: 1,
|
||||
source: this.autocompleteSearch.bind(this),
|
||||
select: this.selectIndustry.bind(this),
|
||||
classes: {
|
||||
'ui-autocomplete': 'custom-ui-autocomplete shadow-lg border-0 o_configurator_show_fast',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
autocompleteSearch(request, response) {
|
||||
const lcTerm = request.term.toLowerCase();
|
||||
const limit = 15;
|
||||
const matches = this.state.industries.filter((val) => {
|
||||
return val.label.startsWith(lcTerm);
|
||||
});
|
||||
let results = matches.slice(0, limit);
|
||||
this.labelToName = {};
|
||||
let labels = results.map((val) => val.label);
|
||||
if (labels.length < limit) {
|
||||
let relaxedMatches = this.state.industries.filter((val) => {
|
||||
return val.label.includes(lcTerm) && !labels.includes(val.label);
|
||||
});
|
||||
relaxedMatches = relaxedMatches.slice(0, limit - labels.length);
|
||||
results = results.concat(relaxedMatches);
|
||||
labels = results.map((val) => val.label);
|
||||
}
|
||||
results.forEach((r) => {
|
||||
this.labelToName[r.label] = r.name;
|
||||
});
|
||||
response(labels);
|
||||
}
|
||||
|
||||
selectIndustry(_, ui) {
|
||||
this.dispatch('selectIndustry', this.labelToName[ui.item.label]);
|
||||
this.checkDescriptionCompletion();
|
||||
}
|
||||
|
||||
blurIndustrySelection(ev) {
|
||||
const name = this.labelToName[ev.target.outerText];
|
||||
this.dispatch('selectIndustry', name);
|
||||
if (name === undefined) {
|
||||
this.industrySelection.el.textContent = '';
|
||||
} else {
|
||||
this.checkDescriptionCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
inputIndustrySelection(ev) {
|
||||
this.dispatch('selectIndustry', this.labelToName[ev.target.outerText]);
|
||||
}
|
||||
|
||||
selectWebsiteType(ev) {
|
||||
const {id} = ev.target.dataset;
|
||||
this.dispatch('selectWebsiteType', id);
|
||||
setTimeout(() => {
|
||||
this.industrySelection.el.focus();
|
||||
});
|
||||
this.checkDescriptionCompletion();
|
||||
}
|
||||
|
||||
selectWebsitePurpose(ev) {
|
||||
const {id} = ev.target.dataset;
|
||||
this.dispatch('selectWebsitePurpose', id);
|
||||
this.checkDescriptionCompletion();
|
||||
}
|
||||
|
||||
checkDescriptionCompletion() {
|
||||
const {selectedType, selectedPurpose, selectedIndustry} = this.state;
|
||||
if (selectedType && selectedPurpose && selectedIndustry) {
|
||||
this.env.router.navigate({to: 'CONFIGURATOR_PALETTE_SELECTION_SCREEN'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(DescriptionScreen, {
|
||||
components: {SkipButton},
|
||||
template: 'website.Configurator.DescriptionScreen',
|
||||
});
|
||||
|
||||
class PaletteSelectionScreen extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.state = useStore((state) => state);
|
||||
this.getters = useGetters();
|
||||
this.dispatch = useDispatch();
|
||||
this.logoInputRef = useRef('logoSelectionInput');
|
||||
}
|
||||
|
||||
mounted() {
|
||||
if (this.state.logo) {
|
||||
this.updatePalettes();
|
||||
}
|
||||
}
|
||||
|
||||
uploadLogo() {
|
||||
this.logoInputRef.el.click();
|
||||
}
|
||||
|
||||
async changeLogo() {
|
||||
const logoSelectInput = this.logoInputRef.el;
|
||||
if (logoSelectInput.files.length === 1) {
|
||||
const file = logoSelectInput.files[0];
|
||||
const data = await utils.getDataURLFromFile(file);
|
||||
this.dispatch('changeLogo', data);
|
||||
this.updatePalettes();
|
||||
}
|
||||
}
|
||||
|
||||
async updatePalettes() {
|
||||
let img = this.state.logo.split(',', 2)[1];
|
||||
const [color1, color2] = await rpc.query({
|
||||
model: 'base.document.layout',
|
||||
method: 'extract_image_primary_secondary_colors',
|
||||
args: [img]
|
||||
});
|
||||
this.dispatch('setRecommendedPalette', color1, color2);
|
||||
}
|
||||
|
||||
selectPalette(paletteId) {
|
||||
this.dispatch('selectPalette', paletteId);
|
||||
this.env.router.navigate({to: 'CONFIGURATOR_FEATURES_SELECTION_SCREEN'});
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(PaletteSelectionScreen, {
|
||||
components: {SkipButton},
|
||||
template: 'website.Configurator.PaletteSelectionScreen',
|
||||
});
|
||||
|
||||
class FeaturesSelectionScreen extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.state = useStore((state) => state);
|
||||
this.getters = useGetters();
|
||||
this.dispatch = useDispatch();
|
||||
}
|
||||
|
||||
async buildWebsite() {
|
||||
const industryName = this.state.selectedIndustry;
|
||||
if (!industryName) {
|
||||
this.env.router.navigate({to: 'CONFIGURATOR_DESCRIPTION_SCREEN'});
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
industry_name: industryName,
|
||||
palette: this.state.selectedPalette
|
||||
};
|
||||
const themes = await rpc.query({
|
||||
model: 'website',
|
||||
method: 'configurator_recommended_themes',
|
||||
kwargs: params,
|
||||
});
|
||||
|
||||
if (!themes.length) {
|
||||
await applyConfigurator(this, 'theme_default');
|
||||
} else {
|
||||
this.dispatch('updateRecommendedThemes', themes);
|
||||
this.env.router.navigate({to: 'CONFIGURATOR_THEME_SELECTION_SCREEN'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(FeaturesSelectionScreen, {
|
||||
components: {SkipButton},
|
||||
template: 'website.Configurator.FeatureSelection',
|
||||
});
|
||||
|
||||
class ThemeSelectionScreen extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.state = useStore((state) => state);
|
||||
this.getters = useGetters();
|
||||
this.themeSVGPreviews = [useRef('ThemePreview1'), useRef('ThemePreview2'), useRef('ThemePreview3')];
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.state.themes.forEach((theme, idx) => {
|
||||
$(this.themeSVGPreviews[idx].el).append(theme.svg);
|
||||
});
|
||||
}
|
||||
|
||||
async chooseTheme(themeName) {
|
||||
await applyConfigurator(this, themeName);
|
||||
}
|
||||
}
|
||||
|
||||
ThemeSelectionScreen.template = 'website.Configurator.ThemeSelectionScreen';
|
||||
|
||||
class App extends Component {}
|
||||
|
||||
Object.assign(App, {
|
||||
components: {RouteComponent},
|
||||
template: 'website.Configurator.App',
|
||||
});
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Routes
|
||||
//---------------------------------------------------------
|
||||
|
||||
const ROUTES = [
|
||||
{name: 'CONFIGURATOR_WELCOME_SCREEN', path: '/website/configurator/1', component: WelcomeScreen},
|
||||
{name: 'CONFIGURATOR_DESCRIPTION_SCREEN', path: '/website/configurator/2', component: DescriptionScreen},
|
||||
{name: 'CONFIGURATOR_PALETTE_SELECTION_SCREEN', path: '/website/configurator/3', component: PaletteSelectionScreen},
|
||||
{name: 'CONFIGURATOR_FEATURES_SELECTION_SCREEN', path: '/website/configurator/4', component: FeaturesSelectionScreen},
|
||||
{name: 'CONFIGURATOR_THEME_SELECTION_SCREEN', path: '/website/configurator/5', component: ThemeSelectionScreen},
|
||||
{name: 'CONFIGURATOR_WELCOME_SCREEN_LANG', path: '/{{lang}}/website/configurator/1', component: WelcomeScreen},
|
||||
];
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Store
|
||||
//---------------------------------------------------------
|
||||
|
||||
const getters = {
|
||||
getWebsiteTypes() {
|
||||
return Object.values(WEBSITE_TYPES);
|
||||
},
|
||||
|
||||
getSelectedType(_, id) {
|
||||
return id ? WEBSITE_TYPES[id] : undefined;
|
||||
},
|
||||
|
||||
getWebsitePurpose() {
|
||||
return Object.values(WEBSITE_PURPOSES);
|
||||
},
|
||||
|
||||
getSelectedPurpose(_, id) {
|
||||
return id ? WEBSITE_PURPOSES[id] : undefined;
|
||||
},
|
||||
|
||||
getFeatures({state}) {
|
||||
return Object.values(state.features);
|
||||
},
|
||||
|
||||
getPalettes({state}) {
|
||||
return Object.values(state.palettes);
|
||||
},
|
||||
|
||||
getThemeName({state}, idx) {
|
||||
return state.themes.length > idx ? state.themes[idx].name : undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const actions = {
|
||||
selectWebsiteType({state}, id) {
|
||||
Object.values(state.features).forEach((feature) => {
|
||||
feature.selected = feature.website_types_preselection.includes(WEBSITE_TYPES[id].name);
|
||||
});
|
||||
state.selectedType = id;
|
||||
},
|
||||
selectWebsitePurpose({state}, id) {
|
||||
state.selectedPurpose = id;
|
||||
},
|
||||
selectIndustry({state}, name) {
|
||||
state.selectedIndustry = name;
|
||||
},
|
||||
changeLogo({state}, data) {
|
||||
state.logo = data;
|
||||
},
|
||||
selectPalette({state}, paletteId) {
|
||||
state.selectedPalette = state.allPalettes[paletteId];
|
||||
},
|
||||
toggleFeature({state}, featureId) {
|
||||
const feature = state.features[featureId];
|
||||
const websiteType = WEBSITE_TYPES[state.selectedType];
|
||||
const forceFeatureActive = websiteType && feature.website_types_preselection.includes(websiteType.name);
|
||||
feature.selected = !feature.selected || forceFeatureActive;
|
||||
},
|
||||
setRecommendedPalette({state}, color1, color2) {
|
||||
let palettes = [];
|
||||
if (color1 && color2) {
|
||||
Object.values(state.allPalettes).forEach(palette => {
|
||||
const delta1 = deltaE(color1, palette.color1);
|
||||
const delta2 = deltaE(color2, palette.color2);
|
||||
palette.score = (delta1 + delta2) / 2;
|
||||
});
|
||||
palettes = Object.values(state.allPalettes).sort((a, b) => a.score - b.score);
|
||||
state.recommendedPalette = palettes[0];
|
||||
} else {
|
||||
palettes = Object.values(state.allPalettes);
|
||||
}
|
||||
const selectedPalettes = {};
|
||||
palettes.slice(1, MAX_PALETTES + 1).forEach((palette) => {
|
||||
selectedPalettes[palette.id] = palette;
|
||||
});
|
||||
state.palettes = selectedPalettes;
|
||||
},
|
||||
updateRecommendedThemes({state}, themes) {
|
||||
state.themes = themes.slice(0, 3);
|
||||
}
|
||||
};
|
||||
|
||||
async function getInitialState() {
|
||||
|
||||
// Load values from python and iap
|
||||
try {
|
||||
var results = await rpc.query({
|
||||
model: 'website',
|
||||
method: 'configurator_init',
|
||||
});
|
||||
} catch (_e) {
|
||||
return {};
|
||||
}
|
||||
const r = {
|
||||
features: {},
|
||||
industries: results.industries,
|
||||
logo: results.logo ? 'data:image/png;base64,' + results.logo : false,
|
||||
};
|
||||
results.features.forEach(feature => {
|
||||
r.features[feature.id] = Object.assign({}, feature, {selected: false});
|
||||
const wtp = r.features[feature.id].website_types_preselection;
|
||||
r.features[feature.id].website_types_preselection = wtp ? wtp.split(',') : [];
|
||||
});
|
||||
|
||||
// Load palettes from the current CSS
|
||||
const allPalettes = {}, palettes = {};
|
||||
const style = window.getComputedStyle(document.documentElement);
|
||||
const allPaletteNames = weUtils.getCSSVariableValue('palette-names', style).split(' ').map((name) => {
|
||||
return name.replace(/'/g, "");
|
||||
});
|
||||
allPaletteNames.forEach((paletteName) => {
|
||||
const palette = {
|
||||
id: paletteName
|
||||
};
|
||||
for (let j = 1; j <= 5; j += 1) {
|
||||
const color = weUtils.getCSSVariableValue(`o-palette-${paletteName}-o-color-${j}`, style);
|
||||
palette[`color${j}`] = color;
|
||||
}
|
||||
let duplicate = false;
|
||||
for (const validatedPalette of Object.values(allPalettes)) {
|
||||
if (validatedPalette.color1.toLowerCase() === palette.color1.toLowerCase() && validatedPalette.color2.toLowerCase() === palette.color2.toLowerCase()) {
|
||||
duplicate = true;
|
||||
}
|
||||
}
|
||||
if (!duplicate) {
|
||||
allPalettes[paletteName] = palette;
|
||||
if (Object.keys(palettes).length < MAX_PALETTES) {
|
||||
palettes[paletteName] = allPalettes[paletteName];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Object.assign(r, {
|
||||
selectedType: undefined,
|
||||
selectedPurpose: undefined,
|
||||
selectedIndustry: undefined,
|
||||
selectedPalette: undefined,
|
||||
recommendedPalette: undefined,
|
||||
palettes: palettes,
|
||||
allPalettes: allPalettes,
|
||||
themes: [],
|
||||
});
|
||||
}
|
||||
|
||||
async function skipConfigurator() {
|
||||
await rpc.query({
|
||||
model: 'website',
|
||||
method: 'configurator_skip',
|
||||
});
|
||||
window.location = '/web#action=website.theme_install_kanban_action';
|
||||
}
|
||||
|
||||
async function applyConfigurator(self, themeName) {
|
||||
if (!self.state.selectedIndustry) {
|
||||
self.env.router.navigate({to: 'CONFIGURATOR_DESCRIPTION_SCREEN'});
|
||||
return;
|
||||
}
|
||||
if (!self.state.selectedPalette) {
|
||||
self.env.router.navigate({to: 'CONFIGURATOR_PALETTE_SELECTION_SCREEN'});
|
||||
return;
|
||||
}
|
||||
if (themeName !== undefined) {
|
||||
$('body').append(self.env.loader);
|
||||
const selectedFeatures = Object.values(self.state.features).filter((feature) => feature.selected).map((feature) => feature.id);
|
||||
const data = {
|
||||
selected_features: selectedFeatures,
|
||||
logo: self.state.logo,
|
||||
industry: self.state.selectedIndustry,
|
||||
selected_palette: self.state.selectedPalette.id,
|
||||
theme_name: themeName,
|
||||
};
|
||||
const resp = await rpc.query({
|
||||
model: 'website',
|
||||
method: 'configurator_apply',
|
||||
kwargs: {...data},
|
||||
});
|
||||
window.location = resp.url;
|
||||
}
|
||||
}
|
||||
|
||||
async function makeEnvironment() {
|
||||
const env = {};
|
||||
const router = new Router(env, ROUTES);
|
||||
await router.start();
|
||||
const state = await getInitialState();
|
||||
const store = new Store({state, actions, getters});
|
||||
await session.is_bound;
|
||||
const qweb = new QWeb({translateFn: _t});
|
||||
const loaderTemplate = await owl.utils.loadFile('/website/static/src/xml/theme_preview.xml');
|
||||
const configuratorTemplates = await owl.utils.loadFile('/website/static/src/components/configurator/configurator.xml');
|
||||
qweb.addTemplates(loaderTemplate);
|
||||
qweb.addTemplates(configuratorTemplates);
|
||||
|
||||
env.loader = qweb.renderToString('website.ThemePreview.Loader', {
|
||||
showTips: true
|
||||
});
|
||||
return Object.assign(env, {router, store, qweb});
|
||||
}
|
||||
|
||||
async function setup() {
|
||||
const env = await makeEnvironment();
|
||||
if (!env.store.state.industries) {
|
||||
await skipConfigurator();
|
||||
} else {
|
||||
mount(App, {target: document.body, env});
|
||||
}
|
||||
}
|
||||
|
||||
whenReady(setup);
|
||||
@@ -0,0 +1,234 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="website.Configurator.SkipButton" owl="1">
|
||||
<div class="container-fluid py-2 pb-md-3 text-right pr-lg-5">
|
||||
<button class="btn btn-link" t-on-click="skip()">Skip and start from scratch</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="website.Configurator.WelcomeScreen" owl="1">
|
||||
<div class="o_configurator_screen h-100 d-flex flex-column o_welcome_screen">
|
||||
<div class="container-fluid pt-3 pb-2">
|
||||
<img class="ml-lg-5" style="height: 31px; width: 99px;" src="/website/static/src/img/odoo_logo.svg" title="Odoo Logo"/>
|
||||
</div>
|
||||
<div class="o_configurator_screen_content d-flex h-100">
|
||||
<div class="container align-self-center o_configurator_show">
|
||||
<div class="display-4 mb-2">Ready to build the<br class="d-none d-lg-inline"/>
|
||||
<b>perfect website?</b>
|
||||
</div>
|
||||
<div class="lead font-weight-normal mb-4 text-600">We'll set you up and running in <b>4 steps</b>
|
||||
</div>
|
||||
<button class="o_configurator_show btn btn-primary btn-lg px-4 py-2" t-on-click="goToDescription()">Let's do it</button>
|
||||
</div>
|
||||
</div>
|
||||
<SkipButton/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="website.Configurator.DescriptionScreen" owl="1">
|
||||
<div class="o_configurator_screen h-100 d-flex flex-column o_description_screen">
|
||||
<div class="container-fluid pt-3 pb-2">
|
||||
<img class="ml-lg-5" style="height: 31px; width: 99px;" src="/website/static/src/img/odoo_logo.svg" title="Odoo Logo"/>
|
||||
</div>
|
||||
<div class="o_configurator_screen_content d-flex h-100 flex-grow-1">
|
||||
<div class="container align-self-center">
|
||||
<div class="o_configurator_typing_text d-inline d-md-block mb-md-2 mb-lg-4 o_configurator_show">
|
||||
<span>I want </span>
|
||||
<div t-attf-class="dropdown o_configurator_type_dd d-inline-block {{state.selectedType ? 'o_step_completed' : 'o_step_todo show'}}">
|
||||
<div class="w-100 px-2" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<a class="d-flex align-items-center">
|
||||
<i class="text-primary" t-if="state.selectedType">
|
||||
<t t-esc="getters.getSelectedType(state.selectedType).label" />
|
||||
</i>
|
||||
<i class="fa fa-angle-down text-black-50 ml-auto pl-2" title="dropdown_angle_down" role="img"/>
|
||||
</a>
|
||||
</div>
|
||||
<div t-attf-class="dropdown-menu w-100 border-0 shadow-lg {{state.selectedType ? 'o_step_completed' : 'o_step_todo show'}}" role="menu">
|
||||
<t t-foreach="getters.getWebsiteTypes()" t-as="type" t-key="type.name">
|
||||
<a t-att-title="type.name" t-att-data-id="type.id" t-on-click="selectWebsiteType" class="dropdown-item o_change_website_type">
|
||||
<t t-esc="type.label"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<span t-att-class="!state.selectedType ? 'o_configurator_hide' : 'o_configurator_show'"> for my</span>
|
||||
</div>
|
||||
<div t-attf-class="o_configurator_typing_text d-inline d-md-block o_configurator_industry mb-md-2 mb-lg-4 {{!state.selectedType ? 'o_configurator_hide' : 'o_configurator_show'}}">
|
||||
<div class="o_configurator_industry_wrapper position-relative d-inline">
|
||||
<i t-attf-class="industry_selection d-inline d-md-inline-block rounded bg-100 px-2 px-md-3 mx-2 mx-md-0 {{state.selectedIndustry ? 'o_step_completed' : 'o_step_todo show'}}" contenteditable="True" t-on-blur="blurIndustrySelection" t-on-input="inputIndustrySelection" t-ref="industrySelection"/>
|
||||
</div>
|
||||
<span> business</span>
|
||||
<span t-att-class="!state.selectedIndustry ? 'o_configurator_hide' : 'o_configurator_show'">,</span>
|
||||
</div>
|
||||
<div t-attf-class="o_configurator_typing_text d-inline d-md-block mb-md-2 mb-lg-4 {{!state.selectedIndustry ? 'o_configurator_hide' : 'o_configurator_show'}}">
|
||||
<span>with the main objective to </span>
|
||||
<div t-attf-class="dropdown d-inline-block o_configurator_purpose_dd {{state.selectedPurpose ? 'o_step_completed' : 'o_step_todo'}}">
|
||||
<div class="w-100 px-2" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<a class="d-flex align-items-center">
|
||||
<t t-if="state.selectedPurpose">
|
||||
<t t-esc="getters.getSelectedPurpose(state.selectedPurpose).label" />
|
||||
</t>
|
||||
<i class="fa fa-angle-down text-black-50 ml-auto pl-2" title="dropdown_angle_down" role="img"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown-menu w-100 border-0 shadow-lg" role="menu">
|
||||
<t t-foreach="getters.getWebsitePurpose()" t-as="type" t-key="type.name">
|
||||
<a t-att-title="type.name" t-att-data-id="type.id" t-on-click="selectWebsitePurpose" class="dropdown-item o_change_website_purpose">
|
||||
<t t-esc="type.label"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SkipButton/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="website.Configurator.PaletteSelectionScreen" owl="1">
|
||||
<div class="o_configurator_screen h-100 d-flex flex-column o_palette_selection_screen">
|
||||
<div class="container-fluid pt-3 pb-2">
|
||||
<img class="ml-lg-5" style="height: 31px; width: 99px;" src="/website/static/src/img/odoo_logo.svg" title="Odoo Logo"/>
|
||||
</div>
|
||||
<div class="o_configurator_screen_content container palette_selection d-flex flex-grow-1 h-100">
|
||||
<div class="d-flex flex-column flex-lg-row w-100 h-100 h-lg-auto align-self-md-center o_configurator_show">
|
||||
<div class="w-100 w-lg-25 order-lg-3 my-4 my-md-0 d-flex flex-column">
|
||||
<div class="h4 text-center">
|
||||
<b>Detect</b> from Logo</div>
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<a href="#" t-on-click="uploadLogo" t-attf-class="o_configurator_logo_upload position-relative btn-link rounded bg-100 overflow-hidden d-flex flex-grow-1 justify-content-center align-items-center text-decoration-none {{state.logo? 'h-50' : ''}}">
|
||||
<input type="file" class="logo_selection_input" t-on-change="changeLogo" style="display:none" name="logo_selection" t-ref="logoSelectionInput" accept="image/*"/>
|
||||
<div class="o_configurator_logo_button text-center">
|
||||
<i t-attf-class="fa fa-cloud-upload {{state.logo? 'fa-4x' : 'fa-6x'}}"></i>
|
||||
<div class="text-center">Upload <span t-if="state.logo">a new image</span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="state.logo" class="o_configurator_logo_wrapper position-absolute d-flex justify-content-center align-items-center w-100 h-100">
|
||||
<img style="height: 120px" t-attf-src="{{state.logo}}"/>
|
||||
</div>
|
||||
</a>
|
||||
<div t-if="state.recommendedPalette" class="w-75 mx-auto px-2 pt-3" style="max-width: 184px;">
|
||||
<h6 class="text-center text-success d-block badge mb-0 mt-n2">Detected Colors</h6>
|
||||
<div t-attf-class="palette_card rounded-pill overflow-hidden d-flex" t-on-click="selectPalette(state.recommendedPalette.id)" t-attf-style="background-color: {{state.recommendedPalette.color3}}">
|
||||
<div class="color_sample w-100 first" t-attf-style="background-color: {{state.recommendedPalette.color1}}"/>
|
||||
<div class="color_sample w-100 second" t-attf-style="background-color: {{state.recommendedPalette.color2}}"/>
|
||||
<div class="color_sample w-100 third" t-attf-style="background-color: {{state.recommendedPalette.color3}}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="position-relative d-flex justify-content-center order-lg-2 w-100 w-lg-0 py-3 py-lg-0 px-lg-5 mb-4 mb-lg-0">
|
||||
<div class="border-top w-100"></div>
|
||||
<b class="palette_selection_or bg-white text-muted w-lg-100 text-center px-3 py-lg-3">OR</b>
|
||||
<div class="border-left d-none d-lg-inline h-100 mx-auto w-0"></div>
|
||||
</div>
|
||||
<div class="w-100 w-lg-auto flex-grow-1 o_configurator_show_fast">
|
||||
<div class="h4 text-center">
|
||||
<b>Choose</b> Your Brand Color</div>
|
||||
<div class="d-flex flex-wrap align-items-end">
|
||||
<t t-foreach="getters.getPalettes()" t-as="palette" t-key="palette_index">
|
||||
<div class="w-50 w-md-25 px-2 pt-3">
|
||||
<div t-attf-class="palette_card rounded-pill overflow-hidden d-flex" t-on-click="selectPalette(palette.id)" t-attf-style="background-color: {{palette.color3}}">
|
||||
<div class="color_sample w-100 first" t-attf-style="background-color: {{palette.color1}}"/>
|
||||
<div class="color_sample w-100 second" t-attf-style="background-color: {{palette.color2}}"/>
|
||||
<div class="color_sample w-100 third" t-attf-style="background-color: {{palette.color3}}"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SkipButton/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="website.Configurator.FeatureSelection" owl="1">
|
||||
<div class="o_configurator_screen h-100 d-flex flex-column o_feature_selection_screen">
|
||||
<div class="container-fluid pt-3 pb-2">
|
||||
<img class="ml-lg-5" style="height: 31px; width: 99px;" src="/website/static/src/img/odoo_logo.svg" title="Odoo Logo"/>
|
||||
</div>
|
||||
<div class="o_configurator_screen_content container d-flex flex-grow-1 align-items-center">
|
||||
<div class="m-auto">
|
||||
<div class="o_configurator_typing_text o_configurator_show_fast p-2">Add <b class="text-info">Pages</b> or <b class="text-warning">Features</b></div>
|
||||
<div class="page_feature_selection o_configurator_show">
|
||||
<div class="w-100 page_feature_selection container d-flex flex-wrap py2 py-lg-3">
|
||||
<t t-foreach="getters.getFeatures()" t-as="feature" t-key="feature_index">
|
||||
<div class="p-2 w-100 w-md-50 w-lg-25" t-if="feature.type != 'empty'">
|
||||
<div t-attf-class="card h-100 {{feature.selected ? 'border-success' : ''}}" t-on-click="dispatch('toggleFeature', feature.id)">
|
||||
<div class="card-body py-2">
|
||||
<i t-attf-class="o_configurator_feature_status fa {{feature.selected ? 'fa-check-circle text-success' : 'fa-circle-o text-300'}}" />
|
||||
<h5 class="card-title d-flex align-items-center">
|
||||
<i t-attf-class="mr-2 small fa {{feature.icon}} {{feature.type == 'page' ? 'text-info' : 'text-warning' }}"/>
|
||||
<t t-esc="feature.name"/>
|
||||
</h5>
|
||||
<p class="card-text small text-muted" t-esc="feature.description"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-primary btn-lg ml-3" t-on-click="buildWebsite()">Build my website</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SkipButton/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="website.Configurator.ThemeSelectionScreen" owl="1">
|
||||
<div class="o_configurator_screen h-100 d-flex flex-column o_theme_selection_screen">
|
||||
<div class="container-fluid pt-3 pb-2">
|
||||
<img class="ml-lg-5" style="height: 31px; width: 99px;" src="/website/static/src/img/odoo_logo.svg" title="Odoo Logo"/>
|
||||
</div>
|
||||
<div class="o_configurator_screen_content d-flex flex-column flex-grow-1 align-items-center">
|
||||
<div class="m-auto w-100 w-md-75 w-xl-100">
|
||||
<div class="o_configurator_typing_text text-center mt-4 mb-lg-4">Choose your favorite <b>Theme</b>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row pb-4 pt-5">
|
||||
<div class="col-12 col-lg-4 d-flex align-items-end mb-4 mb-lg-0">
|
||||
<t t-if="getters.getThemeName(1)">
|
||||
<div class="theme_preview border rounded position-relative w-100 small o_configurator_show_fast">
|
||||
<h6 class="theme_preview_tip text-center text-muted">Click to select</h6>
|
||||
<div class="theme_svg_container rounded overflow-hidden" t-ref="ThemePreview2"/>
|
||||
<div class="button_area" t-on-click="chooseTheme(getters.getThemeName(1))"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 d-flex align-items-end mb-4 mb-lg-0">
|
||||
<t t-if="getters.getThemeName(0)">
|
||||
<div class="theme_preview border rounded position-relative w-100 o_configurator_show">
|
||||
<h6 class="theme_preview_tip text-center text-muted">Click to select</h6>
|
||||
<div class="theme_svg_container rounded overflow-hidden" t-ref="ThemePreview1"/>
|
||||
<div class="button_area" t-on-click="chooseTheme(getters.getThemeName(0))"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 d-flex align-items-end">
|
||||
<t t-if="getters.getThemeName(2)">
|
||||
<div class="theme_preview border rounded position-relative w-100 small o_configurator_show_fast">
|
||||
<h6 class="theme_preview_tip text-center text-muted">Click to select</h6>
|
||||
<div class="theme_svg_container rounded overflow-hidden" t-ref="ThemePreview3"/>
|
||||
<div class="button_area" t-on-click="chooseTheme(getters.getThemeName(2))"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="website.Configurator.App">
|
||||
<div class="o_configurator_container">
|
||||
<RouteComponent />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="621px" height="196px" viewBox="0 0 621 196" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Odoo Logo</title>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="odoo_logo" fill-rule="nonzero">
|
||||
<path d="M546,196 C504.579,196 471,162.421 471,121 C471,79.579 504.579,46 546,46 C587.421,46 621,79.579 621,121 C621,162.421 587.421,196 546,196 Z M546,165 C570.3,165 590,145.3 590,121 C590,96.7 570.3,77 546,77 C521.7,77 502,96.7 502,121 C502,145.3 521.7,165 546,165 Z M389,196 C347.579,196 314,162.421 314,121 C314,79.579 347.579,46 389,46 C430.421,46 464,79.579 464,121 C464,162.421 430.421,196 389,196 Z M389,165 C413.3,165 433,145.3 433,121 C433,96.7 413.3,77 389,77 C364.7,77 345,96.7 345,121 C345,145.3 364.7,165 389,165 Z M307,120 C307,161.935 273.408,196 231.991,196 C190.575,196 157,162.005 157,120.07 C157,78.134 187.5,46 231.991,46 C248.433,46 263.638,49.496 275.998,58.58 L276,15.09 C276,6.756 283.27,0 291.5,0 C299.728,0 307,6.762 307,15.09 L307,120 Z M232,165 C256.3,165 276,145.3 276,121 C276,96.7 256.3,77 232,77 C207.7,77 188,96.7 188,121 C188,145.3 207.7,165 232,165 Z" fill="#8F8F8F"></path>
|
||||
<path d="M75,196 C33.579,196 0,162.421 0,121 C0,79.579 33.579,46 75,46 C116.421,46 150,79.579 150,121 C150,162.421 116.421,196 75,196 Z M75,165 C99.3,165 119,145.3 119,121 C119,96.7 99.3,77 75,77 C50.7,77 31,96.7 31,121 C31,145.3 50.7,165 75,165 Z" fill="#875A7B"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,48 @@
|
||||
odoo.define("website.configurator.tour", function (require) {
|
||||
"use strict";
|
||||
|
||||
const wTourUtils = require("website.tour_utils");
|
||||
const core = require("web.core");
|
||||
const _t = core._t;
|
||||
|
||||
let titleSelector = '#wrap > section:first-child';
|
||||
let title = $(titleSelector).find('h1, h2').first();
|
||||
if (!title.length) {
|
||||
titleSelector = titleSelector.replace('section:first-child', 'section:nth-child(2)');
|
||||
title = $(titleSelector).find('h1, h2').first();
|
||||
}
|
||||
let isTitleTextImage = $(titleSelector).hasClass('s_text_image');
|
||||
titleSelector = titleSelector.concat(` ${title.is('h1') ? 'h1' : 'h2'}`);
|
||||
|
||||
const shapeSelector = '#wrap > section[data-oe-shape-data]';
|
||||
const backgroundSelector = '#wrap > section:nth-child(2)';
|
||||
|
||||
const imageStep = isTitleTextImage ?
|
||||
wTourUtils.changeImage(titleSelector.replace('h2', 'img')) : wTourUtils.changeBackground();
|
||||
|
||||
const backgroundColorStep = [wTourUtils.changeBackgroundColor()];
|
||||
if (!isTitleTextImage) {
|
||||
backgroundColorStep.unshift(wTourUtils.clickOnSnippet(backgroundSelector));
|
||||
}
|
||||
|
||||
const shapeStep = [];
|
||||
if ($(shapeSelector).first().length) {
|
||||
if (!$(backgroundSelector).is($(shapeSelector))) {
|
||||
shapeStep.push(wTourUtils.clickOnSnippet(shapeSelector));
|
||||
}
|
||||
shapeStep.push(wTourUtils.changeOption('BackgroundShape', 'we-toggler', _t('Background Shape')));
|
||||
shapeStep.push(wTourUtils.selectNested('we-select-page', 'BackgroundShape', ':not(.o_we_pager_controls', _t('Background Shape')));
|
||||
}
|
||||
|
||||
const steps = [
|
||||
wTourUtils.clickOnText(titleSelector),
|
||||
imageStep,
|
||||
...backgroundColorStep,
|
||||
...shapeStep,
|
||||
wTourUtils.changePaddingSize('top'),
|
||||
wTourUtils.clickOnSave(),
|
||||
];
|
||||
|
||||
wTourUtils.registerThemeHomepageTour('configurator_tour', steps);
|
||||
|
||||
});
|
||||
@@ -48,7 +48,7 @@ function addMedia(position = "right") {
|
||||
|
||||
function changeBackground(snippet, position = "bottom") {
|
||||
return {
|
||||
trigger: ".o_we_customize_panel .o_we_edit_image",
|
||||
trigger: ".o_we_customize_panel .o_we_bg_success",
|
||||
content: _t("<b>Customize</b> any block through this menu. Try to change the background image of this block."),
|
||||
position: position,
|
||||
run: "click",
|
||||
@@ -94,7 +94,7 @@ function changeIcon(snippet, index = 0, position = "bottom") {
|
||||
|
||||
function changeImage(snippet, position = "bottom") {
|
||||
return {
|
||||
trigger: `#wrapwrap .${snippet.id} img`,
|
||||
trigger: snippet.id ? `#wrapwrap .${snippet.id} img` : snippet,
|
||||
content: _t("<b>Double click on an image</b> to change it with one of your choice."),
|
||||
position: position,
|
||||
run: "dblclick",
|
||||
@@ -160,7 +160,7 @@ function clickOnEdit(position = "bottom") {
|
||||
*/
|
||||
function clickOnSnippet(snippet, position = "bottom") {
|
||||
return {
|
||||
trigger: `#wrapwrap .${snippet.id}`,
|
||||
trigger: snippet.id ? `#wrapwrap .${snippet.id}` : snippet,
|
||||
content: _t("<b>Click on a snippet</b> to access its options menu."),
|
||||
position: position,
|
||||
run: "click",
|
||||
@@ -184,7 +184,7 @@ function clickOnSave(position = "bottom") {
|
||||
*/
|
||||
function clickOnText(snippet, element, position = "bottom") {
|
||||
return {
|
||||
trigger: `#wrapwrap .${snippet.id} ${element}`,
|
||||
trigger: snippet.id ? `#wrapwrap .${snippet.id} ${element}` : snippet,
|
||||
content: _t("<b>Click on a text</b> to start editing it."),
|
||||
position: position,
|
||||
run: "text",
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
.o_configurator_container {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
|
||||
.o_configurator_screen {
|
||||
|
||||
// ==== Animations
|
||||
@keyframes configuratorFadeIn{
|
||||
from {opacity: 0}
|
||||
to {opacity: 1}
|
||||
}
|
||||
|
||||
@keyframes theme_screenshot_scroll {
|
||||
to { transform: translate3d(0, -24%, 0)}
|
||||
}
|
||||
|
||||
@keyframes theme_screenshot_scroll_small {
|
||||
to { transform: translate3d(0, -31.5%, 0)}
|
||||
}
|
||||
|
||||
// ==== General Components
|
||||
%o_configurator_titles_weight {
|
||||
font-weight: 300;
|
||||
|
||||
b {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.o_configurator_typing_text {
|
||||
font-size: 28px;
|
||||
@extend %o_configurator_titles_weight;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@for $i from 1 through 6 {
|
||||
.h#{$i} {
|
||||
@extend %o_configurator_titles_weight;
|
||||
}
|
||||
}
|
||||
|
||||
.o_configurator_hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.o_configurator_show {
|
||||
animation: configuratorFadeIn 1s;
|
||||
}
|
||||
|
||||
.o_configurator_show_fast {
|
||||
animation: configuratorFadeIn .35s;
|
||||
}
|
||||
|
||||
.o_configurator_screen_content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.o_configurator_logo_wrapper {
|
||||
@extend %o-preview-alpha-background;
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
// ==== Screens
|
||||
|
||||
&.o_description_screen {
|
||||
.dropdown, .industry_selection, .custom-ui-autocomplete {
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.o_configurator_type_dd.o_step_todo {
|
||||
min-width: 13ch;
|
||||
}
|
||||
|
||||
.industry_selection.o_step_todo {
|
||||
min-width: 17ch;
|
||||
}
|
||||
|
||||
.o_configurator_purpose_dd.o_step_todo {
|
||||
min-width: 12ch;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
box-shadow: inset 0 -3px 0 theme-color(primary);
|
||||
|
||||
&.o_step_todo .fa-angle-down {
|
||||
transform: translateY(-0.26em);
|
||||
}
|
||||
|
||||
&.o_step_completed {
|
||||
transition: all 300ms ease;
|
||||
|
||||
.fa-angle-down {
|
||||
@include o-position-absolute($right: 0);
|
||||
opacity:0;
|
||||
}
|
||||
|
||||
&:hover, &.show {
|
||||
padding-right: 1.3rem!important;
|
||||
|
||||
.fa-angle-down {
|
||||
transition: all 300ms ease 100ms;
|
||||
opacity:1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu, .custom-ui-autocomplete {
|
||||
@include border-bottom-radius($border-radius);
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.dropdown-item, .fa-angle-down, .ui-menu-item a, .ui-menu-item a.ui-state-active {
|
||||
font-size: .65em;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.industry_selection {
|
||||
border: 1px solid $border-color;
|
||||
color: theme-color(primary);
|
||||
|
||||
&:focus {
|
||||
border-color: theme-color(primary);
|
||||
color: inherit;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-ui-autocomplete {
|
||||
max-height: 300px; // <- Fallback
|
||||
max-height: 35vh;
|
||||
|
||||
.ui-menu-item {
|
||||
list-style: none;
|
||||
|
||||
a {
|
||||
padding: $dropdown-item-padding-y*2 $dropdown-item-padding-x;
|
||||
font-family: $font-family-base;
|
||||
text-decoration: none;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item, .custom-ui-autocomplete .ui-menu-item a {
|
||||
&:hover, &:active, &.ui-state-active {
|
||||
background-color: $dropdown-link-hover-bg;
|
||||
color: $dropdown-link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.o_palette_selection_screen {
|
||||
.palette_card {
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 0 1px gray('200');
|
||||
|
||||
.color_sample {
|
||||
padding-top: 30%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px #FFF, 0 0 0 3px theme-color("primary")
|
||||
}
|
||||
}
|
||||
|
||||
.o_configurator_logo_wrapper {
|
||||
transition: opacity .1s ease;
|
||||
|
||||
&:hover {
|
||||
transition: opacity .3s ease;
|
||||
opacity: .1;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 120px;
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.o_configurator_logo_upload {
|
||||
@include o-hover-text-color(gray('400'), gray('800'));
|
||||
}
|
||||
|
||||
.palette_selection_or {
|
||||
@include o-position-absolute(50%);
|
||||
transform: translateY(-50%);
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.o_feature_selection_screen {
|
||||
.card {
|
||||
cursor: pointer;
|
||||
}
|
||||
.o_configurator_feature_status {
|
||||
@include o-position-absolute($card-spacer-y * 0.5 , $card-spacer-x * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&.o_theme_selection_screen{
|
||||
.button_area, .theme_svg_container {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.theme_preview_tip {
|
||||
@include o-position-absolute(0, 0, auto, 0);
|
||||
}
|
||||
|
||||
.theme_preview, .theme_svg_container svg, .theme_preview_tip {
|
||||
transform: translate3d(0,0,0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.theme_preview, .theme_preview_tip {
|
||||
transition: all .3s ease 0s;
|
||||
}
|
||||
|
||||
.theme_svg_container svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.button_area {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theme_preview {
|
||||
padding-top: 65%;
|
||||
box-shadow: $box-shadow-sm;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: 200%;
|
||||
|
||||
&:hover {
|
||||
transform: translate3d(0,-10px,0);
|
||||
}
|
||||
|
||||
&.small {
|
||||
padding-top: 180%;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: theme-color(primary)!important;
|
||||
box-shadow: $box-shadow;
|
||||
|
||||
.theme_preview_tip {
|
||||
transform: translate3d(0,-120%,0);
|
||||
}
|
||||
|
||||
.theme_svg_container svg {
|
||||
animation: theme_screenshot_scroll 4s cubic-bezier(0.455, 0.030, 0.515, 0.955) .1s infinite alternate;
|
||||
}
|
||||
|
||||
&.small .theme_svg_container svg {
|
||||
animation-name: theme_screenshot_scroll_small;
|
||||
animation-duration: 5s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2418,4 +2418,18 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template id="website_configurator" name="Website Configurator" inherit_id="web.frontend_layout" primary="True">
|
||||
<xpath expr="//html" position="attributes">
|
||||
<attribute name="lang"><t t-esc='lang'/></attribute>
|
||||
</xpath>
|
||||
<xpath expr="//head/t[@t-call-assets][last()]" position="after">
|
||||
<t t-call-assets="website.website_configurator_assets_js" lazy_load="True"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-call-assets='web.assets_frontend']" position="replace">
|
||||
<t t-call-assets="website.website_configurator_assets_scss" t-js="false"/>
|
||||
</xpath>
|
||||
<xpath expr="//nav" position="replace"/>
|
||||
<xpath expr="//div[@id='wrapwrap']" position="replace"/>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -65,11 +65,12 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="inside">
|
||||
<footer>
|
||||
<button name="create_and_redirect_to_theme" type="object" string="Pick a Theme" class="btn btn-primary" icon="fa-paint-brush"/>
|
||||
<button name="create_and_redirect_configurator" type="object" string="Create" class="btn btn-primary"/>
|
||||
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</xpath>
|
||||
<xpath expr="//notebook" position="replace"/>
|
||||
<xpath expr="//div[@name='logo']" position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -519,31 +520,6 @@
|
||||
<field name="domain" model="ir.module.module" eval="obj().get_themes_domain()"/>
|
||||
</record>
|
||||
|
||||
<record id="theme_install_act_url" model="ir.actions.act_url">
|
||||
<field name="name">Website</field>
|
||||
<field name="url">/web?reload#action=website.theme_install_kanban_action</field>
|
||||
<field name="target">self</field>
|
||||
</record>
|
||||
|
||||
<record id="theme_install_todo" model="ir.actions.todo">
|
||||
<field name="name">Go to theme</field>
|
||||
<field name="action_id" ref="theme_install_act_url"/>
|
||||
<field name="sequence">0</field>
|
||||
</record>
|
||||
|
||||
<!-- ====== Onboarding =================================================
|
||||
==================================================================== -->
|
||||
<template id="onboarding_website_theme_step">
|
||||
<t t-call="base.onboarding_step">
|
||||
<t t-set="title">Website Theme</t>
|
||||
<t t-set="description">Select a theme for your website.</t>
|
||||
<t t-set="btn_text">Let's start!</t>
|
||||
<t t-set="method" t-value="'action_open_website_theme_selector'" />
|
||||
<t t-set="model" t-value="'res.company'" />
|
||||
<t t-set="done" t-value="company.website_theme_onboarding_done" />
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- ====== Menu Items =================================================
|
||||
==================================================================== -->
|
||||
<menuitem name="Website"
|
||||
@@ -570,5 +546,17 @@
|
||||
sequence="20" name="Analytics"
|
||||
action="website.ir_actions_server_website_google_analytics"/>
|
||||
|
||||
<!-- Configurator actions -->
|
||||
<record id="start_configurator_act_url" model="ir.actions.act_url">
|
||||
<field name="name">Website Configurator</field>
|
||||
<field name="url">/website/configurator/1</field>
|
||||
<field name="target">self</field>
|
||||
</record>
|
||||
|
||||
<record id="website_configurator_todo" model="ir.actions.todo">
|
||||
<field name="name">Start Website Configurator</field>
|
||||
<field name="action_id" ref="start_configurator_act_url"/>
|
||||
<field name="sequence">0</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user