合并企业版代码(未测试,先提交到测试分支)

This commit is contained in:
qihao.gong@jikimo.com
2023-04-14 17:42:23 +08:00
parent 7a7b3d7126
commit d28525526a
1300 changed files with 513579 additions and 5426 deletions

View File

@@ -0,0 +1,191 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import { browser } from "@web/core/browser/browser";
import { sprintf } from "@web/core/utils/strings";
import { deserializeDateTime, serializeDate, formatDate } from "@web/core/l10n/dates";
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { ExpirationPanel } from "./expiration_panel";
const { DateTime } = luxon;
const { Component, xml, useState } = owl;
function daysUntil(datetime) {
const duration = datetime.diff(DateTime.utc(), "days");
return Math.round(duration.values.days);
}
export class SubscriptionManager {
constructor(env, { rpc, orm, cookie, notification }) {
this.env = env;
this.rpc = rpc;
this.orm = orm;
this.cookie = cookie;
this.notification = notification;
if (session.expiration_date) {
this.expirationDate = deserializeDateTime(session.expiration_date);
} else {
// If no date found, assume 1 month and hope for the best
this.expirationDate = DateTime.utc().plus({ days: 30 });
}
this.expirationReason = session.expiration_reason;
// Hack: we need to know if there is at least one app installed (except from App and
// Settings). We use mail to do that, as it is a dependency of almost every addon. To
// determine whether mail is installed or not, we check for the presence of the key
// "notification_type" in session_info, as it is added in mail for internal users.
this.hasInstalledApps = "notification_type" in session;
// "user" or "admin"
this.warningType = session.warning;
this.lastRequestStatus = null;
this.isWarningHidden = this.cookie.current.oe_instance_hide_panel;
}
get formattedExpirationDate() {
return formatDate(this.expirationDate, { format: "DDD" });
}
get daysLeft() {
return daysUntil(this.expirationDate);
}
get unregistered() {
return ["trial", "demo", false].includes(this.expirationReason);
}
hideWarning() {
// Hide warning for 24 hours.
this.cookie.setCookie("oe_instance_hide_panel", true, 24 * 60 * 60);
this.isWarningHidden = true;
}
async buy() {
const limitDate = serializeDate(DateTime.utc().minus({ days: 15 }));
const args = [
[
["share", "=", false],
["login_date", ">=", limitDate],
],
];
const nbUsers = await this.orm.call("res.users", "search_count", args);
}
/**
* Save the registration code then triggers a ping to submit it.
*/
async submitCode(enterpriseCode) {
const [oldDate, , linkedSubscriptionUrl, linkedEmail] = await Promise.all([
this.orm.call("ir.config_parameter", "get_param", ["database.expiration_date"]),
this.orm.call("ir.config_parameter", "set_param", [
"database.enterprise_code",
enterpriseCode,
]),
// Aren't these a race condition ??? They depend on the upcoming ping...
this.orm.call("ir.config_parameter", "get_param", [
"database.already_linked_subscription_url",
]),
this.orm.call("ir.config_parameter", "get_param", ["database.already_linked_email"]),
]);
await this.orm.call("publisher_warranty.contract", "update_notification", [[]]);
const expirationDate = await this.orm.call("ir.config_parameter", "get_param", [
"database.expiration_date",
]);
if (linkedSubscriptionUrl) {
this.lastRequestStatus = "link";
this.linkedSubscriptionUrl = linkedSubscriptionUrl;
this.mailDeliveryStatus = null;
this.linkedEmail = linkedEmail;
} else if (expirationDate !== oldDate) {
this.lastRequestStatus = "success";
this.expirationDate = deserializeDateTime(expirationDate);
if (this.daysLeft > 30) {
const message = _t(
"Thank you, your registration was successful! Your database is valid until %s."
);
this.notification.add(sprintf(message, this.formattedExpirationDate), {
type: "success",
});
}
} else {
this.lastRequestStatus = "error";
}
}
async checkStatus() {
await this.orm.call("publisher_warranty.contract", "update_notification", [[]]);
const expirationDateStr = await this.orm.call("ir.config_parameter", "get_param", [
"database.expiration_date",
]);
this.lastRequestStatus = "update";
this.expirationDate = deserializeDateTime(expirationDateStr);
}
async sendUnlinkEmail() {
const sendUnlinkInstructionsUrl = await this.orm.call("ir.config_parameter", "get_param", [
"database.already_linked_send_mail_url",
]);
this.mailDeliveryStatus = "ongoing";
const { result, reason } = await this.rpc(sendUnlinkInstructionsUrl);
if (result) {
this.mailDeliveryStatus = "success";
} else {
this.mailDeliveryStatus = "fail";
this.mailDeliveryStatusError = reason;
}
}
async renew() {
const enterpriseCode = await this.orm.call("ir.config_parameter", "get_param", [
"database.enterprise_code",
]);
const contractQueryString = enterpriseCode ? `?contract=${enterpriseCode}` : "";
browser.location = `${url}${contractQueryString}`;
}
async upsell() {
const limitDate = serializeDate(DateTime.utc().minus({ days: 15 }));
const [enterpriseCode, nbUsers] = await Promise.all([
this.orm.call("ir.config_parameter", "get_param", ["database.enterprise_code"]),
this.orm.call("res.users", "search_count", [
[
["share", "=", false],
["login_date", ">=", limitDate],
],
]),
]);
const contractQueryString = enterpriseCode ? `&contract=${enterpriseCode}` : "";
browser.location = `${url}?num_users=${nbUsers}${contractQueryString}`;
}
}
class ExpiredSubscriptionBlockUI extends Component {
setup() {
this.subscription = useState(useService("enterprise_subscription"));
}
}
ExpiredSubscriptionBlockUI.template = xml`
<t t-if="subscription.daysLeft &lt;= 0">
<div class="o_blockUI"/>
<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 1100" class="d-flex align-items-center justify-content-center">
<ExpirationPanel/>
</div>
</t>`;
ExpiredSubscriptionBlockUI.components = { ExpirationPanel };
export const enterpriseSubscriptionService = {
name: "enterprise_subscription",
dependencies: ["orm", "rpc", "cookie", "notification"],
start(env, { rpc, orm, cookie, notification }) {
registry
.category("main_components")
.add("expired_subscription_block_ui", { Component: ExpiredSubscriptionBlockUI });
return new SubscriptionManager(env, { rpc, orm, cookie, notification });
},
};
registry.category("services").add("enterprise_subscription", enterpriseSubscriptionService);

