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",
},
{