diff --git a/addons/test_website/static/tests/tours/custom_snippets.js b/addons/test_website/static/tests/tours/custom_snippets.js index 1a770c46fcb..51d59135a71 100644 --- a/addons/test_website/static/tests/tours/custom_snippets.js +++ b/addons/test_website/static/tests/tours/custom_snippets.js @@ -42,7 +42,7 @@ wTourUtils.registerWebsitePreviewTour('test_custom_snippet', { }, { content: "confirm reload", - trigger: ".modal-dialog button span:contains('Save and Reload')", + trigger: ".modal-dialog button:contains('Save and Reload')", }, { content: "ensure custom snippet appeared", @@ -87,7 +87,7 @@ wTourUtils.registerWebsitePreviewTour('test_custom_snippet', { }, { content: "confirm delete", - trigger: ".modal-dialog button:has(span:contains('Yes'))", + trigger: ".modal-dialog button:contains('Yes')", }, { content: "ensure custom snippet disappeared", diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index 046c69f3e09..9fddb11cff3 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -2,7 +2,7 @@ import { Mutex } from "@web/core/utils/concurrency"; import { clamp } from "@web/core/utils/numbers"; -import Dialog from "@web/legacy/js/core/dialog"; +import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; import dom from "@web/legacy/js/core/dom"; import Widget from "@web/legacy/js/core/widget"; import { useDragAndDrop } from "@web_editor/js/editor/drag_and_drop"; @@ -1826,6 +1826,7 @@ var SnippetsMenu = Widget.extend({ this.orm = this.bindService("orm"); this.notification = this.bindService("notification"); + this.dialog = this.bindService("dialog"); }, /** * @override @@ -3923,54 +3924,38 @@ var SnippetsMenu = Widget.extend({ * @param {Event} ev */ _onInstallBtnClick: function (ev) { - var self = this; var $snippet = $(ev.currentTarget).closest('[data-module-id]'); var moduleID = $snippet.data('moduleId'); var name = $snippet.attr('name'); - new Dialog(this, { + const bodyText = _t("Do you want to install %s App?", name); + const linkText = _t("More info about this app."); + const linkUrl = '/web#id=' + encodeURIComponent(moduleID) + '&view_type=form&model=ir.module.module&action=base.open_module_tree'; + this.dialog.add(ConfirmationDialog, { title: _t("Install %s", name), - size: 'medium', - $content: $('
', {text: _t("Do you want to install the %s App?", name)}).append( - $('', { - target: '_blank', - href: '/web#id=' + encodeURIComponent(moduleID) + '&view_type=form&model=ir.module.module&action=base.open_module_tree', - text: _t("More info about this app."), - class: 'ml4', - }) - ), - buttons: [{ - text: _t("Save and Install"), - classes: 'btn-primary', - click: function () { - this.$footer.find('.btn').toggleClass('o_hidden'); - this.orm.call("ir.module.module", "button_immediate_install", [[moduleID]]).then(() => { - self.trigger_up('request_save', { - invalidateSnippetCache: true, - _toMutex: true, - reloadWebClient: true, - }); - }).catch(reason => { - if (reason instanceof RPCError) { - this.close(); - const message = markup(_t("Could not install module %s", escape(name))); - self.notification.add(message, { - type: 'danger', - sticky: true, - }); - } else { - return Promise.reject(reason); - } + body: markup(`${escape(bodyText)}\n${escape(linkText)}`), + confirm: async () => { + try { + await this.orm.call("ir.module.module", "button_immediate_install", [[moduleID]]); + this.trigger_up('request_save', { + invalidateSnippetCache: true, + _toMutex: true, + reloadWebClient: true, }); - }, - }, { - text: _t("Install in progress"), - icon: 'fa-spin fa-circle-o-notch fa-spin mr8', - classes: 'btn-primary disabled o_hidden', - }, { - text: _t("Cancel"), - close: true, - }], - }).open(); + } catch (e) { + if (e instanceof RPCError) { + const message = escape(_t("Could not install module %s", name)); + this.notification.add(message, { + type: "danger", + sticky: true, + }); + } else { + throw e; + } + } + }, + confirmLabel: _t("Save and Install"), + cancel: () => {}, + }); }, /** * @private @@ -4012,26 +3997,20 @@ var SnippetsMenu = Widget.extend({ const $snippet = $(ev.target).closest('.oe_snippet'); const snippetId = parseInt(ev.currentTarget.dataset.snippetId); ev.stopPropagation(); - new Dialog(this, { - size: 'medium', - title: _t('Confirmation'), - $content: $('

' + _t("Are you sure you want to delete the snippet: %s?", $snippet.attr('name')) + '

'), - buttons: [{ - text: _t("Yes"), - close: true, - classes: 'btn-primary', - click: async () => { - await this.orm.call("ir.ui.view", "delete_snippet", [], { - 'view_id': snippetId, - 'template_key': this.options.snippets, - }); - await this._loadSnippetsTemplates(true); - }, - }, { - text: _t("No"), - close: true, - }], - }).open(); + const message = _t("Are you sure you want to delete the snippet %s?", $snippet[0].getAttribute("name")); + this.dialog.add(ConfirmationDialog, { + body: message, + confirm: async () => { + await this.orm.call("ir.ui.view", "delete_snippet", [], { + 'view_id': snippetId, + 'template_key': this.options.snippets, + }); + await this._loadSnippetsTemplates(true); + }, + cancel: () => null, + confirmLabel: _t("Yes"), + cancelLabel: _t("No"), + }); }, /** * @private diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 2807f3db9bb..394ed1e57f2 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -2,7 +2,7 @@ import { attachComponent } from "@web/legacy/utils"; import { MediaDialog } from "@web_editor/components/media_dialog/media_dialog"; -import Dialog from "@web/legacy/js/core/dialog"; +import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; import dom from "@web/legacy/js/core/dom"; import { throttleForAnimation, debounce } from "@web/core/utils/timing"; import { clamp } from "@web/core/utils/numbers"; @@ -3228,6 +3228,8 @@ const SnippetOptionWidget = Widget.extend({ this._userValueWidgets = []; this._actionQueues = new Map(); + + this.dialog = this.bindService("dialog"); }, /** * @override @@ -4303,9 +4305,10 @@ const SnippetOptionWidget = Widget.extend({ const warnMessage = await this._checkIfWidgetsUpdateNeedWarning(widgets); if (warnMessage) { const okWarning = await new Promise(resolve => { - Dialog.confirm(this, warnMessage, { - confirm_callback: () => resolve(true), - cancel_callback: () => resolve(false), + this.dialog.add(ConfirmationDialog, { + body: warnMessage, + confirm: () => resolve(true), + cancel: () => resolve(false), }); }); if (!okWarning) { @@ -8750,65 +8753,56 @@ registry.SnippetSave = SnippetOptionWidget.extend({ */ saveSnippet: function (previewMode, widgetValue, params) { return new Promise(resolve => { - Dialog.confirm(this, _t("To save a snippet, we need to save all your previous modifications and reload the page."), { - cancel_callback: () => resolve(false), - buttons: [ - { - text: _t("Save and Reload"), - classes: 'btn-primary', - close: true, - click: () => { - const isButton = this.$target[0].matches("a.btn"); - const snippetKey = !isButton ? this.$target[0].dataset.snippet : "s_button"; - let thumbnailURL; - this.trigger_up('snippet_thumbnail_url_request', { - key: snippetKey, - onSuccess: url => thumbnailURL = url, - }); - let context; - this.trigger_up('context_get', { - callback: ctx => context = ctx, - }); - this.trigger_up('request_save', { - reloadEditor: true, - invalidateSnippetCache: true, - onSuccess: async () => { - const defaultSnippetName = !isButton - ? _t("Custom %s", this.data.snippetName) - : _t("Custom Button"); - const targetCopyEl = this.$target[0].cloneNode(true); - delete targetCopyEl.dataset.name; - if (isButton) { - targetCopyEl.classList.remove("mb-2"); - targetCopyEl.classList.add("o_snippet_drop_in_only", "s_custom_button"); - } - // By the time onSuccess is called after request_save, the - // current widget has been destroyed and is orphaned, so this._rpc - // will not work as it can't trigger_up. For this reason, we need - // to bypass the service provider and use the global RPC directly - await jsonrpc(`/web/dataset/call_kw/ir.ui.view/save_snippet`, { - model: "ir.ui.view", - method: "save_snippet", - args: [], - kwargs: { - 'name': defaultSnippetName, - 'arch': targetCopyEl.outerHTML, - 'template_key': this.options.snippets, - 'snippet_key': snippetKey, - 'thumbnail_url': thumbnailURL, - 'context': context, - }, - }); + this.dialog.add(ConfirmationDialog, { + body: _t("To save a snippet, we need to save all your previous modifications and reload the page."), + cancel: () => resolve(false), + confirmLabel: _t("Save and Reload"), + confirm: () => { + const isButton = this.$target[0].matches("a.btn"); + const snippetKey = !isButton ? this.$target[0].dataset.snippet : "s_button"; + let thumbnailURL; + this.trigger_up('snippet_thumbnail_url_request', { + key: snippetKey, + onSuccess: url => thumbnailURL = url, + }); + let context; + this.trigger_up('context_get', { + callback: ctx => context = ctx, + }); + this.trigger_up('request_save', { + reloadEditor: true, + invalidateSnippetCache: true, + onSuccess: async () => { + const defaultSnippetName = !isButton + ? _t("Custom %s", this.data.snippetName) + : _t("Custom Button"); + const targetCopyEl = this.$target[0].cloneNode(true); + delete targetCopyEl.dataset.name; + if (isButton) { + targetCopyEl.classList.remove("mb-2"); + targetCopyEl.classList.add("o_snippet_drop_in_only", "s_custom_button"); + } + // By the time onSuccess is called after request_save, the + // current widget has been destroyed and is orphaned, so this._rpc + // will not work as it can't trigger_up. For this reason, we need + // to bypass the service provider and use the global RPC directly + await jsonrpc(`/web/dataset/call_kw/ir.ui.view/save_snippet`, { + model: "ir.ui.view", + method: "save_snippet", + args: [], + kwargs: { + 'name': defaultSnippetName, + 'arch': targetCopyEl.outerHTML, + 'template_key': this.options.snippets, + 'snippet_key': snippetKey, + 'thumbnail_url': thumbnailURL, + 'context': context, }, }); - resolve(true); - } - }, { - text: _t("Cancel"), - close: true, - click: () => resolve(false), - } - ] + }, + }); + resolve(true); + }, }); }); }, diff --git a/addons/website/static/src/js/editor/snippets.editor.js b/addons/website/static/src/js/editor/snippets.editor.js index da2c92a1feb..7694bfc3aa1 100644 --- a/addons/website/static/src/js/editor/snippets.editor.js +++ b/addons/website/static/src/js/editor/snippets.editor.js @@ -1,12 +1,13 @@ /** @odoo-modules **/ import { _t } from "@web/core/l10n/translation"; -import Dialog from "@web/legacy/js/core/dialog"; +import { Dialog } from "@web/core/dialog/dialog"; +import { useChildRef } from "@web/core/utils/hooks"; import weSnippetEditor from "@web_editor/js/editor/snippets.editor"; import wSnippetOptions from "@website/js/editor/snippets.options"; import wUtils from "@website/js/utils"; import * as OdooEditorLib from "@web_editor/js/editor/odoo-editor/src/utils/utils"; -import { renderToElement } from "@web/core/utils/render"; +import { Component, onMounted, useRef, useState } from "@odoo/owl"; import { throttleForAnimation } from "@web/core/utils/timing"; import { applyTextHighlight, @@ -14,7 +15,6 @@ import { switchTextHighlight, getCurrentTextHighlight } from "@website/js/text_processing"; - const getDeepRange = OdooEditorLib.getDeepRange; const getTraversedNodes = OdooEditorLib.getTraversedNodes; @@ -52,6 +52,7 @@ const wSnippetMenu = weSnippetEditor.SnippetsMenu.extend({ init() { this._super(...arguments); this.notification = this.bindService("notification"); + this.dialog = this.bindService("dialog"); }, /** * @override @@ -192,43 +193,53 @@ const wSnippetMenu = weSnippetEditor.SnippetsMenu.extend({ $apiKeyHelp.empty().text(message); } - const $content = $(renderToElement('website.s_google_map_modal', { - apiKey: apiKey, - })); - if (!apiKeyValidation.isValid && apiKeyValidation.message) { - applyError.call($content, apiKeyValidation.message); - } + const GoogleMapAPIKeyDialog = class extends Component { + static template = "website.s_google_map_modal"; + static components = { Dialog }; + static props = { + onMounted: Function, + close: Function, + confirm: Function + }; + setup() { + this.modalRef = useChildRef(); + this.state = useState({ apiKey: apiKey }); + this.apiKeyInput = useRef("apiKeyInput"); + onMounted(() => this.props.onMounted(this.modalRef)); + } + onClickSave() { + this.props.confirm(this.modalRef, this.state.apiKey); + } + }; return new Promise(resolve => { let invalidated = false; - const dialog = new Dialog(this, { - size: 'medium', - title: _t("Google Map API Key"), - buttons: [ - {text: _t("Save"), classes: 'btn-primary', click: async (ev) => { - const valueAPIKey = dialog.$('#api_key_input').val(); - if (!valueAPIKey) { - applyError.call(dialog.$el, _t("Enter an API Key")); - return; - } - const $button = $(ev.currentTarget); - $button.prop('disabled', true); - const res = await this._validateGMapAPIKey(valueAPIKey); - if (res.isValid) { - await this.orm.write("website", [websiteId], { google_maps_api_key: valueAPIKey }); - invalidated = true; - dialog.close(); - } else { - applyError.call(dialog.$el, res.message); - } - $button.prop("disabled", false); - }}, - {text: _t("Cancel"), close: true} - ], - $content: $content, + this.dialog.add(GoogleMapAPIKeyDialog, { + onMounted: (modalRef) => { + if (!apiKeyValidation.isValid && apiKeyValidation.message) { + applyError.call($(modalRef.el), apiKeyValidation.message); + } + }, + confirm: async (modalRef, valueAPIKey) => { + if (!valueAPIKey) { + applyError.call($(modalRef.el), _t("Enter an API Key")); + return; + } + const $button = $(modalRef.el).find("button"); + $button.prop('disabled', true); + const res = await this._validateGMapAPIKey(valueAPIKey); + if (res.isValid) { + await this.orm.write("website", [websiteId], {google_maps_api_key: valueAPIKey}); + invalidated = true; + return true; + } else { + applyError.call($(modalRef.el), res.message); + } + $button.prop("disabled", false); + } + }, { + onClose: () => resolve(invalidated), }); - dialog.on('closed', this, () => resolve(invalidated)); - dialog.open(); }); }, /** diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index a580bdf7d23..42f632aadf2 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -1,7 +1,9 @@ /** @odoo-module **/ import { loadCSS } from "@web/core/assets"; -import Dialog from "@web/legacy/js/core/dialog"; +import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; +import { Dialog } from "@web/core/dialog/dialog"; +import { useChildRef } from "@web/core/utils/hooks"; import weUtils from "@web_editor/js/common/utils"; import options from "@web_editor/js/editor/snippets.options"; import { NavbarLinkPopoverWidget } from "@website/js/widgets/link_popover_widget"; @@ -26,7 +28,7 @@ import { drawTextHighlightSVG, } from "@website/js/text_processing"; -import { markup } from "@odoo/owl"; +import { Component, markup, useRef, useState } from "@odoo/owl"; const InputUserValueWidget = options.userValueWidgetsRegistry['we-input']; const SelectUserValueWidget = options.userValueWidgetsRegistry['we-select']; @@ -147,6 +149,13 @@ const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({ }), fontVariables: [], // Filled by editor menu when all options are loaded + /** + * @override + */ + init() { + this.dialog = this.bindService("dialog"); + return this._super(...arguments); + }, /** * @override */ @@ -272,81 +281,92 @@ const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({ * @private */ _onAddGoogleFontClick: function (ev) { + const addGoogleFontDialog = class extends Component { + static template = "website.dialog.addGoogleFont"; + static components = { Dialog }; + static props = { close: Function, title: String, onClickSave: Function }; + title = _t("Add a Google Font"); + state = useState({ valid: true, loading: false, googleServe: true }); + fontInput = useRef("fontInput"); + async onClickSave() { + if (this.state.loading) { + return; + } + this.state.loading = true; + const shouldClose = await this.props.onClickSave(this.state, this.fontInput.el); + if (shouldClose) { + this.props.close(); + return; + } + this.state.loading = false; + } + onClickCancel() { + this.props.close(); + } + }; const variable = $(ev.currentTarget).data('variable'); - const dialog = new Dialog(this, { + this.dialog.add(addGoogleFontDialog, { title: _t("Add a Google Font"), - $content: $(renderToElement('website.dialog.addGoogleFont')), - buttons: [ - { - text: _t("Save & Reload"), - classes: 'btn-primary', - click: async () => { - const inputEl = dialog.el.querySelector('.o_input_google_font'); - // if font page link (what is expected) - let m = inputEl.value.match(/\bspecimen\/([\w+]+)/); - if (!m) { - // if embed code (so that it works anyway if the user put the embed code instead of the page link) - m = inputEl.value.match(/\bfamily=([\w+]+)/); - if (!m) { - inputEl.classList.add('is-invalid'); - return; - } - } + onClickSave: async (state, inputEl) => { + // if font page link (what is expected) + let m = inputEl.value.match(/\bspecimen\/([\w+]+)/); + if (!m) { + // if embed code (so that it works anyway if the user put the embed code instead of the page link) + m = inputEl.value.match(/\bfamily=([\w+]+)/); + if (!m) { + inputEl.classList.add('is-invalid'); + return; + } + } - let isValidFamily = false; + let isValidFamily = false; - try { - // Font family is an encoded query parameter: - // "Open+Sans" needs to remain "Open+Sans". - const result = await fetch("https://fonts.googleapis.com/css?family=" + m[1] + ':300,300i,400,400i,700,700i', {method: 'HEAD'}); - // Google fonts server returns a 400 status code if family is not valid. - if (result.ok) { - isValidFamily = true; - } - } catch (error) { - console.error(error); - } + try { + // Font family is an encoded query parameter: + // "Open+Sans" needs to remain "Open+Sans". + const result = await fetch("https://fonts.googleapis.com/css?family=" + m[1] + ':300,300i,400,400i,700,700i', {method: 'HEAD'}); + // Google fonts server returns a 400 status code if family is not valid. + if (result.ok) { + isValidFamily = true; + } + } catch (error) { + console.error(error); + } - if (!isValidFamily) { - inputEl.classList.add('is-invalid'); - return; - } + if (!isValidFamily) { + inputEl.classList.add('is-invalid'); + return; + } - const font = m[1].replace(/\+/g, ' '); - const googleFontServe = dialog.el.querySelector('#google_font_serve').checked; - const fontName = `'${font}'`; - // If the font already exists, it will only be added if - // the user chooses to add it locally when it is already - // imported from the Google Fonts server. - const fontExistsLocally = this.googleLocalFonts.some(localFont => localFont.split(':')[0] === fontName); - const fontExistsOnServer = this.allFonts.includes(fontName); - const preventFontAddition = fontExistsLocally || (fontExistsOnServer && googleFontServe); - if (preventFontAddition) { - inputEl.classList.add('is-invalid'); - // Show custom validity error message. - inputEl.setCustomValidity(_t("This font already exists, you can only add it as a local font to replace the server version.")); - inputEl.reportValidity(); - return; - } - if (googleFontServe) { - this.googleFonts.push(font); - } else { - this.googleLocalFonts.push(`'${font}': ''`); - } - this.trigger_up('google_fonts_custo_request', { - values: {[variable]: `'${font}'`}, - googleFonts: this.googleFonts, - googleLocalFonts: this.googleLocalFonts, - }); - }, - }, - { - text: _t("Discard"), - close: true, - }, - ], + const font = m[1].replace(/\+/g, ' '); + const googleFontServe = state.googleServe; + const fontName = `'${font}'`; + // If the font already exists, it will only be added if + // the user chooses to add it locally when it is already + // imported from the Google Fonts server. + const fontExistsLocally = this.googleLocalFonts.some(localFont => localFont.split(':')[0] === fontName); + const fontExistsOnServer = this.allFonts.includes(fontName); + const preventFontAddition = fontExistsLocally || (fontExistsOnServer && googleFontServe); + if (preventFontAddition) { + inputEl.classList.add('is-invalid'); + // Show custom validity error message. + inputEl.setCustomValidity(_t("This font already exists, you can only add it as a local font to replace the server version.")); + inputEl.reportValidity(); + return; + } + if (googleFontServe) { + this.googleFonts.push(font); + } else { + this.googleLocalFonts.push(`'${font}': ''`); + } + this.trigger_up('google_fonts_custo_request', { + values: {[variable]: `'${font}'`}, + googleFonts: this.googleFonts, + googleLocalFonts: this.googleLocalFonts, + }); + return true; + }, }); - dialog.open(); }, /** * @private @@ -357,9 +377,10 @@ const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({ const values = {}; const save = await new Promise(resolve => { - Dialog.confirm(this, _t("Deleting a font requires a reload of the page. This will save all your changes and reload the page, are you sure you want to proceed?"), { - confirm_callback: () => resolve(true), - cancel_callback: () => resolve(false), + this.dialog.add(ConfirmationDialog, { + body: _t("Deleting a font requires a reload of the page. This will save all your changes and reload the page, are you sure you want to proceed?"), + confirm: () => resolve(true), + cancel: () => resolve(false), }); }); if (!save) { @@ -1399,9 +1420,10 @@ options.registry.OptionsTab = options.Class.extend({ */ async switchTheme(previewMode, widgetValue, params) { const save = await new Promise(resolve => { - Dialog.confirm(this, _t("Changing theme requires to leave the editor. This will save all your changes, are you sure you want to proceed? Be careful that changing the theme will reset all your color customizations."), { - confirm_callback: () => resolve(true), - cancel_callback: () => resolve(false), + this.dialog.add(ConfirmationDialog, { + body: _t("Changing theme requires to leave the editor. This will save all your changes, are you sure you want to proceed? Be careful that changing the theme will reset all your color customizations."), + confirm: () => resolve(true), + cancel: () => resolve(false), }); }); if (!save) { @@ -1420,14 +1442,11 @@ options.registry.OptionsTab = options.Class.extend({ // the dialog box 'action_view_base_language_install' const websiteId = this.options.context.website_id; const save = await new Promise((resolve) => { - Dialog.confirm( - this, - _t("Adding a language requires to leave the editor. This will save all your changes, are you sure you want to proceed?"), - { - confirm_callback: () => resolve(true), - cancel_callback: () => resolve(false), - } - ); + this.dialog.add(ConfirmationDialog, { + body: _t("Adding a language requires to leave the editor. This will save all your changes, are you sure you want to proceed?"), + confirm: () => resolve(true), + cancel: () => resolve(false), + }); }); if (!save) { return; @@ -2691,51 +2710,54 @@ options.registry.anchor = options.Class.extend({ * @param {Element} buttonEl */ _openAnchorDialog(buttonEl) { - var self = this; - var buttons = [{ - text: _t("Save & copy"), - classes: 'btn-primary', - click: function () { - var $input = this.$('.o_input_anchor_name'); - var anchorName = self._text2Anchor($input.val()); - if (self.$target[0].id === anchorName) { + const anchorDialog = class extends Component { + static template = "website.dialog.anchorName"; + static props = { close: Function, confirm: Function, delete: Function, currentAnchor: String }; + static components = { Dialog }; + title = _t("Link Anchor"); + modalRef = useChildRef(); + onClickConfirm() { + const shouldClose = this.props.confirm(this.modalRef); + if (shouldClose) { + this.props.close(); + } + } + onClickDelete() { + this.props.delete(); + this.props.close(); + } + onClickDiscard() { + this.props.close(); + } + }; + const props = { + confirm: (modalRef) => { + const inputEl = modalRef.el.querySelector(".o_input_anchor_name"); + const anchorName = this._text2Anchor(inputEl.value); + if (this.$target[0].id === anchorName) { // If the chosen anchor name is already the one used by the // element, close the dialog and do nothing else this.close(); return; } - const alreadyExists = !!document.getElementById(anchorName); - this.$('.o_anchor_already_exists').toggleClass('d-none', !alreadyExists); - $input.toggleClass('is-invalid', alreadyExists); + const alreadyExists = !!this.$target[0].ownerDocument.getElementById(anchorName); + modalRef.el.querySelector('.o_anchor_already_exists').classList.toggle('d-none', !alreadyExists); + inputEl.classList.toggle('is-invalid', alreadyExists); if (!alreadyExists) { - self._setAnchorName(anchorName); - this.close(); + this._setAnchorName(anchorName); buttonEl.click(); + return true; } }, - }, { - text: _t("Discard"), - close: true, - }]; + currentAnchor: decodeURIComponent(this.$target.attr('id')), + }; if (this.$target.attr('id')) { - buttons.push({ - text: _t("Remove"), - classes: 'btn-link ms-auto', - icon: 'fa-trash', - close: true, - click: function () { - self._setAnchorName(); - }, - }); + props["delete"] = () => { + this._setAnchorName(); + }; } - new Dialog(this, { - title: _t("Link Anchor"), - $content: $(renderToElement('website.dialog.anchorName', { - currentAnchor: decodeURIComponent(this.$target.attr('id')), - })), - buttons: buttons, - }).open(); + this.dialog.add(anchorDialog, props); }, /** * @private diff --git a/addons/website/static/src/js/user_custom_javascript.js b/addons/website/static/src/js/user_custom_javascript.js index 5d9827033dc..79d0188ae28 100644 --- a/addons/website/static/src/js/user_custom_javascript.js +++ b/addons/website/static/src/js/user_custom_javascript.js @@ -7,14 +7,17 @@ // world !' message in a popup: // /* -import Dialog from '@web/legacy/js/core/dialog'; +import { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog'; import publicWidget from '@web/legacy/js/public/public_widget'; publicWidget.registry.HelloWorldPopup = publicWidget.Widget.extend({ selector: '#wrapwrap', - start: function () { - Dialog.alert(this, "Hello, world!"); + init() { + this.dialog = this.bindService("dialog"); + }, + start() { + this.dialog.add(ConfirmationDialog, { body: 'Hello World' }); return this._super.apply(this, arguments); }, }); diff --git a/addons/website/static/src/snippets/s_embed_code/options.js b/addons/website/static/src/snippets/s_embed_code/options.js index 06663209dd1..a97227fba7c 100644 --- a/addons/website/static/src/snippets/s_embed_code/options.js +++ b/addons/website/static/src/snippets/s_embed_code/options.js @@ -1,12 +1,31 @@ /** @odoo-module **/ - -import Dialog from '@web/legacy/js/core/dialog'; +import { Dialog } from "@web/core/dialog/dialog"; +import { CodeEditor } from "@web/core/code_editor/code_editor"; import options from '@web_editor/js/editor/snippets.options'; -import { loadBundle } from "@web/core/assets"; import { _t } from "@web/core/l10n/translation"; -import { renderToElement } from "@web/core/utils/render"; + +import { Component, useState } from "@odoo/owl"; + +class CodeEditorDialog extends Component { + static template = "website.s_embed_code_dialog"; + static components = { Dialog, CodeEditor }; + setup() { + this.state = useState({ value: this.props.value }); + } + onCodeChange(newValue) { + this.state.value = newValue; + } + onConfirm() { + this.props.confirm(this.state.value); + this.props.close(); + } +} options.registry.EmbedCode = options.Class.extend({ + init() { + this._super(...arguments); + this.dialog = this.bindService("dialog"); + }, //-------------------------------------------------------------------------- // Options //-------------------------------------------------------------------------- @@ -15,72 +34,19 @@ options.registry.EmbedCode = options.Class.extend({ const $container = this.$target.find('.s_embed_code_embedded'); const code = $container.html().trim(); - await loadBundle({ - jsLibs: [ - '/web/static/lib/ace/ace.js', - '/web/static/lib/ace/mode-xml.js', - '/web/static/lib/ace/mode-qweb.js', - ], - }); - await new Promise(resolve => { - const $content = $(renderToElement('website.custom_code_dialog_content')); - const aceEditor = this._renderAceEditor($content.find('.o_ace_editor_container')[0], code || ''); - const dialog = new Dialog(this, { + this.dialog.add(CodeEditorDialog, { title: _t("Edit embedded code"), - $content, - buttons: [ - { - text: _t("Save"), - classes: 'btn-primary', - click: async () => { - $container[0].innerHTML = aceEditor.getValue(); - }, - close: true, - }, - { - text: _t("Discard"), - close: true, - }, - ], + value: code, + mode: "xml", + confirm: (newValue) => { + $container[0].innerHTML = newValue; + } + }, { + onClose: resolve, }); - dialog.on('closed', this, resolve); - dialog.open(); }); }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - * @param {DOMElement} node - * @param {String} content text of the editor - * @returns {Object} - */ - _renderAceEditor(node, content) { - const aceEditor = window.ace.edit(node); - aceEditor.setTheme('ace/theme/monokai'); - aceEditor.setValue(content, 1); - aceEditor.setOptions({ - minLines: 20, - maxLines: Infinity, - showPrintMargin: false, - }); - aceEditor.renderer.setOptions({ - highlightGutterLine: true, - showInvisibles: true, - fontSize: 14, - }); - - const aceSession = aceEditor.getSession(); - aceSession.setOptions({ - mode: "ace/mode/xml", - useWorker: false, - }); - return aceEditor; - }, }); export default { diff --git a/addons/website/static/src/snippets/s_website_form/options.js b/addons/website/static/src/snippets/s_website_form/options.js index 6bc61e7c129..5f9e5964bc0 100644 --- a/addons/website/static/src/snippets/s_website_form/options.js +++ b/addons/website/static/src/snippets/s_website_form/options.js @@ -2,8 +2,7 @@ import FormEditorRegistry from "@website/js/form_editor_registry"; import options from "@web_editor/js/editor/snippets.options"; -import Dialog from "@web/legacy/js/core/dialog"; -import dom from "@web/legacy/js/core/dom"; +import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; import weUtils from "@web_editor/js/common/utils"; import "@website/js/editor/snippets.options"; import { unique } from "@web/core/utils/arrays"; @@ -365,6 +364,7 @@ options.registry.WebsiteFormEditor = FormEditor.extend({ init() { this._super(...arguments); this.notification = this.bindService("notification"); + this.dialog = this.bindService("dialog"); }, /** * @override @@ -509,42 +509,25 @@ options.registry.WebsiteFormEditor = FormEditor.extend({ promptSaveRedirect: function (name, value, widgetValue) { return new Promise((resolve, reject) => { const message = _t("Would you like to save before being redirected? Unsaved changes will be discarded."); - Dialog.confirm(this, message, { - cancel_callback: () => resolve(), - buttons: [ - { - text: _t("Save"), - classes: 'btn-primary', - click: (ev) => { - const restore = dom.addButtonLoadingEffect(ev.currentTarget); - this.trigger_up('request_save', { - reload: false, - onSuccess: () => { - this._redirectToAction(value); - }, - onFailure: () => { - restore(); - this.notification.add(_t("Something went wrong."), { - type: 'danger', - sticky: true, - }); - reject(); - }, - }); - resolve(); - }, - }, { - text: _t("Discard"), - click: (ev) => { - dom.addButtonLoadingEffect(ev.currentTarget); + this.dialog.add(ConfirmationDialog, { + body: message, + confirmLabel: _t("Save"), + confirm: () => { + this.trigger_up('request_save', { + reload: false, + onSuccess: () => { this._redirectToAction(value); }, - }, { - text: _t("Cancel"), - close: true, - click: () => resolve(), - }, - ], + onFailure: () => { + this.notification.add(_t("Something went wrong."), { + type: 'danger', + sticky: true, + }); + reject(); + }, + }); + }, + cancel: () => resolve(), }); }); }, @@ -1135,7 +1118,9 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({ this._setVisibilityDependency(firstValue); return; } - Dialog.confirm(this, _t("There is no field available for this option.")); + this.dialog.add(ConfirmationDialog, { + body: _t("There is no field available for this option."), + }); } this._deleteConditionalVisibility(this.$target[0]); }, diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml index 768e2864ab5..1389e9a6455 100644 --- a/addons/website/static/src/xml/website.editor.xml +++ b/addons/website/static/src/xml/website.editor.xml @@ -1,36 +1,57 @@ -
-
- -
- -
-

The chosen name already exists

+ + +
+ +
+ +
+

The chosen name already exists

+
-
-
+ + + + + + + -
-
- -
-