[IMP] website: adapt ThemePreview to OWL

This commit will adapt ThemePreview views to OWL.

Those views use custom components to achieve the desired layout and
functionality.

Part-of: odoo/odoo#99150
Co-authored-by: Younn Olivier <yol@odoo.com>
This commit is contained in:
Arthur Detroux (ard)
2022-09-08 08:36:52 +02:00
co-authored by Younn Olivier
parent ed7794839b
commit b9fef88c47
8 changed files with 218 additions and 293 deletions
@@ -1,28 +1,19 @@
odoo.define('website.iframe_widget', function (require) {
"use strict";
/** @odoo-module **/
import { registry } from '@web/core/registry';
import { useBus } from "@web/core/utils/hooks";
var AbstractField = require('web.AbstractField');
var core = require('web.core');
var fieldRegistry = require('web.field_registry');
const { Component, useState } = owl;
var QWeb = core.qweb;
class FieldIframePreview extends Component {
setup() {
this.state = useState({isMobile: false});
/**
* Display iframe
*/
var FieldIframePreview = AbstractField.extend({
className: 'd-block o_field_iframe_preview m-0 h-100',
useBus(this.env.bus, 'THEME_PREVIEW:SWITCH_MODE', (ev) => {
this.state.isMobile = ev.detail.mode === 'mobile';
});
}
}
FieldIframePreview.template = 'website.iframeWidget';
_render: function () {
this.$el.html(QWeb.render('website.iframeWidget', {
url: this.value,
}));
},
});
fieldRegistry.add('iframe', FieldIframePreview);
return FieldIframePreview;
});
registry.category('fields').add('iframe', FieldIframePreview);
@@ -2,16 +2,20 @@
<templates xml:space="preserve">
<t t-name="website.iframeWidget">
<t t-name="website.iframeWidget" owl="1">
<div t-if="props.value" class="h-100" t-att-class="this.state.isMobile and 'is_mobile'">
<iframe
t-if="url"
frameBorder="0"
class="d-block"
height="100%"
width="100%"
frameBorder="0"
t-att-src="url"
class='d-block'
></iframe>
<div t-else="">No Url</div>
</t>
t-att-src="props.value"/>
<img t-if="this.state.isMobile"
alt="phone"
class="img_mobile"
src="/website/static/src/img/phone.png"/>
</div>
<div t-else="">No Url</div>
</t>
</templates>
@@ -1,38 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="website.ThemePreview.Buttons">
<div>
<div class="o_form_buttons_view" role="toolbar" aria-label="Main actions">
<button type="object" name="button_choose_theme" class="btn btn-primary o_use_theme">
Start Now
</button>
<button class="btn btn-link o_switch_theme">
Choose another theme
</button>
</div>
</div>
</t>
<t t-name="website.ThemePreview.SwitchModeButton">
<div class="btn-group btn-group-toggle ms-1" data-bs-toggle="buttons">
<label class="btn btn-secondary active">
<input type="radio" name="viewer" data-mode='desktop' autocomplete="off" checked='checked'/> Desktop
</label>
<label class="btn btn-secondary">
<input type="radio" name="viewer" data-mode='mobile' autocomplete="off"/> Mobile
</label>
<t t-name="website.ThemePreview.Buttons" owl="1">
<div>
<div class="o_form_buttons_view" role="toolbar" aria-label="Main actions">
<ViewButton className="'btn btn-primary o_use_theme'"
clickParams="{type: 'object', name: 'button_choose_theme'}"
record="this.model.root">
<t t-set-slot="contents">Start Now</t>
</ViewButton>
<button class="btn btn-link o_switch_theme" t-on-click.stop="this.back">
Choose another theme
</button>
</div>
</t>
<t t-name="website.ThemePreview.Loader">
<div class="o_theme_install_loader_container position-fixed fixed-top fixed-left
h-100 w-100 d-flex flex-column align-items-center text-white fw-bold text-center">
<t t-if="showTips">Building your website...</t>
<div class="o_theme_install_loader"></div>
<p t-if="showTips" class="o_theme_install_loader_tip w-25">
TIP: Once loaded, follow the
<span class="o_tooltip o_tooltip_visible bottom o_animated position-relative"></span>
<br/>pointer to build the perfect page in 7 steps.
</p>
</div>
</t>
<t t-name="website.ThemePreviewForm.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_cp_bottom_right')]" position="replace">
<div class="o_cp_bottom_right" role="group">
<input class="btn-check" id="themeViewerDesktop" type="radio" name="viewer" data-mode='desktop' autocomplete="off" checked='checked'/>
<label class="btn btn-secondary" for="themeViewerDesktop" t-on-click="this.onDesktopClick">Desktop</label>
<input class="btn-check" id="themeViewerMobile" type="radio" name="viewer" data-mode='mobile' autocomplete="off"/>
<label class="btn btn-secondary" for="themeViewerMobile" t-on-click="this.onMobileClick">Mobile</label>
</div>
</t>
</xpath>
</t>
<t t-name="website.ThemePreviewKanban.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_cp_top_left')]" position="replace">
<div class="o_cp_top_left">
<ol class="breadcrumb" role="navigation">
<li class="breadcrumb-item active text-black-75">
<span class="text-900">Pick a Theme</span>
<small class="mx-2 text-muted">Don't worry, you can switch later.</small>
</li>
</ol>
</div>
</xpath>
<xpath expr="//div[hasclass('o_cp_top_right')]" position="replace">
<div class="o_cp_top_right d-flex flex-row-reverse">
<a class="btn btn-secondary" t-on-click="this.close">
<i class="fa fa-times" />
</a>
</div>
</xpath>
</t>
</templates>
@@ -1,157 +1,98 @@
odoo.define('website.theme_preview_form', function (require) {
"use strict";
/** @odoo-module **/
var FormController = require('web.FormController');
var FormView = require('web.FormView');
var viewRegistry = require('web.view_registry');
var core = require('web.core');
var qweb = core.qweb;
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { FormController } from "@web/views/form/form_controller";
import { formView } from "@web/views/form/form_view";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { ViewButton } from "@web/views/view_button/view_button";
const { useSubEnv, useEnv } = owl;
/*
* Common code for theme installation/update handler.
* It overrides the onClickViewButton function that's present in the env.
* That way, we display our own Loader and make a silent call to the ORM.
*/
const ThemePreviewControllerCommon = {
/**
* Called to add loading effect and install/pdate the selected theme depending on action.
*
* @private
* @param {number} res_id
* @param {String} action
*/
_handleThemeAction(res_id, action) {
this.$loader = $(qweb.render('website.ThemePreview.Loader', {
'showTips': action !== 'button_refresh_theme',
}));
let actionCallback = undefined;
this._addLoader();
switch (action) {
case 'button_choose_theme':
actionCallback = result => {
this.do_action(result);
this._removeLoader();
};
break;
case 'button_refresh_theme':
actionCallback = () => this._removeLoader();
break;
export function useLoaderOnClick() {
const website = useService('website');
const orm = useService('orm');
const action = useService('action');
const env = useEnv();
const previousOnClickViewButton = env.onClickViewButton;
useSubEnv({
async onClickViewButton(params) {
const name = params.clickParams.name;
if (['button_refresh_theme', 'button_choose_theme'].includes(name)) {
website.showLoader({ showTips: name !== 'button_refresh_theme' });
try {
const resParams = params.getResParams();
const callback = await orm.silent.call(resParams.resModel, name, [[resParams.resId]]);
if (callback) {
await action.doAction(callback);
}
website.hideLoader();
} catch (error) {
website.hideLoader();
throw error;
}
} else {
return previousOnClickViewButton(...arguments);
}
}
const rpcData = {
model: 'ir.module.module',
method: action,
args: [res_id],
context: this.initialState.context,
};
const rpcOptions = {
shadow: true,
};
this._rpc(rpcData, rpcOptions)
.then(actionCallback)
.guardedCatch(() => this._removeLoader());
},
});
}
class ThemePreviewFormController extends FormController {
/**
* Called to add loader element in DOM.
*
* @private
* @override
*/
_addLoader() {
$('body').append(this.$loader);
},
/**
* @private
*/
_removeLoader() {
this.$loader.remove();
setup() {
super.setup();
useLoaderOnClick();
}
};
var ThemePreviewController = FormController.extend(ThemePreviewControllerCommon, {
events: Object.assign({}, FormController.prototype.events, {
'click .o_use_theme': '_onStartNowClick',
'click .o_switch_theme': '_onSwitchThemeClick',
'change input[name="viewer"]': '_onSwitchButtonChange',
}),
/**
* @override
*/
start: function () {
this.$el.addClass('o_view_form_theme_preview_controller');
return this._super.apply(this, arguments);
},
// -------------------------------------------------------------------------
// Public
// -------------------------------------------------------------------------
/**
* @override
*/
renderButtons: function ($node) {
this.$buttons = $(qweb.render('website.ThemePreview.Buttons'));
if ($node) {
$node.html(this.$buttons);
}
},
/**
* Overriden to prevent the controller from hiding the buttons
* @see FormController
*
* @override
*/
updateButtons: function () { },
// -------------------------------------------------------------------------
// Private
// -------------------------------------------------------------------------
/**
* Add Switcher View Mobile / Desktop near pager
*
* @private
*/
_updateControlPanelProps: async function () {
const props = this._super(...arguments);
const $switchModeButton = $(qweb.render('website.ThemePreview.SwitchModeButton'));
this.controlPanelProps.cp_content.$pager = $switchModeButton;
return props;
},
// -------------------------------------------------------------------------
// Handlers
// -------------------------------------------------------------------------
/**
* Handler called when user click on 'Desktop/Mobile' switcher button.
*
* @private
*/
_onSwitchButtonChange: function () {
this.$('.o_preview_frame').toggleClass('is_mobile');
},
get className() {
return {...super.className, 'o_view_form_theme_preview_controller': true};
}
/**
* Handler called when user click on 'Choose another theme' button.
*
* @private
*/
_onSwitchThemeClick: function () {
this.trigger_up('history_back');
},
/**
* Handler called when user click on 'START NOW' button in form view.
*
* @private
*/
_onStartNowClick: function () {
this._handleThemeAction(this.getSelectedIds()[0], 'button_choose_theme');
},
});
var ThemePreviewFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
Controller: ThemePreviewController
}),
});
viewRegistry.add('theme_preview_form', ThemePreviewFormView);
return {
ThemePreviewControllerCommon: ThemePreviewControllerCommon
back() {
this.env.config.historyBack();
}
}
});
ThemePreviewFormController.components = { ...FormController.components, ViewButton };
class ThemePreviewFormControlPanel extends ControlPanel {
/**
* Triggers an event on the main bus.
* @see {FieldIframePreview} for the event handler.
*/
onMobileClick() {
this.env.bus.trigger('THEME_PREVIEW:SWITCH_MODE', {mode: 'mobile'});
}
/**
* @see {onMobileClick}
*/
onDesktopClick() {
this.env.bus.trigger('THEME_PREVIEW:SWITCH_MODE', {mode: 'desktop'});
}
}
ThemePreviewFormControlPanel.template = 'website.ThemePreviewForm.ControlPanel';
const ThemePreviewFormView = {
...formView,
display: {
controlPanel: {
'top-right': false,
'bottom-right': true,
}
},
buttonTemplate: 'website.ThemePreview.Buttons',
Controller: ThemePreviewFormController,
ControlPanel: ThemePreviewFormControlPanel,
};
registry.category('views').add('theme_preview_form', ThemePreviewFormView);
@@ -1,63 +1,45 @@
odoo.define('website.theme_preview_kanban', function (require) {
"use strict";
/** @odoo-module */
var KanbanController = require('web.KanbanController');
var KanbanView = require('web.KanbanView');
var ViewRegistry = require('web.view_registry');
const ThemePreviewControllerCommon = require('website.theme_preview_form').ThemePreviewControllerCommon;
var core = require('web.core');
var _lt = core._lt;
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { kanbanView } from "@web/views/kanban/kanban_view";
import { KanbanController } from "@web/views/kanban/kanban_controller";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { useLoaderOnClick } from './theme_preview_form';
var ThemePreviewKanbanController = KanbanController.extend(ThemePreviewControllerCommon, {
class ThemePreviewKanbanController extends KanbanController {
/**
* @override
*/
start: async function () {
await this._super(...arguments);
setup() {
super.setup();
useLoaderOnClick();
}
}
// hide pager
this.el.classList.add('o_view_kanban_theme_preview_controller');
class ThemePreviewControlPanel extends ControlPanel {
setup() {
super.setup();
this.website = useService('website');
}
close() {
this.website.goToWebsite();
}
}
ThemePreviewControlPanel.template = 'website.ThemePreviewKanban.ControlPanel';
// update breacrumb
const websiteLink = Object.assign(document.createElement('a'), {
className: 'btn btn-secondary ms-3 text-black-75',
href: '/web#action=website.website_preview',
innerHTML: '<i class="fa fa-close"></i>',
});
if (!this.initialState.context.module) { // not coming from res.config.settings
const smallBreadcumb = Object.assign(document.createElement('small'), {
className: 'mx-2 text-muted',
innerHTML: _lt("Don't worry, you can switch later."),
});
this._controlPanelWrapper.el.querySelector('.o_cp_top li').appendChild(smallBreadcumb);
this._controlPanelWrapper.el.querySelector('.o_cp_top').appendChild(websiteLink);
}
this._controlPanelWrapper.el.querySelector('.o_cp_top .breadcrumb li.active').classList.add('text-black-75');
const ThemePreviewKanbanView = {
...kanbanView,
Controller: ThemePreviewKanbanController,
ControlPanel: ThemePreviewControlPanel,
display: {
controlPanel: {
'bottom-left': false,
'bottom-right': false,
},
},
/**
* Called when user click on any button in kanban view.
* Targeted buttons are selected using name attribute value.
*
* @override
*/
_onButtonClicked: function (ev) {
const attrName = ev.data.attrs.name;
if (attrName === 'button_choose_theme' || attrName === 'button_refresh_theme') {
this._handleThemeAction(ev.data.record.res_id, attrName);
} else {
this._super(...arguments);
}
},
});
};
var ThemePreviewKanbanView = KanbanView.extend({
withSearchBar: false, // hide searchBar
config: _.extend({}, KanbanView.prototype.config, {
Controller: ThemePreviewKanbanController,
}),
});
ViewRegistry.add('theme_preview_kanban', ThemePreviewKanbanView);
});
registry.category('views').add('theme_preview_kanban', ThemePreviewKanbanView);
@@ -166,7 +166,7 @@
display: contents;
}
.o_legacy_kanban_view.o_theme_kanban {
.o_kanban_renderer.o_theme_kanban {
$o-theme-kanban-gray: #fcfcfc;
background-color: $o-theme-kanban-gray;
@@ -187,10 +187,18 @@
.o_kanban_record {
margin-left: $grid-gutter-width * 2;
padding: 0;
box-shadow: none;
background: none;
border: none;
margin-top: 16px;
margin-bottom: 16px;
.o_theme_preview {
box-shadow: none;
background: none;
border: none;
padding: 0;
&.o_theme_installed .o_theme_preview_top {
@include o-we-active-wrapper($top: 7px, $right: 7px);
}
}
.o_theme_preview_top {
position: relative;
@@ -267,10 +275,6 @@
transition: opacity 0.2s ease 0.1s;
}
}
&.o_theme_installed .o_theme_preview_top {
@include o-we-active-wrapper($top: 7px, $right: 7px);
}
}
/// Grouped Layout
@@ -327,40 +331,33 @@
}
.o_view_form_theme_preview_controller {
.o_control_panel > div:first-of-type {
display: none;
}
div.o_form_nosheet {
padding: 0px;
height:100%;
width:100%;
}
.is_mobile {
@include media-breakpoint-up(md) {
iframe {
.o_field_iframe{
width: 100%;
height: 100%;
div.is_mobile {
@include media-breakpoint-up(md) {
// mobile frame is rounded
border-radius: 15px;
height: 735px;
}
.img_mobile {
pointer-events: none;
display: block !important;
position: absolute;
top: 16px;
left: calc(50% - 200px)
}
.o_field_iframe_preview {
margin: auto !important;
padding: 53px 11px 58px 28px;
width: 416px;
iframe {
height: 735px;
border-radius: 15px;
}
.img_mobile {
pointer-events: none;
display: block;
position: absolute;
top: 16px;
left: calc(50% - 200px)
}
}
}
}
}
.o_view_kanban_theme_preview_controller {
.o_control_panel > div:nth-child(2) {
display: none;
}
}
@@ -78,7 +78,7 @@ wTourUtils.registerEditionTour('automatic_editor_on_new_website', {
},
{
content: "Install a theme",
trigger: 'button[data-name="button_choose_theme"]'
trigger: 'button[name="button_choose_theme"]'
},
{
content: "Check that the editor is loaded",
+2 -4
View File
@@ -325,7 +325,7 @@
<field name="display_name"/>
<field name="is_installed_on_current_website"/>
<templates>
<div t-name="kanban-box" t-attf-class="o_theme_preview mb16 mt16 #{record.is_installed_on_current_website.raw_value? 'o_theme_installed' : ''}">
<div t-name="kanban-box" t-attf-class="o_theme_preview #{record.is_installed_on_current_website.raw_value? 'o_theme_installed' : ''}">
<t t-set="has_image" t-value="record.image_ids.raw_value.length > 0"/>
<t t-set="has_screenshot" t-value="record.image_ids.raw_value.length > 1"/>
<t t-set="image_url" t-value="has_image ? '/web/image/' + record.image_ids.raw_value[0] : record.icon.value"/>
@@ -385,10 +385,8 @@
<field name="arch" type="xml">
<form create="false" edit="false" delete="0" js_class="theme_preview_form">
<div class="o_preview_frame h-100">
<field name='url' widget='iframe'/>
<img alt='phone' class='img_mobile' style='display:none' src="/website/static/src/img/phone.png"/>
<field name="url" widget='iframe' class="d-block m-0 h-100"/>
</div>
</form>
</field>
</record>