From 06a3edba3f626f4466019c0268c9fa8b7133e100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20=28blse=29?= Date: Wed, 14 May 2025 16:29:23 +0200 Subject: [PATCH] Fix setting/removing image in covers from /blog page --- .../static/src/core/save_plugin.js | 80 ++++----- .../options/cover_properties_option.js | 12 ++ .../options/cover_properties_option.xml | 8 +- .../options/cover_properties_option_plugin.js | 161 ++++++++++-------- .../blog_cover_properties_option.js | 16 +- .../blog_cover_properties_option.xml | 25 --- 6 files changed, 150 insertions(+), 152 deletions(-) delete mode 100644 addons/website_blog/static/src/website_builder/blog_cover_properties_option.xml diff --git a/addons/html_builder/static/src/core/save_plugin.js b/addons/html_builder/static/src/core/save_plugin.js index 58265fe20e978..29322c068aebe 100644 --- a/addons/html_builder/static/src/core/save_plugin.js +++ b/addons/html_builder/static/src/core/save_plugin.js @@ -2,12 +2,6 @@ import { Plugin } from "@html_editor/plugin"; import { rpc } from "@web/core/network/rpc"; import { registry } from "@web/core/registry"; -const oeStructureSelector = "#wrapwrap .oe_structure[data-oe-xpath][data-oe-id]"; -const oeFieldSelector = "#wrapwrap [data-oe-field]:not([data-oe-sanitize-prevent-edition])"; -const OE_RECORD_COVER_SELECTOR = "#wrapwrap .o_record_cover_container[data-res-model]"; -const oeCoverSelector = `#wrapwrap .s_cover[data-res-model], ${OE_RECORD_COVER_SELECTOR}`; -const SAVABLE_SELECTOR = `${oeStructureSelector}, ${oeFieldSelector}, ${oeCoverSelector}`; - export class SavePlugin extends Plugin { static id = "savePlugin"; static shared = ["save"]; @@ -16,6 +10,11 @@ export class SavePlugin extends Plugin { handleNewRecords: this.handleMutations.bind(this), start_edition_handlers: this.startObserving.bind(this), // Resource definitions: + savable_selectors: [ + "#wrapwrap .oe_structure[data-oe-xpath][data-oe-id]", + "#wrapwrap [data-oe-field]:not([data-oe-sanitize-prevent-edition])", + "#wrapwrap .s_cover[data-res-model]", + ], before_save_handlers: [ // async () => { // called at the very beginning of the save process @@ -27,6 +26,12 @@ export class SavePlugin extends Plugin { // root is the clone of a node that was o_dirty // } ], + save_element_handlers: [ + // async (el) => { + // called when saving an element (in parallel to saving the view) + // } + this.saveView.bind(this), + ], save_handlers: [ // async () => { // called at the very end of the save process @@ -37,6 +42,7 @@ export class SavePlugin extends Plugin { setup() { this.canObserve = false; + this.savableSelector = this.getResource("savable_selectors").join(", "); } async save() { @@ -58,7 +64,13 @@ export class SavePlugin extends Plugin { if (this.config.isTranslation) { await this.saveTranslationElement(cleanedEl); } else { - await this.saveView(cleanedEl); + const proms = this.getResource("save_element_handlers") + .map((h) => h(cleanedEl)) + .filter(Boolean); + if (!proms.length) { + console.warning("no save_element_handlers for dirty element", cleanedEl); + } + await Promise.all(proms); } }); // used to track dirty out of the editable scope, like header, footer or wrapwrap @@ -66,43 +78,15 @@ export class SavePlugin extends Plugin { await Promise.all(saveProms.concat(willSaves)); } - async saveCoverProperties(el) { - const resModel = el.dataset.resModel; - const resID = Number(el.dataset.resId); - - if (!resModel || !resID) { - throw new Error("There should be a model and id associated to the cover"); - } - - const coverProps = { - "background-image": el.dataset.bgImage, - background_color_class: el.dataset.bgColorClass, - background_color_style: el.dataset.bgColorStyle, - opacity: el.dataset.filterValue, - resize_class: el.dataset.coverClass, - text_align_class: el.dataset.textAlignClass, - }; - - return this.services.orm.write(resModel, [resID], { - cover_properties: JSON.stringify(coverProps), - }); - } - /** * Saves one (dirty) element of the page. * * @param {HTMLElement} el - the element to save. */ - async saveView(el) { - const proms = []; + saveView(el) { const viewID = Number(el.dataset["oeId"]); - - if (el.classList.contains("o_record_cover_container")) { - proms.push(this.saveCoverProperties(el)); - - if (!viewID) { - return Promise.all(proms); - } + if (!viewID) { + return; } const context = { @@ -113,19 +97,12 @@ export class SavePlugin extends Plugin { delay_translations: false, }; - proms.push( - this.services.orm.call( - "ir.ui.view", - "save", - [ - viewID, - el.outerHTML, - (!el.dataset["oeExpression"] && el.dataset["oeXpath"]) || null, - ], - { context } - ) + return this.services.orm.call( + "ir.ui.view", + "save", + [viewID, el.outerHTML, (!el.dataset["oeExpression"] && el.dataset["oeXpath"]) || null], + { context } ); - return Promise.all(proms); } /** @@ -211,7 +188,7 @@ export class SavePlugin extends Plugin { if (!targetEl) { continue; } - const savableEl = targetEl.closest(SAVABLE_SELECTOR); + const savableEl = targetEl.closest(this.savableSelector); if ( !savableEl || savableEl.classList.contains("o_dirty") || @@ -223,4 +200,5 @@ export class SavePlugin extends Plugin { } } } + registry.category("translation-plugins").add(SavePlugin.id, SavePlugin); diff --git a/addons/website/static/src/builder/plugins/options/cover_properties_option.js b/addons/website/static/src/builder/plugins/options/cover_properties_option.js index 2fbea0f28e1d8..41919b402b680 100644 --- a/addons/website/static/src/builder/plugins/options/cover_properties_option.js +++ b/addons/website/static/src/builder/plugins/options/cover_properties_option.js @@ -1,4 +1,5 @@ import { BaseOptionComponent, useDomState } from "@html_builder/core/utils"; +import { _t } from "@web/core/l10n/translation"; export class CoverPropertiesOption extends BaseOptionComponent { static template = "html_builder.CoverPropertiesOption"; @@ -10,5 +11,16 @@ export class CoverPropertiesOption extends BaseOptionComponent { useTextAlign: editingElement.dataset.use_text_align === "True", useSize: editingElement.dataset.use_size === "True", })); + this.coverSizeClasses = Object.keys(coverSizeClassLabels); + } + + coverSizeLabel(className) { + return coverSizeClassLabels[className]; } } + +export const coverSizeClassLabels = { + o_full_screen_height: _t("Full Screen"), + o_half_screen_height: _t("Half Screen"), + cover_auto: _t("Fit text"), +}; diff --git a/addons/website/static/src/builder/plugins/options/cover_properties_option.xml b/addons/website/static/src/builder/plugins/options/cover_properties_option.xml index ca57c859177b7..068d8f8ce441a 100644 --- a/addons/website/static/src/builder/plugins/options/cover_properties_option.xml +++ b/addons/website/static/src/builder/plugins/options/cover_properties_option.xml @@ -2,7 +2,7 @@ - + @@ -14,9 +14,7 @@ - Full Screen - Half Screen - Fit text + @@ -36,7 +34,7 @@ Right - + diff --git a/addons/website/static/src/builder/plugins/options/cover_properties_option_plugin.js b/addons/website/static/src/builder/plugins/options/cover_properties_option_plugin.js index 4d56af26652a6..1b8e109d89f8b 100644 --- a/addons/website/static/src/builder/plugins/options/cover_properties_option_plugin.js +++ b/addons/website/static/src/builder/plugins/options/cover_properties_option_plugin.js @@ -6,6 +6,7 @@ import { loadImageInfo } from "@html_editor/utils/image_processing"; import { rpc } from "@web/core/network/rpc"; import { withSequence } from "@html_editor/utils/resource"; import { COVER_PROPERTIES } from "@website/builder/option_sequence"; +import { coverSizeClassLabels } from "./cover_properties_option"; class CoverPropertiesOptionPlugin extends Plugin { static id = "coverPropertiesOption"; @@ -28,9 +29,11 @@ class CoverPropertiesOptionPlugin extends Plugin { }, apply: this.applyBackgroundImage.bind(this), }, + markCoverPropertiesToBeSaved: { apply: this.markCoverPropertiesToBeSaved.bind(this) }, }, + savable_selectors: "#wrapwrap .o_record_cover_container[data-res-model]", before_save_handlers: this.savePendingBackgroundImage.bind(this), - clean_for_save_handlers: this.saveToDataset.bind(this), + save_element_handlers: this.saveCoverProperties.bind(this), }; loadBackgroundImage({ params: { mainParam: setBackground } }) { @@ -87,84 +90,106 @@ class CoverPropertiesOptionPlugin extends Plugin { params: { mainParam: "background-image" }, value: imageSrc ? `url('${imageSrc}')` : "", }); + + this.markCoverPropertiesToBeSaved({ editingElement }); + } + + markCoverPropertiesToBeSaved({ editingElement }) { + editingElement.closest(".o_record_cover_container").dataset.coverPropertiesToBeSaved = true; } async savePendingBackgroundImage(editableEl = this.editable) { - const coverEl = editableEl.querySelector(".o_record_cover_container"); - const bgEl = coverEl?.querySelector(".o_record_cover_image"); - const bgImage = bgEl?.style.backgroundImage; - if (bgImage && bgEl.classList.contains("o_b64_cover_image_to_save")) { - const resModel = coverEl.dataset.resModel; - const resID = Number(coverEl.dataset.resId); - if (!resModel || !resID) { - throw new Error("There should be a model and id associated to the cover"); - } + for (const coverEl of editableEl.querySelectorAll(".o_record_cover_container")) { + const bgEl = coverEl.querySelector(".o_record_cover_image"); + const bgImage = bgEl?.style.backgroundImage; + if (bgImage && bgEl.classList.contains("o_b64_cover_image_to_save")) { + const resModel = coverEl.dataset.resModel; + const resID = Number(coverEl.dataset.resId); + if (!resModel || !resID) { + throw new Error("There should be a model and id associated to the cover"); + } - // Checks if the image is in base64 format for RPC call. Relying - // only on the presence of the class "o_b64_cover_image_to_save" is not - // robust enough. - const groups = bgImage.match( - /url\("data:(?.*);base64,(?.*)"\)/ - )?.groups; - if (groups?.imageData) { - const modelName = await this.services.website.getUserModelName(resModel); - const recordNameEl = bgEl - .closest("body") - .querySelector( - `[data-oe-model="${resModel}"][data-oe-id="${resID}"][data-oe-field="name"]` - ); - const recordName = recordNameEl - ? `'${recordNameEl.textContent.replaceAll("/", "")}'` - : resID; - const attachment = await rpc("/web_editor/attachment/add_data", { - name: `${modelName} ${recordName} cover image.${groups.mimetype.split("/")[1]}`, - data: groups.imageData, - is_image: true, - res_model: "ir.ui.view", - }); - bgEl.style.backgroundImage = `url(${attachment.image_src})`; + // Checks if the image is in base64 format for RPC call. Relying + // only on the presence of the class "o_b64_cover_image_to_save" is not + // robust enough. + const groups = bgImage.match( + /url\("data:(?.*);base64,(?.*)"\)/ + )?.groups; + if (groups?.imageData) { + const modelName = await this.services.website.getUserModelName(resModel); + const recordNameEl = bgEl + .closest("body") + .querySelector( + `[data-oe-model="${resModel}"][data-oe-id="${resID}"][data-oe-field="name"]` + ); + const recordName = recordNameEl + ? `'${recordNameEl.textContent.replaceAll("/", "")}'` + : resID; + const attachment = await rpc("/web_editor/attachment/add_data", { + name: `${modelName} ${recordName} cover image.${ + groups.mimetype.split("/")[1] + }`, + data: groups.imageData, + is_image: true, + res_model: "ir.ui.view", + }); + bgEl.style.backgroundImage = `url(${attachment.image_src})`; + } + bgEl.classList.remove("o_b64_cover_image_to_save"); } - bgEl.classList.remove("o_b64_cover_image_to_save"); } } - /** - * Updates the cover properties dataset used for saving. - */ - saveToDataset({ root }) { - if (root.matches(".o_record_cover_container")) { - const bg = root.querySelector(".o_record_cover_image")?.style.backgroundImage || ""; - root.dataset.bgImage = bg; - - // TODO: `o_record_has_cover` should be handled using model field, not - // resize_class to avoid all of this. - let coverClass = ["o_full_screen_height", "o_half_screen_height", "cover_auto"].find( - (e) => root.classList.contains(e) - ); - if (bg && bg !== "none") { - coverClass += " o_record_has_cover"; - } - root.dataset.coverClass = coverClass; - - root.dataset.textAlignClass = - ["text-center", "text-end"].find((e) => root.classList.contains(e)) || ""; - - root.dataset.filterValue = - root.querySelector(".o_record_cover_filter")?.style.opacity || 0.0; - - root.dataset.bgColorClass = [...root.classList.values()] - .filter((e) => e.startsWith("bg-") || e.startsWith("o_cc")) - .join(" "); - if (root.style.backgroundImage) { - root.dataset.bgColorStyle = `background-color: rgba(0, 0, 0, 0); background-image: ${root.style.backgroundImage};`; - } else if (root.style.backgroundColor) { - root.dataset.bgColorStyle = `background-color: ${root.style.backgroundColor};`; - } else { - root.dataset.bgColorStyle = ""; - } + saveCoverProperties(el) { + if (!el.dataset.coverPropertiesToBeSaved) { + return; } + delete el.dataset.coverPropertiesToBeSaved; + + const resModel = el.dataset.resModel; + const resID = Number(el.dataset.resId); + + if (!resModel || !resID) { + throw new Error("There should be a model and id associated to the cover"); + } + + return this.services.orm.write(resModel, [resID], { + cover_properties: JSON.stringify(this.readCoverPoperties(el)), + }); + } + + readCoverPoperties(el) { + const coverProperties = {}; + const bg = el.querySelector(".o_record_cover_image")?.style.backgroundImage || ""; + coverProperties["background-image"] = bg; + + // TODO: `o_record_has_cover` should be handled using model field, not + // resize_class to avoid all of this. + let coverClass = Object.keys(coverSizeClassLabels).find((e) => el.classList.contains(e)); + if (bg && bg !== "none") { + coverClass += " o_record_has_cover"; + } + coverProperties.resize_class = coverClass; + + coverProperties.text_align_class = + ["text-center", "text-end"].find((e) => el.classList.contains(e)) || ""; + + coverProperties.opacity = el.querySelector(".o_record_cover_filter")?.style.opacity || 0.0; + + coverProperties.background_color_class = [...el.classList.values()] + .filter((e) => e.startsWith("bg-") || e.startsWith("o_cc")) + .join(" "); + if (el.style.backgroundImage) { + coverProperties.background_color_style = `background-color: rgba(0, 0, 0, 0); background-image: ${el.style.backgroundImage};`; + } else if (el.style.backgroundColor) { + coverProperties.background_color_style = `background-color: ${el.style.backgroundColor};`; + } else { + coverProperties.background_color_style = ""; + } + return coverProperties; } } + registry .category("website-plugins") .add(CoverPropertiesOptionPlugin.id, CoverPropertiesOptionPlugin); diff --git a/addons/website_blog/static/src/website_builder/blog_cover_properties_option.js b/addons/website_blog/static/src/website_builder/blog_cover_properties_option.js index a4840a5c3d82e..fa6facf3f91cb 100644 --- a/addons/website_blog/static/src/website_builder/blog_cover_properties_option.js +++ b/addons/website_blog/static/src/website_builder/blog_cover_properties_option.js @@ -1,10 +1,8 @@ import { patch } from "@web/core/utils/patch"; import { useDomState } from "@html_builder/core/utils"; import { CoverPropertiesOption } from "@website/builder/plugins/options/cover_properties_option"; +import { _t } from "@web/core/l10n/translation"; -patch(CoverPropertiesOption, { - template: "website_blog.BlogCoverPropertiesOption", -}); patch(CoverPropertiesOption.prototype, { setup() { super.setup(); @@ -12,4 +10,16 @@ patch(CoverPropertiesOption.prototype, { isRegularCover: editingElement.classList.contains("o_wblog_post_page_cover_regular"), })); }, + + coverSizeLabel(className) { + return this.blogState.isRegularCover + ? blogCoverSizeClassLabels[className] + : super.coverSizeLabel(className); + }, }); + +const blogCoverSizeClassLabels = { + o_full_screen_height: _t("Large"), + o_half_screen_height: _t("Medium"), + cover_auto: _t("Tiny"), +}; diff --git a/addons/website_blog/static/src/website_builder/blog_cover_properties_option.xml b/addons/website_blog/static/src/website_builder/blog_cover_properties_option.xml deleted file mode 100644 index abbf6105f9ec4..0000000000000 --- a/addons/website_blog/static/src/website_builder/blog_cover_properties_option.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - Large - - - Medium - - - Tiny - - - -