From 7c5612defc02d0fc6d57733c6b61726faff97360 Mon Sep 17 00:00:00 2001 From: "Robin Lejeune (role)" Date: Mon, 24 Mar 2025 16:22:48 +0100 Subject: [PATCH] Add New Content systray --- .../website_preview/install_module_dialog.js | 23 ++ .../website_preview/install_module_dialog.xml | 13 + .../website_preview/new_content_element.js | 31 +++ .../website_preview/new_content_element.xml | 18 ++ .../src/website_preview/new_content_modal.js | 238 ++++++++++++++++++ .../src/website_preview/new_content_modal.xml | 30 +++ .../new_content_systray_item.js | 20 ++ .../new_content_systray_item.xml | 11 + .../website_preview/website_builder_action.js | 9 +- .../website_preview/website_systray_item.js | 2 + .../website_preview/website_systray_item.xml | 2 +- .../static/tests/translation.test.js | 2 + .../static/src/systray_items/new_content.scss | 2 +- 13 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 addons/html_builder/static/src/website_preview/install_module_dialog.js create mode 100644 addons/html_builder/static/src/website_preview/install_module_dialog.xml create mode 100644 addons/html_builder/static/src/website_preview/new_content_element.js create mode 100644 addons/html_builder/static/src/website_preview/new_content_element.xml create mode 100644 addons/html_builder/static/src/website_preview/new_content_modal.js create mode 100644 addons/html_builder/static/src/website_preview/new_content_modal.xml create mode 100644 addons/html_builder/static/src/website_preview/new_content_systray_item.js create mode 100644 addons/html_builder/static/src/website_preview/new_content_systray_item.xml diff --git a/addons/html_builder/static/src/website_preview/install_module_dialog.js b/addons/html_builder/static/src/website_preview/install_module_dialog.js new file mode 100644 index 0000000000000..153a16128ac89 --- /dev/null +++ b/addons/html_builder/static/src/website_preview/install_module_dialog.js @@ -0,0 +1,23 @@ +import { Component } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; +import { WebsiteDialog } from "@website/components/dialog/dialog"; + +export class InstallModuleDialog extends Component { + static components = { WebsiteDialog }; + static template = "html_builder.InstallModuleDialog"; + static props = { + title: String, + installationText: String, + installModule: Function, + close: Function, + }; + + setup() { + this.installButtonTitle = _t("Install"); + } + + onClickInstall() { + this.props.close(); + this.props.installModule(); + } +} diff --git a/addons/html_builder/static/src/website_preview/install_module_dialog.xml b/addons/html_builder/static/src/website_preview/install_module_dialog.xml new file mode 100644 index 0000000000000..8eb21d3de4843 --- /dev/null +++ b/addons/html_builder/static/src/website_preview/install_module_dialog.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/addons/html_builder/static/src/website_preview/new_content_element.js b/addons/html_builder/static/src/website_preview/new_content_element.js new file mode 100644 index 0000000000000..292cd90648829 --- /dev/null +++ b/addons/html_builder/static/src/website_preview/new_content_element.js @@ -0,0 +1,31 @@ +import { Component } from "@odoo/owl"; + +export const MODULE_STATUS = { + NOT_INSTALLED: "NOT_INSTALLED", + INSTALLING: "INSTALLING", + FAILED_TO_INSTALL: "FAILED_TO_INSTALL", + INSTALLED: "INSTALLED", +}; + +export class NewContentElement extends Component { + static template = "html_builder.NewContentElement"; + static props = { + name: { type: String, optional: true }, + title: String, + onClick: Function, + status: { type: String, optional: true }, + moduleXmlId: { type: String, optional: true }, + slots: Object, + }; + static defaultProps = { + status: MODULE_STATUS.INSTALLED, + }; + + setup() { + this.MODULE_STATUS = MODULE_STATUS; + } + + onClick(ev) { + this.props.onClick(); + } +} diff --git a/addons/html_builder/static/src/website_preview/new_content_element.xml b/addons/html_builder/static/src/website_preview/new_content_element.xml new file mode 100644 index 0000000000000..3d34e69999b5b --- /dev/null +++ b/addons/html_builder/static/src/website_preview/new_content_element.xml @@ -0,0 +1,18 @@ + + + + +
+ +
+
+ +
diff --git a/addons/html_builder/static/src/website_preview/new_content_modal.js b/addons/html_builder/static/src/website_preview/new_content_modal.js new file mode 100644 index 0000000000000..1e7104a9396b6 --- /dev/null +++ b/addons/html_builder/static/src/website_preview/new_content_modal.js @@ -0,0 +1,238 @@ +import { InstallModuleDialog } from "@html_builder/website_preview/install_module_dialog"; +import { + MODULE_STATUS, + NewContentElement, +} from "@html_builder/website_preview/new_content_element"; +import { Component, onWillStart, useState, xml } from "@odoo/owl"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; +import { _t } from "@web/core/l10n/translation"; +import { rpc } from "@web/core/network/rpc"; +import { useActiveElement } from "@web/core/ui/ui_service"; +import { user } from "@web/core/user"; +import { useService } from "@web/core/utils/hooks"; +import { sprintf } from "@web/core/utils/strings"; +import { redirect } from "@web/core/utils/urls"; + +export class NewContentModal extends Component { + static template = "html_builder.NewContentModal"; + static components = { NewContentElement }; + static props = { + onNewPage: Function, + }; + + setup() { + this.orm = useService("orm"); + this.dialogs = useService("dialog"); + this.website = useService("website"); + this.action = useService("action"); + this.isSystem = user.isSystem; + useActiveElement("modalRef"); + + this.newContentText = { + failed: _t('Failed to install "%s"'), + installInProgress: _t("The installation of an App is already in progress."), + installNeeded: _t('Do you want to install the "%s" App?'), + installPleaseWait: _t('Installing "%s"'), + }; + + this.state = useState({ + newContentElements: [ + { + moduleName: "website_blog", + moduleXmlId: "base.module_website_blog", + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: _t("Blog Post"), + }, + { + moduleName: "website_event", + moduleXmlId: "base.module_website_event", + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: _t("Event"), + }, + { + moduleName: "website_forum", + moduleXmlId: "base.module_website_forum", + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + redirectUrl: "/forum", + title: _t("Forum"), + }, + { + moduleName: "website_hr_recruitment", + moduleXmlId: "base.module_website_hr_recruitment", + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: _t("Job Position"), + }, + { + moduleName: "website_sale", + moduleXmlId: "base.module_website_sale", + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: _t("Product"), + }, + { + moduleName: "website_slides", + moduleXmlId: "base.module_website_slides", + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: _t("Course"), + }, + { + moduleName: "website_livechat", + moduleXmlId: "base.module_website_livechat", + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: _t("Livechat Widget"), + redirectUrl: "/livechat", + }, + ], + }); + + this.websiteContext = useState(this.website.context); + useHotkey("escape", () => { + if (this.websiteContext.showNewContentModal) { + this.websiteContext.showNewContentModal = false; + } + }); + + onWillStart(this.onWillStart.bind(this)); + } + + async onWillStart() { + this.isDesigner = await user.hasGroup("website.group_website_designer"); + this.canInstall = await user.isAdmin; + if (this.canInstall) { + const moduleNames = this.state.newContentElements + .filter(({ status }) => status === MODULE_STATUS.NOT_INSTALLED) + .map(({ moduleName }) => moduleName); + this.modulesInfo = {}; + for (const record of await this.orm.searchRead( + "ir.module.module", + [["name", "in", moduleNames]], + ["id", "name", "shortdesc"] + )) { + this.modulesInfo[record.name] = { id: record.id, name: record.shortdesc }; + } + } + const modelsToCheck = []; + const elementsToUpdate = {}; + for (const element of this.state.newContentElements) { + if (element.model) { + modelsToCheck.push(element.model); + elementsToUpdate[element.model] = element; + } + } + const accesses = await rpc("/website/check_new_content_access_rights", { + models: modelsToCheck, + }); + for (const [model, access] of Object.entries(accesses)) { + elementsToUpdate[model].isDisplayed = access; + } + } + + get sortedNewContentElements() { + return this.state.newContentElements + .filter(({ status }) => status !== MODULE_STATUS.NOT_INSTALLED) + .concat( + this.state.newContentElements.filter( + ({ status }) => status === MODULE_STATUS.NOT_INSTALLED + ) + ); + } + + async installModule(id, redirectUrl) { + await this.orm.silent.call("ir.module.module", "button_immediate_install", [id]); + if (redirectUrl) { + this.website.prepareOutLoader(); + window.location.replace(redirectUrl); + } else { + const { + id, + metadata: { path, viewXmlid }, + } = this.website.currentWebsite; + const url = new URL(path); + if (viewXmlid === "website.page_404") { + url.pathname = ""; + } + // A reload is needed after installing a new module, to instantiate + // a NewContentModal with patches from the installed module. + this.website.prepareOutLoader(); + redirect( + `/odoo/action-website.website_preview?website_id=${id}&path=${encodeURIComponent( + url.toString() + )}&display_new_content=true` + ); + } + } + + onClickNewContent(element) { + if (element.createNewContent) { + return element.createNewContent(); + } + + const { id, name } = this.modulesInfo[element.moduleName]; + const dialogProps = { + title: element.title, + installationText: sprintf(this.newContentText.installNeeded, name), + installModule: async () => { + // Update the NewContentElement with installing icon and text. + this.state.newContentElements = this.state.newContentElements.map((el) => { + if (el.moduleXmlId === element.moduleXmlId) { + el.status = MODULE_STATUS.INSTALLING; + el.icon = xml``; + el.title = sprintf(this.newContentText.installPleaseWait, name); + } + return el; + }); + this.website.showLoader({ title: _t("Building your %s", name) }); + try { + await this.installModule(id, element.redirectUrl); + } catch (error) { + this.website.hideLoader(); + // Update the NewContentElement with failure icon and text. + this.state.newContentElements = this.state.newContentElements.map((el) => { + if (el.moduleXmlId === element.moduleXmlId) { + el.status = MODULE_STATUS.FAILED_TO_INSTALL; + el.icon = xml``; + el.title = sprintf(this.newContentText.failed, name); + } + return el; + }); + console.error(error); + } + }, + }; + this.dialogs.add(InstallModuleDialog, dialogProps); + } + + /** + * This method registers the action to perform when a new content is + * saved. The path must be computed once the record is saved, to + * perform the 'ir.act_window_close' action, which will be used when + * the dialog is closed to go to the correct website page. + */ + async onAddContent(action, edition = false, context = null) { + this.action.doAction(action, { + additionalContext: context ? context : {}, + onClose: (infos) => { + if (infos) { + this.website.goToWebsite({ path: infos.path, edition: edition }); + } + }, + props: { + onSave: (record, params) => { + if (record.resId) { + const path = params.computePath(); + this.action.doAction({ + type: "ir.actions.act_window_close", + infos: { path }, + }); + } + }, + }, + }); + } +} diff --git a/addons/html_builder/static/src/website_preview/new_content_modal.xml b/addons/html_builder/static/src/website_preview/new_content_modal.xml new file mode 100644 index 0000000000000..843c308e8c612 --- /dev/null +++ b/addons/html_builder/static/src/website_preview/new_content_modal.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/addons/html_builder/static/src/website_preview/new_content_systray_item.js b/addons/html_builder/static/src/website_preview/new_content_systray_item.js new file mode 100644 index 0000000000000..1aa1ffcd95a58 --- /dev/null +++ b/addons/html_builder/static/src/website_preview/new_content_systray_item.js @@ -0,0 +1,20 @@ +import { NewContentModal } from "@html_builder/website_preview/new_content_modal"; +import { Component, useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +export class NewContentSystrayItem extends Component { + static template = "html_builder.NewContentSystrayItem"; + static components = { NewContentModal }; + static props = { + onNewPage: Function, + }; + + setup() { + this.website = useService("website"); + this.websiteContext = useState(this.website.context); + } + + onClick() { + this.websiteContext.showNewContentModal = !this.websiteContext.showNewContentModal; + } +} diff --git a/addons/html_builder/static/src/website_preview/new_content_systray_item.xml b/addons/html_builder/static/src/website_preview/new_content_systray_item.xml new file mode 100644 index 0000000000000..c96c180fd6c97 --- /dev/null +++ b/addons/html_builder/static/src/website_preview/new_content_systray_item.xml @@ -0,0 +1,11 @@ + + + + +
+ + +
+
+ +
diff --git a/addons/html_builder/static/src/website_preview/website_builder_action.js b/addons/html_builder/static/src/website_preview/website_builder_action.js index e4b255aaf08c8..60585d59e27ab 100644 --- a/addons/html_builder/static/src/website_preview/website_builder_action.js +++ b/addons/html_builder/static/src/website_preview/website_builder_action.js @@ -15,6 +15,7 @@ import { WebsiteSystrayItem } from "./website_systray_item"; import { uniqueId } from "@web/core/utils/functions"; import { LocalOverlayContainer } from "@html_editor/local_overlay_container"; import { _t } from "@web/core/l10n/translation"; +import { AddPageDialog } from "@website/components/dialog/add_page_dialog"; export class WebsiteBuilder extends Component { static template = "html_builder.WebsiteBuilder"; @@ -25,6 +26,7 @@ export class WebsiteBuilder extends Component { this.target = null; this.orm = useService("orm"); this.notification = useService("notification"); + this.dialog = useService("dialog"); this.websiteContent = useRef("iframe"); useSubEnv({ builderRef: useRef("container"), @@ -107,7 +109,12 @@ export class WebsiteBuilder extends Component { } onNewPage() { - console.log("todo: new page"); + this.dialog.add(AddPageDialog, { + onAddPage: () => { + this.websiteService.context.showNewContentModal = false; + }, + websiteId: this.websiteService.currentWebsite.id, + }); } async onEditPage() { diff --git a/addons/html_builder/static/src/website_preview/website_systray_item.js b/addons/html_builder/static/src/website_preview/website_systray_item.js index 713edc3e35828..6fb9de5a70d73 100644 --- a/addons/html_builder/static/src/website_preview/website_systray_item.js +++ b/addons/html_builder/static/src/website_preview/website_systray_item.js @@ -1,3 +1,4 @@ +import { NewContentSystrayItem } from "./new_content_systray_item"; import { EditWebsiteSystrayItem } from "./edit_website_systray_item"; import { Component, onWillStart } from "@odoo/owl"; @@ -9,6 +10,7 @@ export class WebsiteSystrayItem extends Component { iframeLoaded: { type: Object }, }; static components = { + NewContentSystrayItem, EditWebsiteSystrayItem, }; diff --git a/addons/html_builder/static/src/website_preview/website_systray_item.xml b/addons/html_builder/static/src/website_preview/website_systray_item.xml index 8d7347663c0d4..2480b88e2d7c3 100644 --- a/addons/html_builder/static/src/website_preview/website_systray_item.xml +++ b/addons/html_builder/static/src/website_preview/website_systray_item.xml @@ -3,7 +3,7 @@
- New +
diff --git a/addons/html_builder/static/tests/translation.test.js b/addons/html_builder/static/tests/translation.test.js index e381981636b81..4063a1088f707 100644 --- a/addons/html_builder/static/tests/translation.test.js +++ b/addons/html_builder/static/tests/translation.test.js @@ -19,6 +19,8 @@ const websiteServiceInTranslateMode = { defaultLangName: "English (US)", }, }, + // Minimal context to avoid crashes. + context: { showNewContentModal: false }, }; test("systray in translate mode", async () => { diff --git a/addons/website/static/src/systray_items/new_content.scss b/addons/website/static/src/systray_items/new_content.scss index 7b2729a52c9c4..5e1593f92fc7b 100644 --- a/addons/website/static/src/systray_items/new_content.scss +++ b/addons/website/static/src/systray_items/new_content.scss @@ -48,7 +48,7 @@ margin: auto; } - a { + button { display: block; font-size: 34px; text-align: center;