[REF] website, website_*: adapt code to owl 2

Owl 2 changelog: https://github.com/odoo/owl/blob/a9f29c4caad4f32d06be1ec4780572825781cd9b/CHANGELOG.md

Part-of: odoo/odoo#80156
Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Bruno Boi <boi@odoo.com>
Co-authored-by: Géry Debongnie <ged@odoo.com>
Co-authored-by: Samuel Degueldre <sad@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Co-authored-by: Simon Genin (ges) <ges@odoo.com>
Co-authored-by: Francois (fge) <fge@odoo.com>
Co-authored-by: Michael Mattiello (mcm) <mcm@odoo.com>
Co-authored-by: Julien Mougenot <jum@odoo.com>
Co-authored-by: luvi <luvi@odoo.com>
Co-authored-by: Lucas Perais (lpe) <lpe@odoo.com>
Co-authored-by: Jorge Pinna Puissant <jpp@odoo.com>
This commit is contained in:
Samuel Degueldre
2022-02-10 07:46:12 +00:00
co-authored by Aaron Bohy Bruno Boi Géry Debongnie Mathieu Duckerts-Antoine Simon Genin Francois Michael Mattiello Julien Mougenot luvi Lucas Perais Jorge Pinna Puissant
parent 6d13271aac
commit 56cc3dfab1
6 changed files with 255 additions and 202 deletions
@@ -8,8 +8,16 @@ import {ColorpickerWidget} from 'web.Colorpicker';
import {_t, _lt} from 'web.core';
import {svgToPNG} from 'website.utils';
import {useService} from "@web/core/utils/hooks";
import {renderToString} from "@web/core/utils/render";
const { Component, QWeb, RouteComponent, Router, Store, loadFile, mount, useDispatch, useGetters, useRef, useStore, whenReady } = owl;
const { App, Component, onMounted, reactive, useEnv, useRef, useState, whenReady } = owl;
const ROUTES = {
descriptionScreen: 2,
paletteSelectionScreen: 3,
featuresSelectionScreen: 4,
themeSelectionScreen: 5,
};
const WEBSITE_TYPES = {
1: {id: 1, label: _lt("a business website"), name: 'business'},
@@ -69,13 +77,13 @@ class SkipButton extends Component {
SkipButton.template = 'website.Configurator.SkipButton';
class WelcomeScreen extends Component {
constructor() {
super(...arguments);
this.dispatch = useDispatch();
setup() {
this.state = useStore();
this.router = useRouter();
}
goToDescription() {
this.env.router.navigate({to: 'CONFIGURATOR_DESCRIPTION_SCREEN'});
this.router.navigate(ROUTES.descriptionScreen);
}
}
@@ -85,18 +93,18 @@ Object.assign(WelcomeScreen, {
});
class DescriptionScreen extends Component {
constructor() {
super(...arguments);
setup() {
this.industrySelection = useRef('industrySelection');
this.state = useStore((state) => state);
this.state = useStore();
this.router = useRouter();
this.labelToId = {};
this.getters = useGetters();
this.dispatch = useDispatch();
this.autocompleteHasResults = true;
onMounted(() => this.onMounted());
}
mounted() {
this.dispatch('selectWebsitePurpose', undefined);
onMounted() {
this.selectWebsitePurpose();
$(this.industrySelection.el).autocomplete({
appendTo: '.o_configurator_industry_wrapper',
delay: 400,
@@ -124,7 +132,7 @@ class DescriptionScreen extends Component {
_clearIndustrySelection() {
this.industrySelection.el.value = '';
this.industrySelection.el.parentNode.dataset.value = '';
this.dispatch('selectIndustry', undefined, undefined);
this.state.selectIndustry();
}
/**
@@ -137,7 +145,7 @@ class DescriptionScreen extends Component {
_setSelectedIndustry(label) {
this.industrySelection.el.parentNode.dataset.value = label;
const id = this.labelToId[label];
this.dispatch('selectIndustry', label, id);
this.state.selectIndustry(label, id);
this.checkDescriptionCompletion();
}
@@ -236,25 +244,23 @@ class DescriptionScreen extends Component {
}
}
selectWebsiteType(ev) {
const {id} = ev.target.dataset;
this.dispatch('selectWebsiteType', id);
selectWebsiteType(id) {
this.state.selectWebsiteType(id);
setTimeout(() => {
this.industrySelection.el.focus();
});
this.checkDescriptionCompletion();
}
selectWebsitePurpose(ev) {
const {id} = ev.target.dataset;
this.dispatch('selectWebsitePurpose', id);
selectWebsitePurpose(id) {
this.state.selectWebsitePurpose(id);
this.checkDescriptionCompletion();
}
checkDescriptionCompletion() {
const {selectedType, selectedPurpose, selectedIndustry} = this.state;
if (selectedType && selectedPurpose && selectedIndustry) {
this.env.router.navigate({to: 'CONFIGURATOR_PALETTE_SELECTION_SCREEN'});
this.router.navigate(ROUTES.paletteSelectionScreen);
}
}
}
@@ -265,19 +271,17 @@ Object.assign(DescriptionScreen, {
});
class PaletteSelectionScreen extends Component {
constructor() {
super(...arguments);
this.state = useStore((state) => state);
this.getters = useGetters();
this.dispatch = useDispatch();
setup() {
this.state = useStore();
this.router = useRouter();
this.logoInputRef = useRef('logoSelectionInput');
this.notification = useService("notification");
}
mounted() {
if (this.state.logo) {
this.updatePalettes();
}
onMounted(() => {
if (this.state.logo) {
this.updatePalettes();
}
});
}
uploadLogo() {
@@ -298,7 +302,7 @@ class PaletteSelectionScreen extends Component {
}
});
if (!attachment.error) {
this.dispatch('changeLogo', data, attachment.id);
this.state.changeLogo(data, attachment.id);
this.updatePalettes();
} else {
this.notification.notify({
@@ -321,12 +325,12 @@ class PaletteSelectionScreen extends Component {
args: [img],
kwargs: {mitigate: 255},
});
this.dispatch('setRecommendedPalette', color1, color2);
this.state.setRecommendedPalette(color1, color2);
}
selectPalette(paletteName) {
this.dispatch('selectPalette', paletteName);
this.env.router.navigate({to: 'CONFIGURATOR_FEATURES_SELECTION_SCREEN'});
this.state.selectPalette(paletteName);
this.router.navigate(ROUTES.featuresSelectionScreen);
}
}
@@ -336,18 +340,15 @@ Object.assign(PaletteSelectionScreen, {
});
class FeaturesSelectionScreen extends Component {
constructor() {
super(...arguments);
this.state = useStore((state) => state);
this.getters = useGetters();
this.dispatch = useDispatch();
setup() {
this.state = useStore();
this.router = useRouter();
}
async buildWebsite() {
const industryId = this.state.selectedIndustry && this.state.selectedIndustry.id;
if (!industryId) {
this.env.router.navigate({to: 'CONFIGURATOR_DESCRIPTION_SCREEN'});
return;
return this.router.navigate(ROUTES.descriptionScreen);
}
const params = {
industry_id: industryId,
@@ -362,8 +363,8 @@ class FeaturesSelectionScreen extends Component {
if (!themes.length) {
await applyConfigurator(this, 'theme_default');
} else {
this.dispatch('updateRecommendedThemes', themes);
this.env.router.navigate({to: 'CONFIGURATOR_THEME_SELECTION_SCREEN'});
this.state.updateRecommendedThemes(themes);
this.router.navigate(ROUTES.themeSelectionScreen);
}
}
}
@@ -374,16 +375,15 @@ Object.assign(FeaturesSelectionScreen, {
});
class ThemeSelectionScreen extends Component {
constructor() {
super(...arguments);
this.state = useStore((state) => state);
this.getters = useGetters();
setup() {
this.state = useStore();
this.router = useRouter();
this.themeSVGPreviews = [useRef('ThemePreview1'), useRef('ThemePreview2'), useRef('ThemePreview3')];
}
mounted() {
this.state.themes.forEach((theme, idx) => {
$(this.themeSVGPreviews[idx].el).append(theme.svg);
onMounted(() => {
this.state.themes.forEach((theme, idx) => {
$(this.themeSVGPreviews[idx].el).append(theme.svg);
});
});
}
@@ -394,107 +394,139 @@ class ThemeSelectionScreen extends Component {
ThemeSelectionScreen.template = 'website.Configurator.ThemeSelectionScreen';
class App extends Component {}
class Configurator extends Component {
setup() {
this.router = useRouter();
}
}
Object.assign(App, {
components: {RouteComponent},
template: 'website.Configurator.App',
Object.assign(Configurator, {
components: {
WelcomeScreen,
DescriptionScreen,
PaletteSelectionScreen,
FeaturesSelectionScreen,
ThemeSelectionScreen,
},
template: 'website.Configurator.Configurator',
});
//---------------------------------------------------------
// Routes
// Router
//---------------------------------------------------------
const ROUTES = [
{name: 'CONFIGURATOR_WELCOME_SCREEN', path: '/website/configurator', component: WelcomeScreen},
{name: 'CONFIGURATOR_WELCOME_SCREEN_FALLBACK', 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},
];
class Router {
constructor() {
this.location = window.location.pathname;
}
navigate(id) {
this.location = `/website/configurator${id ? '/' + id : ''}`;
history.pushState({}, '', window.location.origin + this.location);
}
}
const useRouter = () => {
const env = useEnv();
return useState(env.router);
};
//---------------------------------------------------------
// Store
//---------------------------------------------------------
const getters = {
class Store {
async start() {
Object.assign(this, await getInitialState());
}
//-------------------------------------------------------------------------
// Getters
//-------------------------------------------------------------------------
getWebsiteTypes() {
return Object.values(WEBSITE_TYPES);
},
}
getSelectedType(_, id) {
return id ? WEBSITE_TYPES[id] : undefined;
},
getSelectedType(id) {
return id && WEBSITE_TYPES[id];
}
getWebsitePurpose() {
return Object.values(WEBSITE_PURPOSES);
},
}
getSelectedPurpose(_, id) {
return id ? WEBSITE_PURPOSES[id] : undefined;
},
getSelectedPurpose(id) {
return id && WEBSITE_PURPOSES[id];
}
getFeatures({state}) {
return Object.values(state.features);
},
getFeatures() {
return Object.values(this.features);
}
getPalettes({state}) {
return Object.values(state.palettes);
},
getPalettes() {
return Object.values(this.palettes);
}
getThemeName(idx) {
return this.themes.length > idx && this.themes[idx].name;
}
getThemeName({state}, idx) {
return state.themes.length > idx ? state.themes[idx].name : undefined;
},
/**
* @param {Object} obj
* @param {string|undefined} [obj.state]
* @returns {string|false}
* @returns {string | false}
*/
getSelectedPaletteName({state}) {
const palette = state.selectedPalette;
getSelectedPaletteName() {
const palette = this.selectedPalette;
return palette ? (palette.name || 'recommendedPalette') : false;
},
};
}
const actions = {
selectWebsiteType({state}, id) {
Object.values(state.features).filter((feature) => feature.module_state !== 'installed').forEach((feature) => {
//-------------------------------------------------------------------------
// Actions
//-------------------------------------------------------------------------
selectWebsiteType(id) {
Object.values(this.features).filter((feature) => feature.module_state !== 'installed').forEach((feature) => {
feature.selected = feature.website_config_preselection.includes(WEBSITE_TYPES[id].name);
});
state.selectedType = id;
},
selectWebsitePurpose({state}, id) {
Object.values(state.features).filter((feature) => feature.module_state !== 'installed').forEach((feature) => {
this.selectedType = id;
}
selectWebsitePurpose(id) {
Object.values(this.features).filter((feature) => feature.module_state !== 'installed').forEach((feature) => {
// need to check id, since we set to undefined in mount() to avoid the auto next screen on back button
feature.selected |= id && feature.website_config_preselection.includes(WEBSITE_PURPOSES[id].name);
});
state.selectedPurpose = id;
},
selectIndustry({state}, label, id) {
this.selectedPurpose = id;
}
selectIndustry(label, id) {
if (!label || !id) {
state.selectedIndustry = undefined;
this.selectedIndustry = undefined;
} else {
state.selectedIndustry = {id, label};
this.selectedIndustry = { id, label };
}
},
changeLogo({state}, data, attachmentId) {
state.logo = data;
state.logoAttachmentId = attachmentId;
},
selectPalette({state}, paletteName) {
}
changeLogo(data, attachmentId) {
this.logo = data;
this.logoAttachmentId = attachmentId;
}
selectPalette(paletteName) {
if (paletteName === 'recommendedPalette') {
state.selectedPalette = state.recommendedPalette;
this.selectedPalette = this.recommendedPalette;
} else {
state.selectedPalette = state.palettes[paletteName];
this.selectedPalette = this.palettes[paletteName];
}
},
toggleFeature({state}, featureId) {
const feature = state.features[featureId];
}
toggleFeature(featureId) {
const feature = this.features[featureId];
const isModuleInstalled = feature.module_state === 'installed';
feature.selected = !feature.selected || isModuleInstalled;
},
setRecommendedPalette({state}, color1, color2) {
}
setRecommendedPalette(color1, color2) {
if (color1 && color2) {
if (color1 === color2) {
color2 = ColorpickerWidget.mixCssColors('#FFFFFF', color1, 0.2);
@@ -507,20 +539,20 @@ const actions = {
color5: ColorpickerWidget.mixCssColors(color1, '#000000', 0.75),
};
CUSTOM_BG_COLOR_ATTRS.forEach((attr) => {
recommendedPalette[attr] = recommendedPalette[state.defaultColors[attr]];
recommendedPalette[attr] = recommendedPalette[this.defaultColors[attr]];
});
state.recommendedPalette = recommendedPalette;
this.recommendedPalette = recommendedPalette;
} else {
state.recommendedPalette = undefined;
this.recommendedPalette = undefined;
}
},
updateRecommendedThemes({state}, themes) {
state.themes = themes.slice(0, 3);
}
};
updateRecommendedThemes(themes) {
this.themes = themes.slice(0, 3);
}
}
async function getInitialState() {
// Load values from python and iap
var results = await rpc.query({
model: 'website',
@@ -596,6 +628,15 @@ async function getInitialState() {
});
}
const useStore = () => {
const env = useEnv();
return useState(env.state);
};
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
async function skipConfigurator() {
await rpc.query({
model: 'website',
@@ -607,16 +648,15 @@ async function skipConfigurator() {
async function applyConfigurator(self, themeName) {
if (!self.state.selectedIndustry) {
self.env.router.navigate({to: 'CONFIGURATOR_DESCRIPTION_SCREEN'});
return;
return self.router.navigate(ROUTES.descriptionScreen);
}
if (!self.state.selectedPalette) {
self.env.router.navigate({to: 'CONFIGURATOR_PALETTE_SELECTION_SCREEN'});
return;
return self.router.navigate(ROUTES.paletteSelectionScreen);
}
if (themeName !== undefined) {
$('body').append(self.env.loader);
const selectedFeatures = Object.values(self.state.features).filter((feature) => feature.selected).map((feature) => feature.id);
const loader = renderToString('website.ThemePreview.Loader', {showTips: true});
$('body').append(loader);
const selectedFeatures = Object.values(self.state.features).filter((feature) => feature.selected).map((feature) => feature.id)
let selectedPalette = self.state.selectedPalette.name;
if (!selectedPalette) {
selectedPalette = [
@@ -644,48 +684,57 @@ async function applyConfigurator(self, themeName) {
window.sessionStorage.removeItem(SESSION_STORAGE_ITEM_NAME);
window.location = resp.url;
}
};
function updateStorage(state) {
const newState = JSON.stringify({
defaultColors: state.defaultColors,
features: state.features,
logo: state.logo,
logoAttachmentId: state.logoAttachmentId,
selectedIndustry: state.selectedIndustry,
selectedPalette: state.selectedPalette,
selectedPurpose: state.selectedPurpose,
selectedType: state.selectedType,
recommendedPalette: state.recommendedPalette,
});
window.sessionStorage.setItem(SESSION_STORAGE_ITEM_NAME, newState);
}
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});
store.on("update", null, () => {
const newState = {
selectedType: store.state.selectedType,
selectedPurpose: store.state.selectedPurpose,
selectedIndustry: store.state.selectedIndustry,
selectedPalette: store.state.selectedPalette,
recommendedPalette: store.state.recommendedPalette,
defaultColors: store.state.defaultColors,
features: store.state.features,
logo: store.state.logo,
logoAttachmentId: store.state.logoAttachmentId,
};
window.sessionStorage.setItem(SESSION_STORAGE_ITEM_NAME, JSON.stringify(newState));
});
const state = reactive(new Store(), () => updateStorage(state));
await state.start();
updateStorage(state);
const env = {
state,
router: reactive(new Router()),
services: Component.env.services,
};
await session.is_bound;
const qweb = new QWeb({translateFn: _t});
const loaderTemplate = await loadFile('/website/static/src/xml/theme_preview.xml');
const configuratorTemplates = await loadFile('/website/static/src/components/configurator/configurator.xml');
qweb.addTemplates(loaderTemplate);
qweb.addTemplates(configuratorTemplates);
env.loader = qweb.renderToString('website.ThemePreview.Loader', {
showTips: true
});
const services = Component.env.services;
return Object.assign(env, {router, store, qweb, services});
return env;
}
//-----------------------------------------------------------------------------
// Setup
//-----------------------------------------------------------------------------
async function setup() {
const env = await makeEnvironment();
if (!env.store.state.industries) {
if (!env.state.industries) {
await skipConfigurator();
} else {
mount(App, {target: document.body, env});
const templates = await (await fetch('/website/static/src/components/configurator/configurator.xml')).text();
const app = new App(Configurator, {
env,
dev: env.debug,
templates,
translatableAttributes: ["data-tooltip"],
translateFn: _t,
});
const loaderTemplate = await (await fetch('/website/static/src/xml/theme_preview.xml')).text();
app.addTemplates(loaderTemplate);
renderToString.app = app;
await app.mount(document.body);
}
}
@@ -3,7 +3,7 @@
<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>
<button class="btn btn-link" t-on-click="skip">Skip and start from scratch</button>
</div>
</t>
@@ -19,7 +19,7 @@
</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>
<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/>
@@ -39,14 +39,14 @@
<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" />
<t t-esc="state.getSelectedType(state.selectedType).label"/>
</i>
<i class="fa fa-angle-down text-black-50 ml-auto pl-2" role="img"/>
</a>
</div>
<div t-attf-class="dropdown-menu 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-foreach="state.getWebsiteTypes()" t-as="type" t-key="type.name">
<a t-att-title="type.name" t-on-click="() => this.selectWebsiteType(type.id)" class="dropdown-item o_change_website_type">
<t t-esc="type.label"/>
</a>
</t>
@@ -67,14 +67,14 @@
<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 t-esc="state.getSelectedPurpose(state.selectedPurpose).label"/>
</t>
<i class="fa fa-angle-down text-black-50 ml-auto pl-2" role="img"/>
</a>
</div>
<div class="dropdown-menu 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-foreach="state.getWebsitePurpose()" t-as="type" t-key="type.name">
<a t-att-title="type.name" t-on-click="() => this.selectWebsitePurpose(type.id)" class="dropdown-item o_change_website_purpose">
<t t-esc="type.label"/>
</a>
</t>
@@ -110,13 +110,13 @@
</div>
</div>
<div t-if="state.recommendedPalette" class="w-75 mx-auto px-2 pt-3" style="max-width: 184px;">
<div t-attf-class="palette_card rounded-pill overflow-hidden d-flex {{getters.getSelectedPaletteName() == 'recommendedPalette' ? 'selected' : ''}}"
t-on-click="selectPalette('recommendedPalette')" t-attf-style="background-color: {{state.recommendedPalette.color3}}">
<div t-attf-class="palette_card rounded-pill overflow-hidden d-flex {{state.getSelectedPaletteName() == 'recommendedPalette' ? 'selected' : ''}}"
t-on-click="() => this.selectPalette('recommendedPalette')" t-attf-style="background-color: {{state.recommendedPalette.color3}}">
<div class="color_sample w-100" t-attf-style="background-color: {{state.recommendedPalette.color1}}"/>
<div class="color_sample w-100" t-attf-style="background-color: {{state.recommendedPalette.color3}}"/>
<div class="color_sample w-100" t-attf-style="background-color: {{state.recommendedPalette.color2}}"/>
</div>
<button class="btn btn-primary text-nowrap mt-3 d-block mx-auto" t-on-click="selectPalette('recommendedPalette')">
<button class="btn btn-primary text-nowrap mt-3 d-block mx-auto" t-on-click="() => this.selectPalette('recommendedPalette')">
Let's go!<i class="fa fa-angle-right text-white-50 pl-2" role="img"/>
</button>
</div>
@@ -131,10 +131,10 @@
<div class="h4 text-center">
<b>Choose</b> a pre-made Palette</div>
<div class="d-flex flex-wrap align-items-end">
<t t-foreach="getters.getPalettes()" t-as="palette" t-key="palette_index">
<t t-foreach="state.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 {{getters.getSelectedPaletteName() == palette.name ? 'selected' : ''}}"
t-on-click="selectPalette(palette.name)" t-attf-style="background-color: {{palette.color3}}">
<div t-attf-class="palette_card rounded-pill overflow-hidden d-flex {{state.getSelectedPaletteName() == palette.name ? 'selected' : ''}}"
t-on-click="() => this.selectPalette(palette.name)" t-attf-style="background-color: {{palette.color3}}">
<div class="color_sample w-100" t-attf-style="background-color: {{palette.color1}}"/>
<div class="color_sample w-100" t-attf-style="background-color: {{palette.color3}}"/>
<div class="color_sample w-100" t-attf-style="background-color: {{palette.color2}}"/>
@@ -160,10 +160,10 @@
<h5 class="o_configurator_show_fast text-muted pb-lg-3">You'll be able to create your pages later on.</h5>
<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="getters.getFeatures()" t-as="feature" t-key="feature_index">
<t t-foreach="state.getFeatures()" t-as="feature" t-key="feature_index">
<t t-set='isInstalled' t-value="feature.module_state == 'installed'"/>
<div class="p-2 w-100 w-md-50 w-lg-25" t-if="feature.type != 'empty'">
<div t-attf-class="card h-100 {{isInstalled ? 'card_installed' : (feature.selected ? 'border-success' : '')}}" t-on-click="dispatch('toggleFeature', feature.id)">
<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">
<t t-if="isInstalled">
<i t-attf-class="o_configurator_feature_status fa fa-info-circle text-muted" title="Already installed"/>
@@ -183,7 +183,7 @@
</div>
</div>
<div class="text-right border-top pt-2">
<button class="btn btn-primary btn-lg ml-3" t-on-click="buildWebsite()">Build my website</button>
<button class="btn btn-primary btn-lg ml-3" t-on-click="buildWebsite">Build my website</button>
</div>
</div>
</div>
@@ -203,29 +203,29 @@
<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)">
<t t-if="state.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 class="button_area" t-on-click="() => this.chooseTheme(state.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)">
<t t-if="state.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 class="button_area" t-on-click="() => this.chooseTheme(state.getThemeName(0))"/>
</div>
</t>
</div>
<div class="col-12 col-lg-4 d-flex align-items-end">
<t t-if="getters.getThemeName(2)">
<t t-if="state.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 class="button_area" t-on-click="() => this.chooseTheme(state.getThemeName(2))"/>
</div>
</t>
</div>
@@ -236,9 +236,13 @@
</div>
</t>
<t t-name="website.Configurator.App">
<t t-name="website.Configurator.Configurator" owl="1">
<div class="o_configurator_container">
<RouteComponent />
<DescriptionScreen t-if="router.location === '/website/configurator/2'"/>
<PaletteSelectionScreen t-elif="router.location === '/website/configurator/3'"/>
<FeaturesSelectionScreen t-elif="router.location === '/website/configurator/4'"/>
<ThemeSelectionScreen t-elif="router.location === '/website/configurator/5'"/>
<WelcomeScreen t-else=""/>
</div>
</t>
@@ -11,10 +11,10 @@ const debugMenuService = {
const systray = document.querySelector('.o_menu_systray');
if (systray) {
Object.assign(env, createDebugContext(env, {categories: ["default"]}));
mount(DebugMenuBasic, {
target: systray,
mount(DebugMenuBasic, systray, {
position: 'first-child',
env,
templates: window.__OWL_TEMPLATES__,
});
}
}
@@ -15,14 +15,14 @@ QUnit.module('Website Backend Dashboard', {
assert.expect(5);
patch(ControlPanel.prototype, 'test.ControlPanel', {
mounted() {
this.__superMounted = this._super.bind(this);
assert.step('mounted');
this.__superMounted(...arguments);
},
willUnmount() {
assert.step('willUnmount');
this.__superMounted(...arguments);
setup() {
this._super();
owl.onMounted(() => {
assert.step('mounted');
});
owl.onWillUnmount(() => {
assert.step('willUnmount');
})
},
});
@@ -6,6 +6,7 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var wysiwygLoader = require('web_editor.loader');
var publicWidget = require('web.public.widget');
const { Markup } = require('web.utils');
var session = require('web.session');
var qweb = core.qweb;
@@ -253,10 +254,9 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
// translation, to fix in the appropriate version
notifOptions.message = `${karma} ${_t("karma is required to perform this action. ")}`;
if (forumID) {
notifOptions.messageIsHtml = true;
const linkLabel = _.escape(_t("Read the guidelines to know how to gain karma."));
notifOptions.message = `
${_.escape(notifOptions.message)}<br/>
const linkLabel = _t("Read the guidelines to know how to gain karma.");
notifOptions.message = Markup`
${notifOptions.message}<br/>
<a class="alert-link" href="/forum/${forumID}/faq">${linkLabel}</a>
`;
}
@@ -10,8 +10,8 @@ QUnit.module('activity_tests.js', {
beforeEach(this);
this.start = async params => {
const res = await start({ ...params, data: this.data });
const { components, env, widget } = res;
this.components = components;
const { apps, env, widget } = res;
this.apps = apps;
this.env = env;
this.widget = widget;
return res;