From 34bbdd88c0bc354b76b254bb78c4eb786f3d08c0 Mon Sep 17 00:00:00 2001 From: Julien Mougenot <jum@odoo.com> Date: Fri, 22 Nov 2019 11:36:52 +0000 Subject: [PATCH] [ADD] web: Owl Dialog New Owl component: Dialog. It does not rely on Bootstrap but behaves the same and does not conflict with legacy dialogs. To call this component, you need to define it in the XML parent template instead of calling it in the JS file, and put a flag in the parent state to toggle the dialog. Old dialog file has also been slightly updated to ensure compatibility between both versions. This commit also updates a list view test which did not wait for click events to properly trigger and would cause the new dialog system to crash. --- addons/web/static/src/js/core/dialog.js | 7 + addons/web/static/src/js/core/owl_dialog.js | 259 ++++++++++++++++++ addons/web/static/src/scss/modal.scss | 26 +- addons/web/static/src/scss/utils.scss | 7 + addons/web/static/src/xml/base.xml | 48 ++++ addons/web/static/src/xml/dialog.xml | 28 +- .../web/static/tests/core/owl_dialog_tests.js | 250 +++++++++++++++++ addons/web/static/tests/views/list_tests.js | 8 +- addons/web/views/webclient_templates.xml | 2 + 9 files changed, 617 insertions(+), 18 deletions(-) create mode 100644 addons/web/static/src/js/core/owl_dialog.js create mode 100644 addons/web/static/tests/core/owl_dialog_tests.js diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js index bb522992c2f4..6e9620421c39 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 000000000000..a6ee3c7b1d8a --- /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 3463d9e20ac5..d82ddec6213b 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 794b8bea90e7..1d410fc74756 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 a3fdb09611c8..961c11cdc90d 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 645e6e2391ea..848f5a1e9b61 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 000000000000..8114408bf489 --- /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 8c815e334d02..6a99aec06724 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 eae536c47d19..5cff86834279 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> -- GitLab