[IMP] website: polish the configurator and save info in IAP database

The first objective of this commit is to polish the website
configurator:
1) On page 1 of it: - The size of the text is increased.
2) On page 2 of it: - The font size inside dropdown is reduced.
                    - 30 results are displayed inside dropdown instead
of 15.
3) On page 3 of it: - The size of the text is increased.
3) On page 4: - The cards are refactored.
              - The end line is removed.
              - The "Build my website" button is moved to the left.
4) On page 5 : - It is now impossible for the user to click on a theme
preview before it is completely loaded.

5) On all pages, the size of the "Skip and start from scratch" button
has been increased.

On the second page of the website configurator, the user has to choose
an industry type. Sometimes, it happens that the user is searching for
an industry that is not known by the server. Before this commit, a
message of type "No result found" appeared and the user had to choose
an other industry among the existing ones. From this commit, the
"No result found" message is replaced by the name of the industry that
the user is writing. To choose its industry type, the user can click on
the displayed industry name or outside of the input area. This last
option calls the `_blurIndustrySelection` function and the written
industry is kept as the final industry choice. If the user chooses an
industry name that is not known by the server then the remaining of the
website configuration is done with the "abbey" industry. Moreover, this
unknown industry name as well as the language used by the user is sent
and stored to the IAP database. The goal of it is to regularly update
the industry list so that it becomes as most complete as possible.

IAP PR : https://github.com/odoo/iap-apps/pull/549
task-3062171

closes odoo/odoo#110187

