diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js index bb522992c2f481c9b77ac1194a50385723343ea2..6e9620421c3918e08d654b984205d27d5bec95d6 100644 --- a/addons/web/static/src/js/core/dialog.js +++ b/addons/web/static/src/js/core/dialog.js @@ -4,6 +4,7 @@ odoo.define('web.Dialog', function (require) { var core = require('web.core'); var dom = require('web.dom'); var Widget = require('web.Widget'); +const OwlDialog = require('web.OwlDialog'); var QWeb = core.qweb; var _t = core._t; @@ -205,6 +206,9 @@ var Dialog = Widget.extend({ if (options && options.shouldFocusButtons) { self._onFocusControlButton(); } + + // Notifies OwlDialog to adjust focus/active properties on owl dialogs + OwlDialog.display(self); }); return self; @@ -236,6 +240,9 @@ var Dialog = Widget.extend({ return; } + // Notifies OwlDialog to adjust focus/active properties on owl dialogs + OwlDialog.hide(this); + // Triggers the onForceClose event if the callback is defined if (this.onForceClose) { this.onForceClose(); diff --git a/addons/web/static/src/js/core/owl_dialog.js b/addons/web/static/src/js/core/owl_dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..a6ee3c7b1d8a6bf194f93fad7fd39cd663176cca --- /dev/null +++ b/addons/web/static/src/js/core/owl_dialog.js @@ -0,0 +1,259 @@ +odoo.define('web.OwlDialog', function (require) { + "use strict"; + + const { useExternalListener } = require('web.custom_hooks'); + + const { Component, hooks, misc } = owl; + const { Portal } = misc; + const { useRef } = hooks; + const SIZE_CLASSES = { + 'extra-large': 'modal-xl', + 'large': 'modal-lg', + 'small': 'modal-sm', + }; + + /** + * Dialog (owl version) + * + * Represents a bootstrap-styled dialog handled with pure JS. Its implementation + * is roughly the same as the legacy dialog, the only exception being the buttons. + * @extends Component + **/ + class Dialog extends Component { + /** + * @param {Object} [props] + * @param {(boolean|string)} [props.backdrop='static'] The kind of modal backdrop + * to use (see Bootstrap documentation). + * @param {string} [props.contentClass] Class to add to the dialog + * @param {boolean} [props.fullscreen=false] Whether the dialog should be + * open in fullscreen mode (the main usecase is mobile). + * @param {boolean} [props.renderFooter=true] Whether the dialog footer + * should be rendered. + * @param {boolean} [props.renderHeader=true] Whether the dialog header + * should be rendered. + * @param {string} [props.size='large'] 'extra-large', 'large', 'medium' + * or 'small'. + * @param {string} [props.subtitle=''] + * @param {string} [props.title='Odoo'] + * @param {boolean} [props.technical=true] If set to false, the modal will have + * the standard frontend style (use this for non-editor frontend features). + */ + constructor() { + super(...arguments); + + this.modalRef = useRef('modal'); + this.footerRef = useRef('modal-footer'); + + useExternalListener(window, 'keydown', this._onKeydown); + } + + mounted() { + this.constructor.display(this); + + this.env.bus.on('close_dialogs', this, this._close); + + if (this.props.renderFooter) { + // Set up main button : will first look for an element with the + // 'btn-primary' class, then a 'btn' class, then the first button + // element. + let mainButton = this.footerRef.el.querySelector('.btn.btn-primary'); + if (!mainButton) { + mainButton = this.footerRef.el.querySelector('.btn'); + } + if (!mainButton) { + mainButton = this.footerRef.el.querySelector('button'); + } + if (mainButton) { + this.mainButton = mainButton; + this.mainButton.addEventListener('keydown', this._onMainButtonKeydown.bind(this)); + this.mainButton.focus(); + } + } + + this._removeTooltips(); + } + + async willUnmount() { + this.env.bus.off('close_dialogs', this, this._close); + + this._removeTooltips(); + + this.constructor.hide(this); + } + + //-------------------------------------------------------------------------- + // Getters + //-------------------------------------------------------------------------- + + /** + * @returns {string} + */ + get size() { + return SIZE_CLASSES[this.props.size]; + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Send an event signaling that the dialog must be closed. + * @private + */ + _close() { + this.trigger('dialog_closed'); + } + + /** + * Remove any existing tooltip present in the DOM. + * @private + */ + _removeTooltips() { + for (const tooltip of document.querySelectorAll('.tooltip')) { + tooltip.remove(); // remove open tooltip if any to prevent them staying when modal is opened + } + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onBackdropClick() { + if (this.props.backdrop === 'static') { + if (this.mainButton) { + this.mainButton.focus(); + } + } else { + this._close(); + } + } + + /** + * @private + */ + _onFocus() { + if (this.mainButton) { + this.mainButton.focus(); + } + } + + /** + * Manage the TAB key on the main button. If the focus is on a primary + * button and the user tries to tab to go to the next button : a tooltip + * will be displayed. + * @private + * @param {KeyboardEvent} ev + */ + _onMainButtonKeydown(ev) { + if (ev.key === 'Tab' && !ev.shiftKey) { + ev.preventDefault(); + $(this.mainButton) + .tooltip({ + delay: { show: 200, hide: 0 }, + title: () => this.env.qweb.render('DialogButton.tooltip', { + title: this.mainButton.innerText.toUpperCase(), + }), + trigger: 'manual', + }) + .tooltip('show'); + } + } + + /** + * @private + * @param {KeyboardEvent} ev + */ + _onKeydown(ev) { + if ( + ev.key === 'Escape' && + !['INPUT', 'TEXTAREA'].includes(ev.target.tagName) && + this.constructor.displayed[this.constructor.displayed.length - 1] === this + ) { + ev.preventDefault(); + ev.stopImmediatePropagation(); + ev.stopPropagation(); + this._close(); + } + } + + //-------------------------------------------------------------------------- + // Static + //-------------------------------------------------------------------------- + + /** + * Push the given dialog at the end of the displayed list then set it as + * active and all the others as passive. + * @param {(LegacyDialog|OwlDialog)} dialog + */ + static display(dialog) { + // Deactivate previous dialog + const activeDialogEl = document.querySelector('.modal.o_active_modal'); + if (activeDialogEl) { + activeDialogEl.classList.remove('o_active_modal'); + } + // Push dialog + this.displayed.push(dialog); + // Add active class + const modalEl = dialog instanceof this ? + // Owl dialog + dialog.modalRef.el : + // Legacy dialog + dialog.$modal[0]; + modalEl.classList.add('o_active_modal'); + // Update body class + document.body.classList.add('modal-open'); + } + + /** + * Set the given displayed dialog as passive and the last added displayed dialog + * as active, then remove it from the displayed list. + * @param {(LegacyDialog|OwlDialog)} dialog + */ + static hide(dialog) { + // Remove given dialog from the list + this.displayed.splice(this.displayed.indexOf(dialog), 1); + // Activate last dialog and update body class + const lastDialog = this.displayed[this.displayed.length - 1]; + if (lastDialog) { + lastDialog.el.focus(); + const modalEl = lastDialog instanceof this ? + // Owl dialog + lastDialog.modalRef.el : + // Legacy dialog + lastDialog.$modal[0]; + modalEl.classList.add('o_active_modal'); + } else { + document.body.classList.remove('modal-open'); + } + } + } + + Dialog.displayed = []; + + Dialog.components = { Portal }; + Dialog.defaultProps = { + backdrop: 'static', + renderFooter: true, + renderHeader: true, + size: 'large', + technical: true, + title: "Odoo", + }; + Dialog.props = { + backdrop: { validate: b => ['static', true, false].includes(b), optional: 1 }, + contentClass: { type: String, optional: 1 }, + fullscreen: { type: Boolean, optional: 1 }, + renderFooter: { type: Boolean, optional: 1 }, + renderHeader: { type: Boolean, optional: 1 }, + size: { validate: s => ['extra-large', 'large', 'medium', 'small'].includes(s), optional: 1 }, + subtitle: { type: String, optional: 1 }, + technical: { type: Boolean, optional: 1 }, + title: { type: String, optional: 1 }, + }; + Dialog.template = 'OwlDialog'; + + return Dialog; +}); diff --git a/addons/web/static/src/scss/modal.scss b/addons/web/static/src/scss/modal.scss index 3463d9e20ac50fba2687eeb082bf066bfad03c52..d82ddec6213b76f1e8ef1e15a02d46b68deda6bf 100644 --- a/addons/web/static/src/scss/modal.scss +++ b/addons/web/static/src/scss/modal.scss @@ -54,10 +54,9 @@ footer { > :not(:first-child) { margin-left: .25rem; } > :not(:last-child) { margin-right: .25rem; } - } - - button { - margin-bottom: .5rem; + button { + margin-bottom: .5rem; + } } } } @@ -107,9 +106,28 @@ } } +.modal:not(.o_active_modal) { + z-index: 1030; +} + +.o_dialog { + + > .modal { + display: block; + pointer-events: none; + } + + .modal-content { + pointer-events: auto; + } +} + body.modal-open { // Allow vertical scrolling in modals on iOS -webkit-overflow-scrolling: touch; + .modal { + @include o-scrollbar-overlay; + } } // Temporary fix for modals which are not instantiated thanks to the Dialog diff --git a/addons/web/static/src/scss/utils.scss b/addons/web/static/src/scss/utils.scss index 794b8bea90e782fa804cfef74b3c063a2651f399..1d410fc74756bcc314b1bc0220f6c75482da0185 100644 --- a/addons/web/static/src/scss/utils.scss +++ b/addons/web/static/src/scss/utils.scss @@ -385,6 +385,13 @@ } } +// Scrollbar doesn't overlap with content +@mixin o-scrollbar-overlay { + overflow-y: auto; + overflow-y: overlay; + -ms-overflow-style: -ms-autohiding-scrollbar; +} + %o-nocontent-init-image { content: ""; display: block; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index a3fdb09611c8897533bd38709e9d4d389908949e..961c11cdc90d9a7de8645e2ff8d3f31252394133 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1,6 +1,54 @@ <?xml version="1.0" encoding="UTF-8"?> <templates id="template" xml:space="preserve"> +<!-- Owl Templates --> + +<t t-name="DialogButton.tooltip" owl="1"> + <div class="oe_tooltip_string" role="tooltip"> + <div class="tooltip-inner"> + Hit ENTER to <t t-esc="title"/> + </div> + </div> +</t> + +<t t-name="OwlDialog" owl="1"> + <Portal target="'body'"> + <div class="o_dialog" t-on-focus="_onFocus"> + <div t-if="props.backdrop" class="modal-backdrop show" t-on-click="_onBackdropClick"/> + <div role="dialog" class="modal" + tabindex="-1" + t-att-class="{ o_technical_modal: props.technical, o_modal_full: props.fullscreen }" + t-ref="modal" + t-on-close_dialog.stop="_close" + > + <div class="modal-dialog" t-att-class="size"> + <div class="modal-content" t-att-class="props.contentClass"> + <header t-if="props.renderHeader" class="modal-header"> + <h4 class="modal-title"> + <t t-esc="props.title"/> + <span t-if="props.subtitle" class="o_subtitle text-muted small" t-esc="props.subtitle"/> + </h4> + <button type="button" class="close" aria-label="Close" tabindex="-1" t-on-click="_close">×</button> + </header> + <main class="modal-body"> + <t t-slot="default"/> + </main> + <footer t-if="props.renderFooter" class="modal-footer" t-ref="modal-footer"> + <t t-slot="buttons"> + <button class="btn btn-primary" t-on-click.prevent="_close"> + Ok + </button> + </t> + </footer> + </div> + </div> + </div> + </div> + </Portal> +</t> + +<!-- Legacy Templates --> + <div t-name="EmptyComponent"/> <div t-name="Loading" class="o_loading"/> diff --git a/addons/web/static/src/xml/dialog.xml b/addons/web/static/src/xml/dialog.xml index 645e6e2391ea59bb0566109316afe49f41b4cdb6..848f5a1e9b613573bbd8140a7d6820b685d31fa9 100644 --- a/addons/web/static/src/xml/dialog.xml +++ b/addons/web/static/src/xml/dialog.xml @@ -3,17 +3,25 @@ <!-- These templates are accessible in backend and frontend --> -<div role="dialog" t-name="Dialog" t-attf-class="modal#{technical ? ' o_technical_modal' : ''}#{fullscreen ? ' o_modal_full': ''}" tabindex="-1" data-backdrop="static" t-att-id="_.uniqueId('modal_')" aria-hidden="true"> - <div class="modal-dialog"> - <div class="modal-content"> - <header t-if="renderHeader" class="modal-header"> - <h4 class="modal-title"><t t-raw="title"/><span class="o_subtitle text-muted small"><t t-esc="subtitle"/></span></h4> - <button type="button" class="close" data-dismiss="modal" aria-label="Close" tabindex="-1">×</button> - </header> - <main class="modal-body"/> - <footer t-if="renderFooter" class="modal-footer"/> +<t t-name="Dialog"> + <div role="dialog" + t-attf-class="modal o_legacy_dialog #{ technical ? ' o_technical_modal' : '' } #{ fullscreen ? ' o_modal_full': '' }" + tabindex="-1" + data-backdrop="static" + t-att-id="_.uniqueId('modal_')" + aria-hidden="true" + > + <div class="modal-dialog"> + <div class="modal-content"> + <header t-if="renderHeader" class="modal-header"> + <h4 class="modal-title"><t t-raw="title"/><span class="o_subtitle text-muted small" t-esc="subtitle"/></h4> + <button type="button" class="close" data-dismiss="modal" aria-label="Close" tabindex="-1">×</button> + </header> + <main class="modal-body"/> + <footer t-if="renderFooter" class="modal-footer"/> + </div> </div> </div> -</div> +</t> </templates> diff --git a/addons/web/static/tests/core/owl_dialog_tests.js b/addons/web/static/tests/core/owl_dialog_tests.js new file mode 100644 index 0000000000000000000000000000000000000000..8114408bf4890e06b4a2590f3fdfe5845f05b2a2 --- /dev/null +++ b/addons/web/static/tests/core/owl_dialog_tests.js @@ -0,0 +1,250 @@ +odoo.define('web.owl_dialog_tests', function (require) { + "use strict"; + + const LegacyDialog = require('web.Dialog'); + const makeTestEnvironment = require('web.test_env'); + const Dialog = require('web.OwlDialog'); + const testUtils = require('web.test_utils'); + + const { Component, tags, useState } = owl; + const EscapeKey = { key: 'Escape', keyCode: 27, which: 27 }; + const { xml } = tags; + + QUnit.module('core', {}, function () { + QUnit.module('OwlDialog'); + + QUnit.test("Rendering of all props", async function (assert) { + assert.expect(26); + + class SubComponent extends Component { + // Handlers + _onClick() { + assert.step('subcomponent_clicked'); + } + } + SubComponent.template = xml`<div class="o_subcomponent" t-esc="props.text" t-on-click="_onClick"/>`; + + class Parent extends Component { + constructor() { + super(...arguments); + this.state = useState({ textContent: "sup" }); + } + // Handlers + _onButtonClicked(ev) { + assert.step('button_clicked'); + } + _onDialogClosed() { + assert.step('dialog_closed'); + } + } + Parent.components = { Dialog, SubComponent }; + Parent.env = makeTestEnvironment(); + Parent.template = xml` + <Dialog + backdrop="state.backdrop" + contentClass="state.contentClass" + fullscreen="state.fullscreen" + renderFooter="state.renderFooter" + renderHeader="state.renderHeader" + size="state.size" + subtitle="state.subtitle" + technical="state.technical" + title="state.title" + t-on-dialog_closed="_onDialogClosed" + > + <SubComponent text="state.textContent"/> + <t t-set="buttons"> + <button class="btn btn-primary" t-on-click="_onButtonClicked">The Button</button> + </t> + </Dialog>`; + + const parent = new Parent(); + await parent.mount(testUtils.prepareTarget()); + const dialog = document.querySelector('.o_dialog'); + + // Helper function + async function changeProps(key, value) { + parent.state[key] = value; + await testUtils.nextTick(); + } + + // Basic layout with default properties + assert.containsOnce(dialog, '.modal-backdrop.show'); + assert.containsOnce(dialog, '.modal.o_technical_modal'); + assert.hasClass(dialog.querySelector('.modal .modal-dialog'), 'modal-lg'); + assert.containsOnce(dialog, '.modal-header > button.close'); + assert.containsOnce(dialog, '.modal-footer > button.btn.btn-primary'); + assert.strictEqual(dialog.querySelector('.modal-body').innerText.trim(), "sup", + "Subcomponent should match with its given text"); + + // Backdrop (default: 'static') + // Static backdrop click should focus first button + // => we need to reset that property + await testUtils.dom.click(dialog.querySelector('.modal-backdrop')); + assert.strictEqual(document.activeElement, dialog.querySelector('.btn-primary'), + "Button should be focused when clicking on backdrop"); + + await changeProps('backdrop', false); + assert.containsNone(document.body, '.modal-backdrop'); + + await changeProps('backdrop', true); + await testUtils.dom.click(dialog.querySelector('.modal-backdrop')); + + // Dialog class (default: '') + await changeProps('contentClass', 'my_dialog_class'); + assert.hasClass(dialog.querySelector('.modal-content'), 'my_dialog_class'); + + // Full screen (default: false) + assert.doesNotHaveClass(dialog.querySelector('.modal'), 'o_modal_full'); + await changeProps('fullscreen', true); + assert.hasClass(dialog.querySelector('.modal'), 'o_modal_full'); + + // Size class (default: 'large') + await changeProps('size', 'extra-large'); + assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog modal-xl', + "Modal should have taken the class modal-xl"); + await changeProps('size', 'medium'); + assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog', + "Modal should not have any additionnal class with 'medium'"); + await changeProps('size', 'small'); + assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog modal-sm', + "Modal should have taken the class modal-sm"); + + // Subtitle (default: '') + await changeProps('subtitle', "The Subtitle"); + assert.strictEqual(dialog.querySelector('span.o_subtitle').innerText.trim(), "The Subtitle", + "Subtitle should match with its given text"); + + // Technical (default: true) + assert.hasClass(dialog.querySelector('.modal'), 'o_technical_modal'); + await changeProps('technical', false); + assert.doesNotHaveClass(dialog.querySelector('.modal'), 'o_technical_modal'); + + // Title (default: 'Odoo') + assert.strictEqual(dialog.querySelector('h4.modal-title').innerText.trim(), "Odoo" + "The Subtitle", + "Title should match with its default text"); + await changeProps('title', "The Title"); + assert.strictEqual(dialog.querySelector('h4.modal-title').innerText.trim(), "The Title" + "The Subtitle", + "Title should match with its given text"); + + // Reactivity of buttons + await testUtils.dom.click(dialog.querySelector('.modal-footer .btn-primary')); + + // Render footer (default: true) + await changeProps('renderFooter', false); + assert.containsNone(dialog, '.modal-footer'); + + // Render header (default: true) + await changeProps('renderHeader', false); + assert.containsNone(dialog, '.header'); + + // Reactivity of subcomponents + await changeProps('textContent', "wassup"); + assert.strictEqual(dialog.querySelector('.o_subcomponent').innerText.trim(), "wassup", + "Subcomponent should match with its given text"); + await testUtils.dom.click(dialog.querySelector('.o_subcomponent')); + + assert.verifySteps(['dialog_closed', 'button_clicked', 'subcomponent_clicked']); + + parent.destroy(); + }); + + QUnit.test("Interactions between multiple dialogs", async function (assert) { + assert.expect(22); + + class Parent extends Component { + constructor() { + super(...arguments); + this.dialogIds = useState([]); + } + // Handlers + _onDialogClosed(id) { + assert.step(`dialog_${id}_closed`); + this.dialogIds.splice(this.dialogIds.findIndex(d => d === id), 1); + } + } + Parent.components = { Dialog }; + Parent.env = makeTestEnvironment(); + Parent.template = xml` + <div> + <Dialog t-foreach="dialogIds" t-as="dialogId" t-key="dialogId" + contentClass="'dialog_' + dialogId" + t-on-dialog_closed="_onDialogClosed(dialogId)" + /> + </div>`; + + const parent = new Parent(); + await parent.mount(testUtils.prepareTarget()); + + // Dialog 1 : Owl + parent.dialogIds.push(1); + await testUtils.nextTick(); + // Dialog 2 : Legacy + new LegacyDialog(null, {}).open(); + await testUtils.nextTick(); + // Dialog 3 : Legacy + new LegacyDialog(null, {}).open(); + await testUtils.nextTick(); + // Dialog 4 : Owl + parent.dialogIds.push(4); + await testUtils.nextTick(); + // Dialog 5 : Owl + parent.dialogIds.push(5); + await testUtils.nextTick(); + + let modals = document.querySelectorAll('.modal'); + assert.ok(modals[modals.length - 1].classList.contains('o_active_modal'), + "last dialog should have the active class"); + assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'), + "active dialog should not have the legacy class"); + assert.containsN(document.body, '.o_dialog', 3); + assert.containsN(document.body, '.o_legacy_dialog', 2); + + // Reactivity with owl dialogs + await testUtils.dom.triggerEvent(modals[modals.length - 1], 'keydown', EscapeKey); // Press Escape + + modals = document.querySelectorAll('.modal'); + assert.ok(modals[modals.length - 1].classList.contains('o_active_modal'), + "last dialog should have the active class"); + assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'), + "active dialog should not have the legacy class"); + assert.containsN(document.body, '.o_dialog', 2); + assert.containsN(document.body, '.o_legacy_dialog', 2); + + await testUtils.dom.click(modals[modals.length - 1].querySelector('.btn.btn-primary')); // Click on 'Ok' button + + modals = document.querySelectorAll('.modal'); + assert.containsOnce(document.body, '.modal.o_legacy_dialog.o_active_modal', + "active dialog should have the legacy class"); + assert.containsOnce(document.body, '.o_dialog'); + assert.containsN(document.body, '.o_legacy_dialog', 2); + + // Reactivity with legacy dialogs + await testUtils.dom.triggerEvent(modals[modals.length - 1], 'keydown', EscapeKey); + + modals = document.querySelectorAll('.modal'); + assert.containsOnce(document.body, '.modal.o_legacy_dialog.o_active_modal', + "active dialog should have the legacy class"); + assert.containsOnce(document.body, '.o_dialog'); + assert.containsOnce(document.body, '.o_legacy_dialog'); + + await testUtils.dom.click(modals[modals.length - 1].querySelector('.close')); + + modals = document.querySelectorAll('.modal'); + assert.ok(modals[modals.length - 1].classList.contains('o_active_modal'), + "last dialog should have the active class"); + assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'), + "active dialog should not have the legacy class"); + assert.containsOnce(document.body, '.o_dialog'); + assert.containsNone(document.body, '.o_legacy_dialog'); + + parent.unmount(); + + assert.containsNone(document.body, '.modal'); + // dialog 1 is closed through the removal of its parent => no callback + assert.verifySteps(['dialog_5_closed', 'dialog_4_closed']); + + parent.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 8c815e334d0265f145e0c8e53a040412bc7d22d3..6a99aec06724f8973c460d751b9b918035a83405 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -2462,18 +2462,18 @@ QUnit.module('Views', { assert.isNotVisible(list.sidebar.$el, 'sidebar should be invisible'); assert.containsN(list, 'tbody td.o_list_record_selector', 4, "should have 4 records"); - testUtils.dom.click(list.$('tbody td.o_list_record_selector:first input')); + await testUtils.dom.click(list.$('tbody td.o_list_record_selector:first input')); assert.isVisible(list.sidebar.$el, 'sidebar should be visible'); assert.verifySteps(['/web/dataset/search_read']); - testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); + await testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); await testUtils.dom.click(list.sidebar.$('a:contains(Archive)')); assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed'); - testUtils.dom.click($('.modal-footer .btn-secondary')); + await testUtils.dom.click($('.modal-footer .btn-secondary')); assert.containsN(list, 'tbody td.o_list_record_selector', 4, "still should have 4 records"); - testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); + await testUtils.dom.click(list.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); await testUtils.dom.click(list.sidebar.$('a:contains(Archive)')); assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed'); await testUtils.dom.click($('.modal-footer .btn-primary')); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index eae536c47d195ffecaf3c297a4c8cec2f4cc9a4c..5cff868342794644e15d8b1b8403aff8a0586a72 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -138,6 +138,7 @@ <script type="text/javascript" src="/web/static/src/js/core/collections.js"/> <script type="text/javascript" src="/web/static/src/js/core/concurrency.js"></script> <script type="text/javascript" src="/web/static/src/js/core/dialog.js"></script> + <script type="text/javascript" src="/web/static/src/js/core/owl_dialog.js"></script> <script type="text/javascript" src="/web/static/src/js/core/dom.js"></script> <script type="text/javascript" src="/web/static/src/js/core/local_storage.js"></script> <script type="text/javascript" src="/web/static/src/js/core/mixins.js"></script> @@ -705,6 +706,7 @@ <script type="text/javascript" src="/web/static/tests/core/util_tests.js"></script> <script type="text/javascript" src="/web/static/tests/core/widget_tests.js"></script> <script type="text/javascript" src="/web/static/tests/core/dialog_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/core/owl_dialog_tests.js"></script> <script type="text/javascript" src="/web/static/tests/core/dom_tests.js"></script> <script type="text/javascript" src="/web/static/tests/chrome/action_manager_tests.js"></script>