[IMP] website: add options on page list view

The goal of this commit is to add options to manage website pages as in
frontend page manager using list view.

- The click on list view item will redirect iframe to the targeted page.
- The Page Properties dialogs (DuplicatePageDialog, DeletePageDialog)
  are used to clone/remove pages.
- The pages listView is loaded using the current website domain.

See merge commit for more information.

task-2687506
This commit is contained in:
xO-Tx
2022-06-24 10:28:06 +02:00
committed by qsm-odoo
parent a8ca8372b6
commit 0db19449d0
11 changed files with 248 additions and 169 deletions
+1
View File
@@ -191,6 +191,7 @@
'website/static/src/js/new_content_form.js',
'website/static/src/js/utils.js',
'website/static/src/components/translator/*',
'website/static/src/js/website_page_list.js',
],
'web.qunit_suite_tests': [
'website/static/tests/dashboard_tests.js',
-44
View File
@@ -540,50 +540,6 @@ class Website(Home):
# Edit
# ------------------------------------------------------
@http.route(['/website/pages', '/website/pages/page/<int:page>'], type='http', auth="user", website=True)
def pages_management(self, page=1, sortby='url', search='', **kw):
# only website_designer should access the page Management
if not request.env.user.has_group('website.group_website_designer'):
raise werkzeug.exceptions.NotFound()
Page = request.env['website.page']
searchbar_sortings = {
'url': {'label': _('Sort by Url'), 'order': 'url'},
'name': {'label': _('Sort by Name'), 'order': 'name'},
}
# default sortby order
sort_order = searchbar_sortings.get(sortby, 'url')['order'] + ', website_id desc, id'
domain = request.website.website_domain()
if search:
domain += ['|', ('name', 'ilike', search), ('url', 'ilike', search)]
pages = Page.search(domain, order=sort_order)
if sortby != 'url' or not request.session.debug:
pages = pages._get_most_specific_pages()
pages_count = len(pages)
step = 50
pager = portal_pager(
url="/website/pages",
url_args={'sortby': sortby},
total=pages_count,
page=page,
step=step
)
pages = pages[(page - 1) * step:page * step]
values = {
'pager': pager,
'pages': pages,
'search': search,
'sortby': sortby,
'searchbar_sortings': searchbar_sortings,
'search_count': pages_count,
}
return request.render("website.list_website_pages", values)
@http.route(['/website/add', '/website/add/<path:path>'], type='http', auth="user", website=True, methods=['POST'])
def pagenew(self, path="", add_menu=False, template=False, **kwargs):
# for supported mimetype, get correct default template
+1 -5
View File
@@ -426,11 +426,7 @@ class View(models.Model):
return super(View, self).get_default_lang_code()
def redirect_to_page_manager(self):
return {
'type': 'ir.actions.act_url',
'url': '/website/pages',
'target': 'self',
}
return self.env["ir.actions.server"]._for_xml_id('website.action_website_pages_list')
def _read_template_keys(self):
return super(View, self)._read_template_keys() + ['website_id']
+10
View File
@@ -10,6 +10,7 @@ from odoo import api, fields, models
from odoo.osv import expression
from odoo.tools import escape_psql
from odoo.tools.safe_eval import safe_eval
from odoo.tools.translate import _
class Page(models.Model):
@@ -277,6 +278,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_manage_website_pages(self):
return {
'name': _('Website Pages'),
'type': 'ir.actions.act_window',
'res_model': 'website.page',
'view_mode': 'tree',
'view_id': self.env.ref('website.website_pages_tree_view').id,
}
# this is just a dummy function to be used as ormcache key
def _cached_response():
pass
@@ -5,6 +5,7 @@ import { useService } from '@web/core/utils/hooks';
import core from 'web.core';
import { WebsiteEditorComponent } from '../../components/editor/editor';
import { WebsiteTranslator } from '../../components/translator/translator';
import {OptimizeSEODialog} from '@website/components/dialog/seo';
const { Component, onWillStart, useRef, useEffect, useState } = owl;
@@ -46,6 +47,7 @@ BlockIframe.template = 'website.BlockIframe';
export class WebsitePreview extends Component {
setup() {
this.websiteService = useService('website');
this.dialogService = useService('dialog');
this.title = useService('title');
this.iframeFallbackUrl = '/website/iframefallback';
@@ -65,6 +67,12 @@ export class WebsitePreview extends Component {
this.websiteService.context.showNewContentModal = this.props.action.context.params && this.props.action.context.params.display_new_content;
this.websiteService.context.edition = this.props.action.context.params && !!this.props.action.context.params.enable_editor;
this.websiteService.context.translation = this.props.action.context.params && !!this.props.action.context.params.edit_translations;
if (this.props.action.context.params && this.props.action.context.params.enable_seo) {
this.iframe.el.addEventListener('load', () => {
this.websiteService.pageDocument = this.iframe.el.contentDocument;
this.dialogService.add(OptimizeSEODialog);
}, {once: true});
}
return () => {
this.websiteService.currentWebsiteId = null;
this.websiteService.websiteRootInstance = undefined;
@@ -91,5 +91,26 @@ FieldPageName.components = {PageDependencies};
FieldPageName.supportedFieldTypes = ['char'];
FieldPageName.template = 'website.FieldPageName';
/**
* 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('page_url', FieldPageUrl);
fieldRegistry.add('page_name', FieldPageName);
fieldRegistry.add('fa_prefix', FieldFaPrefix);
@@ -32,4 +32,8 @@
</div>
</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>
</templates>
@@ -0,0 +1,168 @@
/** @odoo-module **/
import ListController from 'web.ListController';
import viewRegistry from 'web.view_registry';
import ListView from 'web.ListView';
import {ComponentWrapper} from 'web.OwlCompatibility';
import {PagePropertiesDialogWrapper} from '@website/components/dialog/page_properties';
import {qweb} from 'web.core';
const WebsitePageListController = ListController.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @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);
}
},
/**
* Used to set the new dialog (created by PagePropertiesDialogWrapper for page
* record).
*/
setPageManagerDialog(dialog) {
this.pageManagerDialog = dialog;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @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);
this.pageManagerDialog.open();
break;
case 'action_clone_page':
await this._addDialog(record, 'clone');
this.pageManagerDialog.open();
break;
case 'action_delete_page':
await this._addDialog(record, 'delete');
this.pageManagerDialog.open();
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,
}
},
});
},
/**
* @private
*/
async _addDialog(record, mode = '') {
this._pagePropertiesDialog = new ComponentWrapper(this, PagePropertiesDialogWrapper, {
setPagePropertiesDialog: this.setPageManagerDialog.bind(this),
currentPage: record.data.id,
onClose: this._onCloseDialog.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]] : [],
});
}
});
const WebsitePageListView = ListView.extend({
config: _.extend({}, ListView.prototype.config, {
Controller: WebsitePageListController,
}),
});
viewRegistry.add('website_page_list', WebsitePageListView);
export default {
WebsitePageListController: WebsitePageListController,
WebsitePageListView: WebsitePageListView,
};
@@ -112,4 +112,17 @@
</a>
</t>
<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-toggle="dropdown" aria-expanded="false">
<span class="o_website_active" t-esc="active.name"/> <i class="fa fa-caret-down ml-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>
</templates>
-108
View File
@@ -2385,72 +2385,6 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
<span t-foreach="enumerate(parts)" t-as="part" t-att-class="'text-primary' if part[0] % 2 else None" t-esc="part[1]"/>
</template>
<template id="list_website_pages" name="Website Pages Management">
<t t-call="website.layout">
<div id="wrap">
<div class="container" id="list_website_pages">
<t t-call="website.website_search_box_input">
<t t-set="_form_classes" t-valuef="mt8 float-right"/>
<t t-set="search_type" t-valuef="pages"/>
<t t-set="action" t-valuef="/website/pages"/>
</t>
<div t-if="searchbar_sortings" class="dropdown float-right mt8 mr8">
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown">
<span class="fa fa-sort fa-lg" role="img" aria-label="Sort" title="Sort"/>
<span class="d-none d-xl-inline" t-esc="searchbar_sortings[sortby].get('label', 'Newest')"/>
</button>
<div class="dropdown-menu" aria-labelledby="portal_searchbar_sortby" role="menu">
<t t-foreach="searchbar_sortings" t-as="option">
<a role="menuitem"
t-att-href="request.httprequest.path + '?' + keep_query('*', sortby=option)"
t-attf-class="dropdown-item#{sortby == option and ' active' or ''}">
<span t-esc="searchbar_sortings[option].get('label')"/>
</a>
</t>
</div>
</div>
<h3 class="mt16">Manage Your Pages</h3>
<t t-if="not pages">
<div t-if="search" class="alert alert-warning mt8" role="alert">
Your search '<t t-esc="search" />' did not match any pages.
</div>
<div t-else="" class="alert alert-warning mt8" role="alert">
There are currently no pages for your website.
</div>
</t>
<div t-if="pages" class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Url</th>
<th class="text-center"><i title="Is the page included in the main menu?" class="fa fa-thumb-tack"></i></th>
<th class="text-center"><i title="Is the page published?" class="fa fa-eye"></i></th>
<th class="text-center"><i title="Is the page indexed by search engines?" class="fa fa-globe"></i></th>
<th class="text-center"><i title="Is the page SEO optimized?" class="fa fa-search"></i></th>
<th></th>
</tr>
</thead>
<t t-set='prev_page' t-value='False' />
<t t-set='page' t-value='pages[0]' />
<t t-foreach="pages[1:]" t-as="next_page">
<t t-call='website.one_page_line'/>
<t t-set='prev_page' t-value='page' />
<t t-set='page' t-value='next_page' />
</t>
<t t-set='next_page' t-value='False'/>
<t t-call='website.one_page_line' />
</table>
</div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="website.pager"/>
</div>
</div>
</div>
</t>
</template>
<template id="list_website_public_pages" name="Website Pages">
<t t-call="website.layout">
<div id="wrap">
@@ -2495,48 +2429,6 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
</t>
</template>
<template id="one_page_line">
<t t-set='specific_page' t-value="page.website_id"/>
<t t-set='final_page' t-value="(next_page and page.url != next_page.url) or not next_page or specific_page"/>
<tr t-att-style='not final_page and "color:#999"'>
<td>
<t t-if="debug">
<i t-if='specific_page and prev_page and prev_page.url == page.url and not prev_page.website_id' class="fa fa-level-up fa-rotate-90 ml32 mr4"/>
<i t-else="1" class="fa fa-globe mr4" title="Websites-shared page" t-att-style="'visibility:hidden;' if specific_page else ''"/>
</t>
<i t-if="page.is_homepage" class="fa fa-home" title="Homepage"/> <span t-esc="page.name"/>
</td>
<td>
<a t-if='final_page' t-att-href="page.url"><t t-esc="page.url"/></a>
</td>
<td class="text-center">
<i t-if="page.menu_ids" class="fa fa-check" title="In main menu"/>
<i t-else="" class="fa fa-times text-muted" title="Not in main menu"/>
</td>
<td class="text-center">
<t t-set='date_formatted'><t t-options='{"widget": "date"}' t-esc="page.date_publish"/></t>
<i t-if="page.is_visible" class="fa fa-check" title="Published"/>
<i t-elif="page.website_published" class="fa fa-eye-slash" t-attf-title="This page will be published on {{ date_formatted }}"/>
<i t-else="" class="fa fa-times text-muted" title="Not published"/>
</td>
<td class="text-center">
<i t-if="page.website_indexed" class="fa fa-check" title="Indexed"/>
<i t-else="" class="fa fa-times text-muted" title="Not indexed"/>
</td>
<td class="text-center">
<i t-if="page.is_seo_optimized" class="fa fa-check" title="SEO optimized"/>
<i t-else="" class="fa fa-times text-muted" title="Not SEO optimized"/>
</td>
<td class="text-right" style="white-space:nowrap;">
<a class="mr4 fa fa-cog js_page_properties" href="#" t-att-data-id="page.id" title="Manage this page"/>
<a class="mr4 fa fa-search" t-attf-href="{{ page.url}}?enable_seo" title="Optimize SEO of this page"/>
<a groups="base.group_no_one" class="mr4 fa fa-bug" t-attf-href="/web#id=#{page.view_id.id}&amp;view_type=form&amp;model=ir.ui.view" title="Edit code in backend"/>
<a class="mr4 fa fa-clone js_clone_page" t-att-data-id="page.id" href="#" title="Clone this page"/>
<a class="fa fa-trash js_delete_page" t-att-data-id="page.id" href="#" title="Delete this page"/>
</td>
</tr>
</template>
<template id="list_hybrid" name="Any Search Results">
<t t-call="website.layout">
<div id="wrap">
+22 -12
View File
@@ -174,16 +174,25 @@
<field name="name">website.page.list</field>
<field name="model">website.page</field>
<field name="arch" type="xml">
<tree string="Website Pages" default_order="name" multi_edit="1">
<field name="name"/>
<tree js_class="website_page_list" string="Website Pages" default_order="url" create="false">
<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="url"/>
<field name="website_id" groups="website.group_multi_website"/>
<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="create_uid" invisible="1"/>
<field name="write_uid"/>
<field name="write_date"/>
<field name="track"/>
<field name="write_uid" widget="many2one_avatar" optional="hidden"/>
<field name="write_date" optional="hidden"/>
<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"/>
</tree>
</field>
</record>
@@ -203,12 +212,13 @@
</field>
</record>
<record id="action_website_pages_list" model="ir.actions.act_window">
<field name="name">Website Pages</field>
<field name="res_model">website.page</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="website_pages_tree_view"/>
<field name="target">current</field>
<record id="action_website_pages_list" model="ir.actions.server">
<field name="name">Website: Pages</field>
<field name="model_id" ref="website.model_website_page"/>
<field name="state">code</field>
<field name="code">
action = model.action_manage_website_pages()
</field>
</record>
<!-- ====== website.menu views =========================================