合并企业版代码(未测试,先提交到测试分支)
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
/** @odoo-module **/
|
||||
import { BurgerMenu } from "@web/webclient/burger_menu/burger_menu";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export class EnterpriseBurgerMenu extends BurgerMenu {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.hm = useService("home_menu");
|
||||
}
|
||||
|
||||
get currentApp() {
|
||||
return !this.hm.hasHomeMenu && super.currentApp;
|
||||
}
|
||||
}
|
||||
|
||||
const systrayItem = {
|
||||
Component: EnterpriseBurgerMenu,
|
||||
};
|
||||
|
||||
registry.category("systray").add("burger_menu", systrayItem, { sequence: 0, force: true });
|
||||
@@ -0,0 +1,6 @@
|
||||
// = Burger Menu Variables
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
$o-burger-topbar-bg: $o-gray-100 !default;
|
||||
$o-burger-topbar-color: $o-gray-900 !default;
|
||||
@@ -0,0 +1,20 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { getCookie, setCookie } from "web.utils.cookies";
|
||||
|
||||
export function switchColorSchemeItem(env) {
|
||||
return {
|
||||
type: "switch",
|
||||
id: "color_scheme.switch_theme",
|
||||
description: env._t("Dark Mode"),
|
||||
callback: () => {
|
||||
const cookie = getCookie("color_scheme");
|
||||
const theme = cookie === "dark" ? "light" : "dark";
|
||||
setCookie("color_scheme", theme);
|
||||
browser.location.reload();
|
||||
},
|
||||
isChecked: getCookie("color_scheme") == "dark" ? true : false,
|
||||
sequence: 30,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { switchColorSchemeItem } from "./color_scheme_menu_items";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
const userMenuRegistry = registry.category("user_menuitems");
|
||||
|
||||
const colorThemeService = {
|
||||
start() {
|
||||
userMenuRegistry.add("color_scheme.switch", switchColorSchemeItem);
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("color_scheme", colorThemeService);
|
||||
@@ -0,0 +1,27 @@
|
||||
// = DataRange Picker
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_web_client {
|
||||
// Since the component's design is defined in assets_common, scope variables
|
||||
// declarations within `o_web_client` selector.
|
||||
|
||||
.daterangepicker {
|
||||
--daterangepicker-border: #{$dropdown-border-width} solid #{$dropdown-border-color};
|
||||
--daterangepicker-border-radius: #{$dropdown-border-radius};
|
||||
--daterangepicker-bg: #{$table-bg};
|
||||
--daterangepicker-color: #{$o-gray-900};
|
||||
--daterangepicker-box-shadow: #{$dropdown-box-shadow};
|
||||
|
||||
--daterangepicker__table-bg: #{$table-bg};
|
||||
--daterangepicker__thead-bg: #{$o-gray-100};
|
||||
|
||||
--daterangepicker__cell-border-color: #{$table-border-color};
|
||||
--daterangepicker__cell-bg--hover: #{$o-gray-400};
|
||||
--daterangepicker__cell-bg--off: #{$o-gray-400};
|
||||
|
||||
--daterangepicker__select-bg: #{$form-select-bg};
|
||||
--daterangepicker__select-border-color: #{$input-border-color};
|
||||
--daterangepicker__select-color: #{$form-select-color};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// = Home Menu
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_web_client {
|
||||
// Since the component's design is defined in assets_common, scope variables
|
||||
// declarations within `o_web_client` selector.
|
||||
|
||||
.o_datepicker, .datepicker, .bootstrap-datetimepicker-widget {
|
||||
--bs-datetimepicker-btn-hover-bg: #{$o-gray-400};
|
||||
}
|
||||
|
||||
.daterangepicker {
|
||||
--daterangerpicker__arrow-background-color: #{$o-view-background-color};
|
||||
--daterangerpicker__arrow-border-color: #{$border-color};
|
||||
}
|
||||
|
||||
.bootstrap-datetimepicker-widget {
|
||||
--bs-datetimepicker-secondary-border-color-rgba: #{$o-gray-300};
|
||||
--bs-datetimepicker-primary-border-color: #{$o-gray-300};
|
||||
--datepicker-border-color: #{$border-color};
|
||||
}
|
||||
|
||||
.bootstrap-datetimepicker-widget .datepicker table {
|
||||
--datepicker-color: #{map-get($grays, '900')};
|
||||
--datepicker__thead-bg: #{map-get($grays, '100')};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// = Domain Selector
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_domain_debug_container {
|
||||
--DomainDebugContainer-background-colomap-get: #{$o-gray-100};
|
||||
}
|
||||
@@ -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 <= 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);
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="DatabaseExpirationPanel" owl="1">
|
||||
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
310
web_enterprise/static/src/webclient/home_menu/home_menu.js
Normal file
310
web_enterprise/static/src/webclient/home_menu/home_menu.js
Normal 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";
|
||||
114
web_enterprise/static/src/webclient/home_menu/home_menu.scss
Normal file
114
web_enterprise/static/src/webclient/home_menu/home_menu.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
46
web_enterprise/static/src/webclient/home_menu/home_menu.xml
Normal file
46
web_enterprise/static/src/webclient/home_menu/home_menu.xml
Normal 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 <= 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+'&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>
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,9 @@
|
||||
// = Navbar
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_main_navbar {
|
||||
--o-navbar-badge-bg: #{$primary};
|
||||
--o-navbar-badge-color: #{$white};
|
||||
--o-navbar-badge-text-shadow: none;
|
||||
}
|
||||
47
web_enterprise/static/src/webclient/navbar/navbar.js
Normal file
47
web_enterprise/static/src/webclient/navbar/navbar.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { NavBar } from "@web/webclient/navbar/navbar";
|
||||
import { useService, useBus } from "@web/core/utils/hooks";
|
||||
|
||||
const { useEffect, useRef } = owl;
|
||||
|
||||
export class EnterpriseNavBar extends NavBar {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.hm = useService("home_menu");
|
||||
this.menuAppsRef = useRef("menuApps");
|
||||
this.navRef = useRef("nav");
|
||||
useBus(this.env.bus, "HOME-MENU:TOGGLED", () => this._updateMenuAppsIcon());
|
||||
useEffect(() => this._updateMenuAppsIcon());
|
||||
}
|
||||
get hasBackgroundAction() {
|
||||
return this.hm.hasBackgroundAction;
|
||||
}
|
||||
get isInApp() {
|
||||
return !this.hm.hasHomeMenu;
|
||||
}
|
||||
_updateMenuAppsIcon() {
|
||||
const menuAppsEl = this.menuAppsRef.el;
|
||||
menuAppsEl.classList.toggle("o_hidden", !this.isInApp && !this.hasBackgroundAction);
|
||||
menuAppsEl.classList.toggle(
|
||||
"o_menu_toggle_back",
|
||||
!this.isInApp && this.hasBackgroundAction
|
||||
);
|
||||
const { _t } = this.env;
|
||||
const title =
|
||||
!this.isInApp && this.hasBackgroundAction ? _t("Previous view") : _t("Home menu");
|
||||
menuAppsEl.title = title;
|
||||
menuAppsEl.ariaLabel = title;
|
||||
|
||||
const menuBrand = this.navRef.el.querySelector(".o_menu_brand");
|
||||
if (menuBrand) {
|
||||
menuBrand.classList.toggle("o_hidden", !this.isInApp);
|
||||
}
|
||||
|
||||
const appSubMenus = this.appSubMenus.el;
|
||||
if (appSubMenus) {
|
||||
appSubMenus.classList.toggle("o_hidden", !this.isInApp);
|
||||
}
|
||||
}
|
||||
}
|
||||
EnterpriseNavBar.template = "web_enterprise.EnterpriseNavBar";
|
||||
63
web_enterprise/static/src/webclient/navbar/navbar.scss
Normal file
63
web_enterprise/static/src/webclient/navbar/navbar.scss
Normal file
@@ -0,0 +1,63 @@
|
||||
// = Main Navbar
|
||||
// ============================================================================
|
||||
.o_main_navbar {
|
||||
.o_menu_toggle {
|
||||
@extend %-main-navbar-entry-base;
|
||||
@extend %-main-navbar-entry-spacing;
|
||||
color: $o-navbar-entry-color;
|
||||
|
||||
rect, g {
|
||||
transform-origin: 0 50%;
|
||||
}
|
||||
|
||||
// Define a local mixin to handle the toggle state
|
||||
// --------------------------------------------------------------------
|
||||
@mixin o_main_navbar_toggler_toggled() {
|
||||
rect {
|
||||
width: 6px;
|
||||
height: 3px;
|
||||
|
||||
&:first-child {
|
||||
transform: translate(12%,0);
|
||||
rx: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#o_menu_toggle_row_0 {
|
||||
transform: scale3d(.5, 1, 1) translate(0, 45%) skewY(-22deg);
|
||||
|
||||
+ g rect {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#o_menu_toggle_row_2 {
|
||||
transform: scale3d(.5, 1, 1) translate(0, -37%) skewY(22deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.o_menu_toggle_back {
|
||||
@include o_main_navbar_toggler_toggled();
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
// Animate on large screen without 'reduced-motion' only.
|
||||
// --------------------------------------------------------------------
|
||||
@include media-breakpoint-up(lg) {
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
&:hover {
|
||||
@include o_main_navbar_toggler_toggled();
|
||||
}
|
||||
|
||||
&, g {
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
rect {
|
||||
transition: all .1s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// = Navbar Variables
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
$o-navbar-border-bottom: 1px solid $o-border-color !default;
|
||||
|
||||
$o-navbar-background: $o-view-background-color!default;
|
||||
$o-navbar-entry-color: $o-gray-900 !default;
|
||||
|
||||
$o-navbar-home-menu-badge-bg: $o-enterprise-primary-color !default;
|
||||
@@ -0,0 +1,9 @@
|
||||
// = Enterprise Main Navbar Variables
|
||||
// ============================================================================
|
||||
$o-navbar-border-bottom: 0 !default;
|
||||
|
||||
$o-navbar-background: linear-gradient(45deg, $o-brand-odoo, adjust-color($o-brand-odoo, $saturation: -8%, $lightness: -4%)) !default;
|
||||
|
||||
$o-navbar-badge-size-adjust: 0 !default;
|
||||
$o-navbar-badge-bg: $o-enterprise-primary-color-light !default;
|
||||
$o-navbar-home-menu-badge-bg: $o-brand-primary !default;
|
||||
19
web_enterprise/static/src/webclient/navbar/navbar.xml
Normal file
19
web_enterprise/static/src/webclient/navbar/navbar.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web_enterprise.EnterpriseNavBar" t-inherit="web.NavBar" t-inherit-mode="primary">
|
||||
<xpath expr="//nav" position="attributes">
|
||||
<attribute name="t-ref">nav</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-call='web.NavBar.AppsMenu']" position="replace">
|
||||
<a href="#" class="o_menu_toggle" accesskey="h" t-ref="menuApps" t-on-click.prevent="() => this.hm.toggle()">
|
||||
<svg width="14px" height="14px" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" >
|
||||
<g t-foreach="[0, 5, 10]" t-as="Y" t-att-id="'o_menu_toggle_row_' + Y_index" fill="currentColor" t-key="'o_menu_toggle_row_' + Y_index">
|
||||
<rect t-foreach="[0, 5, 10]" t-as="X" width="4" height="4" t-att-x="X" t-att-y="Y" t-key="'o_menu_toggle_cell_' + X_index"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,47 @@
|
||||
/** @odoo-module */
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { useChildRef, useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, useExternalListener } = owl;
|
||||
|
||||
export class PromoteStudioDialog extends Component {
|
||||
setup() {
|
||||
this.ormService = useService("orm");
|
||||
this.uiService = useService("ui");
|
||||
|
||||
this.modalRef = useChildRef();
|
||||
|
||||
useExternalListener(window, "mousedown", this.onWindowMouseDown);
|
||||
}
|
||||
|
||||
async onClickInstallStudio() {
|
||||
this.disableClick = true;
|
||||
this.uiService.block();
|
||||
const modules = await this.ormService.searchRead(
|
||||
"ir.module.module",
|
||||
[["name", "=", "web_studio"]],
|
||||
["id"]
|
||||
);
|
||||
await this.ormService.call("ir.module.module", "button_immediate_install", [
|
||||
[modules[0].id],
|
||||
]);
|
||||
// on rpc call return, the framework unblocks the page
|
||||
// make sure to keep the page blocked until the reload ends.
|
||||
this.uiService.unblock();
|
||||
browser.localStorage.setItem("openStudioOnReload", "main");
|
||||
browser.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the dialog on outside click.
|
||||
*/
|
||||
onWindowMouseDown(ev) {
|
||||
const dialogContent = this.modalRef.el.querySelector(".modal-content");
|
||||
if (!this.disableClick && !dialogContent.contains(ev.target)) {
|
||||
this.props.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
PromoteStudioDialog.template = "web_enterprise.PromoteStudioDialog";
|
||||
PromoteStudioDialog.components = { Dialog };
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<template>
|
||||
|
||||
<t t-name="web_enterprise.PromoteStudioDialog" owl="1">
|
||||
<Dialog title="title" header="false" modalRef="modalRef">
|
||||
<div class="modal-studio">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" tabindex="-1" t-on-click="props.close"></button>
|
||||
<h2 class="modal-title">Add new fields and much more with <b>Odoo Studio</b></h2>
|
||||
<div class="o_video_embed">
|
||||
<div class="ratio ratio-16x9">
|
||||
<iframe class="embed-responsive-item" t-attf-src="https://www.youtube.com/embed/xCvFZrrQq7k?autoplay=1" frameborder="0" allowfullscreen="true"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-set-slot="footer">
|
||||
<div class="o_install_studio_btn">
|
||||
<button class="btn btn-primary btn-block o_install_studio" t-on-click.stop="onClickInstallStudio"><b>Install Odoo Studio</b></button>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="web.res_config_edition" t-inherit-mode="extension" owl="1">
|
||||
|
||||
<xpath expr="//h3" position="replace">
|
||||
<h3 class="px-0">
|
||||
Odoo <t t-esc="serverVersion"/> (Enterprise Edition)
|
||||
</h3>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//*[@id='license']" position="replace">
|
||||
<a id="license" target="_blank" href="https://github.com/odoo/enterprise/blob/13.0/LICENSE" style="text-decoration: underline;">Odoo Enterprise Edition License V1.0</a>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//h3" position="after">
|
||||
<t t-if="expirationDate">
|
||||
<h5>Database expiration: <t t-esc="expirationDate"/></h5>
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,10 @@
|
||||
// = Settings
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o-settings-form-view .o_base_settings {
|
||||
--settings__tab-bg: #{$o-gray-100};
|
||||
--settings__tab-bg--active: #{$o-gray-300};
|
||||
--settings__tab-color: #{$o-gray-700};
|
||||
--settings__title-bg: #{$o-gray-100};
|
||||
}
|
||||
17
web_enterprise/static/src/webclient/webclient.js
Normal file
17
web_enterprise/static/src/webclient/webclient.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { EnterpriseNavBar } from "./navbar/navbar";
|
||||
|
||||
export class WebClientEnterprise extends WebClient {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.hm = useService("home_menu");
|
||||
useService("enterprise_legacy_service_provider");
|
||||
}
|
||||
_loadDefaultApp() {
|
||||
return this.hm.toggle(true);
|
||||
}
|
||||
}
|
||||
WebClientEnterprise.components = { ...WebClient.components, NavBar: EnterpriseNavBar };
|
||||
Reference in New Issue
Block a user