Files
jikimo_sf/web_studio/static/src/studio_service.js
2023-04-14 17:42:23 +08:00

371 lines
14 KiB
JavaScript

/** @odoo-module **/
import { registry } from "@web/core/registry";
import { browser } from "@web/core/browser/browser";
import { getCookie, setCookie } from "web.utils.cookies";
import { delay } from "web.concurrency";
import legacyBus from "web_studio.bus";
import { resetViewCompilerCache } from "@web/views/view_compiler";
const { EventBus } = owl;
const URL_VIEW_KEY = "_view_type";
const URL_ACTION_KEY = "_action";
const URL_TAB_KEY = "_tab";
const URL_MODE_KEY = "mode";
export const MODES = {
EDITOR: "editor",
HOME_MENU: "home_menu",
APP_CREATOR: "app_creator",
};
export class NotEditableActionError extends Error {}
export const SUPPORTED_VIEW_TYPES = [
"activity",
"calendar",
"cohort",
"dashboard",
"form",
"gantt",
"graph",
"kanban",
"list",
"map",
"pivot",
"search",
];
export const studioService = {
dependencies: ["action", "home_menu", "router", "user"],
async start(env, { user }) {
function _getCurrentAction() {
const currentController = env.services.action.currentController;
return currentController ? currentController.action : null;
}
function _isStudioEditable(action) {
if (action.type === "ir.actions.client") {
// home_menu is somehow customizable (app creator)
return action.tag === "menu" ? true : false;
}
if (action.type === "ir.actions.act_window" && action.xml_id) {
if (
action.res_model.indexOf("settings") > -1 &&
action.res_model.indexOf("x_") !== 0
) {
return false; // settings views aren't editable; but x_settings is
}
if (action.res_model === "board.board") {
return false; // dashboard isn't editable
}
if (action.view_mode === "qweb") {
// Apparently there is a QWebView that allows to
// implement ActWindow actions that are completely custom
// but not editable by studio
return false;
}
if (action.res_model === "knowledge.article") {
// The knowledge form view is very specific and custom, it doesn't make sense
// to edit it. Editing the list and kanban is more debatable, but for simplicity's sake
// we set them to not editable too.
return false;
}
return action.res_model ? true : false;
}
return false;
}
function isViewEditable(view) {
return view && SUPPORTED_VIEW_TYPES.includes(view);
}
const bus = new EventBus();
let inStudio = false;
const state = {
studioMode: null,
editedViewType: null,
editedAction: null,
editedControllerState: null,
editorTab: "views",
x2mEditorPath: [],
editedReport: null,
};
async function _loadParamsFromURL() {
const currentHash = env.services.router.current.hash;
user.removeFromContext("studio");
if (currentHash.action === "studio") {
user.updateContext({ studio: 1 });
state.studioMode = currentHash[URL_MODE_KEY];
state.editedViewType = currentHash[URL_VIEW_KEY] || null;
state.editorTab = currentHash[URL_TAB_KEY] || null;
const editedActionId = currentHash[URL_ACTION_KEY];
const additionalContext = {};
if (state.studioMode === MODES.EDITOR) {
if (currentHash.active_id) {
additionalContext.active_id = currentHash.active_id;
}
if (editedActionId) {
state.editedAction = await env.services.action.loadAction(
editedActionId,
additionalContext
);
} else {
state.editedAction = null;
}
}
if (!state.editedAction || !_isStudioEditable(state.editedAction)) {
state.studioMode = state.studioMode || MODES.HOME_MENU;
state.editedAction = null;
state.editedViewType = null;
state.editorTab = null;
}
}
}
let studioProm = _loadParamsFromURL();
env.bus.on("ROUTE_CHANGE", null, async () => {
studioProm = _loadParamsFromURL();
});
async function _openStudio(targetMode, action = false, viewType = false) {
if (!targetMode) {
throw new Error("mode is mandatory");
}
const options = {};
if (targetMode === MODES.EDITOR) {
let controllerState;
if (!action) {
// systray open
const currentController = env.services.action.currentController;
if (currentController) {
action = currentController.action;
viewType = currentController.view.type;
controllerState = Object.assign({}, currentController.getLocalState());
const { resIds } = currentController.getGlobalState() || {};
if (resIds) {
controllerState.resIds = resIds;
}
}
}
if (!_isStudioEditable(action)) {
throw new NotEditableActionError();
}
if (action !== state.editedAction) {
options.clearBreadcrumbs = true;
}
state.editedAction = action;
const vtype = viewType || action.views[0][1]; // fallback on first view of action
state.editedViewType = isViewEditable(vtype) ? vtype : null;
state.editorTab = "views";
state.editedControllerState = controllerState || {};
}
if (inStudio) {
options.stackPosition = "replaceCurrentAction";
}
state.studioMode = targetMode;
user.updateContext({ studio: 1 });
// LPE: we don't manage errors during do action.....
const res = await env.services.action.doAction("studio", options);
// force color_scheme light
if (getCookie("color_scheme") === "dark") {
// ensure studio is fully loaded
await delay(0);
setCookie("color_scheme", "light");
browser.location.reload();
}
return res;
}
async function open(mode = false, actionId = false) {
if (!mode && inStudio) {
throw new Error("can't already be in studio");
}
if (!mode) {
mode = env.services.home_menu.hasHomeMenu ? MODES.HOME_MENU : MODES.EDITOR;
}
let action;
if (actionId) {
action = await env.services.action.loadAction(actionId);
}
resetViewCompilerCache();
return _openStudio(mode, action);
}
async function leave() {
if (!inStudio) {
throw new Error("leave when not in studio???");
}
resetViewCompilerCache();
env.bus.trigger("CLEAR-CACHES");
const options = {
stackPosition: "replacePreviousAction", // If target is menu, then replaceCurrent, see comment above why we cannot do this
};
let actionId;
if (state.studioMode === MODES.EDITOR) {
actionId = state.editedAction.id;
options.additionalContext = state.editedAction.context;
options.viewType = state.editedViewType;
if (state.editedControllerState) {
options.props = { resId: state.editedControllerState.currentId };
}
} else {
actionId = "menu";
}
user.removeFromContext("studio");
await env.services.action.doAction(actionId, options);
// force rendering of the main navbar to allow adaptation of the size
env.bus.trigger("MENUS:APP-CHANGED");
state.studioMode = null;
state.x2mEditorPath = [];
}
async function reload(params = {}) {
resetViewCompilerCache();
env.bus.trigger("CLEAR-CACHES");
const action = await env.services.action.loadAction(state.editedAction.id);
setParams({ action, ...params });
}
function toggleHomeMenu() {
if (!inStudio) {
throw new Error("is it possible?");
}
let targetMode;
if (state.studioMode === MODES.APP_CREATOR || state.studioMode === MODES.EDITOR) {
targetMode = MODES.HOME_MENU;
} else {
targetMode = MODES.EDITOR;
}
const action = targetMode === MODES.EDITOR ? state.editedAction : null;
if (targetMode === MODES.EDITOR && !action) {
throw new Error("this button should not be clickable/visible");
}
const viewType = targetMode === MODES.EDITOR ? state.editedViewType : null;
return _openStudio(targetMode, action, viewType);
}
function pushState() {
const hash = { action: "studio" };
hash[URL_MODE_KEY] = state.studioMode;
hash[URL_ACTION_KEY] = undefined;
hash[URL_VIEW_KEY] = undefined;
hash[URL_TAB_KEY] = undefined;
if (state.studioMode === MODES.EDITOR) {
hash[URL_ACTION_KEY] = JSON.stringify(state.editedAction.id);
hash[URL_VIEW_KEY] = state.editedViewType || undefined;
hash[URL_TAB_KEY] = state.editorTab;
}
if (
state.editedAction &&
state.editedAction.context &&
state.editedAction.context.active_id
) {
hash.active_id = state.editedAction.context.active_id;
}
env.services.router.pushState(hash, { replace: true });
}
function setParams(params = {}) {
if ("mode" in params) {
state.studioMode = params.mode;
}
if ("viewType" in params) {
state.editedViewType = params.viewType || null;
state.x2mEditorPath = [];
}
if ("action" in params) {
state.editedAction = params.action || null;
}
if ("editorTab" in params) {
state.editorTab = params.editorTab;
state.x2mEditorPath = [];
if (!("viewType" in params)) {
// clean me
state.editedViewType = null;
}
if (!("editedReport" in params)) {
state.editedReport = null;
}
}
if ("editedReport" in params) {
state.editedReport = params.editedReport;
}
if ("x2mEditorPath" in params) {
state.x2mEditorPath = params.x2mEditorPath;
}
if (state.editorTab !== "reports") {
state.editedReport = null;
}
bus.trigger("UPDATE");
}
legacyBus.on("STUDIO_ENTER_X2M", null, (newX2mPath) => {
const x2mEditorPath = state.x2mEditorPath.slice();
x2mEditorPath.push(newX2mPath);
setParams({ x2mEditorPath });
});
env.bus.on("ACTION_MANAGER:UI-UPDATED", null, (mode) => {
if (mode === "new") {
return;
}
const action = _getCurrentAction();
inStudio = action.tag === "studio";
});
const legacyBusTrigger = legacyBus.trigger.bind(legacyBus);
const busTrigger = bus.trigger.bind(bus);
function mappedTrigger(...args) {
legacyBusTrigger(...args);
busTrigger(...args);
}
legacyBus.trigger = mappedTrigger;
bus.trigger = mappedTrigger;
return {
MODES,
bus,
isStudioEditable() {
const action = _getCurrentAction();
return action ? _isStudioEditable(action) : false;
},
open,
reload,
pushState,
leave,
toggleHomeMenu,
setParams,
get ready() {
return studioProm;
},
get mode() {
return state.studioMode;
},
get editedAction() {
return state.editedAction;
},
get editedViewType() {
return state.editedViewType;
},
get editedControllerState() {
return state.editedControllerState;
},
get editedReport() {
return state.editedReport;
},
get editorTab() {
return state.editorTab;
},
get x2mEditorPath() {
return state.x2mEditorPath;
},
};
},
};
registry.category("services").add("studio", studioService);