Signed-off-by: Romain Derie (rde) <rde@odoo.com>
This commit is contained in:
Louis (loco)
2023-02-20 12:39:24 +01:00
committed by Romain Derie
parent 4551be92c6
commit e82a1cb2ef
4 changed files with 79 additions and 79 deletions
+10
View File
@@ -386,6 +386,16 @@ class Website(models.Model):
website = self.get_current_website()
website.configurator_done = True
@api.model
def configurator_missing_industry(self, unknown_industry):
self._website_api_rpc(
'/api/website/unknown_industry',
{
'unknown_industry': unknown_industry,
'lang': self.env.context.get('lang'),
}
)
@api.model
def configurator_apply(self, **kwargs):
def set_colors(selected_palette):
@@ -4,7 +4,7 @@ import concurrency from 'web.concurrency';
import utils from 'web.utils';
import weUtils from 'web_editor.utils';
import {ColorpickerWidget} from 'web.Colorpicker';
import {_t, _lt} from 'web.core';
import {_lt} from 'web.core';
import {svgToPNG} from 'website.utils';
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
@@ -89,6 +89,7 @@ class DescriptionScreen extends Component {
this.state = useStore();
this.labelToId = {};
this.autocompleteHasResults = true;
this.orm = useService('orm');
onMounted(() => this.onMounted());
}
@@ -101,8 +102,6 @@ class DescriptionScreen extends Component {
minLength: 1,
source: this._autocompleteSearch.bind(this),
select: this._selectIndustry.bind(this),
open: this._customizeNoResultMenuStyle.bind(this),
focus: this._disableKeyboardNav.bind(this),
classes: {
'ui-autocomplete': 'custom-ui-autocomplete shadow-lg border-0 o_configurator_show_fast o_configurator_industry_dropdown',
},
@@ -114,17 +113,6 @@ class DescriptionScreen extends Component {
}
}
/**
* Clear the input and its parent label and set the selected industry to undefined.
*
* @private
*/
_clearIndustrySelection() {
this.industrySelection.el.value = '';
this.industrySelection.el.parentNode.dataset.value = '';
this.state.selectIndustry();
}
/**
* Set the input's parent label value to automatically adapt input size
* and update the selected industry.
@@ -134,37 +122,17 @@ class DescriptionScreen extends Component {
*/
_setSelectedIndustry(label) {
this.industrySelection.el.parentNode.dataset.value = label;
const id = this.labelToId[label];
this.state.selectIndustry(label, id);
if (!this.autocompleteHasResults) {
// If no industry has been found, the first one is chosen by
// default to continue the website configuration.
this.state.selectIndustry(label, 1);
} else {
const id = this.labelToId[label];
this.state.selectIndustry(label, id);
}
this.checkDescriptionCompletion();
}
/**
* Called each time the suggestion menu is opened or updated. If there are no
* results to display the style of the "No result found" message is customized.
*
* @private
*/
_customizeNoResultMenuStyle() {
if (!this.autocompleteHasResults) {
const noResultLinkEl = this.industrySelection.el.parentElement.getElementsByTagName('a')[0];
noResultLinkEl.classList.add('o_no_result');
}
}
/**
* Disables keyboard navigation when there are no results to avoid selecting the
* "No result found" message by pressing the down arrow key.
*
* @private
* @param {Event} ev
*/
_disableKeyboardNav(ev) {
if (!this.autocompleteHasResults) {
ev.preventDefault();
}
}
/**
* Called each time the autocomplete input's value changes. Only industries
* having a label or a synonym containing all terms of the input value are
@@ -172,7 +140,7 @@ class DescriptionScreen extends Component {
* The order received from IAP is kept (expected to be on descending hit
* count) unless there are 7 or less matches in which case the results are
* sorted alphabetically.
* The result size is limited to 15.
* The result size is limited to 30.
*
* @param {Object} request object with a single 'term' property which is the
* input current value
@@ -181,7 +149,7 @@ class DescriptionScreen extends Component {
*/
_autocompleteSearch(request, response) {
const terms = request.term.toLowerCase().split(/[|,\n]+/);
const limit = 15;
const limit = 30;
const sortLimit = 7;
// `this.state.industries` is already sorted by hit count (from IAP).
// That order should be kept after manipulating the recordset.
@@ -196,7 +164,7 @@ class DescriptionScreen extends Component {
});
if (matches.length > limit) {
// Keep matches with the least number of words so that e.g.
// "restaurant" remains available even if there are 15 specific
// "restaurant" remains available even if there are 30 specific
// sub-types that have a higher hit count.
matches = matches.sort((x, y) => x.wordCount - y.wordCount)
.slice(0, limit)
@@ -215,14 +183,13 @@ class DescriptionScreen extends Component {
this.labelToId[r.label] = r.id;
});
} else {
labels = [_t("No result found, broaden your search.")];
labels = [request.term];
}
response(labels);
}
/**
* Called when a menu option is selected. Update the selected industry or
* clear the input if the option is the "No result found" message.
* Called when a menu option is selected. Update the selected industry.
*
* @private
* @param {Event} ev
@@ -230,27 +197,19 @@ class DescriptionScreen extends Component {
* the selected option.
*/
_selectIndustry(ev, ui) {
if (this.autocompleteHasResults) {
this._setSelectedIndustry(ui.item.label);
} else {
this._clearIndustrySelection();
ev.preventDefault();
}
this._setSelectedIndustry(ui.item.label);
}
/**
* Called on industrySelection input blur. Updates the selected industry or
* clears the input if its current value is not a valid industry.
* Called on industrySelection input blur. Updates the selected industry.
*
* @private
* @param {Event} ev
*/
_blurIndustrySelection(ev) {
if (this.labelToId[ev.target.value] !== undefined) {
this._setSelectedIndustry(ev.target.value);
} else {
this._clearIndustrySelection();
}
// Check if the input is known by the server.
this.autocompleteHasResults = !!this.labelToId[ev.target.value];
this._setSelectedIndustry(ev.target.value);
}
selectWebsiteType(id) {
@@ -269,6 +228,13 @@ class DescriptionScreen extends Component {
checkDescriptionCompletion() {
const {selectedType, selectedPurpose, selectedIndustry} = this.state;
if (selectedType && selectedPurpose && selectedIndustry) {
// If the industry name is not known by the server, send it to the
// IAP server.
if (!this.autocompleteHasResults) {
this.orm.call('website', 'configurator_missing_industry', [], {
'unknown_industry': selectedIndustry.label,
});
}
this.props.navigate(ROUTES.paletteSelectionScreen);
}
}
@@ -451,13 +417,35 @@ class ThemeSelectionScreen extends ApplyConfiguratorScreen {
setup() {
super.setup();
this.uiService = useService('ui');
this.orm = useService('orm');
this.state = useStore();
this.themeSVGPreviews = [useRef('ThemePreview1'), useRef('ThemePreview2'), useRef('ThemePreview3')];
const proms = [];
onMounted(() => {
onMounted(async () => {
// Add a loading effect during the loading of the images inside the
// svgs.
this.uiService.block();
this.state.themes.forEach((theme, idx) => {
$(this.themeSVGPreviews[idx].el).append(theme.svg);
// Transform the text svg into a svg element.
const svgEl = new DOMParser().parseFromString(theme.svg, 'image/svg+xml').documentElement;
for (const imgEl of svgEl.querySelectorAll('image')) {
proms.push(new Promise((resolve, reject) => {
imgEl.addEventListener('load', () => {
resolve(imgEl);
}, {once: true});
imgEl.addEventListener('error', () => {
reject(imgEl);
}, {once: true});
}));
}
$(this.themeSVGPreviews[idx].el).append(svgEl);
});
// When all the images inside the svgs are loaded then remove the
// loading effect.
Promise.allSettled(proms).then(() => {
this.uiService.unblock();
});
});
}
@@ -171,7 +171,7 @@
}
.dropdown-item, .fa-angle-down, .ui-menu-item a, .ui-menu-item a.ui-state-active {
font-size: .65em;
font-size: .5em;
font-weight: inherit;
}
@@ -3,7 +3,7 @@
<t t-name="website.Configurator.SkipButton" owl="1">
<div class="container-fluid py-2 pb-md-3 text-end pe-lg-5">
<button class="btn btn-link" t-on-click="this.props.skip">Skip and start from scratch</button>
<button class="btn btn-link btn-lg" t-on-click="this.props.skip">Skip and start from scratch</button>
</div>
</t>
@@ -12,10 +12,10 @@
<div class="o_configurator_odoo_logo mt-4 ms-4" style="height: 31px; width: 99px;"/>
<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"/>
<div class="display-2 mb-2">Ready to build the <br class="d-none d-lg-inline"/>
<b>perfect website?</b>
</div>
<div class="lead fw-normal mb-4 text-600">We'll set you up and running in <b>4 steps</b>
<div class="h1 fw-light 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>
@@ -89,12 +89,12 @@
<div class="o_configurator_screen_content container palette_selection d-flex flex-grow-1 h-100">
<div class="o_content_container d-flex flex-row w-100 h-auto align-self-md-center o_configurator_show">
<div class="o_detect_logo w-25 order-lg-3 my-4 my-md-0 d-flex flex-column">
<div class="h4 text-center">
<div class="h2 text-center">
<b>Detect</b> from Logo</div>
<div class="d-flex flex-column flex-grow-1 py-4">
<div 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">
<div class="h3 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>
@@ -122,7 +122,7 @@
<div class="border-start d-none d-lg-inline h-100 mx-auto w-0"></div>
</div>
<div class="o_palettes_container w-auto flex-grow-1 o_configurator_show_fast">
<div class="h4 text-center">
<div class="h2 text-center">
<b>Choose</b> a pre-made Palette</div>
<div class="d-flex flex-wrap align-items-end">
<t t-foreach="state.getPalettes()" t-as="palette" t-key="palette_index">
@@ -149,33 +149,35 @@
<div class="o_configurator_screen_content overflow-hidden container d-flex flex-grow-1 align-items-center">
<div class="m-auto d-flex mh-100 flex-column">
<div class="o_configurator_typing_text o_configurator_show_fast">Add <b class="text-info">Pages</b> and <b class="text-warning">Features</b></div>
<h5 class="o_configurator_show_fast text-muted pb-lg-3">You'll be able to create your pages later on.</h5>
<h2 class="o_configurator_show_fast text-muted pb-lg-3">You'll be able to create your pages later on.</h2>
<div class="page_feature_selection o_configurator_show overflow-auto mt-lg-3 mx-n2">
<div class="w-100 page_feature_selection d-flex flex-wrap">
<t t-foreach="state.getFeatures()" t-as="feature" t-key="feature_index">
<t t-set='isInstalled' t-value="feature.module_state == 'installed'"/>
<div class="o_feature_card_container p-2 w-25" t-if="feature.type != 'empty'">
<div t-attf-class="card h-100 {{isInstalled ? 'card_installed' : (feature.selected ? 'border-success' : '')}}" t-on-click="() => state.toggleFeature(feature.id)">
<div class="card-body py-2">
<div class="h3 card-body py-3 px-4">
<t t-if="isInstalled">
<i t-attf-class="o_configurator_feature_status fa fa-info-circle text-muted" title="Already installed"/>
</t>
<t t-else="">
<i t-attf-class="o_configurator_feature_status fa {{feature.selected ? 'fa-check-circle text-success' : 'fa-circle-o text-300'}}" />
</t>
<h5 t-attf-class="card-title d-flex align-items-center {{isInstalled ? 'text-muted' : ''}}">
<i t-attf-class="me-2 small fa {{feature.icon}} {{isInstalled ? 'text-muted' : (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 t-attf-class="h2 mb-2 card-title d-flex align-items-center {{isInstalled ? 'text-muted' : ''}}">
<i t-attf-class="me-2 fa {{feature.icon}} {{isInstalled ? 'text-muted' : (feature.type == 'page' ? 'text-info' : 'text-warning')}}"/>
<h3 class="m-1">
<t t-esc="feature.name"/>
</h3>
</div>
<p class="h4 card-text text-muted" t-esc="feature.description"/>
</div>
</div>
</div>
</t>
</div>
</div>
<div class="text-end border-top pt-2">
<button class="btn btn-primary btn-lg ms-3" t-on-click="buildWebsite">Build my website</button>
<div class="pt-2">
<button class="btn btn-primary btn-lg" t-on-click="buildWebsite">Build my website</button>
</div>
</div>
</div>