From d1d631590cbe525d84efa19568af81429798cff1 Mon Sep 17 00:00:00 2001 From: Jinjiu Liu Date: Thu, 22 May 2025 12:39:25 +0200 Subject: [PATCH 01/10] [FIX] remove the unnecessary link preview loading and notification Because when we change the link element in the popover and apply, we always close the current one and open a new one, the loading at the apply is redundant. We do the reopening to let the overlay plugin reposition popover, and let browser handle the relative/root-relative url to add the domain. --- addons/html_editor/static/src/main/link/link_popover.js | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/html_editor/static/src/main/link/link_popover.js b/addons/html_editor/static/src/main/link/link_popover.js index d8ff9c7220457..860df0047d051 100644 --- a/addons/html_editor/static/src/main/link/link_popover.js +++ b/addons/html_editor/static/src/main/link/link_popover.js @@ -215,7 +215,6 @@ export class LinkPopover extends Component { this.state.url = deducedUrl ? this.correctLink(deducedUrl) : this.correctLink(this.state.url); - this.loadAsyncLinkPreview(); } onClickEdit() { this.state.editing = true; From 40c29506a816fd9a6075da02ac65536779ea92ee Mon Sep 17 00:00:00 2001 From: Alessandro Lupo Date: Mon, 12 May 2025 14:30:16 +0200 Subject: [PATCH 02/10] [FIX] html_builder: fix space between rating stars --- .../static/src/core/setup_editor_plugin.js | 3 +- .../builder/plugins/rating_option_plugin.js | 8 +-- .../builder/options/rating_option.test.js | 71 ++++++++++--------- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/addons/html_builder/static/src/core/setup_editor_plugin.js b/addons/html_builder/static/src/core/setup_editor_plugin.js index 719f4b47c3e61..33101f986362a 100644 --- a/addons/html_builder/static/src/core/setup_editor_plugin.js +++ b/addons/html_builder/static/src/core/setup_editor_plugin.js @@ -1,13 +1,14 @@ import { Plugin } from "@html_editor/plugin"; import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; +import { withSequence } from "@html_editor/utils/resource"; export class SetupEditorPlugin extends Plugin { static id = "setup_editor_plugin"; static shared = ["getEditableAreas"]; resources = { clean_for_save_handlers: this.cleanForSave.bind(this), - normalize_handlers: this.setContenteditable.bind(this), + normalize_handlers: withSequence(0, this.setContenteditable.bind(this)), }; setup() { diff --git a/addons/website/static/src/builder/plugins/rating_option_plugin.js b/addons/website/static/src/builder/plugins/rating_option_plugin.js index 675f899f00183..f324ea850f2af 100644 --- a/addons/website/static/src/builder/plugins/rating_option_plugin.js +++ b/addons/website/static/src/builder/plugins/rating_option_plugin.js @@ -111,11 +111,9 @@ function createIcons({ editingElement, nbActiveIcons, nbTotalIcons }) { const iconEls = getAllIcons(editingElement); [...iconEls].forEach((iconEl) => iconEl.remove()); for (let i = 0; i < nbTotalIcons; i++) { - if (i < nbActiveIcons) { - activeIconEl.appendChild(document.createElement("i")); - } else { - inactiveIconEl.append(document.createElement("i")); - } + const targetEl = i < nbActiveIcons ? activeIconEl : inactiveIconEl; + targetEl.appendChild(document.createElement("i")); + targetEl.appendChild(document.createTextNode(" ")); } renderIcons(editingElement); } diff --git a/addons/website/static/tests/builder/options/rating_option.test.js b/addons/website/static/tests/builder/options/rating_option.test.js index 3f7e7cc92d6ab..c85d645ea42d0 100644 --- a/addons/website/static/tests/builder/options/rating_option.test.js +++ b/addons/website/static/tests/builder/options/rating_option.test.js @@ -5,23 +5,24 @@ import { contains } from "@web/../tests/web_test_helpers"; defineWebsiteModels(); +const websiteContent = ` +
+

Quality

+
+ + + + + + + + + +
+
`; + test("change rating score", async () => { - await setupWebsiteBuilder( - `
-

Quality

-
- - - - - - - - - -
-
` - ); + await setupWebsiteBuilder(websiteContent); expect(":iframe .s_rating .s_rating_active_icons i").toHaveCount(3); expect(":iframe .s_rating .s_rating_inactive_icons i").toHaveCount(2); await contains(":iframe .s_rating").click(); @@ -33,24 +34,30 @@ test("change rating score", async () => { await clear(); await fill("4"); expect(":iframe .s_rating .s_rating_inactive_icons i").toHaveCount(3); -}); -test("Ensure order of operations when clicking very fast on two options", async () => { - await setupWebsiteBuilder( - `
-

Quality

-
- - - - - - - - - -
+ expect(":iframe .s_rating").toHaveInnerHTML( + `

Quality

+
+ + + ​ + + + + + ​ + + + ​ + + + ​ + +
` ); +}); +test("Ensure order of operations when clicking very fast on two options", async () => { + await setupWebsiteBuilder(websiteContent); await contains(":iframe .s_rating").click(); await waitFor("[data-label='Icon']"); expect("[data-label='Icon'] .btn-primary.dropdown-toggle").toHaveText("Stars"); From a7e45286056c6db00b0741245276c4ba1b344106 Mon Sep 17 00:00:00 2001 From: Alessandro Lupo Date: Fri, 9 May 2025 17:10:53 +0200 Subject: [PATCH 03/10] [FIX] html_builder: search for snippet name in add snippet dialog --- addons/html_builder/static/src/snippets/snippet_viewer.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/addons/html_builder/static/src/snippets/snippet_viewer.js b/addons/html_builder/static/src/snippets/snippet_viewer.js index 8fed3102f79cd..ba431f3a280ba 100644 --- a/addons/html_builder/static/src/snippets/snippet_viewer.js +++ b/addons/html_builder/static/src/snippets/snippet_viewer.js @@ -109,9 +109,12 @@ export class SnippetViewer extends Component { } if (this.props.state.search) { const strMatches = (str) => - str.toLowerCase().includes(this.props.state.search.toLowerCase()); + str ? str.toLowerCase().includes(this.props.state.search.toLowerCase()) : false; return snippetStructures.filter( - (snippet) => strMatches(snippet.title) || strMatches(snippet.keyWords || "") + (snippet) => + strMatches(snippet.name) || + strMatches(snippet.title) || + strMatches(snippet.keyWords) ); } From c60bdbe0d9c11145d1befab23eec82c7360f4ddf Mon Sep 17 00:00:00 2001 From: emge-odoo Date: Mon, 19 May 2025 12:48:31 +0200 Subject: [PATCH 04/10] [FIX] website: default colorpicker tab = custom in theme colors option --- .../builder/plugins/theme/theme_colors_option.xml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/addons/website/static/src/builder/plugins/theme/theme_colors_option.xml b/addons/website/static/src/builder/plugins/theme/theme_colors_option.xml index ee75d98474e38..a22b256b72b92 100644 --- a/addons/website/static/src/builder/plugins/theme/theme_colors_option.xml +++ b/addons/website/static/src/builder/plugins/theme/theme_colors_option.xml @@ -18,16 +18,19 @@ title="Primary" action="'customizeWebsiteColor'" actionParam="'o-color-1'" - enabledTabs="['solid', 'custom']"/> + enabledTabs="['solid', 'custom']" + selectedTab="'custom'"/> + enabledTabs="['solid', 'custom']" + selectedTab="'custom'"/> + enabledTabs="['solid', 'custom']" + selectedTab="'custom'"/>
@@ -36,11 +39,13 @@ + enabledTabs="['solid', 'custom']" + selectedTab="'custom'"/> + enabledTabs="['solid', 'custom']" + selectedTab="'custom'"/>
From 29ef1a5b87cd872bdf77d41d83dd34f7979480a2 Mon Sep 17 00:00:00 2001 From: divy-odoo Date: Thu, 15 May 2025 16:05:01 +0530 Subject: [PATCH 05/10] [FIX] test_website: adapt TestImageUploadProgress test --- .../static/tests/tours/image_upload_progress.js | 12 ++++++------ .../test_website/tests/test_image_upload_progress.py | 3 --- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/addons/test_website/static/tests/tours/image_upload_progress.js b/addons/test_website/static/tests/tours/image_upload_progress.js index 4a3661fcc539a..f955964fc5f0a 100644 --- a/addons/test_website/static/tests/tours/image_upload_progress.js +++ b/addons/test_website/static/tests/tours/image_upload_progress.js @@ -1,6 +1,6 @@ import { insertSnippet, registerWebsitePreviewTour } from "@website/js/tours/tour_utils"; -import { FileSelectorControlPanel } from "@web_editor/components/media_dialog/file_selector"; +import { FileSelectorControlPanel } from "@html_editor/main/media/media_dialog/file_selector"; import { patch } from "@web/core/utils/patch"; let patchWithError = false; @@ -72,7 +72,7 @@ registerWebsitePreviewTour('test_image_upload_progress', { run: "click", }, { content: "click on add images to open image dialog (in multi mode)", - trigger: 'we-customizeblock-option [data-add-images]', + trigger: "button[data-action-id='addImage']", run: "click", }, { content: "manually trigger input change", @@ -123,7 +123,7 @@ registerWebsitePreviewTour('test_image_upload_progress', { run: "click", }, { content: "click on replace media to open image dialog", - trigger: 'we-customizeblock-option [data-replace-media]', + trigger: "button[data-action-id='replaceMedia']", run: "click", }, { content: "manually trigger input change", @@ -162,7 +162,7 @@ registerWebsitePreviewTour('test_image_upload_progress', { run: "click", }, { content: "click on replace media to open image dialog", - trigger: 'we-customizeblock-option [data-replace-media]', + trigger: "button[data-action-id='replaceMedia']", run: "click", }, { content: "manually trigger input change", @@ -206,7 +206,7 @@ registerWebsitePreviewTour('test_image_upload_progress_unsplash', { run: "click", }, { content: "click on replace media to open image dialog", - trigger: 'we-customizeblock-option [data-replace-media]', + trigger: "button[data-action-id='replaceMedia']", run: "click", }, { content: "search 'fox' images", @@ -230,7 +230,7 @@ registerWebsitePreviewTour('test_image_upload_progress_unsplash', { run: "click", }, { content: "unsplash image (mocked to logo) should have been used", - trigger: ":iframe #wrap .s_image_gallery .img[data-original-src^='/unsplash/HQqIOc8oYro/fox']", + trigger: ":iframe #wrap .s_image_gallery img[src^='/unsplash/HQqIOc8oYro/fox']", run() { unpatchMediaDialog(); }, diff --git a/addons/test_website/tests/test_image_upload_progress.py b/addons/test_website/tests/test_image_upload_progress.py index a9d0c5d6bda18..019728c4700cb 100644 --- a/addons/test_website/tests/test_image_upload_progress.py +++ b/addons/test_website/tests/test_image_upload_progress.py @@ -9,9 +9,6 @@ from odoo import http import unittest - -# TODO master-mysterious-egg fix error -@unittest.skip("prepare mysterious-egg for merging") @odoo.tests.common.tagged('post_install', '-at_install') class TestImageUploadProgress(odoo.tests.HttpCase): From 1a79c817f44c23a58c912208c0c9278ed7abbb48 Mon Sep 17 00:00:00 2001 From: divy-odoo Date: Thu, 15 May 2025 15:18:37 +0530 Subject: [PATCH 06/10] [FIX] test_website: adapt test_01_multi_website_settings test --- addons/test_website/tests/test_settings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/test_website/tests/test_settings.py b/addons/test_website/tests/test_settings.py index 032ec85890f42..7f569ac528e97 100644 --- a/addons/test_website/tests/test_settings.py +++ b/addons/test_website/tests/test_settings.py @@ -7,8 +7,7 @@ @odoo.tests.tagged('-at_install', 'post_install') class TestWebsiteSettings(odoo.tests.HttpCase): - # TODO master-mysterious-egg fix error - @unittest.skip("prepare mysterious-egg for merging") + def test_01_multi_website_settings(self): # If not enabled (like in demo data), landing on res.config will try # to disable module_sale_quotation_builder and raise an issue From 05412f4a1522c7d445a62988b76ce9de0f7799c4 Mon Sep 17 00:00:00 2001 From: divy-odoo Date: Fri, 9 May 2025 18:06:52 +0530 Subject: [PATCH 07/10] [FIX] website: adapt reset_view test --- addons/test_website/static/tests/tours/reset_views.js | 3 ++- addons/test_website/tests/test_reset_views.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/addons/test_website/static/tests/tours/reset_views.js b/addons/test_website/static/tests/tours/reset_views.js index 9a4323b81a54e..0e242a37a7f4f 100644 --- a/addons/test_website/static/tests/tours/reset_views.js +++ b/addons/test_website/static/tests/tours/reset_views.js @@ -20,7 +20,8 @@ registerWebsitePreviewTour( () => [ { content: "Drag the Intro snippet group and drop it in #oe_structure_test_website_page.", - trigger: '#oe_snippets .oe_snippet[name="Intro"] .oe_snippet_thumbnail:not(.o_we_ongoing_insertion)', + trigger: + "#snippet_groups .o_snippet[name='Intro'] .o_snippet_thumbnail:not(.o_we_ongoing_insertion) .o_snippet_thumbnail_area", // id starting by 'oe_structure..' will actually create an inherited view run: "drag_and_drop :iframe #oe_structure_test_website_page", }, diff --git a/addons/test_website/tests/test_reset_views.py b/addons/test_website/tests/test_reset_views.py index 498e01665ecdd..aa48e4d0cda3f 100644 --- a/addons/test_website/tests/test_reset_views.py +++ b/addons/test_website/tests/test_reset_views.py @@ -93,8 +93,6 @@ def test_06_reset_specific_view_controller_inexisting_template(self): self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (2)") self.fix_it('/test_view') - # TODO master-mysterious-egg fix error - @unittest.skip("prepare mysterious-egg for merging") @mute_logger('odoo.http') def test_07_reset_page_view_complete_flow(self): self.start_tour(self.env['website'].get_client_action_url('/test_page_view'), 'test_reset_page_view_complete_flow_part1', login="admin") From 12bd75e7e92c5727419c07ba9f119b6dabd0d35e Mon Sep 17 00:00:00 2001 From: Keval Bhatt Date: Fri, 9 May 2025 13:25:57 +0530 Subject: [PATCH 08/10] [IMP] website_crm: adapt website_crm_pre_tour --- addons/website_crm/static/tests/tours/website_crm.js | 6 +++--- addons/website_crm/tests/test_website_crm.py | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/addons/website_crm/static/tests/tours/website_crm.js b/addons/website_crm/static/tests/tours/website_crm.js index 36910c0f6fe99..87da2771d03e7 100644 --- a/addons/website_crm/static/tests/tours/website_crm.js +++ b/addons/website_crm/static/tests/tours/website_crm.js @@ -13,15 +13,15 @@ registerWebsitePreviewTour('website_crm_pre_tour', { run: "click", }, { - trigger: "#oe_snippets .o_we_customize_snippet_btn.active", + trigger: ".o-snippets-menu .o-snippets-tabs [data-name='customize'].active", }, { content: "Open action select", - trigger: "we-select:has(we-button:contains('Create an Opportunity')) we-toggler", + trigger: ".o-snippets-menu [data-container-title='Block'] [data-label='Action'] .dropdown-toggle", run: "click", }, { content: "Select 'Create an Opportunity' as form action", - trigger: "we-select we-button:contains('Create an Opportunity')", + trigger: ".o_popover [data-action-id='selectAction']:contains('Create an Opportunity')", run: "click", }, ...clickOnSave(), diff --git a/addons/website_crm/tests/test_website_crm.py b/addons/website_crm/tests/test_website_crm.py index 38e21c626de92..5b1048276da8f 100644 --- a/addons/website_crm/tests/test_website_crm.py +++ b/addons/website_crm/tests/test_website_crm.py @@ -2,11 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import odoo.tests -import unittest - -# TODO master-mysterious-egg fix error -@unittest.skip("prepare mysterious-egg for merging") @odoo.tests.tagged('post_install', '-at_install') class TestWebsiteCrm(odoo.tests.HttpCase): From 8478118431e8f848431d728253b69e26d1edd57d Mon Sep 17 00:00:00 2001 From: Benoit Socias Date: Wed, 21 May 2025 14:47:48 +0200 Subject: [PATCH 09/10] [FIX] website, html_builder: various fixes This commit contains multiple changes squashed together, coming from various pull requests on master-mysterious-egg. These changes includes: - No image in colorpicker and working gradient editor for preset background - Clean carousel related tests, remove repeated ones and merge/rename files - Fix logo adding/removing option - Use only "color" tabs in countdown color picker options - Add support for compact hex colors (e.g. #fff) - Fix color styling - Fix setExtraStep - Auto-optimize image upon replace media - Adapt extra product image to also rely on openMediaDialog - Use correct mimetype fields from dataset - Adapt tests - Refresh Dynamic Snippet Carousel when scrolling mode is changed Co-authored-by: Alessandro Lupo Co-authored-by: Benoit Socias Co-authored-by: divy-odoo Co-authored-by: emge-odoo Co-authored-by: FrancoisGe Co-authored-by: Jinjiu Liu Co-authored-by: Keval Bhatt Co-authored-by: Serhii Rubanskyi - seru --- .../src/core/core_builder_action_plugin.js | 21 +++- .../static/src/main/font/color_plugin.js | 8 +- .../src/main/media/image_crop_plugin.js | 8 +- .../main/media/image_post_process_plugin.js | 9 +- .../main/media/media_dialog/image_selector.js | 2 +- .../main/media/media_dialog/media_dialog.js | 1 + .../static/src/main/media/media_plugin.js | 22 +++- .../plugins/caption_plugin/caption_plugin.js | 2 +- addons/web/static/src/core/utils/colors.js | 10 ++ .../background_image_option_plugin.js | 23 ++-- .../plugins/customize_website_plugin.js | 8 +- .../image/image_filter_option_plugin.js | 8 +- .../plugins/image/image_format_option.js | 76 +++++++------ .../image/image_format_option_plugin.js | 8 +- .../image/image_shape_option_plugin.js | 2 +- .../plugins/image/image_tool_option_plugin.js | 48 ++++++-- .../add_element_option_plugin.js | 3 +- .../plugins/options/countdown_option.xml | 6 +- .../options/cover_properties_option_plugin.js | 18 +-- .../plugins/options/header_element_option.js | 25 +++++ .../builder/plugins/options/header_option.xml | 5 +- .../plugins/options/header_option_plugin.js | 3 +- .../options/image_gallery_option_plugin.js | 5 +- .../options/image_snippet_option_plugin.js | 3 +- .../dynamic_snippet_carousel.edit.js | 20 ++++ .../dynamic_snippet_carousel.js | 6 - .../image_snippet_option.test.js | 4 +- ...der.edit.test.js => carousel.edit.test.js} | 16 ++- ...arousel_bootstrap_upgrade_fix.edit.test.js | 88 ++++++++------- .../carousel_bootstrap_upgrade_fix.test.js | 60 ---------- .../carousel/carousel_slider.edit.test.js | 106 ------------------ .../carousel/carousel_slider.test.js | 1 - .../attachment_media_dialog.js | 10 -- .../website_builder/checkout_page_option.xml | 6 +- .../checkout_page_option_plugin.js | 29 ++++- .../product_page_option_plugin.js | 7 +- 36 files changed, 329 insertions(+), 348 deletions(-) create mode 100644 addons/website/static/src/builder/plugins/options/header_element_option.js create mode 100644 addons/website/static/src/snippets/s_dynamic_snippet_carousel/dynamic_snippet_carousel.edit.js rename addons/website/static/tests/interactions/carousel/{carousel_section_slider.edit.test.js => carousel.edit.test.js} (92%) delete mode 100644 addons/website/static/tests/interactions/carousel/carousel_bootstrap_upgrade_fix.test.js delete mode 100644 addons/website/static/tests/interactions/carousel/carousel_slider.edit.test.js delete mode 100644 addons/website_sale/static/src/website_builder/attachment_media_dialog.js diff --git a/addons/html_builder/static/src/core/core_builder_action_plugin.js b/addons/html_builder/static/src/core/core_builder_action_plugin.js index b300b9a62a758..08ed7ee6b2511 100644 --- a/addons/html_builder/static/src/core/core_builder_action_plugin.js +++ b/addons/html_builder/static/src/core/core_builder_action_plugin.js @@ -1,5 +1,10 @@ import { Plugin } from "@html_editor/plugin"; -import { CSS_SHORTHANDS, applyNeededCss, areCssValuesEqual } from "@html_builder/utils/utils_css"; +import { + CSS_SHORTHANDS, + applyNeededCss, + areCssValuesEqual, + normalizeColor, +} from "@html_builder/utils/utils_css"; export function withoutTransition(editingElement, callback) { if (editingElement.classList.contains("o_we_force_no_transition")) { @@ -263,10 +268,18 @@ const attributeAction = { }; const dataAttributeAction = { - getValue: ({ editingElement, params: { mainParam: attributeName } = {} }) => - editingElement.dataset[attributeName], + // if it's a color action, we have to normalize the value + getValue: ({ editingElement, params: { mainParam: attributeName } = {} }) => { + if (!/(^color|Color)($|(?=[A-Z]))/.test(attributeName)) { + return editingElement.dataset[attributeName]; + } + const color = normalizeColor(editingElement.dataset[attributeName]); + return color; + }, isApplied: ({ editingElement, params: { mainParam: attributeName } = {}, value }) => { if (value) { + const match = value.match(/^var\(--(.*)\)$/); + value = match ? match[1] : value; return editingElement.dataset[attributeName] === value; } else { return !(attributeName in editingElement.dataset); @@ -274,6 +287,8 @@ const dataAttributeAction = { }, apply: ({ editingElement, params: { mainParam: attributeName } = {}, value }) => { if (value) { + const match = value.match(/^var\(--(.*)\)$/); + value = match ? match[1] : value; editingElement.dataset[attributeName] = value; } else { delete editingElement.dataset[attributeName]; diff --git a/addons/html_editor/static/src/main/font/color_plugin.js b/addons/html_editor/static/src/main/font/color_plugin.js index 0bbf47b67ca7d..42552f24bfa8b 100644 --- a/addons/html_editor/static/src/main/font/color_plugin.js +++ b/addons/html_editor/static/src/main/font/color_plugin.js @@ -23,6 +23,7 @@ import { rgbaToHex, COLOR_COMBINATION_CLASSES_REGEX, } from "@web/core/utils/colors"; +import { backgroundImageCssToParts } from "@html_editor/utils/image"; import { ColorSelector } from "./color_selector"; const RGBA_OPACITY = 0.6; @@ -136,7 +137,8 @@ export class ColorPlugin extends Plugin { getElementColors(el) { const elStyle = getComputedStyle(el); const backgroundImage = elStyle.backgroundImage; - const hasGradient = isColorGradient(backgroundImage); + const gradient = backgroundImageCssToParts(backgroundImage).gradient; + const hasGradient = isColorGradient(gradient); const hasTextGradientClass = el.classList.contains("text-gradient"); let backgroundColor = elStyle.backgroundColor; @@ -155,9 +157,9 @@ export class ColorPlugin extends Plugin { } return { - color: hasGradient && hasTextGradientClass ? backgroundImage : rgbaToHex(elStyle.color), + color: hasGradient && hasTextGradientClass ? gradient : rgbaToHex(elStyle.color), backgroundColor: - hasGradient && !hasTextGradientClass ? backgroundImage : rgbaToHex(backgroundColor), + hasGradient && !hasTextGradientClass ? gradient : rgbaToHex(backgroundColor), }; } diff --git a/addons/html_editor/static/src/main/media/image_crop_plugin.js b/addons/html_editor/static/src/main/media/image_crop_plugin.js index 3ce206db760e3..3be9b5910fc0b 100644 --- a/addons/html_editor/static/src/main/media/image_crop_plugin.js +++ b/addons/html_editor/static/src/main/media/image_crop_plugin.js @@ -42,10 +42,10 @@ export class ImageCropPlugin extends Plugin { onSave: async (newDataset) => { // todo: should use the mutex if there is one? const updateImageAttributes = - await this.dependencies.imagePostProcess.processImage( - selectedImg, - newDataset - ); + await this.dependencies.imagePostProcess.processImage({ + img: selectedImg, + newDataset, + }); updateImageAttributes(); this.dependencies.history.addStep(); }, diff --git a/addons/html_editor/static/src/main/media/image_post_process_plugin.js b/addons/html_editor/static/src/main/media/image_post_process_plugin.js index c048042f26b58..d39cb1fe4f776 100644 --- a/addons/html_editor/static/src/main/media/image_post_process_plugin.js +++ b/addons/html_editor/static/src/main/media/image_post_process_plugin.js @@ -24,14 +24,21 @@ export class ImagePostProcessPlugin extends Plugin { * * @param {HTMLImageElement} img the image to which modifications are applied * @param {Object} newDataset an object containing the modifications to apply + * @param {Function} [onImageInfoLoaded] can be used to fill + * newDataset after having access to image info, return true to cancel call * @returns {Function} callback that sets dataURL of the image with the * applied modifications to `img` element */ - async processImage(img, newDataset = {}) { + async processImage({ img, newDataset = {}, onImageInfoLoaded }) { const processContext = {}; if (!newDataset.originalSrc || !newDataset.mimetypeBeforeConversion) { Object.assign(newDataset, await loadImageInfo(img)); } + if (onImageInfoLoaded) { + if (await onImageInfoLoaded(newDataset)) { + return () => {}; + } + } for (const cb of this.getResource("process_image_warmup_handlers")) { const addedContext = await cb(img, newDataset); if (addedContext) { diff --git a/addons/html_editor/static/src/main/media/media_dialog/image_selector.js b/addons/html_editor/static/src/main/media/media_dialog/image_selector.js index 5ce3121d28135..f369d2f0aa3e3 100644 --- a/addons/html_editor/static/src/main/media/media_dialog/image_selector.js +++ b/addons/html_editor/static/src/main/media/media_dialog/image_selector.js @@ -94,7 +94,7 @@ export class ImageSelector extends FileSelector { this.fileMimetypes = IMAGE_MIMETYPES.join(","); this.isImageField = - !!this.props.media?.closest("[data-oe-type=image]") || !!this.env.addFieldImage; + !!this.props.media?.closest("[data-oe-type=image]") || !!this.props.addFieldImage; } get canLoadMore() { diff --git a/addons/html_editor/static/src/main/media/media_dialog/media_dialog.js b/addons/html_editor/static/src/main/media/media_dialog/media_dialog.js index 72cef1ab5924a..f85430413082b 100644 --- a/addons/html_editor/static/src/main/media/media_dialog/media_dialog.js +++ b/addons/html_editor/static/src/main/media/media_dialog/media_dialog.js @@ -133,6 +133,7 @@ export class MediaDialog extends Component { this.addTab(TABS.IMAGES, { useMediaLibrary: this.props.useMediaLibrary, multiSelect: this.props.multiImages, + addFieldImage: this.props.addFieldImage, }); } if (!noIcons) { diff --git a/addons/html_editor/static/src/main/media/media_plugin.js b/addons/html_editor/static/src/main/media/media_plugin.js index 1a8732d9e2dae..444ac91bce01b 100644 --- a/addons/html_editor/static/src/main/media/media_plugin.js +++ b/addons/html_editor/static/src/main/media/media_plugin.js @@ -126,13 +126,12 @@ export class MediaPlugin extends Plugin { } } - onSaveMediaDialog(element, { node }) { + async onSaveMediaDialog(element, { node }) { if (!element) { // @todo @phoenix to remove throw new Error("Element is required: onSaveMediaDialog"); // return; } - if (node) { const changedIcon = isIconElement(node) && isIconElement(element); if (changedIcon) { @@ -151,11 +150,25 @@ export class MediaPlugin extends Plugin { // Collapse selection after the inserted/replaced element. const [anchorNode, anchorOffset] = rightPos(element); this.dependencies.selection.setSelection({ anchorNode, anchorOffset }); - this.delegateTo("afer_save_media_dialog_handlers", element); + this.delegateTo("after_save_media_dialog_handlers", element); this.dependencies.history.addStep(); } openMediaDialog(params = {}, editableEl = null) { + const oldSave = + params.save || ((element) => this.onSaveMediaDialog(element, { node: params.node })); + params.save = async (...args) => { + const selection = args[0]; + const elements = selection + ? selection[Symbol.iterator] + ? selection + : [selection] + : []; + for (const onMediaDialogSaved of this.getResource("on_media_dialog_saved_handlers")) { + await onMediaDialogSaved(elements, { node: params.node }); + } + return oldSave(...args); + }; const { resModel, resId, field, type } = this.getRecordInfo(editableEl); const mediaDialogClosedPromise = this.dependencies.dialog.addDialog(MediaDialog, { resModel, @@ -165,9 +178,6 @@ export class MediaPlugin extends Plugin { ((resModel === "ir.ui.view" && field === "arch") || type === "html") ), // @todo @phoenix: should be removed and moved to config.mediaModalParams media: params.node, - save: (element) => { - this.onSaveMediaDialog(element, { node: params.node }); - }, onAttachmentChange: this.config.onAttachmentChange || (() => {}), noVideos: !this.config.allowMediaDialogVideo, noImages: !this.config.allowImage, diff --git a/addons/html_editor/static/src/others/embedded_components/plugins/caption_plugin/caption_plugin.js b/addons/html_editor/static/src/others/embedded_components/plugins/caption_plugin/caption_plugin.js index 4278d51a27fda..6ade00ab4f982 100644 --- a/addons/html_editor/static/src/others/embedded_components/plugins/caption_plugin/caption_plugin.js +++ b/addons/html_editor/static/src/others/embedded_components/plugins/caption_plugin/caption_plugin.js @@ -37,7 +37,7 @@ export class CaptionPlugin extends Plugin { clean_for_save_handlers: this.cleanForSave.bind(this), mount_component_handlers: this.setupNewCaption.bind(this), delete_image_handlers: this.handleDeleteImage.bind(this), - afer_save_media_dialog_handlers: this.onImageReplaced.bind(this), + after_save_media_dialog_handlers: this.onImageReplaced.bind(this), hints: [{ selector: "FIGCAPTION", text: _t("Write a caption...") }], unsplittable_node_predicates: [ (node) => ["FIGURE", "FIGCAPTION"].includes(node.nodeName), // avoid merge diff --git a/addons/web/static/src/core/utils/colors.js b/addons/web/static/src/core/utils/colors.js index e18306eabbc3e..6dca83f3d6c15 100644 --- a/addons/web/static/src/core/utils/colors.js +++ b/addons/web/static/src/core/utils/colors.js @@ -217,6 +217,16 @@ export function convertCSSColorToRgba(cssColor) { } // Otherwise, check if cssColor is an hexadecimal code color + // first check if it's in its compact form (e.g. #FFF) + if (/^#([0-9a-f]{3})$/i.test(cssColor)) { + return { + red: parseInt(cssColor[1] + cssColor[1], 16), + green: parseInt(cssColor[2] + cssColor[2], 16), + blue: parseInt(cssColor[3] + cssColor[3], 16), + opacity: 100, + }; + } + if (/^#([0-9A-F]{6}|[0-9A-F]{8})$/i.test(cssColor)) { return { red: parseInt(cssColor.substr(1, 2), 16), diff --git a/addons/website/static/src/builder/plugins/background_option/background_image_option_plugin.js b/addons/website/static/src/builder/plugins/background_option/background_image_option_plugin.js index dd13d966310f7..3615d3a030f16 100644 --- a/addons/website/static/src/builder/plugins/background_option/background_image_option_plugin.js +++ b/addons/website/static/src/builder/plugins/background_option/background_image_option_plugin.js @@ -77,7 +77,7 @@ export class BackgroundImageOptionPlugin extends Plugin { editingElement.querySelector(".o_we_bg_filter")?.remove(); this.applyReplaceBackgroundImage.bind(this)({ editingElement: editingElement, - loadResult: "", + loadResult: undefined, params: { forceClean: true }, }); this.dispatchTo("on_bg_image_hide_handlers", editingElement); @@ -134,12 +134,13 @@ export class BackgroundImageOptionPlugin extends Plugin { newEditingEl.classList.toggle("o_modified_image_to_save", isModifiedImage); } } - loadReplaceBackgroundImage() { + loadReplaceBackgroundImage({ editingElement }) { return new Promise((resolve) => { const onClose = this.dependencies.media.openMediaDialog({ onlyImages: true, - save: (imageEl) => { - resolve(imageEl.getAttribute("src")); + node: editingElement, + save: async (imageEl) => { + resolve(imageEl); }, }); onClose.then(resolve); @@ -147,18 +148,24 @@ export class BackgroundImageOptionPlugin extends Plugin { } applyReplaceBackgroundImage({ editingElement, - loadResult: imageSrc, + loadResult: imageEl, params: { forceClean = false }, }) { - if (!forceClean && !imageSrc) { + if (!forceClean && !imageEl) { // Do nothing: no images has been selected on the media dialog return; } - this.setImageBackground(editingElement, imageSrc); + const src = imageEl?.src || ""; + this.setImageBackground(editingElement, src); for (const attr of removeOnImageChangeAttrs) { delete editingElement.dataset[attr]; } - // TODO: call _autoOptimizeImage of the ImageHandlersOption + if (imageEl) { + if (src.startsWith("data:")) { + editingElement.classList.add("o_modified_image_to_save"); + } + Object.assign(editingElement.dataset, imageEl.dataset); + } } /** * diff --git a/addons/website/static/src/builder/plugins/customize_website_plugin.js b/addons/website/static/src/builder/plugins/customize_website_plugin.js index be35f7febc928..169ae96807f8a 100644 --- a/addons/website/static/src/builder/plugins/customize_website_plugin.js +++ b/addons/website/static/src/builder/plugins/customize_website_plugin.js @@ -87,7 +87,13 @@ export class CustomizeWebsitePlugin extends Plugin { if (gradientColor) { const gradientValue = this.getWebsiteVariableValue(gradientColor); if (gradientValue) { - return gradientValue; + // Pass through style to restore rgb/a which might + // have been lost during SCSS generation process. + // TODO Remove this once colorpicker will be able + // to cope with #rrggbb gradient color elements. + const el = document.createElement("div"); + el.style.setProperty("background-image", gradientValue); + return el.style.getPropertyValue("background-image"); } } return getCSSVariableValue(color, style); diff --git a/addons/website/static/src/builder/plugins/image/image_filter_option_plugin.js b/addons/website/static/src/builder/plugins/image/image_filter_option_plugin.js index ea9684bbddc65..b17c409dc9ad4 100644 --- a/addons/website/static/src/builder/plugins/image/image_filter_option_plugin.js +++ b/addons/website/static/src/builder/plugins/image/image_filter_option_plugin.js @@ -20,9 +20,9 @@ class ImageFilterOptionPlugin extends Plugin { } }, load: async ({ editingElement: img, params: { mainParam: glFilterName } }) => - await this.dependencies.imagePostProcess.processImage(img, { + await this.dependencies.imagePostProcess.processImage({ img, newDataset: { glFilter: glFilterName, - }), + }}), apply: ({ loadResult: updateImageAttributes }) => { updateImageAttributes(); }, @@ -53,9 +53,9 @@ class ImageFilterOptionPlugin extends Plugin { const filterOptions = JSON.parse(img.dataset.filterOptions || "{}"); filterOptions[filterProperty] = filterProperty === "filterColor" ? normalizeColor(value) : value; - return this.dependencies.imagePostProcess.processImage(img, { + return this.dependencies.imagePostProcess.processImage({ img, newDataset: { filterOptions: JSON.stringify(filterOptions), - }); + }}); }, apply: ({ loadResult: updateImageAttributes }) => { updateImageAttributes(); diff --git a/addons/website/static/src/builder/plugins/image/image_format_option.js b/addons/website/static/src/builder/plugins/image/image_format_option.js index c13f2b6e1e5ac..e2d10d4ca4efa 100644 --- a/addons/website/static/src/builder/plugins/image/image_format_option.js +++ b/addons/website/static/src/builder/plugins/image/image_format_option.js @@ -38,42 +38,46 @@ export class ImageFormatOption extends BaseOptionComponent { if (this.props.computeMaxDisplayWidth) { return this.props.computeMaxDisplayWidth(img); } - const window = img.ownerDocument.defaultView; - if (!window) { - return; - } - const computedStyles = window.getComputedStyle(img); - const displayWidth = parseFloat(computedStyles.getPropertyValue("width")); - const gutterWidth = - parseFloat(computedStyles.getPropertyValue("--o-grid-gutter-width")) || 30; + return computeMaxDisplayWidth(img, this.MAX_SUGGESTED_WIDTH); + } +} - // For the logos we don't want to suggest a width too small. - if (img.closest("nav")) { - return Math.round(Math.min(displayWidth * 3, this.MAX_SUGGESTED_WIDTH)); - // If the image is in a container(-small), it might get bigger on - // smaller screens. So we suggest the width of the current image unless - // it is smaller than the size of the container on the md breapoint - // (which is where our bootstrap columns fallback to full container - // width since we only use col-lg-* in Odoo). - } else if (img.closest(".container, .o_container_small")) { - const mdContainerMaxWidth = - parseFloat(computedStyles.getPropertyValue("--o-md-container-max-width")) || 720; - const mdContainerInnerWidth = mdContainerMaxWidth - gutterWidth; - return Math.round(clamp(displayWidth, mdContainerInnerWidth, this.MAX_SUGGESTED_WIDTH)); - // If the image is displayed in a container-fluid, it might also get - // bigger on smaller screens. The same way, we suggest the width of the - // current image unless it is smaller than the max size of the container - // on the md breakpoint (which is the LG breakpoint since the container - // fluid is full-width). - } else if (img.closest(".container-fluid")) { - const lgBp = parseFloat(computedStyles.getPropertyValue("--breakpoint-lg")) || 992; - const mdContainerFluidMaxInnerWidth = lgBp - gutterWidth; - return Math.round( - clamp(displayWidth, mdContainerFluidMaxInnerWidth, this.MAX_SUGGESTED_WIDTH) - ); - } - // If it's not in a container, it's probably not going to change size - // depending on breakpoints. We still keep a margin safety. - return Math.round(Math.min(displayWidth * 1.5, this.MAX_SUGGESTED_WIDTH)); +export function computeMaxDisplayWidth(img, MAX_SUGGESTED_WIDTH = 1920) { + const window = img.ownerDocument.defaultView; + if (!window) { + return; + } + const computedStyles = window.getComputedStyle(img); + const displayWidth = parseFloat(computedStyles.getPropertyValue("width")); + const gutterWidth = + parseFloat(computedStyles.getPropertyValue("--o-grid-gutter-width")) || 30; + + // For the logos we don't want to suggest a width too small. + if (img.closest("nav")) { + return Math.round(Math.min(displayWidth * 3, MAX_SUGGESTED_WIDTH)); + // If the image is in a container(-small), it might get bigger on + // smaller screens. So we suggest the width of the current image unless + // it is smaller than the size of the container on the md breapoint + // (which is where our bootstrap columns fallback to full container + // width since we only use col-lg-* in Odoo). + } else if (img.closest(".container, .o_container_small")) { + const mdContainerMaxWidth = + parseFloat(computedStyles.getPropertyValue("--o-md-container-max-width")) || 720; + const mdContainerInnerWidth = mdContainerMaxWidth - gutterWidth; + return Math.round(clamp(displayWidth, mdContainerInnerWidth, MAX_SUGGESTED_WIDTH)); + // If the image is displayed in a container-fluid, it might also get + // bigger on smaller screens. The same way, we suggest the width of the + // current image unless it is smaller than the max size of the container + // on the md breakpoint (which is the LG breakpoint since the container + // fluid is full-width). + } else if (img.closest(".container-fluid")) { + const lgBp = parseFloat(computedStyles.getPropertyValue("--breakpoint-lg")) || 992; + const mdContainerFluidMaxInnerWidth = lgBp - gutterWidth; + return Math.round( + clamp(displayWidth, mdContainerFluidMaxInnerWidth, MAX_SUGGESTED_WIDTH) + ); } + // If it's not in a container, it's probably not going to change size + // depending on breakpoints. We still keep a margin safety. + return Math.round(Math.min(displayWidth * 1.5, MAX_SUGGESTED_WIDTH)); } diff --git a/addons/website/static/src/builder/plugins/image/image_format_option_plugin.js b/addons/website/static/src/builder/plugins/image/image_format_option_plugin.js index b73a3e16ba021..00d2a4bd51740 100644 --- a/addons/website/static/src/builder/plugins/image/image_format_option_plugin.js +++ b/addons/website/static/src/builder/plugins/image/image_format_option_plugin.js @@ -29,10 +29,10 @@ class ImageFormatOptionPlugin extends Plugin { ); }, load: async ({ editingElement: img, params: { width, mimetype } }) => - this.dependencies.imagePostProcess.processImage(img, { + this.dependencies.imagePostProcess.processImage({ img, newDataset: { resizeWidth: width, formatMimetype: mimetype, - }), + }}), apply: ({ loadResult: updateImageAttributes }) => { updateImageAttributes(); }, @@ -41,9 +41,9 @@ class ImageFormatOptionPlugin extends Plugin { getValue: ({ editingElement: img }) => ("quality" in img.dataset && img.dataset.quality) || DEFAULT_IMAGE_QUALITY, load: async ({ editingElement: img, value: quality }) => - this.dependencies.imagePostProcess.processImage(img, { + this.dependencies.imagePostProcess.processImage({ img, newDataset: { quality, - }), + }}), apply: ({ loadResult: updateImageAttributes }) => { updateImageAttributes(); }, diff --git a/addons/website/static/src/builder/plugins/image/image_shape_option_plugin.js b/addons/website/static/src/builder/plugins/image/image_shape_option_plugin.js index ad3fb968f2d87..00097c1cecd19 100644 --- a/addons/website/static/src/builder/plugins/image/image_shape_option_plugin.js +++ b/addons/website/static/src/builder/plugins/image/image_shape_option_plugin.js @@ -143,7 +143,7 @@ class ImageShapeOptionPlugin extends Plugin { return shapeSvgText; } async loadShape(img, newData = {}) { - return this.dependencies.imagePostProcess.processImage(img, newData); + return this.dependencies.imagePostProcess.processImage({ img, newDataset: newData }); //todo: handle hover effect before // todo: is it still needed? // await loadImage(shapeDataURL, img); diff --git a/addons/website/static/src/builder/plugins/image/image_tool_option_plugin.js b/addons/website/static/src/builder/plugins/image/image_tool_option_plugin.js index d70aa6a889987..9a04043531593 100644 --- a/addons/website/static/src/builder/plugins/image/image_tool_option_plugin.js +++ b/addons/website/static/src/builder/plugins/image/image_tool_option_plugin.js @@ -1,4 +1,4 @@ -import { cropperDataFieldsWithAspectRatio, isGif } from "@html_editor/utils/image_processing"; +import { cropperDataFieldsWithAspectRatio, isGif, loadImage } from "@html_editor/utils/image_processing"; import { registry } from "@web/core/registry"; import { Plugin } from "@html_editor/plugin"; import { ImageToolOption } from "./image_tool_option"; @@ -10,6 +10,7 @@ import { ALIGNMENT_STYLE_PADDING, } from "@html_builder/utils/option_sequence"; import { ReplaceMediaOption, searchSupportedParentLinkEl } from "./replace_media_option"; +import { computeMaxDisplayWidth } from "./image_format_option"; export const REPLACE_MEDIA_SELECTOR = "img, .media_iframe_video, span.fa, i.fa"; export const REPLACE_MEDIA_EXCLUDE = @@ -45,6 +46,39 @@ class ImageToolOptionPlugin extends Plugin { }), ], builder_actions: this.getActions(), + on_media_dialog_saved_handlers: async (elements, { node }) => { + for (const image of elements) { + if (image && image.tagName === "IMG") { + const updateImageAttributes = await this.dependencies.imagePostProcess.processImage({ + img: image, + newDataset: { + formatMimetype: "image/webp", + }, + // TODO Using a callback is currently needed to avoid + // the extra RPC that would occur if loadImageInfo was + // called before processImage as well. This flow can be + // simplified if image infos are somehow cached. + onImageInfoLoaded: async (dataset) => { + if (!dataset.originalSrc || !dataset.originalId) { + return true; + } + const original = await loadImage(dataset.originalSrc); + const maxWidth = dataset.width ? image.naturalWidth : original.naturalWidth; + const optimizedWidth = Math.min(maxWidth, computeMaxDisplayWidth(node || this.editable)); + if (!["image/gif", "image/svg+xml"].includes(dataset.mimetypeBeforeConversion)) { + // Convert to recommended format and width. + dataset.resizeWidth = optimizedWidth; + } else if (dataset.shape && dataset.mimetypeBeforeConversion !== "image/gif") { + dataset.resizeWidth = optimizedWidth; + } else { + return true; + } + }, + }); + updateImageAttributes(); + } + } + }, }; getActions() { return { @@ -57,7 +91,7 @@ class ImageToolOptionPlugin extends Plugin { onClose: resolve, onSave: async (newDataset) => { resolve( - this.dependencies.imagePostProcess.processImage(img, newDataset) + this.dependencies.imagePostProcess.processImage({ img, newDataset }) ); }, }); @@ -71,7 +105,7 @@ class ImageToolOptionPlugin extends Plugin { const newDataset = Object.fromEntries( cropperDataFieldsWithAspectRatio.map((field) => [field, undefined]) ); - return this.dependencies.imagePostProcess.processImage(img, newDataset); + return this.dependencies.imagePostProcess.processImage({ img, newDataset }); }, apply: ({ loadResult: updateImageAttributes }) => { updateImageAttributes(); @@ -96,14 +130,14 @@ class ImageToolOptionPlugin extends Plugin { }, replaceMedia: { load: async ({ editingElement }) => { - let icon; + let image; await this.dependencies.media.openMediaDialog({ node: editingElement, - save: (newIcon) => { - icon = newIcon; + save: (newImage) => { + image = newImage; }, }); - return icon; + return image; }, apply: ({ editingElement, loadResult: newImage }) => { if (!newImage) { diff --git a/addons/website/static/src/builder/plugins/layout_option/add_element_option_plugin.js b/addons/website/static/src/builder/plugins/layout_option/add_element_option_plugin.js index 8a8069fcd7074..c0cce21b0d4c9 100644 --- a/addons/website/static/src/builder/plugins/layout_option/add_element_option_plugin.js +++ b/addons/website/static/src/builder/plugins/layout_option/add_element_option_plugin.js @@ -28,11 +28,12 @@ export class AddElementOptionPlugin extends Plugin { }, }, addElImage: { - load: async () => { + load: async ({ editingElement }) => { let selectedImage; await new Promise((resolve) => { const onClose = this.dependencies.media.openMediaDialog({ onlyImages: true, + node: editingElement, save: (images) => { selectedImage = images; resolve(); diff --git a/addons/website/static/src/builder/plugins/options/countdown_option.xml b/addons/website/static/src/builder/plugins/options/countdown_option.xml index 5132fd4806935..6660b90cf77b4 100644 --- a/addons/website/static/src/builder/plugins/options/countdown_option.xml +++ b/addons/website/static/src/builder/plugins/options/countdown_option.xml @@ -41,7 +41,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -82,7 +82,7 @@ - + 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..47e055191a6b2 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 @@ -2,7 +2,6 @@ import { Plugin } from "@html_editor/plugin"; import { registry } from "@web/core/registry"; import { CoverPropertiesOption } from "@website/builder/plugins/options/cover_properties_option"; import { classAction } from "@html_builder/core/core_builder_action_plugin"; -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"; @@ -43,22 +42,7 @@ class CoverPropertiesOptionPlugin extends Plugin { onlyImages: true, save: (imageEl) => { resultPromise = (async () => { - Object.assign(imageEl.dataset, await loadImageInfo(imageEl)); - let b64ToSave = false; - if ( - imageEl.dataset.mimetypeBeforeConversion && - !["image/gif", "image/svg+xml", "image/webp"].includes( - imageEl.dataset.mimetypeBeforeConversion - ) - ) { - // Convert to webp but keep original width. - const updateImgAttributes = - await this.dependencies.imagePostProcess.processImage(imageEl, { - formatMimetype: "image/webp", - }); - updateImgAttributes(); - b64ToSave = true; - } + const b64ToSave = imageEl.getAttribute("src").startsWith("data:"); return { imageSrc: imageEl.getAttribute("src"), b64ToSave }; })(); }, diff --git a/addons/website/static/src/builder/plugins/options/header_element_option.js b/addons/website/static/src/builder/plugins/options/header_element_option.js new file mode 100644 index 0000000000000..f37b86b90cb07 --- /dev/null +++ b/addons/website/static/src/builder/plugins/options/header_element_option.js @@ -0,0 +1,25 @@ +import { BaseOptionComponent } from "@html_builder/core/utils"; + +export class HeaderElementOption extends BaseOptionComponent { + static template = "website.headerElementOption"; + + setup(){ + super.setup(); + this.customizeWebsite = this.env.editor.shared.customizeWebsite; + const views = [ + "website.option_header_brand_logo", + "website.option_header_brand_name" + ]; + this.customizeWebsite.loadConfigKey({ views }); + } + + get websiteLogoParams(){ + const views = this.customizeWebsite.getConfigKey("website.option_header_brand_name") + ? ["website.option_header_brand_name"] + : ["website.option_header_brand_logo"]; + return { + views, + resetViewArch: true, + } + } +} diff --git a/addons/website/static/src/builder/plugins/options/header_option.xml b/addons/website/static/src/builder/plugins/options/header_option.xml index f7ff1f6d533f1..4eeea5adf335e 100644 --- a/addons/website/static/src/builder/plugins/options/header_option.xml +++ b/addons/website/static/src/builder/plugins/options/header_option.xml @@ -383,10 +383,7 @@ diff --git a/addons/website/static/src/builder/plugins/options/header_option_plugin.js b/addons/website/static/src/builder/plugins/options/header_option_plugin.js index b7320628e318b..922cfd9cfb4bc 100644 --- a/addons/website/static/src/builder/plugins/options/header_option_plugin.js +++ b/addons/website/static/src/builder/plugins/options/header_option_plugin.js @@ -12,6 +12,7 @@ import { Plugin } from "@html_editor/plugin"; import { withSequence } from "@html_editor/utils/resource"; import { registry } from "@web/core/registry"; import { HeaderBorderOption } from "./header_border_option"; +import { HeaderElementOption } from "./header_element_option"; const [ HEADER_TEMPLATE, @@ -74,7 +75,7 @@ class HeaderOptionPlugin extends Plugin { }), withSequence(HEADER_ELEMENT, { editableOnly: false, - template: "website.headerElementOption", + OptionComponent: HeaderElementOption, selector: "#wrapwrap > header", groups: ["website.group_website_designer"], }), diff --git a/addons/website/static/src/builder/plugins/options/image_gallery_option_plugin.js b/addons/website/static/src/builder/plugins/options/image_gallery_option_plugin.js index c5408cde1063c..b85c4cc67806b 100644 --- a/addons/website/static/src/builder/plugins/options/image_gallery_option_plugin.js +++ b/addons/website/static/src/builder/plugins/options/image_gallery_option_plugin.js @@ -164,6 +164,7 @@ class ImageGalleryOption extends Plugin { const onClose = this.dependencies.media.openMediaDialog({ onlyImages: true, multiImages: true, + node: editingElement, save: (images) => { selectedImages = images; resolve(); @@ -364,10 +365,10 @@ class ImageGalleryOption extends Plugin { !["image/gif", "image/svg+xml", "image/webp"].includes(mimetypeBeforeConversion) ) { // Convert to webp but keep original width. - const update = await this.dependencies.imagePostProcess.processImage(img, { + const update = await this.dependencies.imagePostProcess.processImage({ img, newDataset: { formatMimetype: "image/webp", ...newDataset, - }); + }}); update(); } }; diff --git a/addons/website/static/src/builder/plugins/options/image_snippet_option_plugin.js b/addons/website/static/src/builder/plugins/options/image_snippet_option_plugin.js index a10bbc4e4bf77..283fc0ec33b02 100644 --- a/addons/website/static/src/builder/plugins/options/image_snippet_option_plugin.js +++ b/addons/website/static/src/builder/plugins/options/image_snippet_option_plugin.js @@ -20,7 +20,8 @@ class ImageSnippetOptionPlugin extends Plugin { await new Promise((resolve) => { const onClose = this.dependencies.media.openMediaDialog({ onlyImages: true, - save: (selectedImageEl) => { + node: snippetEl, + save: async (selectedImageEl) => { isImageSelected = true; snippetEl.replaceWith(selectedImageEl); }, diff --git a/addons/website/static/src/snippets/s_dynamic_snippet_carousel/dynamic_snippet_carousel.edit.js b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/dynamic_snippet_carousel.edit.js new file mode 100644 index 0000000000000..6f148369bf1d4 --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/dynamic_snippet_carousel.edit.js @@ -0,0 +1,20 @@ +import { DynamicSnippetCarousel } from "@website/snippets/s_dynamic_snippet_carousel/dynamic_snippet_carousel"; +import { registry } from "@web/core/registry"; + +const DynamicSnippetCarouselEdit = (I) => + class extends I { + getConfigurationSnapshot() { + let snapshot = super.getConfigurationSnapshot(); + if (this.el.classList.contains("o_carousel_multi_items")) { + snapshot = JSON.parse(snapshot || "{}"); + snapshot.multi_items = true; + snapshot = JSON.stringify(snapshot); + } + return snapshot; + } + }; + +registry.category("public.interactions.edit").add("website.dynamic_snippet_carousel", { + Interaction: DynamicSnippetCarousel, + mixin: DynamicSnippetCarouselEdit, +}); diff --git a/addons/website/static/src/snippets/s_dynamic_snippet_carousel/dynamic_snippet_carousel.js b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/dynamic_snippet_carousel.js index 19299e3620b95..e7ddd76d21e5c 100644 --- a/addons/website/static/src/snippets/s_dynamic_snippet_carousel/dynamic_snippet_carousel.js +++ b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/dynamic_snippet_carousel.js @@ -28,9 +28,3 @@ export class DynamicSnippetCarousel extends DynamicSnippet { registry .category("public.interactions") .add("website.dynamic_snippet_carousel", DynamicSnippetCarousel); - -registry - .category("public.interactions.edit") - .add("website.dynamic_snippet_carousel", { - Interaction: DynamicSnippetCarousel, - }); diff --git a/addons/website/static/tests/builder/website_builder/image_snippet_option.test.js b/addons/website/static/tests/builder/website_builder/image_snippet_option.test.js index 052e3b61e4df1..d9eb54a241185 100644 --- a/addons/website/static/tests/builder/website_builder/image_snippet_option.test.js +++ b/addons/website/static/tests/builder/website_builder/image_snippet_option.test.js @@ -42,10 +42,10 @@ test("Drag & drop an 'Image' snippet opens the dialog to select an image", async expect(".o-website-builder_sidebar .fa-undo").not.toBeEnabled(); await contains(".o_select_media_dialog img[title='logo']").click(); - expect(".o_select_media_dialog").toHaveCount(0); await waitForEndOfOperation(); + expect(".o_select_media_dialog").toHaveCount(0); - expect(":iframe div img[src='/web/static/img/logo2.png']").toHaveCount(1); + expect(":iframe div img[src^='data:image/webp;base64,']").toHaveCount(1); expect(":iframe img").toHaveCount(1); expect(".o-website-builder_sidebar .fa-undo").toBeEnabled(); }); diff --git a/addons/website/static/tests/interactions/carousel/carousel_section_slider.edit.test.js b/addons/website/static/tests/interactions/carousel/carousel.edit.test.js similarity index 92% rename from addons/website/static/tests/interactions/carousel/carousel_section_slider.edit.test.js rename to addons/website/static/tests/interactions/carousel/carousel.edit.test.js index 56bfd2160ff89..3253a9c792bbd 100644 --- a/addons/website/static/tests/interactions/carousel/carousel_section_slider.edit.test.js +++ b/addons/website/static/tests/interactions/carousel/carousel.edit.test.js @@ -1,10 +1,5 @@ -import { - startInteractions, - setupInteractionWhiteList, -} from "@web/../tests/public/helpers"; - +import { startInteractions, setupInteractionWhiteList } from "@web/../tests/public/helpers"; import { describe, expect, test } from "@odoo/hoot"; - import { switchToEditMode } from "../../helpers"; import { queryAll } from "@odoo/hoot-dom"; @@ -12,8 +7,9 @@ setupInteractionWhiteList("website.carousel_edit"); describe.current.tags("interaction_dev"); -test("carousel_section_slider resets slide to attributes", async () => { - const { core } = await startInteractions(` +test("[EDIT] carousel_edit resets slide to attributes", async () => { + const { core } = await startInteractions( + `
- `, { waitForStart: true, editMode: true }); + `, + { waitForStart: true, editMode: true } + ); await switchToEditMode(core); expect(core.interactions).toHaveLength(1); diff --git a/addons/website/static/tests/interactions/carousel/carousel_bootstrap_upgrade_fix.edit.test.js b/addons/website/static/tests/interactions/carousel/carousel_bootstrap_upgrade_fix.edit.test.js index 154c33e7661e3..9f5ac679db935 100644 --- a/addons/website/static/tests/interactions/carousel/carousel_bootstrap_upgrade_fix.edit.test.js +++ b/addons/website/static/tests/interactions/carousel/carousel_bootstrap_upgrade_fix.edit.test.js @@ -1,52 +1,49 @@ -import { - startInteractions, - setupInteractionWhiteList, -} from "@web/../tests/public/helpers"; - +import { startInteractions, setupInteractionWhiteList } from "@web/../tests/public/helpers"; import { describe, expect, test } from "@odoo/hoot"; -import { queryOne } from "@odoo/hoot-dom"; - +import { click, queryOne } from "@odoo/hoot-dom"; +import { advanceTime } from "@odoo/hoot-mock"; import { switchToEditMode } from "../../helpers"; setupInteractionWhiteList("website.carousel_bootstrap_upgrade_fix"); describe.current.tags("interaction_dev"); -test("[EDIT] carousel_bootstrap_upgrade_fix prevents ride", async () => { - const { core } = await startInteractions(` - `; + +test("[EDIT] carousel_bootstrap_upgrade_fix prevents ride", async () => { + const { core } = await startInteractions(imageGalleryCarouselStyleSnippet); expect(core.interactions).toHaveLength(1); await switchToEditMode(core); const carouselEl = queryOne(".carousel"); @@ -54,3 +51,18 @@ test("[EDIT] carousel_bootstrap_upgrade_fix prevents ride", async () => { expect(carouselBS._config.ride).toBe(false); expect(carouselBS._config.pause).toBe(true); }); + +test("carousel_bootstrap_upgrade_fix is tagged while sliding", async () => { + const { core } = await startInteractions(imageGalleryCarouselStyleSnippet); + expect(core.interactions).toHaveLength(1); + + const carouselEl = queryOne(".carousel"); + expect(carouselEl).toHaveAttribute("data-bs-interval", "5000"); + expect(carouselEl).not.toHaveClass("o_carousel_sliding"); + + await click(carouselEl.querySelector(".carousel-control-next")); + + expect(carouselEl).toHaveClass("o_carousel_sliding"); + await advanceTime(750); + expect(carouselEl).not.toHaveClass("o_carousel_sliding"); +}); diff --git a/addons/website/static/tests/interactions/carousel/carousel_bootstrap_upgrade_fix.test.js b/addons/website/static/tests/interactions/carousel/carousel_bootstrap_upgrade_fix.test.js deleted file mode 100644 index 9f89ce958b04d..0000000000000 --- a/addons/website/static/tests/interactions/carousel/carousel_bootstrap_upgrade_fix.test.js +++ /dev/null @@ -1,60 +0,0 @@ -import { - startInteractions, - setupInteractionWhiteList, -} from "@web/../tests/public/helpers"; - -import { describe, expect, test } from "@odoo/hoot"; -import { click, queryOne } from "@odoo/hoot-dom"; -import { advanceTime } from "@odoo/hoot-mock"; - -setupInteractionWhiteList("website.carousel_bootstrap_upgrade_fix"); - -describe.current.tags("interaction_dev"); - -test("carousel_bootstrap_upgrade_fix is tagged while sliding", async () => { - const { core } = await startInteractions(` - - `); - expect(core.interactions).toHaveLength(1); - - const carouselEl = queryOne(".carousel"); - expect(carouselEl).toHaveAttribute("data-bs-interval", "5000"); - expect(carouselEl).not.toHaveClass("o_carousel_sliding"); - - await click(carouselEl.querySelector(".carousel-control-next")); - - expect(carouselEl).toHaveClass("o_carousel_sliding"); - await advanceTime(750); - expect(carouselEl).not.toHaveClass("o_carousel_sliding"); -}); diff --git a/addons/website/static/tests/interactions/carousel/carousel_slider.edit.test.js b/addons/website/static/tests/interactions/carousel/carousel_slider.edit.test.js deleted file mode 100644 index 4e3b80aeba641..0000000000000 --- a/addons/website/static/tests/interactions/carousel/carousel_slider.edit.test.js +++ /dev/null @@ -1,106 +0,0 @@ -import { setupInteractionWhiteList, startInteractions } from "@web/../tests/public/helpers"; - -import { describe, expect, test } from "@odoo/hoot"; -import { manuallyDispatchProgrammaticEvent, queryFirst, queryOne } from "@odoo/hoot-dom"; - -import { switchToEditMode } from "../../helpers"; - -setupInteractionWhiteList("website.carousel_edit"); - -describe.current.tags("interaction_dev"); - -// TODO: @mysterious-egg -test.todo("[EDIT] carousel_slider prevents ride", async () => { - const { core } = await startInteractions(` -
- -
- `, - { editMode: true } - ); - await switchToEditMode(core); - - expect(core.interactions).toHaveLength(1); - const carouselEl = queryOne(".carousel"); - const carouselBS = window.Carousel.getInstance(carouselEl); - expect(carouselBS._config.ride).toBe(false); - expect(carouselBS._config.pause).toBe(true); - - core.stopInteractions(); - - expect(core.interactions).toHaveLength(0); - expect(carouselEl).toHaveAttribute("data-bs-ride", "noAutoSlide"); -}); - -// TODO: @mysterious-egg -test.todo("[EDIT] carousel_slider updates min height on content_changed", async () => { - const { core } = await startInteractions(` - - `, - { editMode: true } - ); - await switchToEditMode(core); - - expect(core.interactions).toHaveLength(1); - const carouselEl = queryOne(".carousel"); - const itemEl = queryFirst(".carousel-item"); - const maxHeight = itemEl.style.minHeight; - itemEl.style.minHeight = ""; - expect(itemEl).not.toHaveStyle({ minHeight: maxHeight }); - await manuallyDispatchProgrammaticEvent(carouselEl, "content_changed"); - expect(itemEl).toHaveStyle({ minHeight: maxHeight }); -}); diff --git a/addons/website/static/tests/interactions/carousel/carousel_slider.test.js b/addons/website/static/tests/interactions/carousel/carousel_slider.test.js index 06a240ee01b78..1606a8deb3221 100644 --- a/addons/website/static/tests/interactions/carousel/carousel_slider.test.js +++ b/addons/website/static/tests/interactions/carousel/carousel_slider.test.js @@ -1,5 +1,4 @@ import { setupInteractionWhiteList, startInteractions } from "@web/../tests/public/helpers"; - import { beforeEach, describe, expect, test } from "@odoo/hoot"; import { queryAll } from "@odoo/hoot-dom"; import { enableTransitions } from "@odoo/hoot-mock"; diff --git a/addons/website_sale/static/src/website_builder/attachment_media_dialog.js b/addons/website_sale/static/src/website_builder/attachment_media_dialog.js deleted file mode 100644 index 769782d642185..0000000000000 --- a/addons/website_sale/static/src/website_builder/attachment_media_dialog.js +++ /dev/null @@ -1,10 +0,0 @@ -import { MediaDialog } from "@html_editor/main/media/media_dialog/media_dialog"; -import { useChildSubEnv } from "@odoo/owl"; - -// Small override of the MediaDialog to retrieve the attachment ids instead of img elements -export class AttachmentMediaDialog extends MediaDialog { - setup() { - super.setup(); - useChildSubEnv({ addFieldImage: true }); - } -} diff --git a/addons/website_sale/static/src/website_builder/checkout_page_option.xml b/addons/website_sale/static/src/website_builder/checkout_page_option.xml index 2d9954067ecd4..0bc648f590748 100644 --- a/addons/website_sale/static/src/website_builder/checkout_page_option.xml +++ b/addons/website_sale/static/src/website_builder/checkout_page_option.xml @@ -2,10 +2,10 @@ + + + - - - diff --git a/addons/website_sale/static/src/website_builder/checkout_page_option_plugin.js b/addons/website_sale/static/src/website_builder/checkout_page_option_plugin.js index 8fbd352e5259d..34833fc188e42 100644 --- a/addons/website_sale/static/src/website_builder/checkout_page_option_plugin.js +++ b/addons/website_sale/static/src/website_builder/checkout_page_option_plugin.js @@ -1,9 +1,11 @@ import { Plugin } from "@html_editor/plugin"; -import { registry } from "@web/core/registry"; import { _t } from "@web/core/l10n/translation"; +import { rpc } from "@web/core/network/rpc"; +import { registry } from "@web/core/registry"; class CheckoutPageOptionPlugin extends Plugin { static id = "checkoutPageOption"; + static dependencies = ["builderActions"]; resources = { builder_options: [ { @@ -14,7 +16,32 @@ class CheckoutPageOptionPlugin extends Plugin { groups: ["website.group_website_designer"], }, ], + builder_actions: this.getActions(), }; + getActions() { + const plugin = this; + return { + get setExtraStep() { + const websiteConfigAction = + plugin.dependencies.builderActions.getAction("websiteConfig"); + return { + ...websiteConfigAction, + apply: async (...args) => { + await Promise.all([ + websiteConfigAction.apply(...args), + rpc("/shop/config/website", { extra_step: "true" }), + ]); + }, + clean: async (...args) => { + await Promise.all([ + websiteConfigAction.clean(...args), + rpc("/shop/config/website", { extra_step: "false" }), + ]); + }, + }; + }, + }; + } } registry.category("website-plugins").add(CheckoutPageOptionPlugin.id, CheckoutPageOptionPlugin); diff --git a/addons/website_sale/static/src/website_builder/product_page_option_plugin.js b/addons/website_sale/static/src/website_builder/product_page_option_plugin.js index 5c7b8ba0bcf26..da4ddbe5cd29d 100644 --- a/addons/website_sale/static/src/website_builder/product_page_option_plugin.js +++ b/addons/website_sale/static/src/website_builder/product_page_option_plugin.js @@ -2,7 +2,6 @@ import { Plugin } from "@html_editor/plugin"; import { registry } from "@web/core/registry"; import { _t } from "@web/core/l10n/translation"; import { ProductPageOption } from "./product_page_option"; -import { AttachmentMediaDialog } from "./attachment_media_dialog"; import { rpc } from "@web/core/network/rpc"; import { isImageCorsProtected } from "@html_editor/utils/image"; import { TABS } from "@html_editor/main/media/media_dialog/media_dialog"; @@ -10,7 +9,7 @@ import { TABS } from "@html_editor/main/media/media_dialog/media_dialog"; export const productPageSelector = "main:has(.o_wsale_product_page)"; class ProductPageOptionPlugin extends Plugin { static id = "productPageOption"; - static dependencies = ["builderActions", "dialog", "customizeWebsite"]; + static dependencies = ["builderActions", "media", "customizeWebsite"]; resources = { builder_options: { OptionComponent: ProductPageOption, @@ -183,10 +182,12 @@ class ProductPageOptionPlugin extends Plugin { ); } await new Promise((resolve) => { - const onClose = this.dependencies.dialog.addDialog(AttachmentMediaDialog, { + const onClose = this.dependencies.media.openMediaDialog({ + addFieldImage: true, multiImages: true, noDocuments: true, noIcons: true, + node: el, // Kinda hack-ish but the regular save does not get the information we need save: async (imgEls, selectedMedia, activeTab) => { if (selectedMedia.length) { From 433d69478e059b6025f6edb6b5f5e51ad80d5666 Mon Sep 17 00:00:00 2001 From: emge-odoo Date: Wed, 21 May 2025 17:17:11 +0200 Subject: [PATCH 10/10] fix tour --- .../static/tests/tours/link_to_document.js | 58 ++++++++++++++----- addons/website/tests/test_attachment.py | 1 - 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/addons/website/static/tests/tours/link_to_document.js b/addons/website/static/tests/tours/link_to_document.js index ea2562a094547..93aee814fe532 100644 --- a/addons/website/static/tests/tours/link_to_document.js +++ b/addons/website/static/tests/tours/link_to_document.js @@ -22,6 +22,17 @@ const unpatchStep = { run: () => unpatch(), }; +function showLinkPopover(linkEl) { + const doc = linkEl.ownerDocument; + const selection = doc.getSelection(); + const range = doc.createRange(); + selection.removeAllRanges(); + range.setStart(linkEl.childNodes[0], 0); + range.setEnd(linkEl.childNodes[0], 0); + doc.dispatchEvent(new Event("focus")); + selection.addRange(range); +} + /** * The purpose of this tour is to check the Linktools to create a link to an * uploaded document. @@ -41,31 +52,52 @@ registerWebsitePreviewTour( { content: "Click on button Start Now", trigger: ":iframe #wrap .s_banner a:nth-child(1)", - run: "click", + run: ({ anchor }) => { + showLinkPopover(anchor); + }, }, - patchStep, { - content: "Click on link to an uploaded document", - trigger: ".o_url_input .o_we_user_value_widget.fa.fa-upload", + content: "Click on edit link", + trigger: ".o-we-linkpopover .o_we_edit_link", run: "click", }, { - content: "Check if a document link is created", - trigger: ":iframe #wrap .s_banner .oe_edited_link[href^='/web/content']", + content: "Make upload button appear by emptying the link input", + trigger: ".o_we_href_input_link", + run: ({ anchor }) => { + anchor.value = ""; + anchor.dispatchEvent(new Event("input")); + }, }, - unpatchStep, + patchStep, { - content: "Check if by default the option auto-download is enabled", - trigger: ":iframe #wrap .s_banner .oe_edited_link[href$='download=true']", + content: "Click on upload button", + trigger: ".o_we_href_input_link + span button", + run: "click", }, { - content: "Deactivate direct download", - trigger: ".o_switch > we-checkbox[name='direct_download']", + content: "Click on apply", + trigger: ".o_we_apply_link", run: "click", }, { - content: "Check if auto-download is disabled", - trigger: ":iframe #wrap .s_banner .oe_edited_link:not([href$='download=true'])", + content: "Check if a document link is created", + trigger: ":iframe #wrap .s_banner a[href^='/web/content']", }, + unpatchStep, + // TODO: find where this option is, if it is still present + // { + // content: "Check if by default the option auto-download is enabled", + // trigger: ":iframe #wrap .s_banner .oe_edited_link[href$='download=true']", + // }, + // { + // content: "Deactivate direct download", + // trigger: ".o_switch > we-checkbox[name='direct_download']", + // run: "click", + // }, + // { + // content: "Check if auto-download is disabled", + // trigger: ":iframe #wrap .s_banner .oe_edited_link:not([href$='download=true'])", + // }, ] ); diff --git a/addons/website/tests/test_attachment.py b/addons/website/tests/test_attachment.py index cb6b580b6a3ed..21cf1230501cc 100644 --- a/addons/website/tests/test_attachment.py +++ b/addons/website/tests/test_attachment.py @@ -41,7 +41,6 @@ def test_01_type_url_301_image(self): def test_02_image_quality(self): self.start_tour(self.env['website'].get_client_action_url('/'), 'website_image_quality', login="admin") - @unittest.skip def test_03_link_to_document(self): text = b'Lorem Ipsum' self.env['ir.attachment'].create({