diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index a4a0231ca7d..529496a017c 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -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', diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index cc77158065f..450ced02ca1 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -540,50 +540,6 @@ class Website(Home): # Edit # ------------------------------------------------------ - @http.route(['/website/pages', '/website/pages/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/'], 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 diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index 5463e6651a6..5f3a7664319 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -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'] diff --git a/addons/website/models/website_page.py b/addons/website/models/website_page.py index 7633ad0c6fc..804276c8741 100644 --- a/addons/website/models/website_page.py +++ b/addons/website/models/website_page.py @@ -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 diff --git a/addons/website/static/src/client_actions/website_preview/website_preview.js b/addons/website/static/src/client_actions/website_preview/website_preview.js index 11d12266f05..ab6909d3125 100644 --- a/addons/website/static/src/client_actions/website_preview/website_preview.js +++ b/addons/website/static/src/client_actions/website_preview/website_preview.js @@ -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; diff --git a/addons/website/static/src/components/fields/fields.js b/addons/website/static/src/components/fields/fields.js index 93d26d40a4a..5d8a5b42baa 100644 --- a/addons/website/static/src/components/fields/fields.js +++ b/addons/website/static/src/components/fields/fields.js @@ -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. `` 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); diff --git a/addons/website/static/src/components/fields/fields.xml b/addons/website/static/src/components/fields/fields.xml index 9e9ebf0ed6b..e2df315f39e 100644 --- a/addons/website/static/src/components/fields/fields.xml +++ b/addons/website/static/src/components/fields/fields.xml @@ -32,4 +32,8 @@ + + + + diff --git a/addons/website/static/src/js/website_page_list.js b/addons/website/static/src/js/website_page_list.js new file mode 100644 index 00000000000..af352deb484 --- /dev/null +++ b/addons/website/static/src/js/website_page_list.js @@ -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, +}; diff --git a/addons/website/static/src/xml/website.backend.xml b/addons/website/static/src/xml/website.backend.xml index c54f98dd540..2f6cba28376 100644 --- a/addons/website/static/src/xml/website.backend.xml +++ b/addons/website/static/src/xml/website.backend.xml @@ -112,4 +112,17 @@ + +