From ca2e143d54622d598201826a2cd669bad64b205d Mon Sep 17 00:00:00 2001 From: Younn Olivier Date: Mon, 3 Jan 2022 17:56:36 +0100 Subject: [PATCH] [IMP] website, *: add new content systray item *: website_blog, website_event, website_forum, website_hr_recruitment, website_livechat, website_sale, website_slides The NewContentModal component is added. It displays tiles, which will install a module if not already installed, or perform an action defined by the module otherwise. So that modules can patch that component to define an action when installed (for example, website_sale will handle the logic of creating a new product), the elements to displayed and their state (NOT_INSTALLED, INSTALLING, INSTALLED) are listed in the state of the component. A key 'isDisplayed' is added on the new content elements that should not be displayed to the user, depended on his security groups. By default, the new content elements are displayed to the system user. See merge commit for more information. task-2687506 --- addons/website/__manifest__.py | 1 - addons/website/controllers/main.py | 10 + .../website_preview/website_preview.js | 5 + .../static/src/components/navbar/navbar.js | 1 - .../website/static/src/scss/website.ui.scss | 77 --- .../static/src/services/website_service.js | 18 +- .../static/src/systray_items/new_content.js | 532 +++++++----------- .../static/src/systray_items/new_content.scss | 80 +++ .../static/src/systray_items/new_content.xml | 44 ++ addons/website_blog/__manifest__.py | 3 + .../src/js/systray_items/new_content.js | 63 +-- addons/website_event/__manifest__.py | 3 + .../src/js/systray_items/new_content.js | 281 +-------- addons/website_forum/__manifest__.py | 3 + .../src/js/systray_items/new_content.js | 150 +---- addons/website_hr_recruitment/__manifest__.py | 3 + .../src/js/systray_items/new_content.js | 16 + addons/website_livechat/__manifest__.py | 1 + .../src/js/systray_items/new_content.js | 50 +- addons/website_sale/__manifest__.py | 1 + .../src/js/systray_items/new_content.js | 16 + addons/website_slides/__manifest__.py | 1 + .../src/js/systray_items/new_content.js | 187 +----- 23 files changed, 446 insertions(+), 1100 deletions(-) create mode 100644 addons/website/static/src/systray_items/new_content.scss create mode 100644 addons/website/static/src/systray_items/new_content.xml create mode 100644 addons/website_hr_recruitment/static/src/js/systray_items/new_content.js create mode 100644 addons/website_sale/static/src/js/systray_items/new_content.js diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index d94645bfde4..118ce911c2e 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -235,7 +235,6 @@ 'website/static/src/js/menu/debug_menu.js', 'website/static/src/components/wysiwyg_adapter/wysiwyg_adapter.js', 'website/static/src/systray_items/mobile_preview.js', - 'website/static/src/systray_items/new_content.js', 'website/static/src/components/dialog/seo.js', 'website/static/src/systray_items/translate_website.js', 'website/static/src/js/set_view_track.js', diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 0a76e721883..deb5eff4006 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -303,6 +303,16 @@ class Website(Home): ] } + @http.route('/website/get_modules_info', type='json', auth="user") + def get_modules_info(self, xml_ids): + def get_module_info(xml_id): + module_info = request.env.ref(xml_id) + return {'id': module_info.id, 'name': module_info.shortdesc} + modules_info = {} + for xml_id in xml_ids: + modules_info[xml_id] = get_module_info(xml_id) + return modules_info + @http.route('/website/snippet/filters', type='json', auth='public', website=True) def get_dynamic_filter(self, filter_id, template_key, limit=None, search_domain=None, with_sample=False): dynamic_filter = request.env['website.snippet.filter'].sudo().search( 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 c5716b08dfb..1c8f8762a85 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 @@ -22,6 +22,7 @@ export class WebsitePreview extends Component { useEffect(() => { this.websiteService.currentWebsiteId = this.websiteId; + this.websiteService.context.showNewContentModal = this.props.action.context.params && this.props.action.context.params.display_new_content; return () => this.websiteService.currentWebsiteId = null; }, () => [this.props.action.context.params]); } @@ -83,6 +84,10 @@ export class WebsitePreview extends Component { this.currentUrl = this.iframe.el.contentDocument.location.href; history.replaceState({}, this.props.action.display_name, this.currentUrl); + this.websiteService.currentMetadata = { + path: this.currentUrl, + }; + // Before leaving the iframe, its content is replicated on an // underlying iframe, to avoid for white flashes (visible on // Chrome Windows/Linux). diff --git a/addons/website/static/src/components/navbar/navbar.js b/addons/website/static/src/components/navbar/navbar.js index f70cddd517a..05c9e6baf3b 100644 --- a/addons/website/static/src/components/navbar/navbar.js +++ b/addons/website/static/src/components/navbar/navbar.js @@ -3,7 +3,6 @@ import { NavBar } from '@web/webclient/navbar/navbar'; import { useService, useBus } from '@web/core/utils/hooks'; import { registry } from "@web/core/registry"; - import { patch } from 'web.utils'; const websiteSystrayRegistry = registry.category('website_systray'); diff --git a/addons/website/static/src/scss/website.ui.scss b/addons/website/static/src/scss/website.ui.scss index 5b8cc3cd9c1..e73fa3a9cb5 100644 --- a/addons/website/static/src/scss/website.ui.scss +++ b/addons/website/static/src/scss/website.ui.scss @@ -471,83 +471,6 @@ body .modal { } } -// ADD NEW PAGE MODAL - -.o_new_content_open { - // Kill the scroll on the body - overflow: hidden; -} - -#o_new_content_menu_choices { - @include o-w-preserve-base; - @include o-position-absolute($o-navbar-height, 0, 0, 0); - position: fixed; - display: flex; - overflow: auto; - background-color: rgba(0, 0, 0, 0.8); - font-family: $o-we-font-family; - - &::before { - content: " "; - @include o-position-absolute(0, 0, 0, 0); - z-index: -1; - pointer-events: none; - } - - .container { - max-width: 720px; - margin: auto; - } - - .o_new_content_element { - opacity: 0; - animation: fadeInDownSmall 1s forwards; - - .o_uninstalled_module { - filter: brightness(70%) contrast(60%); - - } - - .module_icon { - display: block; - margin: auto; - } - - a { - display: block; - font-size: 34px; - text-align: center; - text-decoration: none; - - i { - width: 110px; - height: 110px; - border: 3px solid lighten(#2C2C36, 10%); - border-radius: 100%; - line-height: 104px; - background-color: #2C2C36; - color: white; - - transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1) 0s; - } - p { - color: white; - margin-top: 0.7em; - font-size: 0.5em; - } - - &:hover, &:focus { - text-decoration: none; - outline: none; // remove ugly dotted border on Firefox - i { - border-color: #1cc1a9; - box-shadow: 0 0 10px rgba(28, 193, 169, 0.46); - } - } - } - } -} - // LOGIN FORM .oe_login_form, .oe_signup_form, .oe_reset_password_form { max-width: 300px; diff --git a/addons/website/static/src/services/website_service.js b/addons/website/static/src/services/website_service.js index 4bd8d15a2b1..0b6a1d95fcf 100644 --- a/addons/website/static/src/services/website_service.js +++ b/addons/website/static/src/services/website_service.js @@ -2,6 +2,8 @@ import { registry } from '@web/core/registry'; +const { reactive } = owl; + const websiteSystrayRegistry = registry.category('website_systray'); const unslugHtmlDataObject = (repr) => { @@ -20,6 +22,10 @@ export const websiteService = { async start(env, { orm, action }) { let websites = []; let currentWebsiteId; + let currentMetadata = {}; + const context = reactive({ + showNewContentModal: false, + }); const setCurrentWebsiteId = id => { currentWebsiteId = id; @@ -29,12 +35,22 @@ export const websiteService = { set currentWebsiteId(id) { setCurrentWebsiteId(id); }, + set currentMetadata(metadata) { + currentMetadata = metadata; + }, get currentWebsite() { - return websites.find(website => website.id === currentWebsiteId); + const currentWebsite = websites.find(w => w.id === currentWebsiteId); + if (currentWebsite) { + currentWebsite.metadata = currentMetadata; + } + return currentWebsite; }, get websites() { return websites; }, + get context() { + return context; + }, goToWebsite({ websiteId = currentWebsiteId || websites[0].id, path = '/' }) { action.doAction('website.website_preview', { clearBreadcrumbs: true, diff --git a/addons/website/static/src/systray_items/new_content.js b/addons/website/static/src/systray_items/new_content.js index 1eac6393be9..52fafd2dd92 100644 --- a/addons/website/static/src/systray_items/new_content.js +++ b/addons/website/static/src/systray_items/new_content.js @@ -1,354 +1,204 @@ -odoo.define('website.newMenu', function (require) { -'use strict'; +/** @odoo-module **/ -var core = require('web.core'); -var Dialog = require('web.Dialog'); -var websiteNavbarData = require('website.navbar'); -var wUtils = require('website.utils'); -var tour = require('web_tour.tour'); +import { registry } from '@web/core/registry'; +import { useService } from '@web/core/utils/hooks'; +import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; -const { registry } = require("@web/core/registry"); +const { Component, xml, useState, onWillStart } = owl; -const {qweb, _t} = core; +export const MODULE_STATUS = { + NOT_INSTALLED: 'NOT_INSTALLED', + INSTALLING: 'INSTALLING', + FAILED_TO_INSTALL: 'FAILED_TO_INSTALL', + INSTALLED: 'INSTALLED', +}; -var enableFlag = 'enable_new_content'; +class NewContentElement extends Component { + setup() { + this.MODULE_STATUS = MODULE_STATUS; + } -var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ - xmlDependencies: ['/website/static/src/xml/website.editor.xml'], - actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, { - close_all_widgets: '_handleCloseDemand', - new_page: '_createNewPage', - }), - events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.events || {}, { - 'click': '_onBackgroundClick', - 'click [data-module-id]': '_onModuleIdClick', - 'keydown': '_onBackgroundKeydown', - }), - // allow text to be customized with inheritance - 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"'), - }, - - /** - * Prepare the navigation and find the modules to install. - * Move not installed module buttons after installed modules buttons, - * but keep the original index to be able to move back the pending install - * button at its final position, so the user can click at the same place. - * - * @override - */ - start: function () { - this.pendingInstall = false; - this.$newContentMenuChoices = this.$('#o_new_content_menu_choices'); - - var $modules = this.$newContentMenuChoices.find('.o_new_content_element'); - _.each($modules, function (el, index) { - var $el = $(el); - $el.data('original-index', index); - if ($el.data('module-id')) { - $el.appendTo($el.parent()); - $el.find('a i, a p').addClass('o_uninstalled_module'); - } - }); - - this.$firstLink = this.$newContentMenuChoices.find('a:eq(0)'); - this.$lastLink = this.$newContentMenuChoices.find('a:last'); - - if ($.deparam.querystring()[enableFlag] !== undefined) { - Object.keys(tour.tours).forEach( - el => { - let element = tour.tours[el]; - if (element.steps[0].trigger == '#new-content-menu > a' - && !element.steps[0].extra_trigger) { - element.steps[0].auto = true; - } - } - ); - this._showMenu(); - } - this.$loader = $(qweb.render('website.new_content_loader')); - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Actions - //-------------------------------------------------------------------------- - - /** - * Asks the user information about a new page to create, then creates it and - * redirects the user to this new page. - * - * @private - * @returns {Promise} Unresolved if there is a redirection - */ - _createNewPage: function () { - return wUtils.prompt({ - id: 'editor_new_page', - window_title: _t("New Page"), - input: _t("Page Title"), - init: function () { - var $group = this.$dialog.find('div.form-group'); - $group.removeClass('mb0'); - - var $add = $('
', {'class': 'form-group mb0 row'}) - .append($('', {'class': 'offset-md-3 col-md-9 text-left'}) - .append(qweb.render('website.components.switch', {id: 'switch_addTo_menu', label: _t("Add to menu")}))); - $add.find('input').prop('checked', true); - $group.after($add); - } - }).then(function (result) { - var val = result.val; - var $dialog = result.dialog; - if (!val) { - return; - } - var url = '/website/add/' + encodeURIComponent(val); - wUtils.sendRequest(url, { - add_menu: $dialog.find('input[type="checkbox"]').is(':checked') || '', - }); - return new Promise(function () {}); - }); - }, - /** - * @private - */ - _handleCloseDemand: function () { - this._hideMenu(); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Set the focus on the first link - * - * @private - */ - _focusFirstLink: function () { - this.$firstLink.focus(); - }, - /** - * Set the focus on the last link - * - * @private - */ - _focusLastLink: function () { - this.$lastLink.focus(); - }, - /** - * Hide the menu - * - * @private - */ - _hideMenu: function () { - this.shown = false; - this.$newContentMenuChoices.addClass('o_hidden'); - $('body').removeClass('o_new_content_open'); - }, - /** - * Install a module - * - * @private - * @param {number} moduleId: the module to install - * @return {Promise} - */ - _install: function (moduleId) { - this.pendingInstall = true; - $('body').css('pointer-events', 'none'); - return this._rpc({ - model: 'ir.module.module', - method: 'button_immediate_install', - args: [[moduleId]], - }).guardedCatch(function () { - $('body').css('pointer-events', ''); - }); - }, - /** - * Show the menu - * - * @private - * @returns {Promise} - */ - _showMenu: function () { - var self = this; - return new Promise(function (resolve, reject) { - self.trigger_up('action_demand', { - actionName: 'close_all_widgets', - onSuccess: resolve, - onFailure: reject, - }); - }).then(function () { - self.firstTab = true; - self.shown = true; - self.$newContentMenuChoices.removeClass('o_hidden'); - $('body').addClass('o_new_content_open'); - self.$('> a').focus(); - }); - }, - /** - * Called to add loader element in DOM. - * - * @param {string} moduleName - * @private - */ - _addLoader(moduleName) { - const newContentLoaderText = _.str.sprintf(_t("Building your %s"), moduleName); - this.$loader.find('#new_content_loader_text').replaceWith(newContentLoaderText); - $('body').append(this.$loader); - }, - /** - * @private - */ - _removeLoader() { - this.$loader.remove(); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when the menu's toggle button is clicked: - * -> Opens the menu and reset the tab navigation (if closed) - * -> Close the menu (if open) - * Called when a click outside the menu's options occurs -> Close the menu - * - * @private - * @param {Event} ev - */ - _onBackgroundClick: function (ev) { - if (this.$newContentMenuChoices.hasClass('o_hidden')) { - this._showMenu(); - } else { - this._hideMenu(); - } - }, - /** - * Called when a keydown occurs: - * ESC -> Closes the modal - * TAB -> Navigation (captured in the modal) - * - * @private - * @param {Event} ev - */ - _onBackgroundKeydown: function (ev) { - if (!this.shown) { - return; - } - switch (ev.which) { - case $.ui.keyCode.ESCAPE: - this._hideMenu(); - ev.stopPropagation(); - break; - case $.ui.keyCode.TAB: - if (ev.shiftKey) { - if (this.firstTab || document.activeElement === this.$firstLink[0]) { - this._focusLastLink(); - ev.preventDefault(); - } - } else { - if (this.firstTab || document.activeElement === this.$lastLink[0]) { - this._focusFirstLink(); - ev.preventDefault(); - } - } - this.firstTab = false; - break; - } - }, - /** - * Open the install dialog related to an element: - * - open the dialog depending on access right and another pending install - * - if ok to install, prepare the install action: - * - call the proper action on click - * - change the button text and style - * - handle the result (reload on the same page or error) - * - * @private - * @param {Event} ev - */ - _onModuleIdClick: function (ev) { - var self = this; - var $el = $(ev.currentTarget); - var $i = $el.find('a i'); - var $p = $el.find('a p'); - - var title = $p.text(); - var content = ''; - var buttons; - - var moduleId = $el.data('module-id'); - var name = $el.data('module-shortdesc'); - - ev.stopPropagation(); + onClick(ev) { ev.preventDefault(); + ev.stopPropagation(); + this.props.onClick(); + } +} +NewContentElement.template = "website.NewContentElement"; +NewContentElement.props = { + name: { type: String, optional: true }, + title: String, + onClick: Function, + status: { type: String, optional: true }, + slots: Object, +}; +NewContentElement.defaultProps = { + status: MODULE_STATUS.INSTALLED, +}; - if (this.pendingInstall) { - content = this.newContentText.installInProgress; +export class NewContentModal extends Component { + setup() { + this.user = useService('user'); + this.orm = useService('orm'); + this.rpc = useService('rpc'); + this.dialogs = useService('dialog'); + this.website = useService('website'); + this.isSystem = this.user.isSystem; + + this.newContentText = { + failed: this.env._t('Failed to install "%s"'), + installInProgress: this.env._t("The installation of an App is already in progress."), + installNeeded: this.env._t('Do you want to install the "%s" App?'), + installPleaseWait: this.env._t('Installing "%s"'), + }; + + this.state = useState({ + newContentElements: [ + { + moduleXmlId: 'base.module_website_blog', + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: this.env._t('Blog Post'), + }, + { + moduleXmlId: 'base.module_website_event', + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: this.env._t('Event'), + }, + { + moduleXmlId: 'base.module_website_forum', + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + redirectUrl: '/forum', + title: this.env._t('Forum'), + }, + { + moduleXmlId: 'base.module_website_hr_recruitment', + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: this.env._t('Job Offer'), + }, + { + moduleXmlId: 'base.module_website_sale', + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: this.env._t('Product'), + }, + { + moduleXmlId: 'base.module_website_slides', + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: this.env._t('Course'), + }, + { + moduleXmlId: 'base.module_website_livechat', + status: MODULE_STATUS.NOT_INSTALLED, + icon: xml``, + title: this.env._t('Livechat Widget'), + redirectUrl: '/livechat' + }, + ] + }); + + this.websiteContext = useState(this.website.context); + + onWillStart(async () => { + this.isDesigner = await this.user.hasGroup('website.group_website_designer'); + + const xmlIds = this.state.newContentElements.filter(({status}) => status === MODULE_STATUS.NOT_INSTALLED).map(({moduleXmlId}) => moduleXmlId); + + this.modulesInfo = await this.rpc('/website/get_modules_info', {xml_ids: xmlIds}); + }); + + useHotkey('escape', () => { + if (this.websiteContext.showNewContentModal) { + this.websiteContext.showNewContentModal = false; + } + }); + } + + get sortedNewContentElements() { + return this.state.newContentElements.filter(({status}) => status !== MODULE_STATUS.NOT_INSTALLED).concat(this.state.newContentElements.filter(({status}) => status === MODULE_STATUS.NOT_INSTALLED)); + } + + createNewPage() {} + + async installModule(id, redirectUrl) { + await this.orm.call( + 'ir.module.module', + 'button_immediate_install', + [id], + ); + if (redirectUrl) { + window.location.replace(redirectUrl); } else { - content = _.str.sprintf(this.newContentText.installNeeded, name); - buttons = [{ - text: _t("Install"), - classes: 'btn-primary', - close: true, - click: function () { - // move the element where it will be after installation - var $finalPosition = self.$newContentMenuChoices - .find('.o_new_content_element:not([data-module-id])') - .filter(function () { - return $(this).data('original-index') < $el.data('original-index'); - }).last(); - if ($finalPosition) { - $el.fadeTo(400, 0, function () { - // if once installed, button disapeear, don't need to move it. - if (!$el.hasClass('o_new_content_element_once')) { - $el.insertAfter($finalPosition); - } - // change style to use spinner - $i.removeClass() - .addClass('fa fa-spin fa-circle-o-notch fa-spin') - .css('background-image', 'none'); - $p.removeClass('o_uninstalled_module') - .text(_.str.sprintf(self.newContentText.installPleaseWait, name)); - $el.fadeTo(1000, 1); - self._addLoader(name); - }); - } + const { id, metadata: { path } } = this.website.currentWebsite; + // A reload is needed after installing a new module, to instantiate + // a NewContentModal with patches from the installed module. + window.location.replace(`/web#action=website.website_preview&website_id=${id}&path=${encodeURIComponent(path)}&display_new_content=true`); + } + } - self._install(moduleId).then(function () { - var origin = window.location.origin; - var redirectURL = $el.find('a').data('url') || (window.location.pathname + '?' + enableFlag); - window.location.href = origin + redirectURL; - self._removeLoader(); - }, function () { - $i.removeClass() - .addClass('fa fa-exclamation-triangle'); - $p.text(_.str.sprintf(self.newContentText.failed, name)); - }); - } - }, { - text: _t("Cancel"), - close: true, - }]; + onClickNewContent(element) { + if (element.createNewContent) { + return element.createNewContent(); } - new Dialog(this, { - title: title, - size: 'medium', - $content: $('
', {text: content}), - buttons: buttons - }).open(); - }, -}); + const {id, name} = this.modulesInfo[element.moduleXmlId]; + const dialogProps = { + title: this.env._t("Install"), + body: _.str.sprintf(this.newContentText.installNeeded, name), + confirm: 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 = _.str.sprintf(this.newContentText.installPleaseWait, name); + } + return el; + }); + try { + await this.installModule(id, element.redirectUrl); + } catch (error) { + // 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 = _.str.sprintf(this.newContentText.failed, name); + } + return el; + }); + console.error(error); + } + }, + cancel: () => {}, + }; + this.dialogs.add(ConfirmationDialog, dialogProps); + } +} +NewContentModal.template = "website.NewContentModal"; +NewContentModal.components = { NewContentElement }; -registry.category("website_navbar_widgets").add("NewContentMenu", { - Widget: NewContentMenu, - selector: '.o_new_content_menu', -}); +class NewContentSystray extends Component { + setup() { + this.rpc = useService('rpc'); + this.website = useService('website'); + this.websiteContext = useState(this.website.context); + } -return NewContentMenu; -}); + onClick() { + this.websiteContext.showNewContentModal = !this.websiteContext.showNewContentModal; + } +} +NewContentSystray.template = "website.NewContentSystray"; +NewContentSystray.components = { NewContentModal }; + +export const systrayItem = { + Component: NewContentSystray, +}; + +registry.category("website_systray").add("NewContent", systrayItem, { sequence: 10 }); diff --git a/addons/website/static/src/systray_items/new_content.scss b/addons/website/static/src/systray_items/new_content.scss new file mode 100644 index 00000000000..5153ce089e6 --- /dev/null +++ b/addons/website/static/src/systray_items/new_content.scss @@ -0,0 +1,80 @@ +@keyframes fadeInDownSmall { + 0% { + opacity: 0; + transform: translate(0, -5px); + } + 100% { + opacity: 1; + transform: translate(0, 0); + } +} + +#o_new_content_menu_choices { + @include o-position-absolute(0, 0, 0, 0); + position: fixed; + display: flex; + overflow: auto; + background-color: rgba(0, 0, 0, 0.8); + font-family: $o-we-font-family; + z-index: $zindex-fixed - 1; + + &::before { + content: " "; + @include o-position-absolute(0, 0, 0, 0); + z-index: -1; + pointer-events: none; + } + + .container { + max-width: 720px; + margin: auto; + } + + .o_new_content_element { + opacity: 0; + animation: fadeInDownSmall 1s forwards; + + .o_uninstalled_module { + filter: brightness(70%) contrast(60%); + + } + + .module_icon { + display: block; + margin: auto; + } + + a { + display: block; + font-size: 34px; + text-align: center; + text-decoration: none; + + i { + width: 110px; + height: 110px; + border: 3px solid lighten(#2C2C36, 10%); + border-radius: 100%; + line-height: 104px; + background-color: #2C2C36; + color: white; + + transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1) 0s; + } + p { + color: white; + margin-top: 0.7em; + font-size: 0.5em; + } + + &:hover, &:focus { + text-decoration: none; + outline: none; // remove ugly dotted border on Firefox + i { + border-color: #1cc1a9; + box-shadow: 0 0 10px rgba(28, 193, 169, 0.46); + } + } + } + } +} diff --git a/addons/website/static/src/systray_items/new_content.xml b/addons/website/static/src/systray_items/new_content.xml new file mode 100644 index 00000000000..19ae618fa10 --- /dev/null +++ b/addons/website/static/src/systray_items/new_content.xml @@ -0,0 +1,44 @@ + + + +
+ + + +
+
+ + +
+
+
+ + +

Page

+
+ + + + +

+
+
+
+
+
+
+ + +
+ New + +
+
+
diff --git a/addons/website_blog/__manifest__.py b/addons/website_blog/__manifest__.py index 3db7e32d2a3..a050761700d 100644 --- a/addons/website_blog/__manifest__.py +++ b/addons/website_blog/__manifest__.py @@ -43,6 +43,9 @@ 'website_blog/static/src/js/contentshare.js', 'website_blog/static/src/js/website_blog.js', ], + 'web.assets_backend': [ + 'website_blog/static/src/js/systray_items/new_content.js', + ], }, 'license': 'LGPL-3', } diff --git a/addons/website_blog/static/src/js/systray_items/new_content.js b/addons/website_blog/static/src/js/systray_items/new_content.js index 7cc9aa09f33..a9c015f3522 100644 --- a/addons/website_blog/static/src/js/systray_items/new_content.js +++ b/addons/website_blog/static/src/js/systray_items/new_content.js @@ -1,57 +1,16 @@ -odoo.define('website_blog.new_blog_post', function (require) { -'use strict'; +/** @odoo-module **/ -var core = require('web.core'); -var wUtils = require('website.utils'); -var WebsiteNewMenu = require('website.newMenu'); +import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content'; +import { patch } from 'web.utils'; -var _t = core._t; +patch(NewContentModal.prototype, 'website_blog_new_content', { + setup() { + this._super(); -WebsiteNewMenu.include({ - actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, { - new_blog_post: '_createNewBlogPost', - }), - - //-------------------------------------------------------------------------- - // Actions - //-------------------------------------------------------------------------- - - /** - * Asks the user information about a new blog post to create, then creates - * it and redirects the user to this new post. - * - * @private - * @returns {Promise} Unresolved if there is a redirection - */ - _createNewBlogPost: function () { - return this._rpc({ - model: 'blog.blog', - method: 'search_read', - args: [wUtils.websiteDomain(this), ['name']], - }).then(function (blogs) { - if (blogs.length === 1) { - document.location = '/blog/' + blogs[0]['id'] + '/post/new'; - return new Promise(function () {}); - } else if (blogs.length > 1) { - return wUtils.prompt({ - id: 'editor_new_blog', - window_title: _t("New Blog Post"), - select: _t("Select Blog"), - init: function (field) { - return _.map(blogs, function (blog) { - return [blog['id'], blog['name']]; - }); - }, - }).then(function (result) { - var blog_id = result.val; - if (!blog_id) { - return; - } - document.location = '/blog/' + blog_id + '/post/new'; - return new Promise(function () {}); - }); - } - }); + const newBlogElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_blog'); + newBlogElement.createNewContent = () => this.createNewBlogPost(); + newBlogElement.status = MODULE_STATUS.INSTALLED; }, -}); + + createNewBlogPost() {} }); diff --git a/addons/website_event/__manifest__.py b/addons/website_event/__manifest__.py index 8a6650cddda..8d1f25dffdd 100644 --- a/addons/website_event/__manifest__.py +++ b/addons/website_event/__manifest__.py @@ -66,6 +66,9 @@ 'website.assets_wysiwyg': [ '/website_event/static/src/snippets/s_events/options.js', ], + 'web.assets_backend': [ + 'website_event/static/src/js/systray_items/new_content.js', + ], }, 'license': 'LGPL-3', } diff --git a/addons/website_event/static/src/js/systray_items/new_content.js b/addons/website_event/static/src/js/systray_items/new_content.js index 4b61e93dd88..c05c1aa8270 100644 --- a/addons/website_event/static/src/js/systray_items/new_content.js +++ b/addons/website_event/static/src/js/systray_items/new_content.js @@ -1,277 +1,16 @@ -odoo.define('website_event.editor', function (require) { -"use strict"; +/** @odoo-module **/ -var core = require('web.core'); -var Dialog = require('web.Dialog'); -var time = require('web.time'); -var WebsiteNewMenu = require('website.newMenu'); -var _t = core._t; +import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content'; +import { patch } from 'web.utils'; -var EventCreateDialog = Dialog.extend({ - xmlDependencies: Dialog.prototype.xmlDependencies.concat(['/website_event/static/src/xml/event_create.xml']), - template: 'website_event.event_create_dialog', - events: _.extend({}, Dialog.prototype.events, { - 'change select[name="event_location"]': '_onLocationChanged', - 'click .input-group-append': '_onDatetimeClicked', - }), - jsLibs: [ - '/web/static/lib/daterangepicker/daterangepicker.js', - '/web/static/src/legacy/js/libs/daterangepicker.js', - ], - +patch(NewContentModal.prototype, 'website_event_new_content', { + setup() { + this._super(); - /** - * @override - * @param {Object} parent - * @param {Object} options - */ - init: function (parent, options) { - options = _.defaults(options || {}, { - title: _t("New Event"), - size: 'medium', - buttons: [ - { - text: _t("Create"), - classes: 'btn-primary', - click: this._onClickCreate.bind(this), - }, - { - text: _t("Discard"), - close: true - }, - ] - }); - this.eventStart = moment(); - this.eventEnd = moment().add(1, "d"); - this._super(parent, options); + const newEventElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_event'); + newEventElement.createNewContent = () => this.createNewEvent(); + newEventElement.status = MODULE_STATUS.INSTALLED; }, - /** - * @override - */ - start: function () { - var self = this; - return this._super.apply(this, arguments).then(() => { - // stat / end datetimes widgets - self.$('input.daterange-input').each(function () { - self._initDateRangePicker($(this)); - }); - // address select2 configuration - self.$('input.o_wevent_js_address_id').select2(self._getSelect2AddressConfig()); - }); - }, - - /** - * @param {*} errors - */ - _display_errors: function (errors) { - this.$("p.text-danger").toggleClass('d-none', true); - for (let i = 0; i < errors.length; i++) { - this.$("#" + errors[i] + " p.text-danger").toggleClass('d-none', false); - } - }, - - _getSelect2AddressConfig: function () { - var self = this; - return { - allowClear: true, - formatNoMatches: false, - multiple: false, - selection_data: false, - width: '100%', - - createSearchChoice: function (name, data) { - var addedPartner = $(this.opts.element).select2('data'); - if (_.filter(_.union(addedPartner, data), function (partner) { - return partner.text.toLowerCase().localeCompare(name.toLowerCase()) === 0; - }).length === 0) { - return { - create: true, - id: _.uniqueId('address_'), - name: name, - text: _.str.sprintf(_t('Create "%s"'), name), - }; - } - }, - fill_data: function (query, data) { - var that = this; - var partners = {results: []}; - _.each(data, function (partner) { - var contact_address = partner.contact_address.replace(/[\n]+/g, '').trim(); - if (contact_address && that.matcher(query.term, contact_address)) { - partners.results.push({ - id: partner.id, - text: contact_address, - }); - } - }); - query.callback(partners); - }, - formatSelection: function (data) { - if (data.name) { - data.text = data.name; - } - return data.text; - }, - query: function (query) { - var that = this; - // fetch data only once and store it - if (!this.selection_data) { - self._rpc({ - model: 'res.partner', - method: 'search_read', - fields: ['contact_address'], - domain: [], - }).then(function (data) { - that.fill_data(query, data); - that.selection_data = data; - }); - } else { - this.fill_data(query, this.selection_data); - } - } - } - }, - - _getSelect2AddressValues: function () { - var select2Value = this.$('input.o_wevent_js_address_id').select2('data'); - if (select2Value) { - if (select2Value.create) { - return [0, {'name': select2Value.name}]; - } else { - return [select2Value.id, {}]; - } - } - return []; - }, - - /** - * - * @param {*} $dateGroup - */ - _initDateRangePicker: function ($dateGroup) { - var minDate = moment().subtract(1, "d"); - var maxDate = moment().add(200, "y"); - var self = this; - var textDirection = _t.database.parameters.direction; - - $dateGroup.daterangepicker({ - // dates - endDate: this.eventEnd, - maxDate: maxDate, - minDate: minDate, - startDate: this.eventStart, - // display - locale: { - direction: textDirection, - format: time.getLangDatetimeFormat().replace(':ss', ''), - applyLabel: _t('Apply'), - cancelLabel: _t('Cancel'), - weekLabel: 'W', - customRangeLabel: _t('Custom Range'), - daysOfWeek: moment.weekdaysMin(), - monthNames: moment.monthsShort(), - firstDay: moment.localeData().firstDayOfWeek() - }, - opens: 'left', - timePicker: true, - timePicker24Hour: true, - viewDate: moment(new Date()).hours(minDate.hours()).minutes(minDate.minutes()).seconds(minDate.seconds()).milliseconds(minDate.milliseconds()), - }, function (start, end, label) { - self.eventStart = start; - self.eventEnd = end; - }); - }, - /** - * @private - */ - _prepareFormValues: function () { - return { - address_values: this._getSelect2AddressValues(), - event_start: this.eventStart, - event_end: this.eventEnd, - location: this.$('select[name=event_location]').val(), - name: this.$('input[name=name]').val().trim(), - }; - }, - /** - * @private - * @param {*} values - */ - _validateForm: function (values) { - var errors = []; - if (!values.name){ - errors.push('name'); - } - if (values.location === 'on_site' && !values.address_values.length) { - errors.push('address'); - } - if (!values.event_start || !values.event_end) { - errors.push('event_dates'); - } - return errors; - }, - - /** - * @private - */ - _submitForm: function () { - var eventValues = this._prepareFormValues(); - var errors = this._validateForm(eventValues) - console.log(eventValues, errors); - if (errors.length) { - this._display_errors(errors); - return; - } - return this._rpc({ - route: '/event/add_event', - params: eventValues, - }).then((url) => { - window.location.href = url; - return new Promise(function () {}); - }); - }, - - /** - * @private - */ - _onClickCreate: function () { - return this._submitForm(); - }, - - /** - * @private - * @param {*} ev - */ - _onLocationChanged: function (ev) { - this.$('.show_visibility_address').toggleClass('d-none', ev.target.value === 'online'); - }, -}); - -WebsiteNewMenu.include({ - actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, { - new_event: '_createNewEvent', - }), - - //-------------------------------------------------------------------------- - // Actions - //-------------------------------------------------------------------------- - - /** - * Asks the user information about a new event to create, then creates it - * and redirects the user to this new event. - * - * @private - * @returns {Promise} Unresolved if there is a redirection - */ - _createNewEvent: function () { - var self = this; - var def = new Promise(function (resolve) { - var dialog = new EventCreateDialog(self, {}); - dialog.open(); - dialog.on('closed', self, resolve); - }); - return def; - }, -}); + createNewEvent() {} }); diff --git a/addons/website_forum/__manifest__.py b/addons/website_forum/__manifest__.py index 2ece59da5de..676e7fe4da0 100644 --- a/addons/website_forum/__manifest__.py +++ b/addons/website_forum/__manifest__.py @@ -52,6 +52,9 @@ Ask questions, get answers, no distractions 'website_forum/static/src/js/website_forum.js', 'website_forum/static/src/js/website_forum.share.js', ], + 'web.assets_backend': [ + 'website_forum/static/src/js/systray_items/new_content.js', + ], 'web.assets_qweb': [ 'website_forum/static/src/xml/*.xml', ], diff --git a/addons/website_forum/static/src/js/systray_items/new_content.js b/addons/website_forum/static/src/js/systray_items/new_content.js index 74072c592b4..532856fe8a8 100644 --- a/addons/website_forum/static/src/js/systray_items/new_content.js +++ b/addons/website_forum/static/src/js/systray_items/new_content.js @@ -1,144 +1,16 @@ -odoo.define('website_forum.editor', function (require) { -"use strict"; +/** @odoo-module **/ -var core = require('web.core'); -var WebsiteNewMenu = require('website.newMenu'); -var Dialog = require('web.Dialog'); +import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content'; +import { patch } from 'web.utils'; -var _t = core._t; +patch(NewContentModal.prototype, 'website_forum_new_content', { + setup() { + this._super(); -var ForumCreateDialog = Dialog.extend({ - xmlDependencies: Dialog.prototype.xmlDependencies.concat( - ['/website_forum/static/src/xml/website_forum_templates.xml'] - ), - template: 'website_forum.add_new_forum', - events: _.extend({}, Dialog.prototype.events, { - 'change input[name="privacy"]': '_onPrivacyChanged', - }), + const newForumElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_forum'); + newForumElement.createNewContent = () => this.createNewForum(); + newForumElement.status = MODULE_STATUS.INSTALLED; + }, - /** - * @override - * @param {Object} parent - * @param {Object} options - */ - init: function (parent, options) { - options = _.defaults(options || {}, { - title: _t("New Forum"), - size: 'medium', - buttons: [ - { - text: _t("Create"), - classes: 'btn-primary', - click: this.onCreateClick.bind(this), - }, - { - text: _t("Discard"), - close: true - }, - ] - }); - this._super(parent, options); - }, - start: function () { - var self = this; - return this._super.apply(this, arguments).then(function () { - var $input = self.$('#group_id'); - $input.select2({ - width: '100%', - allowClear: true, - formatNoMatches: false, - multiple: false, - selection_data: false, - fill_data: function (query, data) { - var that = this; - var tags = {results: []}; - _.each(data, function (obj) { - if (that.matcher(query.term, obj.display_name)) { - tags.results.push({id: obj.id, text: obj.display_name}); - } - }); - query.callback(tags); - }, - query: function (query) { - var that = this; - // fetch data only once and store it - if (!this.selection_data) { - self._rpc({ - model: 'res.groups', - method: 'search_read', - args: [[], ['display_name']], - }).then(function (data) { - that.fill_data(query, data); - that.selection_data = data; - }); - } else { - this.fill_data(query, this.selection_data); - } - } - }); - }); - }, - onCreateClick: function () { - var $dialog = this.$el; - var $forumName = $dialog.find('input[name=forum_name]'); - if (!$forumName.val()) { - $forumName.addClass('border-danger'); - return; - } - var $forumPrivacyGroup = $dialog.find('input[name=group_id]'); - var forumPrivacy = $dialog.find('input:radio[name=privacy]:checked').val(); - if (forumPrivacy === 'private' && !$forumPrivacyGroup.val()) { - this.$("#group-required").removeClass('d-none'); - return; - } - var addMenu = ($dialog.find('input[type="checkbox"]').is(':checked')); - var forumMode = $dialog.find('input:radio[name=mode]:checked').val(); - return this._rpc({ - route: '/forum/new', - params: { - forum_name: $forumName.val(), - forum_mode: forumMode, - forum_privacy: forumPrivacy, - forum_privacy_group: $forumPrivacyGroup.val(), - add_menu: addMenu || "", - }, - }).then(function (url) { - window.location.href = url; - return new Promise(function () {}); - }); - }, - /** - * @private - */ - _onPrivacyChanged: function (ev) { - this.$('.show_visibility_group').toggleClass('d-none', ev.target.value !== 'private'); - }, -}); - -WebsiteNewMenu.include({ - actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, { - new_forum: '_createNewForum', - }), - - //-------------------------------------------------------------------------- - // Actions - //-------------------------------------------------------------------------- - - /** - * Asks the user information about a new forum to create, then creates it - * and redirects the user to this new forum. - * - * @private - * @returns {Promise} Unresolved if there is a redirection - */ - _createNewForum: function () { - var self = this; - var def = new Promise(function (resolve) { - var dialog = new ForumCreateDialog(self, {}); - dialog.open(); - dialog.on('closed', self, resolve); - }); - return def; - }, -}); + createNewForum() {} }); diff --git a/addons/website_hr_recruitment/__manifest__.py b/addons/website_hr_recruitment/__manifest__.py index b9ac5d18383..783e97d3316 100644 --- a/addons/website_hr_recruitment/__manifest__.py +++ b/addons/website_hr_recruitment/__manifest__.py @@ -27,6 +27,9 @@ 'web.assets_frontend': [ 'website_hr_recruitment/static/src/scss/**/*', ], + 'web.assets_backend': [ + 'website_hr_recruitment/static/src/js/systray_items/new_content.js', + ], 'website.assets_editor': [ 'website_hr_recruitment/static/src/js/**/*', ], diff --git a/addons/website_hr_recruitment/static/src/js/systray_items/new_content.js b/addons/website_hr_recruitment/static/src/js/systray_items/new_content.js new file mode 100644 index 00000000000..2d2e4205576 --- /dev/null +++ b/addons/website_hr_recruitment/static/src/js/systray_items/new_content.js @@ -0,0 +1,16 @@ +/** @odoo-module **/ + +import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content'; +import { patch } from 'web.utils'; + +patch(NewContentModal.prototype, 'website_hr_recruitment_new_content', { + setup() { + this._super(); + + const newJobElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_hr_recruitment'); + newJobElement.createNewContent = () => this.createNewJob(); + newJobElement.status = MODULE_STATUS.INSTALLED; + }, + + async createNewJob() {} +}); diff --git a/addons/website_livechat/__manifest__.py b/addons/website_livechat/__manifest__.py index d44a5235f9c..dea83dfcf48 100644 --- a/addons/website_livechat/__manifest__.py +++ b/addons/website_livechat/__manifest__.py @@ -41,6 +41,7 @@ Allow website visitors to chat with the collaborators. This module also brings a 'website_livechat/static/src/components/*/*.js', 'website_livechat/static/src/components/*/*.scss', 'website_livechat/static/src/models/*.js', + 'website_livechat/static/src/js/systray_items/new_content.js', ], 'web.assets_tests': [ 'website_livechat/static/tests/tours/**/*', diff --git a/addons/website_livechat/static/src/js/systray_items/new_content.js b/addons/website_livechat/static/src/js/systray_items/new_content.js index 67f68bba9d9..46a2b933a97 100644 --- a/addons/website_livechat/static/src/js/systray_items/new_content.js +++ b/addons/website_livechat/static/src/js/systray_items/new_content.js @@ -1,46 +1,16 @@ /** @odoo-module **/ -import { _t } from 'web.core'; -import wUtils from 'website.utils'; -import WebsiteNewMenu from 'website.newMenu'; +import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content'; +import { patch } from 'web.utils'; -WebsiteNewMenu.include({ - actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, { - new_channel: '_createNewChannel', - }), +patch(NewContentModal.prototype, 'website_livechat_new_content', { + setup() { + this._super(); - //-------------------------------------------------------------------------- - // Actions - //-------------------------------------------------------------------------- - - /** - * Asks the user information about a new channel to create, then creates it - * and redirects the user to this new channel. - * - * @private - * @returns {Promise} Unresolved if there is a redirection - */ - _createNewChannel: function () { - var self = this; - return wUtils.prompt({ - window_title: _t("New Channel"), - input: _t("Name"), - }).then(function (result) { - var name = result.val; - if (!name) { - return; - } - return self._rpc({ - model: 'im_livechat.channel', - method: 'create_and_get_website_url', - args: [[]], - kwargs: { - name: name, - }, - }).then(function (url) { - window.location.href = url; - return new Promise(function () {}); - }); - }); + const newChannelElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_livechat'); + newChannelElement.createNewContent = () => this.createNewChannel(); + newChannelElement.status = MODULE_STATUS.INSTALLED; }, + + createNewChannel() {} }); diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py index 4191da76126..fa0fa3fb868 100644 --- a/addons/website_sale/__manifest__.py +++ b/addons/website_sale/__manifest__.py @@ -66,6 +66,7 @@ 'website_sale/static/src/scss/website_sale_dashboard.scss', 'website_sale/static/src/scss/website_sale_backend.scss', 'website_sale/static/src/js/tours/website_sale_shop_backend.js', + 'website_sale/static/src/js/systray_items/new_content.js', ], 'website.assets_wysiwyg': [ 'website_sale/static/src/scss/website_sale.editor.scss', diff --git a/addons/website_sale/static/src/js/systray_items/new_content.js b/addons/website_sale/static/src/js/systray_items/new_content.js new file mode 100644 index 00000000000..f1985c4d566 --- /dev/null +++ b/addons/website_sale/static/src/js/systray_items/new_content.js @@ -0,0 +1,16 @@ +/** @odoo-module **/ + +import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content'; +import { patch } from 'web.utils'; + +patch(NewContentModal.prototype, 'website_sale_new_content', { + setup() { + this._super(); + + const newProductElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_sale'); + newProductElement.createNewContent = () => this.createNewProduct(); + newProductElement.status = MODULE_STATUS.INSTALLED; + }, + + createNewProduct() {} +}); diff --git a/addons/website_slides/__manifest__.py b/addons/website_slides/__manifest__.py index abda6179c1b..bfbf938b2ce 100644 --- a/addons/website_slides/__manifest__.py +++ b/addons/website_slides/__manifest__.py @@ -70,6 +70,7 @@ Featuring 'website_slides/static/src/js/slide_category_one2many.js', 'website_slides/static/src/js/rating_field_backend.js', 'website_slides/static/src/models/*.js', + 'website_slides/static/src/js/systray_items/new_content.js', ], 'web.assets_frontend': [ 'website_slides/static/src/scss/website_slides.scss', diff --git a/addons/website_slides/static/src/js/systray_items/new_content.js b/addons/website_slides/static/src/js/systray_items/new_content.js index 284faca7ecf..c5ea11000bb 100644 --- a/addons/website_slides/static/src/js/systray_items/new_content.js +++ b/addons/website_slides/static/src/js/systray_items/new_content.js @@ -1,183 +1,16 @@ /** @odoo-module **/ -import { _t } from 'web.core'; -import Dialog from 'web.Dialog'; -import WebsiteNewMenu from 'website.newMenu'; -import SlidesCourseTagAdd from '@website_slides/js/slides_course_tag_add'; +import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content'; +import { patch } from 'web.utils'; -const TagCourseDialog = SlidesCourseTagAdd.TagCourseDialog; +patch(NewContentModal.prototype, 'website_slides_new_content', { + setup() { + this._super(); -export const ChannelCreateDialog = Dialog.extend({ - template: 'website.slide.channel.create', - xmlDependencies: Dialog.prototype.xmlDependencies.concat( - ['/website_slides/static/src/xml/website_slides_channel.xml', - '/website_slides/static/src/xml/website_slides_channel_tag.xml'] - ), - events: _.extend({}, Dialog.prototype.events, { - 'change input#tag_ids' : '_onChangeTag', - }), - custom_events: _.extend({}, Dialog.prototype.custom_events, { - 'tag_refresh': '_onTagRefresh', - 'tag_remove_new': '_onTagRemoveNew', - }), - /** - * @override - * @param {Object} parent - * @param {Object} options - */ - init: function (parent, options) { - options = _.defaults(options || {}, { - title: _t("New Course"), - size: 'medium', - buttons: [{ - text: _t("Create"), - classes: 'btn-primary', - click: this._onClickFormSubmit.bind(this) - }, { - text: _t("Discard"), - close: true - },] - }); - this._super(parent, options); - }, - start: function () { - var self = this; - return this._super.apply(this, arguments).then(function () { - var $input = self.$('#tag_ids'); - $input.select2({ - width: '100%', - allowClear: true, - formatNoMatches: false, - multiple: true, - selection_data: false, - formatSelection: function (data) { - if (data.tag) { - data.text = data.tag; - } - return data.text; - }, - createSearchChoice: function(term, data) { - var addedTags = $(this.opts.element).select2('data'); - if (_.filter(_.union(addedTags, data), function (tag) { - return tag.text.toLowerCase().localeCompare(term.toLowerCase()) === 0; - }).length === 0) { - if (this.opts.can_create) { - return { - id: _.uniqueId('tag_'), - create: true, - tag: term, - text: _.str.sprintf(_t("Create new Tag '%s'"), term), - }; - } else { - return undefined; - } - } - }, - fill_data: function (query, data) { - var that = this, - tags = {results: []}; - _.each(data, function (obj) { - if (that.matcher(query.term, obj.name)) { - tags.results.push({id: obj.id, text: obj.name}); - } - }); - query.callback(tags); - }, - query: function (query) { - var that = this; - // fetch data only once and store it - if (!this.selection_data) { - self._rpc({ - route: '/slides/channel/tag/search_read', - params: { - fields: ['name'], - domain: [], - } - }).then(function (data) { - that.can_create = data.can_create; - that.fill_data(query, data.read_results); - that.selection_data = data.read_results; - }); - } else { - this.fill_data(query, this.selection_data); - } - } - }); - }); - }, - _onClickFormSubmit: function (ev) { - var $form = this.$("#slide_channel_add_form"); - var $title = this.$("#title"); - if (!$title[0].value){ - $title.addClass('border-danger'); - this.$("#title-required").removeClass('d-none'); - } else { - $form.submit(); - } - }, - _onChangeTag: function (ev) { - var self = this; - var tags = $(ev.currentTarget).select2('data'); - tags.forEach(function (element) { - if (element.create) { - new TagCourseDialog(self, { defaultTag: element.text }).open(); - } - }); - }, - /** - * Replace the new tag ID by its real ID - * @param ev - * @private - */ - _onTagRefresh: function (ev) { - var $tag_ids = $('#tag_ids'); - var tags = $tag_ids.select2('data'); - tags.forEach(function (element) { - if (element.create) { - element.id = ev.data.tag_id; - element.create = false; - } - }); - $tag_ids.select2('data', tags); - // Set selection_data to false to force tag reload - $tag_ids.data('select2').opts.selection_data = false; - }, - /** - * Remove the created tag if the user clicks on 'Discard' on the create tag Dialog - * @private - */ - _onTagRemoveNew: function () { - var tags = $('#tag_ids').select2('data'); - tags = tags.filter(function (value) { - return !value.create; - }); - $('#tag_ids').select2('data', tags); + const newSlidesChannelElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_slides'); + newSlidesChannelElement.createNewContent = () => this.createNewSlidesChannel(); + newSlidesChannelElement.status = MODULE_STATUS.INSTALLED; }, -}); - -WebsiteNewMenu.include({ - actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, { - new_slide_channel: '_createNewSlideChannel', - }), - - //-------------------------------------------------------------------------- - // Actions - //-------------------------------------------------------------------------- - - /** - * Displays the popup to create a new slide channel, - * and redirects the user to this channel. - * - * @private - * @returns {Promise} Unresolved if there is a redirection - */ - _createNewSlideChannel: function () { - var self = this; - var def = new Promise(function (resolve) { - var dialog = new ChannelCreateDialog(self, {}); - dialog.open(); - dialog.on('closed', self, resolve); - }); - return def; - }, + + createNewSlidesChannel() {} });