[REF] web: adapt code to owl 2
Owl 2 changelog: https://github.com/odoo/owl/blob/a9f29c4caad4f32d06be1ec4780572825781cd9b/CHANGELOG.md Part-of: odoo/odoo#80156 Co-authored-by: Aaron Bohy <aab@odoo.com> Co-authored-by: Bruno Boi <boi@odoo.com> Co-authored-by: Géry Debongnie <ged@odoo.com> Co-authored-by: Samuel Degueldre <sad@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: Simon Genin (ges) <ges@odoo.com> Co-authored-by: Francois (fge) <fge@odoo.com> Co-authored-by: Michael Mattiello (mcm) <mcm@odoo.com> Co-authored-by: Julien Mougenot <jum@odoo.com> Co-authored-by: luvi <luvi@odoo.com> Co-authored-by: Lucas Perais (lpe) <lpe@odoo.com> Co-authored-by: Jorge Pinna Puissant <jpp@odoo.com>
This commit is contained in:
committed by
Samuel Degueldre
co-authored by
Aaron Bohy
Bruno Boi
Géry Debongnie
Samuel Degueldre
Simon Genin
Francois
Michael Mattiello
Julien Mougenot
luvi
Lucas Perais
Jorge Pinna Puissant
parent
966abc39b5
commit
6d13271aac
@@ -359,7 +359,7 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/lib/underscore.string/lib/underscore.string.js',
|
||||
'web/static/lib/moment/moment.js',
|
||||
'web/static/lib/owl/owl.js',
|
||||
'web/static/src/owl_next_adapter.js',
|
||||
'web/static/src/owl2_compatibility/*.js',
|
||||
'web/static/src/legacy/js/component_extension.js',
|
||||
'web/static/lib/jquery/jquery.js',
|
||||
'web/static/lib/jquery.ui/jquery-ui.js',
|
||||
@@ -405,7 +405,6 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/src/legacy/js/core/ajax.js',
|
||||
'web/static/src/legacy/js/core/browser_detection.js',
|
||||
'web/static/src/legacy/js/core/bus.js',
|
||||
'web/static/src/legacy/js/core/custom_hooks.js',
|
||||
'web/static/src/legacy/js/core/class.js',
|
||||
'web/static/src/legacy/js/core/collections.js',
|
||||
'web/static/src/legacy/js/core/concurrency.js',
|
||||
@@ -453,6 +452,11 @@ This module provides the core of the Odoo Web Client.
|
||||
# first called in web.
|
||||
'web/static/tests/legacy/helpers/test_utils_file.js'
|
||||
],
|
||||
# remove this bundle alongside the owl2 compatibility layer
|
||||
'web.tests_assets_common': [
|
||||
('include', 'web.assets_common'),
|
||||
('after', 'web/static/src/owl2_compatibility/app.js', 'web/static/tests/owl2_compatibility_app.js'),
|
||||
],
|
||||
'web.tests_assets': [
|
||||
'web/static/lib/qunit/qunit-2.9.1.css',
|
||||
'web/static/lib/qunit/qunit-2.9.1.js',
|
||||
|
||||
@@ -55,7 +55,7 @@ class AssetsLoadingError extends Error {}
|
||||
* @param {string} url the url of the script
|
||||
* @returns {Promise<true>} resolved when the script has been loaded
|
||||
*/
|
||||
const loadJS = memoize(function loadJS(url) {
|
||||
export const loadJS = memoize(function loadJS(url) {
|
||||
if (document.querySelector(`script[src="${url}"]`)) {
|
||||
// Already in the DOM and wasn't loaded through this function
|
||||
// Unfortunately there is no way to check whether a script has loaded
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
let sessionStorage = window.sessionStorage;
|
||||
let localStorage = owl.browser.localStorage;
|
||||
let localStorage = window.localStorage;
|
||||
try {
|
||||
// Safari crashes in Private Browsing
|
||||
localStorage.setItem("__localStorage__", "true");
|
||||
@@ -20,9 +20,13 @@ try {
|
||||
sessionStorage = makeRAMLocalStorage();
|
||||
}
|
||||
|
||||
export const browser = Object.assign({}, owl.browser, {
|
||||
export const browser = {
|
||||
addEventListener: window.addEventListener.bind(window),
|
||||
removeEventListener: window.removeEventListener.bind(window),
|
||||
setTimeout: window.setTimeout.bind(window),
|
||||
clearTimeout: window.clearTimeout.bind(window),
|
||||
setInterval: window.setInterval.bind(window),
|
||||
clearInterval: window.clearInterval.bind(window),
|
||||
requestAnimationFrame: window.requestAnimationFrame.bind(window),
|
||||
cancelAnimationFrame: window.cancelAnimationFrame.bind(window),
|
||||
console: window.console,
|
||||
@@ -32,7 +36,8 @@ export const browser = Object.assign({}, owl.browser, {
|
||||
XMLHttpRequest: window.XMLHttpRequest,
|
||||
localStorage,
|
||||
sessionStorage,
|
||||
});
|
||||
fetch: window.fetch.bind(window),
|
||||
};
|
||||
|
||||
Object.defineProperty(browser, "location", {
|
||||
set(val) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const { Component, QWeb } = owl;
|
||||
const { Component } = owl;
|
||||
|
||||
/**
|
||||
* Custom checkbox
|
||||
@@ -8,7 +8,7 @@ const { Component, QWeb } = owl;
|
||||
* <CheckBox
|
||||
* value="boolean"
|
||||
* disabled="boolean"
|
||||
* t-on-change="_onValueChange"
|
||||
* onChange="_onValueChange"
|
||||
* >
|
||||
* Change the label text
|
||||
* </CheckBox>
|
||||
@@ -24,5 +24,6 @@ export class CheckBox extends Component {
|
||||
|
||||
CheckBox.template = "web.CheckBox";
|
||||
CheckBox.nextId = 1;
|
||||
|
||||
QWeb.registerComponent("CheckBox", CheckBox);
|
||||
CheckBox.defaultProps = {
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
class="custom-control-input"
|
||||
t-att-disabled="props.disabled"
|
||||
t-att-checked="props.value"
|
||||
t-on-change="(ev) => props.onChange(ev.target.checked)"
|
||||
/>
|
||||
<label t-att-for="props.id or id" class="custom-control-label">
|
||||
<t t-slot="default"/>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useEffect, useService } from "@web/core/utils/hooks";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { useEffect } = owl;
|
||||
|
||||
/**
|
||||
* @typedef {import("./command_service").CommandOptions} CommandOptions
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="o_command_hotkey">
|
||||
<t t-slot="name"/>
|
||||
<span>
|
||||
<t t-foreach="getKeysToPress(props)" t-as="key">
|
||||
<t t-foreach="getKeysToPress(props)" t-as="key" t-key="key_index">
|
||||
<kbd t-esc="key" />
|
||||
<span t-if="!key_last"> + </span>
|
||||
</t>
|
||||
|
||||
@@ -4,20 +4,19 @@
|
||||
<t t-name="web.CommandPalette" owl="1">
|
||||
<div>
|
||||
<div class="o_command_palette_search">
|
||||
<input type="text" t-model="state.searchValue" t-on-input="onSearchInput" autofocus="" t-att-placeholder="state.placeholder"/>
|
||||
<input type="text" t-model="state.searchValue" t-on-input="onSearchInput" t-ref="autofocus" t-att-placeholder="state.placeholder"/>
|
||||
<i t-att-title="state.placeholder" role="img" t-att-aria-label="state.placeholder" class="fa fa-search"></i>
|
||||
</div>
|
||||
|
||||
<div t-ref="listbox" class="o_command_palette_listbox">
|
||||
<div t-if="!state.commands.length" class="o_command_palette_listbox_empty" t-esc="state.emptyMessage"/>
|
||||
<t t-if="!isFuzzySearch">
|
||||
<t t-foreach="commandsByCategory" t-as="category">
|
||||
|
||||
<div class="o_command_category" t-key="category.keyId">
|
||||
<t t-foreach="category.commands" t-as="command">
|
||||
<t t-foreach="commandsByCategory" t-as="category" t-key="category.keyId">
|
||||
<div class="o_command_category">
|
||||
<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="onCommandClicked(commandIndex)" t-on-mouseenter="onCommandMouseEnter(commandIndex)" t-key="command.keyId">
|
||||
<t t-component="command.Component || DefaultCommandItem" name="command.name" searchValue="clearSearchValue" t-props="command.props" t-on-close="props.closeMe()" t-on-execute-command="executeCommand(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-on-execute-command="() => this.executeCommand(command)">
|
||||
<t t-component="command.Component || DefaultCommandItem" name="command.name" searchValue="clearSearchValue" t-props="command.props">
|
||||
<t t-set-slot="name">
|
||||
<span class="o_command_name">
|
||||
<t t-foreach="command.splitName" t-as="name" t-key="name_index">
|
||||
|
||||
@@ -47,7 +47,7 @@ class DefaultFooter extends Component {
|
||||
DefaultFooter.template = xml`
|
||||
<span>
|
||||
<span class='o_promote'>TIP</span> — search for
|
||||
<t t-foreach="elements" t-as="element">
|
||||
<t t-foreach="elements" t-as="element" t-key="element.namespace">
|
||||
<t t-if="!(element_first || element_last)">, </t>
|
||||
<t t-if="element_last and !element_first"> and </t>
|
||||
<span t-esc="element.namespace" class='o_promote'/><t t-esc="element.name"/>
|
||||
|
||||
@@ -23,17 +23,18 @@ export class ConfirmationDialog extends Dialog {
|
||||
}
|
||||
}
|
||||
ConfirmationDialog.props = {
|
||||
...Dialog.props,
|
||||
title: {
|
||||
validate: (m) => {
|
||||
return (
|
||||
typeof m === "string" || (typeof m === "object" && typeof m.toString === "function")
|
||||
);
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
body: String,
|
||||
confirm: { type: Function, optional: true },
|
||||
cancel: { type: Function, optional: true },
|
||||
close: Function,
|
||||
};
|
||||
ConfirmationDialog.defaultProps = {
|
||||
title: _lt("Confirmation"),
|
||||
|
||||
@@ -4,7 +4,15 @@ import { localization } from "@web/core/l10n/localization";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useAutofocus } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, useExternalListener, useRef, useState } = owl;
|
||||
const {
|
||||
Component,
|
||||
onMounted,
|
||||
onWillUpdateProps,
|
||||
onWillUnmount,
|
||||
useExternalListener,
|
||||
useRef,
|
||||
useState,
|
||||
} = owl;
|
||||
const { DateTime } = luxon;
|
||||
|
||||
const formatters = registry.category("formatters");
|
||||
@@ -53,9 +61,13 @@ export class DatePicker extends Component {
|
||||
|
||||
useAutofocus();
|
||||
useExternalListener(window, "scroll", this.onWindowScroll);
|
||||
|
||||
onMounted(this.onMounted);
|
||||
onWillUpdateProps(this.onWillUpdateProps);
|
||||
onWillUnmount(this.onWillUnmount);
|
||||
}
|
||||
|
||||
mounted() {
|
||||
onMounted() {
|
||||
window.$(this.el).on("show.datetimepicker", () => this.inputRef.el.select());
|
||||
window.$(this.el).on("hide.datetimepicker", () => this.onDateChange());
|
||||
window.$(this.el).on("error.datetimepicker", () => false); // Ignores datepicker errors
|
||||
@@ -64,7 +76,7 @@ export class DatePicker extends Component {
|
||||
this.updateInput();
|
||||
}
|
||||
|
||||
willUpdateProps(nextProps) {
|
||||
onWillUpdateProps(nextProps) {
|
||||
const pickerParams = {};
|
||||
for (const prop in nextProps) {
|
||||
if (this.props[prop] !== nextProps[prop]) {
|
||||
@@ -78,7 +90,7 @@ export class DatePicker extends Component {
|
||||
this.bootstrapDateTimePicker(pickerParams);
|
||||
}
|
||||
|
||||
willUnmount() {
|
||||
onWillUnmount() {
|
||||
window.$(this.el).off(); // Removes all jQuery events
|
||||
|
||||
this.bootstrapDateTimePicker("destroy");
|
||||
@@ -229,7 +241,7 @@ DatePicker.props = {
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
calendarWeeks: Boolean,
|
||||
calendarWeeks: { type: Boolean, optional: true },
|
||||
format: { type: String, optional: true },
|
||||
icons: {
|
||||
type: Object,
|
||||
@@ -244,14 +256,15 @@ DatePicker.props = {
|
||||
today: String,
|
||||
up: String,
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
keyBinds: { validate: (kb) => typeof kb === "object" || kb === null, optional: true },
|
||||
locale: { type: String, optional: true },
|
||||
maxDate: DateTime,
|
||||
minDate: DateTime,
|
||||
maxDate: { type: DateTime, optional: true },
|
||||
minDate: { type: DateTime, optional: true },
|
||||
readonly: { type: Boolean, optional: true },
|
||||
useCurrent: Boolean,
|
||||
widgetParent: String,
|
||||
useCurrent: { type: Boolean, optional: true },
|
||||
widgetParent: { type: String, optional: true },
|
||||
};
|
||||
DatePicker.template = "web.DatePicker";
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<div class="o_datepicker" aria-atomic="true" t-att-id="datePickerId" data-target-input="nearest">
|
||||
<input type="text"
|
||||
class="o_datepicker_input o_input datetimepicker-input"
|
||||
autofocus=""
|
||||
t-att-name="props.name"
|
||||
t-att-placeholder="props.placeholder"
|
||||
t-attf-data-target="#{{ datePickerId }}"
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
import { registry } from "../registry";
|
||||
import { memoize } from "../utils/functions";
|
||||
import { useEffect } from "@web/core/utils/hooks";
|
||||
|
||||
const { useEnv, useSubEnv } = owl;
|
||||
const { useEffect, useEnv, useSubEnv } = owl;
|
||||
const debugRegistry = registry.category("debug");
|
||||
|
||||
const getAccessRights = memoize(async function getAccessRights(orm) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { DebugMenuBasic } from "@web/core/debug/debug_menu_basic";
|
||||
import { useCommand } from "@web/core/commands/command_hook";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
@@ -53,3 +55,4 @@ export class DebugMenu extends DebugMenuBasic {
|
||||
);
|
||||
}
|
||||
}
|
||||
DebugMenu.components = { Dropdown, DropdownItem };
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DebugMenu" owl="1">
|
||||
<Dropdown class="o_debug_manager"
|
||||
<Dropdown class="'o_debug_manager'"
|
||||
beforeOpen="getElements"
|
||||
position="'bottom-end'"
|
||||
togglerClass="`o-dropdown--narrow ${env.inDialog?'btn btn-link':''}`">
|
||||
togglerClass="`o-dropdown--narrow ${env.inDialog?'btn btn-link':''}`"
|
||||
>
|
||||
<t t-set-slot="toggler">
|
||||
<i class="oi oi-large oi-debug"/>
|
||||
</t>
|
||||
@@ -14,8 +15,9 @@
|
||||
t-if="element.type == 'item'"
|
||||
onSelected="element.callback"
|
||||
href="element.href"
|
||||
t-esc="element.description"
|
||||
/>
|
||||
>
|
||||
<t t-esc="element.description"/>
|
||||
</DropdownItem>
|
||||
<div t-if="element.type == 'separator'" role="separator" class="dropdown-divider"/>
|
||||
<t t-if="element.type == 'component'" t-component="element.Component" t-props="element.props"/>
|
||||
</t>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useEnvDebugContext } from "./debug_context";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
@@ -13,4 +15,8 @@ export class DebugMenuBasic extends Component {
|
||||
};
|
||||
}
|
||||
}
|
||||
DebugMenuBasic.components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
};
|
||||
DebugMenuBasic.template = "web.DebugMenu";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DebugMenu.SetDefaultFooter" owl="1">
|
||||
<button class="btn btn-secondary" t-on-click="trigger('dialog-closed')">Close</button>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export class ProfilingItem extends Component {
|
||||
changeParam(param, ev) {
|
||||
this.profiling.setParam(param, ev.target.value);
|
||||
}
|
||||
toggleParam(param, ev) {
|
||||
toggleParam(param) {
|
||||
const value = this.profiling.state.params.execution_context_qweb;
|
||||
this.profiling.setParam(param, !value);
|
||||
}
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
<div class="align-self-center">
|
||||
<div t-if="profiling.state.isEnabled" class="alert alert-info py-2 mr-3">Recording...</div>
|
||||
<span class="o_profiling_switch">
|
||||
<span class="custom-control custom-switch" t-on-click.stop.prevent="profiling.toggleProfiling()">
|
||||
<span class="custom-control custom-switch" t-on-click.stop.prevent="() => this.profiling.toggleProfiling()">
|
||||
<input type="checkbox" class="custom-control-input" id="enable_profiling" t-att-checked="profiling.state.isEnabled"/>
|
||||
<label class="custom-control-label">Enable profiling</label>
|
||||
</span>
|
||||
</span>
|
||||
<t t-if="profiling.state.isEnabled">
|
||||
<span class="o_profiling_switch custom-control custom-switch mt-2" t-on-click.stop.prevent="profiling.toggleCollector('sql')">
|
||||
<span class="o_profiling_switch custom-control custom-switch mt-2" t-on-click.stop.prevent="() => this.profiling.toggleCollector('sql')">
|
||||
<input type="checkbox" class="custom-control-input" id="profile_sql"
|
||||
t-att-checked="profiling.isCollectorEnabled('sql')"/>
|
||||
<label class="custom-control-label" for="profile_sql">Record sql</label>
|
||||
</span>
|
||||
<span class="o_profiling_switch custom-control custom-switch mt-2" t-on-click.stop.prevent="profiling.toggleCollector('traces_async')">
|
||||
<span class="o_profiling_switch custom-control custom-switch mt-2" t-on-click.stop.prevent="() => this.profiling.toggleCollector('traces_async')">
|
||||
<input type="checkbox" class="custom-control-input" id="profile_traces_async"
|
||||
t-att-checked="profiling.isCollectorEnabled('traces_async')"/>
|
||||
<label class="custom-control-label" for="profile_traces_async">Record traces</label>
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">Interval</div>
|
||||
</div>
|
||||
<select class="profile_param form-control" t-on-change="changeParam('traces_async_interval')">
|
||||
<select class="profile_param form-control" t-on-change="(ev) => this.changeParam('traces_async_interval', ev)">
|
||||
<t t-set="interval" t-value="profiling.state.params.traces_async_interval"/>
|
||||
<option value="">Default</option>
|
||||
<option value="0.001" t-att-selected="interval === '0.001'">0.001</option>
|
||||
@@ -36,12 +36,12 @@
|
||||
<option value="1" t-att-selected="interval === '1'">1</option>
|
||||
</select>
|
||||
</div>
|
||||
<span t-if="profiling.isCollectorEnabled('sql') || profiling.isCollectorEnabled('traces_async')" class="o_profiling_switch custom-control custom-switch mt-2" t-on-click.stop.prevent="toggleParam('execution_context_qweb')">
|
||||
<span t-if="profiling.isCollectorEnabled('sql') || profiling.isCollectorEnabled('traces_async')" class="o_profiling_switch custom-control custom-switch mt-2" t-on-click.stop.prevent="(ev) => this.toggleParam('execution_context_qweb', ev)">
|
||||
<input type="checkbox" class="custom-control-input" id="profile_execution_context_qweb"
|
||||
t-att-checked="!!profiling.state.params.execution_context_qweb"/>
|
||||
<label class="custom-control-label" for="profile_execution_context_qweb">Add qweb directive context</label>
|
||||
</span>
|
||||
<span class="o_profiling_switch custom-control custom-switch mt-2" t-on-click.stop.prevent="profiling.toggleCollector('qweb')">
|
||||
<span class="o_profiling_switch custom-control custom-switch mt-2" t-on-click.stop.prevent="() => this.profiling.toggleCollector('qweb')">
|
||||
<input type="checkbox" class="custom-control-input" id="profile_qweb"
|
||||
t-att-checked="profiling.isCollectorEnabled('qweb')"/>
|
||||
<label class="custom-control-label" for="profile_qweb">Record qweb</label>
|
||||
|
||||
@@ -24,7 +24,15 @@ const profilingService = {
|
||||
const debugItem = document.querySelector(".o_debug_manager .dropdown-toggle");
|
||||
if (state.isEnabled) {
|
||||
recordingIcon = document.createElement("i");
|
||||
recordingIcon.classList.add("o_recording", "badge", "badge-pill", "d-inline", "p-2", "bg-danger", "border");
|
||||
recordingIcon.classList.add(
|
||||
"o_recording",
|
||||
"badge",
|
||||
"badge-pill",
|
||||
"d-inline",
|
||||
"p-2",
|
||||
"bg-danger",
|
||||
"border"
|
||||
);
|
||||
debugItem.appendChild(recordingIcon);
|
||||
} else if (recordingIcon) {
|
||||
debugItem.removeChild(recordingIcon);
|
||||
@@ -33,7 +41,7 @@ const profilingService = {
|
||||
}
|
||||
|
||||
if (env.debug) {
|
||||
env.bus.on("WEB_CLIENT_READY", null, updateDebugIcon);
|
||||
env.bus.addEventListener("WEB_CLIENT_READY", updateDebugIcon);
|
||||
}
|
||||
|
||||
async function setProfiling(params) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
|
||||
import { useActiveElement } from "../ui/ui_service";
|
||||
|
||||
const { Component, useRef, useSubEnv, xml } = owl;
|
||||
const { Component, useRef, useChildSubEnv, xml } = owl;
|
||||
|
||||
export class Dialog extends Component {
|
||||
setup() {
|
||||
@@ -17,7 +17,7 @@ export class Dialog extends Component {
|
||||
useHotkey("escape", () => {
|
||||
this.close();
|
||||
});
|
||||
useSubEnv({ inDialog: true });
|
||||
useChildSubEnv({ inDialog: true });
|
||||
this.close = this.close.bind(this);
|
||||
this.contentClass = this.constructor.contentClass;
|
||||
this.fullscreen = this.constructor.fullscreen;
|
||||
@@ -47,3 +47,8 @@ Dialog.technical = true;
|
||||
Dialog.title = "Odoo";
|
||||
Dialog.bodyTemplate = xml`<div/>`;
|
||||
Dialog.footerTemplate = "web.DialogFooterDefault";
|
||||
Dialog.props = {
|
||||
close: Function,
|
||||
isActive: { optional: true },
|
||||
"*": true,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.Dialog" owl="1">
|
||||
<div class="o_dialog">
|
||||
<div class="o_dialog" t-att-class="{ o_inactive_modal: !props.isActive }" t-on-dialog-closed="close">
|
||||
<div role="dialog" class="modal"
|
||||
tabindex="-1"
|
||||
t-att-class="{ o_technical_modal: technical, o_modal_full: fullscreen }"
|
||||
|
||||
@@ -6,7 +6,7 @@ const { Component, xml } = owl;
|
||||
|
||||
export class DialogContainer extends Component {
|
||||
setup() {
|
||||
this.props.bus.on("UPDATE", this, this.render);
|
||||
this.props.bus.addEventListener("UPDATE", this.render.bind(this));
|
||||
}
|
||||
|
||||
close(id) {
|
||||
@@ -23,16 +23,18 @@ export class DialogContainer extends Component {
|
||||
}
|
||||
}
|
||||
DialogContainer.components = { ErrorHandler, NotUpdatable };
|
||||
//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}">
|
||||
<t t-foreach="Object.values(props.dialogs)" t-as="dialog" t-key="dialog.id">
|
||||
<NotUpdatable>
|
||||
<ErrorHandler onError="(error) => this.handleError(error, dialog.id)">
|
||||
<t t-component="dialog.class" t-props="dialog.props"
|
||||
t-on-dialog-closed="dialog.props.close()"
|
||||
t-att-class="{o_inactive_modal: !dialog_last}"/>
|
||||
</ErrorHandler>
|
||||
</NotUpdatable>
|
||||
</t>
|
||||
<div>
|
||||
<t t-foreach="Object.values(props.dialogs)" t-as="dialog" t-key="dialog.id">
|
||||
<NotUpdatable>
|
||||
<ErrorHandler onError="(error) => this.handleError(error, dialog.id)">
|
||||
<t t-component="dialog.class" t-props="dialog.props" isActive="dialog_last"/>
|
||||
</ErrorHandler>
|
||||
</NotUpdatable>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useBus, useEffect, useService } from "@web/core/utils/hooks";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { usePosition } from "../position/position_hook";
|
||||
import { useDropdownNavigation } from "./dropdown_navigation_hook";
|
||||
import { localization } from "../l10n/localization";
|
||||
@@ -8,12 +8,12 @@ import { localization } from "../l10n/localization";
|
||||
const {
|
||||
Component,
|
||||
EventBus,
|
||||
QWeb,
|
||||
onWillStart,
|
||||
useEffect,
|
||||
useExternalListener,
|
||||
useRef,
|
||||
useState,
|
||||
useSubEnv,
|
||||
useChildSubEnv,
|
||||
} = owl;
|
||||
|
||||
const DIRECTION_CARET_CLASS = {
|
||||
@@ -59,7 +59,9 @@ export class Dropdown extends Component {
|
||||
// Close on outside click listener
|
||||
useExternalListener(window, "click", this.onWindowClicked);
|
||||
// Listen to all dropdowns state changes
|
||||
useBus(Dropdown.bus, "state-changed", this.onDropdownStateChanged);
|
||||
useBus(Dropdown.bus, "state-changed", ({ detail }) =>
|
||||
this.onDropdownStateChanged(detail)
|
||||
);
|
||||
}
|
||||
|
||||
// Set up UI active element related behavior ---------------------------
|
||||
@@ -75,7 +77,7 @@ export class Dropdown extends Component {
|
||||
|
||||
// Set up nested dropdowns ---------------------------------------------
|
||||
this.parentDropdown = this.env[DROPDOWN];
|
||||
useSubEnv({
|
||||
useChildSubEnv({
|
||||
[DROPDOWN]: {
|
||||
close: this.close.bind(this),
|
||||
closeAllParents: () => {
|
||||
@@ -273,6 +275,10 @@ export class Dropdown extends Component {
|
||||
}
|
||||
Dropdown.bus = new EventBus();
|
||||
Dropdown.props = {
|
||||
class: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
toggler: {
|
||||
type: String,
|
||||
optional: true,
|
||||
@@ -314,7 +320,9 @@ Dropdown.props = {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
slots: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
};
|
||||
Dropdown.template = "web.Dropdown";
|
||||
|
||||
QWeb.registerComponent("Dropdown", Dropdown);
|
||||
|
||||
@@ -4,13 +4,20 @@
|
||||
<t t-name="web.Dropdown" owl="1">
|
||||
<div
|
||||
class="o-dropdown dropdown"
|
||||
t-att-class="{ show: state.open, 'o-dropdown--no-caret': !parentDropdown }"
|
||||
t-attf-class="{{ directionCaretClass || '' }}"
|
||||
t-att-class="props.class"
|
||||
t-attf-class="
|
||||
{{ directionCaretClass || ''}}
|
||||
{{ state.open ? 'show' : ''}}
|
||||
{{ !parentDropdown ? 'o-dropdown--no-caret' : '' }}
|
||||
"
|
||||
>
|
||||
<button
|
||||
t-if="props.toggler !== 'parent'"
|
||||
class="dropdown-toggle"
|
||||
t-attf-class="{{props.togglerClass || ''}} {{parentDropdown ? 'dropdown-item' : ''}}"
|
||||
t-attf-class="
|
||||
{{props.togglerClass || ''}}
|
||||
{{parentDropdown ? 'dropdown-item' : ''}}
|
||||
"
|
||||
t-on-click="onTogglerClick"
|
||||
t-on-mouseenter="onTogglerMouseEnter"
|
||||
t-att-title="props.title"
|
||||
@@ -20,7 +27,12 @@
|
||||
>
|
||||
<t t-slot="toggler" />
|
||||
</button>
|
||||
<div t-if="state.open" class="o-dropdown--menu dropdown-menu d-block" t-att-class="props.menuClass" t-ref="menuRef">
|
||||
<div
|
||||
t-if="state.open"
|
||||
class="o-dropdown--menu dropdown-menu d-block"
|
||||
t-att-class="props.menuClass"
|
||||
t-ref="menuRef"
|
||||
>
|
||||
<t t-slot="default" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
import { DROPDOWN } from "./dropdown";
|
||||
|
||||
const { Component, QWeb } = owl;
|
||||
const { Component } = owl;
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
@@ -12,19 +12,6 @@ const ParentClosingMode = {
|
||||
AllParents: "all",
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef DropdownItemSelectedEventDetail
|
||||
* @property {*} payload
|
||||
* @property {Object} dropdownClosingRequest
|
||||
* @property {boolean} dropdownClosingRequest.isFresh
|
||||
* @property {ParentClosingMode} dropdownClosingRequest.mode
|
||||
*
|
||||
* @typedef {CustomEvent<DropdownItemSelectedEventDetail>} DropdownItemSelectedEvent
|
||||
*/
|
||||
|
||||
/**
|
||||
* @extends Component
|
||||
*/
|
||||
export class DropdownItem extends Component {
|
||||
/**
|
||||
* Tells the parent dropdown that an item was selected and closes the
|
||||
@@ -71,6 +58,10 @@ DropdownItem.props = {
|
||||
type: Function,
|
||||
optional: true,
|
||||
},
|
||||
class: {
|
||||
type: [String, Object],
|
||||
optional: true,
|
||||
},
|
||||
parentClosingMode: {
|
||||
type: ParentClosingMode,
|
||||
optional: true,
|
||||
@@ -83,6 +74,10 @@ DropdownItem.props = {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
slots: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
optional: true,
|
||||
@@ -95,5 +90,3 @@ DropdownItem.props = {
|
||||
DropdownItem.defaultProps = {
|
||||
parentClosingMode: ParentClosingMode.AllParents,
|
||||
};
|
||||
|
||||
QWeb.registerComponent("DropdownItem", DropdownItem);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
t-tag="props.href ? 'a' : 'span'"
|
||||
t-att-href="props.href"
|
||||
class="dropdown-item"
|
||||
t-att-class="props.class"
|
||||
t-on-click="onClick"
|
||||
t-att-title="props.title"
|
||||
t-att-data-hotkey="props.hotkey"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { useEffect, useService } from "@web/core/utils/hooks";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { browser } from "../browser/browser";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { scrollTo } from "../utils/scrolling";
|
||||
|
||||
const { useComponent, useRef } = owl;
|
||||
const { useComponent, useEffect, useRef } = owl;
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const { Component, xml } = owl;
|
||||
const { Component, xml, onWillDestroy } = owl;
|
||||
|
||||
export class EffectContainer extends Component {
|
||||
setup() {
|
||||
this.effect = null;
|
||||
this.props.bus.on("UPDATE", this, (effect) => {
|
||||
this.effect = effect;
|
||||
const listenerRef = this.props.bus.addEventListener("UPDATE", (ev) => {
|
||||
this.effect = ev.detail;
|
||||
this.render();
|
||||
});
|
||||
onWillDestroy(() => {
|
||||
this.props.bus.removeEventListener("UPDATE", listenerRef);
|
||||
});
|
||||
}
|
||||
removeEffect() {
|
||||
this.effect = null;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { registry } from "../registry";
|
||||
import { EffectContainer } from "./effect_container";
|
||||
import { RainbowMan } from "./rainbow_man";
|
||||
|
||||
const { Component, EventBus } = owl;
|
||||
const { EventBus } = owl;
|
||||
|
||||
const effectRegistry = registry.category("effects");
|
||||
|
||||
@@ -25,8 +25,6 @@ const effectRegistry = registry.category("effects");
|
||||
* Can be a string representation of html (prefer component if you want interactions in the DOM)
|
||||
* @param {string} [params.img_url="/web/static/img/smile.svg"]
|
||||
* The url of the image to display inside the rainbow
|
||||
* @param {boolean} [params.messageIsHtml=false]
|
||||
* Set to true if the message encodes html, s.t. it will be correctly inserted into the DOM.
|
||||
* @param {"slow"|"medium"|"fast"|"no"} [params.fadeout="medium"]
|
||||
* Delay for rainbowman to disappear
|
||||
* 'fast' will make rainbowman dissapear quickly
|
||||
@@ -58,7 +56,6 @@ function rainbowMan(env, params = {}) {
|
||||
imgUrl: params.img_url || "/web/static/img/smile.svg",
|
||||
fadeout: params.fadeout || "medium",
|
||||
message,
|
||||
messageIsHtml: params.messageIsHtml || false,
|
||||
Component: params.Component,
|
||||
props: params.props,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { useEffect } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, useExternalListener } = owl;
|
||||
const { Component, useEffect, useExternalListener } = owl;
|
||||
|
||||
/**
|
||||
* @typedef Common
|
||||
@@ -16,7 +15,6 @@ const { Component, useExternalListener } = owl;
|
||||
*
|
||||
* @typedef Simple
|
||||
* @property {string} message Message to be displayed on rainbowman card
|
||||
* @property {boolean} [messageIsHtml=false]
|
||||
*
|
||||
* @typedef Custom
|
||||
* @property {Component} Component
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<path d="M310,170a140,140,0,0,0-280,0" style="stroke:#80D8FF;"/>
|
||||
<path d="M330,170a160,160,0,0,0-320,0" style="stroke:#c794ba;"/>
|
||||
</svg>
|
||||
<div class="o_reward_stars" t-foreach="[1, 2, 3, 4]" t-as="star">
|
||||
<div class="o_reward_stars" t-foreach="[1, 2, 3, 4]" t-as="star" t-key="star_index">
|
||||
<t t-call="web.RainbowMan.Star">
|
||||
<t t-set="class" t-value="star"/>
|
||||
</t>
|
||||
@@ -36,8 +36,7 @@
|
||||
<div class="o_reward_msg_card">
|
||||
<div class="o_reward_msg_content">
|
||||
<t t-if="!props.Component">
|
||||
<t t-if="props.messageIsHtml" t-raw="props.message" />
|
||||
<t t-else="" t-esc="props.message" />
|
||||
<t t-out="props.message"/>
|
||||
</t>
|
||||
<t t-else="" t-component="props.Component" t-props="props.props"/>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<button class="btn btn-link" t-on-click="state.showTraceback = !state.showTraceback">See details</button>
|
||||
<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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, QWeb, useRef } = owl;
|
||||
const { Component, useRef } = owl;
|
||||
|
||||
/**
|
||||
* Custom file input
|
||||
@@ -73,6 +73,7 @@ FileInput.defaultProps = {
|
||||
accepted_file_extensions: "*",
|
||||
action: "/web/binary/upload",
|
||||
multi_upload: false,
|
||||
onUpload: () => {},
|
||||
};
|
||||
FileInput.props = {
|
||||
accepted_file_extensions: { type: String, optional: 1 },
|
||||
@@ -80,7 +81,7 @@ FileInput.props = {
|
||||
id: { type: Number, optional: 1 },
|
||||
model: { type: String, optional: 1 },
|
||||
multi_upload: { type: Boolean, optional: 1 },
|
||||
onUpload: { type: Function, optional: 1 },
|
||||
slots: { type: Object, optional: 1 },
|
||||
};
|
||||
FileInput.template = "web.FileInput";
|
||||
|
||||
QWeb.registerComponent("FileInput", FileInput);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.FileInput" owl="1">
|
||||
<span class="o_file_input" aria-atomic="true">
|
||||
<span class="o_file_input" aria-atomic="true" t-on-uploaded="(ev) => props.onUpload(ev.detail.files)">
|
||||
<span class="o_file_input_trigger" t-on-click.prevent="onTriggerClicked">
|
||||
<t t-slot="default">
|
||||
<button class="btn btn-primary">Choose File</button>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useEffect, useService } from "@web/core/utils/hooks";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { useEffect } = owl;
|
||||
|
||||
/**
|
||||
* This hook will register/unregister the given registration
|
||||
|
||||
@@ -7,14 +7,9 @@ import { localization } from "./localization";
|
||||
import { translatedTerms, _t } from "./translation";
|
||||
import { session } from "@web/session";
|
||||
|
||||
const { config } = owl;
|
||||
|
||||
export const localizationService = {
|
||||
dependencies: ["user"],
|
||||
start: async (env, { user }) => {
|
||||
// add "data-toolip" to the list of translatable attributes in owl templates
|
||||
config.translatableAttributes.push("data-tooltip");
|
||||
|
||||
const cacheHashes = session.cache_hashes || {};
|
||||
const translationsHash = cacheHashes.translations || new Date().getTime().toString();
|
||||
const lang = user.lang || null;
|
||||
@@ -42,7 +37,6 @@ export const localizationService = {
|
||||
|
||||
Object.setPrototypeOf(translatedTerms, terms);
|
||||
env._t = _t;
|
||||
env.qweb.translateFn = _t;
|
||||
|
||||
// Setup lang inside luxon. The locale codes received from the server contain "_", whereas
|
||||
// the Intl codes use "-" (Unicode BCP 47). There's only one exception, which is locale
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const { Component, onWillUnmount } = owl;
|
||||
const { Component } = owl;
|
||||
|
||||
export class Notification extends Component {
|
||||
setup() {
|
||||
if (this.props.onClose) {
|
||||
onWillUnmount(() => this.props.onClose());
|
||||
}
|
||||
}
|
||||
|
||||
shouldUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get className() {
|
||||
let className;
|
||||
switch (this.props.type) {
|
||||
@@ -48,7 +38,6 @@ Notification.props = {
|
||||
optional: true,
|
||||
validate: (t) => ["warning", "danger", "success", "info"].includes(t),
|
||||
},
|
||||
messageIsHtml: { type: Boolean, optional: true },
|
||||
className: { type: String, optional: true },
|
||||
buttons: {
|
||||
type: Array,
|
||||
@@ -61,12 +50,12 @@ Notification.props = {
|
||||
onClick: Function,
|
||||
},
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
onClose: { type: Function, optional: true },
|
||||
close: { type: Function },
|
||||
};
|
||||
Notification.defaultProps = {
|
||||
buttons: [],
|
||||
className: "",
|
||||
messageIsHtml: false,
|
||||
type: "warning",
|
||||
};
|
||||
|
||||
@@ -6,17 +6,16 @@
|
||||
role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="o_notification_body">
|
||||
<button type="button" class="close o_notification_close"
|
||||
aria-label="Close" t-on-click="trigger('close')">
|
||||
aria-label="Close" t-on-click="props.close">
|
||||
<span class="d-inline" aria-hidden="true">×</span>
|
||||
</button>
|
||||
<div t-if="props.title" class="font-weight-bold o_notification_title mb-1" t-esc="props.title"/>
|
||||
<div t-if="props.message" class="mr-auto o_notification_content">
|
||||
<t t-if="props.messageIsHtml" t-raw="props.message"/>
|
||||
<t t-else="" t-esc="props.message"/>
|
||||
<t t-out="props.message"/>
|
||||
</div>
|
||||
<div t-if="props.buttons.length" class="mt-2 o_notification_buttons">
|
||||
<button t-foreach="props.buttons" t-as="button" type="button"
|
||||
t-attf-class="btn {{button.primary ? 'btn-primary' : 'btn-link'}}" t-on-click="button.onClick()">
|
||||
<button t-foreach="props.buttons" t-as="button" type="button" t-key="button_index"
|
||||
t-attf-class="btn {{button.primary ? 'btn-primary' : 'btn-link'}}" t-on-click="button.onClick">
|
||||
<t t-if="button.icon">
|
||||
<i t-if="button.icon.indexOf('fa-') === 0" role="img"
|
||||
t-att-aria-label="button.name" t-att-title="button.name"
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Notification } from "./notification";
|
||||
import { Transition } from "@web/core/transition";
|
||||
|
||||
const { Component, xml } = owl;
|
||||
const { Component, xml, useState } = owl;
|
||||
|
||||
export class NotificationContainer extends Component {
|
||||
setup() {
|
||||
// this works, but then this component cannot be unmounted, then
|
||||
// remounted. would need a destroyed hook different from willunmount
|
||||
this.props.bus.on("UPDATE", this, this.render);
|
||||
this.notifications = useState(this.props.notifications);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationContainer.template = xml`
|
||||
<div class="o_notification_manager">
|
||||
<t t-foreach="props.notifications" t-as="notification" t-key="notification.id">
|
||||
<Notification
|
||||
t-props="notification.props"
|
||||
t-transition="o_notification_fade"
|
||||
t-on-close="notification.close()"
|
||||
/>
|
||||
<t t-foreach="notifications" t-as="notification" t-key="notification">
|
||||
<Transition leaveDuration="0" name="'o_notification_fade'" t-slot-scope="transition">
|
||||
<Notification t-props="notification_value.props" className="transition.className"/>
|
||||
</Transition>
|
||||
</t>
|
||||
</div>`;
|
||||
NotificationContainer.components = { Notification };
|
||||
NotificationContainer.components = { Notification, Transition };
|
||||
|
||||
@@ -4,7 +4,7 @@ import { browser } from "../browser/browser";
|
||||
import { registry } from "../registry";
|
||||
import { NotificationContainer } from "./notification_container";
|
||||
|
||||
const { EventBus } = owl;
|
||||
const { reactive } = owl;
|
||||
|
||||
const AUTOCLOSE_DELAY = 4000;
|
||||
|
||||
@@ -27,12 +27,11 @@ const AUTOCLOSE_DELAY = 4000;
|
||||
export const notificationService = {
|
||||
start() {
|
||||
let notifId = 0;
|
||||
let notifications = [];
|
||||
const bus = new EventBus();
|
||||
const notifications = reactive({});
|
||||
|
||||
registry.category("main_components").add("NotificationContainer", {
|
||||
Component: NotificationContainer,
|
||||
props: { bus, notifications },
|
||||
props: { notifications },
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -41,17 +40,17 @@ export const notificationService = {
|
||||
*/
|
||||
function add(message, options = {}) {
|
||||
const id = ++notifId;
|
||||
const props = Object.assign({}, options, { message });
|
||||
const closeFn = () => close(id);
|
||||
const props = Object.assign({}, options, { message, close: closeFn });
|
||||
const sticky = props.sticky;
|
||||
delete props.sticky;
|
||||
const closeFn = () => close(id);
|
||||
delete props.onClose;
|
||||
const notification = {
|
||||
id,
|
||||
props,
|
||||
close: closeFn,
|
||||
onClose: options.onClose,
|
||||
};
|
||||
notifications.push(notification);
|
||||
bus.trigger("UPDATE");
|
||||
notifications[id] = notification;
|
||||
if (!sticky) {
|
||||
browser.setTimeout(closeFn, AUTOCLOSE_DELAY);
|
||||
}
|
||||
@@ -59,10 +58,12 @@ export const notificationService = {
|
||||
}
|
||||
|
||||
function close(id) {
|
||||
const index = notifications.findIndex((n) => n.id === id);
|
||||
if (index > -1) {
|
||||
notifications.splice(index, 1);
|
||||
bus.trigger("UPDATE");
|
||||
if (notifications[id]) {
|
||||
const notification = notifications[id];
|
||||
if (notification.onClose) {
|
||||
notification.onClose();
|
||||
}
|
||||
delete notifications[id];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export class Pager extends Component {
|
||||
this.inputRef = useRef("input");
|
||||
useExternalListener(document, "mousedown", this.onClickAway, { capture: true });
|
||||
|
||||
this.forceFocus = useAutofocus();
|
||||
this.forceFocus = useAutofocus("input");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,6 +162,6 @@ Pager.props = {
|
||||
limit: Number,
|
||||
total: Number,
|
||||
onUpdate: Function,
|
||||
isEditable: Boolean,
|
||||
withAccessKey: Boolean,
|
||||
isEditable: { type: Boolean, optional: true },
|
||||
withAccessKey: { type: Boolean, optional: true },
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<nav class="o_pager" aria-label="Pager">
|
||||
<span class="o_pager_counter mr-2" t-on-click.stop="">
|
||||
<t t-if="state.isEditing">
|
||||
<input type="text" class="o_pager_value o_input" t-ref="input" autofocus="" t-att-value="value" t-on-blur="onInputBlur" t-on-change="onInputChange" t-on-keydown.stop="onInputKeydown" />
|
||||
<input type="text" class="o_pager_value o_input" t-ref="input" t-att-value="value" t-on-blur="onInputBlur" t-on-change="onInputChange" t-on-keydown.stop="onInputKeydown" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="o_pager_value" t-esc="value" t-on-click="onValueClick" />
|
||||
@@ -15,8 +15,8 @@
|
||||
</span>
|
||||
<span class="btn-group" aria-atomic="true">
|
||||
<!-- accesskeys not wanted in X2Many widgets -->
|
||||
<button type="button" class="fa fa-chevron-left btn btn-secondary o_pager_previous px-2 rounded-left" aria-label="Previous" data-tooltip="Previous" tabindex="-1" t-att-data-hotkey="props.withAccessKey ? 'p' : false" t-att-disabled="state.isDisabled or isSinglePage" t-on-click.stop="navigate(-1)" />
|
||||
<button type="button" class="fa fa-chevron-right btn btn-secondary o_pager_next px-2 rounded-right" aria-label="Next" data-tooltip="Next" tabindex="-1" t-att-data-hotkey="props.withAccessKey ? 'n' : false" t-att-disabled="state.isDisabled or isSinglePage" t-on-click.stop="navigate(1)" />
|
||||
<button type="button" class="fa fa-chevron-left btn btn-secondary o_pager_previous px-2 rounded-left" aria-label="Previous" data-tooltip="Previous" tabindex="-1" t-att-data-hotkey="props.withAccessKey ? 'p' : false" t-att-disabled="state.isDisabled or isSinglePage" t-on-click.stop="() => this.navigate(-1)" />
|
||||
<button type="button" class="fa fa-chevron-right btn btn-secondary o_pager_next px-2 rounded-right" aria-label="Next" data-tooltip="Next" tabindex="-1" t-att-data-hotkey="props.withAccessKey ? 'n' : false" t-att-disabled="state.isDisabled or isSinglePage" t-on-click.stop="() => this.navigate(1)" />
|
||||
</span>
|
||||
</nav>
|
||||
</t>
|
||||
|
||||
@@ -18,6 +18,7 @@ Popover.defaultProps = {
|
||||
position: "bottom",
|
||||
};
|
||||
Popover.props = {
|
||||
close: { type: Function },
|
||||
popoverClass: {
|
||||
optional: true,
|
||||
type: String,
|
||||
@@ -25,6 +26,14 @@ Popover.props = {
|
||||
position: {
|
||||
type: String,
|
||||
validate: (p) => ["top", "bottom", "left", "right"].includes(p),
|
||||
optional: true,
|
||||
},
|
||||
target: HTMLElement,
|
||||
slots: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
shape: {
|
||||
default: { optional: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<div role="tooltip" class="o_popover"
|
||||
t-att-class="props.popoverClass"
|
||||
t-on-popover-compute="compute"
|
||||
t-on-popover-closed="props.close"
|
||||
>
|
||||
<t t-slot="default" />
|
||||
</div>
|
||||
|
||||
@@ -2,28 +2,26 @@
|
||||
|
||||
import { Popover } from "./popover";
|
||||
|
||||
const { Component, useExternalListener, useState, xml } = owl;
|
||||
const { Component, onMounted, onWillUnmount, useExternalListener, useState, xml } = owl;
|
||||
|
||||
class PopoverController extends Component {
|
||||
setup() {
|
||||
this.state = useState({ displayed: false });
|
||||
this.targetObserver = new MutationObserver(this.onTargetMutate.bind(this));
|
||||
|
||||
useExternalListener(window, "click", this.onClickAway, { capture: true });
|
||||
onMounted(this.onMounted);
|
||||
onWillUnmount(this.onWillUnmount);
|
||||
}
|
||||
mounted() {
|
||||
onMounted() {
|
||||
this.targetObserver.observe(this.target.parentElement, { childList: true });
|
||||
}
|
||||
willUnmount() {
|
||||
onWillUnmount() {
|
||||
this.targetObserver.disconnect();
|
||||
}
|
||||
|
||||
shouldUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get popoverProps() {
|
||||
return {
|
||||
close: this.props.close,
|
||||
target: this.target,
|
||||
position: this.props.position,
|
||||
popoverClass: this.props.popoverClass,
|
||||
@@ -57,14 +55,14 @@ PopoverController.defaultProps = {
|
||||
closeOnClickAway: true,
|
||||
};
|
||||
PopoverController.template = xml/*xml*/ `
|
||||
<Popover t-props="popoverProps" t-on-popover-closed="props.close()">
|
||||
<t t-component="props.Component" t-props="props.props" />
|
||||
<Popover t-props="popoverProps">
|
||||
<t t-component="props.Component" t-props="props.props"/>
|
||||
</Popover>
|
||||
`;
|
||||
|
||||
export class PopoverContainer extends Component {
|
||||
setup() {
|
||||
this.props.bus.on("UPDATE", this, this.render);
|
||||
this.props.bus.addEventListener("UPDATE", this.render.bind(this));
|
||||
}
|
||||
}
|
||||
PopoverContainer.components = { PopoverController };
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { onWillUnmount, useComponent } = owl;
|
||||
const { onWillUnmount, status, useComponent } = owl;
|
||||
|
||||
export function usePopover() {
|
||||
const removeFns = new Set();
|
||||
@@ -33,7 +33,7 @@ export function usePopover() {
|
||||
const newOptions = Object.create(options);
|
||||
newOptions.onClose = function () {
|
||||
removeFns.delete(removeFn);
|
||||
if (options.onClose && component.__owl__.status !== 5 /* DESTROYED */) {
|
||||
if (options.onClose && status(component) !== "destroyed") {
|
||||
options.onClose();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { useEffect } from "@web/core/utils/hooks";
|
||||
import { throttleForAnimation } from "../utils/timing";
|
||||
|
||||
const { onWillUnmount, useComponent, useExternalListener, useRef } = owl;
|
||||
const { onWillUnmount, useComponent, useEffect, useExternalListener, useRef } = owl;
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
|
||||
@@ -29,7 +29,7 @@ export class Registry extends EventBus {
|
||||
this.elements = null;
|
||||
this.entries = null;
|
||||
|
||||
this.on("UPDATE", this, () => {
|
||||
this.addEventListener("UPDATE", () => {
|
||||
this.elements = null;
|
||||
this.entries = null;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/** @odoo-module */
|
||||
import { browser } from "./browser/browser";
|
||||
|
||||
const { Component, useState, useEffect, xml, onWillUpdateProps, useComponent } = owl;
|
||||
|
||||
// Allows to disable transitions globally, useful for testing (and maybe for
|
||||
// a reduced motion setting in the future?)
|
||||
export const config = {
|
||||
disabled: false,
|
||||
};
|
||||
/**
|
||||
* Creates a transition to be used within the current component. Usage:
|
||||
* --- in JS:
|
||||
* this.transition = useTransition({ name: "myClass" });
|
||||
* --- in XML:
|
||||
* <div t-if="transition.shouldMount" t-att-class="transition.class"/>
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string} options.name the prefix to use for the transition classes
|
||||
* @param {boolean} [options.onOff] whether to start the transition in the on or
|
||||
* off state
|
||||
* @param {number} [options.leaveDuration] the leaveDuration of the transition
|
||||
* @param {Function} [options.onLeave] a function that will be called when the
|
||||
* element will be removed in the next render cycle
|
||||
* @returns {{ shouldMount, class }} an object containing two fields that
|
||||
* indicate whether an element on which the transition is applied should be
|
||||
* mounted and the class string that should be put on it
|
||||
*/
|
||||
export function useTransition({
|
||||
name,
|
||||
initialVisibility = true,
|
||||
leaveDuration = 500,
|
||||
onLeave = () => {},
|
||||
}) {
|
||||
const component = useComponent();
|
||||
const state = useState({
|
||||
shouldMount: initialVisibility,
|
||||
stage: initialVisibility ? "enter" : "leave",
|
||||
});
|
||||
|
||||
if (config.disabled) {
|
||||
return {
|
||||
get shouldMount() {
|
||||
return state.shouldMount;
|
||||
},
|
||||
set shouldMount(val) {
|
||||
state.shouldMount = val;
|
||||
},
|
||||
get className() {
|
||||
return `${name} ${name}-enter-active`;
|
||||
},
|
||||
get stage() {
|
||||
return "enter-active";
|
||||
},
|
||||
};
|
||||
}
|
||||
// We need to allow the element to be mounted in the enter state so that it
|
||||
// will get the transition when we activate the enter-active class. This
|
||||
// onNextPatch allows us to activate the class that we want the next time
|
||||
// the component is patched.
|
||||
let onNextPatch = null;
|
||||
useEffect(() => {
|
||||
if (onNextPatch) {
|
||||
onNextPatch();
|
||||
onNextPatch = null;
|
||||
}
|
||||
});
|
||||
|
||||
let prevState, timer;
|
||||
const transition = {
|
||||
get shouldMount() {
|
||||
return state.shouldMount;
|
||||
},
|
||||
set shouldMount(newState) {
|
||||
if (newState === prevState) {
|
||||
return;
|
||||
}
|
||||
browser.clearTimeout(timer);
|
||||
prevState = newState;
|
||||
// when true - transition from enter to enter-active
|
||||
// when false - transition from enter-active to leave, unmount after leaveDuration
|
||||
if (newState) {
|
||||
state.stage = "enter";
|
||||
state.shouldMount = true;
|
||||
// force a render here so that we get a patch even if the state didn't change
|
||||
component.render();
|
||||
onNextPatch = () => {
|
||||
state.stage = "enter-active";
|
||||
};
|
||||
} else {
|
||||
state.stage = "leave";
|
||||
timer = browser.setTimeout(() => {
|
||||
state.shouldMount = false;
|
||||
onLeave();
|
||||
}, leaveDuration);
|
||||
}
|
||||
},
|
||||
get className() {
|
||||
return `${name} ${name}-${state.stage}`;
|
||||
},
|
||||
get stage() {
|
||||
return state.stage;
|
||||
},
|
||||
};
|
||||
transition.shouldMount = initialVisibility;
|
||||
return transition;
|
||||
}
|
||||
|
||||
/**
|
||||
* A higher order component that handles a transition to be used within its
|
||||
* default slot. Generally, the useTransition hook is simpler to use, but the
|
||||
* HOC has the advantage that it can be spawned as needed during the render (eg:
|
||||
* in a t-foreach loop) without knowing at setup-time how many transitions need
|
||||
* to be created. @see useTransition
|
||||
*/
|
||||
export class Transition extends Component {
|
||||
setup() {
|
||||
const { visible, leaveDuration, name, onLeave } = this.props;
|
||||
this.transition = useTransition({
|
||||
initialVisibility: visible,
|
||||
leaveDuration,
|
||||
name,
|
||||
onLeave,
|
||||
});
|
||||
onWillUpdateProps(({ visible = true }) => {
|
||||
this.transition.shouldMount = visible;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Transition.template = xml`<t t-slot="default" t-if="transition.shouldMount" className="transition.className"/>`;
|
||||
Transition.props = {
|
||||
name: String,
|
||||
visible: { type: Boolean, optional: true },
|
||||
leaveDuration: { type: Number, optional: true },
|
||||
onLeave: { type: Function, optional: true },
|
||||
slots: Object,
|
||||
};
|
||||
@@ -42,8 +42,8 @@ export class BlockUI extends Component {
|
||||
line2: "",
|
||||
});
|
||||
|
||||
this.props.bus.on("BLOCK", this, this.block);
|
||||
this.props.bus.on("UNBLOCK", this, this.unblock);
|
||||
this.props.bus.addEventListener("BLOCK", this.block.bind(this));
|
||||
this.props.bus.addEventListener("UNBLOCK", this.unblock.bind(this));
|
||||
}
|
||||
|
||||
replaceMessage(index) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useEffect, useService } from "@web/core/utils/hooks";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
import { BlockUI } from "./block_ui";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
const { Component, EventBus, useRef } = owl;
|
||||
const { EventBus, useComponent, useEffect, useRef } = owl;
|
||||
|
||||
export const SIZES = { XS: 0, VSM: 1, SM: 2, MD: 3, LG: 4, XL: 5, XXL: 6 };
|
||||
|
||||
@@ -23,13 +23,15 @@ export const SIZES = { XS: 0, VSM: 1, SM: 2, MD: 3, LG: 4, XL: 5, XXL: 6 };
|
||||
*/
|
||||
export function useActiveElement(refName = null) {
|
||||
const uiService = useService("ui");
|
||||
const owner = refName ? useRef(refName) : Component.current;
|
||||
const owner = refName ? useRef(refName) : useComponent();
|
||||
useEffect(
|
||||
() => {
|
||||
uiService.activateElement(owner.el);
|
||||
return () => uiService.deactivateElement(owner.el);
|
||||
(el) => {
|
||||
if (el) {
|
||||
uiService.activateElement(el);
|
||||
return () => uiService.deactivateElement(el);
|
||||
}
|
||||
},
|
||||
() => []
|
||||
() => [owner.el]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const { Component, xml } = owl;
|
||||
const { Component, onError, useComponent, xml } = owl;
|
||||
|
||||
export class NotUpdatable extends Component {
|
||||
shouldUpdate() {
|
||||
return false;
|
||||
setup() {
|
||||
const node = useComponent().__owl__;
|
||||
node.patch = () => {};
|
||||
node.updateAndRender = () => Promise.resolve();
|
||||
}
|
||||
}
|
||||
NotUpdatable.template = xml`<t t-slot="default" />`;
|
||||
|
||||
export class ErrorHandler extends Component {
|
||||
catchError(error) {
|
||||
if (this.props.onError) {
|
||||
setup() {
|
||||
onError((error) => {
|
||||
this.props.onError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
ErrorHandler.template = xml`<t t-slot="default" />`;
|
||||
ErrorEvent.props = ["onError"];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { SERVICES_METADATA } from "@web/env";
|
||||
|
||||
const { onMounted, onPatched, onWillPatch, onWillUnmount, useComponent } = owl;
|
||||
const { status, useComponent, useEffect, useRef } = owl;
|
||||
|
||||
/**
|
||||
* This file contains various custom hooks.
|
||||
@@ -24,37 +24,36 @@ const { onMounted, onPatched, onWillPatch, onWillUnmount, useComponent } = owl;
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Focus a given selector as soon as it appears in the DOM and if it was not
|
||||
* displayed before. If the selected target is an input|textarea, set the selection
|
||||
* Focus a dom element with provided ref as soon as it appears in the DOM and if it was not
|
||||
* displayed before. If it is an input|textarea, set the selection
|
||||
* at the end.
|
||||
*
|
||||
* @param {Object} [params]
|
||||
* @param {string} [params.selector='autofocus'] default: select the first element
|
||||
* with an `autofocus` attribute.
|
||||
* @param {string} [refName="autofocus"]
|
||||
* @returns {Function} function that forces the focus on the next update if visible.
|
||||
*/
|
||||
export function useAutofocus(params = {}) {
|
||||
export function useAutofocus(refName = "autofocus") {
|
||||
const comp = useComponent();
|
||||
// Prevent autofocus in mobile
|
||||
if (comp.env.isSmall) {
|
||||
return () => {};
|
||||
}
|
||||
const selector = params.selector || "[autofocus]";
|
||||
// LEGACY
|
||||
if (comp.env.device && comp.env.device.isMobileDevice) {
|
||||
return () => {};
|
||||
}
|
||||
// LEGACY
|
||||
let ref = useRef(refName);
|
||||
let forceFocusCount = 0;
|
||||
useEffect(
|
||||
function autofocus() {
|
||||
const target = comp.el.querySelector(selector);
|
||||
if (target) {
|
||||
target.focus();
|
||||
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
|
||||
const inputEl = target;
|
||||
inputEl.selectionStart = inputEl.selectionEnd = inputEl.value.length;
|
||||
(el) => {
|
||||
if (el) {
|
||||
el.focus();
|
||||
if (["INPUT", "TEXTAREA"].includes(el.tagName)) {
|
||||
el.selectionStart = el.selectionEnd = el.value.length;
|
||||
}
|
||||
}
|
||||
},
|
||||
() => [forceFocusCount]
|
||||
() => [ref.el, forceFocusCount]
|
||||
);
|
||||
|
||||
return function focusOnUpdate() {
|
||||
forceFocusCount++; // force the effect to rerun on next patch
|
||||
};
|
||||
@@ -75,62 +74,14 @@ export function useBus(bus, eventName, callback) {
|
||||
const component = useComponent();
|
||||
useEffect(
|
||||
() => {
|
||||
bus.on(eventName, component, callback);
|
||||
return () => bus.off(eventName, component);
|
||||
const listener = callback.bind(component);
|
||||
bus.addEventListener(eventName, listener);
|
||||
return () => bus.removeEventListener(eventName, listener);
|
||||
},
|
||||
() => []
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useEffect
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const NO_OP = () => {};
|
||||
/**
|
||||
* @callback Effect
|
||||
* @param {...any} dependencies the dependencies computed by computeDependencies
|
||||
* @returns {void|(()=>void)} a cleanup function that reverses the side
|
||||
* effects of the effect callback.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This hook will run a callback when a component is mounted and patched, and
|
||||
* will run a cleanup function before patching and before unmounting the
|
||||
* the component.
|
||||
*
|
||||
* @param {Effect} effect the effect to run on component mount and/or patch
|
||||
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* dependencies that will decide if the effect needs to be cleaned up and
|
||||
* run again. If the dependencies did not change, the effect will not run
|
||||
* again. The default value returns an array containing only NaN because
|
||||
* NaN !== NaN, which will cause the effect to rerun on every patch.
|
||||
*/
|
||||
export function useEffect(effect, computeDependencies = () => [NaN]) {
|
||||
let cleanup, dependencies;
|
||||
onMounted(() => {
|
||||
dependencies = computeDependencies();
|
||||
cleanup = effect(...dependencies) || NO_OP;
|
||||
});
|
||||
|
||||
let shouldReapplyOnPatch = false;
|
||||
onWillPatch(() => {
|
||||
const newDeps = computeDependencies();
|
||||
shouldReapplyOnPatch = newDeps.some((val, i) => val !== dependencies[i]);
|
||||
if (shouldReapplyOnPatch) {
|
||||
cleanup();
|
||||
dependencies = newDeps;
|
||||
}
|
||||
});
|
||||
onPatched(() => {
|
||||
if (shouldReapplyOnPatch) {
|
||||
cleanup = effect(...dependencies) || NO_OP;
|
||||
}
|
||||
});
|
||||
|
||||
onWillUnmount(() => cleanup());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useListener
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -157,7 +108,7 @@ export function useEffect(effect, computeDependencies = () => [NaN]) {
|
||||
*
|
||||
* useListener('click', 'button', () => { console.log('clicked'); });
|
||||
*
|
||||
* Note: components that alter the event's target (e.g. Portal) are not
|
||||
* Note: components that alter the event's target and the t-portal directive are not
|
||||
* expected to behave as expected with event delegation.
|
||||
*
|
||||
* @param {string} eventName the name of the event
|
||||
@@ -215,11 +166,11 @@ export function useListener(eventName, querySelector, handler, options = {}) {
|
||||
|
||||
function _protectMethod(component, caller, fn) {
|
||||
return async (...args) => {
|
||||
if (component.__owl__.status === 5 /* DESTROYED */) {
|
||||
if (status(component) === "destroyed") {
|
||||
throw new Error("Component is destroyed");
|
||||
}
|
||||
const result = await fn.call(caller, ...args);
|
||||
return component.__owl__.status === 5 ? new Promise(() => {}) : result;
|
||||
return status(component) === "destroyed" ? new Promise(() => {}) : result;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -250,17 +201,3 @@ export function useService(serviceName) {
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes "callback" when the component is being destroyed
|
||||
* @param {Function} callback
|
||||
*/
|
||||
export function onDestroyed(callback) {
|
||||
const component = useComponent();
|
||||
const _destroy = component.__destroy;
|
||||
component.__destroy = (...args) => {
|
||||
_destroy.call(component, ...args);
|
||||
// callback is called after super to guarantee the component is actually destroyed
|
||||
callback();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const { blockDom } = owl;
|
||||
|
||||
/**
|
||||
* loads a template using getTemplate, and render the template immediately;
|
||||
*
|
||||
* @param {string} template
|
||||
* @param {Object} context
|
||||
* @returns string: the html of the template
|
||||
*/
|
||||
|
||||
export function renderToString(template, context = {}) {
|
||||
const app = renderToString.app;
|
||||
if (!app) {
|
||||
throw new Error("an app must be configured before using renderToString");
|
||||
}
|
||||
const templateFn = app.getTemplate(template);
|
||||
const bdom = templateFn(context, {});
|
||||
const div = document.createElement("div");
|
||||
blockDom.mount(bdom, div);
|
||||
return div.innerHTML;
|
||||
}
|
||||
@@ -1,5 +1,24 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Escapes a string for HTML.
|
||||
* Note that it doesn't work for escaping node attributes.
|
||||
*
|
||||
* @param {string | number} [str] the string to escape
|
||||
* @returns an escaped string
|
||||
*/
|
||||
export function escape(str) {
|
||||
if (str === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof str === "number") {
|
||||
return String(str);
|
||||
}
|
||||
const p = document.createElement("p");
|
||||
p.textContent = str;
|
||||
return p.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string to use as a RegExp.
|
||||
* @url https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
|
||||
@@ -89,6 +108,7 @@ export function capitalize(s) {
|
||||
return s ? s[0].toUpperCase() + s.slice(1) : "";
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const diacriticsMap = {
|
||||
'\u0041': 'A','\u24B6': 'A','\uFF21': 'A','\u00C0': 'A','\u00C1': 'A','\u00C2': 'A','\u1EA6': 'A','\u1EA4': 'A','\u1EAA': 'A','\u1EA8': 'A',
|
||||
'\u00C3': 'A','\u0100': 'A','\u0102': 'A','\u1EB0': 'A','\u1EAE': 'A','\u1EB4': 'A','\u1EB2': 'A','\u0226': 'A','\u01E0': 'A','\u00C4': 'A',
|
||||
|
||||
@@ -9,16 +9,11 @@
|
||||
xml: <tag t-transition="o_notification_fade" />
|
||||
*/
|
||||
@mixin owl-fade($time, $name) {
|
||||
.#{$name}_fade-enter-active,
|
||||
.#{$name}_fade-active {
|
||||
.#{$name}_fade {
|
||||
transition: all $time;
|
||||
}
|
||||
|
||||
.#{$name}_fade-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.#{$name}_fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { registry } from "./core/registry";
|
||||
|
||||
const { EventBus, QWeb } = owl;
|
||||
const { EventBus } = owl;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -29,7 +29,6 @@ const { EventBus, QWeb } = owl;
|
||||
*/
|
||||
export function makeEnv() {
|
||||
return {
|
||||
qweb: new QWeb(),
|
||||
bus: new EventBus(),
|
||||
services: {},
|
||||
debug: odoo.debug,
|
||||
@@ -59,12 +58,12 @@ export const SERVICES_METADATA = {};
|
||||
*/
|
||||
export async function startServices(env) {
|
||||
const toStart = new Set();
|
||||
serviceRegistry.on("UPDATE", null, async (payload) => {
|
||||
serviceRegistry.addEventListener("UPDATE", async (ev) => {
|
||||
// Wait for all synchronous code so that if new services that depend on
|
||||
// one another are added to the registry, they're all present before we
|
||||
// start them regardless of the order they're added to the registry.
|
||||
await Promise.resolve();
|
||||
const { operation, key: name, value: service } = payload;
|
||||
const { operation, key: name, value: service } = ev.detail;
|
||||
if (operation === "delete") {
|
||||
// We hardly see why it would be usefull to remove a service.
|
||||
// Furthermore we could encounter problems with dependencies.
|
||||
|
||||
@@ -3,9 +3,8 @@ import { registry } from "@web/core/registry";
|
||||
import legacyFieldRegistry from "web.field_registry";
|
||||
import owlFieldRegistry from "web.field_registry_owl";
|
||||
import { ComponentAdapter } from "web.OwlCompatibility";
|
||||
import { useEffect } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, useRef, xml } = owl;
|
||||
const { Component, useEffect, xml } = owl;
|
||||
|
||||
const fieldRegistry = registry.category("fields");
|
||||
|
||||
@@ -36,13 +35,12 @@ class Field extends Component {
|
||||
|
||||
setup() {
|
||||
const { record, type, name } = this.props;
|
||||
this.fieldRef = useRef("fieldRef");
|
||||
this.FieldComponent = Field.getTangibleField({ record, type, name }).FieldClass;
|
||||
}
|
||||
}
|
||||
|
||||
Field.template = xml/* xml */ `
|
||||
<t t-component="FieldComponent" t-props="props" class="o-field" t-key="props.record.id" t-ref="fieldRef"/>
|
||||
<t t-component="FieldComponent" t-props="props" class="o-field" t-key="props.record.id"/>
|
||||
`;
|
||||
|
||||
class FieldSupportsLegacy extends Field {
|
||||
@@ -107,7 +105,7 @@ class FieldSupportsLegacy extends Field {
|
||||
FieldSupportsLegacy.template = xml/* xml */ `<t>
|
||||
<t t-if="!isOwlLegacy and !isLegacy" t-call="${Field.template}" />
|
||||
<t t-elif="isOwlLegacy" t-component="FieldComponent" t-props="legacyProps" />
|
||||
<t t-elif="isLegacy" t-component="ComponentAdapter" widgetArgs="widgetArgs" Component="FieldComponent" t-key="renderId" />
|
||||
<ComponentAdapter t-elif="isLegacy" widgetArgs="widgetArgs" Component="FieldComponent" t-key="renderId" />
|
||||
</t>
|
||||
`;
|
||||
FieldSupportsLegacy.components = { ComponentAdapter };
|
||||
|
||||
@@ -6,12 +6,23 @@ import { ComponentAdapter } from "web.OwlCompatibility";
|
||||
import { objectToQuery } from "../core/browser/router_service";
|
||||
import { useDebugCategory } from "../core/debug/debug_context";
|
||||
import { Dialog } from "../core/dialog/dialog";
|
||||
import { useEffect, useService } from "@web/core/utils/hooks";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { ViewNotFoundError } from "../webclient/actions/action_service";
|
||||
import { cleanDomFromBootstrap, wrapSuccessOrFail } from "./utils";
|
||||
import { cleanDomFromBootstrap, wrapSuccessOrFail, useLegacyRefs } from "./utils";
|
||||
import { mapDoActionOptionAPI } from "./backend_utils";
|
||||
import { setScrollPosition } from "@web/core/utils/scrolling";
|
||||
|
||||
const { Component, onMounted, useExternalListener, xml } = owl;
|
||||
const {
|
||||
Component,
|
||||
onMounted,
|
||||
onWillUnmount,
|
||||
onWillUpdateProps,
|
||||
status,
|
||||
useEffect,
|
||||
useExternalListener,
|
||||
useComponent,
|
||||
xml,
|
||||
} = owl;
|
||||
|
||||
const warningDialogBodyTemplate = xml`<t t-esc="props.message"/>`;
|
||||
|
||||
@@ -24,6 +35,8 @@ class ActionAdapter extends ComponentAdapter {
|
||||
this.notifications = useService("notification");
|
||||
this.dialogs = useService("dialog");
|
||||
this.wowlEnv = this.env;
|
||||
const legacyRefs = useLegacyRefs();
|
||||
legacyRefs.component = this;
|
||||
// a legacy widget widget can push_state anytime including during its async rendering
|
||||
// In Wowl, we want to have all states pushed during the same setTimeout.
|
||||
// This is protected in legacy (backward compatibility) but should not e supported in Wowl
|
||||
@@ -31,9 +44,7 @@ class ActionAdapter extends ComponentAdapter {
|
||||
let originalUpdateControlPanel;
|
||||
useEffect(
|
||||
() => {
|
||||
if (this.wowlEnv.setLegacyControllerWidget) {
|
||||
this.wowlEnv.setLegacyControllerWidget(this.widget);
|
||||
}
|
||||
legacyRefs.widget = this.widget;
|
||||
const query = this.widget.getState();
|
||||
Object.assign(query, this.tempQuery);
|
||||
this.tempQuery = null;
|
||||
@@ -41,10 +52,11 @@ class ActionAdapter extends ComponentAdapter {
|
||||
if (!this.wowlEnv.inDialog) {
|
||||
this.pushState(query);
|
||||
}
|
||||
this.wowlEnv.bus.on("ACTION_MANAGER:UPDATE", this, () => {
|
||||
const onActionManagerUpdate = () => {
|
||||
this.env.bus.trigger("close_dialogs");
|
||||
cleanDomFromBootstrap();
|
||||
});
|
||||
};
|
||||
this.wowlEnv.bus.addEventListener("ACTION_MANAGER:UPDATE", onActionManagerUpdate);
|
||||
originalUpdateControlPanel = this.__widget.updateControlPanel.bind(this.__widget);
|
||||
this.__widget.updateControlPanel = (newProps) => {
|
||||
this.wowlEnv.config.setDisplayName(this.__widget.getTitle());
|
||||
@@ -54,7 +66,10 @@ class ActionAdapter extends ComponentAdapter {
|
||||
|
||||
return () => {
|
||||
this.__widget.updateControlPanel = originalUpdateControlPanel;
|
||||
this.wowlEnv.bus.off("ACTION_MANAGER:UPDATE", this);
|
||||
this.wowlEnv.bus.removeEventListener(
|
||||
"ACTION_MANAGER:UPDATE",
|
||||
onActionManagerUpdate
|
||||
);
|
||||
};
|
||||
},
|
||||
() => []
|
||||
@@ -62,6 +77,21 @@ class ActionAdapter extends ComponentAdapter {
|
||||
useExternalListener(window, "click", () => {
|
||||
cleanDomFromBootstrap();
|
||||
});
|
||||
|
||||
onWillUpdateProps(() => {
|
||||
if (this.widget === null) {
|
||||
this.widget = this.__widget;
|
||||
}
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
if (this.__widget && this.__widget.on_detach_callback) {
|
||||
this.__widget.on_detach_callback();
|
||||
}
|
||||
});
|
||||
|
||||
this.onScrollTo = (payload) => {
|
||||
setScrollPosition(this, { left: payload.left, top: payload.top });
|
||||
};
|
||||
}
|
||||
|
||||
get actionId() {
|
||||
@@ -125,6 +155,8 @@ class ActionAdapter extends ComponentAdapter {
|
||||
}
|
||||
} else if (ev.name === "history_back") {
|
||||
this.wowlEnv.config.historyBack();
|
||||
} else if (ev.name === "scrollTo") {
|
||||
this.onScrollTo(payload);
|
||||
} else {
|
||||
super._trigger_up(ev);
|
||||
}
|
||||
@@ -152,21 +184,15 @@ class ActionAdapter extends ComponentAdapter {
|
||||
return this.__widget.canBeRemoved();
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
willUnmount() {
|
||||
if (this.__widget && this.__widget.on_detach_callback) {
|
||||
this.__widget.on_detach_callback();
|
||||
}
|
||||
super.willUnmount();
|
||||
}
|
||||
__destroy() {
|
||||
if (this.actionService.__legacy__isActionInStack(this.actionId)) {
|
||||
this.widget = null;
|
||||
}
|
||||
super.__destroy(...arguments);
|
||||
}
|
||||
get el() {
|
||||
return (this.widget || this.__widget).el;
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientActionAdapter extends ActionAdapter {
|
||||
@@ -192,7 +218,7 @@ export class ClientActionAdapter extends ActionAdapter {
|
||||
return this.props.widgetArgs[0].jsId;
|
||||
}
|
||||
|
||||
async willStart() {
|
||||
async onWillStart() {
|
||||
if (this.props.widget) {
|
||||
this.widget = this.props.widget;
|
||||
this.widget.setParent(this);
|
||||
@@ -201,7 +227,7 @@ export class ClientActionAdapter extends ActionAdapter {
|
||||
}
|
||||
return this.updateWidget();
|
||||
}
|
||||
return super.willStart();
|
||||
return super.onWillStart();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,7 +245,7 @@ export class ClientActionAdapter extends ActionAdapter {
|
||||
const magicReloadSymbol = Symbol("magicReload");
|
||||
|
||||
function useMagicLegacyReload() {
|
||||
const comp = Component.current;
|
||||
const comp = useComponent();
|
||||
if (comp.props.widget && comp.props.widget[magicReloadSymbol]) {
|
||||
return comp.props.widget[magicReloadSymbol];
|
||||
}
|
||||
@@ -283,7 +309,7 @@ export class ViewAdapter extends ActionAdapter {
|
||||
return this.props.viewParams.action.jsId;
|
||||
}
|
||||
|
||||
async willStart() {
|
||||
async onWillStart() {
|
||||
if (this.props.widget) {
|
||||
this.widget = this.props.widget;
|
||||
this.widget.setParent(this);
|
||||
@@ -294,7 +320,7 @@ export class ViewAdapter extends ActionAdapter {
|
||||
} else {
|
||||
const view = new this.props.View(this.props.viewInfo, this.props.viewParams);
|
||||
this.widget = await view.getController(this);
|
||||
if (this.__owl__.status === 5 /* DESTROYED */) {
|
||||
if (status(this) === "destroyed") {
|
||||
// the component might have been destroyed meanwhile, but if so, `this.widget` wasn't
|
||||
// destroyed by OwlCompatibility layer as it wasn't set yet, so destroy it now
|
||||
if (!this.actionService.__legacy__isActionInStack(this.actionId)) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { formatDateTime, parseDateTime } from "@web/core/l10n/dates";
|
||||
import { formatMany2one } from "@web/fields/formatters";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const { useState } = owl;
|
||||
const { onWillStart, useState } = owl;
|
||||
|
||||
const debugRegistry = registry.category("debug");
|
||||
|
||||
@@ -14,9 +14,10 @@ class GetMetadataDialog extends Dialog {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({});
|
||||
onWillStart(this.onWillStart);
|
||||
}
|
||||
|
||||
async willStart() {
|
||||
async onWillStart() {
|
||||
await this.getMetadata();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ var FieldBoolean = AbstractField.extend({
|
||||
change: '_onChange',
|
||||
}),
|
||||
supportedFieldTypes: ['boolean'],
|
||||
isQuickEditable: true,
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
|
||||
@@ -25,7 +25,6 @@ odoo.define("web.commonEnv", function (require) {
|
||||
const { _t } = require("web.translation");
|
||||
const utils = require("web.utils");
|
||||
|
||||
const { QWeb } = owl;
|
||||
const browser = {
|
||||
clearInterval: window.clearInterval.bind(window),
|
||||
clearTimeout: window.clearTimeout.bind(window),
|
||||
@@ -50,7 +49,6 @@ odoo.define("web.commonEnv", function (require) {
|
||||
bus,
|
||||
device,
|
||||
isDebug,
|
||||
qweb: new QWeb({ translateFn: _t }),
|
||||
services: {
|
||||
ajaxJsonRPC() {
|
||||
return jsonRpc(...arguments);
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
return new Promise((resolve, reject) => {
|
||||
return this.env.services.rpc(...arguments)
|
||||
.then(result => {
|
||||
if (this.__owl__.status !== 5 /* not destroyed */) {
|
||||
if (owl.status(this) !== "destroyed") {
|
||||
resolve(result);
|
||||
}
|
||||
})
|
||||
.catch(reason => {
|
||||
if (this.__owl__.status !== 5) /* not destroyed */ {
|
||||
if (owl.status(this) !== "destroyed") {
|
||||
reject(reason);
|
||||
}
|
||||
});
|
||||
@@ -35,11 +35,11 @@
|
||||
* a widgetSymbol key in the environment, corresponding to the hook to call
|
||||
* (see ComponentWrapper).
|
||||
*/
|
||||
const originalTrigger = Component.prototype.__trigger;
|
||||
Component.prototype.__trigger = function (component, evType, payload) {
|
||||
const originalTrigger = owl.Component.prototype.__trigger;
|
||||
Component.prototype.__trigger = function (evType, payload) {
|
||||
if (this.env[odoo.widgetSymbol]) {
|
||||
this.env[odoo.widgetSymbol](evType);
|
||||
}
|
||||
originalTrigger.call(this, component, evType, payload);
|
||||
originalTrigger.call(this, evType, payload);
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -5,7 +5,13 @@ odoo.define('web.ActionMenus', function (require) {
|
||||
const DropdownMenu = require('web.DropdownMenu');
|
||||
const Registry = require('web.Registry');
|
||||
|
||||
const { Component } = owl;
|
||||
const {
|
||||
Component,
|
||||
onMounted,
|
||||
onPatched,
|
||||
onWillStart,
|
||||
onWillUpdateProps,
|
||||
} = owl;
|
||||
|
||||
let registryActionId = 1;
|
||||
|
||||
@@ -22,23 +28,21 @@ odoo.define('web.ActionMenus', function (require) {
|
||||
* @extends Component
|
||||
*/
|
||||
class ActionMenus extends Component {
|
||||
|
||||
async willStart() {
|
||||
this.actionItems = await this._setActionItems(this.props);
|
||||
this.printItems = await this._setPrintItems(this.props);
|
||||
}
|
||||
|
||||
async willUpdateProps(nextProps) {
|
||||
this.actionItems = await this._setActionItems(nextProps);
|
||||
this.printItems = await this._setPrintItems(nextProps);
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this._addTooltips();
|
||||
}
|
||||
|
||||
patched() {
|
||||
this._addTooltips();
|
||||
setup() {
|
||||
onWillStart(async () => {
|
||||
this.actionItems = await this._setActionItems(this.props);
|
||||
this.printItems = await this._setPrintItems(this.props);
|
||||
});
|
||||
onWillUpdateProps(async (nextProps) => {
|
||||
this.actionItems = await this._setActionItems(nextProps);
|
||||
this.printItems = await this._setPrintItems(nextProps);
|
||||
});
|
||||
onMounted(() => {
|
||||
this._addTooltips();
|
||||
});
|
||||
onPatched(() => {
|
||||
this._addTooltips();
|
||||
});
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
@@ -14,7 +14,7 @@ odoo.define('web.CustomCheckbox', function (require) {
|
||||
* value="boolean"
|
||||
* disabled="boolean"
|
||||
* text="'Change the label text'"
|
||||
* t-on-change="_onValueChange"
|
||||
* onChange="_onValueChange"
|
||||
* />
|
||||
*
|
||||
* @extends Component
|
||||
@@ -27,21 +27,30 @@ odoo.define('web.CustomCheckbox', function (require) {
|
||||
* @param {boolean} [props.disabled=false]
|
||||
* @param {string} [props.text]
|
||||
*/
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
setup() {
|
||||
this._id = `checkbox-comp-${utils.generateID()}`;
|
||||
}
|
||||
}
|
||||
|
||||
CustomCheckbox.defaultProps = {
|
||||
onChange: () => {},
|
||||
};
|
||||
CustomCheckbox.props = {
|
||||
id: {
|
||||
type: [String, Number],
|
||||
optional: 1,
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
optional: 1,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
optional: 1,
|
||||
},
|
||||
onChange: {
|
||||
type: Function,
|
||||
optional: 1,
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
optional: 1,
|
||||
|
||||
@@ -23,9 +23,7 @@ odoo.define('web.CustomFileInput', function (require) {
|
||||
* @param {string} [props.multi_upload=false] Whether the input should allow
|
||||
* to upload multiple files at once.
|
||||
*/
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
setup() {
|
||||
this.fileInputRef = useRef('file-input');
|
||||
}
|
||||
|
||||
@@ -73,13 +71,16 @@ odoo.define('web.CustomFileInput', function (require) {
|
||||
accepted_file_extensions: '*',
|
||||
action: '/web/binary/upload',
|
||||
multi_upload: false,
|
||||
onUpload: () => {},
|
||||
};
|
||||
CustomFileInput.props = {
|
||||
accepted_file_extensions: { type: String, optional: 1 },
|
||||
action: { type: String, optional: 1 },
|
||||
id: { type: Number, optional: 1 },
|
||||
model: { type: String, optional: 1 },
|
||||
multi_upload: { type: Boolean, optional: 1 },
|
||||
accepted_file_extensions: { type: String, optional: true },
|
||||
action: { type: String, optional: true },
|
||||
id: { type: Number, optional: true },
|
||||
model: { type: String, optional: true },
|
||||
multi_upload: { type: Boolean, optional: true },
|
||||
onUpload: { type: Function, optional: true },
|
||||
slots: { type: Object, optional: true },
|
||||
};
|
||||
CustomFileInput.template = 'web.CustomFileInput';
|
||||
|
||||
|
||||
@@ -3,9 +3,18 @@ odoo.define('web.DatePickerOwl', function (require) {
|
||||
|
||||
const field_utils = require('web.field_utils');
|
||||
const time = require('web.time');
|
||||
const { useAutofocus } = require('web.custom_hooks');
|
||||
const { useAutofocus } = require("@web/core/utils/hooks");
|
||||
|
||||
const { Component, useExternalListener, useRef, useState } = owl;
|
||||
const {
|
||||
Component,
|
||||
onMounted,
|
||||
onPatched,
|
||||
onWillUnmount,
|
||||
onWillUpdateProps,
|
||||
useExternalListener,
|
||||
useRef,
|
||||
useState,
|
||||
} = owl;
|
||||
|
||||
let datePickerId = 0;
|
||||
|
||||
@@ -23,44 +32,52 @@ odoo.define('web.DatePickerOwl', function (require) {
|
||||
* @extends Component
|
||||
*/
|
||||
class DatePicker extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.inputRef = useRef('input');
|
||||
setup() {
|
||||
this.state = useState({ warning: false });
|
||||
|
||||
this.datePickerId = `o_datepicker_${datePickerId++}`;
|
||||
this.typeOfDate = 'date';
|
||||
|
||||
useAutofocus();
|
||||
useAutofocus("input");
|
||||
useExternalListener(window, 'scroll', this._onWindowScroll);
|
||||
}
|
||||
|
||||
mounted() {
|
||||
$(this.el).on('show.datetimepicker', this._onDateTimePickerShow.bind(this));
|
||||
$(this.el).on('hide.datetimepicker', this._onDateTimePickerHide.bind(this));
|
||||
$(this.el).on('error.datetimepicker', () => false);
|
||||
this.inputRef = useRef('input');
|
||||
|
||||
const pickerOptions = Object.assign({ format: this.defaultFormat }, this.props);
|
||||
this._datetimepicker(pickerOptions);
|
||||
this.inputRef.el.value = this._formatDate(this.props.date);
|
||||
}
|
||||
onMounted(() => {
|
||||
$(this.el).on('show.datetimepicker', this._onDateTimePickerShow.bind(this));
|
||||
$(this.el).on('hide.datetimepicker', this._onDateTimePickerHide.bind(this));
|
||||
$(this.el).on('error.datetimepicker', () => false);
|
||||
|
||||
willUnmount() {
|
||||
$(this.el).off('show.datetimepicker hide.datetimepicker error.datetimepicker');
|
||||
this._datetimepicker('destroy');
|
||||
}
|
||||
const pickerOptions = Object.assign({ format: this.defaultFormat }, this.props);
|
||||
this._datetimepicker(pickerOptions);
|
||||
this.inputRef.el.value = this._formatDate(this.props.date);
|
||||
});
|
||||
|
||||
willUpdateProps(nextProps) {
|
||||
for (const prop in nextProps) {
|
||||
if (prop == "onDateTimeChanged") {
|
||||
continue;
|
||||
onWillUnmount(() => {
|
||||
$(this.el).off('show.datetimepicker hide.datetimepicker error.datetimepicker');
|
||||
this._datetimepicker('destroy');
|
||||
});
|
||||
|
||||
let newDateReceived = false;
|
||||
|
||||
onWillUpdateProps((nextProps) => {
|
||||
for (const prop in nextProps) {
|
||||
if (prop == "onDateTimeChanged") {
|
||||
continue;
|
||||
}
|
||||
this._datetimepicker(prop, nextProps[prop]);
|
||||
}
|
||||
this._datetimepicker(prop, nextProps[prop]);
|
||||
}
|
||||
if (nextProps.date) {
|
||||
this.inputRef.el.value = this._formatDate(nextProps.date);
|
||||
}
|
||||
if (nextProps.date) {
|
||||
newDateReceived = true;
|
||||
}
|
||||
});
|
||||
|
||||
onPatched(() => {
|
||||
if (newDateReceived) {
|
||||
newDateReceived = false;
|
||||
this.inputRef.el.value = this._formatDate(this.props.date);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
@@ -210,7 +227,7 @@ odoo.define('web.DatePickerOwl', function (require) {
|
||||
},
|
||||
optional: 1,
|
||||
},
|
||||
calendarWeeks: Boolean,
|
||||
calendarWeeks: { type: Boolean, optional: true },
|
||||
format: { type: String, optional: 1 },
|
||||
icons: {
|
||||
type: Object,
|
||||
@@ -225,14 +242,15 @@ odoo.define('web.DatePickerOwl', function (require) {
|
||||
today: String,
|
||||
up: String,
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
keyBinds: { validate: kb => typeof kb === 'object' || kb === null, optional: 1 },
|
||||
locale: String,
|
||||
maxDate: moment,
|
||||
minDate: moment,
|
||||
locale: { type: String, optional: true },
|
||||
maxDate: { type: moment, optional: true },
|
||||
minDate: { type: moment, optional: true },
|
||||
readonly: { type: Boolean, optional: 1 },
|
||||
useCurrent: Boolean,
|
||||
widgetParent: String,
|
||||
useCurrent: { type: Boolean, optional: true },
|
||||
widgetParent: { type: String, optional: true },
|
||||
};
|
||||
DatePicker.template = "web.Legacy.DatePicker";
|
||||
|
||||
@@ -247,9 +265,8 @@ odoo.define('web.DatePickerOwl', function (require) {
|
||||
* @extends DatePicker
|
||||
*/
|
||||
class DateTimePicker extends DatePicker {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.typeOfDate = 'datetime';
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +28,7 @@ odoo.define('web.DropdownMenu', function (require) {
|
||||
* @extends Component
|
||||
*/
|
||||
class DropdownMenu extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
setup() {
|
||||
this.dropdownMenu = useRef('dropdown');
|
||||
this.state = useState({ open: false });
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
odoo.define('web.DropdownMenuItem', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
|
||||
const { Component, useExternalListener, useRef, useState } = owl;
|
||||
|
||||
@@ -27,9 +27,7 @@ odoo.define('web.DropdownMenuItem', function (require) {
|
||||
* @extends Component
|
||||
*/
|
||||
class DropdownMenuItem extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
setup() {
|
||||
this.canBeOpened = Boolean(this.props.options && this.props.options.length);
|
||||
|
||||
this.fallbackFocusRef = useRef('fallback-focus');
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
odoo.define('web.Pager', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { useAutofocus } = require('web.custom_hooks');
|
||||
const { useAutofocus } = require("@web/core/utils/hooks");
|
||||
|
||||
const { Component, useState } = owl;
|
||||
const { Component, onWillUpdateProps, useState } = owl;
|
||||
|
||||
/**
|
||||
* Pager
|
||||
@@ -30,18 +30,18 @@ odoo.define('web.Pager', function (require) {
|
||||
* @param {boolean} [props.withAccessKey] can be disabled, for example,
|
||||
* for x2m widgets
|
||||
*/
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
disabled: false,
|
||||
editing: false,
|
||||
});
|
||||
|
||||
useAutofocus();
|
||||
|
||||
onWillUpdateProps(this.onWillUpdateProps);
|
||||
}
|
||||
|
||||
async willUpdateProps() {
|
||||
async onWillUpdateProps() {
|
||||
this.state.editing = false;
|
||||
this.state.disabled = false;
|
||||
}
|
||||
@@ -154,7 +154,7 @@ odoo.define('web.Pager', function (require) {
|
||||
// have to disable the edition manually here.
|
||||
this.state.editing = false;
|
||||
}
|
||||
this.trigger('pager-changed', { currentMinimum, limit });
|
||||
this.props.onPagerChanged({ currentMinimum, limit });
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
@@ -212,11 +212,12 @@ odoo.define('web.Pager', function (require) {
|
||||
};
|
||||
Pager.props = {
|
||||
currentMinimum: { type: Number, optional: 1 },
|
||||
editable: Boolean,
|
||||
editable: { type: Boolean, optional: true },
|
||||
limit: { validate: l => !isNaN(l), optional: 1 },
|
||||
size: { type: Number, optional: 1 },
|
||||
validate: Function,
|
||||
withAccessKey: Boolean,
|
||||
validate: { type: Function, optional: true },
|
||||
withAccessKey: { type: Boolean, optional: true },
|
||||
onPagerChanged: Function,
|
||||
};
|
||||
Pager.template = 'web.legacy.Pager';
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
odoo.define("web.ComparisonMenu", function (require) {
|
||||
"use strict";
|
||||
|
||||
const { Dropdown } = require("@web/core/dropdown/dropdown");
|
||||
const { DropdownItem } = require("@web/core/dropdown/dropdown_item");
|
||||
const { FACET_ICONS } = require("web.searchUtils");
|
||||
const { useModel } = require("web.Model");
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
class ComparisonMenu extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
setup() {
|
||||
this.icon = FACET_ICONS.comparison;
|
||||
this.model = useModel('searchModel');
|
||||
}
|
||||
@@ -29,6 +30,7 @@ odoo.define("web.ComparisonMenu", function (require) {
|
||||
}
|
||||
}
|
||||
ComparisonMenu.template = "web.ComparisonMenu";
|
||||
ComparisonMenu.components = { Dropdown, DropdownItem };
|
||||
|
||||
return ComparisonMenu;
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ odoo.define('web.ControlPanel', function (require) {
|
||||
const SearchBar = require('web.SearchBar');
|
||||
const { useModel } = require('web.Model');
|
||||
|
||||
const { Component, useRef, useSubEnv } = owl;
|
||||
const { Component, onMounted, onPatched, onWillDestroy, onWillUpdateProps, useRef, useSubEnv } = owl;
|
||||
|
||||
/**
|
||||
* TODO: remove this whole mechanism as soon as `cp_content` is completely removed.
|
||||
@@ -96,15 +96,16 @@ odoo.define('web.ControlPanel', function (require) {
|
||||
* @extends Component
|
||||
*/
|
||||
class ControlPanel extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
setup() {
|
||||
this.additionalContent = getAdditionalContent(this.props);
|
||||
|
||||
let subEnvView = this.props.view;
|
||||
useSubEnv({
|
||||
action: this.props.action,
|
||||
searchModel: this.props.searchModel,
|
||||
view: this.props.view,
|
||||
get view() {
|
||||
return subEnvView;
|
||||
},
|
||||
});
|
||||
|
||||
// Connect to the model
|
||||
@@ -124,27 +125,40 @@ odoo.define('web.ControlPanel', function (require) {
|
||||
this.fields = this._formatFields(this.props.fields);
|
||||
|
||||
this.sprintf = _.str.sprintf;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this._attachAdditionalContent();
|
||||
}
|
||||
onWillDestroy(() => {
|
||||
const content = this.props.cp_content;
|
||||
if (content) {
|
||||
if (content.$buttons) {
|
||||
content.$buttons.remove();
|
||||
}
|
||||
if (content.$searchview) {
|
||||
content.$searchview.remove();
|
||||
}
|
||||
if (content.$pager) {
|
||||
content.$pager.remove();
|
||||
}
|
||||
if (content.$searchview_buttons) {
|
||||
content.$searchview_buttons.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
patched() {
|
||||
this._attachAdditionalContent();
|
||||
}
|
||||
|
||||
async willUpdateProps(nextProps) {
|
||||
// Note: action and searchModel are not likely to change during
|
||||
// the lifespan of a ControlPanel instance, so we only need to update
|
||||
// the view information.
|
||||
if ('view' in nextProps) {
|
||||
this.env.view = nextProps.view;
|
||||
}
|
||||
if ('fields' in nextProps) {
|
||||
this.fields = this._formatFields(nextProps.fields);
|
||||
}
|
||||
this.additionalContent = getAdditionalContent(nextProps);
|
||||
// Cannot use useEffect. See prepareForFinish in owl_compatibility.js
|
||||
onMounted(() => this._attachAdditionalContent());
|
||||
onPatched(() => this._attachAdditionalContent());
|
||||
onWillUpdateProps((nextProps) => {
|
||||
// Note: action and searchModel are not likely to change during
|
||||
// the lifespan of a ControlPanel instance, so we only need to update
|
||||
// the view information.
|
||||
if ("view" in nextProps) {
|
||||
subEnvView = nextProps.view;
|
||||
}
|
||||
if ("fields" in nextProps) {
|
||||
this.fields = this._formatFields(nextProps.fields);
|
||||
}
|
||||
this.additionalContent = getAdditionalContent(nextProps);
|
||||
});
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
@@ -202,18 +216,18 @@ odoo.define('web.ControlPanel', function (require) {
|
||||
};
|
||||
ControlPanel.props = {
|
||||
action: Object,
|
||||
breadcrumbs: Array,
|
||||
breadcrumbs: { type: Array, optional: true },
|
||||
searchModel: ActionModel,
|
||||
cp_content: { type: Object, optional: 1 },
|
||||
fields: Object,
|
||||
fields: { type: Object, optional: true },
|
||||
pager: { validate: p => typeof p === 'object' || p === null, optional: 1 },
|
||||
searchMenuTypes: Array,
|
||||
searchMenuTypes: { type: Array, optional: true },
|
||||
actionMenus: { validate: s => typeof s === 'object' || s === null, optional: 1 },
|
||||
title: { type: String, optional: 1 },
|
||||
view: { type: Object, optional: 1 },
|
||||
views: Array,
|
||||
withBreadcrumbs: Boolean,
|
||||
withSearchBar: Boolean,
|
||||
views: { type: Array, optional: true },
|
||||
withBreadcrumbs: { type: Boolean, optional: true },
|
||||
withSearchBar: { type: Boolean, optional: true },
|
||||
};
|
||||
ControlPanel.template = 'web.Legacy.ControlPanel';
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
odoo.define('web.CustomFavoriteItem', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { CheckBox } = require("@web/core/checkbox/checkbox");
|
||||
const { Dropdown } = require("@web/core/dropdown/dropdown");
|
||||
const FavoriteMenu = require('web.FavoriteMenu');
|
||||
const { useAutofocus } = require('web.custom_hooks');
|
||||
const { useAutofocus } = require("@web/core/utils/hooks");
|
||||
const { useModel } = require('web.Model');
|
||||
|
||||
const { Component, useRef, useState } = owl;
|
||||
@@ -29,9 +31,7 @@ odoo.define('web.CustomFavoriteItem', function (require) {
|
||||
* and save the context to a new filter.
|
||||
*/
|
||||
class CustomFavoriteItem extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
setup() {
|
||||
const favId = favoriteId++;
|
||||
this.useByDefaultId = `o_favorite_use_by_default_${favId}`;
|
||||
this.shareAllUsersId = `o_favorite_share_all_users_${favId}`;
|
||||
@@ -45,7 +45,7 @@ odoo.define('web.CustomFavoriteItem', function (require) {
|
||||
isShared: false,
|
||||
});
|
||||
|
||||
useAutofocus();
|
||||
useAutofocus("description");
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
@@ -92,10 +92,9 @@ odoo.define('web.CustomFavoriteItem', function (require) {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev change Event
|
||||
* @param {boolean} checked
|
||||
*/
|
||||
onDefaultCheckboxChange(ev) {
|
||||
const { checked } = ev.target;
|
||||
onDefaultCheckboxChange(checked) {
|
||||
this.state.isDefault = checked;
|
||||
if (checked) {
|
||||
this.state.isShared = false;
|
||||
@@ -104,10 +103,9 @@ odoo.define('web.CustomFavoriteItem', function (require) {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev change Event
|
||||
* @param {boolean} checked
|
||||
*/
|
||||
onShareCheckboxChange(ev) {
|
||||
const { checked } = ev.target;
|
||||
onShareCheckboxChange(checked) {
|
||||
this.state.isShared = checked;
|
||||
if (checked) {
|
||||
this.state.isDefault = false;
|
||||
@@ -147,6 +145,7 @@ odoo.define('web.CustomFavoriteItem', function (require) {
|
||||
|
||||
CustomFavoriteItem.props = {};
|
||||
CustomFavoriteItem.template = "web.CustomFavoriteItem";
|
||||
CustomFavoriteItem.components = { CheckBox, Dropdown };
|
||||
CustomFavoriteItem.groupNumber = 3; // have 'Save Current Search' in its own group
|
||||
|
||||
FavoriteMenu.registry.add('favorite-generator-menu', CustomFavoriteItem, 0);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
odoo.define('web.CustomFilterItem', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { Dropdown } = require("@web/core/dropdown/dropdown");
|
||||
const { DropdownItem } = require("@web/core/dropdown/dropdown_item");
|
||||
const { DatePicker, DateTimePicker } = require('web.DatePickerOwl');
|
||||
const Domain = require('web.Domain');
|
||||
const { FIELD_OPERATORS, FIELD_TYPES } = require('web.searchUtils');
|
||||
@@ -42,9 +44,7 @@ odoo.define('web.CustomFilterItem', function (require) {
|
||||
* ]
|
||||
*/
|
||||
class CustomFilterItem extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
setup() {
|
||||
this.model = useModel('searchModel');
|
||||
|
||||
this.conditions = useState([]);
|
||||
@@ -270,7 +270,7 @@ odoo.define('web.CustomFilterItem', function (require) {
|
||||
}
|
||||
}
|
||||
|
||||
CustomFilterItem.components = { DatePicker, DateTimePicker };
|
||||
CustomFilterItem.components = { DatePicker, DateTimePicker, Dropdown };
|
||||
CustomFilterItem.props = { fields: Object };
|
||||
CustomFilterItem.template = "web.CustomFilterItem";
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
odoo.define('web.CustomGroupByItem', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { useModel } = require('web.Model');
|
||||
|
||||
const { Component, useState } = owl;
|
||||
|
||||
class CustomGroupByItem extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.state = useState({
|
||||
fieldName: this.props.fields[0].name,
|
||||
});
|
||||
|
||||
this.model = useModel('searchModel');
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Handlers
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onApply() {
|
||||
const field = this.props.fields.find(f => f.name === this.state.fieldName);
|
||||
this.model.dispatch('createNewGroupBy', field);
|
||||
}
|
||||
}
|
||||
|
||||
CustomGroupByItem.template = "web.CustomGroupByItem";
|
||||
CustomGroupByItem.props = {
|
||||
fields: Array,
|
||||
};
|
||||
|
||||
return CustomGroupByItem;
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
odoo.define('web.FavoriteMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { Dropdown } = require("@web/core/dropdown/dropdown");
|
||||
const { DropdownItem } = require("@web/core/dropdown/dropdown_item");
|
||||
const Dialog = require('web.OwlDialog');
|
||||
const { FACET_ICONS } = require("web.searchUtils");
|
||||
const Registry = require('web.Registry');
|
||||
@@ -18,8 +20,7 @@ odoo.define('web.FavoriteMenu', function (require) {
|
||||
* the `web` module.
|
||||
*/
|
||||
class FavoriteMenu extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
setup() {
|
||||
this.icon = FACET_ICONS.favorite;
|
||||
this.model = useModel('searchModel');
|
||||
this.state = useState({ deletedFavorite: false });
|
||||
@@ -81,7 +82,7 @@ odoo.define('web.FavoriteMenu', function (require) {
|
||||
}
|
||||
|
||||
FavoriteMenu.registry = new Registry();
|
||||
FavoriteMenu.components = { Dialog };
|
||||
FavoriteMenu.components = { Dialog, Dropdown, DropdownItem };
|
||||
FavoriteMenu.template = 'web.Legacy.FavoriteMenu';
|
||||
|
||||
return FavoriteMenu;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
odoo.define('web.FilterMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { Dropdown } = require("@web/core/dropdown/dropdown");
|
||||
const { DropdownItem } = require("@web/core/dropdown/dropdown_item");
|
||||
const CustomFilterItem = require('web.CustomFilterItem');
|
||||
const { FACET_ICONS } = require("web.searchUtils");
|
||||
const { useModel } = require('web.Model');
|
||||
@@ -16,8 +18,7 @@ odoo.define('web.FilterMenu', function (require) {
|
||||
*/
|
||||
class FilterMenu extends Component {
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
setup() {
|
||||
this.icon = FACET_ICONS.filter;
|
||||
this.model = useModel('searchModel');
|
||||
}
|
||||
@@ -53,8 +54,15 @@ odoo.define('web.FilterMenu', function (require) {
|
||||
}
|
||||
|
||||
FilterMenu.components = { CustomFilterItem };
|
||||
FilterMenu.props = { fields: Object };
|
||||
FilterMenu.defaultProps = {
|
||||
class: "",
|
||||
};
|
||||
FilterMenu.props = {
|
||||
fields: Object,
|
||||
class: { String, optional: true },
|
||||
};
|
||||
FilterMenu.template = "web.legacy.FilterMenu";
|
||||
FilterMenu.components = { CustomFilterItem, Dropdown, DropdownItem };
|
||||
|
||||
return FilterMenu;
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
odoo.define('web.GroupByMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
const CustomGroupByItem = require('web.CustomGroupByItem');
|
||||
const { Dropdown } = require("@web/core/dropdown/dropdown");
|
||||
const { DropdownItem } = require("@web/core/dropdown/dropdown_item");
|
||||
const { CustomGroupByItem } = require('@web/search/group_by_menu/custom_group_by_item');
|
||||
const { FACET_ICONS, GROUPABLE_TYPES } = require('web.searchUtils');
|
||||
const { useModel } = require('web.Model');
|
||||
|
||||
@@ -9,8 +11,7 @@ odoo.define('web.GroupByMenu', function (require) {
|
||||
|
||||
class GroupByMenu extends Component {
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
setup() {
|
||||
this.icon = FACET_ICONS.groupBy;
|
||||
|
||||
this.fields = Object.values(this.props.fields)
|
||||
@@ -64,9 +65,13 @@ odoo.define('web.GroupByMenu', function (require) {
|
||||
this.model.dispatch('toggleFilter', itemId);
|
||||
}
|
||||
}
|
||||
onAddCustomGroup(fieldName) {
|
||||
const field = this.props.fields[fieldName];
|
||||
this.model.dispatch("createNewGroupBy", field);
|
||||
}
|
||||
}
|
||||
|
||||
GroupByMenu.components = { CustomGroupByItem };
|
||||
GroupByMenu.components = { CustomGroupByItem, Dropdown, DropdownItem };
|
||||
GroupByMenu.props = { fields: Object };
|
||||
GroupByMenu.template = "web.GroupByMenu";
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ odoo.define('web.SearchBar', function (require) {
|
||||
|
||||
const Domain = require('web.Domain');
|
||||
const field_utils = require('web.field_utils');
|
||||
const { useAutofocus } = require('web.custom_hooks');
|
||||
const { useAutofocus } = require("@web/core/utils/hooks");
|
||||
const { useModel } = require('web.Model');
|
||||
const { fuzzyTest } = require('@web/core/utils/search');
|
||||
|
||||
const { Component, useExternalListener, useRef, useState } = owl;
|
||||
const { Component, onMounted, onWillUnmount, useExternalListener, useRef, useState } = owl;
|
||||
const CHAR_FIELDS = ['char', 'html', 'many2many', 'many2one', 'one2many', 'text'];
|
||||
|
||||
let sourceId = 0;
|
||||
@@ -37,10 +37,8 @@ odoo.define('web.SearchBar', function (require) {
|
||||
* @extends Component
|
||||
*/
|
||||
class SearchBar extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.focusOnUpdate = useAutofocus();
|
||||
setup() {
|
||||
this.focusOnUpdate = useAutofocus('search-input');
|
||||
this.inputRef = useRef('search-input');
|
||||
this.model = useModel('searchModel');
|
||||
this.state = useState({
|
||||
@@ -56,19 +54,19 @@ odoo.define('web.SearchBar', function (require) {
|
||||
|
||||
useExternalListener(window, 'click', this._onWindowClick);
|
||||
useExternalListener(window, 'keydown', this._onWindowKeydown);
|
||||
}
|
||||
|
||||
mounted() {
|
||||
// 'search' will always patch the search bar, 'focus' will never.
|
||||
this.env.searchModel.on('search', this, this.focusOnUpdate);
|
||||
this.env.searchModel.on('focus-control-panel', this, () => {
|
||||
this.inputRef.el.focus();
|
||||
onMounted(() => {
|
||||
// 'search' will always patch the search bar, 'focus' will never.
|
||||
this.model.on('search', this, this.focusOnUpdate);
|
||||
this.model.on('focus-control-panel', this, () => {
|
||||
this.inputRef.el.focus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
willUnmount() {
|
||||
this.env.searchModel.off('search', this);
|
||||
this.env.searchModel.off('focus-control-panel', this);
|
||||
onWillUnmount(() => {
|
||||
this.model.off('search', this);
|
||||
this.model.off('focus-control-panel', this);
|
||||
});
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
@@ -485,7 +483,7 @@ odoo.define('web.SearchBar', function (require) {
|
||||
fields: {},
|
||||
};
|
||||
SearchBar.props = {
|
||||
fields: Object,
|
||||
fields: { type: Object, optional: true },
|
||||
};
|
||||
SearchBar.template = 'web.Legacy.SearchBar';
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
odoo.define('web.custom_hooks', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { useEffect } = require("@web/core/utils/hooks");
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
/**
|
||||
* Focus a given selector as soon as it appears in the DOM and if it was not
|
||||
* displayed before. If the selected target is an input|textarea, set the selection
|
||||
* at the end.
|
||||
* @param {Object} [params]
|
||||
* @param {string} [params.selector='autofocus'] default: select the first element
|
||||
* with an `autofocus` attribute.
|
||||
* @returns {Function} function that forces the focus on the next update if visible.
|
||||
*/
|
||||
function useAutofocus(params = {}) {
|
||||
const comp = Component.current;
|
||||
// Prevent autofocus in mobile
|
||||
if (comp.env.device.isMobileDevice) {
|
||||
return () => {};
|
||||
}
|
||||
const selector = params.selector || '[autofocus]';
|
||||
let forceFocusCount = 0;
|
||||
useEffect(function autofocus(target) {
|
||||
if (target) {
|
||||
target.focus();
|
||||
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
|
||||
const inputEl = target;
|
||||
inputEl.selectionStart = inputEl.selectionEnd = inputEl.value.length;
|
||||
}
|
||||
}
|
||||
}, () => [comp.el.querySelector(selector), forceFocusCount]);
|
||||
|
||||
return function focusOnUpdate() {
|
||||
forceFocusCount++; // force the effect to rerun on next patch
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The useListener hook offers an alternative to Owl's classical event
|
||||
* registration mechanism (with attribute 't-on-eventName' in xml). It is
|
||||
* especially useful for abstract components, meant to be extended by
|
||||
* specific ones. If those abstract components need to define event handlers,
|
||||
* but don't have any template (because the template completely depends on
|
||||
* specific cases), then using the 't-on' mechanism isn't adequate, as the
|
||||
* handlers would be lost by the template override. In this case, using this
|
||||
* hook instead is more convenient.
|
||||
*
|
||||
* Example: navigation event handling in AbstractField
|
||||
*
|
||||
* Usage: like all Owl hooks, this function has to be called in the
|
||||
* constructor of an Owl component:
|
||||
*
|
||||
* useListener('click', () => { console.log('clicked'); });
|
||||
*
|
||||
* An optional native query selector can be specified as second argument for
|
||||
* event delegation. In this case, the handler is only called if the event
|
||||
* is triggered on an element matching the given selector.
|
||||
*
|
||||
* useListener('click', 'button', () => { console.log('clicked'); });
|
||||
*
|
||||
* Note: components that alter the event's target (e.g. Portal) are not
|
||||
* expected to behave as expected with event delegation.
|
||||
*
|
||||
* @param {string} eventName the name of the event
|
||||
* @param {string} [querySelector] a JS native selector for event delegation
|
||||
* @param {function} handler the event handler (will be bound to the component)
|
||||
* @param {Object} [addEventListenerOptions] to be passed to addEventListener as options.
|
||||
* Useful for listening in the capture phase
|
||||
*/
|
||||
function useListener(eventName, querySelector, handler, addEventListenerOptions) {
|
||||
if (typeof arguments[1] !== 'string') {
|
||||
querySelector = null;
|
||||
handler = arguments[1];
|
||||
addEventListenerOptions = arguments[2];
|
||||
}
|
||||
if (typeof handler !== 'function') {
|
||||
throw new Error('The handler must be a function');
|
||||
}
|
||||
|
||||
const comp = Component.current;
|
||||
let boundHandler;
|
||||
if (querySelector) {
|
||||
boundHandler = function (ev) {
|
||||
let el = ev.target;
|
||||
let target;
|
||||
while (el && !target) {
|
||||
if (el.matches(querySelector)) {
|
||||
target = el;
|
||||
} else if (el === comp.el) {
|
||||
el = null;
|
||||
} else {
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
if (el) {
|
||||
handler.call(comp, ev);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
boundHandler = handler.bind(comp);
|
||||
}
|
||||
useEffect(() => {
|
||||
comp.el.addEventListener(eventName, boundHandler, addEventListenerOptions);
|
||||
return () => {
|
||||
comp.el.removeEventListener(eventName, boundHandler, addEventListenerOptions);
|
||||
};
|
||||
}, () => []);
|
||||
}
|
||||
|
||||
return {
|
||||
useAutofocus,
|
||||
useListener,
|
||||
};
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { loadJS } from "web.ajax";
|
||||
import { loadJS } from "@web/core/assets";
|
||||
import BrowserDetection from "web.BrowserDetection";
|
||||
import { _t } from "web.core";
|
||||
/**
|
||||
|
||||
@@ -183,10 +183,9 @@ function displayNotification(parent, action) {
|
||||
links = links.map(({url, label}) => `<a href="${_.escape(url)}" target="_blank">${_.escape(label)}</a>`)
|
||||
parent.displayNotification({
|
||||
title, // no escape for the title because it is done in the template
|
||||
message: sprintf(_.escape(message), ...links),
|
||||
message: owl.markup(sprintf(_.escape(message), ...links)),
|
||||
type,
|
||||
sticky,
|
||||
messageIsHtml: true, // dynamic parts of the message are escaped above
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
odoo.define('web.OwlDialog', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { Component, Portal, useRef } = owl;
|
||||
const { Component, useRef, onMounted, onWillUnmount } = owl;
|
||||
const SIZE_CLASSES = {
|
||||
'extra-large': 'modal-xl',
|
||||
'large': 'modal-lg',
|
||||
'small': 'modal-sm',
|
||||
};
|
||||
const { renderToString } = require('@web/core/utils/render');
|
||||
|
||||
/**
|
||||
* Dialog (owl version)
|
||||
@@ -36,51 +37,53 @@ odoo.define('web.OwlDialog', function (require) {
|
||||
* @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);
|
||||
|
||||
setup() {
|
||||
this.modalRef = useRef('modal');
|
||||
this.dialogRef = useRef('dialog');
|
||||
this.footerRef = useRef('modal-footer');
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
if (this.props.dialogAPI) {
|
||||
this.props.dialogAPI.close = this._close.bind(this);
|
||||
}
|
||||
|
||||
this._removeTooltips();
|
||||
onMounted(() => {
|
||||
this.constructor.display(this);
|
||||
|
||||
// Notifies new webclient to adjust UI active element
|
||||
this.env.bus.trigger("owl_dialog_mounted", this);
|
||||
}
|
||||
this.env.bus.on('close_dialogs', this, this._close);
|
||||
|
||||
willUnmount() {
|
||||
// Notifies new webclient to adjust UI active element
|
||||
this.env.bus.trigger("owl_dialog_willunmount", this);
|
||||
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.env.bus.off('close_dialogs', this, this._close);
|
||||
this._removeTooltips();
|
||||
|
||||
this._removeTooltips();
|
||||
// Notifies new webclient to adjust UI active element
|
||||
this.env.bus.trigger("owl_dialog_mounted", this);
|
||||
});
|
||||
|
||||
this.constructor.hide(this);
|
||||
onWillUnmount(() => {
|
||||
// Notifies new webclient to adjust UI active element
|
||||
this.env.bus.trigger("owl_dialog_willunmount", this);
|
||||
|
||||
this.env.bus.off('close_dialogs', this, this._close);
|
||||
|
||||
this._removeTooltips();
|
||||
|
||||
this.constructor.hide(this);
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -178,7 +181,7 @@ odoo.define('web.OwlDialog', function (require) {
|
||||
$(this.mainButton)
|
||||
.tooltip({
|
||||
delay: { show: 200, hide: 0 },
|
||||
title: () => this.env.qweb.renderToString('web.DialogButton.tooltip', {
|
||||
title: () => renderToString('web.DialogButton.tooltip', {
|
||||
title: this.mainButton.innerText.toUpperCase(),
|
||||
}),
|
||||
trigger: 'manual',
|
||||
@@ -225,7 +228,13 @@ odoo.define('web.OwlDialog', function (require) {
|
||||
// Activate last dialog and update body class
|
||||
const lastDialog = this.displayed[this.displayed.length - 1];
|
||||
if (lastDialog) {
|
||||
lastDialog.el.focus();
|
||||
if (lastDialog.el) {
|
||||
// legacy dialog
|
||||
lastDialog.el.focus();
|
||||
} else {
|
||||
// Owl dialog | LegacyAdaptedDialog
|
||||
lastDialog.dialogRef.el.focus();
|
||||
}
|
||||
const modalEl = lastDialog.modalRef ?
|
||||
// Owl dialog | LegacyAdaptedDialog
|
||||
lastDialog.modalRef.el :
|
||||
@@ -241,7 +250,6 @@ odoo.define('web.OwlDialog', function (require) {
|
||||
|
||||
Dialog.displayed = [];
|
||||
|
||||
Dialog.components = { Portal };
|
||||
Dialog.defaultProps = {
|
||||
backdrop: 'static',
|
||||
renderFooter: true,
|
||||
@@ -252,17 +260,26 @@ odoo.define('web.OwlDialog', function (require) {
|
||||
title: "Odoo",
|
||||
};
|
||||
Dialog.props = {
|
||||
backdrop: { validate: b => ['static', true, false].includes(b) },
|
||||
backdrop: { validate: b => ['static', true, false].includes(b), optional: true },
|
||||
contentClass: { type: String, optional: 1 },
|
||||
fullscreen: { type: Boolean, optional: 1 },
|
||||
onClosed: { type: Function, optional: 1 },
|
||||
renderFooter: Boolean,
|
||||
renderHeader: Boolean,
|
||||
size: { validate: s => ['extra-large', 'large', 'medium', 'small'].includes(s) },
|
||||
stopClicks: Boolean,
|
||||
renderFooter: { type: Boolean, optional: true },
|
||||
renderHeader: { type: Boolean, optional: true },
|
||||
size: { validate: s => ['extra-large', 'large', 'medium', 'small'].includes(s), optional: true },
|
||||
stopClicks: { type: Boolean, optional: true },
|
||||
subtitle: { type: String, optional: 1 },
|
||||
technical: Boolean,
|
||||
title: String,
|
||||
technical: { type: Boolean, optional: true },
|
||||
title: { type: String, optional: true },
|
||||
dialogAPI: { type: Object, optional: 1 },
|
||||
slots: {
|
||||
type: Object,
|
||||
optional: 1,
|
||||
shape: {
|
||||
default: { optional: 1 },
|
||||
buttons: { optional: 1 },
|
||||
},
|
||||
},
|
||||
};
|
||||
Dialog.template = 'web.OwlDialog';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
odoo.define('web.Popover', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { Component, Portal, QWeb, useRef, useState } = owl;
|
||||
const { Component, status, onWillUnmount, useEffect, useRef, useState } = owl;
|
||||
|
||||
/**
|
||||
* Popover
|
||||
@@ -17,8 +17,7 @@ odoo.define('web.Popover', function (require) {
|
||||
* @param {String} [props.position='bottom']
|
||||
* @param {String} [props.title]
|
||||
*/
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
setup() {
|
||||
this.popoverRef = useRef('popover');
|
||||
this.orderedPositions = ['top', 'bottom', 'left', 'right'];
|
||||
this.state = useState({
|
||||
@@ -39,20 +38,13 @@ odoo.define('web.Popover', function (require) {
|
||||
* keeping the count of global handlers low.
|
||||
*/
|
||||
this._hasGlobalEventListeners = false;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this._compute();
|
||||
}
|
||||
|
||||
patched() {
|
||||
this._compute();
|
||||
}
|
||||
|
||||
willUnmount() {
|
||||
if (this._hasGlobalEventListeners) {
|
||||
this._removeGlobalEventListeners();
|
||||
}
|
||||
useEffect(() => this._compute());
|
||||
onWillUnmount(() => {
|
||||
if (this._hasGlobalEventListeners) {
|
||||
this._removeGlobalEventListeners();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
@@ -82,7 +74,6 @@ odoo.define('web.Popover', function (require) {
|
||||
if (this.props.onClosed) {
|
||||
this.props.onClosed();
|
||||
}
|
||||
this.trigger('o-popover-closed');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,17 +163,19 @@ odoo.define('web.Popover', function (require) {
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClick(ev) {
|
||||
const props = this.props;
|
||||
this.state.displayed = !this.state.displayed;
|
||||
if (this.state.displayed) {
|
||||
if (this.props.onOpened) {
|
||||
this.props.onOpened();
|
||||
if (props.onOpened) {
|
||||
props.onOpened();
|
||||
}
|
||||
this.trigger('o-popover-opened');
|
||||
} else {
|
||||
if (this.props.onClosed) {
|
||||
this.props.onClosed();
|
||||
if (props.onClosed) {
|
||||
props.onClosed();
|
||||
}
|
||||
this.trigger('o-popover-closed');
|
||||
}
|
||||
if (!props.propagateClick) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,9 +199,8 @@ odoo.define('web.Popover', function (require) {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onPopoverClose(ev) {
|
||||
_onPopoverClose() {
|
||||
this._close();
|
||||
}
|
||||
|
||||
@@ -230,7 +222,7 @@ odoo.define('web.Popover', function (require) {
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onResizeWindow(ev) {
|
||||
if (this.__owl__.status === 5 /* destroyed */) {
|
||||
if (status(this) === "destroyed") {
|
||||
return;
|
||||
}
|
||||
this._compute();
|
||||
@@ -244,7 +236,7 @@ odoo.define('web.Popover', function (require) {
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onScrollDocument(ev) {
|
||||
if (this.__owl__.status === 5 /* destroyed */) {
|
||||
if (status(this) === "destroyed") {
|
||||
return;
|
||||
}
|
||||
this._compute();
|
||||
@@ -320,12 +312,20 @@ odoo.define('web.Popover', function (require) {
|
||||
|
||||
}
|
||||
|
||||
Popover.components = { Portal };
|
||||
Popover.template = 'Popover';
|
||||
Popover.defaultProps = {
|
||||
position: 'bottom',
|
||||
propagateClick: true,
|
||||
};
|
||||
Popover.props = {
|
||||
propagateClick: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
onClosed: {
|
||||
type: Function,
|
||||
optional: true,
|
||||
@@ -341,12 +341,19 @@ odoo.define('web.Popover', function (require) {
|
||||
position: {
|
||||
type: String,
|
||||
validate: (p) => ['top', 'bottom', 'left', 'right'].includes(p),
|
||||
optional: true,
|
||||
},
|
||||
title: { type: String, optional: true },
|
||||
titleAttribute: { type: String, optional: true },
|
||||
slots: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
shape: {
|
||||
default: { optional: true },
|
||||
opened: { optional: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
QWeb.registerComponent('Popover', Popover);
|
||||
|
||||
return Popover;
|
||||
});
|
||||
|
||||
@@ -247,7 +247,6 @@ var ServicesMixin = {
|
||||
* @param {string} [options.type='warning'] 'info', 'success', 'warning', 'danger' or ''
|
||||
* @param {boolean} [options.sticky=false]
|
||||
* @param {string} [options.className]
|
||||
* @param {boolean} [options.messageIsHtml=false]
|
||||
*/
|
||||
displayNotification: function (options) {
|
||||
return this.call('notification', 'notify', options);
|
||||
|
||||
@@ -3,6 +3,7 @@ odoo.define('web.translation', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Class = require('web.Class');
|
||||
const { _lt } = require("@web/core/l10n/translation");
|
||||
|
||||
var TranslationDataBase = Class.extend(/** @lends instance.TranslationDataBase# */{
|
||||
init: function() {
|
||||
@@ -90,20 +91,6 @@ var TranslationDataBase = Class.extend(/** @lends instance.TranslationDataBase#
|
||||
* @returns {String} source translated into the current locale
|
||||
*/
|
||||
var _t = new TranslationDataBase().build_translation_function();
|
||||
/**
|
||||
* Lazy translation function, only performs the translation when actually
|
||||
* printed (e.g. inserted into a template)
|
||||
*
|
||||
* Useful when defining translatable strings in code evaluated before the
|
||||
* translation database is loaded, as class attributes or at the top-level of
|
||||
* an OpenERP Web module
|
||||
*
|
||||
* @param {String} s string to translate
|
||||
* @returns {Object} lazy translation object
|
||||
*/
|
||||
var _lt = function (s) {
|
||||
return {toString: function () { return _t(s); }};
|
||||
};
|
||||
|
||||
/** Setup jQuery timeago */
|
||||
/*
|
||||
|
||||
@@ -255,11 +255,13 @@ _.escape = function escape(s) {
|
||||
// TODO (?)
|
||||
// * Markup.join / Markup#join => escapes items and returns a Markup
|
||||
// * Markup#replace => automatically escapes the replacements (difficult impl)
|
||||
class _Markup extends String {
|
||||
[_.escapeMethod]() {
|
||||
return this;
|
||||
}
|
||||
|
||||
// get a reference to the internalMarkup class from owl
|
||||
const _Markup = owl.markup('').constructor;
|
||||
_Markup.prototype[_.escapeMethod] = function () {
|
||||
return this;
|
||||
}
|
||||
|
||||
// exposed for qweb2.js
|
||||
window._Markup = _Markup;
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@ odoo.define('web.AbstractFieldOwl', function (require) {
|
||||
"use strict";
|
||||
|
||||
const field_utils = require('web.field_utils');
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const { useEffect } = require("@web/core/utils/hooks");
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
|
||||
const { Component } = owl;
|
||||
const { Component, onWillUpdateProps, useEffect } = owl;
|
||||
|
||||
/**
|
||||
* This file defines the Owl version of the AbstractField. Specific fields
|
||||
@@ -60,9 +59,7 @@ odoo.define('web.AbstractFieldOwl', function (require) {
|
||||
* @param {string} [props.options.mode=readonly] should be 'readonly' or 'edit'
|
||||
* @param {string} [props.options.viewType=default]
|
||||
*/
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
setup() {
|
||||
this._canQuickEdit = this.constructor.isQuickEditable;
|
||||
this._isValid = true;
|
||||
// this is the last value that was set by the user, unparsed. This is
|
||||
@@ -73,33 +70,14 @@ odoo.define('web.AbstractFieldOwl', function (require) {
|
||||
useListener('keydown', this._onKeydown);
|
||||
useListener('navigation-move', this._onNavigationMove);
|
||||
useEffect(() => this._applyDecorations());
|
||||
}
|
||||
/**
|
||||
* Hack: studio tries to find the field with a selector base on its
|
||||
* name, before it is mounted into the DOM. Ideally, this should be
|
||||
* done in the onMounted hook, but in this case we are too late, and
|
||||
* Studio finds nothing. As a consequence, the field can't be edited
|
||||
* by clicking on its label (or on the row formed by the pair label-field).
|
||||
*
|
||||
* TODO: move this to mounted at some point?
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
__patch() {
|
||||
const res = super.__patch(...arguments);
|
||||
this.el.setAttribute('name', this.name);
|
||||
this.el.classList.add('o_field_widget');
|
||||
this.el.classList.toggle('o_quick_editable', this._canQuickEdit);
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* @async
|
||||
* @param {Object} [nextProps]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async willUpdateProps(nextProps) {
|
||||
this._lastSetValue = undefined;
|
||||
return super.willUpdateProps(nextProps);
|
||||
useEffect(() => {
|
||||
this.el.setAttribute('name', this.name);
|
||||
this.el.classList.add('o_field_widget');
|
||||
this.el.classList.toggle('o_quick_editable', this._canQuickEdit);
|
||||
});
|
||||
onWillUpdateProps(() => {
|
||||
this._lastSetValue = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
@@ -5,6 +5,7 @@ odoo.define('web.basic_fields_owl', function (require) {
|
||||
const CustomCheckbox = require('web.CustomCheckbox');
|
||||
const { _lt } = require('web.translation');
|
||||
|
||||
const { onPatched } = owl;
|
||||
|
||||
/**
|
||||
* FieldBadge displays the field's value inside a bootstrap pill badge.
|
||||
@@ -27,11 +28,13 @@ odoo.define('web.basic_fields_owl', function (require) {
|
||||
|
||||
|
||||
class FieldBoolean extends AbstractField {
|
||||
patched() {
|
||||
super.patched();
|
||||
if (this.props.event && this.props.event.target === this) {
|
||||
this.activate();
|
||||
}
|
||||
setup() {
|
||||
super.setup();
|
||||
onPatched(() => {
|
||||
if (this.props.event && this.props.event.target === this) {
|
||||
this.activate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
@@ -23,6 +23,9 @@ odoo.define('web._field_registry_owl', function (require) {
|
||||
|
||||
// Basic fields
|
||||
registry
|
||||
.add('badge', basicFields.FieldBadge)
|
||||
.add('boolean', basicFields.FieldBoolean);
|
||||
.add('badge', basicFields.FieldBadge);
|
||||
// deactivate the owl FieldBoolean as it causes issues in the Settings form view
|
||||
// if it is in an invisible block. This was a legacy implementation of the owl FieldBoolean
|
||||
// anyway, and we're currently rewritting the basic views in owl.
|
||||
// .add('boolean', basicFields.FieldBoolean);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ odoo.define('web.FieldWrapper', function (require) {
|
||||
const { ComponentWrapper } = require('web.OwlCompatibility');
|
||||
const field_utils = require('web.field_utils');
|
||||
|
||||
const { App } = owl;
|
||||
|
||||
/**
|
||||
* This file defines the FieldWrapper component, an extension of ComponentWrapper,
|
||||
* needed to instanciate Owl fields inside legacy widgets. This component
|
||||
@@ -66,8 +68,8 @@ odoo.define('web.FieldWrapper', function (require) {
|
||||
* not the real field Component, which triggers the 'field-changed'
|
||||
* event. This function writes the attribute on that field Component.
|
||||
*/
|
||||
mounted() {
|
||||
super.mounted(...arguments);
|
||||
on_attach_callback() {
|
||||
super.on_attach_callback();
|
||||
this.componentRef.comp.__node = this.__node;
|
||||
}
|
||||
|
||||
@@ -124,9 +126,16 @@ odoo.define('web.FieldWrapper', function (require) {
|
||||
// instantiated yet when the renderer first asks if it is set
|
||||
// (only the wrapper is instantiated), so we instantiate one
|
||||
// with the same props, get its 'isSet' status, and destroy it.
|
||||
const c = new this.Component(null, this.props);
|
||||
const isSet = c.isSet;
|
||||
c.destroy();
|
||||
const app = new App(null, {
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
env: this.env,
|
||||
dev: this.env.isDebug(),
|
||||
translateFn: owl.Component.env._t,
|
||||
translatableAttributes: ["data-tooltip"],
|
||||
});
|
||||
const node = app.makeNode(this.Component, this.props);
|
||||
const isSet = node.component.isSet;
|
||||
app.destroy();
|
||||
return isSet;
|
||||
}
|
||||
isValid() {
|
||||
@@ -137,9 +146,16 @@ odoo.define('web.FieldWrapper', function (require) {
|
||||
// instantiated yet when the renderer first asks if it is set
|
||||
// (only the wrapper is instantiated), so we instantiate one
|
||||
// with the same props, get its 'isValid' status, and destroy it.
|
||||
const c = new this.Component(null, this.props);
|
||||
const isValid = c.isValid;
|
||||
c.destroy();
|
||||
const app = new App(null, {
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
env: this.env,
|
||||
dev: this.env.isDebug(),
|
||||
translateFn: owl.Component.env._t,
|
||||
translatableAttributes: ["data-tooltip"],
|
||||
});
|
||||
const node = app.makeNode(this.Component, this.props);
|
||||
const isValid = node.component.isValid;
|
||||
app.destroy();
|
||||
return isValid;
|
||||
}
|
||||
removeInvalidClass() {
|
||||
|
||||
@@ -28,8 +28,7 @@ var KanbanRenderer = require('web.KanbanRenderer');
|
||||
var ListRenderer = require('web.ListRenderer');
|
||||
const { ComponentWrapper, WidgetAdapterMixin } = require('web.OwlCompatibility');
|
||||
const { sprintf, toBoolElse } = require("web.utils");
|
||||
|
||||
const { escape } = owl;
|
||||
const { escape } = require("@web/core/utils/strings");
|
||||
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
@@ -1100,7 +1099,6 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, {
|
||||
navigation_move: '_onNavigationMove',
|
||||
save_optional_fields: '_onSaveOrLoadOptionalFields',
|
||||
load_optional_fields: '_onSaveOrLoadOptionalFields',
|
||||
pager_changed: '_onPagerChanged',
|
||||
}),
|
||||
|
||||
// We need to trigger the reset on every changes to be aware of the parent changes
|
||||
@@ -1145,6 +1143,7 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, {
|
||||
Promise.resolve();
|
||||
},
|
||||
withAccessKey: false,
|
||||
onPagerChanged: this._onPagerChanged.bind(this),
|
||||
};
|
||||
var arch = this.view && this.view.arch;
|
||||
if (arch) {
|
||||
@@ -1776,9 +1775,7 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, {
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onPagerChanged: function (ev) {
|
||||
ev.stopPropagation();
|
||||
const { currentMinimum, limit } = ev.data;
|
||||
_onPagerChanged: function ({ currentMinimum, limit }) {
|
||||
this._updateControlPanel({ currentMinimum, limit });
|
||||
this.trigger_up('load', {
|
||||
id: this.value.id,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
odoo.define("web.Model", function (require) {
|
||||
"use strict";
|
||||
|
||||
const { groupBy, partitionBy } = require("web.utils");
|
||||
const { groupBy } = require("web.utils");
|
||||
const Registry = require("web.Registry");
|
||||
const { useBus } = require("@web/core/utils/hooks");
|
||||
|
||||
const { Component, EventBus, Observer } = owl;
|
||||
const { EventBus, onWillRender, useComponent } = owl;
|
||||
const isNotNull = (val) => val !== null && val !== undefined;
|
||||
|
||||
/**
|
||||
@@ -232,7 +233,7 @@ odoo.define("web.Model", function (require) {
|
||||
this.dispatching = false;
|
||||
this.extensions = [];
|
||||
this.externalState = {};
|
||||
this.mapping = {};
|
||||
this.mapping = new WeakMap();
|
||||
this.rev = 1;
|
||||
|
||||
const { name, registry } = this.constructor;
|
||||
@@ -426,19 +427,9 @@ odoo.define("web.Model", function (require) {
|
||||
* @see Context.__notifyComponents() in owl.js for explanation
|
||||
* @private
|
||||
*/
|
||||
async _notifyComponents() {
|
||||
_notifyComponents() {
|
||||
const rev = ++this.rev;
|
||||
const subscriptions = this.subscriptions.update || [];
|
||||
const groups = partitionBy(subscriptions, (s) =>
|
||||
s.owner ? s.owner.__owl__.depth : -1
|
||||
);
|
||||
for (let group of groups) {
|
||||
const proms = group.map((sub) =>
|
||||
sub.callback.call(sub.owner, rev)
|
||||
);
|
||||
Component.scheduler.flush();
|
||||
await Promise.all(proms);
|
||||
}
|
||||
this.trigger("update", { rev });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,47 +443,32 @@ odoo.define("web.Model", function (require) {
|
||||
* @returns {model}
|
||||
*/
|
||||
function useModel(modelName) {
|
||||
const component = Component.current;
|
||||
const component = useComponent();
|
||||
const model = component.env[modelName];
|
||||
if (!(model instanceof Model)) {
|
||||
throw new Error(`No Model found when connecting '${
|
||||
component.name
|
||||
}'`);
|
||||
throw new Error(`No Model found when connecting '${component.constructor.name}'`);
|
||||
}
|
||||
|
||||
const mapping = model.mapping;
|
||||
const __owl__ = component.__owl__;
|
||||
const componentId = __owl__.id;
|
||||
if (!__owl__.observer) {
|
||||
__owl__.observer = new Observer();
|
||||
__owl__.observer.notifyCB = component.render.bind(component);
|
||||
}
|
||||
const currentCB = __owl__.observer.notifyCB;
|
||||
__owl__.observer.notifyCB = function () {
|
||||
if (model.rev > mapping[componentId]) {
|
||||
model.mapping.set(component, 0);
|
||||
const __render = component.render.bind(component);
|
||||
component.render = function () {
|
||||
if (model.rev > model.mapping.get(component)) {
|
||||
return;
|
||||
}
|
||||
currentCB();
|
||||
};
|
||||
mapping[componentId] = 0;
|
||||
const renderFn = __owl__.renderFn;
|
||||
__owl__.renderFn = function (comp, params) {
|
||||
mapping[componentId] = model.rev;
|
||||
return renderFn(comp, params);
|
||||
__render();
|
||||
};
|
||||
|
||||
model.on("update", component, async (modelRev) => {
|
||||
if (mapping[componentId] < modelRev) {
|
||||
mapping[componentId] = modelRev;
|
||||
await component.render();
|
||||
}
|
||||
onWillRender(() => {
|
||||
model.mapping.set(component, model.rev);
|
||||
});
|
||||
|
||||
const __destroy = component.__destroy;
|
||||
component.__destroy = (parent) => {
|
||||
model.off("update", component);
|
||||
__destroy.call(component, parent);
|
||||
};
|
||||
useBus(model, "update", (ev) => {
|
||||
const { rev } = ev.detail;
|
||||
if (model.mapping.get(component) < rev) {
|
||||
model.mapping.set(component, rev);
|
||||
component.render();
|
||||
}
|
||||
});
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
odoo.define('web.OwlCompatibility', function () {
|
||||
odoo.define('web.OwlCompatibility', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { Component, useRef, useSubEnv, xml } = owl;
|
||||
|
||||
/**
|
||||
* This file defines the necessary tools for the transition phase where Odoo
|
||||
* legacy widgets and Owl components will coexist. There are two possible
|
||||
@@ -11,11 +9,24 @@ odoo.define('web.OwlCompatibility', function () {
|
||||
* 2) A legacy widget has to instantiate Owl components
|
||||
*/
|
||||
|
||||
const {
|
||||
App,
|
||||
Component,
|
||||
onMounted,
|
||||
onWillStart,
|
||||
onWillUnmount,
|
||||
onPatched,
|
||||
onWillUpdateProps,
|
||||
onWillDestroy,
|
||||
useSubEnv,
|
||||
xml,
|
||||
status,
|
||||
} = owl;
|
||||
|
||||
const widgetSymbol = odoo.widgetSymbol;
|
||||
const children = new WeakMap(); // associates legacy widgets with their Owl children
|
||||
|
||||
const templateForLegacy = xml`<div/>`;
|
||||
const templateForLegacy = xml`<t/>`;
|
||||
const templateForOwl = xml`<t t-component="props.Component" t-props="childProps" />`;
|
||||
/**
|
||||
* Case 1) An Owl component has to instantiate legacy widgets
|
||||
@@ -80,7 +91,7 @@ odoo.define('web.OwlCompatibility', function () {
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
constructor(parent, props) {
|
||||
constructor(props) {
|
||||
if (!props.Component) {
|
||||
throw Error(`ComponentAdapter: 'Component' prop is missing.`);
|
||||
}
|
||||
@@ -93,11 +104,78 @@ odoo.define('web.OwlCompatibility', function () {
|
||||
ComponentAdapter.template = template;
|
||||
super(...arguments);
|
||||
this.template = template;
|
||||
ComponentAdapter.template = null;
|
||||
|
||||
this.widget = null; // widget instance, if Component is a legacy widget
|
||||
}
|
||||
|
||||
setup() {
|
||||
onWillUpdateProps((nextProps) => {
|
||||
if (this.widget) {
|
||||
return this.updateWidget(nextProps);
|
||||
}
|
||||
});
|
||||
|
||||
let widgetIsAttached = false;
|
||||
const insertWidget = () => {
|
||||
this.removeEl();
|
||||
if (!this.widget || !this.widget.el) {
|
||||
return;
|
||||
}
|
||||
const node = this.__owl__.firstNode();
|
||||
node.parentNode.insertBefore(this.widget.el, node);
|
||||
this.widgetEl = this.widget.el;
|
||||
widgetIsAttached = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
insertWidget();
|
||||
if (this.widget && this.widget.on_attach_callback) {
|
||||
this.widget.on_attach_callback();
|
||||
}
|
||||
});
|
||||
|
||||
onPatched(() => {
|
||||
if (widgetIsAttached) {
|
||||
this.renderWidget();
|
||||
} else {
|
||||
insertWidget();
|
||||
}
|
||||
});
|
||||
|
||||
onWillDestroy(() => this.__destroy(this.__owl__.parent.component));
|
||||
|
||||
onWillStart(this.onWillStart);
|
||||
onWillUnmount(this.onWillUnmount);
|
||||
}
|
||||
|
||||
onWillStart() {
|
||||
if (!(this.props.Component.prototype instanceof Component)) {
|
||||
this.widget = new this.props.Component(this, ...this.widgetArgs);
|
||||
return this.widget._widgetRenderAndInsert(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
onWillUnmount() {
|
||||
this.removeEl();
|
||||
if (this.widget && this.widget.on_detach_callback) {
|
||||
this.widget.on_detach_callback();
|
||||
}
|
||||
}
|
||||
|
||||
removeEl() {
|
||||
if (this.widgetEl) {
|
||||
this.widgetEl.remove();
|
||||
this.widgetEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
__destroy() {
|
||||
this.removeEl();
|
||||
if (this.widget) {
|
||||
this.widget.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
get childProps() {
|
||||
if (!this._childProps) {
|
||||
this._childProps = Object.assign({}, this.props);
|
||||
@@ -106,80 +184,6 @@ odoo.define('web.OwlCompatibility', function () {
|
||||
return this._childProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the legacy widget (not in the DOM yet)
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
willStart() {
|
||||
if (!(this.props.Component.prototype instanceof Component)) {
|
||||
this.widget = new this.props.Component(this, ...this.widgetArgs);
|
||||
return this.widget._widgetRenderAndInsert(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal state of the legacy widget (but doesn't re-render
|
||||
* it yet).
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
willUpdateProps(nextProps) {
|
||||
if (this.widget) {
|
||||
return this.updateWidget(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks just before the actual patch to replace the fake div in the
|
||||
* vnode by the actual node of the legacy widget. If the widget has to
|
||||
* be re-render (because it has previously been updated), re-render it.
|
||||
* This must be synchronous.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
__patch(target, vnode) {
|
||||
if (this.widget) {
|
||||
if (this.__owl__.vnode) { // not at first rendering
|
||||
this.renderWidget();
|
||||
}
|
||||
vnode.elm = this.widget.el;
|
||||
}
|
||||
const result = super.__patch(...arguments);
|
||||
if (this.widget && this.el !== this.widget.el) {
|
||||
this.__owl__.vnode.elm = this.widget.el;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
mounted() {
|
||||
if (this.widget && this.widget.on_attach_callback) {
|
||||
this.widget.on_attach_callback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
willUnmount() {
|
||||
if (this.widget && this.widget.on_detach_callback) {
|
||||
this.widget.on_detach_callback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
__destroy() {
|
||||
super.__destroy(...arguments);
|
||||
if (this.widget) {
|
||||
this.widget.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter that translates the props (except 'Component') into the array
|
||||
* of arguments used to initialize the legacy widget.
|
||||
@@ -274,8 +278,27 @@ odoo.define('web.OwlCompatibility', function () {
|
||||
this.trigger(evType.replace(/_/g, '-'), payload);
|
||||
}
|
||||
}
|
||||
|
||||
get el() {
|
||||
if (this.widget) {
|
||||
return this.widget.el;
|
||||
}
|
||||
return super.el;
|
||||
}
|
||||
}
|
||||
|
||||
function standaloneAdapter(props) {
|
||||
const env = owl.Component.env;
|
||||
const app = new App(null, {
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
env,
|
||||
dev: env.isDebug(),
|
||||
translatableAttributes: ["data-tooltip"],
|
||||
translateFn: env._t,
|
||||
});
|
||||
const node = app.makeNode(ComponentAdapter, props);
|
||||
return node.component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Case 2) A legacy widget has to instantiate Owl components
|
||||
@@ -341,154 +364,139 @@ odoo.define('web.OwlCompatibility', function () {
|
||||
* the DOM (will be removed alongside this widget).
|
||||
*/
|
||||
destroy() {
|
||||
for (const component of children.get(this) || []) {
|
||||
component.env.qweb.off("update", component);
|
||||
component.__destroy();
|
||||
for (const wrapper of children.get(this) || []) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
children.delete(this);
|
||||
},
|
||||
};
|
||||
class ComponentWrapper extends Component {
|
||||
/**
|
||||
* Stores the reference of the instance in the parent (in __components).
|
||||
* Also creates a sub environment with a function that will be called
|
||||
* just before events are triggered (see component_extension.js). This
|
||||
* allows to add DOM event listeners on-the-fly, to redirect those Owl
|
||||
* custom (yet DOM) events to legacy custom events (trigger_up).
|
||||
*
|
||||
* @override
|
||||
* @param {Widget|null} parent
|
||||
* @param {Component} Component this is a Class, not an instance
|
||||
* @param {Object} props
|
||||
*/
|
||||
constructor(parent, Component, props) {
|
||||
if (parent instanceof Component) {
|
||||
throw new Error('ComponentWrapper must be used with a legacy Widget as parent');
|
||||
|
||||
//----------------------------------//
|
||||
// Low-level coordination functions //
|
||||
//----------------------------------//
|
||||
|
||||
/**
|
||||
* Calls "callback" recursively on a ComponentNode's hierarchy
|
||||
*
|
||||
* @param {ComponentNode}
|
||||
* @param {Boolean} childrenFirst whether to execute on the bottom-most child first
|
||||
* @param {Function} callback
|
||||
*/
|
||||
function recursiveCall(node, childrenFirst = false, callback) {
|
||||
if (!childrenFirst) {
|
||||
callback(node);
|
||||
}
|
||||
for (const child of Object.values(node.children)) {
|
||||
recursiveCall(child, childrenFirst, callback);
|
||||
}
|
||||
if (childrenFirst) {
|
||||
callback(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the node able to distinguish between
|
||||
* mounting in the DOM and mounting outside of if.
|
||||
* @param {ComponentNode} node
|
||||
*/
|
||||
function prepareForFinish(node) {
|
||||
const fiber = node.fiber;
|
||||
const complete = fiber.complete;
|
||||
fiber.complete = function() {
|
||||
// if target is not in dom
|
||||
// just trigger mounted hooks on the Proxy, not on any other node
|
||||
if (!document.contains(this.target)) {
|
||||
this.mounted = [this];
|
||||
// We skipped a bunch of mounted calls.
|
||||
// Following calls to patched may crash because of this.
|
||||
// (e.g. useEffect dependencies set in mounted and used in patched)
|
||||
}
|
||||
super(null, props);
|
||||
if (parent) {
|
||||
this._register(parent);
|
||||
complete.call(this);
|
||||
};
|
||||
}
|
||||
|
||||
const nodesToRemount = new WeakMap();
|
||||
/**
|
||||
* Pushed a node into the nodesToRemount WeakMap.
|
||||
* The value is a callback that sets the node for remounting
|
||||
* @param {ComponentNode} node
|
||||
* @param {function} updateAndRender The original node's prototype's
|
||||
*/
|
||||
function setToRemount(node, updateAndRender) {
|
||||
let toRemount = true;
|
||||
|
||||
node.mounted.push(() => {
|
||||
toRemount = false;
|
||||
});
|
||||
node.updateAndRender = function (props, parentFiber) {
|
||||
const res = updateAndRender.call(this, ...arguments);
|
||||
const rootMounted = parentFiber.root.mounted;
|
||||
if (toRemount && !rootMounted.includes(this.fiber)) {
|
||||
rootMounted.push(this.fiber);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
return () => toRemount = true;
|
||||
}
|
||||
/**
|
||||
* Make the node able to remount its children nodes correctly
|
||||
* Typically, in that case we don't call willPatch and patched hooks,
|
||||
* rather, we want to call the mounted hooks
|
||||
* @param {ComponentNode} mainNode
|
||||
*/
|
||||
function prepareForRemount(mainNode) {
|
||||
const updateAndRender = mainNode.updateAndRender;
|
||||
recursiveCall(mainNode, false, (node) => {
|
||||
if (mainNode === node) {
|
||||
return;
|
||||
}
|
||||
if (nodesToRemount.has(node)) {
|
||||
nodesToRemount.get(node)();
|
||||
return;
|
||||
} else {
|
||||
nodesToRemount.set(node, setToRemount(node, updateAndRender));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a wrapper instance as a child of the given parent in the
|
||||
* 'children' weakMap.
|
||||
*
|
||||
* @private
|
||||
* @param {Widget} parent
|
||||
*/
|
||||
function registerWrapper(parent, wrapper) {
|
||||
let parentChildren = children.get(parent);
|
||||
if (!parentChildren) {
|
||||
parentChildren = [];
|
||||
children.set(parent, parentChildren);
|
||||
}
|
||||
parentChildren.push(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* The component class that will be instanciated between a legacy and an OWL 2 layer.
|
||||
*/
|
||||
class ProxyComponent extends Component {
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
this.props.mounted();
|
||||
});
|
||||
onPatched(() => {
|
||||
this.props.patched();
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
// The current el will be change if we remount after unmounting
|
||||
this._handledEvents = new Set();
|
||||
});
|
||||
this.parentWidget = this.props.parentWidget;
|
||||
this._handledEvents = new Set();
|
||||
useSubEnv({
|
||||
[widgetSymbol]: this._addListener.bind(this)
|
||||
});
|
||||
|
||||
this.parentWidget = parent;
|
||||
this.Component = Component;
|
||||
this.props = props || {};
|
||||
this._handledEvents = new Set(); // Owl events we are redirecting
|
||||
|
||||
this.componentRef = useRef("component");
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls __callMounted on itself and on each sub component (as this
|
||||
* function isn't recursive) when the component is appended into the DOM.
|
||||
*/
|
||||
on_attach_callback() {
|
||||
function recursiveCallMounted(component) {
|
||||
const { status, currentFiber } = component.__owl__;
|
||||
|
||||
if (status === 2 && currentFiber && !currentFiber.isCompleted) {
|
||||
// the component is rendered but another rendering is being done
|
||||
// it would be foolish to declare the component and children as mounted
|
||||
return;
|
||||
}
|
||||
if (
|
||||
status !== 2 /* RENDERED */ &&
|
||||
status !== 3 /* MOUNTED */ &&
|
||||
status !== 4 /* UNMOUNTED */
|
||||
) {
|
||||
// Avoid calling mounted on a component that is not even
|
||||
// rendered. Doing otherwise will lead to a crash if a
|
||||
// specific mounted callback is legitimately relying on the
|
||||
// component being mounted.
|
||||
return;
|
||||
}
|
||||
for (const key in component.__owl__.children) {
|
||||
recursiveCallMounted(component.__owl__.children[key]);
|
||||
}
|
||||
component.__callMounted();
|
||||
}
|
||||
recursiveCallMounted(this);
|
||||
}
|
||||
/**
|
||||
* Calls __callWillUnmount to notify the component it will be unmounted.
|
||||
*/
|
||||
on_detach_callback() {
|
||||
this.__callWillUnmount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides to remove the reference to this component in the parent.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
destroy() {
|
||||
if (this.parentWidget) {
|
||||
const parentChildren = children.get(this.parentWidget);
|
||||
if (parentChildren) {
|
||||
const index = parentChildren.indexOf(this);
|
||||
children.get(this.parentWidget).splice(index, 1);
|
||||
}
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the parent of the wrapper component. This is a function of the
|
||||
* legacy widgets (ParentedMixin), so we have to handle it someway.
|
||||
* It simply removes the reference of this component in the current
|
||||
* parent (if there was one), and adds the reference to the new one.
|
||||
*
|
||||
* We have at least one usecase for this: in views, the renderer is
|
||||
* instantiated without parent, then a controller is instantiated with
|
||||
* the renderer as argument, and finally, setParent is called to set the
|
||||
* controller as parent of the renderer. This implies that Owl renderers
|
||||
* can't trigger events in their constructor.
|
||||
*
|
||||
* @param {Widget} parent
|
||||
*/
|
||||
setParent(parent) {
|
||||
if (parent instanceof Component) {
|
||||
throw new Error('ComponentWrapper must be used with a legacy Widget as parent');
|
||||
}
|
||||
this._register(parent);
|
||||
if (this.parentWidget) {
|
||||
const parentChildren = children.get(this.parentWidget);
|
||||
parentChildren.splice(parentChildren.indexOf(this), 1);
|
||||
}
|
||||
this.parentWidget = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the props and re-render the component.
|
||||
*
|
||||
* @async
|
||||
* @param {Object} props
|
||||
* @return {Promise}
|
||||
*/
|
||||
async update(props = {}) {
|
||||
if (this.__owl__.status === 5 /* destroyed */) {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
Object.assign(this.props, props);
|
||||
|
||||
let prom;
|
||||
if (this.__owl__.status === 3 /* mounted */) {
|
||||
prom = this.render();
|
||||
} else {
|
||||
// we may not be in the DOM, but actually want to be redrawn
|
||||
// (e.g. we were detached from the DOM, and now we're going to
|
||||
// be re-attached, but we need to be reloaded first). In this
|
||||
// case, we have to call 'mount' as Owl would skip the rendering
|
||||
// if we simply call render.
|
||||
prom = this.mount(...this._mountArgs);
|
||||
}
|
||||
return prom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event handler that will redirect the given Owl event to an
|
||||
* Odoo legacy event. This function is called just before the event is
|
||||
@@ -498,7 +506,7 @@ odoo.define('web.OwlCompatibility', function () {
|
||||
* @param {string} evType
|
||||
*/
|
||||
_addListener(evType) {
|
||||
if (this.parentWidget && !this._handledEvents.has(evType)) {
|
||||
if (this.parentWidget && !this._handledEvents.has(evType) && status(this) === "mounted") {
|
||||
this._handledEvents.add(evType);
|
||||
this.el.addEventListener(evType, ev => {
|
||||
// as the WrappeComponent has the same root node as the
|
||||
@@ -516,52 +524,216 @@ odoo.define('web.OwlCompatibility', function () {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ProxyComponent.template = xml`<t t-component="props.Component" t-props="props.props"/>`;
|
||||
|
||||
/**
|
||||
* Registers this instance as a child of the given parent in the
|
||||
* 'children' weakMap.
|
||||
*
|
||||
* @private
|
||||
* @param {Widget} parent
|
||||
*/
|
||||
_register(parent) {
|
||||
let parentChildren = children.get(parent);
|
||||
if (!parentChildren) {
|
||||
parentChildren = [];
|
||||
children.set(parent, parentChildren);
|
||||
class ComponentWrapper {
|
||||
constructor(parent, Component, props) {
|
||||
if (parent instanceof Component) {
|
||||
throw new Error("ComponentWrapper must be used with a legacy Widget as parent");
|
||||
}
|
||||
parentChildren.push(this);
|
||||
this.setParent(parent);
|
||||
this.props = props;
|
||||
|
||||
this.Component = Component;
|
||||
|
||||
const env = owl.Component.env;
|
||||
const appConfig = {
|
||||
env,
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
dev: env.isDebug(),
|
||||
translatableAttributes: ["data-tooltip"],
|
||||
translateFn: env._t,
|
||||
};
|
||||
const app = new App(null, appConfig);
|
||||
this.app = app;
|
||||
this.node = this._makeOwlNode();
|
||||
this.env = this.node.component.env;
|
||||
app.root = this.node;
|
||||
this.__owl__ = Object.create(this.node);
|
||||
this.componentRef = { comp: null };
|
||||
this.status = "new";
|
||||
this.setup();
|
||||
}
|
||||
/**
|
||||
* Stores mount target and position at first mount. That way, when updating
|
||||
* while out of DOM, we know where and how to remount.
|
||||
* @see update()
|
||||
* @override
|
||||
*/
|
||||
|
||||
_makeOwlNode() {
|
||||
const onPatched = () => {
|
||||
this.componentRef.comp = Object.values(this.node.children)[0].component;
|
||||
if (this.renderResolve) {
|
||||
this.renderResolve();
|
||||
}
|
||||
};
|
||||
|
||||
const props = {
|
||||
props: this.props,
|
||||
mounted: onPatched,
|
||||
patched: onPatched,
|
||||
Component: this.Component,
|
||||
parentWidget: this.parentWidget,
|
||||
};
|
||||
return this.app.makeNode(ProxyComponent, props);
|
||||
}
|
||||
|
||||
setup() {}
|
||||
|
||||
get el() {
|
||||
return this.node.component.el;
|
||||
}
|
||||
|
||||
//------------------//
|
||||
// OWL 1 - like API //
|
||||
//------------------//
|
||||
|
||||
async mount(target, options) {
|
||||
if (options && options.position === 'self') {
|
||||
throw new Error(
|
||||
'Unsupported position: "self" is not allowed for wrapper components. ' +
|
||||
'Contact the JS Framework team or open an issue if your use case is relevant.'
|
||||
);
|
||||
if (this.status === "mounted" || this.status === "willMount") {
|
||||
return this.render();
|
||||
} else if (this.status === "destroyed") {
|
||||
return;
|
||||
}
|
||||
this._mountArgs = arguments;
|
||||
return super.mount(...arguments);
|
||||
if (target) {
|
||||
this.target = target;
|
||||
}
|
||||
if (options) {
|
||||
this.mountOptions = options;
|
||||
}
|
||||
if (this.status === "unmounted") {
|
||||
prepareForRemount(this.node);
|
||||
}
|
||||
this.status = "willMount";
|
||||
const prom = this.app.mountNode(this.node, this.target, this.mountOptions);
|
||||
prepareForFinish(this.node);
|
||||
await prom;
|
||||
if (document.contains(this.target)) {
|
||||
this.status = "mounted";
|
||||
}
|
||||
this.node.willStart = [];
|
||||
// remove the promise.resolve from mounted callbacks
|
||||
const mounted = this.node.mounted;
|
||||
this.node.mounted = mounted.slice(0, mounted.length - 1);
|
||||
return this;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Getters
|
||||
//----------------------------------------------------------------------
|
||||
unmount() {
|
||||
this.on_detach_callback();
|
||||
this.el.remove();
|
||||
this.node.bdom = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.renderProm) {
|
||||
this.node.render();
|
||||
return this.renderProm;
|
||||
}
|
||||
this.renderProm = new Promise((resolve, reject) => {
|
||||
this.renderResolve = resolve;
|
||||
this.renderReject = reject;
|
||||
}).then(() => {
|
||||
this.renderProm = null;
|
||||
this.renderResolve = null;
|
||||
this.renderReject = null;
|
||||
});
|
||||
this.node.render();
|
||||
return this.renderProm;
|
||||
}
|
||||
|
||||
trigger() {
|
||||
return this.node.component.trigger(...arguments);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (["willMount", "unmounted"].includes(this.status)) {
|
||||
recursiveCall(this.node, false, (node) => {
|
||||
node.willUnmount = [];
|
||||
});
|
||||
}
|
||||
this.app.destroy();
|
||||
this.status = "destroyed";
|
||||
}
|
||||
|
||||
//---------------------//
|
||||
// API from legacy POV //
|
||||
//---------------------//
|
||||
|
||||
get $el() {
|
||||
return $(this.el);
|
||||
}
|
||||
|
||||
on_attach_callback() {
|
||||
if (!document.contains(this.el)) {
|
||||
return;
|
||||
}
|
||||
if (this.status === "mounted") {
|
||||
return;
|
||||
}
|
||||
recursiveCall(this.node, true, (node) => {
|
||||
for (const cb of node.mounted) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
this.status = "mounted";
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls willUnmount to notify the component it will be unmounted.
|
||||
*/
|
||||
on_detach_callback() {
|
||||
if (this.status === "unmounted") {
|
||||
return;
|
||||
}
|
||||
recursiveCall(this.node, false, (node) => {
|
||||
// node.status might be "new" (0) here, if the component is
|
||||
// currently being re-rendered, and a new component has just
|
||||
// been instantiated, but as the rendering isn't completed, it
|
||||
// isn't mounted yet
|
||||
if (node.status === 1) {
|
||||
const component = node.component;
|
||||
for (const cb of node.willUnmount) {
|
||||
cb.call(component);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.node.status = 0;
|
||||
this.status = "unmounted";
|
||||
}
|
||||
|
||||
async update(nextProps) {
|
||||
if (this.status === "destroyed") {
|
||||
return;
|
||||
}
|
||||
const props = this.node.component.props.props;
|
||||
const nextComponentProps = Object.assign({}, props, nextProps);
|
||||
this.node.component.props.props = nextComponentProps;
|
||||
this.props = nextComponentProps;
|
||||
if (this.status === "unmounted") {
|
||||
return this.mount(this.target);
|
||||
} else {
|
||||
return this.render();
|
||||
}
|
||||
}
|
||||
|
||||
setParent(parent) {
|
||||
if (parent instanceof Component) {
|
||||
throw new Error('ComponentWrapper must be used with a legacy Widget as parent');
|
||||
}
|
||||
if (parent) {
|
||||
registerWrapper(parent, this);
|
||||
}
|
||||
if (this.parentWidget) {
|
||||
const parentChildren = children.get(this.parentWidget);
|
||||
parentChildren.splice(parentChildren.indexOf(this), 1);
|
||||
}
|
||||
|
||||
this.parentWidget = parent;
|
||||
if (this.node) {
|
||||
this.node.component.parentWidget = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
ComponentWrapper.template = xml`<t t-component="Component" t-props="props" t-ref="component"/>`;
|
||||
|
||||
return {
|
||||
ComponentAdapter,
|
||||
ComponentWrapper,
|
||||
WidgetAdapterMixin,
|
||||
standaloneAdapter,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -20,15 +20,18 @@ import {
|
||||
mapLegacyEnvToWowlEnv,
|
||||
makeLegacyRainbowManService,
|
||||
} from "../../utils";
|
||||
import { ComponentAdapter } from "web.OwlCompatibility";
|
||||
import { standaloneAdapter } from "web.OwlCompatibility";
|
||||
|
||||
import { loadBundleTemplates } from "@web/core/assets";
|
||||
import { makeEnv, startServices } from "@web/env";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { browser } from '@web/core/browser/browser';
|
||||
import { jsonrpc } from '@web/core/network/rpc_service';
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
const { Component, config, mount, whenReady } = owl;
|
||||
const { Component, App, whenReady } = owl;
|
||||
|
||||
// Load localizations outside the PublicRoot to not wait for DOM ready (but
|
||||
// wait for them in PublicRoot)
|
||||
@@ -355,12 +358,10 @@ export const PublicRoot = publicWidget.RootWidget.extend({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Configure Owl with the public env
|
||||
*/
|
||||
config.mode = legacyEnv.isDebug() ? "dev" : "prod";
|
||||
Component.env = legacyEnv;
|
||||
owl.Component.env = legacyEnv;
|
||||
|
||||
/**
|
||||
* This widget is important, because the tour manager needs a root widget in
|
||||
@@ -404,17 +405,21 @@ export async function createPublicRoot(RootWidget) {
|
||||
}, { force: true });
|
||||
|
||||
const wowlEnv = makeEnv();
|
||||
wowlEnv.qweb.addTemplates(await loadBundleTemplates("web.assets_frontend"));
|
||||
|
||||
const templates = await loadBundleTemplates("web.assets_frontend");
|
||||
window.__OWL_TEMPLATES__ = templates;
|
||||
await startServices(wowlEnv);
|
||||
mapLegacyEnvToWowlEnv(legacyEnv, wowlEnv);
|
||||
|
||||
const adapter = new ComponentAdapter(null, { Component }); // Used for _trigger_up compat layer
|
||||
const publicRoot = new RootWidget(adapter);
|
||||
// The root widget's parent is a standalone adapter so that it has _trigger_up
|
||||
const publicRoot = new RootWidget(standaloneAdapter({ Component }));
|
||||
await Promise.all([
|
||||
mount(MainComponentsContainer, {
|
||||
target: document.body,
|
||||
new App(MainComponentsContainer, {
|
||||
env: wowlEnv,
|
||||
}),
|
||||
dev: wowlEnv.debug,
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
translateFn: _t,
|
||||
translatableAttributes: ["data-tooltip"],
|
||||
}).mount(document.body),
|
||||
publicRoot.attachTo(document.body),
|
||||
]);
|
||||
return publicRoot;
|
||||
|
||||
@@ -122,7 +122,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
|
||||
if (this.withControlPanel) {
|
||||
this.searchModel.on('get-controller-query-params', this, this._onGetOwnedQueryParams);
|
||||
}
|
||||
if (!(this.renderer instanceof Component)) {
|
||||
if (!(this.renderer instanceof ComponentWrapper)) {
|
||||
this.renderer.on_attach_callback();
|
||||
}
|
||||
},
|
||||
@@ -135,7 +135,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
|
||||
if (this.withControlPanel) {
|
||||
this.searchModel.off('get-controller-query-params', this);
|
||||
}
|
||||
if (!(this.renderer instanceof Component)) {
|
||||
if (!(this.renderer instanceof ComponentWrapper)) {
|
||||
this.renderer.on_detach_callback();
|
||||
}
|
||||
},
|
||||
@@ -362,7 +362,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
|
||||
* @private
|
||||
*/
|
||||
_startRenderer: function () {
|
||||
if (this.renderer instanceof Component) {
|
||||
if (this.renderer instanceof ComponentWrapper) {
|
||||
return this.renderer.mount(this.$('.o_content')[0]);
|
||||
}
|
||||
return this.renderer.appendTo(this.$('.o_content'));
|
||||
@@ -449,7 +449,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
|
||||
* @return {Promise}
|
||||
*/
|
||||
_updateRendererState(state, params = {}) {
|
||||
if (this.renderer instanceof Component) {
|
||||
if (this.renderer instanceof ComponentWrapper) {
|
||||
return this.renderer.update(state);
|
||||
}
|
||||
return this.renderer.updateState(state, params);
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import { renderToString } from '@web/core/utils/render';
|
||||
import * as mvc from 'web.mvc';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
// Renderers may display sample data when there is no real data to display. In
|
||||
// this case the data is displayed with opacity and can't be clicked. Moreover,
|
||||
// we also want to prevent the user from accessing DOM elements with TAB
|
||||
@@ -154,12 +153,8 @@ export default mvc.Renderer.extend({
|
||||
} else {
|
||||
templateName = "web.NoContentHelper";
|
||||
}
|
||||
const template = document.createElement('template');
|
||||
// FIXME: retrieve owl qweb instance via the env set on Component s.t.
|
||||
// it also works in the tests (importing 'web.env' wouldn't). This
|
||||
// won't be necessary as soon as this will be written in
|
||||
const owlQWeb = Component.env.qweb;
|
||||
template.innerHTML = owlQWeb.renderToString(templateName, context);
|
||||
const innerHTML = renderToString(templateName, context);
|
||||
const template = Object.assign(document.createElement("template"), { innerHTML });
|
||||
this.el.append(template.content.firstChild);
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module alias=web.AbstractRendererOwl **/
|
||||
|
||||
const { Component } = owl;
|
||||
const { Component, onMounted, onPatched } = owl;
|
||||
|
||||
// Renderers may display sample data when there is no real data to display. In
|
||||
// this case the data is displayed with opacity and can't be clicked. Moreover,
|
||||
@@ -15,22 +15,20 @@ const FOCUSABLE_ELEMENTS = [
|
||||
|
||||
class AbstractRenderer extends Component {
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
setup() {
|
||||
// Defines the elements suppressed when in demo data. This must be a list
|
||||
// of DOM selectors matching view elements that will:
|
||||
// 1. receive the 'o_sample_data_disabled' class (greyd out & no user events)
|
||||
// 2. have themselves and any of their focusable children removed from the
|
||||
// tab navigation
|
||||
this.sampleDataTargets = [];
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this._suppressFocusableElements();
|
||||
}
|
||||
|
||||
patched() {
|
||||
this._suppressFocusableElements();
|
||||
onMounted(() => {
|
||||
this._suppressFocusableElements();
|
||||
});
|
||||
onPatched(() => {
|
||||
this._suppressFocusableElements();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -126,15 +126,10 @@ var AbstractView = Factory.extend({
|
||||
// button when the graph view is embedded.
|
||||
var isEmbedded = params.isEmbedded || false;
|
||||
|
||||
// The noContentHelper's message can be empty, i.e. either a real empty string
|
||||
// or an empty html tag. In both cases, we consider the helper empty.
|
||||
var help = params.noContentHelp || "";
|
||||
var htmlHelp = document.createElement("div");
|
||||
htmlHelp.innerHTML = help;
|
||||
this.rendererParams = {
|
||||
arch: this.arch,
|
||||
isEmbedded: isEmbedded,
|
||||
noContentHelp: htmlHelp.innerText.trim() ? help : "",
|
||||
noContentHelp: params.noContentHelp,
|
||||
};
|
||||
|
||||
this.controllerParams = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user