View File

@@ -0,0 +1,77 @@
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
import { Transition } from "@web/core/transition";
import { sprintf } from "@web/core/utils/strings";
const { Component, useState, useRef } = owl;
/**
* Expiration panel
*
* Component representing the banner located on top of the home menu. Its purpose
* is to display the expiration state of the current database and to help the
* user to buy/renew its subscription.
* @extends Component
*/
export class ExpirationPanel extends Component {
setup() {
this.subscription = useState(useService("enterprise_subscription"));
this.cookie = useService("cookie");
this.state = useState({
displayRegisterForm: false,
});
this.inputRef = useRef("input");
}
get buttonText() {
return this.subscription.lastRequestStatus === "error" ? "Retry" : "Register";
}
get alertType() {
if (this.subscription.lastRequestStatus === "success") {
return "success";
}
const { daysLeft } = this.subscription;
if (daysLeft <= 6) {
return "danger";
} else if (daysLeft <= 16) {
return "warning";
}
return "info";
}
get expirationMessage() {
const { _t } = this.env;
const { daysLeft } = this.subscription;
if (daysLeft <= 0) {
return _t("This database has expired. ");
}
const delay = daysLeft === 30 ? _t("1 month") : sprintf(_t("%s days"), daysLeft);
if (this.subscription.expirationReason === "demo") {
return sprintf(_t("This demo database will expire in %s. "), delay);
}
return sprintf(_t("This database will expire in %s. "), delay);
}
showRegistrationForm() {
this.state.displayRegisterForm = !this.state.displayRegisterForm;
}
async onCodeSubmit() {
const enterpriseCode = this.inputRef.el.value;
if (!enterpriseCode) {
return;
}
await this.subscription.submitCode(enterpriseCode);
if (this.subscription.lastRequestStatus === "success") {
this.state.displayRegisterForm = false;
} else {
this.state.buttonText = "Retry";
}
}
}
ExpirationPanel.template = "DatabaseExpirationPanel";
ExpirationPanel.components = { Transition };

View File

