diff --git a/addons/html_builder/static/src/core/builder_options_plugin.js b/addons/html_builder/static/src/core/builder_options_plugin.js index 30c2c3de4ad9d..7e6b6a2a28673 100644 --- a/addons/html_builder/static/src/core/builder_options_plugin.js +++ b/addons/html_builder/static/src/core/builder_options_plugin.js @@ -47,6 +47,9 @@ export class BuilderOptionsPlugin extends Plugin { ...option, id: uniqueId(), })); + this.getResource("patch_builder_options").forEach((option) => { + this.patchBuilderOptions(option); + }); this.builderHeaderMiddleButtons = this.getResource("builder_header_middle_buttons").map( (headerMiddleButton) => ({ ...headerMiddleButton, id: uniqueId() }) ); @@ -249,6 +252,34 @@ export class BuilderOptionsPlugin extends Plugin { this.dispatchTo("clone_disabled_reason_providers", { el, reasons }); return reasons.length ? reasons.join(" ") : undefined; } + patchBuilderOptions({ target_name, target_element, method, value }) { + if (!target_name || !target_element || !method || !value) { + throw new Error( + `Missing patch_builder_options required parameters: target_name, target_element, method, value` + ); + } + + const builderOption = this.builderOptions.find((option) => option.name === target_name); + if (!builderOption) { + throw new Error(`Builder option ${target_name} not found`); + } + + switch (method) { + case "replace": + builderOption[target_element] = value; + break; + case "add": + if (!builderOption[target_element]) { + throw new Error( + `Builder option ${target_name} does not have ${target_element}` + ); + } + builderOption[target_element] += `, ${value}`; + break; + default: + throw new Error(`Unknown method ${method}`); + } + } } function getClosestElements(element, selector) { diff --git a/addons/html_builder/static/src/plugins/image/image_tool_option_plugin.js b/addons/html_builder/static/src/plugins/image/image_tool_option_plugin.js index cd23a96df89e3..b03e9ef8b22a5 100644 --- a/addons/html_builder/static/src/plugins/image/image_tool_option_plugin.js +++ b/addons/html_builder/static/src/plugins/image/image_tool_option_plugin.js @@ -13,6 +13,10 @@ import { ALIGNMENT_STYLE_PADDING, } from "@html_builder/utils/option_sequence"; +export const REPLACE_MEDIA_SELECTOR = "img, .media_iframe_video, span.fa, i.fa"; +export const REPLACE_MEDIA_EXCLUDE = + "[data-oe-xpath], a[href^='/website/social/'] > i.fa, a[class*='s_share_'] > i.fa"; + class ImageToolOptionPlugin extends Plugin { static id = "imageToolOption"; static dependencies = [ @@ -28,9 +32,9 @@ class ImageToolOptionPlugin extends Plugin { builder_options: [ withSequence(REPLACE_MEDIA, { OptionComponent: ReplaceMediaOption, - selector: "img, .media_iframe_video, span.fa, i.fa", - exclude: - "[data-oe-xpath], a[href^='/website/social/'] > i.fa, a[class*='s_share_'] > i.fa", + selector: REPLACE_MEDIA_SELECTOR, + exclude: REPLACE_MEDIA_EXCLUDE, + name: "replaceMediaOption", }), withSequence(IMAGE_TOOL, { OptionComponent: ImageToolOption, diff --git a/addons/html_builder/static/src/plugins/image/replace_media_option.js b/addons/html_builder/static/src/plugins/image/replace_media_option.js index f0bd19bf4b49f..45aaf082de3fa 100644 --- a/addons/html_builder/static/src/plugins/image/replace_media_option.js +++ b/addons/html_builder/static/src/plugins/image/replace_media_option.js @@ -12,7 +12,7 @@ export class ReplaceMediaOption extends BaseOptionComponent { } canSetLink(editingElement) { return ( - this.isImageSupportedForStyle(editingElement) && + isImageSupportedForStyle(editingElement) && !searchSupportedParentLinkEl(editingElement).matches("a[data-oe-xpath]") ); } @@ -20,25 +20,26 @@ export class ReplaceMediaOption extends BaseOptionComponent { const parentEl = searchSupportedParentLinkEl(editingElement); return parentEl.tagName === "A" && parentEl.hasAttribute("href"); } - isImageSupportedForStyle(img) { - if (!img.parentElement) { - return false; - } +} + +export function isImageSupportedForStyle(img) { + if (!img.parentElement) { + return false; + } - // See also `[data-oe-type='image'] > img` added as data-exclude of some - // snippet options. - const isTFieldImg = "oeType" in img.parentElement.dataset; + // See also `[data-oe-type='image'] > img` added as data-exclude of some + // snippet options. + const isTFieldImg = "oeType" in img.parentElement.dataset; - // Editable root elements are technically *potentially* supported here (if - // the edited attributes are not computed inside the related view, they - // could technically be saved... but as we cannot tell the computed ones - // apart from the "static" ones, we choose to not support edition at all in - // those "root" cases). - // See also `[data-oe-xpath]` added as data-exclude of some snippet options. - const isEditableRootElement = "oeXpath" in img.dataset; + // Editable root elements are technically *potentially* supported here (if + // the edited attributes are not computed inside the related view, they + // could technically be saved... but as we cannot tell the computed ones + // apart from the "static" ones, we choose to not support edition at all in + // those "root" cases). + // See also `[data-oe-xpath]` added as data-exclude of some snippet options. + const isEditableRootElement = "oeXpath" in img.dataset; - return !isTFieldImg && !isEditableRootElement; - } + return !isTFieldImg && !isEditableRootElement; } export function searchSupportedParentLinkEl(editingElement) { diff --git a/addons/html_builder/static/src/plugins/image/replace_media_option.xml b/addons/html_builder/static/src/plugins/image/replace_media_option.xml index c19537e451fa5..1c6b3ef789527 100644 --- a/addons/html_builder/static/src/plugins/image/replace_media_option.xml +++ b/addons/html_builder/static/src/plugins/image/replace_media_option.xml @@ -6,7 +6,8 @@ img` added as data-exclude of some - // snippet options. - const isTFieldImg = "oeType" in img.parentElement.dataset; - - // Editable root elements are technically *potentially* supported here (if - // the edited attributes are not computed inside the related view, they - // could technically be saved... but as we cannot tell the computed ones - // apart from the "static" ones, we choose to not support edition at all in - // those "root" cases). - // See also `[data-oe-xpath]` added as data-exclude of some snippet options. - const isEditableRootElement = "oeXpath" in img.dataset; - - return !isTFieldImg && !isEditableRootElement; -} diff --git a/addons/html_builder/static/src/website_sale/product_attribute_option_plugin.js b/addons/html_builder/static/src/website_sale/product_attribute_option_plugin.js index cb30c31606773..bc81f530291fb 100644 --- a/addons/html_builder/static/src/website_sale/product_attribute_option_plugin.js +++ b/addons/html_builder/static/src/website_sale/product_attribute_option_plugin.js @@ -3,7 +3,7 @@ import { registry } from "@web/core/registry"; import { rpc } from "@web/core/network/rpc"; class ProductAttributeOptionPlugin extends Plugin { - static id = "productAtttributeOption"; + static id = "productAttributeOption"; resources = { builder_options: { template: "website_sale.ProductAttributeOption", diff --git a/addons/html_builder/static/src/website_sale/product_image_option.xml b/addons/html_builder/static/src/website_sale/product_image_option.xml new file mode 100644 index 0000000000000..e6977b69db19c --- /dev/null +++ b/addons/html_builder/static/src/website_sale/product_image_option.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + diff --git a/addons/html_builder/static/src/website_sale/product_image_option_plugin.js b/addons/html_builder/static/src/website_sale/product_image_option_plugin.js new file mode 100644 index 0000000000000..d32a6805ba969 --- /dev/null +++ b/addons/html_builder/static/src/website_sale/product_image_option_plugin.js @@ -0,0 +1,66 @@ +import { REPLACE_MEDIA } from "@html_builder/utils/option_sequence"; +import { + REPLACE_MEDIA_SELECTOR, + REPLACE_MEDIA_EXCLUDE, +} from "@html_builder/plugins/image/image_tool_option_plugin"; +import { Plugin } from "@html_editor/plugin"; +import { withSequence } from "@html_editor/utils/resource"; +import { rpc } from "@web/core/network/rpc"; +import { registry } from "@web/core/registry"; + +const PRODUCT_IMAGE_OPTION_SELECTOR = `.o_wsale_product_images :is(${REPLACE_MEDIA_SELECTOR})`; + +export class ProductImageOptionPlugin extends Plugin { + static id = "productImageOption"; + resources = { + builder_options: [ + withSequence(REPLACE_MEDIA, { + template: "website_sale.ProductImageOption", + selector: PRODUCT_IMAGE_OPTION_SELECTOR, + exclude: REPLACE_MEDIA_EXCLUDE, + }), + ], + builder_actions: { + /* + * Change sequence of product page images + */ + setPosition: { + reload: {}, + apply: async ({ editingElement: el, value }) => { + const params = { + image_res_model: el.parentElement.dataset.oeModel, + image_res_id: el.parentElement.dataset.oeId, + move: value, + }; + + await rpc("/shop/product/resequence-image", params); + }, + }, + /* + * Removes the image in the back-end + */ + removeMedia: { + reload: {}, + apply: async ({ editingElement: el }) => { + if (el.parentElement.dataset.oeModel === "product.image") { + // Unlink the "product.image" record as it is not the main product image. + await this.services.orm.unlink("product.image", [ + parseInt(el.parentElement.dataset.oeId), + ]); + } + el.remove(); + }, + }, + }, + patch_builder_options: [ + { + target_name: "replaceMediaOption", + target_element: "exclude", + method: "add", + value: PRODUCT_IMAGE_OPTION_SELECTOR, + }, + ], + }; +} + +registry.category("website-plugins").add(ProductImageOptionPlugin.id, ProductImageOptionPlugin); diff --git a/addons/website/static/tests/tours/media_dialog.js b/addons/website/static/tests/tours/media_dialog.js index e45e5280d0bf6..9081ba4179d58 100644 --- a/addons/website/static/tests/tours/media_dialog.js +++ b/addons/website/static/tests/tours/media_dialog.js @@ -156,7 +156,7 @@ registerWebsitePreviewTour("website_media_dialog_image_shape", { }, { content: "Open MediaDialog from an image", - trigger: ".o_we_bg_success[data-action-id='replaceMedia']", + trigger: ".btn-success[data-action-id='replaceMedia']", run: "click", }, {