odoo18/addons/website/static/src/services/website_service.js

300 lines
12 KiB
JavaScript

import { jsToPyLocale } from "@web/core/l10n/utils";
import { _t } from "@web/core/l10n/translation";
import { registry } from '@web/core/registry';
import { user } from "@web/core/user";
import { loadBundle } from "@web/core/assets";
import { ensureJQuery } from "@web/core/ensure_jquery";
import { isVisible } from "@web/core/utils/ui";
import { FullscreenIndication } from '../components/fullscreen_indication/fullscreen_indication';
import { WebsiteLoader } from '../components/website_loader/website_loader';
import { reactive, EventBus } from "@odoo/owl";
const websiteSystrayRegistry = registry.category('website_systray');
export const unslugHtmlDataObject = (repr) => {
const match = repr && repr.match(/(.+)\((\d+),(.*)\)/);
if (!match) {
return null;
}
return {
model: match[1],
id: match[2] | 0,
};
};
const ANONYMOUS_PROCESS_ID = 'ANONYMOUS_PROCESS_ID';
export const websiteService = {
dependencies: ['orm', 'action', 'hotkey'],
async start(env, { orm, action, hotkey }) {
let websites = [];
let currentWebsiteId;
let currentMetadata = {};
let fullscreen;
let pageDocument;
let contentWindow;
let lastUrl;
let websiteRootInstance;
let isRestrictedEditor;
let isDesigner;
let hasMultiWebsites;
let actionJsId;
let blockingProcesses = [];
let modelNamesProm = null;
const modelNames = {};
let invalidateSnippetCache = false;
let lastWebsiteId = null;
const context = reactive({
showNewContentModal: false,
showResourceEditor: false,
edition: false,
isPublicRootReady: false,
snippetsLoaded: false,
isMobile: false,
});
const bus = new EventBus();
hotkey.add("escape", () => {
// Toggle fullscreen mode when pressing escape.
if (
(!currentWebsiteId && !fullscreen)
|| (pageDocument && isVisible(pageDocument.querySelector(".modal")))
) {
// Only allow to use this feature while on the website app, or
// while it is already fullscreen (in case you left the website
// app in fullscreen mode, thanks to CTRL-K), or if a modal
// is open within the preview and could be closed with escape.
return;
}
fullscreen = !fullscreen;
document.body.classList.toggle('o_website_fullscreen', fullscreen);
bus.trigger(fullscreen ? 'FULLSCREEN-INDICATION-SHOW' : 'FULLSCREEN-INDICATION-HIDE');
}, { global: true });
registry.category('main_components').add('FullscreenIndication', {
Component: FullscreenIndication,
props: { bus },
});
registry.category('main_components').add('WebsiteLoader', {
Component: WebsiteLoader,
props: { bus },
});
return {
set currentWebsiteId(id) {
if (id && id !== lastWebsiteId) {
invalidateSnippetCache = true;
lastWebsiteId = id;
}
currentWebsiteId = id;
websiteSystrayRegistry.trigger('EDIT-WEBSITE');
},
/**
* This represents the current website being edited in the
* WebsitePreview client action. Multiple components based their
* visibility on this value, which is falsy if the client action is
* not displayed.
*/
get currentWebsite() {
const currentWebsite = websites.find(w => w.id === currentWebsiteId);
if (currentWebsite) {
currentWebsite.metadata = currentMetadata;
}
return currentWebsite;
},
get websites() {
return websites;
},
get context() {
return context;
},
get bus() {
return bus;
},
set pageDocument(document) {
pageDocument = document;
if (!document) {
currentMetadata = {};
contentWindow = null;
return;
}
const { dataset } = document.documentElement;
// XML files have no dataset on Firefox, and an empty one on
// Chrome.
const isWebsitePage = dataset && dataset.websiteId;
if (!isWebsitePage) {
currentMetadata = {};
} else {
const { mainObject, seoObject, isPublished, canOptimizeSeo, canPublish, editableInBackend, translatable, viewXmlid, defaultLangName, langName } = dataset;
// We ignore multiple menus with the same `content_menu_id`
// in the DOM, since it's possible to have different
// templates for the same content menu (E.g. used for a
// different desktop / mobile UI).
const contentMenus = [
...new Map(
[...document.querySelectorAll("[data-content_menu_id]")].map(
(menuEl) => [
menuEl.dataset.content_menu_id,
[menuEl.dataset.menu_name, menuEl.dataset.content_menu_id],
]
)
).values(),
];
currentMetadata = {
path: document.location.href,
mainObject: unslugHtmlDataObject(mainObject),
seoObject: unslugHtmlDataObject(seoObject),
isPublished: isPublished === 'True',
canOptimizeSeo: canOptimizeSeo === 'True',
canPublish: canPublish === 'True',
editableInBackend: editableInBackend === 'True',
title: document.title,
translatable: !!translatable,
contentMenus,
// TODO: Find a better way to figure out if
// a page is editable or not. For now, we use
// the editable selector because it's the common
// denominator of editable pages.
editable: !!document.getElementById('wrapwrap'),
viewXmlid: viewXmlid,
lang: jsToPyLocale(document.documentElement.getAttribute("lang")),
defaultLangName: defaultLangName,
langName: langName,
direction: document.documentElement.querySelector('#wrapwrap.o_rtl') ? 'rtl' : 'ltr',
};
}
contentWindow = document.defaultView;
websiteSystrayRegistry.trigger('CONTENT-UPDATED');
},
get pageDocument() {
return pageDocument;
},
get contentWindow() {
return contentWindow;
},
get websiteRootInstance() {
return websiteRootInstance;
},
set websiteRootInstance(rootInstance) {
websiteRootInstance = rootInstance;
context.isPublicRootReady = !!rootInstance;
},
set lastUrl(url) {
lastUrl = url;
},
get lastUrl() {
return lastUrl;
},
get isRestrictedEditor() {
return isRestrictedEditor === true;
},
get isDesigner() {
return isDesigner === true;
},
get hasMultiWebsites() {
return hasMultiWebsites === true;
},
get actionJsId() {
return actionJsId;
},
set actionJsId(jsId) {
actionJsId = jsId;
},
get invalidateSnippetCache() {
return invalidateSnippetCache;
},
set invalidateSnippetCache(value) {
invalidateSnippetCache = value;
},
goToWebsite({ websiteId, path, edition, translation, lang } = {}) {
this.websiteRootInstance = undefined;
if (lang) {
invalidateSnippetCache = true;
path = `/website/lang/${encodeURIComponent(lang)}?r=${encodeURIComponent(path)}`;
}
action.doAction('website.website_preview', {
clearBreadcrumbs: true,
additionalContext: {
params: {
website_id: websiteId || currentWebsiteId,
path: path || (contentWindow && contentWindow.location.href) || '/',
enable_editor: edition,
edit_translations: translation,
},
},
});
},
async fetchUserGroups() {
// Fetch user groups, before fetching the websites.
[isRestrictedEditor, isDesigner, hasMultiWebsites] = await Promise.all([
user.hasGroup('website.group_website_restricted_editor'),
user.hasGroup('website.group_website_designer'),
user.hasGroup('website.group_multi_website'),
]);
},
async fetchWebsites() {
websites = [...(await orm.searchRead('website', [], ['domain', 'id', 'name']))];
},
async loadWysiwyg() {
await ensureJQuery();
await loadBundle('website.backend_assets_all_wysiwyg');
},
blockPreview(showLoader, processId) {
if (!blockingProcesses.length) {
bus.trigger('BLOCK', { showLoader });
}
blockingProcesses.push(processId || ANONYMOUS_PROCESS_ID);
},
unblockPreview(processId) {
const processIndex = blockingProcesses.indexOf(processId || ANONYMOUS_PROCESS_ID);
if (processIndex > -1) {
blockingProcesses.splice(processIndex, 1);
if (blockingProcesses.length === 0) {
bus.trigger('UNBLOCK');
}
}
},
showLoader(props) {
bus.trigger('SHOW-WEBSITE-LOADER', props);
},
hideLoader() {
bus.trigger('HIDE-WEBSITE-LOADER');
},
prepareOutLoader() {
bus.trigger("PREPARE-OUT-WEBSITE-LOADER");
},
/**
* Returns the (translated) "functional" name of a model
* (_description) given its "technical" name (_name).
*
* @param {string} [model]
* @returns {string}
*/
async getUserModelName(model = this.currentWebsite.metadata.mainObject.model) {
if (!modelNamesProm) {
// FIXME the `get_available_models` is to be removed/changed
// in a near future. This code is to be adapted, probably
// with another helper to map a model functional name from
// its technical map without the need of the right access
// rights (which is why I cannot use search_read here).
modelNamesProm = orm.call("ir.model", "get_available_models")
.then(modelsData => {
for (const modelData of modelsData) {
modelNames[modelData['model']] = modelData['display_name'];
}
})
// Precaution in case the util is simply removed without
// adapting this method: not critical, we can restore
// later and use the fallback until the fix is made.
.catch(() => {});
}
await modelNamesProm;
return modelNames[model] || _t("Data");
},
};
},
};
registry.category('services').add('website', websiteService);