@@ -0,0 +1,8 @@
.database_expiration_panel .oe_instance_register_form {
max-height: 0;
transition: max-height 0.4s;
&.o-vertical-slide-enter-active {
max-height: 10rem; // fixed value is required to properly trigger transition
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="DatabaseExpirationPanel" owl="1">
</t>
</templates>

View File

@@ -0,0 +1,9 @@
// = Home Menu
// ============================================================================
// No CSS hacks, variables overrides only
.o_home_menu_background {
.o_menu_systray .badge {
--o-navbar-badge-bg: #{$o-brand-primary};
}
}

View File

@@ -0,0 +1,310 @@
/** @odoo-module **/
import { isIosApp, isMacOS } from "@web/core/browser/feature_detection";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { useService } from "@web/core/utils/hooks";
import { ExpirationPanel } from "./expiration_panel";
const {
Component,
useExternalListener,
onMounted,
onPatched,
onWillUpdateProps,
useState,
useRef,
} = owl;
class FooterComponent extends Component {
setup() {
this.controlKey = isMacOS() ? "COMMAND" : "CONTROL";
}
}
FooterComponent.template = "web_enterprise.HomeMenu.CommandPalette.Footer";
/**
* Home menu
*
* This component handles the display and navigation between the different
* available applications and menus.
* @extends Component
*/
export class HomeMenu extends Component {
/**
* @param {Object} props
* @param {Object[]} props.apps application icons
* @param {number} props.apps[].actionID
* @param {number} props.apps[].id
* @param {string} props.apps[].label
* @param {string} props.apps[].parents
* @param {(boolean|string|Object)} props.apps[].webIcon either:
* - boolean: false (no webIcon)
* - string: path to Odoo icon file
* - Object: customized icon (background, class and color)
* @param {string} [props.apps[].webIconData]
* @param {string} props.apps[].xmlid
*/
setup() {
this.command = useService("command");
this.menus = useService("menu");
this.homeMenuService = useService("home_menu");
this.subscription = useState(useService("enterprise_subscription"));
this.ui = useService("ui");
this.state = useState({
focusedIndex: null,
isIosApp: isIosApp(),
});
this.inputRef = useRef("input");
if (!this.env.isSmall) {
this._registerHotkeys();
}
onWillUpdateProps(() => {
// State is reset on each remount
this.state.focusedIndex = null;
});
onMounted(() => {
this._focusInput();
});
onPatched(() => {
if (this.state.focusedIndex !== null && !this.env.isSmall) {
const selectedItem = document.querySelector(".o_home_menu .o_menuitem.o_focused");
// When TAB is managed externally the class o_focused disappears.
if (selectedItem) {
// Center window on the focused item
selectedItem.scrollIntoView({ block: "center" });
}
}
});
}
//--------------------------------------------------------------------------
// Getters
//--------------------------------------------------------------------------
/**
* @returns {Object[]}
*/
get displayedApps() {
return this.props.apps;
}
/**
* @returns {number}
*/
get maxIconNumber() {
const w = window.innerWidth;
if (w < 576) {
return 3;
} else if (w < 768) {
return 4;
} else {
return 6;
}
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {Object} menu
* @returns {Promise}
*/
_openMenu(menu) {
return this.menus.selectMenu(menu);
}
/**
* Update this.state.focusedIndex if not null.
* @private
* @param {string} cmd
*/
_updateFocusedIndex(cmd) {
const nbrApps = this.displayedApps.length;
const lastIndex = nbrApps - 1;
const focusedIndex = this.state.focusedIndex;
if (lastIndex < 0) {
return;
}
if (focusedIndex === null) {
this.state.focusedIndex = 0;
return;
}
const lineNumber = Math.ceil(nbrApps / this.maxIconNumber);
const currentLine = Math.ceil((focusedIndex + 1) / this.maxIconNumber);
let newIndex;
switch (cmd) {
case "previousElem":
newIndex = focusedIndex - 1;
break;
case "nextElem":
newIndex = focusedIndex + 1;
break;
case "previousColumn":
if (focusedIndex % this.maxIconNumber) {
// app is not the first one on its line
newIndex = focusedIndex - 1;
} else {
newIndex =
focusedIndex + Math.min(lastIndex - focusedIndex, this.maxIconNumber - 1);
}
break;
case "nextColumn":
if (focusedIndex === lastIndex || (focusedIndex + 1) % this.maxIconNumber === 0) {
// app is the last one on its line
newIndex = (currentLine - 1) * this.maxIconNumber;
} else {
newIndex = focusedIndex + 1;
}
break;
case "previousLine":
if (currentLine === 1) {
newIndex = focusedIndex + (lineNumber - 1) * this.maxIconNumber;
if (newIndex > lastIndex) {
newIndex = lastIndex;
}
} else {
// we go to the previous line on same column
newIndex = focusedIndex - this.maxIconNumber;
}
break;
case "nextLine":
if (currentLine === lineNumber) {
newIndex = focusedIndex % this.maxIconNumber;
} else {
// we go to the next line on the closest column
newIndex =
focusedIndex + Math.min(this.maxIconNumber, lastIndex - focusedIndex);
}
break;
}
// if newIndex is out of bounds -> normalize it
if (newIndex < 0) {
newIndex = lastIndex;
} else if (newIndex > lastIndex) {
newIndex = 0;
}
this.state.focusedIndex = newIndex;
}
_focusInput() {
if (!this.env.isSmall && this.inputRef.el) {
this.inputRef.el.focus();
}
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Object} app
*/
_onAppClick(app) {
this._openMenu(app);
}
/**
* @private
*/
_registerHotkeys() {
const hotkeys = [
["ArrowDown", () => this._updateFocusedIndex("nextLine")],
["ArrowRight", () => this._updateFocusedIndex("nextColumn")],
["ArrowUp", () => this._updateFocusedIndex("previousLine")],
["ArrowLeft", () => this._updateFocusedIndex("previousColumn")],
["Tab", () => this._updateFocusedIndex("nextElem")],
["shift+Tab", () => this._updateFocusedIndex("previousElem")],
[
"Enter",
() => {
const menu = this.displayedApps[this.state.focusedIndex];
if (menu) {
this._openMenu(menu);
}
},
],
["Escape", () => this.homeMenuService.toggle(false)],
];
hotkeys.forEach((hotkey) => {
useHotkey(...hotkey, {
allowRepeat: true,
});
});
useExternalListener(window, "keydown", this._onKeydownFocusInput);
}
_onKeydownFocusInput() {
if (
document.activeElement !== this.inputRef.el &&
this.ui.activeElement === document &&
!["TEXTAREA", "INPUT"].includes(document.activeElement.tagName)
) {
this._focusInput();
}
}
_onInputSearch() {
const onClose = () => {
this._focusInput();
this.inputRef.el.value = "";
};
const searchValue = this.compositionStart ? "/" : `/${this.inputRef.el.value.trim()}`;
this.compositionStart = false;
this.command.openMainPalette({ searchValue, FooterComponent }, onClose);
}
_onInputBlur() {
// if we blur search input to focus on body (eg. click on any
// non-interactive element) restore focus to avoid IME input issue
setTimeout(() => {
if (document.activeElement === document.body && this.ui.activeElement === document) {
this._focusInput();
}
}, 0);
}
_onCompositionStart() {
this.compositionStart = true;
}
}
HomeMenu.components = { ExpirationPanel };
HomeMenu.props = {
apps: {
type: Array,
element: {
type: Object,
shape: {
actionID: Number,
appID: Number,
id: Number,
label: String,
parents: String,
webIcon: {
type: [
Boolean,
String,
{
type: Object,
optional: 1,
shape: {
iconClass: String,
color: String,
backgroundColor: String,
},
},
],
optional: true,
},
webIconData: { type: String, optional: 1 },
xmlid: String,
},
},
},
};
HomeMenu.template = "web_enterprise.HomeMenu";

View File

@@ -0,0 +1,114 @@
.o_home_menu_background {
// 'Home menu background' design is shared with enterprise login
// screens and it's located in './home_menu_background.scss'
// When applied on webclient (note: we do not specify the webclient class
// here to avoid breaking studio custom style)
&:not(.o_home_menu_background_custom) .o_main_navbar {
background: transparent;
border-bottom-color: transparent;
.dropdown-menu {
border-color: $dropdown-bg;
}
.o_dropdown_active,
> ul > li.show > a {
outline: none;
}
}
&.o_home_menu_background_custom .o_home_menu {
background: {
size: cover;
repeat: no-repeat;
position: center;
}
}
.o_menu_systray {
@include print-variable(o-navbar-badge-bg, $o-navbar-home-menu-badge-bg);
}
}
.o_home_menu {
font-size: $o-home-menu-font-size-base;
.container {
@include media-breakpoint-up(md) {
max-width: $o-home-menu-container-size !important;
}
}
.o_app {
&,
.o_app_icon {
transition: all 0.3s ease 0s;
}
.o_app_icon {
width: $o-home-menu-app-icon-max-width;
height: $o-home-menu-app-icon-max-width;
background: {
size: cover;
repeat: no-repeat;
position: center;
}
.fa {
font-size: $o-home-menu-app-icon-max-width * 0.5;
text-shadow: 0 2px 0 rgba(0, 0, 0, 0.23);
}
}
.o_caption {
color: $o-home-menu-caption-color;
text-shadow: $o-home-menu-caption-shadow;
}
&:focus,
&.o_focused,
&:hover {
.o_app_icon {
box-shadow: 0 8px 15px -10px black;
transform: translateY(-1px);
}
}
&:focus,
&.o_focused {
background-color: rgba(white, 0.05);
border-radius: $border-radius;
}
}
// iOS iPhone list layout due to Apple AppStore review
@include media-breakpoint-down(md) {
&.o_ios_app {
.o_apps {
flex-direction: column;
font-size: $o-home-menu-font-size-base * 1.25;
margin-top: map-get($spacers, 1);
padding: 0 map-get($spacers, 2);
}
.o_app {
flex-direction: row !important;
justify-content: initial !important;
width: 100%;
background-color: rgba(255, 255, 255, 0.1);
padding: map-get($spacers, 3) map-get($spacers, 4) !important;
}
.o_app_icon {
width: $o-home-menu-app-icon-max-width * 0.75;
height: $o-home-menu-app-icon-max-width * 0.75;
margin-right: map-get($spacers, 4);
}
.o_caption {
text-align: start !important;
}
}
}
}

View File

@@ -0,0 +1,6 @@
// = Home Menu Variables
// ============================================================================
// No CSS hacks, variables overrides only
$o-home-menu-caption-color: $o-black !default;
$o-home-menu-caption-shadow: none !default;

View File

@@ -0,0 +1,6 @@
$o-home-menu-font-size-base: 1.25rem;
$o-home-menu-container-size: 850px;
$o-home-menu-app-icon-max-width: 70px;
$o-home-menu-caption-color: $o-white !default;
$o-home-menu-caption-shadow: 0 1px 1px rgba($o-black, 0.8) !default;

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="web_enterprise.HomeMenu" owl="1">
<div class="o_home_menu h-100 overflow-auto" t-att-class="{ o_ios_app: state.isIosApp }">
<div class="container">
<input t-ref="input" type="text" class="o_search_hidden visually-hidden w-auto" data-allow-hotkeys="true" t-on-input="_onInputSearch" t-on-blur="_onInputBlur" t-on-compositionstart="_onCompositionStart"/>
<!-- When the subscription has expired, the expiration panel is show over the whole UI instead of here -->
<ExpirationPanel t-if="subscription.warningType and !subscription.isWarningHidden and subscription.daysLeft &lt;= 30 and subscription.daysLeft > 0"/>
<div t-if="displayedApps.length" role="listbox" class="o_apps row user-select-none mt-5">
<a t-foreach="displayedApps" t-as="app"
t-att-id="'result_app_' + app_index"
role="option"
t-att-aria-selected="state.focusedIndex === app_index ? 'true' : 'false'"
class="o_app o_menuitem col-4 col-sm-3 col-md-2 p-2 mb-3 d-flex flex-column rounded justify-content-center align-items-center"
t-att-class="{o_focused: state.focusedIndex === app_index}"
t-att-data-menu-xmlid="app.xmlid"
t-att-href="app.href || ('#menu_id='+app.id+'&amp;action_id='+app.actionID)"
t-key="app.id"
t-on-click.prevent="() => this._onAppClick(app)"
>
<img t-if="app.webIconData" class="o_app_icon rounded"
t-attf-src="{{app.webIconData}}"
/>
<div t-else="" class="o_app_icon rounded d-flex p-2 justify-content-center align-items-center"
t-attf-style="background-color: {{app.webIcon.backgroundColor}};"
>
<i t-att-class="app.webIcon.iconClass" t-attf-style="color: {{app.webIcon.color}};"/>
</div>
<div class="o_caption w-100 text-center text-truncate mt-2" t-esc="app.label or app.name"/>
</a>
</div>
<div t-elif="!displayedApps.length" id="result_menu_0" role="option" aria-selected="true" class="o_no_result">
No result
</div>
</div>
</div>
</t>
<t t-name="web_enterprise.HomeMenu.CommandPalette.Footer" owl="1">
<span>
<span class='fw-bolder text-primary'>TIP</span> — open me anywhere with <span t-esc="controlKey" class='fw-bolder text-primary'/> + <span class='fw-bolder text-primary'>K</span>
</span>
</t>
</templates>

View File

@@ -0,0 +1,13 @@
// = Home Menu Background
// ============================================================================
// No CSS hacks, variables overrides only
.o_home_menu_background {
--homeMenu-bg-color: #000511;
--homeMenu-bg-image:
radial-gradient(at 100% 0%, #{rgba($o-gray-100, 0.6)} 0px, #{rgba($o-gray-100, 0.1)} 50%, #{rgba($o-gray-100, 0)} 100%),
radial-gradient(at 7% 13%, #{rgba($o-gray-200, 0.6)} 0px, #{rgba($o-gray-100, 0.05)} 50%, #{rgba($o-gray-100, 0)} 100%),
radial-gradient(at 96% 94%, #{rgba($o-gray-100, 0.5)} 0px, #{rgba($o-gray-100, 0.08)} 50%, #{rgba($o-gray-100, 0)} 100%),
radial-gradient(at 3% 96%, #{rgba($o-gray-200, 0.6)} 0px, #{rgba($o-gray-100, 0.05)} 50%, #{rgba($o-gray-100, 0)} 100%),
url("/web_enterprise/static/img/home-menu-bg-overlay.svg");
}

View File

@@ -0,0 +1,15 @@
// Shared with web client and login screen
.o_home_menu_background, .o_web_client.o_home_menu_background {
background: {
size: cover;
attachment: fixed;
color: var(--homeMenu-bg-color, #917878);
image: var(--homeMenu-bg-image,
radial-gradient(at 100% 0%, hsla(289,17%,21%,0.6) 0px, transparent 50%),
radial-gradient(at 7% 13%, hsla(268,5%,47%,0.42) 0px, transparent 50%),
radial-gradient(at 96% 94%, hsla(267,5%,46%,0.51) 0px, transparent 50%),
radial-gradient(at 3% 96%, hsla(289,17%,21%,0.41) 0px, transparent 50%),
url("/web_enterprise/static/img/home-menu-bg-overlay.svg")
);
}
}

View File

@@ -0,0 +1,88 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Mutex } from "@web/core/utils/concurrency";
import { useService } from "@web/core/utils/hooks";
import { computeAppsAndMenuItems } from "@web/webclient/menus/menu_helpers";
import { ControllerNotFoundError } from "@web/webclient/actions/action_service";
import { HomeMenu } from "./home_menu";
const { Component, onMounted, onWillUnmount, xml } = owl;
export const homeMenuService = {
dependencies: ["action", "router"],
start(env) {
let hasHomeMenu = false; // true iff the HomeMenu is currently displayed
let hasBackgroundAction = false; // true iff there is an action behind the HomeMenu
const mutex = new Mutex(); // used to protect against concurrent toggling requests
class HomeMenuAction extends Component {
setup() {
this.router = useService("router");
this.menus = useService("menu");
this.homeMenuProps = {
apps: computeAppsAndMenuItems(this.menus.getMenuAsTree("root")).apps,
};
onMounted(() => this.onMounted());
onWillUnmount(this.onWillUnmount);
}
async onMounted() {
const { breadcrumbs } = this.env.config;
hasHomeMenu = true;
hasBackgroundAction = breadcrumbs.length > 0;
this.router.pushState({ menu_id: undefined }, { lock: false, replace: true });
this.env.bus.trigger("HOME-MENU:TOGGLED");
}
onWillUnmount() {
hasHomeMenu = false;
hasBackgroundAction = false;
const currentMenuId = this.menus.getCurrentApp();
if (currentMenuId) {
this.router.pushState({ menu_id: currentMenuId.id }, { lock: true });
}
this.env.bus.trigger("HOME-MENU:TOGGLED");
}
}
HomeMenuAction.components = { HomeMenu };
HomeMenuAction.target = "current";
HomeMenuAction.template = xml`<HomeMenu t-props="homeMenuProps"/>`;
registry.category("actions").add("menu", HomeMenuAction);
env.bus.on("HOME-MENU:TOGGLED", null, () => {
document.body.classList.toggle("o_home_menu_background", hasHomeMenu);
});
return {
get hasHomeMenu() {
return hasHomeMenu;
},
get hasBackgroundAction() {
return hasBackgroundAction;
},
async toggle(show) {
return mutex.exec(async () => {
show = show === undefined ? !hasHomeMenu : Boolean(show);
if (show !== hasHomeMenu) {
if (show) {
await env.services.action.doAction("menu");
} else {
try {
await env.services.action.restore();
} catch (err) {
if (!(err instanceof ControllerNotFoundError)) {
throw err;
}
}
}
}
// hack: wait for a tick to ensure that the url has been updated before
// switching again
return new Promise((r) => setTimeout(r));
});
},
};
},
};
registry.category("services").add("home_menu", homeMenuService);