From 940f4ee875332dafa1f379970a7683be6b3ee606 Mon Sep 17 00:00:00 2001 From: xO-Tx Date: Thu, 1 Sep 2022 16:12:42 +0000 Subject: [PATCH] [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 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 --- addons/website/models/mixins.py | 6 +- addons/website/models/website_page.py | 16 ++ .../src/components/dialog/page_properties.js | 53 +--- .../static/src/components/fields/fields.js | 24 -- .../static/src/components/fields/fields.xml | 4 - .../static/src/components/views/page_list.js | 231 +++++++----------- .../static/src/components/views/page_list.xml | 48 +++- .../static/src/systray_items/new_content.js | 12 +- addons/website/views/website_pages_views.xml | 23 +- 9 files changed, 165 insertions(+), 252 deletions(-) diff --git a/addons/website/models/mixins.py b/addons/website/models/mixins.py index 93786227252..ab64ea80d7d 100644 --- a/addons/website/models/mixins.py +++ b/addons/website/models/mixins.py @@ -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, diff --git a/addons/website/models/website_page.py b/addons/website/models/website_page.py index f742e24775d..e789119974e 100644 --- a/addons/website/models/website_page.py +++ b/addons/website/models/website_page.py @@ -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(): diff --git a/addons/website/static/src/components/dialog/page_properties.js b/addons/website/static/src/components/dialog/page_properties.js index 3011d23013a..28bf4437a84 100644 --- a/addons/website/static/src/components/dialog/page_properties.js +++ b/addons/website/static/src/components/dialog/page_properties.js @@ -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, - } -}; diff --git a/addons/website/static/src/components/fields/fields.js b/addons/website/static/src/components/fields/fields.js index 35aae70d298..0905895f91e 100644 --- a/addons/website/static/src/components/fields/fields.js +++ b/addons/website/static/src/components/fields/fields.js @@ -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. `` 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. diff --git a/addons/website/static/src/components/fields/fields.xml b/addons/website/static/src/components/fields/fields.xml index 2364ade9278..eec1073bb0c 100644 --- a/addons/website/static/src/components/fields/fields.xml +++ b/addons/website/static/src/components/fields/fields.xml @@ -62,10 +62,6 @@ - - - -
diff --git a/addons/website/static/src/components/views/page_list.js b/addons/website/static/src/components/views/page_list.js index fe6917063e9..656841201dd 100644 --- a/addons/website/static/src/components/views/page_list.js +++ b/addons/website/static/src/components/views/page_list.js @@ -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); diff --git a/addons/website/static/src/components/views/page_list.xml b/addons/website/static/src/components/views/page_list.xml index 49bc995364d..9949e251d05 100644 --- a/addons/website/static/src/components/views/page_list.xml +++ b/addons/website/static/src/components/views/page_list.xml @@ -1,18 +1,42 @@ - -