8000 Fix setting/removing image in covers from /blog page by blse-odoo · Pull Request #4682 · odoo-dev/odoo · GitHub
[go: up one dir, main page]

Skip to content

Fix setting/removing image in covers from /blog page #4682

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
8000
Diff view
80 changes: 29 additions & 51 deletions addons/html_builder/static/src/core/save_plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand All @@ -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
Expand All @@ -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
Expand All @@ -37,6 +42,7 @@ export class SavePlugin extends Plugin {

setup() {
this.canObserve = false;
this.savableSelector = this.getResource("savable_selectors").join(", ");
}

async save() {
Expand All @@ -58,51 +64,29 @@ 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
const willSaves = this.getResource("save_handlers").map((c) => c());
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 = {
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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") ||
Expand All @@ -223,4 +200,5 @@ export class SavePlugin extends Plugin {
}
}
}

registry.category("translation-plugins").add(SavePlugin.id, SavePlugin);
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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"),
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<templates xml:space="preserve">

<t t-name="html_builder.CoverPropertiesOption">

<BuilderContext action="'markCoverPropertiesToBeSaved'">
<BuilderRow label.translate="Background">
<!-- todo adapt when colorpicker is implemented: snippet_options_background_color_widget-->
<BuilderColorPicker title.translate="Color" styleAction="'background-color'"/>
Expand All @@ -14,9 +14,7 @@

<BuilderRow label.translate="Size" t-if="this.state.useSize">
<BuilderSelect>
<BuilderSelectItem classAction="'o_full_screen_height'"><span>Full Screen</span></BuilderSelectItem>
<BuilderSelectItem classAction="'o_half_screen_height'"><span>Half Screen</span></BuilderSelectItem>
<BuilderSelectItem classAction="'cover_auto'"><span>Fit text</span></BuilderSelectItem>
<BuilderSelectItem t-foreach="coverSizeClasses" t-as="className" t-key="className" classAction="className" t-out="coverSizeLabel(className)"/>
</BuilderSelect>
</BuilderRow>

Expand All @@ -36,7 +34,7 @@
<BuilderSelectItem classAction="'text-end'">Right</BuilderSelectItem>
</BuilderSelect>
</BuilderRow>

</BuilderContext>
</t>

</templates>
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 } }) {
Expand Down Expand Up @@ -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:(?<mimetype>.*);base64,(?<imageData>.*)"\)/
)?.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:(?<mimetype>.*);base64,(?<imageData>.*)"\)/
)?.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("/", "")}'`
F438 : 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);
Loading
0