[IMP] web: improve core Dialog API

*: base_automation, iap, web_tour

Previously, when trying to create a custom dialog, one would need to
extend the base Dialog class and configure it directly on the class,
using separate templates for the body and footer as necessary. This is
very unidiomatic owl code, and it is much more natural to simply extend
Component, and use Dialog at the root of the template, giving it slots
and props to configure its properties and contents.

This commit changes the dialog API to work as described and adapts code
that uses it.

closes odoo/odoo#89113

Enterprise: https://github.com/odoo/enterprise/pull/26462
Related: odoo/enterprise#26462
Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
Samuel Degueldre
2022-04-22 16:59:47 +02:00
parent e9b87c2c54
commit 7c4f200b8e
34 changed files with 578 additions and 617 deletions
@@ -31,7 +31,7 @@ export class BaseAutomationErrorDialog extends ErrorDialog {
await this.orm.write("base.automation", [this.actionId], {
active: false,
});
this.close();
this.props.close();
}
/**
* This method is called when the user clicks on the 'Edit action' button
@@ -50,10 +50,10 @@ export class BaseAutomationErrorDialog extends ErrorDialog {
type: "ir.actions.act_window",
view_mode: "form",
});
this.close();
this.props.close();
}
}
BaseAutomationErrorDialog.bodyTemplate = "base_automation.ErrorDialogBody";
BaseAutomationErrorDialog.template = "base_automation.ErrorDialog";
registry.category("error_dialogs").add("base_automation", BaseAutomationErrorDialog);
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="base_automation.ErrorDialogBody" t-inherit="web.ErrorDialogBody" t-inherit-mode="primary" owl="1">
<t t-name="base_automation.ErrorDialog" t-inherit="web.ErrorDialog" t-inherit-mode="primary" owl="1">
<xpath expr="//div[@role='alert']" position="inside">
<p>
The error occurred during the execution of the automated action
@@ -3,11 +3,12 @@ import { Dialog } from "@web/core/dialog/dialog";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
class InsufficientCreditDialog extends Dialog {
const { Component, onWillStart } = owl;
class InsufficientCreditDialog extends Component {
setup() {
super.setup(...arguments);
this.orm = useService("orm");
owl.onWillStart(this.onWillStart);
onWillStart(this.onWillStart);
}
async onWillStart() {
@@ -30,11 +31,11 @@ class InsufficientCreditDialog extends Dialog {
buyCredits() {
window.open(this.url, "_blank");
this.close();
this.props.close();
}
}
InsufficientCreditDialog.bodyTemplate = "iap.redirect_to_odoo_credit";
InsufficientCreditDialog.footerTemplate = "iap.InsufficientCreditFooter";
InsufficientCreditDialog.components = { Dialog };
InsufficientCreditDialog.template = "iap.InsufficientCreditDialog";
function insufficientCreditHandler(env, error, originalError) {
if (!originalError) {
+21 -18
View File
@@ -2,24 +2,27 @@
<template id="template" xml:space="preserve">
<!-- LAYOUT TEMPLATES -->
<div t-name="iap.redirect_to_odoo_credit" t-att-style="style" owl="1">
<t t-if="props.errorData.body">
<div t-raw="props.errorData.body"/>
</t>
<t t-if="!props.errorData.body">
<t t-if="props.errorData.message">
<span t-esc="props.errorData.message"/>
</t>
<t t-if="!props.errorData.message">
<span>Insufficient credit to perform this service.</span>
</t>
</t>
</div>
<div t-name="iap.InsufficientCreditFooter" owl="1">
<button class="btn btn-primary" t-on-click="buyCredits" t-esc="buttonMessage"/>
<button class="btn" t-on-click="close">Cancel</button>
</div>
<t t-name="iap.InsufficientCreditDialog" owl="1">
<Dialog>
<div t-att-style="style">
<t t-if="props.errorData.body">
<div t-raw="props.errorData.body"/>
</t>
<t t-if="!props.errorData.body">
<t t-if="props.errorData.message">
<span t-esc="props.errorData.message"/>
</t>
<t t-if="!props.errorData.message">
<span>Insufficient credit to perform this service.</span>
</t>
</t>
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="buyCredits" t-esc="buttonMessage"/>
<button class="btn" t-on-click="close">Cancel</button>
</t>
</div>
</Dialog>
</t>
<t t-extend="DashboardMain">
<t t-jquery=".o_web_settings_apps" t-operation="after">
@@ -1,5 +1,6 @@
/** @odoo-module **/
import { Dialog } from "@web/core/dialog/dialog";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { _lt } from "@web/core/l10n/translation";
import { KeepLast } from "@web/core/utils/concurrency";
@@ -9,7 +10,7 @@ import { fuzzyLookup } from "@web/core/utils/search";
import { debounce } from "@web/core/utils/timing";
import { escapeRegExp } from "../utils/strings";
const { Component, onWillStart, useRef, useState, markRaw } = owl;
const { Component, onWillStart, useRef, useState, markRaw, useExternalListener } = owl;
const DEFAULT_PLACEHOLDER = _lt("Search...");
const DEFAULT_EMPTY_MESSAGE = _lt("No result found");
@@ -92,6 +93,7 @@ export class CommandPalette extends Component {
bypassEditableProtection: true,
allowRepeat: true,
});
useExternalListener(window, "mousedown", this.onWindowMouseDown);
/**
* @type {{ commands: CommandItem[],
@@ -104,6 +106,7 @@ export class CommandPalette extends Component {
*/
this.state = useState({});
this.root = useRef("root");
this.listboxRef = useRef("listbox");
onWillStart(() => this.setCommandPaletteConfig(this.props.config));
@@ -241,7 +244,7 @@ export class CommandPalette extends Component {
if (config) {
this.setCommandPaletteConfig(config);
} else {
this.props.closeMe();
this.props.close();
}
}
@@ -295,6 +298,15 @@ export class CommandPalette extends Component {
}
}
/**
* Close the palette on outside click.
*/
onWindowMouseDown(ev) {
if (!this.root.el.contains(ev.target)) {
this.props.close();
}
}
switchNamespace(namespace) {
if (this.lastDebounceSearch) {
this.lastDebounceSearch.cancel();
@@ -317,3 +329,4 @@ export class CommandPalette extends Component {
}
}
CommandPalette.template = "web.CommandPalette";
CommandPalette.components = { Dialog };
@@ -2,47 +2,48 @@
<templates xml:space="preserve">
<t t-name="web.CommandPalette" owl="1">
<div>
<div class="o_command_palette_search input-group mb-2 px-4 py-3 border-bottom">
<span t-if="state.namespace !== 'default'" class="o_namespace d-flex align-items-center mr-1" t-out="state.namespace"/>
<input class="form-control border-0 p-0" type="text" t-att-value="state.searchValue" t-ref="autofocus" t-att-placeholder="state.placeholder" t-on-input="onSearchInput" t-on-keydown="onKeyDown"/>
<span class="input-group-append">
<div class="input-group-text border-0 bg-transparent">
<i t-att-title="state.placeholder" role="img" t-att-aria-label="state.placeholder" class="oi oi-search"/>
</div>
</span>
</div>
<Dialog header="false" footer="false" size="'md'" contentClass="'o_command_palette mt-5'">
<div t-ref="root">
<div class="o_command_palette_search input-group mb-2 px-4 py-3 border-bottom">
<span t-if="state.namespace !== 'default'" class="o_namespace d-flex align-items-center mr-1" t-out="state.namespace"/>
<input class="form-control border-0 p-0" type="text" t-att-value="state.searchValue" t-ref="autofocus" t-att-placeholder="state.placeholder" t-on-input="onSearchInput" t-on-keydown="onKeyDown"/>
<span class="input-group-append">
<div class="input-group-text border-0 bg-transparent">
<i t-att-title="state.placeholder" role="img" t-att-aria-label="state.placeholder" class="oi oi-search"/>
</div>
</span>
</div>
<div t-ref="listbox" class="o_command_palette_listbox position-relative overflow-auto">
<div t-if="!state.commands.length" class="o_command_palette_listbox_empty px-4 py-3 font-italic" t-out="state.emptyMessage"/>
<t t-if="!isFuzzySearch" t-foreach="commandsByCategory" t-as="category" t-key="category.keyId">
<div class="o_command_category px-0">
<t t-foreach="category.commands" t-as="command" t-key="command.keyId">
<t t-set="commandIndex" t-value="state.commands.indexOf(command)"/>
<div t-attf-id="o_command_{{commandIndex}}" class="o_command" t-att-class="{ focused: state.selectedCommand === command }" t-on-click="() => this.onCommandClicked(commandIndex)" t-on-mouseenter="() => this.onCommandMouseEnter(commandIndex)" t-on-close="() => this.props.closeMe()">
<t t-component="command.Component || DefaultCommandItem" name="command.name" searchValue="state.searchValue" t-props="command.props" executeCommand="() => this.executeCommand(command)">
<t t-set-slot="name">
<span class="o_command_name">
<t t-foreach="command.splitName" t-as="name" t-key="name_index">
<b t-if="name_index % 2" t-out="name" class="text-primary"/>
<t t-else="" t-out="name"/>
</t>
</span>
<div t-ref="listbox" class="o_command_palette_listbox position-relative overflow-auto">
<div t-if="!state.commands.length" class="o_command_palette_listbox_empty px-4 py-3 font-italic" t-out="state.emptyMessage"/>
<t t-if="!isFuzzySearch" t-foreach="commandsByCategory" t-as="category" t-key="category.keyId">
<div class="o_command_category px-0">
<t t-foreach="category.commands" t-as="command" t-key="command.keyId">
<t t-set="commandIndex" t-value="state.commands.indexOf(command)"/>
<div t-attf-id="o_command_{{commandIndex}}" class="o_command" t-att-class="{ focused: state.selectedCommand === command }" t-on-click="() => this.onCommandClicked(commandIndex)" t-on-mouseenter="() => this.onCommandMouseEnter(commandIndex)" t-on-close="() => this.props.closeMe()">
<t t-component="command.Component || DefaultCommandItem" name="command.name" searchValue="state.searchValue" t-props="command.props" executeCommand="() => this.executeCommand(command)">
<t t-set-slot="name">
<span class="o_command_name">
<t t-foreach="command.splitName" t-as="name" t-key="name_index">
<b t-if="name_index % 2" t-out="name" class="text-primary"/>
<t t-else="" t-out="name"/>
</t>
</span>
</t>
</t>
</t>
</div>
</t>
</div>
</div>
</t>
</div>
<hr class="my-2 mx-0" t-if="!category_last" />
</t>
<hr class="my-2 mx-0" t-if="!category_last" />
</t>
</div>
<div t-if="state.FooterComponent" class="o_command_palette_footer mt-2 px-4 py-2 border-top bg-100 text-muted">
<t t-component="state.FooterComponent" switchNamespace="(namespace) => this.debounceSearch(namespace.concat(this.state.searchValue))"/>
</div>
</div>
<div t-if="state.FooterComponent" class="o_command_palette_footer mt-2 px-4 py-2 border-top bg-100 text-muted">
<t t-component="state.FooterComponent" switchNamespace="(namespace) => this.debounceSearch(namespace.concat(this.state.searchValue))"/>
</div>
</div>
</Dialog>
</t>
</templates>
@@ -1,34 +0,0 @@
/** @odoo-module **/
import { Dialog } from "@web/core/dialog/dialog";
import { CommandPalette } from "./command_palette";
const { useExternalListener } = owl;
/**
* @typedef {import("./command_service").Command} Command
*/
export class CommandPaletteDialog extends Dialog {
setup() {
super.setup();
useExternalListener(window, "mousedown", this.onWindowMouseDown);
}
/**
* Used to close ourself on outside click.
*/
onWindowMouseDown(ev) {
const element = ev.target.parentElement;
const gotClickedInside = this.modalRef.el.contains(element);
if (!gotClickedInside) {
this.close();
}
}
}
CommandPaletteDialog.renderHeader = false;
CommandPaletteDialog.renderFooter = false;
CommandPaletteDialog.size = "modal-md";
CommandPaletteDialog.contentClass = "o_command_palette mt-5";
CommandPaletteDialog.bodyTemplate = "web.CommandPaletteDialogBody";
CommandPaletteDialog.components = Object.assign({}, Dialog.components, { CommandPalette });
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.CommandPaletteDialogBody" owl="1">
<CommandPalette t-props="props" closeMe="close"/>
</t>
</templates>
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { CommandPaletteDialog } from "./command_palette_dialog";
import { CommandPalette } from "./command_palette";
const { Component, xml } = owl;
@@ -136,7 +136,7 @@ export const commandService = {
// Open Command Palette dialog
isPaletteOpened = true;
dialog.add(
CommandPaletteDialog,
CommandPalette,
{
config,
},
@@ -3,27 +3,27 @@
import { Dialog } from "../dialog/dialog";
import { _lt } from "../l10n/translation";
export class ConfirmationDialog extends Dialog {
setup() {
super.setup();
this.title = this.props.title;
}
const { Component } = owl;
export class ConfirmationDialog extends Component {
_cancel() {
if (this.props.cancel) {
this.props.cancel();
}
this.close();
this.props.close();
}
_confirm() {
if (this.props.confirm) {
this.props.confirm();
}
this.close();
this.props.close();
}
}
ConfirmationDialog.template = "web.ConfirmationDialog";
ConfirmationDialog.components = { Dialog };
ConfirmationDialog.props = {
...Dialog.props,
close: Function,
title: {
validate: (m) => {
return (
@@ -40,23 +40,12 @@ ConfirmationDialog.defaultProps = {
title: _lt("Confirmation"),
};
ConfirmationDialog.bodyTemplate = "web.ConfirmationDialogBody";
ConfirmationDialog.footerTemplate = "web.ConfirmationDialogFooter";
ConfirmationDialog.size = "modal-md";
export class AlertDialog extends ConfirmationDialog {
setup() {
super.setup();
if ("contentClass" in this.props) {
this.contentClass = this.props.contentClass;
}
}
}
AlertDialog.size = "modal-sm";
AlertDialog.props = Object.assign(Object.create(ConfirmationDialog.props), {
export class AlertDialog extends ConfirmationDialog {}
AlertDialog.template = "web.AlertDialog";
AlertDialog.props = {
...ConfirmationDialog.props,
contentClass: { type: String, optional: true },
});
};
AlertDialog.defaultProps = {
title: _lt("Alert"),
};
@@ -1,17 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="web.ConfirmationDialogBody" owl="1">
<t t-esc="props.body" />
<t t-name="web.ConfirmationDialog" owl="1">
<Dialog size="'md'" title="props.title">
<t t-esc="props.body" />
<t t-set-slot="footer" owl="1">
<button class="btn btn-primary" t-on-click="_confirm">
Ok
</button>
<button t-if="props.cancel" class="btn btn-secondary" t-on-click="_cancel">
Cancel
</button>
</t>
</Dialog>
</t>
<t t-name="web.ConfirmationDialogFooter" owl="1">
<button class="btn btn-primary" t-on-click="_confirm">
Ok
</button>
<button t-if="props.cancel" class="btn btn-secondary" t-on-click="_cancel">
Cancel
</button>
<t t-name="web.AlertDialog" owl="1">
<Dialog size="'sm'" title="props.title" contentClass="props.contentClass">
<t t-esc="props.body" />
<t t-set-slot="footer" owl="1">
<button class="btn btn-primary" t-on-click="_confirm">
Ok
</button>
<button t-if="props.cancel" class="btn btn-secondary" t-on-click="_cancel">
Cancel
</button>
</t>
</Dialog>
</t>
</templates>
@@ -1,102 +1,105 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.DebugMenu.SetDefaultFooter" owl="1">
<button class="btn btn-secondary" t-on-click="() => this.trigger('dialog-closed')">Close</button>
<button class="btn btn-secondary" t-on-click="saveDefault">Save default</button>
<t t-name="web.DebugMenu.SetDefaultDialog" owl="1">
<Dialog title="this.constructor.title">
<table style="width: 100%">
<tr>
<td>
<label for="formview_default_fields"
class="oe_label oe_align_right">
Default:
</label>
</td>
<td class="oe_form_required">
<select id="formview_default_fields" class="o_input" t-model="state.fieldToSet">
<option value=""/>
<option t-foreach="defaultFields" t-as="field"
t-att-value="field.name">
<t t-esc="field.string"/> = <t t-esc="field.displayed"/>
</option>
</select>
</td>
</tr>
<tr t-if="conditions.length">
<td>
<label for="formview_default_conditions"
class="oe_label oe_align_right">
Condition:
</label>
</td>
<td>
<select id="formview_default_conditions" class="o_input" t-model="state.condition">
<option value=""/>
<option t-foreach="conditions" t-as="cond"
t-att-value="cond.name + '=' + cond.value">
<t t-esc="cond.string"/>=<t t-esc="cond.displayed"/>
</option>
</select>
</td>
</tr>
<tr>
<td colspan="2">
<input type="radio" id="formview_default_self"
value="self" name="scope" t-model="state.scope"/>
<label for="formview_default_self" class="oe_label"
style="display: inline;">
Only you
</label>
<br/>
<input type="radio" id="formview_default_all"
value="all" name="scope" t-model="state.scope"/>
<label for="formview_default_all" class="oe_label"
style="display: inline;">
All users
</label>
</td>
</tr>
</table>
<t t-set-slot="footer">
<button class="btn btn-secondary" t-on-click="props.close">Close</button>
<button class="btn btn-secondary" t-on-click="saveDefault">Save default</button>
</t>
</Dialog>
</t>
<t t-name="web.DebugMenu.setDefaultBody" owl="1">
<table style="width: 100%">
<tr>
<td>
<label for="formview_default_fields"
class="oe_label oe_align_right">
Default:
</label>
</td>
<td class="oe_form_required">
<select id="formview_default_fields" class="o_input" t-model="state.fieldToSet">
<option value=""/>
<option t-foreach="defaultFields" t-as="field"
t-att-value="field.name">
<t t-esc="field.string"/> = <t t-esc="field.displayed"/>
</option>
</select>
</td>
</tr>
<tr t-if="conditions.length">
<td>
<label for="formview_default_conditions"
class="oe_label oe_align_right">
Condition:
</label>
</td>
<td>
<select id="formview_default_conditions" class="o_input" t-model="state.condition">
<option value=""/>
<option t-foreach="conditions" t-as="cond"
t-att-value="cond.name + '=' + cond.value">
<t t-esc="cond.string"/>=<t t-esc="cond.displayed"/>
</option>
</select>
</td>
</tr>
<tr>
<td colspan="2">
<input type="radio" id="formview_default_self"
value="self" name="scope" t-model="state.scope"/>
<label for="formview_default_self" class="oe_label"
style="display: inline;">
Only you
</label>
<br/>
<input type="radio" id="formview_default_all"
value="all" name="scope" t-model="state.scope"/>
<label for="formview_default_all" class="oe_label"
style="display: inline;">
All users
</label>
</td>
</tr>
</table>
</t>
<t t-name="web.DebugMenu.getMetadataBody" owl="1">
<table class="table table-sm table-striped">
<tr>
<th>ID:</th>
<td><t t-esc="state.id"/></td>
</tr>
<tr>
<th>XML ID:</th>
<td><t t-esc="state.xmlid or '/'"/></td>
</tr>
<tr>
<th>No Update:</th>
<td>
<t t-esc="state.noupdate"/>
<t t-if="state.xmlid">
<a t-on-click="toggleNoupdate"> (change)</a>
</t>
</td>
</tr>
<tr>
<th>Creation User:</th>
<td><t t-esc="state.creator"/></td>
</tr>
<tr>
<th>Creation Date:</th>
<td><t t-esc="state.create_date"/></td>
</tr>
<tr>
<th>Latest Modification by:</th>
<td><t t-esc="state.lastModifiedBy"/></td>
</tr>
<tr>
<th>Latest Modification Date:</th>
<td><t t-esc="state.write_date"/></td>
</tr>
</table>
<t t-name="web.DebugMenu.GetMetadataDialog" owl="1">
<Dialog title="this.constructor.title">
<table class="table table-sm table-striped">
<tr>
<th>ID:</th>
<td><t t-esc="state.id"/></td>
</tr>
<tr>
<th>XML ID:</th>
<td><t t-esc="state.xmlid or '/'"/></td>
</tr>
<tr>
<th>No Update:</th>
<td>
<t t-esc="state.noupdate"/>
<t t-if="state.xmlid">
<a t-on-click="toggleNoupdate"> (change)</a>
</t>
</td>
</tr>
<tr>
<th>Creation User:</th>
<td><t t-esc="state.creator"/></td>
</tr>
<tr>
<th>Creation Date:</th>
<td><t t-esc="state.create_date"/></td>
</tr>
<tr>
<th>Latest Modification by:</th>
<td><t t-esc="state.lastModifiedBy"/></td>
</tr>
<tr>
<th>Latest Modification Date:</th>
<td><t t-esc="state.write_date"/></td>
</tr>
</table>
</Dialog>
</t>
</templates>
+26 -37
View File
@@ -3,52 +3,41 @@
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { useActiveElement } from "../ui/ui_service";
const { Component, useRef, useChildSubEnv, xml } = owl;
const { Component, useRef, useChildSubEnv, useState } = owl;
export class Dialog extends Component {
setup() {
if (this.constructor === Dialog) {
throw new Error(
"Dialog should not be used by itself. Please use the dialog service with a Dialog subclass."
);
}
this.modalRef = useRef("modal");
useActiveElement("modal");
this.data = useState(this.env.dialogData);
useHotkey("escape", () => {
this.close();
this.data.close();
});
useChildSubEnv({ inDialog: true });
this.close = this.close.bind(this);
this.contentClass = this.constructor.contentClass;
this.fullscreen = this.constructor.fullscreen;
this.renderFooter = this.constructor.renderFooter;
this.renderHeader = this.constructor.renderHeader;
this.size = this.constructor.size;
this.technical = this.constructor.technical;
this.title = this.constructor.title;
}
/**
* Send an event signaling that the dialog should be closed.
* @private
*/
close() {
this.props.close();
}
}
Dialog.template = "web.Dialog";
Dialog.contentClass = null;
Dialog.fullscreen = false;
Dialog.renderFooter = true;
Dialog.renderHeader = true;
Dialog.size = "modal-lg";
Dialog.technical = true;
Dialog.title = "Odoo";
Dialog.bodyTemplate = xml`<div/>`;
Dialog.footerTemplate = "web.DialogFooterDefault";
Dialog.props = {
close: Function,
isActive: { optional: true },
"*": true,
contentClass: { type: String, optional: true },
fullscreen: { type: Boolean, optional: true },
footer: { type: Boolean, optional: true },
header: { type: Boolean, optional: true },
size: { type: String, optional: true, validate: (s) => ["sm", "md", "lg", "xl"].includes(s) },
technical: { type: Boolean, optional: true },
title: { type: String, optional: true },
slots: {
type: Object,
shape: {
default: Object, // Content is not optional
footer: { type: Object, optional: true },
},
},
};
Dialog.defaultProps = {
contentClass: "",
fullscreen: false,
footer: true,
header: true,
size: "lg",
technical: true,
title: "Odoo",
};
+14 -16
View File
@@ -2,35 +2,33 @@
<templates xml:space="preserve">
<t t-name="web.Dialog" owl="1">
<div class="o_dialog" t-att-class="{ o_inactive_modal: !props.isActive }" t-on-dialog-closed="close">
<div class="o_dialog" t-att-class="{ o_inactive_modal: !data.isActive }">
<div role="dialog" class="modal d-block"
tabindex="-1"
t-att-class="{ o_technical_modal: technical, o_modal_full: fullscreen }"
t-att-class="{ o_technical_modal: props.technical, o_modal_full: props.fullscreen }"
t-ref="modal"
>
<div class="modal-dialog" t-att-class="size">
<div class="modal-content" t-att-class="contentClass">
<header t-if="renderHeader" class="modal-header">
<div class="modal-dialog" t-attf-class="modal-{{props.size}}">
<div class="modal-content" t-att-class="props.contentClass">
<header t-if="props.header" class="modal-header">
<h4 class="modal-title text-break">
<t t-esc="title"/>
<t t-esc="props.title"/>
</h4>
<button type="button" class="close" aria-label="Close" tabindex="-1" t-on-click="close">×</button>
<button type="button" class="close" aria-label="Close" tabindex="-1" t-on-click="data.close">×</button>
</header>
<main class="modal-body">
<t t-call="{{ constructor.bodyTemplate }}"/>
<t t-slot="default"/>
</main>
<footer t-if="renderFooter" class="modal-footer justify-content-start">
<t t-call="{{ constructor.footerTemplate }}" />
<footer t-if="props.footer" class="modal-footer justify-content-start">
<t t-slot="footer">
<button class="btn btn-primary" t-on-click="data.close">
<t>Ok</t>
</button>
</t>
</footer>
</div>
</div>
</div>
</div>
</t>
<t t-name="web.DialogFooterDefault" owl="1">
<button class="btn btn-primary" t-on-click="close">
<t>Ok</t>
</button>
</t>
</templates>
@@ -1,36 +1,28 @@
/** @odoo-module **/
import { ErrorHandler } from "../utils/components";
import { ErrorHandler, WithEnv } from "../utils/components";
const { Component, xml } = owl;
export class DialogContainer extends Component {
setup() {
this.props.bus.addEventListener("UPDATE", this.render.bind(this));
}
close(id) {
if (this.props.dialogs[id]) {
this.props.dialogs[id].props.close();
}
}
handleError(error, dialogId) {
this.close(dialogId);
handleError(error, dialog) {
dialog.props.close();
Promise.resolve().then(() => {
throw error;
});
}
}
DialogContainer.components = { ErrorHandler };
DialogContainer.components = { ErrorHandler, WithEnv };
//Legacy : The div wrapping the t-foreach, is placed to avoid owl to delete non-owl dialogs.
//This div can be removed after removing all legacy dialogs.
DialogContainer.template = xml`
<div class="o_dialog_container" t-att-class="{'modal-open': Object.keys(props.dialogs).length > 0}">
<div>
<t t-foreach="Object.values(props.dialogs)" t-as="dialog" t-key="dialog.id">
<ErrorHandler onError="(error) => this.handleError(error, dialog.id)">
<t t-component="dialog.class" t-props="dialog.props" isActive="dialog_last"/>
<ErrorHandler onError="(error) => this.handleError(error, dialog)">
<WithEnv env="{ dialogData: dialog.dialogData }">
<t t-component="dialog.class" t-props="dialog.props"/>
</WithEnv>
</ErrorHandler>
</t>
</div>
@@ -1,10 +1,9 @@
/** @odoo-module **/
import { registry } from "../registry";
import { Dialog } from "./dialog";
import { DialogContainer } from "./dialog_container";
const { EventBus } = owl;
const { reactive } = owl;
/**
* @typedef {{
@@ -24,28 +23,28 @@ const { EventBus } = owl;
export const dialogService = {
/** @returns {DialogServiceInterface} */
start() {
const dialogs = {};
const bus = new EventBus();
const dialogs = reactive({});
let dialogId = 0;
registry.category("main_components").add("DialogContainer", {
Component: DialogContainer,
props: { bus, dialogs },
props: { dialogs },
});
function add(dialogClass, props, options = {}) {
if (!(dialogClass.prototype instanceof Dialog)) {
throw new Error(`${dialogClass.name} must be a subclass of Dialog`);
for (const dialog of Object.values(dialogs)) {
dialog.dialogData.isActive = false;
}
const id = ++dialogId;
function close() {
if (dialogs[id]) {
delete dialogs[id];
Object.values(dialogs).forEach((dialog, i, dialogArr) => {
dialog.dialogData.isActive = i === dialogArr.length - 1;
});
if (options.onClose) {
options.onClose();
}
bus.trigger("UPDATE");
}
}
@@ -53,9 +52,9 @@ export const dialogService = {
id,
class: dialogClass,
props: { ...props, close },
dialogData: { isActive: true, close },
};
dialogs[id] = dialog;
bus.trigger("UPDATE");
return close;
}
@@ -7,7 +7,7 @@ import { registry } from "../registry";
import { useService } from "@web/core/utils/hooks";
import { capitalize } from "../utils/strings";
const { useState } = owl;
const { Component, useState } = owl;
export const odooExceptionTitleMap = new Map(
Object.entries({
@@ -26,9 +26,8 @@ export const odooExceptionTitleMap = new Map(
// -----------------------------------------------------------------------------
// Generic Error Dialog
// -----------------------------------------------------------------------------
export class ErrorDialog extends Dialog {
export class ErrorDialog extends Component {
setup() {
super.setup();
this.state = useState({
showTraceback: false,
});
@@ -39,8 +38,8 @@ export class ErrorDialog extends Dialog {
);
}
}
ErrorDialog.contentClass = "o_dialog_error";
ErrorDialog.bodyTemplate = "web.ErrorDialogBody";
ErrorDialog.template = "web.ErrorDialog";
ErrorDialog.components = { Dialog };
ErrorDialog.title = _lt("Odoo Error");
// -----------------------------------------------------------------------------
@@ -98,11 +97,9 @@ export class RPCErrorDialog extends ErrorDialog {
// -----------------------------------------------------------------------------
// Warning Dialog
// -----------------------------------------------------------------------------
export class WarningDialog extends Dialog {
export class WarningDialog extends Component {
setup() {
super.setup();
this.title = this.env._t("Odoo Warning");
this.inferTitle();
this.title = this.inferTitle();
const { data, message } = this.props;
if (data && data.arguments && data.arguments.length > 0) {
this.message = data.arguments[0];
@@ -112,26 +109,27 @@ export class WarningDialog extends Dialog {
}
inferTitle() {
if (this.props.exceptionName && odooExceptionTitleMap.has(this.props.exceptionName)) {
this.title = odooExceptionTitleMap.get(this.props.exceptionName).toString();
return odooExceptionTitleMap.get(this.props.exceptionName).toString();
}
return this.env._t("Odoo Warning");
}
}
WarningDialog.bodyTemplate = "web.WarningDialogBody";
WarningDialog.template = "web.WarningDialog";
WarningDialog.components = { Dialog };
// -----------------------------------------------------------------------------
// Redirect Warning Dialog
// -----------------------------------------------------------------------------
export class RedirectWarningDialog extends Dialog {
export class RedirectWarningDialog extends Component {
setup() {
super.setup();
this.actionService = useService("action");
const { data, subType } = this.props;
const [message, actionId, buttonText, additional_context] = data.arguments;
const [message, actionId, buttonText, additionalContext] = data.arguments;
this.title = capitalize(subType) || this.env._t("Odoo Warning");
this.message = message;
this.actionId = actionId;
this.buttonText = buttonText;
this.additionalContext = additional_context;
this.additionalContext = additionalContext;
}
async onClick() {
const options = {};
@@ -139,32 +137,30 @@ export class RedirectWarningDialog extends Dialog {
options.additionalContext = this.additionalContext;
}
await this.actionService.doAction(this.actionId, options);
this.close();
}
onCancel() {
this.close();
this.props.close();
}
}
RedirectWarningDialog.bodyTemplate = "web.RedirectWarningDialogBody";
RedirectWarningDialog.footerTemplate = "web.RedirectWarningDialogFooter";
RedirectWarningDialog.template = "web.RedirectWarningDialog";
RedirectWarningDialog.components = { Dialog };
// -----------------------------------------------------------------------------
// Error 504 Dialog
// -----------------------------------------------------------------------------
export class Error504Dialog extends Dialog {}
Error504Dialog.bodyTemplate = "web.Error504DialogBody";
export class Error504Dialog extends Component {}
Error504Dialog.template = "web.Error504Dialog";
Error504Dialog.components = { Dialog };
Error504Dialog.title = _lt("Request timeout");
// -----------------------------------------------------------------------------
// Expired Session Error Dialog
// -----------------------------------------------------------------------------
export class SessionExpiredDialog extends Dialog {
export class SessionExpiredDialog extends Component {
onClick() {
browser.location.reload();
}
}
SessionExpiredDialog.bodyTemplate = "web.SessionExpiredDialogBody";
SessionExpiredDialog.footerTemplate = "web.SessionExpiredDialogFooter";
SessionExpiredDialog.template = "web.SessionExpiredDialog";
SessionExpiredDialog.components = { Dialog };
SessionExpiredDialog.title = _lt("Odoo Session Expired");
registry
@@ -1,63 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.WarningDialogBody" owl="1">
<div role="alert" class="o_dialog_warning">
<p t-esc="message" style="white-space: pre-wrap;"/>
</div>
<t t-name="web.WarningDialog" owl="1">
<Dialog title="title">
<div role="alert" class="o_dialog_warning">
<p t-esc="message" style="white-space: pre-wrap;"/>
</div>
</Dialog>
</t>
<t t-name="web.RedirectWarningDialogBody" owl="1">
<div role="alert">
<p t-esc="message" style="white-space: pre-wrap;"/>
</div>
<t t-name="web.RedirectWarningDialog" owl="1">
<Dialog title="title">
<div role="alert">
<p t-esc="message" style="white-space: pre-wrap;"/>
</div>
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="onClick" t-esc="buttonText"/>
<button class="btn btn-secondary" t-on-click="props.close">Cancel</button>
</t>
</Dialog>
</t>
<t t-name="web.RedirectWarningDialogFooter" owl="1">
<button class="btn btn-primary" t-on-click="onClick"><t t-esc="env._t(buttonText)"/></button>
<button class="btn btn-secondary" t-on-click="onCancel">Cancel</button>
</t>
<t t-name="web.Error504DialogBody" owl="1">
<t t-name="web.Error504Dialog" owl="1">
<Dialog title="this.constructor.title">
<div role="alert">
<p style="white-space: pre-wrap;">
The operation was interrupted. This usually means that the current operation is taking too much time.
</p>
</div>
</Dialog>
</t>
<t t-name="web.SessionExpiredDialogBody" owl="1">
<div role="alert">
<p style="white-space: pre-wrap;">
Your Odoo session expired. The current page is about to be refreshed.
</p>
</div>
<t t-name="web.SessionExpiredDialog" owl="1">
<Dialog title="this.constructor.title">
<div role="alert">
<p style="white-space: pre-wrap;">
Your Odoo session expired. The current page is about to be refreshed.
</p>
</div>
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="onClick">Ok</button>
</t>
</Dialog>
</t>
<t t-name="web.SessionExpiredDialogFooter" owl="1">
<button class="btn btn-primary" t-on-click="onClick">Ok</button>
</t>
<t t-name="web.ErrorDialogBody" owl="1">
<div class="alert alert-warning clearfix" role="alert">
<div class="float-right ml8 btn-group-vertical">
<button class="btn btn-primary" t-on-click="onClickClipboard">
<i class="fa fa-clipboard mr8"/><t>Copy the full error to clipboard</t>
</button>
</div>
<p><b>An error occurred</b></p>
<p>Please use the copy button to report the error to your support service.</p>
</div>
<t t-name="web.ErrorDialog" owl="1">
<Dialog contentClass="'o_dialog_error'" title="this.constructor.title">
<div class="alert alert-warning clearfix" role="alert">
<div class="float-right ml8 btn-group-vertical">
<button class="btn btn-primary" t-on-click="onClickClipboard">
<i class="fa fa-clipboard mr8"/><t>Copy the full error to clipboard</t>
</button>
</div>
<p><b>An error occurred</b></p>
<p>Please use the copy button to report the error to your support service.</p>
</div>
<button class="btn btn-link" t-on-click="() => { state.showTraceback = !state.showTraceback }">See details</button>
<div t-if="state.showTraceback and (props.name or props.message)" class="alert clearfix" style="background-color:#ececec;" role="alert">
<code t-if="props.name" t-esc="props.name"></code>
<p t-if="props.message" t-esc="props.message"></p>
</div>
<div t-if="state.showTraceback" class="alert alert-danger o_error_detail" role="alert">
<pre t-esc="traceback or props.traceback"/>
</div>
<button class="btn btn-link" t-on-click="() => { state.showTraceback = !state.showTraceback }">See details</button>
<div t-if="state.showTraceback and (props.name or props.message)" class="alert clearfix" style="background-color:#ececec;" role="alert">
<code t-if="props.name" t-esc="props.name"></code>
<p t-if="props.message" t-esc="props.message"></p>
</div>
<div t-if="state.showTraceback" class="alert alert-danger o_error_detail" role="alert">
<pre t-esc="traceback or props.traceback"/>
</div>
</Dialog>
</t>
</templates>
@@ -1,6 +1,6 @@
/** @odoo-module **/
const { Component, onError, xml } = owl;
const { Component, onError, xml, useSubEnv } = owl;
export class ErrorHandler extends Component {
setup() {
@@ -11,3 +11,10 @@ export class ErrorHandler extends Component {
}
ErrorHandler.template = xml`<t t-slot="default" />`;
ErrorEvent.props = ["onError"];
export class WithEnv extends Component {
setup() {
useSubEnv(this.props.env);
}
}
WithEnv.template = xml`<t t-slot="default"/>`;
@@ -24,7 +24,11 @@ const {
xml,
} = owl;
const warningDialogBodyTemplate = xml`<t t-esc="props.message"/>`;
class WarningDialog extends Component {}
WarningDialog.template = xml`<Dialog title="props.title">
<t t-esc="props.message"/>
</Dialog>`;
WarningDialog.components = { Dialog };
class ActionAdapter extends ComponentAdapter {
setup() {
@@ -134,13 +138,6 @@ class ActionAdapter extends ComponentAdapter {
this.title.setParts({ [part]: title || null });
} else if (ev.name === "warning") {
if (payload.type === "dialog") {
class WarningDialog extends Dialog {
setup() {
super.setup();
this.title = this.props.title;
}
}
WarningDialog.bodyTemplate = warningDialogBodyTemplate;
this.dialogs.add(WarningDialog, {
title: payload.title,
message: payload.message,
@@ -6,11 +6,11 @@ import { formatDateTime, parseDateTime } from "@web/core/l10n/dates";
import { formatMany2one } from "@web/fields/formatters";
import { registry } from "@web/core/registry";
const { onWillStart, useState } = owl;
const { Component, onWillStart, useState } = owl;
const debugRegistry = registry.category("debug");
class GetMetadataDialog extends Dialog {
class GetMetadataDialog extends Component {
setup() {
super.setup();
this.state = useState({});
@@ -44,9 +44,10 @@ class GetMetadataDialog extends Dialog {
this.state.write_date = formatDateTime(parseDateTime(metadata.write_date));
}
}
GetMetadataDialog.bodyTemplate = "web.DebugMenu.getMetadataBody";
GetMetadataDialog.template = "web.DebugMenu.GetMetadataDialog";
GetMetadataDialog.components = { Dialog };
GetMetadataDialog.title = _lt("View Metadata");
class SetDefaultDialog extends Dialog {
class SetDefaultDialog extends Component {
setup() {
super.setup();
this.state = {
@@ -184,11 +185,11 @@ class SetDefaultDialog extends Dialog {
true,
this.state.condition || false,
]);
this.trigger("dialog-closed");
this.props.close();
}
}
SetDefaultDialog.bodyTemplate = "web.DebugMenu.setDefaultBody";
SetDefaultDialog.footerTemplate = "web.DebugMenu.SetDefaultFooter";
SetDefaultDialog.template = "web.DebugMenu.SetDefaultDialog";
SetDefaultDialog.components = { Dialog };
SetDefaultDialog.title = _lt("Set Default");
// Form view items
+6 -4
View File
@@ -5,16 +5,18 @@ import { Dialog } from "@web/core/dialog/dialog";
import { editModelDebug } from "@web/core/debug/debug_utils";
import { registry } from "@web/core/registry";
const { xml } = owl;
const { Component, xml } = owl;
const debugRegistry = registry.category("debug");
class FieldViewGetDialog extends Dialog {}
class FieldViewGetDialog extends Component {}
FieldViewGetDialog.template = xml`<Dialog title="this.constructor.title">
<pre t-esc="props.arch"/>
</Dialog>`;
FieldViewGetDialog.components = { Dialog };
FieldViewGetDialog.props = {
...Dialog.props,
arch: { type: String },
};
FieldViewGetDialog.bodyTemplate = xml`<pre t-esc="props.arch"/>`;
FieldViewGetDialog.title = _lt("Fields View Get");
function viewSeparator() {
@@ -8,9 +8,10 @@ import { useLegacyRefs } from "@web/legacy/utils";
const { useEffect } = owl;
const LEGACY_SIZE_CLASSES = {
"extra-large": "modal-xl",
large: "modal-lg",
small: "modal-sm",
"extra-large": "xl",
large: "lg",
medium: "md",
small: "sm",
};
// -----------------------------------------------------------------------------
@@ -23,14 +24,14 @@ class ActionDialog extends Dialog {
const actionProps = this.props && this.props.actionProps;
const action = actionProps && actionProps.action;
this.actionType = action && action.type;
this.title = "title" in this.props ? this.props.title : this.constructor.title;
}
}
ActionDialog.components = { ...Dialog.components, DebugMenu };
ActionDialog.template = "web.ActionDialog";
ActionDialog.bodyTemplate = "web.ActionDialogBody";
ActionDialog.props = {
...Dialog.props,
close: Function,
slots: { optional: true },
ActionComponent: { optional: true },
actionProps: { optional: true },
title: { optional: true },
@@ -38,7 +39,8 @@ ActionDialog.props = {
/**
* This LegacyAdaptedActionDialog class will disappear when legacy code will be entirely rewritten.
* The "ActionDialog" class should get exported from this file when the cleaning will occur.
* The "ActionDialog" class should get exported from this file when the cleaning will occur, and it
* should stop extending Dialog and use it normally instead at that point.
*/
class LegacyAdaptedActionDialog extends ActionDialog {
setup() {
@@ -47,7 +49,7 @@ class LegacyAdaptedActionDialog extends ActionDialog {
const action = actionProps && actionProps.action;
const actionContext = action && action.context;
const actionDialogSize = actionContext && actionContext.dialog_size;
this.size = LEGACY_SIZE_CLASSES[actionDialogSize] || this.constructor.size;
this.props.size = LEGACY_SIZE_CLASSES[actionDialogSize] || Dialog.defaultProps.size;
const ControllerComponent = this.props && this.props.ActionComponent;
const Controller = ControllerComponent && ControllerComponent.Component;
this.isLegacy = Controller && Controller.isLegacy;
@@ -64,6 +66,6 @@ class LegacyAdaptedActionDialog extends ActionDialog {
);
}
}
LegacyAdaptedActionDialog.footerTemplate = "web.LegacyAdaptedActionDialogFooter";
LegacyAdaptedActionDialog.template = "web.LegacyAdaptedActionDialog";
export { LegacyAdaptedActionDialog as ActionDialog };
@@ -10,20 +10,19 @@
{"o_act_window": actionType === "ir.actions.act_window"}
</attribute>
</xpath>
<xpath expr="//t[@t-slot='default']" position="replace">
<t t-if="props.ActionComponent" t-component="props.ActionComponent" t-props="props.actionProps"/>
</xpath>
</t>
<t t-name="web.ActionDialogBody" owl="1">
<t t-if="props.ActionComponent" t-component="props.ActionComponent" t-props="props.actionProps"/>
</t>
<t t-name="web.LegacyAdaptedActionDialogFooter" owl="1">
<t t-if="!isLegacy">
<t t-slot="buttons">
<button class="btn btn-primary" t-on-click="close">
<t>Ok</t>
</button>
<t t-name="web.LegacyAdaptedActionDialog" t-inherit="web.ActionDialog" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-slot='footer']" position="replace">
<t t-if="!isLegacy">
<t t-slot="buttons">
<button class="btn btn-primary" t-on-click="data.close">Ok</button>
</t>
</t>
</t>
</xpath>
</t>
</templates>
@@ -6,6 +6,8 @@ import { registry } from "../../core/registry";
import { _lt } from "../../core/l10n/translation";
import { session } from "@web/session";
const { Component } = owl;
function documentationItem(env) {
const documentationURL = "https://www.odoo.com/documentation/master";
return {
@@ -34,9 +36,10 @@ function supportItem(env) {
};
}
class ShortCutsDialog extends Dialog {}
ShortCutsDialog.bodyTemplate = "web.UserMenu.shortcutsTable";
ShortCutsDialog.title = _lt("Shortcuts");
class ShortcutsDialog extends Component {}
ShortcutsDialog.template = "web.UserMenu.ShortcutsDialog";
ShortcutsDialog.components = { Dialog };
ShortcutsDialog.title = _lt("Shortcuts");
function shortCutsItem(env) {
return {
@@ -45,7 +48,7 @@ function shortCutsItem(env) {
hide: env.isSmall,
description: env._t("Shortcuts"),
callback: () => {
env.services.dialog.add(ShortCutsDialog);
env.services.dialog.add(ShortcutsDialog);
},
sequence: 30,
};
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.UserMenu.shortcutsTable" owl="1">
<div>
<t t-name="web.UserMenu.ShortcutsDialog" owl="1">
<Dialog title="this.constructor.title">
<div class="container-fluid">
<div class="row">
<div class="offset-md-2 col-md-8">
@@ -102,7 +102,7 @@
</div>
</div>
</div>
</div>
</Dialog>
</t>
</templates>
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { CommandPaletteDialog } from "@web/core/commands/command_palette_dialog";
import { CommandPalette } from "@web/core/commands/command_palette";
import { commandService } from "@web/core/commands/command_service";
import { dialogService } from "@web/core/dialog/dialog_service";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
@@ -40,7 +40,7 @@ TestComponent.template = xml`
</div>
`;
QUnit.module("Command Palette Dialog", {
QUnit.module("Command Palette", {
async beforeEach() {
serviceRegistry.add("ui", uiService);
serviceRegistry.add("dialog", dialogService);
@@ -64,7 +64,7 @@ QUnit.test("empty providers", async (assert) => {
const config = {
providers: [],
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -104,7 +104,7 @@ QUnit.test("custom empty message", async (assert) => {
configByNamespace,
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -168,7 +168,7 @@ QUnit.test("custom debounce delay", async (assert) => {
configByNamespace,
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -227,7 +227,7 @@ QUnit.test("concurrency with custom debounce delay", async (assert) => {
configByNamespace,
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -266,7 +266,7 @@ QUnit.test("custom placeholder", async (assert) => {
placeholder: "placeholder test",
providers: [],
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -285,7 +285,7 @@ QUnit.test("add a footer", async (assert) => {
providers: [],
FooterComponent,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -323,7 +323,7 @@ QUnit.test("command with a Custom Component", async (assert) => {
const config = {
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -376,7 +376,7 @@ QUnit.test("multi namespace with provider", async (assert) => {
const config = {
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -430,7 +430,7 @@ QUnit.test("apply a fuzzysearch on the namespace default not on the others", asy
const config = {
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -493,7 +493,7 @@ QUnit.test("multi provider with the same namespace", async (assert) => {
const config = {
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -531,7 +531,7 @@ QUnit.test("check the concurrency during a research", async (assert) => {
const config = {
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -584,7 +584,7 @@ QUnit.test(
searchValue: "C1",
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -632,7 +632,7 @@ QUnit.test("open the command palette with a namespace already in the searchbar",
searchValue: "@",
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -679,7 +679,7 @@ QUnit.test("open the command palette with a searchValue with a namespace", async
searchValue: "@Test",
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -726,7 +726,7 @@ QUnit.test("open the command palette with a searchValue without namespace", asyn
searchValue: "Command1",
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -799,7 +799,7 @@ QUnit.test("multi provider with categories", async (assert) => {
configByNamespace,
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -901,7 +901,7 @@ QUnit.test("don't display by categories if there is a search value", async (asse
configByNamespace,
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -956,7 +956,7 @@ QUnit.test("click on command", async (assert) => {
const config = {
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -966,7 +966,7 @@ QUnit.test("click on command", async (assert) => {
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
["Command1", "Command2"]
);
let focusedCommand = target.querySelector(".o_command.focused");
const focusedCommand = target.querySelector(".o_command.focused");
assert.strictEqual(focusedCommand.textContent, commands[0].name);
await click(target, ".o_command.focused");
assert.verifySteps(["C1"]);
@@ -996,7 +996,7 @@ QUnit.test("press enter on command", async (assert) => {
const config = {
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -1006,7 +1006,7 @@ QUnit.test("press enter on command", async (assert) => {
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
["Command1", "Command2"]
);
let focusedCommand = target.querySelector(".o_command.focused");
const focusedCommand = target.querySelector(".o_command.focused");
assert.strictEqual(focusedCommand.textContent, commands[0].name);
triggerHotkey("arrowdown");
await nextTick();
@@ -1032,7 +1032,7 @@ QUnit.test("keyboard navigation scroll", async (assert) => {
const config = {
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
@@ -1202,7 +1202,7 @@ QUnit.test("multi level command", async (assert) => {
placeholder: "placeholder test",
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -1225,7 +1225,7 @@ QUnit.test("multi level command", async (assert) => {
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
["Command1"]
);
let focusedCommand = target.querySelector(".o_command.focused");
const focusedCommand = target.querySelector(".o_command.focused");
assert.strictEqual(focusedCommand.textContent, commands[0].name);
triggerHotkey("enter");
await nextTick();
@@ -1254,14 +1254,14 @@ QUnit.test("command palette dialog can be rendered and closed on outside click",
const config = {
providers: [],
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
assert.containsOnce(target, ".o_command_palette");
// Close on outside click
window.dispatchEvent(new MouseEvent("mousedown"));
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
await nextTick();
assert.containsNone(target, ".o_command_palette");
});
@@ -1293,7 +1293,7 @@ QUnit.test("navigate in the command palette with the arrows", async (assert) =>
const config = {
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -1339,7 +1339,7 @@ QUnit.test("navigate in the command palette with an empty list", async (assert)
const config = {
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -1391,7 +1391,7 @@ QUnit.test("bold the searchValue on the commands", async (assert) => {
searchValue: "@",
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -1429,7 +1429,7 @@ QUnit.test("bold the searchValue on the commands with special char", async (asse
searchValue: "&",
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -1460,7 +1460,7 @@ QUnit.test("remove namespace with backspace", async (assert) => {
const config = {
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
env.services.dialog.add(CommandPalette, {
config,
});
await nextTick();
@@ -170,6 +170,10 @@ QUnit.module("DebugMenu", (hooks) => {
QUnit.test("Don't display the DebugMenu if debug mode is disabled", async (assert) => {
const env = await makeTestEnv(testConfig);
env.dialogData = {
isActive: true,
close() {},
};
await mount(ActionDialog, target, {
env,
props: { close: () => {} },
@@ -223,6 +227,10 @@ QUnit.module("DebugMenu", (hooks) => {
}
patchWithCleanup(odoo, { debug: "1" });
const env = await makeTestEnv(testConfig);
env.dialogData = {
isActive: true,
close() {},
};
await mount(WithCustom, target, {
env,
props: { close: () => {} },
@@ -56,8 +56,9 @@ QUnit.module("DialogManager", {
});
QUnit.test("Simple rendering with a single dialog", async (assert) => {
assert.expect(4);
class CustomDialog extends Dialog {}
CustomDialog.title = "Welcome";
class CustomDialog extends Component {}
CustomDialog.components = { Dialog };
CustomDialog.template = xml`<Dialog title="'Welcome'">content</Dialog>`;
await mount(PseudoWebClient, target, { env });
assert.containsNone(target, ".o_dialog_container .o_dialog");
env.services.dialog.add(CustomDialog);
@@ -71,8 +72,9 @@ QUnit.test("Simple rendering with a single dialog", async (assert) => {
QUnit.test("Simple rendering and close a single dialog", async (assert) => {
assert.expect(4);
class CustomDialog extends Dialog {}
CustomDialog.title = "Welcome";
class CustomDialog extends Component {}
CustomDialog.components = { Dialog };
CustomDialog.template = xml`<Dialog title="'Welcome'">content</Dialog>`;
await mount(PseudoWebClient, target, { env });
assert.containsNone(target, ".o_dialog_container .o_dialog");
@@ -94,12 +96,10 @@ QUnit.test("Simple rendering and close a single dialog", async (assert) => {
QUnit.test("rendering with two dialogs", async (assert) => {
assert.expect(7);
class CustomDialog extends Dialog {
setup() {
super.setup();
this.title = this.props.title;
}
}
class CustomDialog extends Component {}
CustomDialog.components = { Dialog };
CustomDialog.template = xml`<Dialog title="props.title">content</Dialog>`;
await mount(PseudoWebClient, target, { env });
assert.containsNone(target, ".o_dialog_container .o_dialog");
env.services.dialog.add(CustomDialog, { title: "Hello" });
@@ -120,12 +120,9 @@ QUnit.test("rendering with two dialogs", async (assert) => {
QUnit.test("multiple dialogs can become the UI active element", async (assert) => {
assert.expect(3);
class CustomDialog extends Dialog {
setup() {
super.setup();
this.title = this.props.title;
}
}
class CustomDialog extends Component {}
CustomDialog.components = { Dialog };
CustomDialog.template = xml`<Dialog title="props.title">content</Dialog>`;
await mount(PseudoWebClient, target, { env });
env.services.dialog.add(CustomDialog, { title: "Hello" });
@@ -165,12 +162,9 @@ QUnit.test("Interactions between multiple dialogs", async (assert) => {
return { active, names };
}
class CustomDialog extends Dialog {
setup() {
super.setup();
this.title = this.props.title;
}
}
class CustomDialog extends Component {}
CustomDialog.components = { Dialog };
CustomDialog.template = xml`<Dialog title="props.title">content</Dialog>`;
await mount(PseudoWebClient, target, { env });
env.services.dialog.add(CustomDialog, { title: "Hello" });
@@ -215,13 +209,13 @@ QUnit.test("Interactions between multiple dialogs", async (assert) => {
QUnit.test("dialog component crashes", async (assert) => {
assert.expect(4);
class FailingDialog extends Dialog {
class FailingDialog extends Component {
setup() {
super.setup();
throw new Error("Some Error");
}
}
FailingDialog.title = "Error";
FailingDialog.components = { Dialog };
FailingDialog.template = xml`<Dialog title="'Error'">content</Dialog>`;
const prom = makeDeferred();
patchWithCleanup(ErrorDialog.prototype, {
+68 -99
View File
@@ -9,40 +9,19 @@ import { makeTestEnv } from "../helpers/mock_env";
import { click, destroy, getFixture, mount } from "../helpers/utils";
import { makeFakeDialogService } from "../helpers/mock_services";
const { Component, useEffect, useState, xml } = owl;
const { Component, useState, onMounted, xml } = owl;
const serviceRegistry = registry.category("services");
let parent;
let target;
class SimpleDialog extends Dialog {
setup() {
super.setup();
this.title = "title" in this.props ? this.props.title : this.constructor.title;
this.renderHeader =
"renderHeader" in this.props ? this.props.renderHeader : this.constructor.renderHeader;
this.renderFooter =
"renderFooter" in this.props ? this.props.renderFooter : this.constructor.renderFooter;
this.contentClass =
"contentClass" in this.props ? this.props.contentClass : this.constructor.contentClass;
this.size = "size" in this.props ? this.props.size : this.constructor.size;
this.fullscreen =
"fullscreen" in this.props ? this.props.fullscreen : this.constructor.fullscreen;
const setRef = () => {
if (this.props.setModalRef) {
this.props.setModalRef(this.modalRef.el);
}
};
useEffect(setRef);
}
async function makeDialogTestEnv() {
const env = await makeTestEnv();
env.dialogData = {
isActive: true,
close: () => {},
};
return env;
}
SimpleDialog.bodyTemplate = xml`<t t-slot="default"/>`;
SimpleDialog.props = {
...Dialog.props,
close: { type: Function, optional: true },
};
SimpleDialog.defaultProps = { close: () => {} };
QUnit.module("Components", (hooks) => {
hooks.beforeEach(async () => {
@@ -61,15 +40,15 @@ QUnit.module("Components", (hooks) => {
QUnit.test("simple rendering", async function (assert) {
assert.expect(8);
class Parent extends Dialog {}
Parent.title = "Wow(l) Effect";
Parent.bodyTemplate = xml`
<t>
class Parent extends Component {}
Parent.components = { Dialog };
Parent.template = xml`
<Dialog title="'Wow(l) Effect'">
Hello!
</t>
</Dialog>
`;
const env = await makeTestEnv();
const env = await makeDialogTestEnv();
parent = await mount(Parent, target, { env, props: {} });
assert.containsOnce(target, ".o_dialog");
assert.containsOnce(
@@ -97,16 +76,16 @@ QUnit.module("Components", (hooks) => {
class Parent extends Component {}
Parent.template = xml`
<div>
<SimpleDialog title="'First Title'">
<Dialog title="'First Title'">
Hello!
</SimpleDialog>
<SimpleDialog title="'Second Title'">
</Dialog>
<Dialog title="'Second Title'">
Hello again!
</SimpleDialog>
</Dialog>
</div>
`;
Parent.components = { SimpleDialog };
const env = await makeTestEnv();
Parent.components = { Dialog };
const env = await makeDialogTestEnv();
parent = await mount(Parent, target, { env });
assert.containsN(target, ".o_dialog", 2);
assert.deepEqual(
@@ -121,20 +100,16 @@ QUnit.module("Components", (hooks) => {
QUnit.test("click on the button x triggers the service close", async function (assert) {
assert.expect(3);
const env = await makeTestEnv();
class MyDialog extends SimpleDialog {}
class Parent extends Component {
close() {
assert.step("close");
}
}
const env = await makeDialogTestEnv();
env.dialogData.close = () => assert.step("close");
class Parent extends Component {}
Parent.template = xml`
<MyDialog close="close">
<Dialog>
Hello!
</MyDialog>
</Dialog>
`;
Parent.components = { MyDialog };
Parent.components = { Dialog };
parent = await mount(Parent, target, { env });
assert.containsOnce(target, ".o_dialog");
await click(target, ".o_dialog header button.close");
@@ -144,21 +119,17 @@ QUnit.module("Components", (hooks) => {
QUnit.test(
"click on the default footer button triggers the service close",
async function (assert) {
const env = await makeTestEnv();
const env = await makeDialogTestEnv();
env.dialogData.close = () => assert.step("close");
assert.expect(3);
class MyDialog extends SimpleDialog {}
class Parent extends Component {
close() {
assert.step("close");
}
}
class Parent extends Component {}
Parent.template = xml`
<MyDialog close="close">
<Dialog>
Hello!
</MyDialog>
</Dialog>
`;
Parent.components = { MyDialog };
Parent.components = { Dialog };
parent = await mount(Parent, target, { env });
assert.containsOnce(target, ".o_dialog");
await click(target, ".o_dialog footer button");
@@ -168,17 +139,18 @@ QUnit.module("Components", (hooks) => {
QUnit.test("render custom footer buttons is possible", async function (assert) {
assert.expect(2);
class SimpleButtonsDialog extends Dialog {}
SimpleButtonsDialog.props = {
...Dialog.props,
close: { type: Function, optional: true },
};
SimpleButtonsDialog.defaultProps = { close: () => {} };
SimpleButtonsDialog.footerTemplate = xml`
<div>
<button class="btn btn-primary">The First Button</button>
<button class="btn btn-primary">The Second Button</button>
</div>
class SimpleButtonsDialog extends Component {}
SimpleButtonsDialog.components = { Dialog };
SimpleButtonsDialog.template = xml`
<Dialog>
content
<t t-set-slot="footer">
<div>
<button class="btn btn-primary">The First Button</button>
<button class="btn btn-primary">The Second Button</button>
</div>
</t>
</Dialog>
`;
class Parent extends Component {
setup() {
@@ -194,7 +166,7 @@ QUnit.module("Components", (hooks) => {
</div>
`;
Parent.components = { SimpleButtonsDialog };
const env = await makeTestEnv();
const env = await makeDialogTestEnv();
parent = await mount(Parent, target, { env });
assert.containsOnce(target, ".o_dialog");
assert.containsN(target, ".o_dialog footer button", 2);
@@ -216,13 +188,13 @@ QUnit.module("Components", (hooks) => {
assert.step("message received by parent");
}
}
Parent.components = { SimpleDialog, SubComponent };
Parent.components = { Dialog, SubComponent };
Parent.template = xml`
<SimpleDialog>
<Dialog>
<SubComponent text="'Wow(l) Effect'" onClicked="_onSubcomponentClicked"/>
</SimpleDialog>
</Dialog>
`;
const env = await makeTestEnv();
const env = await makeDialogTestEnv();
parent = await mount(Parent, target, { env });
assert.containsOnce(target, ".o_dialog");
assert.containsOnce(target, ".o_dialog main .o_subcomponent");
@@ -235,10 +207,10 @@ QUnit.module("Components", (hooks) => {
assert.expect(4);
class Parent extends Component {}
Parent.template = xml`
<SimpleDialog renderHeader="false" renderFooter="false"/>
<Dialog header="false" footer="false">content</Dialog>
`;
const env = await makeTestEnv();
Parent.components = { SimpleDialog };
const env = await makeDialogTestEnv();
Parent.components = { Dialog };
parent = await mount(Parent, target, { env });
assert.containsOnce(target, ".o_dialog");
assert.containsNone(target, ".o_dialog header");
@@ -251,13 +223,13 @@ QUnit.module("Components", (hooks) => {
class Parent extends Component {}
Parent.template = xml`
<div>
<SimpleDialog contentClass="'xl'" size="'modal-xl'"/>
<SimpleDialog contentClass="'lg'"/>
<SimpleDialog contentClass="'md'" size="'modal-md'"/>
<SimpleDialog contentClass="'sm'" size="'modal-sm'"/>
<Dialog contentClass="'xl'" size="'xl'">content</Dialog>
<Dialog contentClass="'lg'">content</Dialog>
<Dialog contentClass="'md'" size="'md'">content</Dialog>
<Dialog contentClass="'sm'" size="'sm'">content</Dialog>
</div>`;
Parent.components = { SimpleDialog };
const env = await makeTestEnv();
Parent.components = { Dialog };
const env = await makeDialogTestEnv();
parent = await mount(Parent, target, { env });
assert.containsN(target, ".o_dialog", 4);
assert.containsOnce(
@@ -282,41 +254,38 @@ QUnit.module("Components", (hooks) => {
assert.expect(2);
class Parent extends Component {}
Parent.template = xml`
<div><SimpleDialog fullscreen="true"/></div>
<Dialog fullscreen="true">content</Dialog>
`;
Parent.components = { SimpleDialog };
const env = await makeTestEnv();
Parent.components = { Dialog };
const env = await makeDialogTestEnv();
parent = await mount(Parent, target, { env });
assert.containsOnce(target, ".o_dialog");
assert.hasClass(target.querySelector(".o_dialog .modal"), "o_modal_full");
});
QUnit.test("can be the UI active element", async function (assert) {
assert.expect(3);
assert.expect(4);
class Parent extends Component {
setup() {
this.ui = useService("ui");
this.modal = null;
assert.strictEqual(
this.ui.activeElement,
document,
"UI active element should be the default (document) as Parent is not mounted yet"
);
owl.onMounted(() => {
onMounted(() => {
assert.containsOnce(target, ".modal");
assert.strictEqual(
this.ui.activeElement,
this.modal,
target.querySelector(".modal"),
"UI active element should be the dialog modal"
);
});
}
setModalRef(modalEl) {
this.modal = modalEl;
}
}
const env = await makeTestEnv();
Parent.template = xml`<div><SimpleDialog setModalRef="el => this.setModalRef(el)"/></div>`;
Parent.components = { SimpleDialog };
const env = await makeDialogTestEnv();
Parent.template = xml`<Dialog>content</Dialog>`;
Parent.components = { Dialog };
const parent = await mount(Parent, target, { env });
destroy(parent);
@@ -20,6 +20,15 @@ let target;
let env;
const serviceRegistry = registry.category("services");
async function makeDialogTestEnv() {
const env = await makeTestEnv();
env.dialogData = {
isActive: true,
close() {},
};
return env;
}
QUnit.module("Error dialogs", {
async beforeEach() {
target = getFixture();
@@ -33,7 +42,7 @@ QUnit.module("Error dialogs", {
QUnit.test("ErrorDialog with traceback", async (assert) => {
assert.expect(11);
assert.containsNone(target, ".o_dialog");
env = await makeTestEnv();
env = await makeDialogTestEnv();
await mount(ErrorDialog, target, {
env,
props: {
@@ -84,7 +93,7 @@ QUnit.test("ErrorDialog with traceback", async (assert) => {
QUnit.test("Client ErrorDialog with traceback", async (assert) => {
assert.expect(11);
assert.containsNone(target, ".o_dialog");
env = await makeTestEnv();
env = await makeDialogTestEnv();
await mount(ClientErrorDialog, target, {
env,
props: {
@@ -153,7 +162,7 @@ QUnit.test("button clipboard copy error traceback", async (assert) => {
},
},
});
env = await makeTestEnv();
env = await makeDialogTestEnv();
await mount(ErrorDialog, target, {
env,
props: {
@@ -171,7 +180,7 @@ QUnit.test("button clipboard copy error traceback", async (assert) => {
QUnit.test("WarningDialog", async (assert) => {
assert.expect(6);
assert.containsNone(target, ".o_dialog");
env = await makeTestEnv();
env = await makeDialogTestEnv();
await mount(WarningDialog, target, {
env,
props: {
@@ -201,7 +210,7 @@ QUnit.test("RedirectWarningDialog", async (assert) => {
},
};
serviceRegistry.add("action", faceActionService);
env = await makeTestEnv();
env = await makeDialogTestEnv();
assert.containsNone(target, ".o_dialog");
await mount(RedirectWarningDialog, target, {
env,
@@ -221,7 +230,7 @@ QUnit.test("RedirectWarningDialog", async (assert) => {
assert.containsOnce(target, ".o_dialog");
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Odoo Warning");
assert.strictEqual(target.querySelector("main").textContent, "Some strange unreadable message");
let footerButtons = target.querySelectorAll("footer button");
const footerButtons = target.querySelectorAll("footer button");
assert.deepEqual(
[...footerButtons].map((el) => el.textContent),
["Buy book on cryptography", "Cancel"]
@@ -236,7 +245,7 @@ QUnit.test("RedirectWarningDialog", async (assert) => {
QUnit.test("Error504Dialog", async (assert) => {
assert.expect(5);
assert.containsNone(target, ".o_dialog");
env = await makeTestEnv();
env = await makeDialogTestEnv();
await mount(Error504Dialog, target, { env, props: { close() {} } });
assert.containsOnce(target, ".o_dialog");
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Request timeout");
@@ -256,7 +265,7 @@ QUnit.test("SessionExpiredDialog", async (assert) => {
},
},
});
env = await makeTestEnv();
env = await makeDialogTestEnv();
assert.containsNone(target, ".o_dialog");
await mount(SessionExpiredDialog, target, { env, props: { close() {} } });
assert.containsOnce(target, ".o_dialog");
@@ -8,6 +8,7 @@ odoo.define('web.owl_dialog_tests', function (require) {
const { makeLegacyDialogMappingTestEnv } = require('@web/../tests/helpers/legacy_env_utils');
const { Dialog: WowlDialog } = require("@web/core/dialog/dialog");
const { WithEnv } = require("@web/core/utils/components");
const {
getFixture,
nextTick,
@@ -17,7 +18,7 @@ odoo.define('web.owl_dialog_tests', function (require) {
const { createWebClient, doAction } = require("@web/../tests/webclient/helpers");
const { LegacyComponent } = require("@web/legacy/legacy_component");
const { useState, xml } = owl;
const { Component, useState, xml } = owl;
const EscapeKey = { key: 'Escape', keyCode: 27, which: 27 };
QUnit.module('core', {}, function () {
@@ -290,12 +291,9 @@ odoo.define('web.owl_dialog_tests', function (require) {
/>
`;
OwlDialogWrapper.components = { Dialog };
class WowlDialogSubClass extends WowlDialog{
setup() {
super.setup();
this.contentClass = this.props.contentClass;
}
}
class WowlDialogWrapper extends Component {}
WowlDialogWrapper.components = { WowlDialog };
WowlDialogWrapper.template = xml`<WowlDialog contentClass="props.contentClass">content</WowlDialog>`;
class Parent extends LegacyComponent {
setup() {
this.dialogs = useState([]);
@@ -309,20 +307,25 @@ odoo.define('web.owl_dialog_tests', function (require) {
Parent.template = xml`
<div>
<div class="o_dialog_container"/>
<t t-foreach="dialogs" t-as="dialog" t-key="dialog.id" t-component="dialog.class"
contentClass="'dialog_' + dialog.id"
close="() => this._onDialogClosed(dialog.id)"
/>
<t t-foreach="dialogs" t-as="dialog" t-key="dialog.id">
<WithEnv env="{ dialogData: { isActive: true, close: () => this._onDialogClosed(dialog.id) } }">
<t t-component="dialog.class"
contentClass="'dialog_' + dialog.id"
close="() => this._onDialogClosed(dialog.id)"
/>
</WithEnv>
</t>
</div>`;
Parent.components = { WithEnv };
const target = getFixture();
const parent = await mount(Parent, target, { env });
parent.dialogs.push({ id: 1, class: WowlDialogSubClass });
parent.dialogs.push({ id: 1, class: WowlDialogWrapper });
await nextTick();
parent.dialogs.push({ id: 2, class: OwlDialogWrapper });
await nextTick();
parent.dialogs.push({ id: 3, class: WowlDialogSubClass });
parent.dialogs.push({ id: 3, class: WowlDialogWrapper });
await nextTick();
assert.verifySteps([]);
@@ -4,9 +4,10 @@ import { useService } from "@web/core/utils/hooks";
import { Dialog } from "@web/core/dialog/dialog";
import { _lt } from "@web/core/l10n/translation";
export default class ToursDialog extends Dialog {
const { Component } = owl;
export default class ToursDialog extends Component {
setup() {
super.setup();
this.tourService = useService("tour");
this.onboardingTours = this.tourService.getOnboardingTours();
this.testingTours = this.tourService.getTestingTours();
@@ -24,7 +25,7 @@ export default class ToursDialog extends Dialog {
*/
_onStartTour(ev) {
this.tourService.reset(ev.target.dataset.name);
this.close();
this.props.close();
}
/**
* Starts the given tour in test mode.
@@ -34,8 +35,9 @@ export default class ToursDialog extends Dialog {
*/
_onTestTour(ev) {
this.tourService.run(ev.target.dataset.name);
this.close();
this.props.close();
}
}
ToursDialog.bodyTemplate = "web_tour.ToursDialog";
ToursDialog.template = "web_tour.ToursDialog";
ToursDialog.components = { Dialog };
ToursDialog.title = _lt("Tours");
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="web_tour.ToursDialog" owl="1">
<div>
<Dialog title="this.constructor.title">
<t t-call="web_tour.ToursDialog.Table">
<t t-set="caption">Onboarding tours</t>
<t t-set="tours" t-value="onboardingTours"/>
@@ -11,7 +11,7 @@
<t t-set="caption">Testing tours</t>
<t t-set="tours" t-value="testingTours"/>
</t>
</div>
</Dialog>
</t>
<t t-name="web_tour.ToursDialog.Table" owl="1">