300 lines
12 KiB
JavaScript
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);
|