[REF] website: adapt website pages list to Owl

After [1], the frontend website page manager was replaced by a custom
'website.page' list view and, with following commits on this PR, this
view will be used with other website content models records (blog,
event, forum, ...) too.

The goal of this commit is to adapt the pages list code to the new Owl
view system.

Other changes (related to the view's behavior) were added in this
commit:

- "Page list action buttons" are removed, we keep only the action that
triggers the form view of 'page.view_id' in debug mode. Page properties
and SEO dialog can be accessed via the website iframe after clicking on
the page entry in the list. The clone button should be later restored as
part of the main website backend menu, still when viewing the iframe.
Meanwhile, it is accessible in page properties. The 'delete' button is
still accessible via page properties too but the standard "delete"
button when selecting records in the list view will later be improved to
act the same way.

- The "website switcher" on top of the list view (to filter records
which impact one website, generic + specific records) is added as part
of the search view. Limitation: it will only be shown for pages at the
moment.

- A standard "Create" button is added and displays the form view dialog
to create a new content record as with "+ New". Known bug: it only
allows to create on the first website at the moment.

- The click on page/content record should redirect to the corresponding
url/website_url in iframe. This is now achieved via the new possibility
of defining an action on the <tree> tag in the view definition. Known
bugs/inconsistency: depending on the model, this opens in edit mode, in
a new tab, ... behavior to unify in a future update.

- The 'PagePropertiesDialogManager' is removed since the code is in Owl
and we don't need page properties dialog on the list view anymore.

Follows the merge of the "website in backend" task at [1].

[1]: https://github.com/odoo/odoo/commit/31cc10b91dc7762e23b4bde9b945be0c4ce3fe3b

task-2889981

Part-of: odoo/odoo#98937
This commit is contained in:
xO-Tx
2022-09-05 05:21:05 +02:00
committed by qsm-odoo
parent 5a949b6d1c
commit 940f4ee875
9 changed files with 165 additions and 252 deletions
+4 -2
View File
@@ -284,8 +284,10 @@ class WebsitePublishedMultiMixin(WebsitePublishedMixin):
def open_website_url(self):
client_action_url = self.env['website'].get_client_action_url(self.website_url)
if self.website_id.domain:
client_action_url = url_join(self.website_id.domain, f'{client_action_url}&website_id={self.website_id.id}')
if self.website_id:
client_action_url = f'{client_action_url}&website_id={self.website_id.id}'
if self.website_id.domain:
client_action_url = url_join(self.website_id.domain, client_action_url)
return {
'type': 'ir.actions.act_url',
'url': client_action_url,
+16
View File
@@ -83,6 +83,13 @@ class Page(models.Model):
# If the page is no longer in menu, we should remove its website_menu
page.menu_ids.unlink()
# This update was added to make sure the mixin calculations are correct
# (page.website_url > page.url).
@api.depends('url')
def _compute_website_url(self):
for page in self:
page.website_url = page.url
def _get_most_specific_pages(self):
''' Returns the most specific pages in self. '''
ids = []
@@ -277,6 +284,15 @@ class Page(models.Model):
url = '/' + slugify(page_url, max_length=1024, path=True)
return self.env['website'].with_context(website_id=website_id).get_unique_path(url)
def action_page_debug_view(self):
return {
'type': 'ir.actions.act_window',
'res_model': 'ir.ui.view',
'res_id': self.view_id.id,
'view_mode': 'form',
'view_id': self.env.ref('website.view_view_form_extend').id,
}
# this is just a dummy function to be used as ormcache key
def _cached_response():
@@ -7,7 +7,7 @@ import {WebsiteDialog} from './dialog';
import {FormViewDialog} from "@web/views/view_dialogs/form_view_dialog";
import {qweb, _t} from 'web.core';
const {Component, onWillStart, onRendered, useState, xml, useRef, markup} = owl;
const {Component, onWillStart, useState, xml, useRef, markup} = owl;
export class PageDependencies extends Component {
setup() {
@@ -209,54 +209,3 @@ PagePropertiesDialog.defaultProps = {
},
onClose: () => {},
};
// TODO remove this once the 'website.page' listView is adapted to OWL.
export class PagePropertiesDialogManager extends Component {
setup() {
this.dialog = useService('dialog');
onRendered(this.createDialog);
}
createDialog() {
switch (this.props.mode) {
case 'clone':
this.dialog.add(DuplicatePageDialog, {
pageId: this.props.resId,
onDuplicate: this.props.onClose,
});
break;
case 'delete':
this.dialog.add(DeletePageDialog, {
pageId: this.props.resId,
onDelete: this.props.onClose,
});
break;
default:
this.dialog.add(PagePropertiesDialog, {
resId: this.props.resId,
onClose: this.props.onClose,
onRecordSaved: this.props.onRecordSaved,
});
break;
}
}
}
PagePropertiesDialogManager.template = xml``;
PagePropertiesDialogManager.props = {
onClose: {
type: Function,
optional: true,
},
mode: {
type: String,
optional: true,
},
resId: {
type: [Number, Boolean],
optional: true,
},
onRecordSaved: {
type: Function,
optional: true,
}
};
@@ -5,8 +5,6 @@ import {standardFieldProps} from '@web/views/fields/standard_field_props';
import {useInputField} from '@web/views/fields/input_field_hook';
import {useService} from '@web/core/utils/hooks';
import {Switch} from '@website/components/switch/switch';
import AbstractFieldOwl from 'web.AbstractFieldOwl';
import fieldRegistry from 'web.field_registry_owl';
import {registry} from '@web/core/registry';
import {formatChar} from '@web/views/fields/formatters';
@@ -114,28 +112,6 @@ PageNameField.supportedTypes = ['char'];
registry.category("fields").add("page_name", PageNameField);
/**
* Displays 'char' field's value prefixed by a FA icon.
* The prefix is shown by default, but the visibility can be updated depending on
* other field value.
* e.g. `<field name="name" widget="fa_prefix" options="{'icon': 'fa-lock',
* 'visibility': 'is_locked'}"/>` renders the icon only when 'is_locked' is True.
*/
class FieldFaPrefix extends AbstractFieldOwl {
get prefix() {
const {icon, visibility, title} = this.nodeOptions;
return {
class: icon.split(' ').filter(str => str.indexOf('fa-') === 0).join(' '),
visible: !visibility || !!this.recordData[visibility],
help: title || '',
};
}
}
FieldFaPrefix.supportedFieldTypes = ['char'];
FieldFaPrefix.template = 'website.FieldFaPrefix';
fieldRegistry.add('fa_prefix', FieldFaPrefix);
/**
* Displays 'Selection' field's values as images to select.
* Image src for each value can be added using the option 'images' on field XML.
@@ -62,10 +62,6 @@
</t>
</t>
<t t-name="website.FieldFaPrefix" owl="1">
<span><i t-attf-class="fa fa-fw {{prefix.class}} {{prefix.visible ? '': 'invisible'}}" t-att-title="prefix.help"/><t t-esc="_formatValue(value)"/></span>
</t>
<t t-name="website.FieldImageRadio" owl="1">
<div class="d-flex">
<t t-foreach="values" t-as="option" t-key="option">
@@ -1,158 +1,109 @@
/** @odoo-module **/
import ListController from 'web.ListController';
import viewRegistry from 'web.view_registry';
import ListView from 'web.ListView';
import {ComponentWrapper} from 'web.OwlCompatibility';
import {PagePropertiesDialogManager} from '@website/components/dialog/page_properties';
import {qweb} from 'web.core';
import {AddPageDialog} from "../dialog/dialog";
import {registry} from '@web/core/registry';
import {listView} from '@web/views/list/list_view';
import {useService} from "@web/core/utils/hooks";
import { csrf_token } from 'web.core';
const WebsitePageListController = ListController.extend({
const {onWillStart, useState} = owl;
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
export class PageListController extends listView.Controller {
/**
* @override
*/
willStart() {
return Promise.all([this._super(...arguments), this._getRenderContext()]);
},
/**
* @override
*/
renderButtons($node) {
this.$buttons = $(qweb.render('PageListView.WebsiteSelect', {
websites: this.websites,
active: this._getWebsite(0),
}));
this.$buttons.find('.dropdown-item').click(this._onButtonClick.bind(this));
if ($node) {
this.$buttons.appendTo($node);
}
},
setup() {
super.setup();
this.website = useService('website');
this.http = useService('http');
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
this.websiteSelection = [{id: 0, name: this.env._t("All Websites")}];
/**
* @override
*/
async _callButtonAction(attrs, record) {
switch (attrs.name) {
case 'action_optimize_seo':
this._goToPage(record.data.url, record.data.website_id.res_id, {
enable_seo: true,
});
break;
case 'action_manage_page':
await this._addDialog(record);
break;
case 'action_clone_page':
await this._addDialog(record, 'clone');
break;
case 'action_delete_page':
await this._addDialog(record, 'delete');
break;
case 'action_edit_page':
this.do_action({
type: 'ir.actions.act_window',
res_model: 'ir.ui.view',
res_id: record.data.view_id.res_id,
views: [[false, 'form']],
});
break;
default:
return this._super(...arguments);
}
},
/**
* @private
*/
_goToPage(path, website, options = {}) {
this.do_action('website.website_preview', {
additional_context: {
params: {
path: path,
website_id: website || '',
...options,
}
},
this.state = useState({
activeWebsite: this.websiteSelection[0],
});
},
/**
* @private
*/
async _addDialog(record, mode = '') {
this._pagePropertiesDialog = new ComponentWrapper(this, PagePropertiesDialogManager, {
resId: record.data.id,
onClose: this._onCloseDialog.bind(this),
onRecordSaved: this.reload.bind(this),
mode: mode,
});
await this._pagePropertiesDialog.mount(this.el);
},
/**
* Removes page properties wrapper when dialog is closed.
*
* @private
*/
_onCloseDialog() {
if (this._pagePropertiesDialog) {
this._pagePropertiesDialog.destroy();
}
this._pagePropertiesDialog = undefined;
},
/**
* @private
*/
_getRenderContext() {
return this._rpc({
model: 'website',
method: 'search_read',
fields: ['id', 'name'],
}).then((result) => {
this.websites = [{id: 0, name: 'All Websites'}, ...result];
});
},
/**
* @private
*/
_getWebsite(id) {
return this.websites.find(website => website.id === id);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @override
*/
_onOpenRecord(event) {
const record = this.model.get(event.data.id, {raw: true});
this._goToPage(record.data.url, record.data.website_id);
},
_onButtonClick: async function (event) {
const activeWebsite = this._getWebsite(parseInt(event.target.dataset.websiteId));
this.$buttons[0].querySelector('.o_website_active').textContent = activeWebsite.name;
this.reload({
domain: activeWebsite.id ? [['website_id', '=', activeWebsite.id]] : [],
onWillStart(async () => {
await this.website.fetchWebsites();
this.websiteSelection.push(...this.website.websites);
});
}
});
const WebsitePageListView = ListView.extend({
config: _.extend({}, ListView.prototype.config, {
Controller: WebsitePageListController,
}),
});
/**
* @override
*/
onClickCreate() {
if (this.props.resModel === 'website.page') {
return this.dialogService.add(AddPageDialog, {
addPage: async (name, addMenu) => {
// TODO this is duplicated code from new_content.js, this
// should be shared somehow.
// FIXME this always create on website 1 whatever we do.
const url = `/website/add/${encodeURIComponent(name)}`;
const data = await this.http.post(url, { 'add_menu': addMenu || '', csrf_token });
if (data.view_id) {
this.actionService.doAction({
'res_model': 'ir.ui.view',
'res_id': data.view_id,
'views': [[false, 'form']],
'type': 'ir.actions.act_window',
'view_mode': 'form',
});
} else {
this.website.goToWebsite({ path: data.url, edition: true });
}
},
});
}
const action = this.props.context.create_action;
if (action) {
if (/^\//.test(action)) {
window.location.replace(action);
return;
}
this.actionService.doAction(action, {
onClose: (data) => {
if (data) {
this.website.goToWebsite({path: data.path});
}
},
});
}
}
viewRegistry.add('website_page_list', WebsitePageListView);
onSelectWebsite(website) {
this.state.activeWebsite = website;
}
}
PageListController.template = `website.PageListView`;
export default {
WebsitePageListController: WebsitePageListController,
WebsitePageListView: WebsitePageListView,
export class PageListRenderer extends listView.Renderer {
/**
* The goal here is to tweak the renderer to display pages following some
* rules:
* - All websites (props.activeWebsite.id === 0):
* -> Show all generic/specific pages.
* - A website is selected:
* -> Display website-specific pages & generic ones (only those without
* specific clones).
*/
pageFilter(record, records) {
return !this.props.activeWebsite.id
|| this.props.activeWebsite.id === record.data.website_id[0]
|| !record.data.website_id[0] && records.filter(rec => rec.data.url === record.data.url).length === 1;
}
}
PageListRenderer.props = [
...listView.Renderer.props,
"activeWebsite",
];
PageListRenderer.recordRowTemplate = "website.PageListRenderer.RecordRow";
export const PageListView = {
...listView,
Renderer: PageListRenderer,
Controller: PageListController,
};
registry.category("views").add("website_pages_list", PageListView);
@@ -1,18 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="PageListView.WebsiteSelect">
<div class="btn-group" role="toolbar" aria-label="Main actions">
<button class="btn btn-primary dropdown-toggle o-no-caret" data-bs-toggle="dropdown" aria-expanded="false">
<span class="o_website_active" t-esc="active.name"/> <i class="fa fa-caret-down ms-1"/>
</button>
<div class="dropdown-menu">
<t t-foreach="websites" t-as="website">
<a role="menuitem" href="#" class="dropdown-item" t-att-data-website-id="website.id"><t t-esc="website.name"/></a>
<div t-if="!website.id" class="dropdown-divider"/>
</t>
</div>
</div>
<t t-name="website.PageListRenderer.RecordRow" t-inherit="web.ListRenderer.RecordRow" owl="1">
<xpath expr="//Field//ancestor::td/*[1]" position="before">
<t t-if="column.name === 'name' and record.data.is_homepage">
<i class="fa fa-fw fa-home fa-lg" title="Home page of the current website"/>
</t>
</xpath>
<xpath expr="//tr[@class='o_data_row']" position="attributes">
<attribute name="t-if">pageFilter(record, list.records)</attribute>
</xpath>
</t>
<t t-name="website.PageListView" t-inherit="web.ListView" owl="1">
<xpath expr="//t[@t-component='props.Renderer']" position="attributes">
<attribute name="activeWebsite">state.activeWebsite</attribute>
</xpath>
<xpath expr="//Layout" position="inside">
<t t-set-slot="control-panel-website-extra-actions" t-if="props.resModel === 'website.page'">
<div class="btn-group" role="toolbar" aria-label="Main actions">
<button class="btn btn-light dropdown-toggle o-no-caret" data-bs-toggle="dropdown" aria-expanded="false">
<i class="me-1 fa fa-globe"/><span t-esc="state.activeWebsite.name"/>
</button>
<div class="dropdown-menu">
<t t-foreach="websiteSelection" t-as="website" t-key="website">
<a role="menuitem" href="#" class="dropdown-item" t-on-click="() => this.onSelectWebsite(website)"><t t-esc="website.name"/></a>
<div t-if="!website.id" class="dropdown-divider"/>
</t>
</div>
</div>
</t>
</xpath>
</t>
<t t-name="website.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_search_options')]/*[last()]" position="after">
<t t-slot="control-panel-website-extra-actions"/>
</xpath>
</t>
</templates>
@@ -148,14 +148,14 @@ export class NewContentModal extends Component {
this.dialogs.add(AddPageDialog, {
addPage: async (name, addMenu) => {
const url = `/website/add/${encodeURIComponent(name)}`;
const data = await this.http.post(url, { add_menu: addMenu || '', csrf_token });
const data = await this.http.post(url, { 'add_menu': addMenu || '', csrf_token });
if (data.view_id) {
this.action.doAction({
res_model: 'ir.ui.view',
res_id: data.view_id,
views: [[false, 'form']],
type: 'ir.actions.act_window',
view_mode: 'form',
'res_model': 'ir.ui.view',
'res_id': data.view_id,
'views': [[false, 'form']],
'type': 'ir.actions.act_window',
'view_mode': 'form',
});
} else {
this.website.goToWebsite({ path: data.url, edition: true });
+11 -12
View File
@@ -74,25 +74,24 @@
<field name="name">website.page.list</field>
<field name="model">website.page</field>
<field name="arch" type="xml">
<tree js_class="website_page_list" string="Website Pages" default_order="url" create="false">
<tree js_class="website_pages_list" type="object" action="open_website_url" default_order="url">
<field name="is_homepage" invisible="1"/>
<field name="name" string="Page Title" widget="fa_prefix" options="{'icon': 'fa-home fa-lg', 'visibility': 'is_homepage', 'title': 'Home page of the current website'}"/>
<field name="name" string="Page Title"/>
<field name="url"/>
<field name="view_id" invisible="1"/>
<field name="website_indexed"/>
<field name="is_in_menu" string="Is In Main Menu"/>
<field name="is_seo_optimized" string="Is SEO optimized"/>
<field name="is_published" string="Is Published"/>
<field name="is_seo_optimized"/>
<field name="is_published"/>
<field name="create_uid" invisible="1"/>
<field name="write_uid" widget="many2one_avatar" optional="hidden"/>
<field name="write_date" optional="hidden"/>
<field name="write_uid" widget="many2one_avatar" optional="hide"/>
<field name="write_date" optional="hide"/>
<field name="track" optional="hide"/>
<field name="website_id" groups="website.group_multi_website"/>
<field name="track" optional="hidden"/>
<button name="action_manage_page" title="Manage this page" icon="fa-cog fa-lg"/>
<button name="action_optimize_seo" title="Optimize SEO of this page" icon="fa-search fa-lg"/>
<button name="action_edit_page" title="Edit code in backend" icon="fa-bug fa-lg" groups="base.group_no_one"/>
<button name="action_clone_page" title="Clone this page" icon="fa-clone fa-lg"/>
<button name="action_delete_page" title="Delete this page" icon="fa-trash fa-lg"/>
<button name="action_page_debug_view" type="object" title="Settings" icon="fa-bug fa-lg" groups="base.group_no_one"/>
</tree>
</field>
</record>