[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:
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user