diff --git a/addons/mass_mailing/views/snippets/s_blockquote.xml b/addons/mass_mailing/views/snippets/s_blockquote.xml
index 2d1fd2e9bd74d..6cd79d0463b23 100644
--- a/addons/mass_mailing/views/snippets/s_blockquote.xml
+++ b/addons/mass_mailing/views/snippets/s_blockquote.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_call_to_action.xml b/addons/mass_mailing/views/snippets/s_call_to_action.xml
index 2b089107fb07e..9b4406ed42d9e 100644
--- a/addons/mass_mailing/views/snippets/s_call_to_action.xml
+++ b/addons/mass_mailing/views/snippets/s_call_to_action.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_color_blocks_2.xml b/addons/mass_mailing/views/snippets/s_color_blocks_2.xml
index 4cf8a2b1ef272..c7036bc43c365 100644
--- a/addons/mass_mailing/views/snippets/s_color_blocks_2.xml
+++ b/addons/mass_mailing/views/snippets/s_color_blocks_2.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_company_team.xml b/addons/mass_mailing/views/snippets/s_company_team.xml
index fa36b2b4b997d..aeb9afc28c0e3 100644
--- a/addons/mass_mailing/views/snippets/s_company_team.xml
+++ b/addons/mass_mailing/views/snippets/s_company_team.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_comparisons.xml b/addons/mass_mailing/views/snippets/s_comparisons.xml
index 29398bf4278a7..fb3e18d6ddcb1 100644
--- a/addons/mass_mailing/views/snippets/s_comparisons.xml
+++ b/addons/mass_mailing/views/snippets/s_comparisons.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_coupon_code.xml b/addons/mass_mailing/views/snippets/s_coupon_code.xml
index 1d1663717ea55..2219ef437c93d 100644
--- a/addons/mass_mailing/views/snippets/s_coupon_code.xml
+++ b/addons/mass_mailing/views/snippets/s_coupon_code.xml
@@ -1,7 +1,7 @@
-
+
GET $20 OFF
Here's your coupon code - but hurry! Ends 9/28
@@ -16,6 +16,6 @@
Use now
-
+
diff --git a/addons/mass_mailing/views/snippets/s_cover.xml b/addons/mass_mailing/views/snippets/s_cover.xml
index 9678f84c72e4b..5c556b842c3d8 100644
--- a/addons/mass_mailing/views/snippets/s_cover.xml
+++ b/addons/mass_mailing/views/snippets/s_cover.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_event.xml b/addons/mass_mailing/views/snippets/s_event.xml
index 247bf175baf34..3d4fa4119ee65 100644
--- a/addons/mass_mailing/views/snippets/s_event.xml
+++ b/addons/mass_mailing/views/snippets/s_event.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_features.xml b/addons/mass_mailing/views/snippets/s_features.xml
index 6712702542544..8822eb2951b41 100644
--- a/addons/mass_mailing/views/snippets/s_features.xml
+++ b/addons/mass_mailing/views/snippets/s_features.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_features_grid.xml b/addons/mass_mailing/views/snippets/s_features_grid.xml
index 9b478dec29455..9442d4c7592e9 100644
--- a/addons/mass_mailing/views/snippets/s_features_grid.xml
+++ b/addons/mass_mailing/views/snippets/s_features_grid.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_image_text.xml b/addons/mass_mailing/views/snippets/s_image_text.xml
index 6e7eb9b18d64f..7e896b867bec5 100644
--- a/addons/mass_mailing/views/snippets/s_image_text.xml
+++ b/addons/mass_mailing/views/snippets/s_image_text.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_masonry_block.xml b/addons/mass_mailing/views/snippets/s_masonry_block.xml
index 574d8e9e3d481..376b222a1d0dd 100644
--- a/addons/mass_mailing/views/snippets/s_masonry_block.xml
+++ b/addons/mass_mailing/views/snippets/s_masonry_block.xml
@@ -3,11 +3,11 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_media_list.xml b/addons/mass_mailing/views/snippets/s_media_list.xml
index 32c3ba7f0d8be..56b5c7cd8490a 100644
--- a/addons/mass_mailing/views/snippets/s_media_list.xml
+++ b/addons/mass_mailing/views/snippets/s_media_list.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_numbers.xml b/addons/mass_mailing/views/snippets/s_numbers.xml
index 329def3522420..04ac9e4daa2d2 100644
--- a/addons/mass_mailing/views/snippets/s_numbers.xml
+++ b/addons/mass_mailing/views/snippets/s_numbers.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_picture.xml b/addons/mass_mailing/views/snippets/s_picture.xml
index 018f0794aa7df..3147395270f3a 100644
--- a/addons/mass_mailing/views/snippets/s_picture.xml
+++ b/addons/mass_mailing/views/snippets/s_picture.xml
@@ -2,7 +2,7 @@
-
+
A Punchy Headline
@@ -11,7 +11,7 @@
Add a caption to enhance the meaning of this image.
-
+
diff --git a/addons/mass_mailing/views/snippets/s_product_list.xml b/addons/mass_mailing/views/snippets/s_product_list.xml
index 905bb2332e3dd..bd0336888b078 100644
--- a/addons/mass_mailing/views/snippets/s_product_list.xml
+++ b/addons/mass_mailing/views/snippets/s_product_list.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_rating.xml b/addons/mass_mailing/views/snippets/s_rating.xml
index ee10b0195b2ca..54052a1255c79 100644
--- a/addons/mass_mailing/views/snippets/s_rating.xml
+++ b/addons/mass_mailing/views/snippets/s_rating.xml
@@ -2,7 +2,7 @@
-
+
Quality
diff --git a/addons/mass_mailing/views/snippets/s_references.xml b/addons/mass_mailing/views/snippets/s_references.xml
index 3f6b3b30f870b..fb125ff9230df 100644
--- a/addons/mass_mailing/views/snippets/s_references.xml
+++ b/addons/mass_mailing/views/snippets/s_references.xml
@@ -2,7 +2,7 @@
-
+
Our References
We are in good company.
@@ -21,7 +21,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_showcase.xml b/addons/mass_mailing/views/snippets/s_showcase.xml
index 0f75a28ce2a1d..8b662bdcfe891 100644
--- a/addons/mass_mailing/views/snippets/s_showcase.xml
+++ b/addons/mass_mailing/views/snippets/s_showcase.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_text_block.xml b/addons/mass_mailing/views/snippets/s_text_block.xml
index 3905c90c71ab1..ae8b670a94cfb 100644
--- a/addons/mass_mailing/views/snippets/s_text_block.xml
+++ b/addons/mass_mailing/views/snippets/s_text_block.xml
@@ -1,7 +1,7 @@
-
+
The open source model of Odoo has allowed us to leverage thousands of developers and
business experts to build hundreds of apps in just a few years.
@@ -11,6 +11,6 @@
of our fully integrated apps.
That way, Odoo evolves much faster than any other solution.
-
+
diff --git a/addons/mass_mailing/views/snippets/s_text_highlight.xml b/addons/mass_mailing/views/snippets/s_text_highlight.xml
index 8906decfd8fdb..b8555deedfaea 100644
--- a/addons/mass_mailing/views/snippets/s_text_highlight.xml
+++ b/addons/mass_mailing/views/snippets/s_text_highlight.xml
@@ -2,7 +2,7 @@
-
+
Text Highlight
Put the focus on what you have to say!
diff --git a/addons/mass_mailing/views/snippets/s_text_image.xml b/addons/mass_mailing/views/snippets/s_text_image.xml
index 4284f550a3f7c..8f7abe728887f 100644
--- a/addons/mass_mailing/views/snippets/s_text_image.xml
+++ b/addons/mass_mailing/views/snippets/s_text_image.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_three_columns.xml b/addons/mass_mailing/views/snippets/s_three_columns.xml
index e6db121f3e35c..f0d361b9ffcc2 100644
--- a/addons/mass_mailing/views/snippets/s_three_columns.xml
+++ b/addons/mass_mailing/views/snippets/s_three_columns.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/mass_mailing/views/snippets/s_title.xml b/addons/mass_mailing/views/snippets/s_title.xml
index a1791a759b227..fd7c369351b6c 100644
--- a/addons/mass_mailing/views/snippets/s_title.xml
+++ b/addons/mass_mailing/views/snippets/s_title.xml
@@ -2,11 +2,11 @@
-
+
diff --git a/addons/mass_mailing/views/snippets_themes.xml b/addons/mass_mailing/views/snippets_themes.xml
index 2424e0bc33011..d37896462d0e3 100644
--- a/addons/mass_mailing/views/snippets_themes.xml
+++ b/addons/mass_mailing/views/snippets_themes.xml
@@ -25,54 +25,69 @@
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
@@ -82,7 +97,7 @@
-
+
-
diff --git a/addons/mass_mailing/views/themes_templates.xml b/addons/mass_mailing/views/themes_templates.xml
index c541e53583945..56aad93377689 100644
--- a/addons/mass_mailing/views/themes_templates.xml
+++ b/addons/mass_mailing/views/themes_templates.xml
@@ -31,7 +31,7 @@
border-top-color: #ced4da !important;
}
-
-
+
+
Thank you for joining us!
We want to take this opportunity to welcome you to our ever-growing community!
@@ -57,7 +57,7 @@
LOGIN
-
+
diff --git a/addons/mass_mailing_egg/__manifest__.py b/addons/mass_mailing_egg/__manifest__.py
new file mode 100644
index 0000000000000..892dc7ef6cef8
--- /dev/null
+++ b/addons/mass_mailing_egg/__manifest__.py
@@ -0,0 +1,63 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'Email Marketing Egg',
+ 'summary': 'Design, send and track emails',
+ 'version': '1.0',
+ 'sequence': 61,
+ 'website': 'https://www.odoo.com/app/email-marketing',
+ 'category': 'Hidden',
+ 'auto_install': True,
+ 'depends': [
+ 'mass_mailing',
+ 'html_builder',
+ 'html_editor',
+ ],
+ 'data': [
+ 'views/mailing_mailing_views.xml'
+ ],
+ 'assets': {
+ 'mass_mailing_egg.assets_builder': [ # equivalent html_builder.assets in website_builder_action.xml
+ # lazy builder assets NOT applied in iframe
+ ('include', 'html_builder.assets'),
+ 'mass_mailing_egg/static/src/builder/**/*',
+ ],
+ # TODO EGGMAIL: evaluate if necessary to have interactions for mass_mailing
+ # 'mass_mailing_egg.assets_iframe_core': [ # web.assets_frontend lite, minimal env to spawn interactions
+ # # minimal JS assets required to view the mail content
+ # ],
+ 'mass_mailing_egg.assets_iframe_style': [ # equivalent website.inside_builder_style in website_builder_action.js
+ # minimal style assets required to view the mail content
+ # convert_inline ONLY uses this and inline styles.
+ #
+ # TODO EGGMAIL: recreate a scss bundle that is strictly used in relation with convert_inline
+ # if there is "editor style" to include, put it in `mass_mailing_egg.assets_iframe_edit``
+ ### CSS part of wysiwyg_iframe_editor_assets
+
+ ### iframe_css_assets_edit fast selection
+ ('include', 'mass_mailing.iframe_css_assets_edit'),
+ ('include', 'web_editor.wysiwyg_iframe_editor_assets'),
+ ('include', 'html_builder.inside_builder_style'),
+ 'mass_mailing_egg/static/src/iframe_assets/**/*',
+ ],
+ 'mass_mailing_egg.assets_iframe_edit': [ # equivalent html_builder.assets_edit_frontend in website_builder_action.js
+ # JS and style assets required to edit the mail content
+ ],
+ 'mass_mailing_egg.assets_iframe_dark': [ # separated complement of assets_iframe_style for dark mode
+ # style assets for dark mode. Not used by convert_inline.
+ # TODO EGGMAIL: investigate how this can behave properly with convert_inline (i.e. user chooses pretty colors for
+ # dark mode, but these colors are not pretty when the dark mode is not there anymore after sending the mail).
+ ],
+ 'web.assets_backend': [
+ 'mass_mailing_egg/static/src/fields/**/*',
+ 'mass_mailing_egg/static/src/themes/**/*',
+ 'mass_mailing_egg/static/src/mobile_preview_dialog/**/*',
+ 'mass_mailing_egg/static/src/iframe/**/*',
+ ],
+ 'web.assets_unit_tests': [
+ # 'mass_mailing_egg/static/tests/**/*',
+ ],
+ },
+ 'author': 'Odoo S.A.',
+ 'license': 'LGPL-3',
+}
diff --git a/addons/mass_mailing_egg/snippets_progress_bar_WIP.xml b/addons/mass_mailing_egg/snippets_progress_bar_WIP.xml
new file mode 100644
index 0000000000000..6e7ec55380525
--- /dev/null
+++ b/addons/mass_mailing_egg/snippets_progress_bar_WIP.xml
@@ -0,0 +1,311 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Border
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Small
+ Medium
+ Large
+
+
+
+
+
+
+
+
+
+ -->
diff --git a/addons/mass_mailing_egg/static/src/builder/builder_overlay_patch.js b/addons/mass_mailing_egg/static/src/builder/builder_overlay_patch.js
new file mode 100644
index 0000000000000..33662afa5149b
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/builder_overlay_patch.js
@@ -0,0 +1,11 @@
+import { sizingX, sizingY } from "@html_builder/core/builder_overlay/builder_overlay";
+import { patch } from "@web/core/utils/patch";
+
+patch(sizingX, {
+ selector: sizingX.selector + ", .row > div",
+ exclude: sizingX.exclude + ", .o_mail_no_resize, .o_mail_no_options, .s_col_no_resize.row > div, .s_col_no_resize",
+})
+patch(sizingY, {
+ selector: sizingY.selector + ", .o_mail_snippet_general, .o_mail_snippet_general .row > div",
+ exclude: sizingY.exclude + ", .o_mail_no_resize, .o_mail_no_options, .s_col_no_resize.row > div, .s_col_no_resize",
+})
diff --git a/addons/mass_mailing_egg/static/src/builder/mass_mailing_builder.js b/addons/mass_mailing_egg/static/src/builder/mass_mailing_builder.js
new file mode 100644
index 0000000000000..2cf3a5fad8134
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/mass_mailing_builder.js
@@ -0,0 +1,57 @@
+import { Builder } from "@html_builder/builder";
+import { CORE_PLUGINS as CORE_BUILDER_PLUGINS } from "@html_builder/core/core_plugins";
+import { removePlugins } from "@html_builder/utils/utils";
+import { MAIN_PLUGINS as MAIN_EDITOR_PLUGINS } from "@html_editor/plugin_sets";
+import { Component } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { DYNAMIC_PLACEHOLDER_PLUGINS } from "@html_editor/backend/plugin_sets";
+
+export class MassMailingBuilder extends Component {
+ static template = "mass_mailing_egg.MassMailingBuilder";
+ static components = { Builder };
+ static props = {
+ builderProps: { type: Object },
+ };
+
+ get builderProps() {
+ const builderProps = Object.assign({}, this.props.builderProps);
+ const massMailingPlugins = [
+ ...registry.category("builder-plugins").getAll(),
+ // TODO EGGMAIL: use this registry for mass_mailing exclusive plugins
+ ...registry.category("mass_mailing-plugins").getAll(),
+ ];
+ // TODO EGGMAIL: copied from website, check if something needs to be changed here
+ const mainEditorPluginsToRemove = [
+ "PowerButtonsPlugin",
+ "DoubleClickImagePreviewPlugin",
+ "SeparatorPlugin",
+ "StarPlugin",
+ "BannerPlugin",
+ "MoveNodePlugin",
+ ];
+ const mainEditorPlugins = removePlugins(
+ [...MAIN_EDITOR_PLUGINS],
+ mainEditorPluginsToRemove
+ );
+ const coreBuilderPluginsToRemove = ["SavePlugin"];
+ const builderEditorPlugins = removePlugins(
+ [...CORE_BUILDER_PLUGINS],
+ coreBuilderPluginsToRemove
+ );
+ const optionalPlugins = [
+ ...(this.props.builderProps.config.dynamicPlaceholder
+ ? DYNAMIC_PLACEHOLDER_PLUGINS
+ : []),
+ ];
+ const Plugins = [
+ ...mainEditorPlugins,
+ ...builderEditorPlugins,
+ ...massMailingPlugins,
+ ...optionalPlugins,
+ ];
+ builderProps.Plugins = Plugins;
+ return builderProps;
+ }
+}
+
+registry.category("lazy_components").add("mass_mailing_egg.MassMailingBuilder", MassMailingBuilder);
diff --git a/addons/mass_mailing_egg/static/src/builder/mass_mailing_builder.xml b/addons/mass_mailing_egg/static/src/builder/mass_mailing_builder.xml
new file mode 100644
index 0000000000000..e3ac11c4e56ec
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/mass_mailing_builder.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/builder/options/alert_option.xml b/addons/mass_mailing_egg/static/src/builder/options/alert_option.xml
new file mode 100644
index 0000000000000..e15cef6356f8b
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/options/alert_option.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ Small
+ Medium
+ Large
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/builder/options/blockquote_option.xml b/addons/mass_mailing_egg/static/src/builder/options/blockquote_option.xml
new file mode 100644
index 0000000000000..d338437889088
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/options/blockquote_option.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Auto
+ 50%
+ 100%
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/builder/options/border_options.xml b/addons/mass_mailing_egg/static/src/builder/options/border_options.xml
new file mode 100644
index 0000000000000..8828af40d2172
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/options/border_options.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/builder/options/horizontal_padding_option.xml b/addons/mass_mailing_egg/static/src/builder/options/horizontal_padding_option.xml
new file mode 100644
index 0000000000000..e0dcdb801ab24
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/options/horizontal_padding_option.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/builder/options/image_tool_option.xml b/addons/mass_mailing_egg/static/src/builder/options/image_tool_option.xml
new file mode 100644
index 0000000000000..a664bd5c0f207
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/options/image_tool_option.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ Left
+ Center
+ Right
+
+
+
+
+
+
+
+
+ true
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/builder/options/masonry_block_template_option.xml b/addons/mass_mailing_egg/static/src/builder/options/masonry_block_template_option.xml
new file mode 100644
index 0000000000000..64322afee361b
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/options/masonry_block_template_option.xml
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
+
+
+
+
+
+
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
+
+
+
+
+
+
+
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
+
+
+
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
+
+
+
+
+
+
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
+
+
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
+
+
+
+
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
A great title
+
And a great subtitle
+
+
+
+
+
+
+
+
A great title
+
And a great subtitle
+
+
+
+
A great title
+
And a great subtitle
+
+
+
+
+
+
+
+
+
+
A great title
+
And a great subtitle
+
+
+
+
A great title
+
And a great subtitle
+
+
+
+
+
+
+
+
A great title
+
And a great subtitle
+
+
+
+
A great title
+
And a great subtitle
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/builder/options/padding_option.xml b/addons/mass_mailing_egg/static/src/builder/options/padding_option.xml
new file mode 100644
index 0000000000000..10146012f9e97
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/options/padding_option.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/alert_option_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/alert_option_plugin.js
new file mode 100644
index 0000000000000..f72aae742b655
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/alert_option_plugin.js
@@ -0,0 +1,30 @@
+import { after, WIDTH } from "@html_builder/utils/option_sequence";
+import { Plugin } from "@html_editor/plugin";
+import { withSequence } from "@html_editor/utils/resource";
+import { registry } from "@web/core/registry";
+
+class AlertOptionPlugin extends Plugin {
+ static id = "mass_mailing.AlertOption";
+ resources = {
+ builder_options: [
+ withSequence(after(WIDTH), {
+ selector: ".s_mail_alert .s_alert",
+ template: "mass_mailing.AlertOption",
+ }),
+ withSequence(after(WIDTH), {
+ selector: ".s_mail_alert .s_alert",
+ template: "mass_mailing.BorderOptionNoRoundedCornersDependency"
+ }),
+ ],
+ patch_builder_options: [
+ {
+ target_name: "alertTypeOption",
+ target_element: "exclude",
+ method: "replace",
+ value: ".s_mail_alert .s_alert",
+ }
+ ],
+ }
+}
+
+registry.category("mass_mailing-plugins").add(AlertOptionPlugin.id, AlertOptionPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/background_option_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/background_option_plugin.js
new file mode 100644
index 0000000000000..48db68cdc6d9c
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/background_option_plugin.js
@@ -0,0 +1,22 @@
+import { BackgroundOption } from "@html_builder/plugins/background_option/background_option";
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+class BackgroundOptionPlugin extends Plugin {
+ static id = "mass_mailing.BackgroundOption";
+ resources = {
+ builder_options: [
+ {
+ selector: ".s_masonry_block .row > div, .s_cover .oe_img_bg",
+ OptionComponent: BackgroundOption,
+ props: {
+ withImages: true,
+ withShapes: false,
+ withColors: true,
+ }
+ }
+ ]
+ }
+}
+
+registry.category("mass_mailing-plugins").add(BackgroundOptionPlugin.id, BackgroundOptionPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/block_alignment_option_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/block_alignment_option_plugin.js
new file mode 100644
index 0000000000000..bae89468f4cd2
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/block_alignment_option_plugin.js
@@ -0,0 +1,18 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+class BlockAlignmentOptionPlugin extends Plugin {
+ static id = "mass_mailing.BlockAlignmentOption";
+ resources = {
+ builder_options: [
+ {
+ template: "html_builder.BlockAlignmentOption",
+ selector: ".s_mail_alert .s_alert, .s_mail_blockquote, .s_mail_text_highlight"
+ }
+ ]
+ }
+}
+
+registry
+ .category("mass_mailing-plugins")
+ .add(BlockAlignmentOptionPlugin.id, BlockAlignmentOptionPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/border_option_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/border_option_plugin.js
new file mode 100644
index 0000000000000..e957e6ae08c91
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/border_option_plugin.js
@@ -0,0 +1,27 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+export class BorderOptionPlugin extends Plugin {
+ static id = "mass_mailing.BorderOption";
+ resources = {
+ builder_options: [
+ {
+ template: "mass_mailing.BorderOptionNoRoundedCornersDependency",
+ selector: ".s_three_columns .row > div, .s_comparisons .row > div, .s_mail_block_event .row > div",
+ applyTo: ".card",
+ },
+ {
+ template: "mass_mailing.BorderOption",
+ selector: ".o_mail_block_discount2",
+ applyTo: "table",
+ },
+ {
+ template: "mass_mailing.BorderOption",
+ selector: ".row > div",
+ exclude: ".o_mail_wrapper_td, .s_col_no_bgcolor, .s_col_no_bgcolor.row > div, .s_image_gallery .row > div",
+ }
+ ],
+ }
+}
+
+registry.category("mass_mailing-plugins").add(BorderOptionPlugin.id, BorderOptionPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/colorpicker_option_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/colorpicker_option_plugin.js
new file mode 100644
index 0000000000000..063468d7e172b
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/colorpicker_option_plugin.js
@@ -0,0 +1,27 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+class ColorPickerOptionPlugin extends Plugin {
+ static id = "mass_mailing.ColorPicker";
+ colorPickerSelector = `.note-editable .oe_structure > div:not(:has(> .o_mail_snippet_general)),
+ .note-editable .oe_structure > .o_mail_snippet_general,
+ .note-editable .oe_structure > .o_mail_snippet_general .o_cc,
+ .s_mail_color_blocks_2 .row > div`;
+
+ resources = {
+ builder_options: [
+ { // Generic option
+ template: "mass_mailing.ColorPickerOption",
+ selector: this.colorPickerSelector,
+ exclude: ".o_mail_no_colorpicker, .o_mail_no_options, .s_mail_color_blocks_2"
+ },
+ {
+ template: "mass_mailing.ColorPickerOption",
+ selector: ".s_three_columns .row > div, .s_comparisons .row > div, .s_mail_block_event .row > div",
+ applyTo: ".card-body"
+ }
+ ]
+ }
+}
+
+registry.category("mass_mailing-plugins").add(ColorPickerOptionPlugin.id, ColorPickerOptionPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/column_option_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/column_option_plugin.js
new file mode 100644
index 0000000000000..097e4a3077e50
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/column_option_plugin.js
@@ -0,0 +1,24 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+import { BorderConfigurator } from "@html_builder/plugins/border_configurator_option";
+import { _t } from "@web/core/l10n/translation";
+
+class ColumnOptionPlugin extends Plugin {
+ static id = "columnPlugin";
+ selector = ".col-lg";
+ resources = {
+ mark_color_level_selector_params: [{ selector: this.selector }],
+ builder_options: [
+ {
+ OptionComponent: BorderConfigurator,
+ selector: this.selector,
+ props: {
+ label: _t("Border")
+ }
+ }
+ ],
+ };
+}
+// TODO: as in master, the position of a background image does not work
+// correctly.
+registry.category("mass_mailing-plugins").add(ColumnOptionPlugin.id, ColumnOptionPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/design_tab_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/design_tab_plugin.js
new file mode 100644
index 0000000000000..c07a669c8008d
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/design_tab_plugin.js
@@ -0,0 +1,74 @@
+import { Plugin } from "@html_editor/plugin";
+import { withSequence } from "@html_editor/utils/resource";
+import { _t } from "@web/core/l10n/translation";
+import { registry } from "@web/core/registry";
+
+export const OPTION_POSITIONS = {
+ BODY: 10,
+ SETTINGS: 20,
+ HEADINGS: 30,
+ PARAGRAPH: 40,
+ BUTTON: 50,
+ LINK: 60,
+};
+
+class DesignTabPlugin extends Plugin {
+ static id = "mass_mailing.DesignTab";
+ static dependencies = ["builderActions"];
+ resources = {
+ builder_options: [
+ withSequence(
+ OPTION_POSITIONS.BODY,
+ this.getDesignOptionBlock("design-body", _t("Body"), {
+ template: "mass_mailing_egg.DesignBodyOption",
+ })
+ ),
+ withSequence(
+ OPTION_POSITIONS.HEADINGS,
+ this.getDesignOptionBlock("design-headings", _t("Headings"), {
+ template: "mass_mailing_egg.DesignHeadingsOption",
+ })
+ ),
+ withSequence(
+ OPTION_POSITIONS.PARAGRAPH,
+ this.getDesignOptionBlock("design-paragraph", _t("Paragraph"), {
+ template: "mass_mailing_egg.DesignParagraphOption",
+ })
+ ),
+ withSequence(
+ OPTION_POSITIONS.BUTTON,
+ this.getDesignOptionBlock("design-button", _t("Button"), {
+ template: "mass_mailing_egg.DesignButtonOption",
+ })
+ ),
+ withSequence(
+ OPTION_POSITIONS.LINK,
+ this.getDesignOptionBlock("design-link", _t("Link"), {
+ template: "mass_mailing_egg.DesignLinkOption",
+ })
+ ),
+ ],
+ };
+
+ getDesignOptionBlock(id, name, options) {
+ const el = this.document.createElement("div");
+ el.dataset.name = name;
+ this.document.body.appendChild(el); // Currently editingElement needs to be isConnected
+
+ options.selector = "*";
+
+ return {
+ id: id,
+ element: el,
+ hasOverlayOptions: false,
+ headerMiddleButton: false,
+ isClonable: false,
+ isRemovable: false,
+ options: [options],
+ optionsContainerTopButtons: [],
+ snippetModel: {},
+ };
+ }
+}
+
+registry.category("mass_mailing-plugins").add(DesignTabPlugin.id, DesignTabPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/dropzone_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/dropzone_plugin.js
new file mode 100644
index 0000000000000..096804a864b8f
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/dropzone_plugin.js
@@ -0,0 +1,46 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+class DropzonePlugin extends Plugin {
+ static id = "mass_mailing.DropzonePlugin";
+
+ resources = {
+ dropzone_selector: [
+ {
+ selector: ".o_mail_snippet_general",
+ dropIn: ":not(p).oe_structure:not(.oe_structure_solo), :not(.o_mega_menu):not(p)[data-oe-type=html], :not(p).oe_structure.oe_structure_solo:not(:has(> section, > div))"
+ }, {
+ selector: ".s_mail_blockquote, .s_mail_alert, .s_rating, .s_hr, .s_mail_text_highlight",
+ dropNear: "p, h1, h2, h3, ul, ol, .row > div > img, .s_mail_blockquote, .s_mail_alert, .s_rating, .s_hr, .s_mail_text_highlight",
+ dropIn: ".content, nav",
+ }, { // table_column
+ selector: ".col>td, .col>th",
+ exclude: this.noOptionsSelector,
+ dropNear: ".col>td, .col>th",
+ }, { // table_column_mv
+ selector: ".col_mv, td, th",
+ exclude: this.noOptionsSelector,
+ dropNear: ".col_mv, td, th"
+ }, { // table_row
+ selector: "tr:has(> .row), tr:has(> .col_mv)",
+ exclude: this.noOptionsSelector,
+ dropNear: "tr:has(> .row), tr:has(> .col_mv)",
+ }, { // content
+ selector: ".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, .oe_snippet_body",
+ exclude: this.noOptionsSelector,
+ dropNear: "[data-oe-field='body_html']:not(:has(.o_layout)) > *, .oe_structure > *",
+ dropIn: "[data-oe-field='body_html']:not(:has(.o_layout)), .oe_structure",
+ }, { // sizing_x
+ selector: ".row > div",
+ exclude: ".o_mail_no_resize, .o_mail_no_options, .s_col_no_resize.row > div, .s_col_no_resize",
+ dropNear: ".row:not(.s_col_no_resize) > div",
+ }
+ ]
+ }
+
+ get noOptionsSelector() {
+ return ".o_mail_no_options";
+ }
+}
+
+registry.category("mass_mailing-plugins").add(DropzonePlugin.id, DropzonePlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/generic_option_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/generic_option_plugin.js
new file mode 100644
index 0000000000000..6ce23f7cfa54f
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/generic_option_plugin.js
@@ -0,0 +1,33 @@
+import { after, before, WIDTH } from "@html_builder/utils/option_sequence";
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+import { withSequence } from "@html_editor/utils/resource";
+import { LayoutColumnOption } from "@html_builder/plugins/layout_column_option";
+
+class GenericBlockOptionPlugin extends Plugin {
+ static id = "GenericBlockOption";
+ resources = {
+ mark_color_level_selector_params: [{ selector: ".o_mail_snippet_general" }],
+ builder_options: [
+ withSequence(before(WIDTH), {
+ OptionComponent: LayoutColumnOption,
+ selector: ".o_mail_snippet_general",
+ applyTo: "* > *:has(> .row:not(.s_nb_column_fixed)), * > .s_allow_columns"
+ }),
+ {
+ template: "mass_mailing.BlockquoteOption",
+ selector: ".o_mail_snippet_general",
+ exclude: ".o_mail_snippet_general .row > div *"
+ },
+ ],
+ so_snippet_addition_selector: [".o_mail_snippet_general"],
+ so_content_addition_selector: [
+ ".s_mail_blockquote, .s_mail_alert, .s_rating, .s_hr, .s_mail_text_highlight"
+ ]
+ };
+}
+// TODO: as in master, the position of a background image does not work
+// correctly.
+registry
+ .category("mass_mailing-plugins")
+ .add(GenericBlockOptionPlugin.id, GenericBlockOptionPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/horizontal_padding_option_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/horizontal_padding_option_plugin.js
new file mode 100644
index 0000000000000..788a3b79e071c
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/horizontal_padding_option_plugin.js
@@ -0,0 +1,24 @@
+import { after, before, WIDTH } from "@html_builder/utils/option_sequence";
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+import { withSequence } from "@html_editor/utils/resource";
+
+class HorizontalPaddingOptionPlugin extends Plugin {
+ static id = "horizontalPaddingOption";
+ selector = "[class*='col-lg-'], .s_discount2, .s_text_block, .s_media_list, .s_picture, .s_rating";
+ resources = {
+ mark_color_level_selector_params: [{ selector: this.selector }],
+ builder_options: [
+ withSequence(after(WIDTH)), {
+ template: "mass_mailing.HorizontalPaddingOption",
+ selector: this.selector,
+ exclude: ".s_col_no_resize.row > div, .s_col_no_resize",
+ },
+ ],
+ };
+}
+// TODO: as in master, the position of a background image does not work
+// correctly.
+registry
+ .category("mass_mailing-plugins")
+ .add(HorizontalPaddingOptionPlugin.id, HorizontalPaddingOptionPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/image_tool_options_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/image_tool_options_plugin.js
new file mode 100644
index 0000000000000..f00a71a96c652
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/image_tool_options_plugin.js
@@ -0,0 +1,34 @@
+import { ImageToolOption } from "@html_builder/plugins/image/image_tool_option";
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+class ImageToolOptionPlugin extends Plugin {
+ static id = "mass_mailing.ImageToolOption";
+ resources = {
+ patch_builder_options: [
+ {
+ target_name: "imageAndFaOption",
+ target_element: "template",
+ method: "replace",
+ value: "mass_mailing.ImageAndFaOption",
+ },
+ {
+ target_name: "imageAndFaOption",
+ target_element: "exclude",
+ method: "remove",
+ },
+ {
+ target_name: "imageToolOption",
+ target_element: "OptionComponent",
+ method: "replace",
+ value: MassMailingImageToolOption,
+ },
+ ]
+ }
+}
+
+class MassMailingImageToolOption extends ImageToolOption {
+ static template = "mass_mailing.ImageToolOption";
+}
+
+registry.category("mass_mailing-plugins").add(ImageToolOptionPlugin.id, ImageToolOptionPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/masonry_block_option_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/masonry_block_option_plugin.js
new file mode 100644
index 0000000000000..26327627c0626
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/masonry_block_option_plugin.js
@@ -0,0 +1,44 @@
+import { BuilderAction } from "@html_builder/core/builder_action";
+import { BaseOptionComponent } from "@html_builder/core/utils";
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+import { renderToElement } from "@web/core/utils/render";
+
+class MasonryBlockTemplateOptionPlugin extends Plugin {
+ static id = "mass_mailing.MasonryBlock";
+ resources = {
+ builder_options: [
+ {
+ groups: ["base.group_user"],
+ OptionComponent: MasonryBlockTemplateOption,
+ selector: ".s_masonry_block",
+ }
+ ],
+ builder_actions: {
+ ChangeMasonryTemplate,
+ }
+ }
+}
+
+class MasonryBlockTemplateOption extends BaseOptionComponent {
+ static template = "mass_mailing.MasonryBlockTemplateOption";
+}
+
+class ChangeMasonryTemplate extends BuilderAction {
+ static id = "changeMasonryTemplate";
+ apply({ editingElement, value}) {
+ editingElement.dataset.templateName = value;
+ const templateName = `mass_mailing.s_masonry_block_${value}`;
+ const newTemplate = renderToElement(templateName);
+ newTemplate.dataset.templateName = value;
+ const target = editingElement.querySelector(".container");
+ target.replaceChildren(newTemplate);
+ }
+ isApplied({ editingElement, value }) {
+ return editingElement.dataset.templateName === value;
+ }
+}
+
+registry
+ .category("mass_mailing-plugins")
+ .add(MasonryBlockTemplateOptionPlugin.id, MasonryBlockTemplateOptionPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/mass_mailing_setup_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/mass_mailing_setup_plugin.js
new file mode 100644
index 0000000000000..e4ef2b8df1d37
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/mass_mailing_setup_plugin.js
@@ -0,0 +1,11 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+export class MassMailingSetupPlugin extends Plugin {
+ static id = "mass_mailing_setup_plugin";
+ resources = {
+ o_editable_selectors: ".o_mail_wrapper_td",
+ };
+}
+
+registry.category("mass_mailing-plugins").add(MassMailingSetupPlugin.id, MassMailingSetupPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/move_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/move_plugin.js
new file mode 100644
index 0000000000000..5205175d3938d
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/move_plugin.js
@@ -0,0 +1,21 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+class MovePlugin extends Plugin {
+ static id = "mass_mailing.MovePlugin";
+ static dependencies = ["move"]
+ resources = {
+ is_movable_selector: [
+ {
+ selector: ".o_mail_snippet_general",
+ direction: "vertical",
+ }, {
+ selector: ".row:not(.s_col_no_resize) > div",
+ direction: "horizontal",
+ exclude: ".s_showcase .row > div",
+ },
+ ],
+ };
+}
+
+registry.category("builder-plugins").add(MovePlugin.id, MovePlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/vertical_alignment_option_plugin_patch.js b/addons/mass_mailing_egg/static/src/builder/plugins/vertical_alignment_option_plugin_patch.js
new file mode 100644
index 0000000000000..9d4c9a6ed1c55
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/vertical_alignment_option_plugin_patch.js
@@ -0,0 +1,8 @@
+import { VerticalAlignmentOptionPlugin } from "@html_builder/plugins/vertical_alignment_option_plugin";
+import { patch } from "@web/core/utils/patch";
+
+patch(VerticalAlignmentOptionPlugin.prototype, {
+ get selector() {
+ return super.selector + ", .s_mail_block_event";
+ }
+})
diff --git a/addons/mass_mailing_egg/static/src/builder/plugins/width_option_plugin.js b/addons/mass_mailing_egg/static/src/builder/plugins/width_option_plugin.js
new file mode 100644
index 0000000000000..742e9ac605c77
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/plugins/width_option_plugin.js
@@ -0,0 +1,18 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+
+class WidthOptionPlugin extends Plugin {
+ static id = "mass_mailing.WidthOption";
+ resources = {
+ patch_builder_options: [
+ {
+ target_name: "widthOption",
+ target_element: "selector",
+ method: "add",
+ value: ".s_mail_alert .s_alert, .s_mail_blockquote, .s_mail_text_highlight"
+ },
+ ]
+ }
+}
+
+registry.category("mass_mailing-plugins").add(WidthOptionPlugin.id, WidthOptionPlugin);
diff --git a/addons/mass_mailing_egg/static/src/builder/tabs/design_tab.js b/addons/mass_mailing_egg/static/src/builder/tabs/design_tab.js
new file mode 100644
index 0000000000000..ba233c24ad47d
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/tabs/design_tab.js
@@ -0,0 +1,16 @@
+import { Component, useState } from "@odoo/owl";
+import { useOptionsSubEnv } from "@html_builder/utils/utils";
+import { OptionsContainer } from "@html_builder/sidebar/option_container";
+
+export class DesignTab extends Component {
+ static template = "mass_mailing_egg.DesignTab";
+ static components = { OptionsContainer };
+
+ setup() {
+ useOptionsSubEnv(() => [this.env.editor.document.body]);
+ this.state = useState({
+ fontsData: {},
+ });
+ this.optionsContainers = this.env.editor.resources["design_options"];
+ }
+}
diff --git a/addons/mass_mailing_egg/static/src/builder/tabs/design_tab.xml b/addons/mass_mailing_egg/static/src/builder/tabs/design_tab.xml
new file mode 100644
index 0000000000000..319041d5c0df0
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/builder/tabs/design_tab.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/fields/html_field/mass_mailing_html_field.js b/addons/mass_mailing_egg/static/src/fields/html_field/mass_mailing_html_field.js
new file mode 100644
index 0000000000000..2040ceaf98a00
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/fields/html_field/mass_mailing_html_field.js
@@ -0,0 +1,276 @@
+import {
+ HtmlMailField,
+ htmlMailField,
+} from "@mail/views/web/fields/html_mail_field/html_mail_field";
+import { registry } from "@web/core/registry";
+import { LocalOverlayContainer } from "@html_editor/local_overlay_container";
+import { MassMailingIframe } from "@mass_mailing_egg/iframe/mass_mailing_iframe";
+import { ThemeSelector } from "@mass_mailing_egg/themes/theme_selector/theme_selector";
+import { onWillUpdateProps, status, toRaw, useEffect, useRef } from "@odoo/owl";
+import { useChildRef, useService } from "@web/core/utils/hooks";
+import { useTransition } from "@web/core/transition";
+import { effect } from "@web/core/utils/reactive";
+import { htmlField, HtmlField } from "@html_editor/fields/html_field";
+import { normalizeHTML, parseHTML } from "@html_editor/utils/html";
+import { Deferred, Race } from "@web/core/utils/concurrency";
+import { useRecordObserver } from "@web/model/relational_model/utils";
+
+export class MassMailingHtmlField extends HtmlMailField {
+ static template = "mass_mailing_egg.HtmlField";
+ static components = {
+ ...HtmlMailField.components,
+ LocalOverlayContainer,
+ MassMailingIframe,
+ ThemeSelector,
+ };
+ static props = {
+ ...HtmlField.props,
+ filterTemplates: { type: Boolean, optional: true },
+ inlineField: { type: String, optional: true },
+ };
+
+ setup() {
+ super.setup();
+ this.alwaysComputeInlineEditorContent = false;
+ this.themeService = useService("mass_mailing_egg.themes");
+ Object.assign(this.state, {
+ // TODO EGGMAIL: maybe define a condition if there is no content to display
+ // theme selectors. Or at least add an interface button to allow changing the theme
+ // TODO EGGMAIL: usage of is_body_empty is forbidden, do something else for
+ // this heuristic
+ showThemeSelector: this.props.record.isNew || this.props.record.data.is_body_empty,
+ activeTheme: undefined,
+ themeOptions: {
+ withBuilder: true,
+ },
+ });
+ useRecordObserver((record) => {
+ this.state.showThemeSelector = record.isNew || record.data.is_body_empty;
+ });
+
+ this.iframeLoadingRace = new Race();
+ this.iframeRef = useChildRef();
+ this.codeViewButtonRef = useRef("codeViewButtonRef");
+
+ // Use a transition to display the HtmlField only when the themes
+ // service finished loading
+ this.displayTransition = useTransition({
+ name: "mass_mailing_html_field",
+ initialVisibility: false,
+ immediate: false,
+ leaveDuration: 150,
+ onLeave: () => {},
+ });
+ const onThemesLoaded = () => {
+ Object.assign(this.displayTransition, {
+ class: "o_mass_mailing_themes_loaded",
+ shouldMount: true,
+ });
+ if (!this.state.showThemeSelector) {
+ this.updateThemeOptions();
+ }
+ };
+ if (!this.themeService.isLoaded()) {
+ const themesPromise = this.themeService.load();
+ themesPromise.then(onThemesLoaded);
+ } else {
+ onThemesLoaded();
+ }
+
+ // Force a full reload for MassMailingIframe on readonly change
+ // TODO EGGMAIL probably need a full reload when switching from normal
+ // editor to builder? maybe it never happens
+ onWillUpdateProps((nextProps) => {
+ if (
+ this.props.readonly !== nextProps.readonly &&
+ (this.props.readonly || nextProps.readonly)
+ ) {
+ this.state.key++;
+ }
+ });
+
+ // Recompute the themeOptions when the html value changes on the record
+ let currentKey;
+ effect(
+ (state) => {
+ if (status(this) === "destroyed") {
+ return;
+ }
+ if (state.key !== currentKey) {
+ this.updateThemeOptions();
+ this.resetIframe();
+ currentKey = state.key;
+ }
+ },
+ [this.state]
+ );
+ useEffect(
+ () => {
+ if (!this.codeViewRef.el) {
+ return;
+ }
+ this.codeViewRef.el.style.height = this.codeViewRef.el.scrollHeight + "px";
+ },
+ () => [this.codeViewRef.el]
+ );
+ }
+
+ resetIframe() {
+ this.iframeLoaded = new Deferred();
+ this.iframeLoadingRace.add(this.iframeLoaded);
+ }
+
+ updateThemeOptions() {
+ const themeOptions = this.themeService.getThemeOptions(this.value);
+ if (toRaw(this.state).activeTheme !== themeOptions.name) {
+ this.state.activeTheme = themeOptions.name;
+ this.state.themeOptions = themeOptions;
+ }
+ }
+
+ getMassMailingIframeProps() {
+ const props = {};
+ if (this.env.debug) {
+ Object.assign(props, {
+ toggleCodeView: () => this.toggleCodeView(),
+ });
+ }
+ return props;
+ }
+
+ /**
+ * @override
+ */
+ getConfig() {
+ if (this.props.readonly) {
+ return this.getReadonlyConfig();
+ } else if (this.state.themeOptions?.withBuilder) {
+ return this.getBuilderConfig();
+ } else {
+ return this.getSimpleEditorConfig();
+ }
+ // TODO EGGMAIL: implement CODEVIEW (iframe d-none, display textarea, apply changes
+ // from textarea to iframe, notify editor for a step)
+ // TODO EGGMAIL do we want dynamic placeholders?
+ }
+
+ /**
+ * @override
+ */
+ getReadonlyConfig() {
+ // TODO EGGMAIL ?
+ return super.getReadonlyConfig();
+ }
+
+ getBuilderConfig() {
+ const config = super.getConfig();
+ // All plugins for the html builder are defined in mass_mailing_builder
+ delete config.Plugins;
+ return {
+ ...config,
+ // TODO EGGMAIL?: allow the builder to show the theme selection again
+ // Applying a new Theme from the builder should CREATE AN EDITOR STEP
+ // that can be UNDONE.
+ toggleThemeSelector: (show) => this.toggleThemeSelector(show),
+ };
+ }
+
+ getSimpleEditorConfig() {
+ // TODO EGGMAIL: special config for no-builder mode
+ return {
+ ...super.getConfig(),
+ toggleThemeSelector: (show) => this.toggleThemeSelector(show),
+ };
+ }
+
+ getThemeSelectorConfig() {
+ return {
+ setThemeOptions: async (themeOptions) => {
+ this.state.activeTheme = themeOptions.name;
+ this.state.themeOptions = themeOptions;
+ await this.updateValue(themeOptions.html);
+ this.state.showThemeSelector = false;
+ },
+ filterTemplates: this.props.filterTemplates,
+ mailingModelId: this.props.record.data.mailing_model_id.id,
+ mailingModelName: this.props.record.data.mailing_model_id.display_name || "",
+ };
+ }
+
+ onIframeLoad(iframeLoaded) {
+ this.iframeLoaded.resolve(iframeLoaded);
+ }
+
+ toggleThemeSelector(show = true) {
+ this.state.showThemeSelector = show;
+ }
+
+ /**
+ * Process the content at a specifically designed location to avoid
+ * interference with the UI.
+ * @override
+ */
+ insertForInlineProcessing(el) {
+ const processingContainer = this.iframeRef.el.contentDocument.querySelector(
+ ".o_mass_mailing_processing_container"
+ );
+ processingContainer.append(el);
+ }
+
+ onTextareaInput(ev) {
+ ev.target.style.height = ev.target.scrollHeight + "px";
+ }
+
+ /**
+ * Complete rewrite of `updateValue` to ensure that both the field and the
+ * inlineField are saved at the same time. Depends on the iframe to compute
+ * the style of the inlineField.
+ * TODO EGGMAIL: this is too slow for urgent save. We should display the
+ * save confirmation popup like in website on beforeUnload, unlike other
+ * html_fields in form views.
+ * @override
+ */
+ async updateValue(value) {
+ await this.iframeLoadingRace.getCurrentProm();
+ this.lastValue = normalizeHTML(value, this.clearElementToCompare.bind(this));
+ this.isDirty = false;
+ const shouldRestoreDisplayNone = this.iframeRef.el.classList.contains("d-none");
+ this.iframeRef.el.classList.remove("d-none");
+ const processingEl = this.iframeRef.el.contentDocument.createElement("DIV");
+ processingEl.append(parseHTML(this.iframeRef.el.contentDocument, value));
+ this.insertForInlineProcessing(processingEl);
+ const inlineValue = (
+ await HtmlMailField.getInlineHTML(processingEl, this.iframeRef.el.contentDocument)
+ ).innerHTML;
+ processingEl.remove();
+ if (shouldRestoreDisplayNone) {
+ this.iframeRef.el.classList.add("d-none");
+ }
+ await this.props.record
+ .update({
+ [this.props.name]: value,
+ [this.props.inlineField]: inlineValue,
+ })
+ .catch(() => (this.isDirty = true));
+ this.props.record.model.bus.trigger("FIELD_IS_DIRTY", this.isDirty);
+ }
+}
+
+export const massMailingHtmlField = {
+ ...htmlMailField,
+ component: MassMailingHtmlField,
+ // TODO EGGMAIL decide which options we want in extractProps?
+ extractProps({ attrs, options }) {
+ const props = htmlField.extractProps(...arguments);
+ Object.assign(props, {
+ filterTemplates: options.filterTemplates,
+ inlineField: options["inline_field"],
+ migrateHTML: false,
+ embeddedComponents: false,
+ });
+ return props;
+ },
+ fieldDependencies: [{ name: "body_html", type: "html", readonly: "false" }],
+};
+
+registry.category("fields").add("mass_mailing_egg_html", massMailingHtmlField);
diff --git a/addons/mass_mailing_egg/static/src/fields/html_field/mass_mailing_html_field.scss b/addons/mass_mailing_egg/static/src/fields/html_field/mass_mailing_html_field.scss
new file mode 100644
index 0000000000000..d08752573f1dd
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/fields/html_field/mass_mailing_html_field.scss
@@ -0,0 +1,17 @@
+.o_field_mass_mailing_egg_html {
+ textarea.o_codeview {
+ font-family: 'Courier New', Courier, monospace;
+ width: 100%;
+ outline: none;
+ resize: none;
+ right: auto;
+ height: 100%;
+ border: none;
+ overflow: hidden;
+ }
+ .o_mass_mailing_code_view {
+ position: sticky;
+ top: 50px;
+ height: fit-content;
+ }
+}
diff --git a/addons/mass_mailing_egg/static/src/fields/html_field/mass_mailing_html_field.xml b/addons/mass_mailing_egg/static/src/fields/html_field/mass_mailing_html_field.xml
new file mode 100644
index 0000000000000..9c91e951eb156
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/fields/html_field/mass_mailing_html_field.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/iframe/mass_mailing_iframe.js b/addons/mass_mailing_egg/static/src/iframe/mass_mailing_iframe.js
new file mode 100644
index 0000000000000..384f3312c9589
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/iframe/mass_mailing_iframe.js
@@ -0,0 +1,348 @@
+import { parseHTML } from "@html_editor/utils/html";
+import {
+ Component,
+ markup,
+ onMounted,
+ onWillDestroy,
+ onWillUpdateProps,
+ status,
+ useEffect,
+ useRef,
+ useState,
+ useSubEnv,
+} from "@odoo/owl";
+import { LazyComponent, loadBundle } from "@web/core/assets";
+import { Deferred } from "@web/core/utils/concurrency";
+import { uniqueId } from "@web/core/utils/functions";
+import { useChildRef, useForwardRefToParent, useService } from "@web/core/utils/hooks";
+import { renderToString } from "@web/core/utils/render";
+import { user } from "@web/core/user";
+import { LocalOverlayContainer } from "@html_editor/local_overlay_container";
+import { Editor } from "@html_editor/editor";
+import { useThrottleForAnimation } from "@web/core/utils/timing";
+import { closestScrollableY } from "@web/core/utils/scrolling";
+import { _t } from "@web/core/l10n/translation";
+import { MassMailingMobilePreviewDialog } from "../mobile_preview_dialog/mobile_preview_dialog";
+
+const IFRAME_VALUE_SELECTOR = ".o_mass_mailing_value";
+
+export class MassMailingIframe extends Component {
+ static template = "mass_mailing_egg.MassMailingIframe";
+ static components = {
+ LazyComponent,
+ LocalOverlayContainer,
+ };
+ static props = {
+ config: { type: Object },
+ themeOptions: { type: Object, optional: true },
+ iframeRef: { type: Function, optional: true },
+ showThemeSelector: { type: Boolean, optional: true },
+ onIframeLoad: { type: Function, optional: true },
+ showCodeView: { type: Boolean, optional: true },
+ toggleCodeView: { type: Function, optional: true },
+ readonly: { type: Boolean, optional: true },
+ onEditorLoad: { type: Function, optional: true },
+ onBlur: { type: Function, optional: true },
+ extraClass: { type: String, optional: true},
+ };
+ static defaultProps = {
+ onEditorLoad: () => {},
+ themeOptions: {},
+ };
+
+ setup() {
+ /**
+ * TODO EGGMAIL:
+ * handle readonly, builder, no-builder modes
+ * => nobuilder must create its own EDITOR
+ * => readonly does nothing?
+ */
+ this.hotkeyService = useService("hotkey");
+ this.dialog = useService("dialog");
+ this.overlayRef = useChildRef();
+ this.iframeRef = useForwardRefToParent("iframeRef");
+ this.sidebarRef = useRef("sidebarRef");
+ useSubEnv({
+ localOverlayContainerKey: uniqueId("mass_mailing_iframe"),
+ });
+ this.state = useState({
+ isMobile: false,
+ ready: false,
+ });
+ this.iframeLoaded = new Deferred();
+ onMounted(() => {
+ if (this.iframeRef.el.contentDocument.readyState === "complete") {
+ this.setupIframe();
+ } else {
+ // Browsers like Firefox only make iframe document available after dispatching "load"
+ this.iframeRef.el.addEventListener("load", () => this.setupIframe(), {
+ once: true,
+ });
+ }
+ });
+ onWillUpdateProps((nextProps) => {
+ if (nextProps.showCodeView) {
+ this.state.showFullscreen = false;
+ }
+ });
+ useEffect(
+ () => {
+ this.iframeLoaded.then(() => {
+ if (status(this) === "destroyed") {
+ return;
+ }
+ this.iframeRef.el.contentDocument.body.classList[
+ this.state.showFullscreen ? "add" : "remove"
+ ]("o_mass_mailing_iframe_fullscreen");
+ });
+ },
+ () => [this.state.showFullscreen]
+ );
+ if (!this.props.readonly && this.props.themeOptions.withBuilder) {
+ this.state.showFullscreen = false;
+ } else if (!this.props.readonly) {
+ this.editor = new Editor(this.props.config, this.env.services);
+ this.props.onEditorLoad(this.editor);
+ onWillDestroy(() => {
+ this.editor.destroy(true);
+ });
+ this.setupBasicEditor();
+ }
+ const iframeResize = () => {
+ const iframe = this.iframeRef.el;
+ if (this.state.showFullscreen) {
+ iframe.style.height = "100%";
+ } else {
+ const height = Math.trunc(
+ iframe.contentDocument.body
+ .querySelector(IFRAME_VALUE_SELECTOR)
+ .getBoundingClientRect().height
+ );
+ iframe.style.height = height + "px";
+ }
+ };
+ const sidebarResize = () => {
+ const sidebar = this.sidebarRef.el;
+ const iframe = this.iframeRef.el;
+ if (!sidebar) {
+ return;
+ }
+ if (this.state.showFullscreen) {
+ sidebar.style.top = "0";
+ sidebar.style.height = "100%";
+ } else if (this.env.inDialog) {
+ // TODO EGGMAIL: test this for marketing automation
+ const scrollableY = closestScrollableY(sidebar);
+ if (scrollableY) {
+ const rect = scrollableY.getBoundingClientRect();
+ sidebar.style.height = `${rect.height}px`;
+ sidebar.style.top = "0";
+ }
+ } else {
+ const scrollableY = closestScrollableY(sidebar);
+ let stickyHeight = 0;
+ let stickyZindex = 0;
+ if (scrollableY) {
+ const statusBar = scrollableY.querySelector(".o_form_statusbar");
+ if (statusBar) {
+ const statusBarStyle = getComputedStyle(statusBar);
+ if (statusBarStyle.position === "sticky") {
+ stickyHeight += statusBar.getBoundingClientRect().height;
+ }
+ stickyZindex = parseInt(statusBarStyle.zIndex) || 0;
+ }
+ }
+ const top = scrollableY
+ ? `${
+ -1 * (parseInt(getComputedStyle(scrollableY).paddingTop) || 0) +
+ stickyHeight
+ }px`
+ : `${stickyHeight}px`;
+ const maxHeight = iframe.getBoundingClientRect().height;
+ const offsetHeight =
+ window.innerHeight -
+ stickyHeight -
+ document.querySelector(".o_content").getBoundingClientRect().y;
+ sidebar.style.height = `${Math.min(maxHeight, offsetHeight)}px`;
+ sidebar.style.top = top;
+ if (stickyZindex > 0) {
+ sidebar.style.zIndex = `${stickyZindex - 1}`;
+ }
+ }
+ };
+ this.throttledResize = useThrottleForAnimation(() => {
+ if (status(this) === "destroyed") {
+ return;
+ }
+ iframeResize();
+ sidebarResize();
+ });
+ }
+
+ /**
+ * Require `mass_mailing_egg.assets_builder` to be loaded.
+ */
+ get snippetModel() {
+ if (!this._snippetModel) {
+ this._snippetModel = this.env.services["html_builder.snippets"].makeSnippetModel(
+ "mass_mailing.email_designer_snippets",
+ {
+ context: {
+ // TODO EGGMAIL: Check if necessary
+ user_lang: user.context.lang,
+ },
+ }
+ );
+ }
+ return this._snippetModel;
+ }
+
+ async setupIframe() {
+ // TODO EGGMAIL: issue: the component is mounted twice, why?
+ await this.loadAssetsEditBundle();
+ if (status(this) === "destroyed") {
+ return;
+ }
+ const htmlResizeObserver = new ResizeObserver(this.throttledResize);
+ this.iframeRef.el.contentDocument.body.classList.add("o_in_iframe");
+ // Set `ready` symbol for tours
+ this.iframeRef.el.contentDocument.head.appendChild(this.renderHeadContent());
+ this.iframeRef.el.contentDocument.body.appendChild(this.renderBodyContent());
+ htmlResizeObserver.observe(
+ this.iframeRef.el.contentDocument.body.querySelector(IFRAME_VALUE_SELECTOR)
+ );
+ if (this.props.readonly) {
+ this.retargetLinks(
+ this.iframeRef.el.contentDocument.querySelector(IFRAME_VALUE_SELECTOR)
+ );
+ }
+ this.iframeRef.el.setAttribute("is-ready", "true");
+ this.iframeRef.el.contentWindow.addEventListener("beforeUnload", () => {
+ this.iframeRef.el.removeAttribute("is-ready");
+ });
+ this.iframeLoaded.resolve(this.iframeRef.el);
+ this.props.onIframeLoad?.(this.iframeLoaded);
+ this.state.ready = true;
+ }
+
+ async setupBasicEditor() {
+ await this.iframeLoaded;
+ if (status(this) === "destroyed") {
+ return;
+ }
+ this.editor.attachTo(
+ this.iframeRef.el.contentDocument.body.querySelector(IFRAME_VALUE_SELECTOR)
+ );
+ }
+
+ async loadAssetsEditBundle() {
+ await Promise.all([
+ // TODO EGGMAIL: properly investigate the required style and JS (need bootstrap js? other js?)
+ loadBundle("mass_mailing_egg.assets_iframe_style", {
+ targetDoc: this.iframeRef.el.contentDocument,
+ css: true,
+ js: false,
+ }),
+ // TODO EGGMAIL: handle dark mode assets
+ // TODO EGGMAIL: to remove following 2 assets if interactions are not needed
+ loadBundle("mass_mailing_egg.assets_iframe_core", {
+ targetDoc: this.iframeRef.el.contentDocument,
+ }),
+ loadBundle("mass_mailing_egg.assets_iframe_edit", {
+ targetDoc: this.iframeRef.el.contentDocument,
+ }),
+ // TODO EGGMAIL: remove if templates never need custom style assets
+ // eslint-disable-next-line no-unsafe-optional-chaining
+ ...(this.props.themeOptions.assets?.map((asset) =>
+ loadBundle(asset, {
+ targetDoc: this.iframeRef.el.contentDocument,
+ })
+ ) ?? []),
+ ]);
+ }
+
+ onBlur(ev) {
+ if (!this.props.readonly) {
+ this.props.onBlur(ev);
+ }
+ }
+
+ /**
+ * Render a template in the realm of the iframe document, to avoid OWL
+ * component validation errors (an Element created from the parent document
+ * of an iframe is not an instance of the Element class from the iframe
+ * document).
+ *
+ * @param {String} template
+ * @returns {DocumentFragment}
+ */
+ renderToIframeRealmFragment(template) {
+ return parseHTML(this.iframeRef.el.contentDocument, renderToString(template, this));
+ }
+
+ renderHeadContent() {
+ return this.renderToIframeRealmFragment("mass_mailing_egg.IframeHead");
+ }
+
+ renderBodyContent() {
+ return this.renderToIframeRealmFragment("mass_mailing_egg.IframeBody");
+ }
+
+ getBuilderProps() {
+ const getExternalScrollableAncestor = () =>
+ !this.showFullscreen && this.iframeRef.el && closestScrollableY(this.iframeRef.el);
+ return {
+ overlayRef: this.overlayRef,
+ iframeLoaded: this.iframeLoaded,
+ // TODO EGGMAIL: investigate if the "savePlugin" feature should be plugged to the form view save or should be disabled completely
+ snippetModel: this.snippetModel,
+ config: {
+ ...this.props.config,
+ getExternalScrollableAncestor,
+ },
+ // codeView => make it an available option in the builder (optional), only in debug?
+ // Plugins => provide plugins selection, properly filter excluded Plugins
+ isMobile: this.state.isMobile,
+ toggleMobile: this.toggleMobile.bind(this),
+ editableSelector: IFRAME_VALUE_SELECTOR,
+ toggleFullscreen: () => {
+ this.state.showFullscreen = !this.state.showFullscreen;
+ },
+ toggleCodeView: this.props.toggleCodeView,
+ onEditorLoad: this.props.onEditorLoad,
+ getExternalScrollableAncestor,
+ getThemeTab: () => {
+ const DesignTab = odoo.loader.modules.get(
+ "@mass_mailing_egg/builder/plugins/design/design_tab"
+ ).DesignTab;
+ DesignTab.displayName = _t("Design");
+ return DesignTab;
+ },
+ };
+ }
+
+ /**
+ * Ensure all links are opened in a new tab.
+ */
+ retargetLinks(container) {
+ for (const link of container.querySelectorAll("a")) {
+ this.retargetLink(link);
+ }
+ }
+
+ retargetLink(link) {
+ link.setAttribute("target", "_blank");
+ link.setAttribute("rel", "noreferrer");
+ }
+
+ toggleMobile(ev) {
+ this.state.isMobile = true;
+ this.mobilePreview = this.dialog.add(MassMailingMobilePreviewDialog, {
+ title: _t("Mobile Preview"),
+ value: markup(this.props.config.content),
+ IframeComponent: MassMailingIframe,
+ }, {
+ onClose: () => this.state.isMobile = false,
+ });
+ }
+}
diff --git a/addons/mass_mailing_egg/static/src/iframe/mass_mailing_iframe.scss b/addons/mass_mailing_egg/static/src/iframe/mass_mailing_iframe.scss
new file mode 100644
index 0000000000000..9d840e1691c6b
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/iframe/mass_mailing_iframe.scss
@@ -0,0 +1,13 @@
+.o_mass_mailing_iframe_wrapper {
+ &.o_mass_mailing_fullscreen {
+ display: block !important;
+ position: fixed !important;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100vw !important;
+ height: 100vh !important;
+ overflow: hidden !important;
+ transform: none !important;
+ z-index: ($zindex-fixed + $zindex-modal-backdrop) / 2;
+ }
+}
diff --git a/addons/mass_mailing_egg/static/src/iframe/mass_mailing_iframe.xml b/addons/mass_mailing_egg/static/src/iframe/mass_mailing_iframe.xml
new file mode 100644
index 0000000000000..61e9eed66dd38
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/iframe/mass_mailing_iframe.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/iframe_assets/iframe_style.scss b/addons/mass_mailing_egg/static/src/iframe_assets/iframe_style.scss
new file mode 100644
index 0000000000000..a47b3bf27e920
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/iframe_assets/iframe_style.scss
@@ -0,0 +1,21 @@
+html:has(body:not(.o_mass_mailing_iframe_fullscreen)) {
+ height: auto !important;
+}
+
+body:not(.o_mass_mailing_iframe_fullscreen) {
+ height: auto !important;
+ .note-editable {
+ overflow: visible;
+ height: auto;
+ .o_layout {
+ min-height: 0 !important;
+ }
+ }
+}
+
+body {
+ .note-editable {
+ padding: 0;
+ border: none;
+ }
+}
diff --git a/addons/mass_mailing_egg/static/src/mobile_preview_dialog/mobile_preview_dialog.js b/addons/mass_mailing_egg/static/src/mobile_preview_dialog/mobile_preview_dialog.js
new file mode 100644
index 0000000000000..4693c6e97b0de
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/mobile_preview_dialog/mobile_preview_dialog.js
@@ -0,0 +1,31 @@
+import { Dialog } from "@web/core/dialog/dialog";
+
+import { Component } from "@odoo/owl";
+import { useChildRef } from "@web/core/utils/hooks";
+
+export class MassMailingMobilePreviewDialog extends Component {
+ static template = "mass_mailing_egg.MobilePreviewDialog";
+ static components = {
+ Dialog,
+ }
+ static props = {
+ close: { type: Function },
+ IframeComponent: { type: Object },
+ title: { type: String },
+ value: { type: String },
+ }
+
+ setup() {
+ this.iframeRef = useChildRef();
+ }
+
+ toggle() {
+ this.iframeRef.el?.closest(".modal-body").classList.toggle("o_invert_orientation");
+ }
+
+ get config() {
+ return {
+ value: this.props.value,
+ }
+ }
+}
diff --git a/addons/mass_mailing_egg/static/src/mobile_preview_dialog/mobile_preview_dialog.scss b/addons/mass_mailing_egg/static/src/mobile_preview_dialog/mobile_preview_dialog.scss
new file mode 100644
index 0000000000000..e9599f1cd2c03
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/mobile_preview_dialog/mobile_preview_dialog.scss
@@ -0,0 +1,63 @@
+.o_dialog:has(.o_mailing_mobile_preview) {
+ text-align: center;
+ user-select: none;
+
+ .modal-dialog {
+ display: inline-block;
+ width: auto;
+
+ .modal-content {
+ background-color: black!important;
+ border: 3px outset gray;
+ border-radius: 20px;
+
+ .modal-header {
+ border: none;
+ color: white;
+ font-family: $o-we-font-family;
+
+ .btn-close {
+ filter: invert(50%) grayscale(100%) brightness(200%);
+ }
+
+ h4 {
+ font-family: inherit;
+ font-weight: normal;
+ color: inherit;
+
+ .fa {
+ margin-left: $grid-gutter-width/2;
+ }
+ }
+ }
+
+ .modal-body {
+ background-color: inherit!important;
+ border-radius: 20px;
+ padding: 15px;
+
+ $mobile-preview-width: 30vh;
+ $mobile-preview-height: 70vh;
+
+ width: $mobile-preview-width + 15;
+ height: $mobile-preview-height;
+ transition: all 400ms ease 0s;
+
+ &.o_invert_orientation {
+ width: $mobile-preview-height;
+ height: $mobile-preview-width + 15;
+ }
+
+ iframe {
+ display: block;
+ width: 100%;
+ border: none;
+ }
+ }
+
+ .modal-footer {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/addons/mass_mailing_egg/static/src/mobile_preview_dialog/mobile_preview_dialog.xml b/addons/mass_mailing_egg/static/src/mobile_preview_dialog/mobile_preview_dialog.xml
new file mode 100644
index 0000000000000..9a156da55aa55
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/mobile_preview_dialog/mobile_preview_dialog.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/themes/theme_selector/theme_selector.js b/addons/mass_mailing_egg/static/src/themes/theme_selector/theme_selector.js
new file mode 100644
index 0000000000000..3d3136fd2bad0
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/themes/theme_selector/theme_selector.js
@@ -0,0 +1,70 @@
+import { Component, markup, onWillStart, useState } from "@odoo/owl";
+import { useService } from "@web/core/utils/hooks";
+
+/**
+ * TODO EGGMAIL: maybe remove the t-key and config to use normal props/willupdateprops
+ */
+export class ThemeSelector extends Component {
+ static template = "mass_mailing_egg.ThemeSelector";
+ static props = {
+ config: { type: Object },
+ };
+
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+ // TODO EGGMAIL: currently depends on the parent component to load themes, maybe clean that up
+ this.themeService = useService("mass_mailing_egg.themes");
+ this.config = this.props.config;
+ this.themes = this.themeService.getThemes();
+ this.favoriteTemplates = useState([]);
+ onWillStart(async () => {
+ const favoriteTemplates = await this.orm.call(
+ "mailing.mailing",
+ "action_fetch_favorites",
+ [this.favoriteDomain]
+ );
+ Object.assign(
+ this.favoriteTemplates,
+ favoriteTemplates.map((favorite) => ({
+ html: markup(favorite.body_arch),
+ id: favorite.id,
+ modelId: favorite.mailing_model_id[0],
+ modelName: favorite.mailing_model_id[1],
+ name: `template_${favorite.id}`,
+ nowrap: true,
+ subject: favorite.subject,
+ userId: favorite.user_id[0],
+ userName: favorite.user_id[1],
+ }))
+ );
+ });
+ }
+
+ get favoriteDomain() {
+ return this.props.config.filterTemplates
+ ? [["mailing_model_id", "=", this.props.config.mailingModelId]]
+ : [];
+ }
+
+ async onRemoveFavorite(index) {
+ const favorite = this.favoriteTemplates[index];
+ if (!favorite) {
+ return;
+ }
+ const notificationAction = await this.orm.call(
+ "mailing.mailing",
+ "action_remove_favorite",
+ [favorite.id]
+ );
+ this.favoriteTemplates.splice(index, 1);
+ this.action.doAction(notificationAction);
+ }
+
+ onSelectTheme(html) {
+ const themeOptions = {
+ ...this.themeService.getThemeOptions(html),
+ };
+ this.props.config.setThemeOptions(themeOptions);
+ }
+}
diff --git a/addons/mass_mailing_egg/static/src/themes/theme_selector/theme_selector.scss b/addons/mass_mailing_egg/static/src/themes/theme_selector/theme_selector.scss
new file mode 100644
index 0000000000000..88762179bb19f
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/themes/theme_selector/theme_selector.scss
@@ -0,0 +1,153 @@
+.o_form_view .o_notebook > .tab-content > .tab-pane {
+ &:has(.o_mail_theme_selector) {
+ padding: 0;
+ }
+}
+
+// TODO EGGMAIL: maybe use bootstrap
+.o_mail_theme_selector {
+ display: block;
+ font-size: 1rem;
+ overflow: auto;
+ background-color: $o-we-sidebar-bg;
+ margin: 0 var(--notebook-margin-x);
+ // TODO EGGMAIL identify why -5px
+ margin-bottom: calc(var(--formView-sheet-padding-y) * -1 - 5px);
+
+ h5 {
+ font-size: 1.1rem!important;
+ }
+
+ .dropdown-item {
+ padding: 10px 10px;
+ &:first-child {
+ display: none;
+ }
+
+ .o_thumb {
+ display: none;
+ background-size: cover;
+ padding-top: 50%;
+ border: 1px solid $o-we-border-color;
+
+ &.logo {
+ display: block;
+ }
+ }
+
+ &:hover {
+ background-color: $o-we-sidebar-bg;
+
+ .o_thumb {
+ border: 1px solid black;
+ }
+ }
+
+ &:focus {
+ background-color: $o-we-sidebar-bg;
+ }
+
+ &.selected .o_thumb {
+ border: 2px solid $o-brand-odoo;
+ background-color: $o-we-sidebar-bg;
+ }
+ }
+
+ .dropdown-item {
+ margin: 0;
+ float: left;
+ clear: none;
+ width: 100%;
+ max-width: 20%;
+ transition: all 0.3s ease 0s;
+
+ &:first-child {
+ display: block;
+ }
+
+ .o_thumb {
+ display: none;
+ padding-top: 107%;
+ border: 1px solid #4e525b;
+ border-top: 1px solid $o-we-border-color;
+ box-shadow: 0 5px 10px rgba(black, 0.8);
+ will-change: transform;
+ backface-visibility: hidden;
+ transition: all 0.3s ease 0s;
+
+ &.small {
+ display: block;
+ }
+
+ @media screen and (min-width: 900px) {
+ &.small {
+ display: none;
+ }
+ &.large {
+ display: block;
+ }
+ }
+ }
+
+ &:hover {
+ background-color: #212629;
+
+ .o_thumb {
+ box-shadow: 0 5px 30px 1px rgba(black, 0.6);
+ }
+ }
+
+ &.o_mass_mailing_themes_upgrade .o_thumb {
+ position: relative;
+ display: block;
+ border: 1px dashed white;
+ opacity: 0.2;
+
+ > .fa {
+ @include o-position-absolute(0, 0, 0, 0);
+ text-align: center;
+ font-size: 50px;
+ color: white;
+
+ &::before {
+ vertical-align: middle;
+ }
+ &::after {
+ content: "";
+ display: inline-block;
+ height: 100%;
+ vertical-align: middle;
+ }
+ }
+ }
+ }
+
+ .o_mail_template_preview {
+ padding: 10px 1.5rem;
+ width: 20%;
+ div i.o_mail_template_remove_favorite {
+ display: none;
+ }
+ div:hover i.o_mail_template_remove_favorite {
+ display: inline-block;
+ &:hover {
+ color: red;
+ }
+ }
+ img {
+ width: 20px;
+ height: 20px;
+ }
+ }
+}
+
+@media (max-width: 768px) {
+ // Show 2 columns for the templates on small screen
+ // can not be done with "o_xxs_form_view" because those
+ // elements are inside an iframe (HTML field).
+ .o_mail_theme_selector {
+ .dropdown-item {
+ max-width: 50%!important;
+ }
+ }
+}
diff --git a/addons/mass_mailing_egg/static/src/themes/theme_selector/theme_selector.xml b/addons/mass_mailing_egg/static/src/themes/theme_selector/theme_selector.xml
new file mode 100644
index 0000000000000..5ab2ab8e2fd3d
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/themes/theme_selector/theme_selector.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/static/src/themes/theme_service.js b/addons/mass_mailing_egg/static/src/themes/theme_service.js
new file mode 100644
index 0000000000000..8ecfea7e9d339
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/themes/theme_service.js
@@ -0,0 +1,130 @@
+import { children } from "@html_editor/utils/dom_traversal";
+import { parseHTML } from "@html_editor/utils/html";
+import { markup } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { Reactive } from "@web/core/utils/reactive";
+import { renderToString } from "@web/core/utils/render";
+
+const DEFAULT_ASSET = "mass_mailing.email_designer_themes";
+
+function hasDataOption(element, attribute) {
+ attribute = "data-" + attribute;
+ return element.hasAttribute(attribute) && element.getAttribute(attribute) !== "false";
+}
+
+function getClassName(name) {
+ return name ? "o_" + name + "_theme" : "";
+}
+
+function getNameFromClass(className) {
+ const match = className.match(/^o_(.*)_theme$/);
+ return match ? match[1] : undefined;
+}
+
+export class ThemeModel extends Reactive {
+ constructor(services) {
+ super();
+ this.orm = services.orm;
+ this.loadedAssets = new Set();
+ this.loadedThemes = new Map();
+ }
+
+ computeThemesTemplates(asset, themesEl) {
+ // TODO EGGMAIL: do we have to use database records for themes? Why not
+ // use assets? Do users have to be able to modify their DB to add templates?
+ for (const theme of children(themesEl)) {
+ this.preProcessImages(theme);
+ const themeOptions = {
+ className: getClassName(theme.dataset.name),
+ hideFromMobile: hasDataOption(theme, "hide-from-mobile"),
+ html: markup(theme.innerHTML.trim()),
+ imgPath: theme.dataset.img || "",
+ layoutStyles: theme.dataset.layoutStyles || "",
+ name: theme.dataset.name,
+ nowrap: hasDataOption(theme, "nowrap"),
+ title: theme.getAttribute("title") || "",
+ // TODO EGGMAIL: are there other themes without the builder?
+ withBuilder: theme.dataset.name !== "basic",
+ };
+ if (hasDataOption(theme, "images-info")) {
+ const imagesInfo = Object.assign(
+ { all: {} },
+ JSON.parse(theme.dataset.imagesInfo || "{}")
+ );
+ for (const [key, info] of Object.entries(imagesInfo)) {
+ imagesInfo[key] = Object.assign(
+ {
+ module: "mass_mailing",
+ format: "jpg",
+ },
+ imagesInfo.all,
+ info
+ );
+ }
+ themeOptions.getImageInfo = (filename) => imagesInfo[filename] || imagesInfo.all;
+ }
+ // Wrap the Theme `html` with a technical layout.
+ themeOptions.html = markup(
+ renderToString("mass_mailing_egg.ThemeLayout", themeOptions)
+ );
+ this.loadedThemes.set(themeOptions.name, themeOptions);
+ }
+ this.loadedAssets.add(asset);
+ }
+
+ preProcessImages(theme) {
+ const images = theme.querySelectorAll("img[src]");
+ for (const image of images) {
+ if (!image.dataset.originalSrc) {
+ image.dataset.originalSrc = image.getAttribute("src");
+ }
+ }
+ }
+
+ getThemeOptions(html) {
+ if (!html) {
+ return {};
+ }
+ const fragment = parseHTML(document, html);
+ const layout = fragment.querySelector(".o_layout");
+ if (!layout) {
+ return {};
+ }
+ const themeClass = [...layout.classList].find(getNameFromClass);
+ const themeName = getNameFromClass(themeClass);
+ if (!themeName || !this.loadedThemes.has(themeName)) {
+ return {};
+ }
+ return this.loadedThemes.get(themeName);
+ }
+
+ getThemes() {
+ return this.loadedThemes;
+ }
+
+ isLoaded(asset = DEFAULT_ASSET) {
+ return this.loadedAssets.has(asset);
+ }
+
+ async load(asset = DEFAULT_ASSET) {
+ if (!this.isLoaded(asset)) {
+ const themesHTML = await this.orm.silent.call(
+ "ir.ui.view",
+ "render_public_asset",
+ [asset, {}],
+ {}
+ );
+ const themesDocument = new DOMParser().parseFromString(themesHTML, "text/html").body;
+ this.computeThemesTemplates(asset, themesDocument);
+ }
+ }
+}
+
+registry.category("services").add("mass_mailing_egg.themes", {
+ dependencies: ["orm"],
+
+ start(env, { orm }) {
+ const services = { orm };
+ return new ThemeModel(services);
+ },
+});
diff --git a/addons/mass_mailing_egg/static/src/themes/theme_wrapper.xml b/addons/mass_mailing_egg/static/src/themes/theme_wrapper.xml
new file mode 100644
index 0000000000000..39de0ab2e7bf0
--- /dev/null
+++ b/addons/mass_mailing_egg/static/src/themes/theme_wrapper.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_egg/views/mailing_mailing_views.xml b/addons/mass_mailing_egg/views/mailing_mailing_views.xml
new file mode 100644
index 0000000000000..b0ba6f75f84b1
--- /dev/null
+++ b/addons/mass_mailing_egg/views/mailing_mailing_views.xml
@@ -0,0 +1,349 @@
+
+
+ mailing.mailing.form.mailing_egg
+ mailing.mailing
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing_themes/views/mass_mailing_themes_templates.xml b/addons/mass_mailing_themes/views/mass_mailing_themes_templates.xml
index e026889bb7bb0..477e40d561920 100644
--- a/addons/mass_mailing_themes/views/mass_mailing_themes_templates.xml
+++ b/addons/mass_mailing_themes/views/mass_mailing_themes_templates.xml
@@ -50,7 +50,7 @@
-
-
+
+
Hi there!
The next edition of Odoo Experience Online is coming soon!
To stir up your curiosity, have a look at all the great talks scheduled and highlighted in our agenda!
-
-
-
-
+
@@ -205,7 +205,7 @@
-
+
-
+
-
+
-
+
@@ -380,7 +380,7 @@
-
-
+
+
-
+
@@ -414,14 +414,14 @@
-
+
-
-
-
-
+
@@ -511,7 +511,7 @@
}
-
+
-
+
-
+
-
+
-
+
@@ -702,7 +702,7 @@
-
-
-
+
+
-
-
+
+
BEGINNING
We are proud to announce that due to our remarkable growth in the Springfield area, we are moving to a new location on July 1.
@@ -739,25 +739,25 @@
READ MORE
-
-
-
+
@@ -803,7 +803,7 @@
-
-
+
+
10% OFF
@@ -829,8 +829,8 @@
SALES
-
-
+
+
GET 10% OFF
Valid on all sales prices in our webshop.
@@ -845,11 +845,11 @@
Visit the website
-
+
-
-
+
@@ -946,7 +946,7 @@
background-color: rgba(0,0,0,0);
}
-
-
+
+
Company News
28 Jan 2022
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1111,18 +1111,18 @@
border-color: #CE0000;
}
-
-
-
-
-
-
+
+
Making companies a better place, one app at a time
-
-
+
+
I needed to change the world. I wanted to... You know how it is when you are young; you have big dreams, a lot of energy and naive stupidity. My dream was to lead the enterprise management market with a fully open source software (I also wanted to get 100 employees before 30 years old with a self-financed company but I failed this one by only a few months).
To fuel my motivation, I had to pick someone to fight against. In business, it's like a playground. When you arrive in a new school, if you want to quickly become the leader, you must choose the class bully, the older guy who terrorizes small boys, and kick his butt in front of everyone. That was my strategy with SAP, the enterprise software giant.
@@ -1157,16 +1157,16 @@
As we worked hard, things started to evolve. We were developing dozens of modules for OpenERP, the open source community was growing and I was even able to pay all employees' salaries at the end of the month without fear (which was a situation I struggled with for 4 years).
In 2010, we had a 100+ employees selling services on OpenERP and a powerful but ugly product. This is what happens when delivering services to customers distracts you from building an exceptional product. It was time to do a pivot in the business model.
-
-
+
+
-
-
-
+
+
We wanted to switch from a service company to a software publisher company. This would allow us to increase our efforts in our research and development activities. As a result, we changed our business model and decided to stop our services for customers and focus on building a strong partner network and maintenance offer. This would cost money, so I had to raise a few million euros.
After a few months of pitching investors, I got roughly 10 LOI from different VCs. We chose Sofinnova Partners, the biggest European VC, and Xavier Niel the founder of Iliad, the only company in France funded in the past 10 years to have reached the 1 billion EUR valuation.
@@ -1174,16 +1174,16 @@
The night before receiving the warrants in front of the notary, my wife checked the contracts. She asked me what would be the taxation on these warrants. I rang the lawyer and guess what? Belgium is probably the only country in the world where you have to pay taxes on warrants when you receive them, even if you never reach the conditions to convert them into shares. If I had accepted these warrants, I would have had to pay a 12.5% tax on 9.8 m€; resulting in a tax of 1.2m€ to pay in 18 months! So, my wife is worth 1.2 million EUR. I would have ended up a homeless person without her, as I still did not have a salary at that time.
We changed the deal and I got the 3 million EUR. It allowed me to recruit a rocking management team.
-
-
+
+
-
-
-
+
+
With this money in our bank account, we boosted two departments: R&D and Sales. We burned 2 million EUR in 18 months, mostly in salaries. The company started to grow even faster. We developed a partner network of 500 partners in 100 countries and we started to sign contracts with 6 zeros.
Then, things became different. You know, tedious things like handling human resources, board meetings, dealing with big customer contracts, traveling to launch international subsidiaries. We did boring stuff like budgets, career paths, management meetings, etc.
@@ -1191,13 +1191,13 @@
But one day, someone (I don't remember who, I have a goldfish memory) made a graph of the monthly turnover of the past 2 years. It was like waking up from a nightmare. In fact, it was not that bad, we had multiplied the monthly turnover by 10 over the span of roughly two years! This is when we understood that OpenERP is a marathon, not a sprint. Only 100% growth a year is ok... if you can keep the rhythm for several years.
As usual, I should have listened to my wife. She is way more lucid than I am. Every week I complained to her "It's not good enough, we should grow faster, what am I missing?" and she used to reply: "But you already are the fastest growing company in Belgium!" (Deloitte awarded us as the fastest growing company of Belgium with 1549% growth of the turnover between 2007 and 2011).
-
-
-
+
+
Then the dream started to become reality. We started to get clues that what we did would change the world:
Something is happening... And it's big!
-
-
-
+
+
After years of rapid growth, we achieved another amazing goal - we secured a new round of 10 Million USD of financing, jointly provided by leading venture capital firms XAnge (France), SRIW (Belgium), Sofinnova (France), and the management team.
The new financing will support the acceleration of the outstanding growth the company has seen over the last years, enabling the doubling of the commercial force and increased R&D staff - already 100+ people strong.
-
-
+
+
-
-
-
+
+
After having disrupted the ERP market, OpenERP moved far beyond the boundaries of traditional ERP players. The integration of business activities is no longer restricted to sales, accounting, inventory and procurements. In June 2014, we released version 8 with awesome CMS & eCommerce, a Point of Sale, an integrated Business Intelligence engine and much more (3000+ modules).
Our exceptional technology allowed us to move to newer markets (CMS & eCommerce) and propose a product that disrupts existing open source players (Wordpress, Magento, etc). The OpenERP CMS is so good that even top competitors admitted it publicly when we released the beta version.
@@ -1239,16 +1239,16 @@
In May 2014, we renamed the company and product to Odoo.
And a new story just started...
-
-
-
+
-
+
@@ -1297,7 +1297,7 @@
color: #35979c;
}
-
+
@@ -1306,8 +1306,8 @@
Odoo invites you on [date] at [time] for their infamous roadshow in [city] : an event that combines learning and networking in a casual afterwork-like atmosphere.
-
-
+
+
Registration is free, but mandatory:
@@ -1319,23 +1319,23 @@
-
-
+
+
Discover Odoo, the all-in-one management platform, and its apps designed to improve employees' daily tasks. From sales and deliveries, to logistics, marketing and accounting ; learn what are the best available options to digitize your company.
-
-
+
+
[speaker] and [speaker] will rely on a unique demo concept in which YOU lead the demo and choose what you wish to see. They'll show how to tackle the challenge using the best tools, live. You will be amazed!
-
-
+
+
You may then take part in round table discussions with experts who will give you their tips and tricks to help digitize companies. We'll wrap up the event with a networking session including a walking dinner.
-
-
+
-
-
+
+
For more information, check the event page : [link] .
We're on the starting blocks to prepare an incredible event for you!
@@ -1408,7 +1408,7 @@
[speaker] & [speaker]
The first 20 participants will get a free copy of our business game "Scale-Up".
-
+
@@ -1429,7 +1429,7 @@
color: #35979c;
}
-
+
@@ -1443,15 +1443,15 @@
Don't hesitate to send us more!
-
-
+
+
You had the chance to discover Odoo during a full 3 hours, how great is that?
Now that you've seen Odoo in action and asked some questions, you likely need more information to move forward in your project and to work with us. You'll find additional points just below.
-
-
+
+
-
+
+
Here are the links to our sponsors' websites if you wish to meet them:
You may test Odoo for free by clicking here .
-
-
+
+
@@ -1483,19 +1483,19 @@
If you wish to become a partner and offer your services in [country], take a look at this page .
Learn more about our pricing here .
-
-
+
+
You may then take part in round table discussions with experts who will give you their tips and tricks to help digitize companies. We'll wrap up the event with a networking session including a walking dinner.
-
-
+
+
Thank you, once again, for your attendance at yesterday's event.
--
[speaker] & [speaker]
-
+
@@ -1532,7 +1532,7 @@
border-top-width: 2px;
}
-
-
+
+
-
-
-
+
+
25 September 2022 - 4:30 PM
-
London, United Kingdom
+
London, United Kingdom
@@ -1585,25 +1585,25 @@
-
-
-
-
-
+
diff --git a/addons/web/static/src/core/position/position_hook.js b/addons/web/static/src/core/position/position_hook.js
index 684d9e052ba30..c3b3e5834f7df 100644
--- a/addons/web/static/src/core/position/position_hook.js
+++ b/addons/web/static/src/core/position/position_hook.js
@@ -97,10 +97,15 @@ export function usePosition(refName, getTarget, options = {}) {
const targetDocument = getTarget()?.ownerDocument;
targetDocument?.addEventListener("scroll", scrollListener, { capture: true });
targetDocument?.addEventListener("load", throttledUpdate, { capture: true });
+ const containerDocument = options.container?.()?.ownerDocument;
+ containerDocument?.addEventListener("scroll", scrollListener, { capture: true });
+ containerDocument?.addEventListener("load", throttledUpdate, { capture: true });
window.addEventListener("resize", throttledUpdate);
return () => {
targetDocument?.removeEventListener("scroll", scrollListener, { capture: true });
targetDocument?.removeEventListener("load", throttledUpdate, { capture: true });
+ containerDocument?.removeEventListener("scroll", scrollListener, { capture: true });
+ containerDocument?.removeEventListener("load", throttledUpdate, { capture: true });
window.removeEventListener("resize", throttledUpdate);
};
}
diff --git a/addons/website/static/src/builder/builder_urlpicker.js b/addons/website/static/src/builder/builder_urlpicker.js
index 4c16236127c8c..6c1f561c44787 100644
--- a/addons/website/static/src/builder/builder_urlpicker.js
+++ b/addons/website/static/src/builder/builder_urlpicker.js
@@ -7,7 +7,6 @@ import { BuilderUrlPicker } from "@html_builder/core/building_blocks/builder_url
export class WebsiteUrlPicker extends BuilderUrlPicker {
setup() {
super.setup();
-
useEffect(
(inputEl) => {
if (!inputEl) {
@@ -33,8 +32,8 @@ export class WebsiteUrlPicker extends BuilderUrlPicker {
}
}
-class UrlPickerPlugin extends Plugin {
- static id = "urlPickerPlugin";
+class WebsiteUrlPickerPlugin extends Plugin {
+ static id = "websiteUrlPickerPlugin";
resources = {
builder_components: {
@@ -43,4 +42,4 @@ class UrlPickerPlugin extends Plugin {
};
}
-registry.category("website-plugins").add(UrlPickerPlugin.id, UrlPickerPlugin);
+registry.category("website-plugins").add(WebsiteUrlPickerPlugin.id, WebsiteUrlPickerPlugin);
diff --git a/addons/website/static/src/builder/plugins/options/background_option.js b/addons/website/static/src/builder/plugins/options/background_option.js
index 91b618550c8c3..fbdb30912e232 100644
--- a/addons/website/static/src/builder/plugins/options/background_option.js
+++ b/addons/website/static/src/builder/plugins/options/background_option.js
@@ -1,7 +1,7 @@
import { BaseOptionComponent, useDomState } from "@html_builder/core/utils";
-import { BackgroundOption } from "@website/builder/plugins/background_option/background_option";
+import { BackgroundOption } from "@html_builder/plugins/background_option/background_option";
import { ParallaxOption } from "./parallax_option";
-import { useBackgroundOption } from "@website/builder/plugins/background_option/background_hook";
+import { useBackgroundOption } from "@html_builder/plugins/background_option/background_hook";
export class WebsiteBackgroundOption extends BaseOptionComponent {
static template = "website.WebsiteBackgroundOption";
diff --git a/addons/website/static/src/builder/plugins/options/parallax_option_plugin.js b/addons/website/static/src/builder/plugins/options/parallax_option_plugin.js
index af61951d7ab14..c729d2a8c17ef 100644
--- a/addons/website/static/src/builder/plugins/options/parallax_option_plugin.js
+++ b/addons/website/static/src/builder/plugins/options/parallax_option_plugin.js
@@ -1,4 +1,4 @@
-import { applyFunDependOnSelectorAndExclude } from "@website/builder/plugins/utils";
+import { applyFunDependOnSelectorAndExclude } from "@html_builder/plugins/utils";
import { getSelectorParams } from "@html_builder/utils/utils";
import { Plugin } from "@html_editor/plugin";
import { registry } from "@web/core/registry";
diff --git a/addons/website/static/src/builder/plugins/options/process_steps_option_plugin.js b/addons/website/static/src/builder/plugins/options/process_steps_option_plugin.js
index d1858e4c4bc18..7438cfe786c27 100644
--- a/addons/website/static/src/builder/plugins/options/process_steps_option_plugin.js
+++ b/addons/website/static/src/builder/plugins/options/process_steps_option_plugin.js
@@ -1,6 +1,6 @@
import { BuilderAction } from "@html_builder/core/builder_action";
import { ClassAction } from "@html_builder/core/core_builder_action_plugin";
-import { applyFunDependOnSelectorAndExclude } from "@website/builder/plugins/utils";
+import { applyFunDependOnSelectorAndExclude } from "@html_builder/plugins/utils";
import { Plugin } from "@html_editor/plugin";
import { registry } from "@web/core/registry";
import { connectorOptionParams, ProcessStepsOption } from "./process_steps_option";
diff --git a/addons/website/static/src/builder/plugins/options/table_of_content_option_plugin.js b/addons/website/static/src/builder/plugins/options/table_of_content_option_plugin.js
index 22800b0f6ea4d..5dc0270105524 100644
--- a/addons/website/static/src/builder/plugins/options/table_of_content_option_plugin.js
+++ b/addons/website/static/src/builder/plugins/options/table_of_content_option_plugin.js
@@ -1,4 +1,4 @@
-import { applyFunDependOnSelectorAndExclude } from "@website/builder/plugins/utils";
+import { applyFunDependOnSelectorAndExclude } from "@html_builder/plugins/utils";
import { Plugin } from "@html_editor/plugin";
import { registry } from "@web/core/registry";
import { BuilderAction } from "@html_builder/core/builder_action";
diff --git a/addons/website/static/src/builder/plugins/snippets_powerbox_plugin.js b/addons/website/static/src/builder/plugins/snippets_powerbox_plugin.js
index 66c2183e1c88e..123251539fb71 100644
--- a/addons/website/static/src/builder/plugins/snippets_powerbox_plugin.js
+++ b/addons/website/static/src/builder/plugins/snippets_powerbox_plugin.js
@@ -139,10 +139,7 @@ class SnippetsPowerboxPlugin extends Plugin {
};
insertSnippet(name) {
- const snippet = this.services["html_builder.snippets"].getSnippetByName(
- "snippet_content",
- name
- );
+ const snippet = this.config.snippetModel.getSnippetByName("snippet_content", name);
const content = snippet.content.cloneNode(true);
this.dependencies.dom.insert(content);
this.dependencies.history.addStep();
diff --git a/addons/website/static/src/builder/plugins/theme/theme_tab.js b/addons/website/static/src/builder/plugins/theme/theme_tab.js
index c536272a867da..bf5b78ecde0f4 100644
--- a/addons/website/static/src/builder/plugins/theme/theme_tab.js
+++ b/addons/website/static/src/builder/plugins/theme/theme_tab.js
@@ -1,6 +1,7 @@
import { Component, useState } from "@odoo/owl";
import { OptionsContainer } from "@html_builder/sidebar/option_container";
import { useOptionsSubEnv } from "@html_builder/utils/utils";
+import { _t } from "@web/core/l10n/translation";
export class ThemeTab extends Component {
static template = "website.ThemeTab";
diff --git a/addons/website/static/src/builder/plugins/utils.js b/addons/website/static/src/builder/plugins/utils.js
index 1d9305a5bce7d..e69de29bb2d1d 100644
--- a/addons/website/static/src/builder/plugins/utils.js
+++ b/addons/website/static/src/builder/plugins/utils.js
@@ -1,24 +0,0 @@
-export function applyFunDependOnSelectorAndExclude(fn, rootEl, selectorParams) {
- const editingEls = getEditingEls(rootEl, selectorParams);
- if (!editingEls.length) {
- return false;
- }
- return Promise.all(editingEls.map((el) => fn(el)));
-}
-
-export function getEditingEls(rootEl, { selector, exclude, applyTo }) {
- const closestSelector = rootEl.closest(selector);
- let editingEls = closestSelector ? [closestSelector] : [...rootEl.querySelectorAll(selector)];
- if (exclude) {
- editingEls = editingEls.filter((selectorEl) => !selectorEl.matches(exclude));
- }
- if (!applyTo) {
- return editingEls;
- }
- const targetEls = [];
- for (const editingEl of editingEls) {
- const applyToEls = applyTo ? editingEl.querySelectorAll(applyTo) : [editingEl];
- targetEls.push(...applyToEls);
- }
- return targetEls;
-}
diff --git a/addons/website/static/src/builder/website_builder.js b/addons/website/static/src/builder/website_builder.js
index 2ae650aca2781..c1b524b44c148 100644
--- a/addons/website/static/src/builder/website_builder.js
+++ b/addons/website/static/src/builder/website_builder.js
@@ -43,7 +43,10 @@ export class WebsiteBuilder extends Component {
const builderProps = Object.assign({}, this.props.builderProps);
const websitePlugins = this.props.translation
? TRANSLATION_PLUGINS
- : registry.category("website-plugins").getAll();
+ : [
+ ...registry.category("builder-plugins").getAll(),
+ ...registry.category("website-plugins").getAll(),
+ ];
const mainEditorPluginsToRemove = [
"PowerButtonsPlugin",
"DoubleClickImagePreviewPlugin",
diff --git a/addons/website/static/src/client_actions/website_preview/website_builder_action.js b/addons/website/static/src/client_actions/website_preview/website_builder_action.js
index 08629bdd30923..71a390a63071a 100644
--- a/addons/website/static/src/client_actions/website_preview/website_builder_action.js
+++ b/addons/website/static/src/client_actions/website_preview/website_builder_action.js
@@ -33,6 +33,7 @@ import { renderToElement } from "@web/core/utils/render";
import { isBrowserChrome, isBrowserMicrosoftEdge } from "@web/core/browser/feature_detection";
import { router } from "@web/core/browser/router";
import { getScrollingElement } from "@web/core/utils/scrolling";
+import { user } from "@web/core/user";
const websiteSystrayRegistry = registry.category("website_systray");
@@ -135,7 +136,7 @@ export class WebsiteBuilderClientAction extends Component {
if (!this.ui.isSmall) {
// preload builder and snippets so clicking on "edit" is faster
loadBundle("website.website_builder_assets").then(() => {
- this.env.services["html_builder.snippets"].load();
+ this.snippetModel.load();
});
}
});
@@ -182,12 +183,31 @@ export class WebsiteBuilderClientAction extends Component {
get testMode() {
return false;
}
-
+
+ /**
+ * Require `html_builder.assets` to be loaded.
+ */
+ get snippetModel() {
+ if (!this._snippetModel) {
+ this._snippetModel = this.env.services["html_builder.snippets"].makeSnippetModel(
+ "website.snippets",
+ {
+ context: {
+ lang: this.websiteService.currentWebsite?.metadata.lang,
+ user_lang: user.context.lang,
+ },
+ }
+ );
+ }
+ return this._snippetModel;
+ }
+
get websiteBuilderProps() {
const builderProps = {
closeEditor: this.reloadIframeAndCloseEditor.bind(this),
+ editableSelector: "#wrapwrap",
reloadEditor: this.reloadEditor.bind(this),
- snippetsName: "website.snippets",
+ snippetModel: this.snippetModel,
toggleMobile: this.toggleMobile.bind(this),
installSnippetModule: this.installSnippetModule.bind(this),
overlayRef: this.overlayRef,
@@ -203,8 +223,13 @@ export class WebsiteBuilderClientAction extends Component {
},
customizeTab: this.translation ? "website.CustomizeTranslationTab" : "",
},
- getThemeTab: () =>
- odoo.loader.modules.get("@website/builder/plugins/theme/theme_tab").ThemeTab,
+ getThemeTab: Object.assign(() => {
+ const ThemeTab = odoo.loader.modules.get(
+ "@website/builder/plugins/theme/theme_tab"
+ ).ThemeTab;
+ ThemeTab.displayName = _t("Theme");
+ return ThemeTab;
+ }),
};
return { translation: this.translation, builderProps };
}
diff --git a/addons/website/static/tests/builder/website_builder/background_option.test.js b/addons/website/static/tests/builder/website_builder/background_option.test.js
index 87ae32fa3e8cb..e163e2a227fd2 100644
--- a/addons/website/static/tests/builder/website_builder/background_option.test.js
+++ b/addons/website/static/tests/builder/website_builder/background_option.test.js
@@ -1,5 +1,5 @@
-import { BackgroundOption } from "@website/builder/plugins/background_option/background_option";
-import { BackgroundPositionOverlay } from "@website/builder/plugins/background_option/background_position_overlay";
+import { BackgroundOption } from "@html_builder/plugins/background_option/background_option";
+import { BackgroundPositionOverlay } from "@html_builder/plugins/background_option/background_position_overlay";
import { expect, test } from "@odoo/hoot";
import { animationFrame, waitFor } from "@odoo/hoot-dom";
import { contains, patchWithCleanup } from "@web/../tests/web_test_helpers";