|
| 1 | +import { InstallModuleDialog } from "@html_builder/website_preview/install_module_dialog"; |
| 2 | +import { |
| 3 | + MODULE_STATUS, |
| 4 | + NewContentElement, |
| 5 | +} from "@html_builder/website_preview/new_content_element"; |
| 6 | +import { Component, onWillStart, useState, xml } from "@odoo/owl"; |
| 7 | +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; |
| 8 | +import { _t } from "@web/core/l10n/translation"; |
| 9 | +import { rpc } from "@web/core/network/rpc"; |
| 10 | +import { useActiveElement } from "@web/core/ui/ui_service"; |
| 11 | +import { user } from "@web/core/user"; |
| 12 | +import { useService } from "@web/core/utils/hooks"; |
| 13 | +import { sprintf } from "@web/core/utils/strings"; |
| 14 | +import { redirect } from "@web/core/utils/urls"; |
| 15 | + |
| 16 | +export class NewContentModal extends Component { |
| 17 | + static template = "html_builder.NewContentModal"; |
| 18 | + static components = { NewContentElement }; |
| 19 | + static props = { |
| 20 | + onNewPage: Function, |
| 21 | + }; |
| 22 | + |
| 23 | + setup() { |
| 24 | + this.orm = useService("orm"); |
| 25 | + this.dialogs = useService("dialog"); |
| 26 | + this.website = useService("website"); |
| 27 | + this.action = useService("action"); |
| 28 | + this.isSystem = user.isSystem; |
| 29 | + useActiveElement("modalRef"); |
| 30 | + |
| 31 | + this.newContentText = { |
| 32 | + failed: _t('Failed to install "%s"'), |
| 33 | + installInProgress: _t("The installation of an App is already in progress."), |
| 34 | + installNeeded: _t('Do you want to install the "%s" App?'), |
| 35 | + installPleaseWait: _t('Installing "%s"'), |
| 36 | + }; |
| 37 | + |
| 38 | + this.state = useState({ |
| 39 | + newContentElements: [ |
| 40 | + { |
| 41 | + moduleName: "website_blog", |
| 42 | + moduleXmlId: "base.module_website_blog", |
| 43 | + status: MODULE_STATUS.NOT_INSTALLED, |
| 44 | + icon: xml`<i class="fa fa-newspaper-o"/>`, |
| 45 | + title: _t("Blog Post"), |
| 46 | + }, |
| 47 | + { |
| 48 | + moduleName: "website_event", |
| 49 | + moduleXmlId: "base.module_website_event", |
| 50 | + status: MODULE_STATUS.NOT_INSTALLED, |
| 51 | + icon: xml`<i class="fa fa-ticket"/>`, |
| 52 | + title: _t("Event"), |
| 53 | + }, |
| 54 | + { |
| 55 | + moduleName: "website_forum", |
| 56 | + moduleXmlId: "base.module_website_forum", |
| 57 | + status: MODULE_STATUS.NOT_INSTALLED, |
| 58 | + icon: xml`<i class="fa fa-comment"/>`, |
| 59 | + redirectUrl: "/forum", |
| 60 | + title: _t("Forum"), |
| 61 | + }, |
| 62 | + { |
| 63 | + moduleName: "website_hr_recruitment", |
| 64 | + moduleXmlId: "base.module_website_hr_recruitment", |
| 65 | + status: MODULE_STATUS.NOT_INSTALLED, |
| 66 | + icon: xml`<i class="fa fa-briefcase"/>`, |
| 67 | + title: _t("Job Position"), |
| 68 | + }, |
| 69 | + { |
| 70 | + moduleName: "website_sale", |
| 71 | + moduleXmlId: "base.module_website_sale", |
| 72 | + status: MODULE_STATUS.NOT_INSTALLED, |
| 73 | + icon: xml`<i class="fa fa-shopping-cart"/>`, |
| 74 | + title: _t("Product"), |
| 75 | + }, |
| 76 | + { |
| 77 | + moduleName: "website_slides", |
| 78 | + moduleXmlId: "base.module_website_slides", |
| 79 | + status: MODULE_STATUS.NOT_INSTALLED, |
| 80 | + icon: xml`<i class="fa module_icon" style="background-image: url('/website/static/src/img/apps_thumbs/website_slide.svg');background-repeat: no-repeat; background-position: center;"/>`, |
| 81 | + title: _t("Course"), |
| 82 | + }, |
| 83 | + { |
| 84 | + moduleName: "website_livechat", |
| 85 | + moduleXmlId: "base.module_website_livechat", |
| 86 | + status: MODULE_STATUS.NOT_INSTALLED, |
| 87 | + icon: xml`<i class="fa fa-comments"/>`, |
| 88 | + title: _t("Livechat Widget"), |
| 89 | + redirectUrl: "/livechat", |
| 90 | + }, |
| 91 | + ], |
| 92 | + }); |
| 93 | + |
| 94 | + this.websiteContext = useState(this.website.context); |
| 95 | + useHotkey("escape", () => { |
| 96 | + if (this.websiteContext.showNewContentModal) { |
| 97 | + this.websiteContext.showNewContentModal = false; |
| 98 | + } |
| 99 | + }); |
| 100 | + |
| 101 | + onWillStart(this.onWillStart.bind(this)); |
| 102 | + } |
| 103 | + |
| 104 | + async onWillStart() { |
| 105 | + this.isDesigner = await user.hasGroup("website.group_website_designer"); |
| 106 | + this.canInstall = await user.isAdmin; |
| 107 | + if (this.canInstall) { |
| 108 | + const moduleNames = this.state.newContentElements |
| 109 | + .filter(({ status }) => status === MODULE_STATUS.NOT_INSTALLED) |
| 110 | + .map(({ moduleName }) => moduleName); |
| 111 | + this.modulesInfo = {}; |
| 112 | + for (const record of await this.orm.searchRead( |
| 113 | + "ir.module.module", |
| 114 | + [["name", "in", moduleNames]], |
| 115 | + ["id", "name", "shortdesc"] |
| 116 | + )) { |
| 117 | + this.modulesInfo[record.name] = { id: record.id, name: record.shortdesc }; |
| 118 | + } |
| 119 | + } |
| 120 | + const modelsToCheck = []; |
| 121 | + const elementsToUpdate = {}; |
| 122 | + for (const element of this.state.newContentElements) { |
| 123 | + if (element.model) { |
| 124 | + modelsToCheck.push(element.model); |
| 125 | + elementsToUpdate[element.model] = element; |
| 126 | + } |
| 127 | + } |
| 128 | + const accesses = await rpc("/website/check_new_content_access_rights", { |
| 129 | + models: modelsToCheck, |
| 130 | + }); |
| 131 | + for (const [model, access] of Object.entries(accesses)) { |
| 132 | + elementsToUpdate[model].isDisplayed = access; |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + get sortedNewContentElements() { |
| 137 | + return this.state.newContentElements |
| 138 | + .filter(({ status }) => status !== MODULE_STATUS.NOT_INSTALLED) |
| 139 | + .concat( |
| 140 | + this.state.newContentElements.filter( |
| 141 | + ({ status }) => status === MODULE_STATUS.NOT_INSTALLED |
| 142 | + ) |
| 143 | + ); |
| 144 | + } |
| 145 | + |
| 146 | + async installModule(id, redirectUrl) { |
| 147 | + await this.orm.silent.call("ir.module.module", "button_immediate_install", [id]); |
| 148 | + if (redirectUrl) { |
| 149 | + this.website.prepareOutLoader(); |
| 150 | + window.location.replace(redirectUrl); |
| 151 | + } else { |
| 152 | + const { |
| 153 | + id, |
| 154 | + metadata: { path, viewXmlid }, |
| 155 | + } = this.website.currentWebsite; |
| 156 | + const url = new URL(path); |
| 157 | + if (viewXmlid === "website.page_404") { |
| 158 | + url.pathname = ""; |
| 159 | + } |
| 160 | + // A reload is needed after installing a new module, to instantiate |
| 161 | + // a NewContentModal with patches from the installed module. |
| 162 | + this.website.prepareOutLoader(); |
| 163 | + redirect( |
| 164 | + `/odoo/action-website.website_preview?website_id=${id}&path=${encodeURIComponent( |
| 165 | + url.toString() |
| 166 | + )}&display_new_content=true` |
| 167 | + ); |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + onClickNewContent(element) { |
| 172 | + if (element.createNewContent) { |
| 173 | + return element.createNewContent(); |
| 174 | + } |
| 175 | + |
| 176 | + const { id, name } = this.modulesInfo[element.moduleName]; |
| 177 | + const dialogProps = { |
| 178 | + title: element.title, |
| 179 | + installationText: sprintf(this.newContentText.installNeeded, name), |
| 180 | + installModule: async () => { |
| 181 | + // Update the NewContentElement with installing icon and text. |
| 182 | + this.state.newContentElements = this.state.newContentElements.map((el) => { |
| 183 | + if (el.moduleXmlId === element.moduleXmlId) { |
| 184 | + el.status = MODULE_STATUS.INSTALLING; |
| 185 | + el.icon = xml`<i class="fa fa-spin fa-circle-o-notch"/>`; |
| 186 | + el.title = sprintf(this.newContentText.installPleaseWait, name); |
| 187 | + } |
| 188 | + return el; |
| 189 | + }); |
| 190 | + this.website.showLoader({ title: _t("Building your %s", name) }); |
| 191 | + try { |
| 192 | + await this.installModule(id, element.redirectUrl); |
| 193 | + } catch (error) { |
| 194 | + this.website.hideLoader(); |
| 195 | + // Update the NewContentElement with failure icon and text. |
| 196 | + this.state.newContentElements = this.state.newContentElements.map((el) => { |
| 197 | + if (el.moduleXmlId === element.moduleXmlId) { |
| 198 | + el.status = MODULE_STATUS.FAILED_TO_INSTALL; |
| 199 | + el.icon = xml`<i class="fa fa-exclamation-triangle"/>`; |
| 200 | + el.title = sprintf(this.newContentText.failed, name); |
| 201 | + } |
| 202 | + return el; |
| 203 | + }); |
| 204 | + console.error(error); |
| 205 | + } |
| 206 | + }, |
| 207 | + }; |
| 208 | + this.dialogs.add(InstallModuleDialog, dialogProps); |
| 209 | + } |
| 210 | + |
| 211 | + /** |
| 212 | + * This method registers the action to perform when a new content is |
| 213 | + * saved. The path must be computed once the record is saved, to |
| 214 | + * perform the 'ir.act_window_close' action, which will be used when |
| 215 | + * the dialog is closed to go to the correct website page. |
| 216 | + */ |
| 217 | + async onAddContent(action, edition = false, context = null) { |
| 218 | + this.action.doAction(action, { |
| 219 | + additionalContext: context ? context : {}, |
| 220 | + onClose: (infos) => { |
| 221 | + if (infos) { |
| 222 | + this.website.goToWebsite({ path: infos.path, edition: edition }); |
| 223 | + } |
| 224 | + }, |
| 225 | + props: { |
| 226 | + onSave: (record, params) => { |
| 227 | + if (record.resId) { |
| 228 | + const path = params.computePath(); |
| 229 | + this.action.doAction({ |
| 230 | + type: "ir.actions.act_window_close", |
| 231 | + infos: { path }, |
| 232 | + }); |
| 233 | + } |
| 234 | + }, |
| 235 | + }, |
| 236 | + }); |
| 237 | + } |
| 238 | +} |
0 commit comments