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..04842625a7e2c 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,8 @@ 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, }), 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..feab376508f07 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 @@ -8,11 +8,12 @@ export class ReplaceMediaOption extends BaseOptionComponent { this.state = useDomState((editingElement) => ({ canSetLink: this.canSetLink(editingElement), hasHref: this.hasHref(editingElement), + isProductPageImage: isProductPageImage(editingElement), })); } canSetLink(editingElement) { return ( - this.isImageSupportedForStyle(editingElement) && + isImageSupportedForStyle(editingElement) && !searchSupportedParentLinkEl(editingElement).matches("a[data-oe-xpath]") ); } @@ -20,28 +21,33 @@ export class ReplaceMediaOption extends BaseOptionComponent { const parentEl = searchSupportedParentLinkEl(editingElement); return parentEl.tagName === "A" && parentEl.hasAttribute("href"); } - 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; +export function isImageSupportedForStyle(img) { + if (!img.parentElement) { + return false; + } - // 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; + // See also `[data-oe-type='image'] > img` added as data-exclude of some + // snippet options. + const isTFieldImg = "oeType" in img.parentElement.dataset; - return !isTFieldImg && !isEditableRootElement; - } + // 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; } export function searchSupportedParentLinkEl(editingElement) { const parentEl = editingElement.parentElement; return parentEl.matches("figure") ? parentEl.parentElement : parentEl; } + +export function isProductPageImage(editingElement) { + return !!editingElement.closest(".o_wsale_product_images"); +} 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..f796e031dc1c1 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 @@ -2,11 +2,12 @@ - + 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.js b/addons/html_builder/static/src/website_sale/product_image_option.js new file mode 100644 index 0000000000000..3c3487f99b4b7 --- /dev/null +++ b/addons/html_builder/static/src/website_sale/product_image_option.js @@ -0,0 +1,14 @@ +import { BaseOptionComponent, useDomState } from "@html_builder/core/utils"; +import { isProductPageImage } from "@html_builder/plugins/image/replace_media_option"; + +export class ProductImageOption extends BaseOptionComponent { + static template = "website_sale.ProductImageOption"; + static props = {}; + + setup() { + super.setup(); + this.state = useDomState((editingElement) => ({ + isProductPageImage: isProductPageImage(editingElement), + })); + } +} 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..7907e3e6c30e9 --- /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..86d37c8a43c58 --- /dev/null +++ b/addons/html_builder/static/src/website_sale/product_image_option_plugin.js @@ -0,0 +1,58 @@ +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"; +import { ProductImageOption } from "./product_image_option"; + +export class ProductImageOptionPlugin extends Plugin { + static id = "productImageOption"; + resources = { + builder_options: [ + withSequence(REPLACE_MEDIA, { + OptionComponent: ProductImageOption, + selector: REPLACE_MEDIA_SELECTOR, + exclude: REPLACE_MEDIA_EXCLUDE, + editableOnly: false, + }), + ], + 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(); + }, + }, + }, + }; +} + +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", }, {