props.setIsRenderPresets( true ) }
+ onClick={ handleAddContainerClick }
>
diff --git a/modules/nested-elements/assets/js/editor/views/select-preset.js b/modules/nested-elements/assets/js/editor/views/select-preset.js
index 4df1ae8f7267..5f2b743cd60e 100644
--- a/modules/nested-elements/assets/js/editor/views/select-preset.js
+++ b/modules/nested-elements/assets/js/editor/views/select-preset.js
@@ -1,4 +1,6 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
+import { EditorOneEventManager } from 'elementor-editor-utils/editor-one-events';
+
export default function SelectPreset( props ) {
const containerHelper = elementor.helpers.container,
onPresetSelected = ( preset, container ) => {
@@ -6,17 +8,33 @@ export default function SelectPreset( props ) {
createWrapper: false,
};
- // Create new one by selected preset.
+ EditorOneEventManager.sendCanvasEmptyBoxAction( {
+ targetName: 'add_container',
+ metadata: {
+ container_type: 'flexbox',
+ structure_type: preset,
+ },
+ containerCreated: true,
+ } );
+
containerHelper.createContainerFromPreset( preset, container, options );
};
+ const handleClose = () => {
+ EditorOneEventManager.sendCanvasEmptyBoxAction( {
+ targetName: 'close',
+ containerCreated: false,
+ } );
+ props.setIsRenderPresets( false );
+ };
+
return (
<>
diff --git a/modules/variables/adapters/prop-type-adapter.php b/modules/variables/adapters/prop-type-adapter.php
index 39a1136d5af0..bacddefe740b 100644
--- a/modules/variables/adapters/prop-type-adapter.php
+++ b/modules/variables/adapters/prop-type-adapter.php
@@ -10,6 +10,7 @@
use Elementor\Modules\Variables\PropTypes\Font_Variable_Prop_Type;
use Elementor\Modules\Variables\PropTypes\Size_Variable_Prop_Type;
use Elementor\Modules\Variables\Storage\Entities\Variable;
+use Elementor\Modules\Variables\Storage\Constants;
use Elementor\Modules\Variables\Storage\Variables_Collection;
class Prop_Type_Adapter {
@@ -17,7 +18,7 @@ class Prop_Type_Adapter {
public static function to_storage( Variables_Collection $collection ): array {
$schema = self::get_schema();
- $collection->set_version( Variables_Collection::FORMAT_VERSION_V2 );
+ $collection->set_version( Constants::FORMAT_VERSION_V2 );
$record = $collection->serialize();
@@ -85,7 +86,7 @@ public static function from_storage( Variables_Collection $collection ): Variabl
$variable->set_value( $value );
} );
- $collection->set_version( Variables_Collection::FORMAT_VERSION_V1 );
+ $collection->set_version( Constants::FORMAT_VERSION_V1 );
return $collection;
}
diff --git a/modules/variables/storage/constants.php b/modules/variables/storage/constants.php
new file mode 100644
index 000000000000..5a7064e14a43
--- /dev/null
+++ b/modules/variables/storage/constants.php
@@ -0,0 +1,14 @@
+kit->get_json_meta( static::VARIABLES_META_KEY );
+ $db_record = $this->kit->get_json_meta( Constants::VARIABLES_META_KEY );
if ( is_array( $db_record ) && ! empty( $db_record ) ) {
return $db_record;
@@ -459,7 +455,7 @@ private function save( array $db_record ) {
++$db_record['watermark'];
- if ( $this->kit->update_json_meta( static::VARIABLES_META_KEY, $db_record ) ) {
+ if ( $this->kit->update_json_meta( Constants::VARIABLES_META_KEY, $db_record ) ) {
return $db_record['watermark'];
}
@@ -482,7 +478,7 @@ private function get_default_meta(): array {
return [
'data' => [],
'watermark' => 0,
- 'version' => self::FORMAT_VERSION_V1,
+ 'version' => Constants::FORMAT_VERSION_V1,
];
}
diff --git a/modules/variables/storage/variables-collection.php b/modules/variables/storage/variables-collection.php
index 8527f983611f..dc24c2efc8ca 100644
--- a/modules/variables/storage/variables-collection.php
+++ b/modules/variables/storage/variables-collection.php
@@ -15,9 +15,6 @@
* we will see if we need to extend collection as time goes on
*/
class Variables_Collection extends Collection {
- const FORMAT_VERSION_V1 = 1;
- const FORMAT_VERSION_V2 = 2;
- const TOTAL_VARIABLES_COUNT = 100;
private int $watermark;
@@ -28,7 +25,7 @@ private function __construct( array $items = [], ?int $watermark = 0, ?int $vers
$this->items = $items;
$this->watermark = $watermark;
- $this->version = $version ?? self::FORMAT_VERSION_V1;
+ $this->version = $version ?? Constants::FORMAT_VERSION_V1;
}
public static function hydrate( array $record ): self {
@@ -76,7 +73,7 @@ public static function default(): self {
return new self(
[],
0,
- self::FORMAT_VERSION_V1
+ Constants::FORMAT_VERSION_V1
);
}
@@ -144,7 +141,7 @@ public function assert_limit_not_reached(): void {
}
}
- if ( self::TOTAL_VARIABLES_COUNT <= $active_count ) {
+ if ( Constants::TOTAL_VARIABLES_COUNT <= $active_count ) {
throw new VariablesLimitReached( 'Total variables count limit reached' );
}
}
diff --git a/modules/variables/storage/variables-repository.php b/modules/variables/storage/variables-repository.php
index 2321ac11241a..84ca654eebf5 100644
--- a/modules/variables/storage/variables-repository.php
+++ b/modules/variables/storage/variables-repository.php
@@ -6,8 +6,6 @@
use Elementor\Modules\Variables\Adapters\Prop_Type_Adapter;
class Variables_Repository {
- private const VARIABLES_META_KEY = '_elementor_global_variables';
-
private Kit $kit;
public function __construct( Kit $kit ) {
@@ -15,7 +13,7 @@ public function __construct( Kit $kit ) {
}
public function load(): Variables_Collection {
- $db_record = $this->kit->get_json_meta( self::VARIABLES_META_KEY );
+ $db_record = $this->kit->get_json_meta( Constants::VARIABLES_META_KEY );
if ( is_array( $db_record ) && ! empty( $db_record ) ) {
$collection = Variables_Collection::hydrate( $db_record );
@@ -33,7 +31,7 @@ public function save( Variables_Collection $collection ) {
$record = Prop_Type_Adapter::to_storage( $collection );
- if ( $this->kit->update_json_meta( static::VARIABLES_META_KEY, $record ) ) {
+ if ( $this->kit->update_json_meta( Constants::VARIABLES_META_KEY, $record ) ) {
return $collection->watermark();
}
diff --git a/package-lock.json b/package-lock.json
index bf130ce913c0..675c573f2f30 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "4.0.0",
"dependencies": {
"@elementor/elementor-one-assets": "0.4.26",
- "@elementor/icons": "1.63.0",
+ "@elementor/icons": "1.68.0",
"@elementor/ui": "1.36.17",
"@reach/router": "1.3.4",
"@reduxjs/toolkit": "^1.8.3",
@@ -3683,9 +3683,9 @@
"license": "MIT"
},
"node_modules/@elementor/icons": {
- "version": "1.63.0",
- "resolved": "https://registry.npmjs.org/@elementor/icons/-/icons-1.63.0.tgz",
- "integrity": "sha512-wBmkIAcejtzl+pRu5VS3jJ9mJquNN2DmWJ0td56obVyP56VNhOeVulpPH39khx1klr6W4vHHV9pLDqd4qLcPpA==",
+ "version": "1.68.0",
+ "resolved": "https://registry.npmjs.org/@elementor/icons/-/icons-1.68.0.tgz",
+ "integrity": "sha512-d/vjMPzovvtO4GwlZchFmC4UVR2NbTDB73VvQdTIo6ttS3/sZYBZMLX/gt/yW/gPYhqUyq+cmTBzKtdiuVKQ/Q==",
"license": "GPL-3.0-or-later",
"peerDependencies": {
"@elementor/ui": "^1.34.0",
@@ -3957,17 +3957,442 @@
"node": ">=16"
}
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
- "linux"
+ "sunos"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
],
"peer": true,
"engines": {
@@ -6213,6 +6638,174 @@
"@parcel/watcher-win32-x64": "2.5.1"
}
},
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+ "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+ "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+ "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+ "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+ "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+ "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+ "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+ "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
@@ -6255,6 +6848,69 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+ "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+ "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+ "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/@paulirish/trace_engine": {
"version": "0.0.59",
"resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.59.tgz",
@@ -18591,14 +19247,6 @@
"url": "https://github.com/sponsors/kossnocorp"
}
},
- "node_modules/date-fns-jalali": {
- "version": "2.13.0-0",
- "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-2.13.0-0.tgz",
- "integrity": "sha512-yjlI9O18Z6ryGNagryrLO8OQ+3rishM3+A0UOX2UX10cWMn2NTlQcQ+ywTEJvF5IJz0FwX/slt2nCcpyQ/c8gw==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/date-format": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
@@ -21548,6 +22196,21 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -30149,6 +30812,21 @@
"node": "*"
}
},
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/playwright/node_modules/playwright-core": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
diff --git a/package.json b/package.json
index 1e39475e7481..553bc48582d8 100644
--- a/package.json
+++ b/package.json
@@ -151,7 +151,7 @@
},
"dependencies": {
"@elementor/elementor-one-assets": "0.4.26",
- "@elementor/icons": "1.63.0",
+ "@elementor/icons": "1.68.0",
"@elementor/ui": "1.36.17",
"@reach/router": "1.3.4",
"@reduxjs/toolkit": "^1.8.3",
diff --git a/packages/package-lock.json b/packages/package-lock.json
index 1de74450afd3..f371f0aa0ca1 100644
--- a/packages/package-lock.json
+++ b/packages/package-lock.json
@@ -21017,7 +21017,7 @@
"@elementor/editor-v1-adapters": "4.0.0",
"@elementor/events": "4.0.0",
"@elementor/http-client": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/locations": "4.0.0",
"@elementor/menus": "4.0.0",
"@elementor/ui": "1.36.17",
@@ -21088,7 +21088,7 @@
"@elementor/editor-v1-adapters": "4.0.0",
"@elementor/events": "4.0.0",
"@elementor/http-client": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/query": "4.0.0",
"@elementor/schema": "4.0.0",
"@elementor/store": "4.0.0",
@@ -21141,7 +21141,7 @@
"@elementor/editor-ui": "4.0.0",
"@elementor/editor-v1-adapters": "4.0.0",
"@elementor/editor-variables": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/locations": "4.0.0",
"@elementor/menus": "4.0.0",
"@elementor/schema": "4.0.0",
@@ -21197,7 +21197,7 @@
"@elementor/editor-variables": "4.0.0",
"@elementor/events": "4.0.0",
"@elementor/http-client": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/query": "4.0.0",
"@elementor/schema": "4.0.0",
"@elementor/store": "4.0.0",
@@ -21224,7 +21224,7 @@
"@elementor/editor-responsive": "4.0.0-524",
"@elementor/editor-ui": "4.0.0",
"@elementor/editor-v1-adapters": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/session": "4.0.0",
"@elementor/ui": "1.36.17",
"@elementor/utils": "4.0.0",
@@ -21319,7 +21319,8 @@
"@elementor/editor-panels": "4.0.0",
"@elementor/editor-v1-adapters": "4.0.0",
"@elementor/env": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/events": "4.0.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/query": "4.0.0",
"@elementor/ui": "1.36.17",
"@wordpress/api-fetch": "^6.42.0",
@@ -21388,7 +21389,7 @@
"@elementor/editor-v1-adapters": "4.0.0",
"@elementor/events": "4.0.0",
"@elementor/http-client": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/menus": "4.0.0",
"@elementor/schema": "4.0.0",
"@elementor/ui": "1.37.2",
@@ -21457,7 +21458,7 @@
"@elementor/env": "4.0.0",
"@elementor/events": "4.0.0",
"@elementor/http-client": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/locations": "4.0.0",
"@elementor/query": "4.0.0",
"@elementor/session": "4.0.0",
@@ -21628,7 +21629,7 @@
"license": "GPL-3.0-or-later",
"dependencies": {
"@elementor/editor-v1-adapters": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/ui": "1.36.17",
"@tanstack/react-virtual": "^3.13.3",
"@wordpress/i18n": "^5.13.0"
diff --git a/packages/package.json b/packages/package.json
index 456d8692c7b8..f6452f2fb3e7 100644
--- a/packages/package.json
+++ b/packages/package.json
@@ -9,6 +9,7 @@
"packageManager": "npm@10.0.0",
"scripts": {
"build": "turbo build",
+ "build:ci": "turbo build --concurrency 1",
"build:tools": "turbo build --filter=\"./packages/tools/*\"",
"dev": "concurrently -n turbo,tsc -c red,blue 'turbo dev --parallel' 'tsc -w --preserveWatchOutput'",
"test": "jest --config='./jest.config.js'",
diff --git a/packages/packages/apps/onboarding/src/__tests__/test-utils.tsx b/packages/packages/apps/onboarding/src/__tests__/test-utils.tsx
index f52ae4632d15..00ea58d45e39 100644
--- a/packages/packages/apps/onboarding/src/__tests__/test-utils.tsx
+++ b/packages/packages/apps/onboarding/src/__tests__/test-utils.tsx
@@ -47,6 +47,8 @@ interface OnboardingConfig {
dashboard: string;
editor: string;
connect: string;
+ comparePlans?: string;
+ exploreFeatures?: string;
};
}
@@ -58,6 +60,11 @@ const DEFAULT_STEPS = [
{ id: 'site_features', label: 'Site features', type: 'multiple' as const },
];
+export const DEFAULT_TEST_URLS = {
+ exploreFeatures: 'https://elementor.com/features/?utm_source=onboarding&utm_medium=wp-dash',
+ comparePlans: 'https://elementor.com/pricing/?utm_source=onboarding&utm_medium=wp-dash',
+} as const;
+
const defaultConfig: OnboardingConfig = {
version: '1.0.0',
restUrl: 'https://test.local/wp-json/elementor/v1/e-onboarding/',
@@ -76,6 +83,7 @@ const defaultConfig: OnboardingConfig = {
dashboard: 'https://test.local/wp-admin/',
editor: 'https://test.local/editor',
connect: 'https://test.local/connect',
+ ...DEFAULT_TEST_URLS,
},
};
diff --git a/packages/packages/apps/onboarding/src/components/__tests__/app.test.tsx b/packages/packages/apps/onboarding/src/components/__tests__/app.test.tsx
index d8274843cef2..d97acd1891a6 100644
--- a/packages/packages/apps/onboarding/src/components/__tests__/app.test.tsx
+++ b/packages/packages/apps/onboarding/src/components/__tests__/app.test.tsx
@@ -48,6 +48,9 @@ interface OnboardingConfig {
dashboard: string;
editor: string;
connect: string;
+ comparePlans?: string;
+ exploreFeatures?: string;
+ createNewPage?: string;
};
}
@@ -95,6 +98,9 @@ const defaultConfig: OnboardingConfig = {
dashboard: 'https://test.local/wp-admin/',
editor: 'https://test.local/editor',
connect: 'https://test.local/connect',
+ comparePlans: 'https://elementor.com/pricing/?utm_source=onboarding&utm_medium=wp-dash',
+ exploreFeatures: 'https://elementor.com/features/?utm_source=onboarding&utm_medium=wp-dash',
+ createNewPage: 'https://test.local/wp-admin/edit.php?action=elementor_new_post&post_type=page',
},
};
@@ -269,13 +275,23 @@ describe( 'App', () => {
render(
);
// Assert
- expect( screen.getByText( 'Finish' ) ).toBeInTheDocument();
- expect( screen.queryByText( 'Skip' ) ).not.toBeInTheDocument();
+ expect( screen.getByText( 'Continue with Free' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Skip' ) ).toBeInTheDocument();
} );
- it( 'should call onComplete when finishing last step', async () => {
+ it( 'should redirect to createNewPage URL with complete:true when finishing last step', async () => {
// Arrange
- const onComplete = jest.fn();
+ let capturedHref = '';
+ Object.defineProperty( window, 'location', {
+ writable: true,
+ value: {
+ ...window.location,
+ set href( url: string ) {
+ capturedHref = url;
+ },
+ },
+ } );
+
window.elementorAppConfig = createMockConfig( {
isConnected: true,
choices: { site_features: [ 'contact_form' ] },
@@ -286,18 +302,23 @@ describe( 'App', () => {
},
} );
- render(
);
+ render(
);
// Act
- fireEvent.click( screen.getByText( 'Finish' ) );
+ fireEvent.click( screen.getByText( 'Continue with Free' ) );
// Assert
await waitFor( () => {
- expect( mockFetch ).toHaveBeenCalled();
+ expect( mockFetch ).toHaveBeenCalledWith(
+ expect.stringContaining( 'user-progress' ),
+ expect.objectContaining( {
+ body: expect.stringContaining( '"complete":true' ),
+ } )
+ );
} );
await waitFor( () => {
- expect( onComplete ).toHaveBeenCalled();
+ expect( capturedHref ).toContain( 'elementor_new_post' );
} );
} );
} );
diff --git a/packages/packages/apps/onboarding/src/components/app-content.tsx b/packages/packages/apps/onboarding/src/components/app-content.tsx
index 29028dc6d243..ff39f4f16f87 100644
--- a/packages/packages/apps/onboarding/src/components/app-content.tsx
+++ b/packages/packages/apps/onboarding/src/components/app-content.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { useCallback, useEffect, useMemo } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import { Box } from '@elementor/ui';
import { __ } from '@wordpress/i18n';
@@ -13,10 +13,12 @@ import { ExperienceLevel } from '../steps/screens/experience-level';
import { Login } from '../steps/screens/login';
import { ProInstall } from '../steps/screens/pro-install';
import { SiteAbout } from '../steps/screens/site-about';
+import { SiteFeatures } from '../steps/screens/site-features';
import { ThemeSelection } from '../steps/screens/theme-selection';
import { getStepVisualConfig } from '../steps/step-visuals';
import { StepId } from '../types';
import { BaseLayout } from './ui/base-layout';
+import { CompletionScreen } from './ui/completion-screen';
import { Footer } from './ui/footer';
import { FooterActions } from './ui/footer-actions';
import { SplitLayout } from './ui/split-layout';
@@ -28,11 +30,10 @@ const isChoiceEmpty = ( choice: unknown ): boolean => {
};
interface AppContentProps {
- onComplete?: () => void;
onClose?: () => void;
}
-export function AppContent( { onComplete, onClose }: AppContentProps ) {
+export function AppContent( { onClose }: AppContentProps ) {
const {
stepId,
stepIndex,
@@ -49,6 +50,8 @@ export function AppContent( { onComplete, onClose }: AppContentProps ) {
actions,
} = useOnboarding();
+ const [ isCompleting, setIsCompleting ] = useState( false );
+
const updateProgress = useUpdateProgress();
const updateChoices = useUpdateChoices();
@@ -100,7 +103,29 @@ export function AppContent( { onComplete, onClose }: AppContentProps ) {
}
}, [ actions, isFirst ] );
+ const redirectToNewPage = useCallback( () => {
+ const redirectUrl = urls.createNewPage || urls.editor || urls.dashboard;
+ window.location.href = redirectUrl;
+ }, [ urls ] );
+
const handleSkip = useCallback( () => {
+ if ( isLast ) {
+ setIsCompleting( true );
+ updateProgress.mutate(
+ {
+ skip_step: true,
+ complete: true,
+ step_index: stepIndex,
+ total_steps: totalSteps,
+ },
+ {
+ onSuccess: redirectToNewPage,
+ onError: redirectToNewPage,
+ }
+ );
+ return;
+ }
+
updateProgress.mutate(
{
skip_step: true,
@@ -116,7 +141,7 @@ export function AppContent( { onComplete, onClose }: AppContentProps ) {
},
}
);
- }, [ actions, stepIndex, totalSteps, updateProgress ] );
+ }, [ actions, isLast, stepIndex, totalSteps, updateProgress, redirectToNewPage ] );
const handleContinue = useCallback(
( directChoice?: Record< string, unknown > ) => {
@@ -130,6 +155,23 @@ export function AppContent( { onComplete, onClose }: AppContentProps ) {
}
}
+ if ( isLast ) {
+ setIsCompleting( true );
+ updateProgress.mutate(
+ {
+ complete_step: stepId,
+ complete: true,
+ step_index: stepIndex,
+ total_steps: totalSteps,
+ },
+ {
+ onSuccess: redirectToNewPage,
+ onError: redirectToNewPage,
+ }
+ );
+ return;
+ }
+
updateProgress.mutate(
{
complete_step: stepId,
@@ -139,12 +181,7 @@ export function AppContent( { onComplete, onClose }: AppContentProps ) {
{
onSuccess: () => {
actions.completeStep( stepId );
-
- if ( ! isLast ) {
- actions.nextStep();
- } else {
- onComplete?.();
- }
+ actions.nextStep();
},
onError: () => {
actions.setError( __( 'Failed to complete step.', 'elementor' ) );
@@ -152,24 +189,28 @@ export function AppContent( { onComplete, onClose }: AppContentProps ) {
}
);
},
- [ stepId, stepIndex, totalSteps, choices, actions, isLast, onComplete, updateProgress, updateChoices ]
+ [ stepId, stepIndex, totalSteps, choices, actions, isLast, updateProgress, updateChoices, redirectToNewPage ]
);
const rightPanelConfig = useMemo( () => getStepVisualConfig( stepId ), [ stepId ] );
const isPending = updateProgress.isPending || isLoading;
const choiceForStep = choices[ stepId as keyof typeof choices ];
- const continueDisabled = isChoiceEmpty( choiceForStep );
+ const continueDisabled = ! isLast && isChoiceEmpty( choiceForStep );
const getContinueLabel = () => {
- if ( isLast ) {
- return __( 'Finish', 'elementor' );
- }
-
if ( stepId === StepId.THEME_SELECTION && ! completedSteps.includes( StepId.THEME_SELECTION ) ) {
return __( 'Continue with this theme', 'elementor' );
}
+ if ( stepId === StepId.SITE_FEATURES && ! completedSteps.includes( StepId.SITE_FEATURES ) ) {
+ return __( 'Continue with Free', 'elementor' );
+ }
+
+ if ( isLast ) {
+ return __( 'Finish', 'elementor' );
+ }
+
return __( 'Continue', 'elementor' );
};
@@ -183,11 +224,17 @@ export function AppContent( { onComplete, onClose }: AppContentProps ) {
return
;
case StepId.THEME_SELECTION:
return
;
+ case StepId.SITE_FEATURES:
+ return
;
default:
return
;
}
};
+ if ( isCompleting ) {
+ return
;
+ }
+
if ( ! hasPassedLogin ) {
return (
void;
onClose?: () => void;
}
diff --git a/packages/packages/apps/onboarding/src/components/fullscreen-card.tsx b/packages/packages/apps/onboarding/src/components/fullscreen-card.tsx
index 8e0225ef2178..c510bd1761aa 100644
--- a/packages/packages/apps/onboarding/src/components/fullscreen-card.tsx
+++ b/packages/packages/apps/onboarding/src/components/fullscreen-card.tsx
@@ -74,7 +74,7 @@ export const TextButton = styled( Button )( ( { theme } ) => ( {
lineHeight: theme.typography.pxToRem( 22 ),
} ) );
-const backgroundUrl = getOnboardingAssetUrl( 'login.webp' );
+const backgroundUrl = getOnboardingAssetUrl( 'login.png' );
interface FullscreenCardProps {
children: React.ReactNode;
diff --git a/packages/packages/apps/onboarding/src/components/site-about/option-card.tsx b/packages/packages/apps/onboarding/src/components/site-about/option-card.tsx
index 24414baf785a..9bdc6c5ebb34 100644
--- a/packages/packages/apps/onboarding/src/components/site-about/option-card.tsx
+++ b/packages/packages/apps/onboarding/src/components/site-about/option-card.tsx
@@ -3,7 +3,8 @@ import type { ElementType } from 'react';
import { CheckIcon } from '@elementor/icons';
import { Typography } from '@elementor/ui';
-import { CheckBadge, OptionCardRoot } from './styled-components';
+import { SelectionBadge } from '../ui/selection-badge';
+import { OptionCardRoot } from './styled-components';
interface OptionCardProps {
label: string;
@@ -19,11 +20,7 @@ export function OptionCard( { label, icon: Icon, selected, onClick }: OptionCard
onClick={ onClick }
aria-pressed={ selected }
>
- { selected && (
-
-
-
- ) }
+ { selected && }
{ label }
diff --git a/packages/packages/apps/onboarding/src/components/site-about/styled-components.ts b/packages/packages/apps/onboarding/src/components/site-about/styled-components.ts
index 0a3660cce12b..eff794641039 100644
--- a/packages/packages/apps/onboarding/src/components/site-about/styled-components.ts
+++ b/packages/packages/apps/onboarding/src/components/site-about/styled-components.ts
@@ -15,24 +15,11 @@ export const OptionCardRoot = styled( ButtonBase )( ( { theme } ) => ( {
transition: 'border-color 150ms ease, background-color 150ms ease',
} ) );
-export const CheckBadge = styled( Box )( ( { theme } ) => ( {
- position: 'absolute',
- top: -8,
- insetInlineEnd: -8,
- width: 18,
- height: 18,
- borderRadius: '50%',
- backgroundColor: theme.palette.text.primary,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
-} ) );
-
export const CardGrid = styled( Box )( ( { theme } ) => ( {
display: 'grid',
gridTemplateColumns: 'repeat(4, 132px)',
gap: 16,
[ theme.breakpoints.down( 'sm' ) ]: {
- gridTemplateColumns: 'repeat(2, 1fr)',
+ gridTemplateColumns: 'repeat(2, minmax(100px, 132px))',
},
} ) );
diff --git a/packages/packages/apps/onboarding/src/components/ui/__tests__/completion-screen.test.tsx b/packages/packages/apps/onboarding/src/components/ui/__tests__/completion-screen.test.tsx
new file mode 100644
index 000000000000..11310fb869d3
--- /dev/null
+++ b/packages/packages/apps/onboarding/src/components/ui/__tests__/completion-screen.test.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import { CompletionScreen } from '../completion-screen';
+
+describe( 'CompletionScreen', () => {
+ it( 'should render loading title', () => {
+ render( );
+
+ expect( screen.getByText( 'Getting things ready' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should render loading subtitle', () => {
+ render( );
+
+ expect( screen.getByText( 'Tailoring the editor to your goals and workflow\u2026' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should render both title and subtitle together', () => {
+ render( );
+
+ expect( screen.getByText( 'Getting things ready' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Tailoring the editor to your goals and workflow\u2026' ) ).toBeInTheDocument();
+ } );
+} );
diff --git a/packages/packages/apps/onboarding/src/components/ui/completion-screen.tsx b/packages/packages/apps/onboarding/src/components/ui/completion-screen.tsx
new file mode 100644
index 000000000000..8a9a921c4cd5
--- /dev/null
+++ b/packages/packages/apps/onboarding/src/components/ui/completion-screen.tsx
@@ -0,0 +1,66 @@
+import * as React from 'react';
+import { Box, Stack, styled, Typography } from '@elementor/ui';
+import { __ } from '@wordpress/i18n';
+
+const PROGRESS_BAR_WIDTH = 192;
+
+const ProgressTrack = styled( Box )( ( { theme } ) => ( {
+ width: PROGRESS_BAR_WIDTH,
+ height: 4,
+ borderRadius: 22,
+ backgroundColor: theme.palette.action.hover,
+ position: 'relative',
+ overflow: 'hidden',
+} ) );
+
+const FAKE_PROGRESS_KEYFRAMES = {
+ '0%': { width: '0%' },
+ '30%': { width: '35%' },
+ '60%': { width: '55%' },
+ '80%': { width: '68%' },
+ '100%': { width: '75%' },
+} as const;
+
+const ProgressFill = styled( Box )( ( { theme } ) => ( {
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ height: '100%',
+ borderRadius: 22,
+ backgroundColor: theme.palette.text.primary,
+ animation: 'e-onboarding-fake-progress 3s ease-out forwards',
+ '@keyframes e-onboarding-fake-progress': FAKE_PROGRESS_KEYFRAMES,
+} ) );
+
+const LOADING_TITLE = __( 'Getting things ready', 'elementor' );
+const LOADING_SUBTITLE = __( 'Tailoring the editor to your goals and workflow\u2026', 'elementor' );
+
+export function CompletionScreen() {
+ return (
+
+
+
+
+
+
+
+
+ { LOADING_TITLE }
+
+
+ { LOADING_SUBTITLE }
+
+
+
+
+ );
+}
diff --git a/packages/packages/apps/onboarding/src/components/ui/core-placeholder-icon.tsx b/packages/packages/apps/onboarding/src/components/ui/core-placeholder-icon.tsx
new file mode 100644
index 000000000000..0e93eb3af7e1
--- /dev/null
+++ b/packages/packages/apps/onboarding/src/components/ui/core-placeholder-icon.tsx
@@ -0,0 +1,53 @@
+import * as React from 'react';
+import { SvgIcon, type SvgIconProps } from '@elementor/ui';
+
+export function CorePlaceholderIcon( props: SvgIconProps ) {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/packages/apps/onboarding/src/components/ui/selection-badge.tsx b/packages/packages/apps/onboarding/src/components/ui/selection-badge.tsx
new file mode 100644
index 000000000000..9ed5f88c00ac
--- /dev/null
+++ b/packages/packages/apps/onboarding/src/components/ui/selection-badge.tsx
@@ -0,0 +1,38 @@
+import * as React from 'react';
+import { Box, styled } from '@elementor/ui';
+
+interface SelectionBadgeRootProps {
+ variant: 'free' | 'paid';
+}
+
+interface SelectionBadgeProps {
+ icon: React.ElementType;
+ variant?: 'free' | 'paid';
+}
+
+const SelectionBadgeRoot = styled( Box, {
+ shouldForwardProp: ( prop ) => 'variant' !== prop,
+} )< SelectionBadgeRootProps >( ( { theme, variant } ) => ( {
+ position: 'absolute',
+ top: theme.spacing( -1 ),
+ insetInlineEnd: theme.spacing( -1 ),
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: theme.spacing( 2.25 ),
+ height: theme.spacing( 2.25 ),
+ borderRadius: '50%',
+ backgroundColor: variant === 'paid' ? theme.palette.promotion.main : theme.palette.text.primary,
+ color: theme.palette.common.white,
+ '& .MuiSvgIcon-root': {
+ fontSize: theme.typography.pxToRem( 14 ),
+ },
+} ) );
+
+export function SelectionBadge( { icon: Icon, variant = 'free' }: SelectionBadgeProps ) {
+ return (
+
+
+
+ );
+}
diff --git a/packages/packages/apps/onboarding/src/components/ui/split-layout.tsx b/packages/packages/apps/onboarding/src/components/ui/split-layout.tsx
index 9603e584eef4..861fb3df32c9 100644
--- a/packages/packages/apps/onboarding/src/components/ui/split-layout.tsx
+++ b/packages/packages/apps/onboarding/src/components/ui/split-layout.tsx
@@ -54,18 +54,26 @@ const SplitLayoutRoot = styled( Box, {
};
} );
-const LeftPanel = styled( Box )( ( { theme } ) => ( {
+interface LeftPanelProps {
+ contentMaxWidth: number;
+}
+
+const LeftPanel = styled( Box, {
+ shouldForwardProp: ( prop ) => 'contentMaxWidth' !== prop,
+} )< LeftPanelProps >( ( { theme, contentMaxWidth } ) => ( {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: LEFT_PANEL_GAP,
padding: `${ LEFT_PANEL_PADDING_TOP }px ${ LEFT_PANEL_PADDING_X }px`,
'& > *': {
- maxWidth: LEFT_PANEL_CONTENT_WIDTH,
width: '100%',
},
+ '& > *:last-of-type': {
+ maxWidth: contentMaxWidth,
+ },
[ theme.breakpoints.down( 'sm' ) ]: {
- padding: `${ LEFT_PANEL_PADDING_TOP }px ${ theme.spacing( 2 ) }`,
+ padding: 0,
gap: LEFT_PANEL_GAP / 2,
'& > *': {
maxWidth: 'none',
@@ -86,11 +94,16 @@ interface SplitLayoutProps {
export function SplitLayout( { left, rightConfig, progress }: SplitLayoutProps ) {
const ratio = LAYOUT_RATIOS[ rightConfig.imageLayout ] ?? LAYOUT_RATIOS.wide;
+ const contentMaxWidth = rightConfig.contentMaxWidth ?? LEFT_PANEL_CONTENT_WIDTH;
return (
-
- { progress && }
+
+ { progress && (
+
+
+
+ ) }
{ left }
diff --git a/packages/packages/apps/onboarding/src/hooks/use-update-progress.ts b/packages/packages/apps/onboarding/src/hooks/use-update-progress.ts
index 16873a96133c..425c5147fd98 100644
--- a/packages/packages/apps/onboarding/src/hooks/use-update-progress.ts
+++ b/packages/packages/apps/onboarding/src/hooks/use-update-progress.ts
@@ -8,6 +8,7 @@ interface UpdateProgressParams {
step_index?: number;
total_steps?: number;
user_exit?: boolean;
+ complete?: boolean;
}
async function updateProgress( params: UpdateProgressParams ): Promise< void > {
diff --git a/packages/packages/apps/onboarding/src/steps/components/site-features/feature-grid.tsx b/packages/packages/apps/onboarding/src/steps/components/site-features/feature-grid.tsx
new file mode 100644
index 000000000000..d972c7f09d00
--- /dev/null
+++ b/packages/packages/apps/onboarding/src/steps/components/site-features/feature-grid.tsx
@@ -0,0 +1,176 @@
+import * as React from 'react';
+import { ArrowRightIcon, CheckIcon, CrownFilledIcon } from '@elementor/icons';
+import { Box, Chip, styled, Typography } from '@elementor/ui';
+import { __ } from '@wordpress/i18n';
+
+import { SelectionBadge } from '../../../components/ui/selection-badge';
+
+export interface FeatureOption {
+ id: string;
+ label: string;
+ Icon: React.ElementType;
+ licenseType: 'core' | 'pro' | 'one' | 'other';
+}
+
+interface ExploreMoreOption extends FeatureOption {
+ id: 'explore_more';
+ isExploreMore: true;
+}
+
+interface FeatureCardProps {
+ isSelected: boolean;
+ isExploreMore?: boolean;
+ isCore?: boolean;
+}
+
+interface FeatureGridProps {
+ options: FeatureOption[];
+ selectedValues: string[];
+ onFeatureClick: ( id: string ) => void;
+ onExploreMoreClick: () => void;
+}
+
+const EXPLORE_MORE_OPTION: ExploreMoreOption = {
+ id: 'explore_more',
+ label: __( 'Explore more', 'elementor' ),
+ Icon: ArrowRightIcon,
+ licenseType: 'other',
+ isExploreMore: true,
+};
+
+const IncludedInCoreChip = styled( Chip )( ( { theme } ) => ( {
+ position: 'absolute',
+ insetBlockStart: theme.spacing( 0.75 ),
+ insetInlineStart: theme.spacing( 0.75 ),
+ height: theme.spacing( 2.25 ),
+ '& .MuiChip-label': {
+ fontSize: theme.spacing( 1.5 ),
+ padding: `${ theme.spacing( 0.375 ) } ${ theme.spacing( 1 ) }`,
+ },
+} ) );
+
+const FeatureCard = styled( Box, {
+ shouldForwardProp: ( prop ) => ! [ 'isSelected', 'isExploreMore', 'isCore' ].includes( prop as string ),
+} )< FeatureCardProps >( ( { theme, isSelected, isExploreMore, isCore } ) => ( {
+ position: 'relative',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ aspectRatio: '1',
+ minHeight: theme.spacing( 12 ),
+ padding: theme.spacing( 2 ),
+ borderRadius: theme.spacing( 1 ),
+ border: isSelected ? `2px solid ${ theme.palette.text.primary }` : `1px solid ${ theme.palette.divider }`,
+ cursor: isCore ? 'default' : 'pointer',
+ transition: 'border-color 0.2s ease, background-color 0.2s ease',
+ ...( ! isCore && {
+ '&:hover': {
+ backgroundColor: theme.palette.action.hover,
+ },
+ } ),
+ ...( isExploreMore && {
+ '& .feature-icon': {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: theme.spacing( 3.75 ),
+ borderRadius: '50%',
+ backgroundColor: theme.palette.text.primary,
+ color: theme.palette.background.paper,
+ aspectRatio: 1,
+ },
+ } ),
+} ) );
+
+interface ExploreMoreCardProps {
+ onClick: () => void;
+ onKeyDown: ( event: React.KeyboardEvent, handler: () => void ) => void;
+}
+
+function ExploreMoreCard( { onClick, onKeyDown }: ExploreMoreCardProps ) {
+ return (
+ onKeyDown( event, onClick ) }
+ >
+
+
+
+
+ { EXPLORE_MORE_OPTION.label }
+
+
+ );
+}
+
+export function FeatureGrid( { options, selectedValues, onFeatureClick, onExploreMoreClick }: FeatureGridProps ) {
+ const handleKeyDown = ( event: React.KeyboardEvent, handler: () => void ) => {
+ if ( [ 'Enter', ' ' ].includes( event.key ) ) {
+ event.preventDefault();
+ handler();
+ }
+ };
+
+ return (
+
+ { options.map( ( option ) => {
+ const isSelected = selectedValues.includes( option.id );
+ const Icon = option.Icon;
+ const BadgeIcon = option.licenseType !== 'core' ? CrownFilledIcon : CheckIcon;
+ const isCore = option.licenseType === 'core';
+
+ const handleClick = () => onFeatureClick( option.id );
+
+ const handleKeyDownEvent = isCore
+ ? undefined
+ : ( event: React.KeyboardEvent ) => handleKeyDown( event, handleClick );
+
+ return (
+
+ { isCore && }
+ { isSelected && (
+
+ ) }
+
+
+
+
+ { option.label }
+
+
+ );
+ } ) }
+
+
+ );
+}
diff --git a/packages/packages/apps/onboarding/src/steps/components/site-features/index.ts b/packages/packages/apps/onboarding/src/steps/components/site-features/index.ts
new file mode 100644
index 000000000000..19469cb5fc26
--- /dev/null
+++ b/packages/packages/apps/onboarding/src/steps/components/site-features/index.ts
@@ -0,0 +1,2 @@
+export { FeatureGrid, type FeatureOption } from './feature-grid';
+export { ProPlanNotice } from './pro-plan-notice';
diff --git a/packages/packages/apps/onboarding/src/steps/components/site-features/pro-plan-notice.tsx b/packages/packages/apps/onboarding/src/steps/components/site-features/pro-plan-notice.tsx
new file mode 100644
index 000000000000..a0867ba1e08d
--- /dev/null
+++ b/packages/packages/apps/onboarding/src/steps/components/site-features/pro-plan-notice.tsx
@@ -0,0 +1,82 @@
+import * as React from 'react';
+import { useCallback } from 'react';
+import { InfoCircleIcon } from '@elementor/icons';
+import type { Theme } from '@elementor/ui';
+import { Box, Button, Stack, styled, Typography } from '@elementor/ui';
+import { __ } from '@wordpress/i18n';
+
+import { useOnboarding } from '../../../hooks/use-onboarding';
+
+const PRO_PLAN_NOTICE_BG = 'rgba(250, 228, 250, 0.6)';
+
+const COMPARE_PLANS_BUTTON_TEXT = __( 'Compare plans', 'elementor' );
+
+const ProPlanNoticeRoot = styled( Box )( ( { theme } ) => ( {
+ display: 'flex',
+ alignItems: 'center',
+ gap: theme.spacing( 1 ),
+ padding: theme.spacing( 1, 2 ),
+ borderRadius: theme.spacing( 1 ),
+ backgroundColor: PRO_PLAN_NOTICE_BG,
+ [ theme.breakpoints.down( 'sm' ) ]: {
+ flexDirection: 'column',
+ justifyContent: 'center',
+ },
+} ) );
+
+interface LicenseNoticeProps {
+ planName: 'Pro' | 'One';
+}
+
+export function ProPlanNotice( { planName }: LicenseNoticeProps ) {
+ const { urls } = useOnboarding();
+ const comparePlansUrl = urls.comparePlans;
+
+ const handleComparePlansClick = useCallback( () => {
+ window.open( comparePlansUrl, '_blank' );
+ }, [ comparePlansUrl ] );
+
+ return (
+
+ ( {
+ display: 'flex',
+ flexDirection: 'row',
+ gap: theme.spacing( 1.5 ),
+ alignItems: 'center',
+ } ) }
+ >
+ ( {
+ fontSize: theme.spacing( 2.5 ),
+ color: 'text.secondary',
+ } ) }
+ />
+ ( {
+ fontSize: theme.spacing( 1.625 ),
+ } ) }
+ >
+ { __( 'Based on the features you chose, we recommend the', 'elementor' ) }{ ' ' }
+ { planName } { __( 'plan', 'elementor' ) }
+
+
+
+
+ );
+}
diff --git a/packages/packages/apps/onboarding/src/steps/screens/__tests__/site-features.test.tsx b/packages/packages/apps/onboarding/src/steps/screens/__tests__/site-features.test.tsx
new file mode 100644
index 000000000000..4f03bd255f1b
--- /dev/null
+++ b/packages/packages/apps/onboarding/src/steps/screens/__tests__/site-features.test.tsx
@@ -0,0 +1,244 @@
+/* eslint-disable testing-library/no-test-id-queries */
+import { fireEvent, screen, waitFor, within } from '@testing-library/react';
+
+import { DEFAULT_TEST_URLS, mockFetch, renderApp, setupOnboardingTests } from '../../../__tests__/test-utils';
+import { FEATURE_OPTIONS } from '../site-features';
+
+const SITE_FEATURES_PROGRESS = {
+ current_step_id: 'site_features',
+ current_step_index: 4,
+};
+
+const STEP_TITLE = 'What do you want to include in your site?';
+const STEP_SUBTITLE = "We'll use this to tailor suggestions for you.";
+const BUILT_IN_LABEL = 'Included';
+const EXPLORE_MORE_LABEL = 'Explore more';
+const FINISH_BUTTON_LABEL = 'Continue with Free';
+const USER_CHOICES_ENDPOINT = 'user-choices';
+const PRO_PLAN_NOTICE_TEXT = 'Some features you selected are available in Pro plan.';
+const COMPARE_PLANS_BUTTON_LABEL = 'Compare plans';
+const TARGET_BLANK = '_blank';
+
+const getFirstProOption = () => {
+ const option = FEATURE_OPTIONS.find( ( featureOption ) => featureOption.licenseType === 'pro' );
+
+ if ( ! option ) {
+ throw new Error( 'No pro option in FEATURE_OPTIONS' );
+ }
+ return option;
+};
+
+const getFirstOneOption = () => {
+ const option = FEATURE_OPTIONS.find( ( featureOption ) => featureOption.licenseType === 'one' );
+
+ if ( ! option ) {
+ throw new Error( 'No one option in FEATURE_OPTIONS' );
+ }
+ return option;
+};
+
+describe( 'SiteFeatures', () => {
+ setupOnboardingTests();
+
+ describe( 'Rendering', () => {
+ it( 'renders step title and subtitle', () => {
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ } );
+
+ expect( screen.getByText( STEP_TITLE ) ).toBeInTheDocument();
+ expect( screen.getByText( STEP_SUBTITLE ) ).toBeInTheDocument();
+ } );
+
+ it( 'renders all feature options', () => {
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ } );
+
+ FEATURE_OPTIONS.forEach( ( option ) => {
+ expect( screen.getByText( option.label ) ).toBeInTheDocument();
+ } );
+ expect( screen.getByText( EXPLORE_MORE_LABEL ) ).toBeInTheDocument();
+ } );
+
+ it( 'renders Included chip on core features', () => {
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ } );
+
+ const coreOptions = FEATURE_OPTIONS.filter( ( option ) => option.licenseType === 'core' );
+ coreOptions.forEach( ( option ) => {
+ const card = screen.getByTestId( `feature-card-${ option.id }` );
+ expect( within( card ).getByText( BUILT_IN_LABEL ) ).toBeInTheDocument();
+ } );
+ } );
+ } );
+
+ describe( 'Default selection state', () => {
+ it( 'pro features are not selected by default', () => {
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ } );
+
+ const proOptions = FEATURE_OPTIONS.filter( ( option ) => option.licenseType === 'pro' );
+ proOptions.forEach( ( option ) => {
+ const button = screen.getByRole( 'button', { name: option.label } );
+ expect( button ).toHaveAttribute( 'aria-pressed', 'false' );
+ } );
+ } );
+ } );
+
+ describe( 'Selection behavior', () => {
+ it( 'clicking Finish after selecting pro feature calls API with correct value', async () => {
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ } );
+
+ const firstProOption = getFirstProOption();
+ fireEvent.click( screen.getByRole( 'button', { name: firstProOption.label } ) );
+ await waitFor( () => {
+ expect( screen.getByRole( 'button', { name: FINISH_BUTTON_LABEL } ) ).toBeEnabled();
+ } );
+ fireEvent.click( screen.getByRole( 'button', { name: FINISH_BUTTON_LABEL } ) );
+
+ await waitFor( () => {
+ expect( mockFetch ).toHaveBeenCalledWith(
+ expect.stringContaining( USER_CHOICES_ENDPOINT ),
+ expect.objectContaining( {
+ method: 'POST',
+ body: expect.stringContaining( firstProOption.id ),
+ } )
+ );
+ } );
+ } );
+
+ it( 'core features are not clickable', () => {
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ } );
+
+ const coreOptions = FEATURE_OPTIONS.filter( ( option ) => option.licenseType === 'core' );
+ coreOptions.forEach( ( option ) => {
+ mockFetch.mockClear();
+ const card = screen.getByTestId( `feature-card-${ option.id }` );
+ fireEvent.click( card );
+ expect( mockFetch ).not.toHaveBeenCalledWith(
+ expect.stringContaining( USER_CHOICES_ENDPOINT ),
+ expect.anything()
+ );
+ } );
+ } );
+ } );
+
+ describe( 'External links', () => {
+ it( '"Explore more" opens features URL in new tab', () => {
+ const openSpy = jest.spyOn( window, 'open' ).mockImplementation( () => null );
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ } );
+
+ fireEvent.click( screen.getByRole( 'button', { name: EXPLORE_MORE_LABEL } ) );
+
+ expect( openSpy ).toHaveBeenCalledWith( DEFAULT_TEST_URLS.exploreFeatures, TARGET_BLANK );
+ openSpy.mockRestore();
+ } );
+
+ it( '"Compare plans" opens pricing URL in new tab', () => {
+ const firstProOption = getFirstProOption();
+ const openSpy = jest.spyOn( window, 'open' ).mockImplementation( () => null );
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ choices: { site_features: [ firstProOption.id ] },
+ } );
+
+ fireEvent.click( screen.getByRole( 'button', { name: COMPARE_PLANS_BUTTON_LABEL } ) );
+
+ expect( openSpy ).toHaveBeenCalledWith( DEFAULT_TEST_URLS.comparePlans, TARGET_BLANK );
+ openSpy.mockRestore();
+ } );
+ } );
+
+ describe( 'Pre-selected state', () => {
+ it( 'restores previously selected pro features from saved choices', () => {
+ const proOptions = FEATURE_OPTIONS.filter( ( option ) => option.licenseType === 'pro' );
+ const preSelectedCount = 2;
+ const selectedIds = proOptions.slice( 0, preSelectedCount ).map( ( option ) => option.id );
+ const unselectedOption = proOptions[ preSelectedCount ];
+ if ( ! unselectedOption ) {
+ throw new Error( 'Expected unselected option' );
+ }
+
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ choices: { site_features: selectedIds },
+ } );
+
+ selectedIds.forEach( ( id ) => {
+ const option = FEATURE_OPTIONS.find( ( opt ) => opt.id === id );
+ if ( ! option ) {
+ throw new Error( 'Expected option for selected id' );
+ }
+ const button = screen.getByRole( 'button', { name: option.label } );
+ expect( button ).toHaveAttribute( 'aria-pressed', 'true' );
+ } );
+ const unselectedButton = screen.getByRole( 'button', { name: unselectedOption.label } );
+ expect( unselectedButton ).toHaveAttribute( 'aria-pressed', 'false' );
+ } );
+ } );
+
+ describe( 'ProPlanNotice visibility', () => {
+ it( 'is hidden when no pro features are selected', () => {
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ } );
+
+ expect( screen.queryByText( PRO_PLAN_NOTICE_TEXT ) ).not.toBeInTheDocument();
+ } );
+ } );
+
+ describe( 'ProPlanNotice plan name', () => {
+ it( 'shows "Pro" plan name when only pro features are selected', () => {
+ const firstProOption = getFirstProOption();
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ choices: { site_features: [ firstProOption.id ] },
+ } );
+
+ expect( screen.getByText( /recommend the/, { selector: 'p' } ) ).toHaveTextContent( /Pro plan/ );
+ } );
+
+ it( 'shows "One" plan name when a one-license feature is selected', () => {
+ const firstOneOption = getFirstOneOption();
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ choices: { site_features: [ firstOneOption.id ] },
+ } );
+
+ expect( screen.getByText( /recommend the/, { selector: 'p' } ) ).toHaveTextContent( /One plan/ );
+ } );
+
+ it( 'shows "One" plan name when both pro and one-license features are selected', () => {
+ const firstProOption = getFirstProOption();
+ const firstOneOption = getFirstOneOption();
+ renderApp( {
+ isConnected: true,
+ progress: SITE_FEATURES_PROGRESS,
+ choices: { site_features: [ firstProOption.id, firstOneOption.id ] },
+ } );
+
+ expect( screen.getByText( /recommend the/, { selector: 'p' } ) ).toHaveTextContent( /One plan/ );
+ } );
+ } );
+} );
diff --git a/packages/packages/apps/onboarding/src/steps/screens/experience-level.tsx b/packages/packages/apps/onboarding/src/steps/screens/experience-level.tsx
index 021537733280..48eb0183d571 100644
--- a/packages/packages/apps/onboarding/src/steps/screens/experience-level.tsx
+++ b/packages/packages/apps/onboarding/src/steps/screens/experience-level.tsx
@@ -38,7 +38,7 @@ export function ExperienceLevel( { onComplete }: ExperienceLevelProps ) {
);
return (
-
+
{ __( 'How much experience do you have with Elementor?', 'elementor' ) }
diff --git a/packages/packages/apps/onboarding/src/steps/screens/site-features.tsx b/packages/packages/apps/onboarding/src/steps/screens/site-features.tsx
new file mode 100644
index 000000000000..45dc030d22e0
--- /dev/null
+++ b/packages/packages/apps/onboarding/src/steps/screens/site-features.tsx
@@ -0,0 +1,159 @@
+import * as React from 'react';
+import { useCallback, useMemo } from 'react';
+import {
+ CodeIcon,
+ ColorSwatchIcon,
+ ElementorAccessibilityIcon,
+ ElementorAIIcon,
+ ElementorEmailDeliverabilityIcon,
+ ElementorImageOptimizerIcon,
+ ThemeBuilderIcon,
+} from '@elementor/icons';
+import { Stack, type Theme, Typography } from '@elementor/ui';
+import { __ } from '@wordpress/i18n';
+
+import { CorePlaceholderIcon } from '../../components/ui/core-placeholder-icon';
+import { useOnboarding } from '../../hooks/use-onboarding';
+import { FeatureGrid, type FeatureOption, ProPlanNotice } from '../components/site-features';
+
+export const FEATURE_OPTIONS: FeatureOption[] = [
+ {
+ id: 'classes_variables',
+ label: __( 'Classes & variables', 'elementor' ),
+ Icon: ( props ) => ,
+ licenseType: 'core',
+ },
+ {
+ id: 'core_placeholder',
+ label: __( 'Core placeholder', 'elementor' ),
+ Icon: CorePlaceholderIcon,
+ licenseType: 'core',
+ },
+ {
+ id: 'theme_builder',
+ label: __( 'Theme builder', 'elementor' ),
+ Icon: ThemeBuilderIcon,
+ licenseType: 'pro',
+ },
+ {
+ id: 'lead_collection',
+ label: __( 'Lead Collection', 'elementor' ),
+ Icon: CorePlaceholderIcon,
+ licenseType: 'pro',
+ },
+ {
+ id: 'custom_code_css',
+ label: __( 'Custom Code', 'elementor' ),
+ Icon: CodeIcon,
+ licenseType: 'pro',
+ },
+ {
+ id: 'email_deliverability',
+ label: __( 'Email deliverability', 'elementor' ),
+ Icon: ElementorEmailDeliverabilityIcon,
+ licenseType: 'one',
+ },
+ {
+ id: 'ai_features',
+ label: __( 'AI generator', 'elementor' ),
+ Icon: ElementorAIIcon,
+ licenseType: 'one',
+ },
+ {
+ id: 'image_optimization',
+ label: __( 'Image optimization', 'elementor' ),
+ Icon: ElementorImageOptimizerIcon,
+ licenseType: 'one',
+ },
+ {
+ id: 'accessibility',
+ label: __( 'Accessibility tools', 'elementor' ),
+ Icon: ElementorAccessibilityIcon,
+ licenseType: 'one',
+ },
+];
+
+const CORE_FEATURE_IDS = new Set(
+ FEATURE_OPTIONS.flatMap( ( option ) => ( option.licenseType === 'core' ? [ option.id ] : [] ) )
+);
+
+const FEATURE_OPTION_IDS = new Set( FEATURE_OPTIONS.map( ( featureOption ) => featureOption.id ) );
+
+const STEP_TITLE = __( 'What do you want to include in your site?', 'elementor' );
+const STEP_SUBTITLE = __( "We'll use this to tailor suggestions for you.", 'elementor' );
+
+export function SiteFeatures() {
+ const { choices, actions, urls } = useOnboarding();
+ const exploreFeaturesUrl = urls.exploreFeatures;
+
+ const storedPaidFeatures = useMemo(
+ () => ( ( choices.site_features as string[] ) || [] ).filter( ( id ) => FEATURE_OPTION_IDS.has( id ) ),
+ [ choices.site_features ]
+ );
+
+ const selectedValues = useMemo( () => {
+ const combined = [ ...CORE_FEATURE_IDS, ...storedPaidFeatures ];
+ return combined.filter( ( id, index ) => combined.indexOf( id ) === index );
+ }, [ storedPaidFeatures ] );
+
+ const handleFeatureClick = useCallback(
+ ( id: string ) => {
+ if ( CORE_FEATURE_IDS.has( id ) && selectedValues.includes( id ) ) {
+ return;
+ }
+
+ const hasPaidFeaturesSelected = storedPaidFeatures.includes( id );
+ const updatedPaidFeatureSelection = hasPaidFeaturesSelected
+ ? storedPaidFeatures.filter( ( featureId ) => featureId !== id )
+ : [ ...storedPaidFeatures, id ];
+
+ actions.setUserChoice( 'site_features', updatedPaidFeatureSelection );
+ },
+ [ storedPaidFeatures, selectedValues, actions ]
+ );
+
+ const planName = useMemo( () => {
+ const hasOneFeature = storedPaidFeatures.some( ( optionId ) => {
+ const option = FEATURE_OPTIONS.find( ( featureOption ) => featureOption.id === optionId );
+ return option?.licenseType === 'one';
+ } );
+
+ return hasOneFeature ? 'One' : 'Pro';
+ }, [ storedPaidFeatures ] );
+
+ const handleExploreMoreClick = useCallback( () => {
+ window.open( exploreFeaturesUrl, '_blank' );
+ }, [ exploreFeaturesUrl ] );
+
+ return (
+ ( {
+ [ theme.breakpoints.down( 'sm' ) ]: {
+ marginBottom: theme.spacing( 10 ),
+ },
+ } ) }
+ >
+
+
+ { STEP_TITLE }
+
+
+ { STEP_SUBTITLE }
+
+
+
+
+
+ { storedPaidFeatures.length > 0 && }
+
+ );
+}
diff --git a/packages/packages/apps/onboarding/src/steps/step-visuals.ts b/packages/packages/apps/onboarding/src/steps/step-visuals.ts
index 0470afad2f9e..e889ba1726a6 100644
--- a/packages/packages/apps/onboarding/src/steps/step-visuals.ts
+++ b/packages/packages/apps/onboarding/src/steps/step-visuals.ts
@@ -19,41 +19,42 @@ const buildBackground = ( fileName: string ) => {
const DEFAULT_CONFIG: StepVisualConfig = {
imageLayout: 'wide',
- background: buildBackground( 'step-1.webp' ),
+ background: buildBackground( 'step-1.png' ),
assets: [],
};
export const LOGIN_CONFIG: StepVisualConfig = {
imageLayout: 'wide',
- background: buildBackground( 'login.webp' ),
+ background: buildBackground( 'login.png' ),
assets: [],
};
const stepVisuals: Record< StepIdType, StepVisualConfig > = {
[ StepId.BUILDING_FOR ]: {
imageLayout: 'wide',
- background: buildBackground( 'step-1.webp' ),
+ background: buildBackground( 'step-1.png' ),
assets: [],
},
[ StepId.SITE_ABOUT ]: {
imageLayout: 'narrow',
- background: buildBackground( 'step-2.webp' ),
+ background: buildBackground( 'step-2.png' ),
assets: [],
},
[ StepId.EXPERIENCE_LEVEL ]: {
imageLayout: 'wide',
- background: buildBackground( 'step-3.webp' ),
+ background: buildBackground( 'step-3.png' ),
assets: [],
},
[ StepId.THEME_SELECTION ]: {
imageLayout: 'narrow',
- background: buildBackground( 'step-4.webp' ),
+ background: buildBackground( 'step-4.png' ),
assets: [],
},
[ StepId.SITE_FEATURES ]: {
imageLayout: 'narrow',
- background: buildBackground( 'step-5.webp' ),
+ background: buildBackground( 'step-5.png' ),
assets: [],
+ contentMaxWidth: 724,
},
};
diff --git a/packages/packages/apps/onboarding/src/store/slice.ts b/packages/packages/apps/onboarding/src/store/slice.ts
index 2b4ce7b145c3..147e9730a0f1 100644
--- a/packages/packages/apps/onboarding/src/store/slice.ts
+++ b/packages/packages/apps/onboarding/src/store/slice.ts
@@ -74,9 +74,9 @@ function getEmptyState(): OnboardingState {
isConnected: false,
isGuest: false,
userName: '',
+ urls: { dashboard: '', editor: '', connect: '', comparePlans: '', exploreFeatures: '' },
shouldShowProInstallScreen: false,
hasProInstallScreenDismissed: false,
- urls: { dashboard: '', editor: '', connect: '' },
};
}
@@ -90,8 +90,13 @@ function buildStateFromConfig(
const steps = parseStepsFromConfig( config.steps );
const firstStepId = steps[ 0 ]?.id ?? StepId.BUILDING_FOR;
const progress = config.progress ?? {};
- const currentStepIndex = progress.current_step_index ?? 0;
- const currentStepId = steps[ currentStepIndex ]?.id ?? ( progress.current_step_id as StepIdType ) ?? firstStepId;
+ let currentStepIndex = progress.current_step_index ?? 0;
+
+ if ( currentStepIndex < 0 || currentStepIndex >= steps.length ) {
+ currentStepIndex = 0;
+ }
+
+ const currentStepId = steps[ currentStepIndex ]?.id ?? firstStepId;
return {
steps,
@@ -109,9 +114,9 @@ function buildStateFromConfig(
isConnected: config.isConnected ?? false,
isGuest: false,
userName: config.userName ?? '',
+ urls: config.urls ?? { dashboard: '', editor: '', connect: '', comparePlans: '', exploreFeatures: '' },
shouldShowProInstallScreen: config.shouldShowProInstallScreen ?? false,
hasProInstallScreenDismissed: false,
- urls: config.urls ?? { dashboard: '', editor: '', connect: '' },
};
}
diff --git a/packages/packages/apps/onboarding/src/types.ts b/packages/packages/apps/onboarding/src/types.ts
index 076804a46c12..9ad9786ee888 100644
--- a/packages/packages/apps/onboarding/src/types.ts
+++ b/packages/packages/apps/onboarding/src/types.ts
@@ -34,6 +34,7 @@ export interface StepVisualConfig {
imageLayout: ImageLayout;
background: string;
assets: RightPanelAsset[];
+ contentMaxWidth?: number;
}
export interface OnboardingProgress {
@@ -70,6 +71,9 @@ export interface OnboardingConfig {
dashboard: string;
editor: string;
connect: string;
+ comparePlans?: string;
+ exploreFeatures?: string;
+ createNewPage?: string;
};
}
@@ -95,5 +99,8 @@ export interface OnboardingState {
dashboard: string;
editor: string;
connect: string;
+ comparePlans?: string;
+ exploreFeatures?: string;
+ createNewPage?: string;
};
}
diff --git a/packages/packages/core/editor-app-bar/package.json b/packages/packages/core/editor-app-bar/package.json
index 3c14f5369232..6576b540f7cb 100644
--- a/packages/packages/core/editor-app-bar/package.json
+++ b/packages/packages/core/editor-app-bar/package.json
@@ -46,7 +46,7 @@
"@elementor/editor-documents": "4.0.0",
"@elementor/editor-responsive": "4.0.0",
"@elementor/editor-v1-adapters": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/locations": "4.0.0",
"@elementor/menus": "4.0.0",
"@elementor/events": "4.0.0",
diff --git a/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-copy-and-share-props.ts b/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-copy-and-share-props.ts
index 5998a9c987db..9889625c78e1 100644
--- a/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-copy-and-share-props.ts
+++ b/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-copy-and-share-props.ts
@@ -2,6 +2,7 @@ import {
__useActiveDocument as useActiveDocument,
__useActiveDocumentActions as useActiveDocumentActions,
} from '@elementor/editor-documents';
+import { useMixpanel } from '@elementor/events';
import { LinkIcon } from '@elementor/icons';
import { __ } from '@wordpress/i18n';
@@ -10,11 +11,28 @@ import { type ActionProps } from '../../../types';
export default function useDocumentCopyAndShareProps(): ActionProps {
const document = useActiveDocument();
const { copyAndShare } = useActiveDocumentActions();
+ const { dispatchEvent, config } = useMixpanel();
return {
icon: LinkIcon,
title: __( 'Copy and Share', 'elementor' ),
- onClick: copyAndShare,
+ onClick: () => {
+ const eventName = config?.names?.editorOne?.topBarPublishDropdown;
+ if ( eventName ) {
+ dispatchEvent?.( eventName, {
+ app_type: config?.appTypes?.editor,
+ window_name: config?.appTypes?.editor,
+ interaction_type: config?.triggers?.click?.toLowerCase(),
+ target_type: config?.targetTypes?.dropdownItem,
+ target_name: config?.targetNames?.publishDropdown?.copyAndShare,
+ interaction_result: config?.interactionResults?.actionSelected,
+ target_location: config?.locations?.topBar?.replace( /\s+/g, '_' ).toLowerCase(),
+ location_l1: config?.secondaryLocations?.publishDropdown?.replace( /\s+/g, '_' ).toLowerCase(),
+ location_l2: config?.targetTypes?.dropdownItem,
+ } );
+ }
+ copyAndShare();
+ },
disabled:
! document || document.isSaving || document.isSavingDraft || ! ( 'publish' === document.status.value ),
visible: document?.permissions?.showCopyAndShare,
diff --git a/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-save-draft-props.ts b/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-save-draft-props.ts
index 32441b9d59ba..e9532df1d007 100644
--- a/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-save-draft-props.ts
+++ b/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-save-draft-props.ts
@@ -2,6 +2,7 @@ import {
__useActiveDocument as useActiveDocument,
__useActiveDocumentActions as useActiveDocumentActions,
} from '@elementor/editor-documents';
+import { useMixpanel } from '@elementor/events';
import { FileReportIcon } from '@elementor/icons';
import { __ } from '@wordpress/i18n';
@@ -10,11 +11,28 @@ import { type ActionProps } from '../../../types';
export default function useDocumentSaveDraftProps(): ActionProps {
const document = useActiveDocument();
const { saveDraft } = useActiveDocumentActions();
+ const { dispatchEvent, config } = useMixpanel();
return {
icon: FileReportIcon,
title: __( 'Save Draft', 'elementor' ),
- onClick: saveDraft,
+ onClick: () => {
+ const eventName = config?.names?.editorOne?.topBarPublishDropdown;
+ if ( eventName ) {
+ dispatchEvent?.( eventName, {
+ app_type: config?.appTypes?.editor,
+ window_name: config?.appTypes?.editor,
+ interaction_type: config?.triggers?.click?.toLowerCase(),
+ target_type: config?.targetTypes?.dropdownItem,
+ target_name: config?.targetNames?.publishDropdown?.saveDraft,
+ interaction_result: config?.interactionResults?.actionSelected,
+ target_location: config?.locations?.topBar?.replace( /\s+/g, '_' ).toLowerCase(),
+ location_l1: config?.secondaryLocations?.publishDropdown?.replace( /\s+/g, '_' ).toLowerCase(),
+ location_l2: config?.targetTypes?.dropdownItem,
+ } );
+ }
+ saveDraft();
+ },
disabled: ! document || document.isSaving || document.isSavingDraft || ! document.isDirty,
};
}
diff --git a/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-save-template-props.ts b/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-save-template-props.ts
index 4c8f00214b41..8abe5f21ff2f 100644
--- a/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-save-template-props.ts
+++ b/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-save-template-props.ts
@@ -1,4 +1,5 @@
import { __useActiveDocumentActions as useActiveDocumentActions } from '@elementor/editor-documents';
+import { useMixpanel } from '@elementor/events';
import { FolderIcon } from '@elementor/icons';
import { __ } from '@wordpress/i18n';
@@ -6,10 +7,27 @@ import { type ActionProps } from '../../../types';
export default function useDocumentSaveTemplateProps(): ActionProps {
const { saveTemplate } = useActiveDocumentActions();
+ const { dispatchEvent, config } = useMixpanel();
return {
icon: FolderIcon,
title: __( 'Save as Template', 'elementor' ),
- onClick: saveTemplate,
+ onClick: () => {
+ const eventName = config?.names?.editorOne?.topBarPublishDropdown;
+ if ( eventName ) {
+ dispatchEvent?.( eventName, {
+ app_type: config?.appTypes?.editor,
+ window_name: config?.appTypes?.editor,
+ interaction_type: config?.triggers?.click?.toLowerCase(),
+ target_type: config?.targetTypes?.dropdownItem,
+ target_name: config?.targetNames?.publishDropdown?.saveAsTemplate,
+ interaction_result: config?.interactionResults?.actionSelected,
+ target_location: config?.locations?.topBar?.replace( /\s+/g, '_' ).toLowerCase(),
+ location_l1: config?.secondaryLocations?.publishDropdown?.replace( /\s+/g, '_' ).toLowerCase(),
+ location_l2: config?.targetTypes?.dropdownItem,
+ } );
+ }
+ saveTemplate();
+ },
};
}
diff --git a/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-view-as-markdown-props.ts b/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-view-as-markdown-props.ts
new file mode 100644
index 000000000000..363eb7601b34
--- /dev/null
+++ b/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-view-as-markdown-props.ts
@@ -0,0 +1,29 @@
+import { __useActiveDocument as useActiveDocument } from '@elementor/editor-documents';
+import { __privateRunCommand as runCommand } from '@elementor/editor-v1-adapters';
+import { EyeIcon } from '@elementor/icons';
+import { __ } from '@wordpress/i18n';
+
+export default function useDocumentViewAsMarkdownProps() {
+ const document = useActiveDocument();
+
+ return {
+ icon: EyeIcon,
+ title: __( 'View as Markdown', 'elementor' ),
+ onClick: async () => {
+ const baseUrl = document?.links?.wpPreview || document?.links?.permalink;
+
+ if ( ! baseUrl ) {
+ return;
+ }
+
+ if ( document?.isDirty ) {
+ await runCommand( 'document/save/auto', { force: true } );
+ }
+
+ const separator = baseUrl.includes( '?' ) ? '&' : '?';
+ const url = baseUrl + separator + 'format=markdown';
+
+ window.open( url, '_blank' );
+ },
+ };
+}
diff --git a/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-view-page-props.ts b/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-view-page-props.ts
index f54f72a95c76..34f36752ac9d 100644
--- a/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-view-page-props.ts
+++ b/packages/packages/core/editor-app-bar/src/extensions/documents-save/hooks/use-document-view-page-props.ts
@@ -1,18 +1,36 @@
import { __useActiveDocument as useActiveDocument } from '@elementor/editor-documents';
import { __privateRunCommand as runCommand } from '@elementor/editor-v1-adapters';
+import { useMixpanel } from '@elementor/events';
import { EyeIcon } from '@elementor/icons';
import { __ } from '@wordpress/i18n';
export default function useDocumentViewPageProps() {
const document = useActiveDocument();
+ const { dispatchEvent, config } = useMixpanel();
return {
icon: EyeIcon,
title: __( 'View Page', 'elementor' ),
- onClick: () =>
- document?.id &&
- runCommand( 'editor/documents/view', {
- id: document.id,
- } ),
+ onClick: () => {
+ const eventName = config?.names?.editorOne?.topBarPublishDropdown;
+ if ( eventName ) {
+ dispatchEvent?.( eventName, {
+ app_type: config?.appTypes?.editor,
+ window_name: config?.appTypes?.editor,
+ interaction_type: config?.triggers?.click?.toLowerCase(),
+ target_type: config?.targetTypes?.dropdownItem,
+ target_name: config?.targetNames?.publishDropdown?.viewPage,
+ interaction_result: config?.interactionResults?.actionSelected,
+ target_location: config?.locations?.topBar?.replace( /\s+/g, '_' ).toLowerCase(),
+ location_l1: config?.secondaryLocations?.publishDropdown?.replace( /\s+/g, '_' ).toLowerCase(),
+ location_l2: config?.targetTypes?.dropdownItem,
+ } );
+ }
+ if ( document?.id ) {
+ runCommand( 'editor/documents/view', {
+ id: document.id,
+ } );
+ }
+ },
};
}
diff --git a/packages/packages/core/editor-app-bar/src/extensions/documents-save/index.ts b/packages/packages/core/editor-app-bar/src/extensions/documents-save/index.ts
index 71fad433c796..4403c6f49094 100644
--- a/packages/packages/core/editor-app-bar/src/extensions/documents-save/index.ts
+++ b/packages/packages/core/editor-app-bar/src/extensions/documents-save/index.ts
@@ -3,6 +3,7 @@ import PrimaryAction from './components/primary-action';
import useDocumentCopyAndShareProps from './hooks/use-document-copy-and-share-props';
import useDocumentSaveDraftProps from './hooks/use-document-save-draft-props';
import useDocumentSaveTemplateProps from './hooks/use-document-save-template-props';
+import useDocumentViewAsMarkdownProps from './hooks/use-document-view-as-markdown-props';
import useDocumentViewPageProps from './hooks/use-document-view-page-props';
import { documentOptionsMenu } from './locations';
@@ -37,4 +38,10 @@ export function init() {
priority: 50,
useProps: useDocumentViewPageProps,
} );
+
+ documentOptionsMenu.registerAction( {
+ id: 'document-view-as-markdown',
+ priority: 60,
+ useProps: useDocumentViewAsMarkdownProps,
+ } );
}
diff --git a/packages/packages/core/editor-canvas/src/form-structure/utils.ts b/packages/packages/core/editor-canvas/src/form-structure/utils.ts
index 0ea9990ddc13..788604609326 100644
--- a/packages/packages/core/editor-canvas/src/form-structure/utils.ts
+++ b/packages/packages/core/editor-canvas/src/form-structure/utils.ts
@@ -32,7 +32,13 @@ export type StorageContent = {
};
export const FORM_ELEMENT_TYPE = 'e-form';
-export const FORM_FIELD_ELEMENT_TYPES = new Set( [ 'e-form-input', 'e-form-textarea', 'e-form-label' ] );
+export const FORM_FIELD_ELEMENT_TYPES = new Set( [
+ 'e-form-input',
+ 'e-form-textarea',
+ 'e-form-label',
+ 'e-form-checkbox',
+ 'e-form-submit-button',
+] );
export function getArgsElementType( args: CreateArgs ): string | undefined {
return args.model?.widgetType || args.model?.elType;
diff --git a/packages/packages/core/editor-components/package.json b/packages/packages/core/editor-components/package.json
index e09b4070d1c8..6e4a329f2daf 100644
--- a/packages/packages/core/editor-components/package.json
+++ b/packages/packages/core/editor-components/package.json
@@ -55,7 +55,7 @@
"@elementor/editor-ui": "4.0.0",
"@elementor/editor-v1-adapters": "4.0.0",
"@elementor/http-client": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/events": "4.0.0",
"@elementor/query": "4.0.0",
"@elementor/schema": "4.0.0",
diff --git a/packages/packages/core/editor-components/src/components/__tests__/components-tab.test.tsx b/packages/packages/core/editor-components/src/components/__tests__/components-tab.test.tsx
index 045a81d5efd5..7a6cb3beb83c 100644
--- a/packages/packages/core/editor-components/src/components/__tests__/components-tab.test.tsx
+++ b/packages/packages/core/editor-components/src/components/__tests__/components-tab.test.tsx
@@ -64,7 +64,7 @@ jest.mock( '../../utils/get-container-for-new-element', () => ( {
} ) ),
} ) );
-jest.mock( '../create-component-form/utils/replace-element-with-component', () => ( {
+jest.mock( '../../utils/create-component-model', () => ( {
createComponentModel: jest.fn( ( { id, name } ) => ( { id, name, elType: 'component' } ) ),
} ) );
@@ -159,7 +159,7 @@ describe( 'ComponentsTab', () => {
const buttonComponent = mockComponents[ 0 ];
// Act
- renderWithStore( , store );
+ renderWithStore( , store );
// Assert
const componentItem = screen.getByRole( 'button', { name: /Button Component/ } );
@@ -172,7 +172,7 @@ describe( 'ComponentsTab', () => {
const [ buttonComponent ] = mockComponents;
// Act
- renderWithStore( , store );
+ renderWithStore( , store );
const componentItem = screen.getByRole( 'button', { name: /Button Component/ } );
fireEvent.dragStart( componentItem );
diff --git a/packages/packages/core/editor-components/src/components/components-tab/components-item.tsx b/packages/packages/core/editor-components/src/components/components-tab/components-item.tsx
index 9e996ed8ce7e..703620ae68c1 100644
--- a/packages/packages/core/editor-components/src/components/components-tab/components-item.tsx
+++ b/packages/packages/core/editor-components/src/components/components-tab/components-item.tsx
@@ -23,18 +23,18 @@ import { __ } from '@wordpress/i18n';
import { useComponentsPermissions } from '../../hooks/use-components-permissions';
import { archiveComponent } from '../../store/actions/archive-component';
import { loadComponentsAssets } from '../../store/actions/load-components-assets';
+import { renameComponent } from '../../store/actions/rename-component';
import { type Component } from '../../types';
import { validateComponentName } from '../../utils/component-name-validation';
+import { createComponentModel } from '../../utils/create-component-model';
import { getContainerForNewElement } from '../../utils/get-container-for-new-element';
-import { createComponentModel } from '../create-component-form/utils/replace-element-with-component';
import { DeleteConfirmationDialog } from './delete-confirmation-dialog';
type ComponentItemProps = {
component: Omit< Component, 'id' > & { id?: number };
- renameComponent: ( newName: string ) => void;
};
-export const ComponentItem = ( { component, renameComponent }: ComponentItemProps ) => {
+export const ComponentItem = ( { component }: ComponentItemProps ) => {
const itemRef = useRef< HTMLElement >( null );
const [ isDeleteDialogOpen, setIsDeleteDialogOpen ] = useState( false );
const { canRename, canDelete } = useComponentsPermissions();
@@ -49,9 +49,10 @@ export const ComponentItem = ( { component, renameComponent }: ComponentItemProp
getProps: getEditableProps,
} = useEditable( {
value: component.name,
- onSubmit: renameComponent,
+ onSubmit: ( newName: string ) => renameComponent( component.uid, newName ),
validation: validateComponentTitle,
} );
+
const componentModel = createComponentModel( component );
const popupState = usePopupState( {
diff --git a/packages/packages/core/editor-components/src/components/components-tab/components-list.tsx b/packages/packages/core/editor-components/src/components/components-tab/components-list.tsx
index d22265c4ff0f..d4d730b38698 100644
--- a/packages/packages/core/editor-components/src/components/components-tab/components-list.tsx
+++ b/packages/packages/core/editor-components/src/components/components-tab/components-list.tsx
@@ -5,7 +5,6 @@ import { __ } from '@wordpress/i18n';
import { useComponents } from '../../hooks/use-components';
import { useComponentsPermissions } from '../../hooks/use-components-permissions';
-import { renameComponent } from '../../store/actions/rename-component';
import { ComponentItem } from './components-item';
import { LoadingComponents } from './loading-components';
import { useSearch } from './search-provider';
@@ -25,24 +24,17 @@ export function ComponentsList() {
if ( isLoading ) {
return ;
}
- const isEmpty = ! components || components.length === 0;
+
+ const isEmpty = ! components?.length;
+
if ( isEmpty ) {
- if ( searchValue.length > 0 ) {
- return ;
- }
- return ;
+ return searchValue.length ? : ;
}
return (
{ components.map( ( component ) => (
- {
- renameComponent( component.uid, newName );
- } }
- />
+
) ) }
);
diff --git a/packages/packages/core/editor-components/src/components/components-tab/loading-components.tsx b/packages/packages/core/editor-components/src/components/components-tab/loading-components.tsx
index ab2e04dde97b..d26f9d0630a8 100644
--- a/packages/packages/core/editor-components/src/components/components-tab/loading-components.tsx
+++ b/packages/packages/core/editor-components/src/components/components-tab/loading-components.tsx
@@ -1,8 +1,7 @@
import * as React from 'react';
import { Box, ListItemButton, Skeleton, Stack } from '@elementor/ui';
-const ROWS_COUNT = 6;
-const rows = Array.from( { length: ROWS_COUNT }, ( _, index ) => index );
+const ROWS = Array.from( { length: 6 }, ( _, index ) => index );
export const LoadingComponents = () => {
return (
@@ -26,7 +25,7 @@ export const LoadingComponents = () => {
},
} }
>
- { rows.map( ( row ) => (
+ { ROWS.map( ( row ) => (
);
}
+
+function useComponentInstanceSettings() {
+ const { element } = useElement();
+
+ const settings = useElementSetting< ComponentInstancePropValue >( element.id, 'component_instance' );
+
+ return componentInstancePropTypeUtil.extract( settings );
+}
diff --git a/packages/packages/core/editor-components/src/components/instance-editing-panel/use-resolved-origin-value.tsx b/packages/packages/core/editor-components/src/components/instance-editing-panel/use-resolved-origin-value.tsx
index 67ad22a7a85a..79e344b0e1e0 100644
--- a/packages/packages/core/editor-components/src/components/instance-editing-panel/use-resolved-origin-value.tsx
+++ b/packages/packages/core/editor-components/src/components/instance-editing-panel/use-resolved-origin-value.tsx
@@ -5,8 +5,8 @@ import { type ComponentInstanceOverride } from '../../prop-types/component-insta
import { componentOverridablePropTypeUtil } from '../../prop-types/component-overridable-prop-type';
import { selectData } from '../../store/store';
import { type OverridableProp, type PublishedComponent } from '../../types';
+import { getOverridableProp } from '../../utils/get-overridable-prop';
import { extractInnerOverrideInfo } from '../../utils/overridable-props-utils';
-import { getOverridableProp } from '../overridable-props/utils/get-overridable-prop';
export function useResolvedOriginValue( override: ComponentInstanceOverride | null, overridableProp: OverridableProp ) {
const components = useSelector( selectData );
diff --git a/packages/packages/core/editor-components/src/components/components-tab/component-introduction.tsx b/packages/packages/core/editor-components/src/extended/components/component-introduction.tsx
similarity index 100%
rename from packages/packages/core/editor-components/src/components/components-tab/component-introduction.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-introduction.tsx
diff --git a/packages/packages/core/editor-components/src/components/component-panel-header/__tests__/component-introdaction.test.tsx b/packages/packages/core/editor-components/src/extended/components/component-panel-header/__tests__/component-introdaction.test.tsx
similarity index 96%
rename from packages/packages/core/editor-components/src/components/component-panel-header/__tests__/component-introdaction.test.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-panel-header/__tests__/component-introdaction.test.tsx
index dcab05db1bde..770e361b252c 100644
--- a/packages/packages/core/editor-components/src/components/component-panel-header/__tests__/component-introdaction.test.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/component-panel-header/__tests__/component-introdaction.test.tsx
@@ -3,7 +3,7 @@ import { renderWithTheme } from 'test-utils';
import { fireEvent, screen } from '@testing-library/react';
import { __ } from '@wordpress/i18n';
-import { ComponentIntroduction } from '../../components-tab/component-introduction';
+import { ComponentIntroduction } from '../../component-introduction';
jest.mock( '@wordpress/i18n' );
diff --git a/packages/packages/core/editor-components/src/components/component-panel-header/__tests__/component-panel-header.test.tsx b/packages/packages/core/editor-components/src/extended/components/component-panel-header/__tests__/component-panel-header.test.tsx
similarity index 99%
rename from packages/packages/core/editor-components/src/components/component-panel-header/__tests__/component-panel-header.test.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-panel-header/__tests__/component-panel-header.test.tsx
index 6c5f8c00022b..e88a50e2c842 100644
--- a/packages/packages/core/editor-components/src/components/component-panel-header/__tests__/component-panel-header.test.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/component-panel-header/__tests__/component-panel-header.test.tsx
@@ -13,7 +13,7 @@ import { describe } from '@jest/globals';
import { fireEvent, screen } from '@testing-library/react';
import { __ } from '@wordpress/i18n';
-import { slice } from '../../../store/store';
+import { slice } from '../../../../store/store';
import { ComponentPanelHeader } from '../component-panel-header';
const mockOpenPropertiesPanel = jest.fn();
diff --git a/packages/packages/core/editor-components/src/components/component-panel-header/component-badge.tsx b/packages/packages/core/editor-components/src/extended/components/component-panel-header/component-badge.tsx
similarity index 100%
rename from packages/packages/core/editor-components/src/components/component-panel-header/component-badge.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-panel-header/component-badge.tsx
diff --git a/packages/packages/core/editor-components/src/components/component-panel-header/component-panel-header.tsx b/packages/packages/core/editor-components/src/extended/components/component-panel-header/component-panel-header.tsx
similarity index 92%
rename from packages/packages/core/editor-components/src/components/component-panel-header/component-panel-header.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-panel-header/component-panel-header.tsx
index 106207aee9a8..0693642a4155 100644
--- a/packages/packages/core/editor-components/src/components/component-panel-header/component-panel-header.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/component-panel-header/component-panel-header.tsx
@@ -8,12 +8,12 @@ import { __getState as getState } from '@elementor/store';
import { Box, Divider, IconButton, Tooltip, Typography } from '@elementor/ui';
import { __ } from '@wordpress/i18n';
+import { useSanitizeOverridableProps } from '../../../hooks/use-sanitize-overridable-props';
+import { type ComponentsSlice, SLICE_NAME, useCurrentComponent } from '../../../store/store';
+import { trackComponentEvent } from '../../../utils/tracking';
import { useNavigateBack } from '../../hooks/use-navigate-back';
-import { useSanitizeOverridableProps } from '../../hooks/use-sanitize-overridable-props';
-import { type ComponentsSlice, SLICE_NAME, useCurrentComponent } from '../../store/store';
-import { trackComponentEvent } from '../../utils/tracking';
+import { ComponentIntroduction } from '../component-introduction';
import { usePanelActions } from '../component-properties-panel/component-properties-panel';
-import { ComponentIntroduction } from '../components-tab/component-introduction';
import { ComponentsBadge } from './component-badge';
const MESSAGE_KEY = 'components-properties-introduction';
diff --git a/packages/packages/core/editor-components/src/components/component-properties-panel/__tests__/component-properties-panel.test.tsx b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/__tests__/component-properties-panel.test.tsx
similarity index 99%
rename from packages/packages/core/editor-components/src/components/component-properties-panel/__tests__/component-properties-panel.test.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-properties-panel/__tests__/component-properties-panel.test.tsx
index b7560c591d26..ed2eaa4e1775 100644
--- a/packages/packages/core/editor-components/src/components/component-properties-panel/__tests__/component-properties-panel.test.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/__tests__/component-properties-panel.test.tsx
@@ -10,8 +10,8 @@ import {
} from '@elementor/store';
import { fireEvent, screen, waitFor } from '@testing-library/react';
-import { type ComponentsSlice, selectOverridableProps, slice } from '../../../store/store';
-import { type OverridableProps, type PublishedComponent } from '../../../types';
+import { type ComponentsSlice, selectOverridableProps, slice } from '../../../../store/store';
+import { type OverridableProps, type PublishedComponent } from '../../../../types';
import { ComponentPropertiesPanelContent as ComponentPropertiesPanel } from '../component-properties-panel-content';
jest.mock( '@elementor/editor-documents', () => ( {
diff --git a/packages/packages/core/editor-components/src/components/component-properties-panel/__tests__/validate-group-label.test.ts b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/__tests__/validate-group-label.test.ts
similarity index 100%
rename from packages/packages/core/editor-components/src/components/component-properties-panel/__tests__/validate-group-label.test.ts
rename to packages/packages/core/editor-components/src/extended/components/component-properties-panel/__tests__/validate-group-label.test.ts
diff --git a/packages/packages/core/editor-components/src/components/component-properties-panel/component-properties-panel-content.tsx b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/component-properties-panel-content.tsx
similarity index 97%
rename from packages/packages/core/editor-components/src/components/component-properties-panel/component-properties-panel-content.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-properties-panel/component-properties-panel-content.tsx
index e797b33545c1..d314f75cfb86 100644
--- a/packages/packages/core/editor-components/src/components/component-properties-panel/component-properties-panel-content.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/component-properties-panel-content.tsx
@@ -7,14 +7,14 @@ import { Divider, IconButton, List, Stack, Tooltip } from '@elementor/ui';
import { generateUniqueId } from '@elementor/utils';
import { __ } from '@wordpress/i18n';
-import { useSanitizeOverridableProps } from '../../hooks/use-sanitize-overridable-props';
+import { useSanitizeOverridableProps } from '../../../hooks/use-sanitize-overridable-props';
+import { useCurrentComponentId } from '../../../store/store';
import { addOverridableGroup } from '../../store/actions/add-overridable-group';
import { deleteOverridableGroup } from '../../store/actions/delete-overridable-group';
import { deleteOverridableProp } from '../../store/actions/delete-overridable-prop';
import { reorderGroupProps } from '../../store/actions/reorder-group-props';
import { reorderOverridableGroups } from '../../store/actions/reorder-overridable-groups';
import { updateOverridablePropParams } from '../../store/actions/update-overridable-prop-params';
-import { useCurrentComponentId } from '../../store/store';
import { PropertiesEmptyState } from './properties-empty-state';
import { PropertiesGroup } from './properties-group';
import { SortableItem, SortableProvider } from './sortable';
diff --git a/packages/packages/core/editor-components/src/components/component-properties-panel/component-properties-panel.tsx b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/component-properties-panel.tsx
similarity index 100%
rename from packages/packages/core/editor-components/src/components/component-properties-panel/component-properties-panel.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-properties-panel/component-properties-panel.tsx
diff --git a/packages/packages/core/editor-components/src/components/component-properties-panel/properties-empty-state.tsx b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/properties-empty-state.tsx
similarity index 94%
rename from packages/packages/core/editor-components/src/components/component-properties-panel/properties-empty-state.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-properties-panel/properties-empty-state.tsx
index 96b4e62be419..1d86daf08f68 100644
--- a/packages/packages/core/editor-components/src/components/component-properties-panel/properties-empty-state.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/properties-empty-state.tsx
@@ -4,7 +4,7 @@ import { ComponentPropListIcon } from '@elementor/icons';
import { Link, Stack, Typography } from '@elementor/ui';
import { __ } from '@wordpress/i18n';
-import { ComponentIntroduction } from '../components-tab/component-introduction';
+import { ComponentIntroduction } from '../component-introduction';
export function PropertiesEmptyState( { introductionRef }: { introductionRef: React.RefObject< HTMLButtonElement > } ) {
const [ isOpen, setIsOpen ] = useState( false );
diff --git a/packages/packages/core/editor-components/src/components/component-properties-panel/properties-group.tsx b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/properties-group.tsx
similarity index 99%
rename from packages/packages/core/editor-components/src/components/component-properties-panel/properties-group.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-properties-panel/properties-group.tsx
index 5f0b9bf913d9..c95e28ae9c7b 100644
--- a/packages/packages/core/editor-components/src/components/component-properties-panel/properties-group.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/properties-group.tsx
@@ -15,7 +15,7 @@ import {
} from '@elementor/ui';
import { __ } from '@wordpress/i18n';
-import { type OverridableProp, type OverridablePropsGroup } from '../../types';
+import { type OverridableProp, type OverridablePropsGroup } from '../../../types';
import { PropertyItem } from './property-item';
import { SortableItem, SortableProvider, SortableTrigger, type SortableTriggerProps } from './sortable';
import { type GroupLabelEditableState } from './use-current-editable-item';
diff --git a/packages/packages/core/editor-components/src/components/component-properties-panel/property-item.tsx b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/property-item.tsx
similarity index 98%
rename from packages/packages/core/editor-components/src/components/component-properties-panel/property-item.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-properties-panel/property-item.tsx
index e781f0b92744..bd3c37150adf 100644
--- a/packages/packages/core/editor-components/src/components/component-properties-panel/property-item.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/property-item.tsx
@@ -3,7 +3,7 @@ import { getWidgetsCache } from '@elementor/editor-elements';
import { XIcon } from '@elementor/icons';
import { bindPopover, bindTrigger, Box, IconButton, Popover, Typography, usePopupState } from '@elementor/ui';
-import { type OverridableProp } from '../../types';
+import { type OverridableProp } from '../../../types';
import { OverridablePropForm } from '../overridable-props/overridable-prop-form';
import { SortableTrigger, type SortableTriggerProps } from './sortable';
diff --git a/packages/packages/core/editor-components/src/components/component-properties-panel/sortable.tsx b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/sortable.tsx
similarity index 100%
rename from packages/packages/core/editor-components/src/components/component-properties-panel/sortable.tsx
rename to packages/packages/core/editor-components/src/extended/components/component-properties-panel/sortable.tsx
diff --git a/packages/packages/core/editor-components/src/components/component-properties-panel/use-current-editable-item.ts b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/use-current-editable-item.ts
similarity index 99%
rename from packages/packages/core/editor-components/src/components/component-properties-panel/use-current-editable-item.ts
rename to packages/packages/core/editor-components/src/extended/components/component-properties-panel/use-current-editable-item.ts
index 7e2b144a6da3..c31a376d9861 100644
--- a/packages/packages/core/editor-components/src/components/component-properties-panel/use-current-editable-item.ts
+++ b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/use-current-editable-item.ts
@@ -4,8 +4,8 @@ import { setDocumentModifiedStatus } from '@elementor/editor-documents';
import { useEditable } from '@elementor/editor-ui';
import { __ } from '@wordpress/i18n';
+import { useCurrentComponentId, useOverridableProps } from '../../../store/store';
import { renameOverridableGroup } from '../../store/actions/rename-overridable-group';
-import { useCurrentComponentId, useOverridableProps } from '../../store/store';
import { validateGroupLabel } from './utils/validate-group-label';
export type GroupLabelEditableState = {
diff --git a/packages/packages/core/editor-components/src/components/component-properties-panel/utils/generate-unique-label.ts b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/utils/generate-unique-label.ts
similarity index 88%
rename from packages/packages/core/editor-components/src/components/component-properties-panel/utils/generate-unique-label.ts
rename to packages/packages/core/editor-components/src/extended/components/component-properties-panel/utils/generate-unique-label.ts
index c99294e616a7..6c7a4d0ebef9 100644
--- a/packages/packages/core/editor-components/src/components/component-properties-panel/utils/generate-unique-label.ts
+++ b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/utils/generate-unique-label.ts
@@ -1,4 +1,4 @@
-import { type OverridablePropsGroup } from '../../../types';
+import { type OverridablePropsGroup } from '../../../../types';
const DEFAULT_NEW_GROUP_LABEL = 'New group';
diff --git a/packages/packages/core/editor-components/src/components/component-properties-panel/utils/validate-group-label.ts b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/utils/validate-group-label.ts
similarity index 90%
rename from packages/packages/core/editor-components/src/components/component-properties-panel/utils/validate-group-label.ts
rename to packages/packages/core/editor-components/src/extended/components/component-properties-panel/utils/validate-group-label.ts
index 65bb4db92362..1961ad0ecf72 100644
--- a/packages/packages/core/editor-components/src/components/component-properties-panel/utils/validate-group-label.ts
+++ b/packages/packages/core/editor-components/src/extended/components/component-properties-panel/utils/validate-group-label.ts
@@ -1,6 +1,6 @@
import { __ } from '@wordpress/i18n';
-import { type OverridablePropsGroup } from '../../../types';
+import { type OverridablePropsGroup } from '../../../../types';
export const ERROR_MESSAGES = {
EMPTY_NAME: __( 'Group name is required', 'elementor' ),
diff --git a/packages/packages/core/editor-components/src/components/__tests__/create-component-form.test.tsx b/packages/packages/core/editor-components/src/extended/components/create-component-form/__tests__/create-component-form.test.tsx
similarity index 98%
rename from packages/packages/core/editor-components/src/components/__tests__/create-component-form.test.tsx
rename to packages/packages/core/editor-components/src/extended/components/create-component-form/__tests__/create-component-form.test.tsx
index 7ec593824353..a8d6f88c744d 100644
--- a/packages/packages/core/editor-components/src/components/__tests__/create-component-form.test.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/create-component-form/__tests__/create-component-form.test.tsx
@@ -17,16 +17,16 @@ import { generateUniqueId } from '@elementor/utils';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
-import { apiClient } from '../../api';
-import { selectComponents, slice } from '../../store/store';
-import { switchToComponent } from '../../utils/switch-to-component';
-import { CreateComponentForm } from '../create-component-form/create-component-form';
+import { apiClient } from '../../../../api';
+import { selectComponents, slice } from '../../../../store/store';
+import { switchToComponent } from '../../../../utils/switch-to-component';
+import { CreateComponentForm } from '../create-component-form';
jest.mock( '@elementor/editor-elements' );
-jest.mock( '../../api' );
+jest.mock( '../../../../api' );
jest.mock( '@elementor/utils' );
jest.mock( '@elementor/editor-v1-adapters' );
-jest.mock( '../../utils/switch-to-component' );
+jest.mock( '../../../../utils/switch-to-component' );
jest.mock( '@elementor/editor-notifications' );
const mockGetElementLabel = jest.mocked( getElementLabel );
diff --git a/packages/packages/core/editor-components/src/components/create-component-form/create-component-form.tsx b/packages/packages/core/editor-components/src/extended/components/create-component-form/create-component-form.tsx
similarity index 95%
rename from packages/packages/core/editor-components/src/components/create-component-form/create-component-form.tsx
rename to packages/packages/core/editor-components/src/extended/components/create-component-form/create-component-form.tsx
index 9001f1c721dd..7572fa7bf87c 100644
--- a/packages/packages/core/editor-components/src/components/create-component-form/create-component-form.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/create-component-form/create-component-form.tsx
@@ -8,15 +8,15 @@ import { __getState as getState } from '@elementor/store';
import { Button, FormLabel, Grid, Popover, Stack, TextField, Typography } from '@elementor/ui';
import { __ } from '@wordpress/i18n';
-import { useComponents } from '../../hooks/use-components';
-import { findNonAtomicElementsInElement } from '../../prevent-non-atomic-nesting';
+import { useComponents } from '../../../hooks/use-components';
+import { selectComponentByUid } from '../../../store/store';
+import { type ComponentFormValues, type PublishedComponent } from '../../../types';
+import { createBaseComponentSchema, createSubmitComponentSchema } from '../../../utils/component-form-schema';
+import { switchToComponent } from '../../../utils/switch-to-component';
+import { trackComponentEvent } from '../../../utils/tracking';
import { createUnpublishedComponent } from '../../store/actions/create-unpublished-component';
-import { selectComponentByUid } from '../../store/store';
-import { type ComponentFormValues, type PublishedComponent } from '../../types';
-import { switchToComponent } from '../../utils/switch-to-component';
-import { trackComponentEvent } from '../../utils/tracking';
+import { findNonAtomicElementsInElement } from '../../sync/prevent-non-atomic-nesting';
import { useForm } from './hooks/use-form';
-import { createBaseComponentSchema, createSubmitComponentSchema } from './utils/component-form-schema';
import {
type ComponentEventData,
type ContextMenuEventOptions,
diff --git a/packages/packages/core/editor-components/src/components/create-component-form/hooks/use-form.ts b/packages/packages/core/editor-components/src/extended/components/create-component-form/hooks/use-form.ts
similarity index 100%
rename from packages/packages/core/editor-components/src/components/create-component-form/hooks/use-form.ts
rename to packages/packages/core/editor-components/src/extended/components/create-component-form/hooks/use-form.ts
diff --git a/packages/packages/core/editor-components/src/components/create-component-form/utils/get-component-event-data.ts b/packages/packages/core/editor-components/src/extended/components/create-component-form/utils/get-component-event-data.ts
similarity index 100%
rename from packages/packages/core/editor-components/src/components/create-component-form/utils/get-component-event-data.ts
rename to packages/packages/core/editor-components/src/extended/components/create-component-form/utils/get-component-event-data.ts
diff --git a/packages/packages/core/editor-components/src/components/edit-component/__tests__/component-modal.test.tsx b/packages/packages/core/editor-components/src/extended/components/edit-component/__tests__/component-modal.test.tsx
similarity index 94%
rename from packages/packages/core/editor-components/src/components/edit-component/__tests__/component-modal.test.tsx
rename to packages/packages/core/editor-components/src/extended/components/edit-component/__tests__/component-modal.test.tsx
index f110e94dfd84..7b7e2a914bb0 100644
--- a/packages/packages/core/editor-components/src/components/edit-component/__tests__/component-modal.test.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/edit-component/__tests__/component-modal.test.tsx
@@ -2,12 +2,12 @@ import * as React from 'react';
import { renderWithTheme } from 'test-utils';
import { fireEvent, screen } from '@testing-library/react';
-import { useCanvasDocument } from '../../../hooks/use-canvas-document';
import { ComponentModal } from '../component-modal';
+import { useCanvasDocument } from '../use-canvas-document';
jest.mock( '@elementor/editor-canvas' );
jest.mock( '@elementor/editor-v1-adapters' );
-jest.mock( '../../../hooks/use-canvas-document' );
+jest.mock( '../use-canvas-document' );
describe( '', () => {
let mockOnClose: jest.Mock;
diff --git a/packages/packages/core/editor-components/src/components/edit-component/__tests__/edit-component.test.tsx b/packages/packages/core/editor-components/src/extended/components/edit-component/__tests__/edit-component.test.tsx
similarity index 97%
rename from packages/packages/core/editor-components/src/components/edit-component/__tests__/edit-component.test.tsx
rename to packages/packages/core/editor-components/src/extended/components/edit-component/__tests__/edit-component.test.tsx
index bfc281f55675..deaf1b7ebefe 100644
--- a/packages/packages/core/editor-components/src/components/edit-component/__tests__/edit-component.test.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/edit-component/__tests__/edit-component.test.tsx
@@ -9,9 +9,9 @@ import {
import { __createStore, __registerSlice as registerSlice, type SliceState, type Store } from '@elementor/store';
import { act, fireEvent, screen } from '@testing-library/react';
-import { apiClient } from '../../../api';
-import { slice } from '../../../store/store';
-import { COMPONENT_DOCUMENT_TYPE } from '../../consts';
+import { apiClient } from '../../../../api';
+import { slice } from '../../../../store/store';
+import { COMPONENT_DOCUMENT_TYPE } from '../../../consts';
import { EditComponent } from '../edit-component';
jest.mock( '../component-modal', () => ( {
@@ -28,7 +28,7 @@ jest.mock( '@elementor/editor-documents', () => ( {
invalidateDocumentData: jest.fn(),
} ) );
-jest.mock( '../../../api' );
+jest.mock( '../../../../api' );
const MOCK_DOCUMENT_ID = 1;
const MOCK_COMPONENT_ID = 123;
diff --git a/packages/packages/core/editor-components/src/components/edit-component/component-modal.tsx b/packages/packages/core/editor-components/src/extended/components/edit-component/component-modal.tsx
similarity index 95%
rename from packages/packages/core/editor-components/src/components/edit-component/component-modal.tsx
rename to packages/packages/core/editor-components/src/extended/components/edit-component/component-modal.tsx
index ad4aad352f66..1da7597847a4 100644
--- a/packages/packages/core/editor-components/src/components/edit-component/component-modal.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/edit-component/component-modal.tsx
@@ -3,8 +3,8 @@ import { type CSSProperties, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { __ } from '@wordpress/i18n';
-import { useCanvasDocument } from '../../hooks/use-canvas-document';
-import { useElementRect } from '../../hooks/use-element-rect';
+import { useCanvasDocument } from './use-canvas-document';
+import { useElementRect } from './use-element-rect';
type ModalProps = {
topLevelElementDom: HTMLElement | null;
diff --git a/packages/packages/core/editor-components/src/components/edit-component/edit-component.tsx b/packages/packages/core/editor-components/src/extended/components/edit-component/edit-component.tsx
similarity index 97%
rename from packages/packages/core/editor-components/src/components/edit-component/edit-component.tsx
rename to packages/packages/core/editor-components/src/extended/components/edit-component/edit-component.tsx
index a636ad63fc80..800882816d3a 100644
--- a/packages/packages/core/editor-components/src/components/edit-component/edit-component.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/edit-component/edit-component.tsx
@@ -6,12 +6,12 @@ import { __privateListenTo as listenTo, commandEndEvent } from '@elementor/edito
import { __useSelector as useSelector } from '@elementor/store';
import { throttle } from '@elementor/utils';
-import { apiClient } from '../../api';
+import { apiClient } from '../../../api';
+import { type ComponentsPathItem, selectPath, useCurrentComponentId } from '../../../store/store';
+import { COMPONENT_DOCUMENT_TYPE } from '../../consts';
import { useNavigateBack } from '../../hooks/use-navigate-back';
import { resetSanitizedComponents } from '../../store/actions/reset-sanitized-components';
import { updateCurrentComponent } from '../../store/actions/update-current-component';
-import { type ComponentsPathItem, selectPath, useCurrentComponentId } from '../../store/store';
-import { COMPONENT_DOCUMENT_TYPE } from '../consts';
import { ComponentModal } from './component-modal';
export function EditComponent() {
diff --git a/packages/packages/core/editor-components/src/hooks/use-canvas-document.ts b/packages/packages/core/editor-components/src/extended/components/edit-component/use-canvas-document.ts
similarity index 100%
rename from packages/packages/core/editor-components/src/hooks/use-canvas-document.ts
rename to packages/packages/core/editor-components/src/extended/components/edit-component/use-canvas-document.ts
diff --git a/packages/packages/core/editor-components/src/hooks/use-element-rect.ts b/packages/packages/core/editor-components/src/extended/components/edit-component/use-element-rect.ts
similarity index 100%
rename from packages/packages/core/editor-components/src/hooks/use-element-rect.ts
rename to packages/packages/core/editor-components/src/extended/components/edit-component/use-element-rect.ts
diff --git a/packages/packages/core/editor-components/src/components/overridable-props/__tests__/overridable-prop-control.test.tsx b/packages/packages/core/editor-components/src/extended/components/overridable-props/__tests__/overridable-prop-control.test.tsx
similarity index 98%
rename from packages/packages/core/editor-components/src/components/overridable-props/__tests__/overridable-prop-control.test.tsx
rename to packages/packages/core/editor-components/src/extended/components/overridable-props/__tests__/overridable-prop-control.test.tsx
index fecf88a40685..85e782f3e48e 100644
--- a/packages/packages/core/editor-components/src/components/overridable-props/__tests__/overridable-prop-control.test.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/overridable-props/__tests__/overridable-prop-control.test.tsx
@@ -15,10 +15,10 @@ import {
import { ErrorBoundary } from '@elementor/ui';
import { fireEvent, screen } from '@testing-library/react';
-import { componentOverridablePropTypeUtil } from '../../../prop-types/component-overridable-prop-type';
-import { useOverridablePropValue } from '../../../provider/overridable-prop-context';
-import { type ComponentsSlice, selectOverridableProps, slice } from '../../../store/store';
-import { type OverridableProp, type OverridableProps, type PublishedComponent } from '../../../types';
+import { componentOverridablePropTypeUtil } from '../../../../prop-types/component-overridable-prop-type';
+import { useOverridablePropValue } from '../../../../provider/overridable-prop-context';
+import { type ComponentsSlice, selectOverridableProps, slice } from '../../../../store/store';
+import { type OverridableProp, type OverridableProps, type PublishedComponent } from '../../../../types';
import { OverridablePropControl } from '../overridable-prop-control';
const mockGetControlReplacements = jest.fn< ControlReplacement[], [] >( () => [] );
diff --git a/packages/packages/core/editor-components/src/components/overridable-props/__tests__/overridable-prop-indicator.test.tsx b/packages/packages/core/editor-components/src/extended/components/overridable-props/__tests__/overridable-prop-indicator.test.tsx
similarity index 97%
rename from packages/packages/core/editor-components/src/components/overridable-props/__tests__/overridable-prop-indicator.test.tsx
rename to packages/packages/core/editor-components/src/extended/components/overridable-props/__tests__/overridable-prop-indicator.test.tsx
index 2c266dcb4d45..b4e4fbc936d0 100644
--- a/packages/packages/core/editor-components/src/components/overridable-props/__tests__/overridable-prop-indicator.test.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/overridable-props/__tests__/overridable-prop-indicator.test.tsx
@@ -13,14 +13,14 @@ import {
} from '@elementor/store';
import { fireEvent, screen } from '@testing-library/react';
-import { componentInstanceOverridePropTypeUtil } from '../../../prop-types/component-instance-override-prop-type';
-import { componentInstanceOverridesPropTypeUtil } from '../../../prop-types/component-instance-overrides-prop-type';
-import { componentInstancePropTypeUtil } from '../../../prop-types/component-instance-prop-type';
-import { componentOverridablePropTypeUtil } from '../../../prop-types/component-overridable-prop-type';
-import { OverridablePropProvider } from '../../../provider/overridable-prop-context';
-import { type ComponentsSlice, selectOverridableProps, slice } from '../../../store/store';
-import { type PublishedComponent } from '../../../types';
-import { getContainerByOriginId } from '../../../utils/get-container-by-origin-id';
+import { componentInstanceOverridePropTypeUtil } from '../../../../prop-types/component-instance-override-prop-type';
+import { componentInstanceOverridesPropTypeUtil } from '../../../../prop-types/component-instance-overrides-prop-type';
+import { componentInstancePropTypeUtil } from '../../../../prop-types/component-instance-prop-type';
+import { componentOverridablePropTypeUtil } from '../../../../prop-types/component-overridable-prop-type';
+import { OverridablePropProvider } from '../../../../provider/overridable-prop-context';
+import { type ComponentsSlice, selectOverridableProps, slice } from '../../../../store/store';
+import { type PublishedComponent } from '../../../../types';
+import { getContainerByOriginId } from '../../../../utils/get-container-by-origin-id';
import { OverridablePropIndicator } from '../overridable-prop-indicator';
jest.mock( '@elementor/editor-controls', () => ( {
@@ -36,7 +36,7 @@ jest.mock( '@elementor/editor-elements', () => ( {
getWidgetsCache: jest.fn(),
getElementSetting: jest.fn(),
} ) );
-jest.mock( '../../../utils/get-container-by-origin-id', () => ( {
+jest.mock( '../../../../utils/get-container-by-origin-id', () => ( {
getContainerByOriginId: jest.fn(),
} ) );
diff --git a/packages/packages/core/editor-components/src/components/overridable-props/indicator.tsx b/packages/packages/core/editor-components/src/extended/components/overridable-props/indicator.tsx
similarity index 100%
rename from packages/packages/core/editor-components/src/components/overridable-props/indicator.tsx
rename to packages/packages/core/editor-components/src/extended/components/overridable-props/indicator.tsx
diff --git a/packages/packages/core/editor-components/src/components/overridable-props/overridable-prop-control.tsx b/packages/packages/core/editor-components/src/extended/components/overridable-props/overridable-prop-control.tsx
similarity index 86%
rename from packages/packages/core/editor-components/src/components/overridable-props/overridable-prop-control.tsx
rename to packages/packages/core/editor-components/src/extended/components/overridable-props/overridable-prop-control.tsx
index 37a6dd92e566..25e479aea4da 100644
--- a/packages/packages/core/editor-components/src/components/overridable-props/overridable-prop-control.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/overridable-props/overridable-prop-control.tsx
@@ -11,16 +11,16 @@ import {
import { createTopLevelObjectType, useElement } from '@elementor/editor-editing-panel';
import { type PropValue } from '@elementor/editor-props';
-import { type ComponentInstanceOverridePropValue } from '../../prop-types/component-instance-override-prop-type';
+import { type ComponentInstanceOverridePropValue } from '../../../prop-types/component-instance-override-prop-type';
import {
componentOverridablePropTypeUtil,
type ComponentOverridablePropValue,
-} from '../../prop-types/component-overridable-prop-type';
-import { OverridablePropProvider } from '../../provider/overridable-prop-context';
-import { updateOverridableProp } from '../../store/actions/update-overridable-prop';
-import { useCurrentComponentId, useOverridableProps } from '../../store/store';
-import { getPropTypeForComponentOverride } from '../../utils/get-prop-type-for-component-override';
-import { OVERRIDABLE_PROP_REPLACEMENT_ID } from '../consts';
+} from '../../../prop-types/component-overridable-prop-type';
+import { OverridablePropProvider } from '../../../provider/overridable-prop-context';
+import { updateOverridableProp } from '../../../store/actions/update-overridable-prop';
+import { useCurrentComponentId, useOverridableProps } from '../../../store/store';
+import { getPropTypeForComponentOverride } from '../../../utils/get-prop-type-for-component-override';
+import { OVERRIDABLE_PROP_REPLACEMENT_ID } from '../../consts';
export function OverridablePropControl< T extends object >( {
OriginalControl,
diff --git a/packages/packages/core/editor-components/src/components/overridable-props/overridable-prop-form.tsx b/packages/packages/core/editor-components/src/extended/components/overridable-props/overridable-prop-form.tsx
similarity index 98%
rename from packages/packages/core/editor-components/src/components/overridable-props/overridable-prop-form.tsx
rename to packages/packages/core/editor-components/src/extended/components/overridable-props/overridable-prop-form.tsx
index 3c1fb95e1882..f099630bcbbe 100644
--- a/packages/packages/core/editor-components/src/components/overridable-props/overridable-prop-form.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/overridable-props/overridable-prop-form.tsx
@@ -4,7 +4,7 @@ import { Form, MenuListItem } from '@elementor/editor-ui';
import { Button, FormLabel, Grid, Select, Stack, type SxProps, TextField, Typography } from '@elementor/ui';
import { __ } from '@wordpress/i18n';
-import { type OverridableProp } from '../../types';
+import { type OverridableProp } from '../../../types';
import { validatePropLabel } from './utils/validate-prop-label';
const SIZE = 'tiny';
diff --git a/packages/packages/core/editor-components/src/components/overridable-props/overridable-prop-indicator.tsx b/packages/packages/core/editor-components/src/extended/components/overridable-props/overridable-prop-indicator.tsx
similarity index 89%
rename from packages/packages/core/editor-components/src/components/overridable-props/overridable-prop-indicator.tsx
rename to packages/packages/core/editor-components/src/extended/components/overridable-props/overridable-prop-indicator.tsx
index 8973ff1059ce..c98abdbe0774 100644
--- a/packages/packages/core/editor-components/src/components/overridable-props/overridable-prop-indicator.tsx
+++ b/packages/packages/core/editor-components/src/extended/components/overridable-props/overridable-prop-indicator.tsx
@@ -6,16 +6,16 @@ import { type PropType, type TransformablePropValue } from '@elementor/editor-pr
import { bindPopover, bindTrigger, Popover, Tooltip, usePopupState } from '@elementor/ui';
import { __ } from '@wordpress/i18n';
-import { useSanitizeOverridableProps } from '../../hooks/use-sanitize-overridable-props';
-import { componentOverridablePropTypeUtil } from '../../prop-types/component-overridable-prop-type';
-import { useComponentInstanceElement, useOverridablePropValue } from '../../provider/overridable-prop-context';
+import { useSanitizeOverridableProps } from '../../../hooks/use-sanitize-overridable-props';
+import { componentOverridablePropTypeUtil } from '../../../prop-types/component-overridable-prop-type';
+import { useComponentInstanceElement, useOverridablePropValue } from '../../../provider/overridable-prop-context';
+import { useCurrentComponentId } from '../../../store/store';
+import { type OverridableProps } from '../../../types';
+import { getOverridableProp } from '../../../utils/get-overridable-prop';
+import { resolveOverridePropValue } from '../../../utils/resolve-override-prop-value';
import { setOverridableProp } from '../../store/actions/set-overridable-prop';
-import { useCurrentComponentId } from '../../store/store';
-import { type OverridableProps } from '../../types';
-import { resolveOverridePropValue } from '../../utils/resolve-override-prop-value';
import { Indicator } from './indicator';
import { OverridablePropForm } from './overridable-prop-form';
-import { getOverridableProp } from './utils/get-overridable-prop';
export function OverridablePropIndicator() {
const { propType } = useBoundProp();
diff --git a/packages/packages/core/editor-components/src/components/overridable-props/utils/validate-prop-label.ts b/packages/packages/core/editor-components/src/extended/components/overridable-props/utils/validate-prop-label.ts
similarity index 100%
rename from packages/packages/core/editor-components/src/components/overridable-props/utils/validate-prop-label.ts
rename to packages/packages/core/editor-components/src/extended/components/overridable-props/utils/validate-prop-label.ts
diff --git a/packages/packages/core/editor-components/src/components/consts.ts b/packages/packages/core/editor-components/src/extended/consts.ts
similarity index 99%
rename from packages/packages/core/editor-components/src/components/consts.ts
rename to packages/packages/core/editor-components/src/extended/consts.ts
index df2e0f5d130a..a40b2b5a5f14 100644
--- a/packages/packages/core/editor-components/src/components/consts.ts
+++ b/packages/packages/core/editor-components/src/extended/consts.ts
@@ -1,2 +1,3 @@
-export const COMPONENT_DOCUMENT_TYPE = 'elementor_component';
export const OVERRIDABLE_PROP_REPLACEMENT_ID = 'overridable-prop';
+
+export const COMPONENT_DOCUMENT_TYPE = 'elementor_component';
diff --git a/packages/packages/core/editor-components/src/hooks/use-navigate-back.ts b/packages/packages/core/editor-components/src/extended/hooks/use-navigate-back.ts
similarity index 85%
rename from packages/packages/core/editor-components/src/hooks/use-navigate-back.ts
rename to packages/packages/core/editor-components/src/extended/hooks/use-navigate-back.ts
index e58e79282e8f..9a4f2243231a 100644
--- a/packages/packages/core/editor-components/src/hooks/use-navigate-back.ts
+++ b/packages/packages/core/editor-components/src/extended/hooks/use-navigate-back.ts
@@ -2,8 +2,8 @@ import { useCallback } from 'react';
import { getV1DocumentsManager } from '@elementor/editor-documents';
import { __useSelector as useSelector } from '@elementor/store';
-import { selectPath } from '../store/store';
-import { switchToComponent } from '../utils/switch-to-component';
+import { selectPath } from '../../store/store';
+import { switchToComponent } from '../../utils/switch-to-component';
export function useNavigateBack() {
const path = useSelector( selectPath );
diff --git a/packages/packages/core/editor-components/src/extended/init.ts b/packages/packages/core/editor-components/src/extended/init.ts
new file mode 100644
index 000000000000..24723076e21d
--- /dev/null
+++ b/packages/packages/core/editor-components/src/extended/init.ts
@@ -0,0 +1,83 @@
+import { injectIntoLogic, injectIntoTop } from '@elementor/editor';
+import { registerControlReplacement } from '@elementor/editor-controls';
+import { getV1CurrentDocument } from '@elementor/editor-documents';
+import { FIELD_TYPE, injectIntoPanelHeaderTop, registerFieldIndicator } from '@elementor/editor-editing-panel';
+import { __registerPanel as registerPanel } from '@elementor/editor-panels';
+import { registerDataHook } from '@elementor/editor-v1-adapters';
+
+import { componentOverridablePropTypeUtil } from '../prop-types/component-overridable-prop-type';
+import { type ExtendedWindow } from '../types';
+import { onElementDrop } from '../utils/tracking';
+import { ComponentPanelHeader } from './components/component-panel-header/component-panel-header';
+import { panel as componentPropertiesPanel } from './components/component-properties-panel/component-properties-panel';
+import { CreateComponentForm } from './components/create-component-form/create-component-form';
+import { EditComponent } from './components/edit-component/edit-component';
+import { OverridablePropControl } from './components/overridable-props/overridable-prop-control';
+import { OverridablePropIndicator } from './components/overridable-props/overridable-prop-indicator';
+import { COMPONENT_DOCUMENT_TYPE, OVERRIDABLE_PROP_REPLACEMENT_ID } from './consts';
+import { initMcp } from './mcp';
+import { beforeSave } from './sync/before-save';
+import { initCleanupOverridablePropsOnDelete } from './sync/cleanup-overridable-props-on-delete';
+import { initHandleComponentEditModeContainer } from './sync/handle-component-edit-mode-container';
+import { initNonAtomicNestingPrevention } from './sync/prevent-non-atomic-nesting';
+import { initRevertOverridablesOnCopyOrDuplicate } from './sync/revert-overridables-on-copy-or-duplicate';
+import { SanitizeOverridableProps } from './sync/sanitize-overridable-props';
+
+export function initExtended() {
+ registerPanel( componentPropertiesPanel );
+
+ registerDataHook( 'dependency', 'editor/documents/close', ( args ) => {
+ const document = getV1CurrentDocument();
+ if ( document.config.type === COMPONENT_DOCUMENT_TYPE ) {
+ args.mode = 'autosave';
+ }
+ return true;
+ } );
+
+ registerDataHook( 'after', 'preview/drop', onElementDrop );
+
+ ( window as unknown as ExtendedWindow ).elementorCommon.__beforeSave = beforeSave;
+
+ injectIntoTop( {
+ id: 'create-component-popup',
+ component: CreateComponentForm,
+ } );
+
+ injectIntoTop( {
+ id: 'edit-component',
+ component: EditComponent,
+ } );
+
+ injectIntoPanelHeaderTop( {
+ id: 'component-panel-header',
+ component: ComponentPanelHeader,
+ } );
+
+ registerFieldIndicator( {
+ fieldType: FIELD_TYPE.SETTINGS,
+ id: 'component-overridable-prop',
+ priority: 1,
+ indicator: OverridablePropIndicator,
+ } );
+
+ registerControlReplacement( {
+ id: OVERRIDABLE_PROP_REPLACEMENT_ID,
+ component: OverridablePropControl,
+ condition: ( { value } ) => componentOverridablePropTypeUtil.isValid( value ),
+ } );
+
+ initCleanupOverridablePropsOnDelete();
+
+ initMcp();
+
+ initNonAtomicNestingPrevention();
+
+ initHandleComponentEditModeContainer();
+
+ initRevertOverridablesOnCopyOrDuplicate();
+
+ injectIntoLogic( {
+ id: 'sanitize-overridable-props',
+ component: SanitizeOverridableProps,
+ } );
+}
diff --git a/packages/packages/core/editor-components/src/mcp/__tests__/save-as-component-tool.test.ts b/packages/packages/core/editor-components/src/extended/mcp/__tests__/save-as-component-tool.test.ts
similarity index 99%
rename from packages/packages/core/editor-components/src/mcp/__tests__/save-as-component-tool.test.ts
rename to packages/packages/core/editor-components/src/extended/mcp/__tests__/save-as-component-tool.test.ts
index c067e4a3afcf..beaa4490b7f3 100644
--- a/packages/packages/core/editor-components/src/mcp/__tests__/save-as-component-tool.test.ts
+++ b/packages/packages/core/editor-components/src/extended/mcp/__tests__/save-as-component-tool.test.ts
@@ -7,7 +7,7 @@ import {
} from '@elementor/editor-elements';
import { AxiosError } from '@elementor/http-client';
-import { apiClient } from '../../api';
+import { apiClient } from '../../../api';
import { createUnpublishedComponent } from '../../store/actions/create-unpublished-component';
import { ERROR_MESSAGES, handleSaveAsComponent } from '../save-as-component-tool';
@@ -19,7 +19,7 @@ jest.mock( '@elementor/editor-mcp', () => ( {
} ),
} ) );
jest.mock( '../../store/actions/create-unpublished-component' );
-jest.mock( '../../api' );
+jest.mock( '../../../api' );
const mockGetContainer = jest.mocked( getContainer );
const mockGetElementType = jest.mocked( getElementType );
diff --git a/packages/packages/core/editor-components/src/mcp/index.ts b/packages/packages/core/editor-components/src/extended/mcp/index.ts
similarity index 100%
rename from packages/packages/core/editor-components/src/mcp/index.ts
rename to packages/packages/core/editor-components/src/extended/mcp/index.ts
diff --git a/packages/packages/core/editor-components/src/mcp/save-as-component-tool.ts b/packages/packages/core/editor-components/src/extended/mcp/save-as-component-tool.ts
similarity index 99%
rename from packages/packages/core/editor-components/src/mcp/save-as-component-tool.ts
rename to packages/packages/core/editor-components/src/extended/mcp/save-as-component-tool.ts
index d2eeef6c24b8..eb842a370263 100644
--- a/packages/packages/core/editor-components/src/mcp/save-as-component-tool.ts
+++ b/packages/packages/core/editor-components/src/extended/mcp/save-as-component-tool.ts
@@ -6,9 +6,9 @@ import { AxiosError } from '@elementor/http-client';
import { z } from '@elementor/schema';
import { generateUniqueId } from '@elementor/utils';
-import { apiClient } from '../api';
+import { apiClient } from '../../api';
+import { type OverridableProps } from '../../types';
import { createUnpublishedComponent } from '../store/actions/create-unpublished-component';
-import { type OverridableProps } from '../types';
const InputSchema = {
element_id: z
diff --git a/packages/packages/core/editor-components/src/store/__tests__/delete-overridable-prop.test.ts b/packages/packages/core/editor-components/src/extended/store/__tests__/delete-overridable-prop.test.ts
similarity index 96%
rename from packages/packages/core/editor-components/src/store/__tests__/delete-overridable-prop.test.ts
rename to packages/packages/core/editor-components/src/extended/store/__tests__/delete-overridable-prop.test.ts
index fd30b62b4568..f9ccad9efa5e 100644
--- a/packages/packages/core/editor-components/src/store/__tests__/delete-overridable-prop.test.ts
+++ b/packages/packages/core/editor-components/src/extended/store/__tests__/delete-overridable-prop.test.ts
@@ -3,16 +3,16 @@ import { getContainer, getElementSetting, updateElementSettings, type V1ElementD
import { numberPropTypeUtil, type PropValue, type TransformablePropValue } from '@elementor/editor-props';
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
-import { componentInstanceOverridePropTypeUtil } from '../../prop-types/component-instance-override-prop-type';
+import { componentInstanceOverridePropTypeUtil } from '../../../prop-types/component-instance-override-prop-type';
import {
componentInstanceOverridesPropTypeUtil,
type ComponentInstanceOverridesPropValue,
-} from '../../prop-types/component-instance-overrides-prop-type';
-import { componentInstancePropTypeUtil } from '../../prop-types/component-instance-prop-type';
-import { componentOverridablePropTypeUtil } from '../../prop-types/component-overridable-prop-type';
-import type { OverridableProp, OverridableProps, PublishedComponent } from '../../types';
+} from '../../../prop-types/component-instance-overrides-prop-type';
+import { componentInstancePropTypeUtil } from '../../../prop-types/component-instance-prop-type';
+import { componentOverridablePropTypeUtil } from '../../../prop-types/component-overridable-prop-type';
+import { SLICE_NAME } from '../../../store/store';
+import type { OverridableProp, OverridableProps, PublishedComponent } from '../../../types';
import { deleteOverridableProp } from '../actions/delete-overridable-prop';
-import { SLICE_NAME } from '../store';
jest.mock( '@elementor/editor-elements' );
diff --git a/packages/packages/core/editor-components/src/store/__tests__/set-overridable-prop.test.ts b/packages/packages/core/editor-components/src/extended/store/__tests__/set-overridable-prop.test.ts
similarity index 98%
rename from packages/packages/core/editor-components/src/store/__tests__/set-overridable-prop.test.ts
rename to packages/packages/core/editor-components/src/extended/store/__tests__/set-overridable-prop.test.ts
index 41527eb7732d..ae5144ba6a67 100644
--- a/packages/packages/core/editor-components/src/store/__tests__/set-overridable-prop.test.ts
+++ b/packages/packages/core/editor-components/src/extended/store/__tests__/set-overridable-prop.test.ts
@@ -1,9 +1,9 @@
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
import { generateUniqueId } from '@elementor/utils';
-import type { PublishedComponent } from '../../types';
+import { SLICE_NAME } from '../../../store/store';
+import type { PublishedComponent } from '../../../types';
import { setOverridableProp } from '../actions/set-overridable-prop';
-import { SLICE_NAME } from '../store';
jest.mock( '@elementor/store', () => ( {
...jest.requireActual( '@elementor/store' ),
diff --git a/packages/packages/core/editor-components/src/store/actions/add-overridable-group.ts b/packages/packages/core/editor-components/src/extended/store/actions/add-overridable-group.ts
similarity index 90%
rename from packages/packages/core/editor-components/src/store/actions/add-overridable-group.ts
rename to packages/packages/core/editor-components/src/extended/store/actions/add-overridable-group.ts
index c59f03d5f472..32d383f31a83 100644
--- a/packages/packages/core/editor-components/src/store/actions/add-overridable-group.ts
+++ b/packages/packages/core/editor-components/src/extended/store/actions/add-overridable-group.ts
@@ -1,8 +1,8 @@
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
-import { type ComponentId, type OverridablePropsGroup } from '../../types';
-import { type Source, trackComponentEvent } from '../../utils/tracking';
-import { selectCurrentComponent, selectOverridableProps, slice } from '../store';
+import { selectCurrentComponent, selectOverridableProps, slice } from '../../../store/store';
+import { type ComponentId, type OverridablePropsGroup } from '../../../types';
+import { type Source, trackComponentEvent } from '../../../utils/tracking';
type AddGroupParams = {
componentId: ComponentId;
diff --git a/packages/packages/core/editor-components/src/store/actions/create-unpublished-component.ts b/packages/packages/core/editor-components/src/extended/store/actions/create-unpublished-component.ts
similarity index 92%
rename from packages/packages/core/editor-components/src/store/actions/create-unpublished-component.ts
rename to packages/packages/core/editor-components/src/extended/store/actions/create-unpublished-component.ts
index 476a0be9270d..64417efbece5 100644
--- a/packages/packages/core/editor-components/src/store/actions/create-unpublished-component.ts
+++ b/packages/packages/core/editor-components/src/extended/store/actions/create-unpublished-component.ts
@@ -4,12 +4,12 @@ import { __dispatch as dispatch } from '@elementor/store';
import { generateUniqueId } from '@elementor/utils';
import { __ } from '@wordpress/i18n';
+import { slice } from '../../../store/store';
+import { type OriginalElementData, type OverridableProps } from '../../../types';
+import { type Source, trackComponentEvent } from '../../../utils/tracking';
import { type ComponentEventData } from '../../components/create-component-form/utils/get-component-event-data';
-import { replaceElementWithComponent } from '../../components/create-component-form/utils/replace-element-with-component';
-import { type OriginalElementData, type OverridableProps } from '../../types';
+import { replaceElementWithComponent } from '../../utils/replace-element-with-component';
import { revertAllOverridablesInElementData } from '../../utils/revert-overridable-settings';
-import { type Source, trackComponentEvent } from '../../utils/tracking';
-import { slice } from '../store';
type CreateUnpublishedComponentParams = {
name: string;
diff --git a/packages/packages/core/editor-components/src/store/actions/delete-overridable-group.ts b/packages/packages/core/editor-components/src/extended/store/actions/delete-overridable-group.ts
similarity index 87%
rename from packages/packages/core/editor-components/src/store/actions/delete-overridable-group.ts
rename to packages/packages/core/editor-components/src/extended/store/actions/delete-overridable-group.ts
index 2f0470dfc56f..76df370ce1fd 100644
--- a/packages/packages/core/editor-components/src/store/actions/delete-overridable-group.ts
+++ b/packages/packages/core/editor-components/src/extended/store/actions/delete-overridable-group.ts
@@ -1,7 +1,7 @@
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
-import { type ComponentId } from '../../types';
-import { selectOverridableProps, slice } from '../store';
+import { selectOverridableProps, slice } from '../../../store/store';
+import { type ComponentId } from '../../../types';
import { deleteGroup } from '../utils/groups-transformers';
type DeleteGroupParams = {
diff --git a/packages/packages/core/editor-components/src/extended/store/actions/delete-overridable-prop.ts b/packages/packages/core/editor-components/src/extended/store/actions/delete-overridable-prop.ts
new file mode 100644
index 000000000000..000cea8acee4
--- /dev/null
+++ b/packages/packages/core/editor-components/src/extended/store/actions/delete-overridable-prop.ts
@@ -0,0 +1,70 @@
+import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
+
+import { selectCurrentComponent, selectOverridableProps, slice } from '../../../store/store';
+import { type ComponentId, type OverridableProp } from '../../../types';
+import { type Source, trackComponentEvent } from '../../../utils/tracking';
+import { revertElementOverridableSetting } from '../../utils/revert-overridable-settings';
+import { removePropFromAllGroups } from '../utils/groups-transformers';
+
+type DeletePropParams = {
+ componentId: ComponentId;
+ propKey: string | string[];
+ source: Source;
+};
+
+export function deleteOverridableProp( { componentId, propKey, source }: DeletePropParams ): void {
+ const overridableProps = selectOverridableProps( getState(), componentId );
+
+ if ( ! overridableProps || Object.keys( overridableProps.props ).length === 0 ) {
+ return;
+ }
+
+ const propKeysToDelete = Array.isArray( propKey ) ? propKey : [ propKey ];
+ const deletedProps: OverridableProp[] = [];
+
+ for ( const key of propKeysToDelete ) {
+ const prop = overridableProps.props[ key ];
+
+ if ( ! prop ) {
+ continue;
+ }
+
+ deletedProps.push( prop );
+ revertElementOverridableSetting( prop.elementId, prop.propKey, prop.originValue, key );
+ }
+
+ if ( deletedProps.length === 0 ) {
+ return;
+ }
+
+ const remainingProps = Object.fromEntries(
+ Object.entries( overridableProps.props ).filter( ( [ key ] ) => ! propKeysToDelete.includes( key ) )
+ );
+
+ const updatedGroups = removePropFromAllGroups( overridableProps.groups, propKey );
+
+ dispatch(
+ slice.actions.setOverridableProps( {
+ componentId,
+ overridableProps: {
+ ...overridableProps,
+ props: remainingProps,
+ groups: updatedGroups,
+ },
+ } )
+ );
+
+ const currentComponent = selectCurrentComponent( getState() );
+
+ for ( const prop of deletedProps ) {
+ trackComponentEvent( {
+ action: 'propertyRemoved',
+ source,
+ component_uid: currentComponent?.uid,
+ property_id: prop.overrideKey,
+ property_path: prop.propKey,
+ property_name: prop.label,
+ element_type: prop.widgetType ?? prop.elType,
+ } );
+ }
+}
diff --git a/packages/packages/core/editor-components/src/store/actions/rename-overridable-group.ts b/packages/packages/core/editor-components/src/extended/store/actions/rename-overridable-group.ts
similarity index 87%
rename from packages/packages/core/editor-components/src/store/actions/rename-overridable-group.ts
rename to packages/packages/core/editor-components/src/extended/store/actions/rename-overridable-group.ts
index 09a1bc189428..efdbcadf60bc 100644
--- a/packages/packages/core/editor-components/src/store/actions/rename-overridable-group.ts
+++ b/packages/packages/core/editor-components/src/extended/store/actions/rename-overridable-group.ts
@@ -1,7 +1,7 @@
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
-import { type ComponentId } from '../../types';
-import { selectOverridableProps, slice } from '../store';
+import { selectOverridableProps, slice } from '../../../store/store';
+import { type ComponentId } from '../../../types';
import { renameGroup } from '../utils/groups-transformers';
type RenameGroupParams = {
diff --git a/packages/packages/core/editor-components/src/store/actions/reorder-group-props.ts b/packages/packages/core/editor-components/src/extended/store/actions/reorder-group-props.ts
similarity index 87%
rename from packages/packages/core/editor-components/src/store/actions/reorder-group-props.ts
rename to packages/packages/core/editor-components/src/extended/store/actions/reorder-group-props.ts
index 9147f356f876..94aadad73a50 100644
--- a/packages/packages/core/editor-components/src/store/actions/reorder-group-props.ts
+++ b/packages/packages/core/editor-components/src/extended/store/actions/reorder-group-props.ts
@@ -1,7 +1,7 @@
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
-import { type ComponentId } from '../../types';
-import { selectOverridableProps, slice } from '../store';
+import { selectOverridableProps, slice } from '../../../store/store';
+import { type ComponentId } from '../../../types';
type ReorderGroupPropsParams = {
componentId: ComponentId;
diff --git a/packages/packages/core/editor-components/src/store/actions/reorder-overridable-groups.ts b/packages/packages/core/editor-components/src/extended/store/actions/reorder-overridable-groups.ts
similarity index 83%
rename from packages/packages/core/editor-components/src/store/actions/reorder-overridable-groups.ts
rename to packages/packages/core/editor-components/src/extended/store/actions/reorder-overridable-groups.ts
index a0fcade77f43..8858417cbf01 100644
--- a/packages/packages/core/editor-components/src/store/actions/reorder-overridable-groups.ts
+++ b/packages/packages/core/editor-components/src/extended/store/actions/reorder-overridable-groups.ts
@@ -1,7 +1,7 @@
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
-import { type ComponentId } from '../../types';
-import { selectOverridableProps, slice } from '../store';
+import { selectOverridableProps, slice } from '../../../store/store';
+import { type ComponentId } from '../../../types';
type ReorderGroupsParams = {
componentId: ComponentId;
diff --git a/packages/packages/core/editor-components/src/store/actions/reset-sanitized-components.ts b/packages/packages/core/editor-components/src/extended/store/actions/reset-sanitized-components.ts
similarity index 77%
rename from packages/packages/core/editor-components/src/store/actions/reset-sanitized-components.ts
rename to packages/packages/core/editor-components/src/extended/store/actions/reset-sanitized-components.ts
index 6c7a9e113dd1..bca0f158c57e 100644
--- a/packages/packages/core/editor-components/src/store/actions/reset-sanitized-components.ts
+++ b/packages/packages/core/editor-components/src/extended/store/actions/reset-sanitized-components.ts
@@ -1,6 +1,6 @@
import { __dispatch as dispatch } from '@elementor/store';
-import { slice } from '../store';
+import { slice } from '../../../store/store';
export function resetSanitizedComponents() {
dispatch( slice.actions.resetSanitizedComponents() );
diff --git a/packages/packages/core/editor-components/src/store/actions/set-overridable-prop.ts b/packages/packages/core/editor-components/src/extended/store/actions/set-overridable-prop.ts
similarity index 96%
rename from packages/packages/core/editor-components/src/store/actions/set-overridable-prop.ts
rename to packages/packages/core/editor-components/src/extended/store/actions/set-overridable-prop.ts
index 90bdc288adcb..2b93f4bf9edb 100644
--- a/packages/packages/core/editor-components/src/store/actions/set-overridable-prop.ts
+++ b/packages/packages/core/editor-components/src/extended/store/actions/set-overridable-prop.ts
@@ -2,9 +2,9 @@ import { type PropValue } from '@elementor/editor-props';
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
import { generateUniqueId } from '@elementor/utils';
-import { type OriginPropFields, type OverridableProp } from '../../types';
-import { type Source, trackComponentEvent } from '../../utils/tracking';
-import { selectCurrentComponent, selectOverridableProps, slice } from '../store';
+import { selectCurrentComponent, selectOverridableProps, slice } from '../../../store/store';
+import { type OriginPropFields, type OverridableProp } from '../../../types';
+import { type Source, trackComponentEvent } from '../../../utils/tracking';
import {
addPropToGroup,
ensureGroupInOrder,
diff --git a/packages/packages/core/editor-components/src/store/actions/update-component-sanitized-attribute.ts b/packages/packages/core/editor-components/src/extended/store/actions/update-component-sanitized-attribute.ts
similarity index 68%
rename from packages/packages/core/editor-components/src/store/actions/update-component-sanitized-attribute.ts
rename to packages/packages/core/editor-components/src/extended/store/actions/update-component-sanitized-attribute.ts
index bf4a20e102d2..82f028a75a41 100644
--- a/packages/packages/core/editor-components/src/store/actions/update-component-sanitized-attribute.ts
+++ b/packages/packages/core/editor-components/src/extended/store/actions/update-component-sanitized-attribute.ts
@@ -1,7 +1,7 @@
import { __dispatch as dispatch } from '@elementor/store';
-import { type ComponentId } from '../../types';
-import { type SanitizeAttributes, slice } from '../store';
+import { type SanitizeAttributes, slice } from '../../../store/store';
+import { type ComponentId } from '../../../types';
export function updateComponentSanitizedAttribute( componentId: ComponentId, attribute: SanitizeAttributes ) {
dispatch( slice.actions.updateComponentSanitizedAttribute( { componentId, attribute } ) );
diff --git a/packages/packages/core/editor-components/src/store/actions/update-current-component.ts b/packages/packages/core/editor-components/src/extended/store/actions/update-current-component.ts
similarity index 50%
rename from packages/packages/core/editor-components/src/store/actions/update-current-component.ts
rename to packages/packages/core/editor-components/src/extended/store/actions/update-current-component.ts
index 5f6fdd1439d4..2fdb2523936b 100644
--- a/packages/packages/core/editor-components/src/store/actions/update-current-component.ts
+++ b/packages/packages/core/editor-components/src/extended/store/actions/update-current-component.ts
@@ -1,7 +1,7 @@
-import { setDocumentModifiedStatus, type V1Document } from '@elementor/editor-documents';
+import { type V1Document } from '@elementor/editor-documents';
import { __getStore as getStore } from '@elementor/store';
-import { type ComponentsPathItem, slice } from '../store';
+import { type ComponentsPathItem, slice } from '../../../store/store';
export function updateCurrentComponent( {
path,
@@ -19,15 +19,3 @@ export function updateCurrentComponent( {
dispatch( slice.actions.setPath( path ) );
dispatch( slice.actions.setCurrentComponentId( currentComponentId ) );
}
-
-export const archiveComponent = ( componentId: number ) => {
- const store = getStore();
- const dispatch = store?.dispatch;
-
- if ( ! dispatch ) {
- return;
- }
-
- dispatch( slice.actions.archive( componentId ) );
- setDocumentModifiedStatus( true );
-};
diff --git a/packages/packages/core/editor-components/src/store/actions/update-overridable-prop-params.ts b/packages/packages/core/editor-components/src/extended/store/actions/update-overridable-prop-params.ts
similarity index 89%
rename from packages/packages/core/editor-components/src/store/actions/update-overridable-prop-params.ts
rename to packages/packages/core/editor-components/src/extended/store/actions/update-overridable-prop-params.ts
index 6004ce5f7d1d..2d5154b8420d 100644
--- a/packages/packages/core/editor-components/src/store/actions/update-overridable-prop-params.ts
+++ b/packages/packages/core/editor-components/src/extended/store/actions/update-overridable-prop-params.ts
@@ -1,7 +1,7 @@
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
-import { type ComponentId, type OverridableProp } from '../../types';
-import { selectOverridableProps, slice } from '../store';
+import { selectOverridableProps, slice } from '../../../store/store';
+import { type ComponentId, type OverridableProp } from '../../../types';
import { movePropBetweenGroups } from '../utils/groups-transformers';
type UpdatePropParams = {
diff --git a/packages/packages/core/editor-components/src/store/utils/groups-transformers.ts b/packages/packages/core/editor-components/src/extended/store/utils/groups-transformers.ts
similarity index 92%
rename from packages/packages/core/editor-components/src/store/utils/groups-transformers.ts
rename to packages/packages/core/editor-components/src/extended/store/utils/groups-transformers.ts
index 62e4818e2f92..3bd48f4c1f0f 100644
--- a/packages/packages/core/editor-components/src/store/utils/groups-transformers.ts
+++ b/packages/packages/core/editor-components/src/extended/store/utils/groups-transformers.ts
@@ -1,11 +1,13 @@
import { generateUniqueId } from '@elementor/utils';
import { __ } from '@wordpress/i18n';
-import { type OverridableProp, type OverridableProps, type OverridablePropsGroup } from '../../types';
+import { type OverridableProp, type OverridableProps, type OverridablePropsGroup } from '../../../types';
type Groups = OverridableProps[ 'groups' ];
-export function removePropFromAllGroups( groups: Groups, propKey: string ): Groups {
+export function removePropFromAllGroups( groups: Groups, propKey: string | string[] ): Groups {
+ const propKeys = Array.isArray( propKey ) ? propKey : [ propKey ];
+
return {
...groups,
items: Object.fromEntries(
@@ -13,7 +15,7 @@ export function removePropFromAllGroups( groups: Groups, propKey: string ): Grou
groupId,
{
...group,
- props: group.props.filter( ( p ) => p !== propKey ),
+ props: group.props.filter( ( p ) => ! propKeys.includes( p ) ),
},
] )
),
@@ -93,7 +95,7 @@ export function resolveOrCreateGroup( groups: Groups, requestedGroupId?: string
return createGroup( groups, requestedGroupId );
}
-export function createGroup( groups: Groups, groupId?: string, label?: string ): ResolvedGroup {
+function createGroup( groups: Groups, groupId?: string, label?: string ): ResolvedGroup {
const newGroupId = groupId || generateUniqueId( 'group' );
const newLabel = label || __( 'Default', 'elementor' );
diff --git a/packages/packages/core/editor-components/src/sync/__tests__/cleanup-overridable-props-on-delete.test.ts b/packages/packages/core/editor-components/src/extended/sync/__tests__/cleanup-overridable-props-on-delete.test.ts
similarity index 74%
rename from packages/packages/core/editor-components/src/sync/__tests__/cleanup-overridable-props-on-delete.test.ts
rename to packages/packages/core/editor-components/src/extended/sync/__tests__/cleanup-overridable-props-on-delete.test.ts
index 0063a5e5be9b..c8725edfaea7 100644
--- a/packages/packages/core/editor-components/src/sync/__tests__/cleanup-overridable-props-on-delete.test.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/__tests__/cleanup-overridable-props-on-delete.test.ts
@@ -1,9 +1,10 @@
-import { createHooksRegistry, createMockElement, setupHooksRegistry } from 'test-utils';
+import { createHooksRegistry, createMockElement, setupHooksRegistry, type WindowWithHooks } from 'test-utils';
import { getAllDescendants, type V1Element } from '@elementor/editor-elements';
-import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
+import { __getState as getState } from '@elementor/store';
-import { SLICE_NAME } from '../../store/store';
-import type { OverridableProps, PublishedComponent } from '../../types';
+import { SLICE_NAME } from '../../../store/store';
+import type { OverridableProps, PublishedComponent } from '../../../types';
+import { deleteOverridableProp } from '../../store/actions/delete-overridable-prop';
import { initCleanupOverridablePropsOnDelete } from '../cleanup-overridable-props-on-delete';
jest.mock( '@elementor/store', () => ( {
@@ -18,6 +19,10 @@ jest.mock( '@elementor/editor-elements', () => ( {
getAllDescendants: jest.fn(),
} ) );
+jest.mock( '../../store/actions/delete-overridable-prop', () => ( {
+ deleteOverridableProp: jest.fn(),
+} ) );
+
describe( 'initCleanupOverridablePropsOnDelete', () => {
const MOCK_COMPONENT_ID = 123;
const ELEMENT_ID_1 = 'element-1';
@@ -127,9 +132,12 @@ describe( 'initCleanupOverridablePropsOnDelete', () => {
},
} );
+ let originalWindow: WindowWithHooks;
+
beforeEach( () => {
jest.clearAllMocks();
setupHooksRegistry( hooksRegistry );
+ originalWindow = { ...( window as unknown as WindowWithHooks ) };
mockState = {
data: [
@@ -148,6 +156,11 @@ describe( 'initCleanupOverridablePropsOnDelete', () => {
} ) );
jest.mocked( getAllDescendants ).mockReturnValue( [] );
+ jest.mocked( deleteOverridableProp ).mockReturnValue( undefined );
+ } );
+
+ afterEach( () => {
+ ( window as unknown as WindowWithHooks ) = originalWindow;
} );
it( 'should register a hook for document/elements/delete command', () => {
@@ -162,7 +175,7 @@ describe( 'initCleanupOverridablePropsOnDelete', () => {
expect( registeredHooks[ 0 ].getCommand() ).toBe( 'document/elements/delete' );
} );
- it( 'should dispatch setOverridableProps with prop removed when element is deleted', () => {
+ it( 'should call deleteOverridableProp with prop removed when element is deleted', () => {
// Arrange
initCleanupOverridablePropsOnDelete();
const registeredHooks = hooksRegistry.getAll();
@@ -176,24 +189,11 @@ describe( 'initCleanupOverridablePropsOnDelete', () => {
hook.apply( { container: deletedElement }, deletedElement );
// Assert
- expect( dispatch ).toHaveBeenCalledWith(
- expect.objectContaining( {
- type: `${ SLICE_NAME }/setOverridableProps`,
- payload: expect.objectContaining( {
- componentId: MOCK_COMPONENT_ID,
- overridableProps: expect.objectContaining( {
- props: {},
- groups: expect.objectContaining( {
- items: expect.objectContaining( {
- [ GROUP_ID ]: expect.objectContaining( {
- props: [],
- } ),
- } ),
- } ),
- } ),
- } ),
- } )
- );
+ expect( deleteOverridableProp ).toHaveBeenCalledWith( {
+ componentId: MOCK_COMPONENT_ID,
+ propKey: [ PROP_KEY_1 ],
+ source: 'system',
+ } );
} );
it( 'should remove multiple props when deleting multiple elements', () => {
@@ -210,15 +210,11 @@ describe( 'initCleanupOverridablePropsOnDelete', () => {
hook.apply( { containers: [ deletedElement1, deletedElement2 ] }, [ deletedElement1, deletedElement2 ] );
// Assert
- expect( dispatch ).toHaveBeenCalledWith(
- expect.objectContaining( {
- payload: expect.objectContaining( {
- overridableProps: expect.objectContaining( {
- props: {},
- } ),
- } ),
- } )
- );
+ expect( deleteOverridableProp ).toHaveBeenCalledWith( {
+ componentId: MOCK_COMPONENT_ID,
+ propKey: [ PROP_KEY_1, PROP_KEY_2 ],
+ source: 'system',
+ } );
} );
it( 'should remove props for parent and descendant elements', () => {
@@ -240,22 +236,11 @@ describe( 'initCleanupOverridablePropsOnDelete', () => {
hook.apply( { container: parentElement }, parentElement );
// Assert
- expect( dispatch ).toHaveBeenCalledWith(
- expect.objectContaining( {
- payload: expect.objectContaining( {
- overridableProps: expect.objectContaining( {
- props: {},
- groups: expect.objectContaining( {
- items: expect.objectContaining( {
- [ GROUP_ID ]: expect.objectContaining( {
- props: [],
- } ),
- } ),
- } ),
- } ),
- } ),
- } )
- );
+ expect( deleteOverridableProp ).toHaveBeenCalledWith( {
+ componentId: MOCK_COMPONENT_ID,
+ propKey: [ PROP_KEY_1, PROP_KEY_CHILD ],
+ source: 'system',
+ } );
} );
it( 'should only remove props for matching elements', () => {
@@ -271,27 +256,14 @@ describe( 'initCleanupOverridablePropsOnDelete', () => {
hook.apply( { container: deletedElement1 }, deletedElement1 );
// Assert
- expect( dispatch ).toHaveBeenCalledWith(
- expect.objectContaining( {
- payload: expect.objectContaining( {
- overridableProps: expect.objectContaining( {
- props: {
- [ PROP_KEY_2 ]: expect.anything(),
- },
- groups: expect.objectContaining( {
- items: expect.objectContaining( {
- [ GROUP_ID ]: expect.objectContaining( {
- props: [ PROP_KEY_2 ],
- } ),
- } ),
- } ),
- } ),
- } ),
- } )
- );
+ expect( deleteOverridableProp ).toHaveBeenCalledWith( {
+ componentId: MOCK_COMPONENT_ID,
+ propKey: [ PROP_KEY_1 ],
+ source: 'system',
+ } );
} );
- it( 'should not dispatch when no matching elements', () => {
+ it( 'should not call deleteOverridableProp when no matching elements', () => {
// Arrange
initCleanupOverridablePropsOnDelete();
const registeredHooks = hooksRegistry.getAll();
@@ -303,10 +275,10 @@ describe( 'initCleanupOverridablePropsOnDelete', () => {
hook.apply( { container: deletedElement }, deletedElement );
// Assert
- expect( dispatch ).not.toHaveBeenCalled();
+ expect( deleteOverridableProp ).not.toHaveBeenCalled();
} );
- it( 'should not dispatch when component has no overridable props', () => {
+ it( 'should not call deleteOverridableProp when component has no overridable props', () => {
// Arrange
mockState.data[ 0 ].overridableProps = { props: {}, groups: { items: {}, order: [] } };
initCleanupOverridablePropsOnDelete();
@@ -319,10 +291,10 @@ describe( 'initCleanupOverridablePropsOnDelete', () => {
hook.apply( { container: deletedElement }, deletedElement );
// Assert
- expect( dispatch ).not.toHaveBeenCalled();
+ expect( deleteOverridableProp ).not.toHaveBeenCalled();
} );
- it( 'should not dispatch when not editing a component', () => {
+ it( 'should not call deleteOverridableProp when not editing a component', () => {
// Arrange
mockState.currentComponentId = null;
initCleanupOverridablePropsOnDelete();
@@ -335,10 +307,10 @@ describe( 'initCleanupOverridablePropsOnDelete', () => {
hook.apply( { container: deletedElement }, deletedElement );
// Assert
- expect( dispatch ).not.toHaveBeenCalled();
+ expect( deleteOverridableProp ).not.toHaveBeenCalled();
} );
- it( 'should not dispatch when container is null', () => {
+ it( 'should not call deleteOverridableProp when container is null', () => {
// Arrange
initCleanupOverridablePropsOnDelete();
const registeredHooks = hooksRegistry.getAll();
@@ -348,6 +320,25 @@ describe( 'initCleanupOverridablePropsOnDelete', () => {
hook.apply( { container: null }, null );
// Assert
- expect( dispatch ).not.toHaveBeenCalled();
+ expect( deleteOverridableProp ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should not call deleteOverridableProp when part of move command', () => {
+ // Arrange
+ initCleanupOverridablePropsOnDelete();
+ const registeredHooks = hooksRegistry.getAll();
+ const hook = registeredHooks[ 0 ];
+
+ ( window as unknown as WindowWithHooks ).$e.commands = {
+ currentTrace: [ 'document/elements/move' ],
+ };
+
+ const deletedElement = createMockElement( { model: { id: ELEMENT_ID_1 } } );
+
+ // Act
+ hook.apply( { container: deletedElement }, { commandsCurrentTrace: [ 'document/elements/move' ] } );
+
+ // Assert
+ expect( deleteOverridableProp ).not.toHaveBeenCalled();
} );
} );
diff --git a/packages/packages/core/editor-components/src/sync/__tests__/create-components-before-save.test.ts b/packages/packages/core/editor-components/src/extended/sync/__tests__/create-components-before-save.test.ts
similarity index 97%
rename from packages/packages/core/editor-components/src/sync/__tests__/create-components-before-save.test.ts
rename to packages/packages/core/editor-components/src/extended/sync/__tests__/create-components-before-save.test.ts
index 8ffb6210feec..7ef6af2f5889 100644
--- a/packages/packages/core/editor-components/src/sync/__tests__/create-components-before-save.test.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/__tests__/create-components-before-save.test.ts
@@ -1,12 +1,12 @@
import { updateElementSettings, type V1ElementData } from '@elementor/editor-elements';
import { __createStore, __dispatch, __getState as getState, __registerSlice } from '@elementor/store';
-import { apiClient } from '../../api';
-import { selectUnpublishedComponents, slice } from '../../store/store';
-import { createComponentsBeforeSave } from '../create-components-before-save';
+import { apiClient } from '../../../api';
+import { selectUnpublishedComponents, slice } from '../../../store/store';
+import { createComponentsBeforeSave } from '../../sync/create-components-before-save';
jest.mock( '@elementor/editor-elements' );
-jest.mock( '../../api' );
+jest.mock( '../../../api' );
const mockUpdateElementSettings = jest.mocked( updateElementSettings );
const mockCreateComponents = jest.mocked( apiClient.create );
diff --git a/packages/packages/core/editor-components/src/sync/__tests__/handle-component-edit-mode-container.test.ts b/packages/packages/core/editor-components/src/extended/sync/__tests__/handle-component-edit-mode-container.test.ts
similarity index 99%
rename from packages/packages/core/editor-components/src/sync/__tests__/handle-component-edit-mode-container.test.ts
rename to packages/packages/core/editor-components/src/extended/sync/__tests__/handle-component-edit-mode-container.test.ts
index 5be2d634a0f3..182a5e9a602d 100644
--- a/packages/packages/core/editor-components/src/sync/__tests__/handle-component-edit-mode-container.test.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/__tests__/handle-component-edit-mode-container.test.ts
@@ -2,7 +2,7 @@ import { createHooksRegistry, createMockElement, setupHooksRegistry } from 'test
import { type V1Document } from '@elementor/editor-documents';
import { createElement, selectElement, type V1Element } from '@elementor/editor-elements';
-import { COMPONENT_DOCUMENT_TYPE } from '../../components/consts';
+import { COMPONENT_DOCUMENT_TYPE } from '../../consts';
import { isEditingComponent } from '../../utils/is-editing-component';
import { type DeleteArgs, initHandleComponentEditModeContainer } from '../handle-component-edit-mode-container';
diff --git a/packages/packages/core/editor-components/src/__tests__/prevent-non-atomic-nesting.test.ts b/packages/packages/core/editor-components/src/extended/sync/__tests__/prevent-non-atomic-nesting.test.ts
similarity index 100%
rename from packages/packages/core/editor-components/src/__tests__/prevent-non-atomic-nesting.test.ts
rename to packages/packages/core/editor-components/src/extended/sync/__tests__/prevent-non-atomic-nesting.test.ts
diff --git a/packages/packages/core/editor-components/src/extended/sync/__tests__/sanitize-overridable-props.test.ts b/packages/packages/core/editor-components/src/extended/sync/__tests__/sanitize-overridable-props.test.ts
new file mode 100644
index 000000000000..26978d25f756
--- /dev/null
+++ b/packages/packages/core/editor-components/src/extended/sync/__tests__/sanitize-overridable-props.test.ts
@@ -0,0 +1,185 @@
+import { renderHookWithStore } from 'test-utils';
+import {
+ __createStore,
+ __dispatch as dispatch,
+ __registerSlice as registerSlice,
+ type SliceState,
+ type Store,
+} from '@elementor/store';
+
+import { slice } from '../../../store/store';
+import { type OverridableProps, type PublishedComponent } from '../../../types';
+import { filterValidOverridableProps } from '../../../utils/filter-valid-overridable-props';
+import { deleteOverridableProp } from '../../store/actions/delete-overridable-prop';
+import { updateComponentSanitizedAttribute } from '../../store/actions/update-component-sanitized-attribute';
+import { SanitizeOverridableProps } from '../sanitize-overridable-props';
+
+jest.mock( '../../../utils/filter-valid-overridable-props', () => ( {
+ filterValidOverridableProps: jest.fn(),
+} ) );
+
+jest.mock( '../../store/actions/delete-overridable-prop', () => ( {
+ deleteOverridableProp: jest.fn(),
+} ) );
+
+jest.mock( '../../store/actions/update-component-sanitized-attribute', () => ( {
+ updateComponentSanitizedAttribute: jest.fn(),
+} ) );
+
+const mockFilterValidOverridableProps = jest.mocked( filterValidOverridableProps );
+const mockDeleteOverridableProp = jest.mocked( deleteOverridableProp );
+const mockUpdateComponentSanitizedAttribute = jest.mocked( updateComponentSanitizedAttribute );
+
+const MOCK_COMPONENT_ID = 123;
+
+function createMockOverridableProps( propKeys: string[] = [ 'prop-1' ] ): OverridableProps {
+ const props: OverridableProps[ 'props' ] = {};
+
+ for ( const key of propKeys ) {
+ props[ key ] = {
+ overrideKey: key,
+ label: `Label ${ key }`,
+ elementId: `element-${ key }`,
+ propKey: 'text',
+ widgetType: 'e-heading',
+ elType: 'widget',
+ groupId: 'content',
+ originValue: { $$type: 'string', value: 'Hello' },
+ };
+ }
+
+ return {
+ props,
+ groups: {
+ items: {
+ content: { id: 'content', label: 'Content', props: propKeys },
+ },
+ order: [ 'content' ],
+ },
+ };
+}
+
+function createMockComponent( id: number, overridableProps?: OverridableProps ): PublishedComponent {
+ return {
+ id,
+ uid: `component-uid-${ id }`,
+ name: `Component ${ id }`,
+ overridableProps,
+ };
+}
+
+function useSanitizeOverridablePropsEffect() {
+ return SanitizeOverridableProps();
+}
+
+describe( 'SanitizeOverridableProps', () => {
+ let store: Store< SliceState< typeof slice > >;
+
+ beforeEach( () => {
+ jest.clearAllMocks();
+ registerSlice( slice );
+ store = __createStore();
+ } );
+
+ it( 'should not run cleanup when no current component', () => {
+ // Act
+ renderHookWithStore( useSanitizeOverridablePropsEffect, store );
+
+ // Assert
+ expect( mockFilterValidOverridableProps ).not.toHaveBeenCalled();
+ expect( mockDeleteOverridableProp ).not.toHaveBeenCalled();
+ expect( mockUpdateComponentSanitizedAttribute ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should not run cleanup when component is already sanitized', () => {
+ // Arrange
+ const mockOverridableProps = createMockOverridableProps();
+ const mockComponent = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
+ dispatch( slice.actions.load( [ mockComponent ] ) );
+ dispatch( slice.actions.setCurrentComponentId( MOCK_COMPONENT_ID ) );
+ dispatch(
+ slice.actions.updateComponentSanitizedAttribute( {
+ componentId: MOCK_COMPONENT_ID,
+ attribute: 'overridableProps',
+ } )
+ );
+
+ // Act
+ renderHookWithStore( useSanitizeOverridablePropsEffect, store );
+
+ // Assert
+ expect( mockFilterValidOverridableProps ).not.toHaveBeenCalled();
+ expect( mockDeleteOverridableProp ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should run cleanup and mark as sanitized when component is not sanitized', () => {
+ // Arrange
+ const mockOverridableProps = createMockOverridableProps();
+ const mockComponent = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
+ dispatch( slice.actions.load( [ mockComponent ] ) );
+ dispatch( slice.actions.setCurrentComponentId( MOCK_COMPONENT_ID ) );
+ mockFilterValidOverridableProps.mockReturnValue( mockOverridableProps );
+
+ // Act
+ renderHookWithStore( useSanitizeOverridablePropsEffect, store );
+
+ // Assert
+ expect( mockFilterValidOverridableProps ).toHaveBeenCalledWith( mockOverridableProps );
+ expect( mockDeleteOverridableProp ).not.toHaveBeenCalled();
+ expect( mockUpdateComponentSanitizedAttribute ).toHaveBeenCalledWith( MOCK_COMPONENT_ID, 'overridableProps' );
+ } );
+
+ it( 'should delete stale props that are not in the filtered result', () => {
+ // Arrange
+ const mockOverridableProps = createMockOverridableProps( [ 'prop-1', 'prop-2', 'prop-3' ] );
+ const filteredProps = createMockOverridableProps( [ 'prop-1' ] );
+ const mockComponent = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
+ dispatch( slice.actions.load( [ mockComponent ] ) );
+ dispatch( slice.actions.setCurrentComponentId( MOCK_COMPONENT_ID ) );
+ mockFilterValidOverridableProps.mockReturnValue( filteredProps );
+
+ // Act
+ renderHookWithStore( useSanitizeOverridablePropsEffect, store );
+
+ // Assert
+ expect( mockDeleteOverridableProp ).toHaveBeenCalledWith( {
+ componentId: MOCK_COMPONENT_ID,
+ propKey: 'prop-2',
+ source: 'system',
+ } );
+ expect( mockDeleteOverridableProp ).toHaveBeenCalledWith( {
+ componentId: MOCK_COMPONENT_ID,
+ propKey: 'prop-3',
+ source: 'system',
+ } );
+ expect( mockDeleteOverridableProp ).toHaveBeenCalledTimes( 2 );
+ expect( mockUpdateComponentSanitizedAttribute ).toHaveBeenCalledWith( MOCK_COMPONENT_ID, 'overridableProps' );
+ } );
+
+ it( 'should re-run cleanup after sanitized state is reset', () => {
+ // Arrange
+ const mockOverridableProps = createMockOverridableProps();
+ const mockComponent = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
+ dispatch( slice.actions.load( [ mockComponent ] ) );
+ dispatch( slice.actions.setCurrentComponentId( MOCK_COMPONENT_ID ) );
+ dispatch(
+ slice.actions.updateComponentSanitizedAttribute( {
+ componentId: MOCK_COMPONENT_ID,
+ attribute: 'overridableProps',
+ } )
+ );
+ mockFilterValidOverridableProps.mockReturnValue( mockOverridableProps );
+
+ // Act
+ const { rerender } = renderHookWithStore( useSanitizeOverridablePropsEffect, store );
+ expect( mockFilterValidOverridableProps ).not.toHaveBeenCalled();
+
+ // Act
+ dispatch( slice.actions.resetSanitizedComponents() );
+ rerender();
+
+ // Assert
+ expect( mockFilterValidOverridableProps ).toHaveBeenCalledTimes( 1 );
+ expect( mockUpdateComponentSanitizedAttribute ).toHaveBeenCalledWith( MOCK_COMPONENT_ID, 'overridableProps' );
+ } );
+} );
diff --git a/packages/packages/core/editor-components/src/sync/__tests__/update-archived-component-before-save.test.ts b/packages/packages/core/editor-components/src/extended/sync/__tests__/update-archived-component-before-save.test.ts
similarity index 91%
rename from packages/packages/core/editor-components/src/sync/__tests__/update-archived-component-before-save.test.ts
rename to packages/packages/core/editor-components/src/extended/sync/__tests__/update-archived-component-before-save.test.ts
index 69b4048d4e04..be8b5b2d22ef 100644
--- a/packages/packages/core/editor-components/src/sync/__tests__/update-archived-component-before-save.test.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/__tests__/update-archived-component-before-save.test.ts
@@ -2,11 +2,11 @@
import { notify } from '@elementor/editor-notifications';
import { __createStore, __dispatch, __registerSlice } from '@elementor/store';
-import { apiClient } from '../../api';
-import { slice } from '../../store/store';
-import { updateArchivedComponentBeforeSave } from '../update-archived-component-before-save';
+import { apiClient } from '../../../api';
+import { slice } from '../../../store/store';
+import { updateArchivedComponentBeforeSave } from '../../sync/update-archived-component-before-save';
-jest.mock( '../../api' );
+jest.mock( '../../../api' );
jest.mock( '@elementor/editor-notifications' );
const mockUpdateArchivedComponents = jest.mocked( apiClient.updateArchivedComponents );
diff --git a/packages/packages/core/editor-components/src/extended/sync/before-save.ts b/packages/packages/core/editor-components/src/extended/sync/before-save.ts
new file mode 100644
index 000000000000..9b5cef2908bc
--- /dev/null
+++ b/packages/packages/core/editor-components/src/extended/sync/before-save.ts
@@ -0,0 +1,52 @@
+import { type V1Document } from '@elementor/editor-documents';
+import { type V1Element, type V1ElementData } from '@elementor/editor-elements';
+
+import { publishDraftComponentsInPageBeforeSave } from '../../sync/publish-draft-components-in-page-before-save';
+import { type DocumentSaveStatus } from '../../types';
+import { setComponentOverridablePropsSettingsBeforeSave } from '../sync/set-component-overridable-props-settings-before-save';
+import { updateArchivedComponentBeforeSave } from '../sync/update-archived-component-before-save';
+import { updateComponentTitleBeforeSave } from '../sync/update-component-title-before-save';
+import { createComponentsBeforeSave } from './create-components-before-save';
+
+type Options = {
+ container: V1Element & {
+ document: V1Document;
+ model: {
+ get: ( key: 'elements' ) => {
+ toJSON: () => V1ElementData[];
+ };
+ };
+ };
+ status: DocumentSaveStatus;
+};
+
+export const beforeSave = ( { container, status }: Options ) => {
+ const elements = container?.model.get( 'elements' ).toJSON?.() ?? [];
+
+ return Promise.all( [
+ syncComponents( { elements, status } ),
+ setComponentOverridablePropsSettingsBeforeSave( { container } ),
+ ] );
+};
+
+// These operations run sequentially to prevent race conditions when multiple
+// edits occur on the same component simultaneously.
+// TODO: Consolidate these into a single PUT /components endpoint.
+const syncComponents = async ( { elements, status }: { elements: V1ElementData[]; status: DocumentSaveStatus } ) => {
+ // This order is important - first update existing components, then create new components,
+ // Since new component validation depends on the existing components (preventing duplicate names).
+ await updateExistingComponentsBeforeSave( { elements, status } );
+ await createComponentsBeforeSave( { elements, status } );
+};
+
+const updateExistingComponentsBeforeSave = async ( {
+ elements,
+ status,
+}: {
+ elements: V1ElementData[];
+ status: DocumentSaveStatus;
+} ) => {
+ await updateComponentTitleBeforeSave( status );
+ await updateArchivedComponentBeforeSave( status );
+ await publishDraftComponentsInPageBeforeSave( { elements, status } );
+};
diff --git a/packages/packages/core/editor-components/src/sync/cleanup-overridable-props-on-delete.ts b/packages/packages/core/editor-components/src/extended/sync/cleanup-overridable-props-on-delete.ts
similarity index 56%
rename from packages/packages/core/editor-components/src/sync/cleanup-overridable-props-on-delete.ts
rename to packages/packages/core/editor-components/src/extended/sync/cleanup-overridable-props-on-delete.ts
index 1a728f35af0e..82e413f759e5 100644
--- a/packages/packages/core/editor-components/src/sync/cleanup-overridable-props-on-delete.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/cleanup-overridable-props-on-delete.ts
@@ -1,9 +1,9 @@
import { getAllDescendants, type V1Element } from '@elementor/editor-elements';
-import { registerDataHook } from '@elementor/editor-v1-adapters';
-import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
+import { type HookOptions, registerDataHook } from '@elementor/editor-v1-adapters';
+import { __getState as getState } from '@elementor/store';
-import { type ComponentsSlice, selectCurrentComponentId, selectOverridableProps, slice } from '../store/store';
-import { removePropFromAllGroups } from '../store/utils/groups-transformers';
+import { type ComponentsSlice, selectCurrentComponentId, selectOverridableProps } from '../../store/store';
+import { deleteOverridableProp } from '../store/actions/delete-overridable-prop';
type DeleteCommandArgs = {
container?: V1Element;
@@ -11,7 +11,14 @@ type DeleteCommandArgs = {
};
export function initCleanupOverridablePropsOnDelete() {
- registerDataHook( 'dependency', 'document/elements/delete', ( args: DeleteCommandArgs ) => {
+ // This hook is not a real dependency - it doesn't block the execution of the command in any case, only perform side effect.
+ // We use `dependency` and not `after` hook because the `after` hook doesn't include the children of a deleted container
+ // in the callback parameters (as they already were deleted).
+ registerDataHook( 'dependency', 'document/elements/delete', ( args: DeleteCommandArgs, options?: HookOptions ) => {
+ if ( isPartOfMoveCommand( options ) ) {
+ return true;
+ }
+
const state = getState() as ComponentsSlice | undefined;
if ( ! state ) {
@@ -50,25 +57,7 @@ export function initCleanupOverridablePropsOnDelete() {
return true;
}
- const remainingProps = Object.fromEntries(
- Object.entries( overridableProps.props ).filter( ( [ propKey ] ) => ! propKeysToDelete.includes( propKey ) )
- );
-
- let updatedGroups = overridableProps.groups;
- for ( const propKey of propKeysToDelete ) {
- updatedGroups = removePropFromAllGroups( updatedGroups, propKey );
- }
-
- dispatch(
- slice.actions.setOverridableProps( {
- componentId: currentComponentId,
- overridableProps: {
- ...overridableProps,
- props: remainingProps,
- groups: updatedGroups,
- },
- } )
- );
+ deleteOverridableProp( { componentId: currentComponentId, propKey: propKeysToDelete, source: 'system' } );
return true;
} );
@@ -83,3 +72,14 @@ function collectDeletedElementIds( containers: V1Element[] ): string[] {
return elementIds;
}
+
+function isPartOfMoveCommand( options?: HookOptions ): boolean {
+ // Skip cleanup if this delete is part of a move command
+ // Move = delete + create, and we don't want to delete the overridable prop in this case.
+ // See assets/dev/js/editor/document/elements/commands/move.js
+ const isMoveCommandInTrace =
+ options?.commandsCurrentTrace?.includes( 'document/elements/move' ) ||
+ options?.commandsCurrentTrace?.includes( 'document/repeater/move' );
+
+ return Boolean( isMoveCommandInTrace );
+}
diff --git a/packages/packages/core/editor-components/src/sync/create-components-before-save.ts b/packages/packages/core/editor-components/src/extended/sync/create-components-before-save.ts
similarity index 93%
rename from packages/packages/core/editor-components/src/sync/create-components-before-save.ts
rename to packages/packages/core/editor-components/src/extended/sync/create-components-before-save.ts
index 97973c0f1809..50a46c688538 100644
--- a/packages/packages/core/editor-components/src/sync/create-components-before-save.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/create-components-before-save.ts
@@ -1,10 +1,10 @@
import { updateElementSettings, type V1ElementData } from '@elementor/editor-elements';
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
-import { apiClient } from '../api';
-import { type ComponentInstanceProp } from '../prop-types/component-instance-prop-type';
-import { selectUnpublishedComponents, slice } from '../store/store';
-import { type DocumentSaveStatus, type UnpublishedComponent } from '../types';
+import { apiClient } from '../../api';
+import { type ComponentInstanceProp } from '../../prop-types/component-instance-prop-type';
+import { selectUnpublishedComponents, slice } from '../../store/store';
+import { type DocumentSaveStatus, type UnpublishedComponent } from '../../types';
export async function createComponentsBeforeSave( {
elements,
diff --git a/packages/packages/core/editor-components/src/sync/handle-component-edit-mode-container.ts b/packages/packages/core/editor-components/src/extended/sync/handle-component-edit-mode-container.ts
similarity index 97%
rename from packages/packages/core/editor-components/src/sync/handle-component-edit-mode-container.ts
rename to packages/packages/core/editor-components/src/extended/sync/handle-component-edit-mode-container.ts
index cda621219a25..8dc47300f5a5 100644
--- a/packages/packages/core/editor-components/src/sync/handle-component-edit-mode-container.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/handle-component-edit-mode-container.ts
@@ -2,7 +2,7 @@ import { type V1Document } from '@elementor/editor-documents';
import { createElement, selectElement, type V1Element } from '@elementor/editor-elements';
import { registerDataHook } from '@elementor/editor-v1-adapters';
-import { COMPONENT_DOCUMENT_TYPE } from '../components/consts';
+import { COMPONENT_DOCUMENT_TYPE } from '../consts';
import { isEditingComponent } from '../utils/is-editing-component';
const V4_DEFAULT_CONTAINER_TYPE = 'e-flexbox';
diff --git a/packages/packages/core/editor-components/src/prevent-non-atomic-nesting.ts b/packages/packages/core/editor-components/src/extended/sync/prevent-non-atomic-nesting.ts
similarity index 97%
rename from packages/packages/core/editor-components/src/prevent-non-atomic-nesting.ts
rename to packages/packages/core/editor-components/src/extended/sync/prevent-non-atomic-nesting.ts
index e16b8c4b0d09..d65a50187da3 100644
--- a/packages/packages/core/editor-components/src/prevent-non-atomic-nesting.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/prevent-non-atomic-nesting.ts
@@ -4,8 +4,8 @@ import { type NotificationData, notify } from '@elementor/editor-notifications';
import { blockCommand } from '@elementor/editor-v1-adapters';
import { __ } from '@wordpress/i18n';
-import { type ExtendedWindow } from './types';
-import { isEditingComponent } from './utils/is-editing-component';
+import { type ExtendedWindow } from '../../types';
+import { isEditingComponent } from '../utils/is-editing-component';
type CreateArgs = {
container?: V1Element;
diff --git a/packages/packages/core/editor-components/src/sync/revert-overridables-on-copy-or-duplicate.ts b/packages/packages/core/editor-components/src/extended/sync/revert-overridables-on-copy-or-duplicate.ts
similarity index 97%
rename from packages/packages/core/editor-components/src/sync/revert-overridables-on-copy-or-duplicate.ts
rename to packages/packages/core/editor-components/src/extended/sync/revert-overridables-on-copy-or-duplicate.ts
index f6b63bc7411d..d0db42c79bab 100644
--- a/packages/packages/core/editor-components/src/sync/revert-overridables-on-copy-or-duplicate.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/revert-overridables-on-copy-or-duplicate.ts
@@ -1,7 +1,7 @@
import { type V1Element, type V1ElementData } from '@elementor/editor-elements';
import { registerDataHook } from '@elementor/editor-v1-adapters';
-import { type ExtendedWindow } from '../types';
+import { type ExtendedWindow } from '../../types';
import { isEditingComponent } from '../utils/is-editing-component';
import {
revertAllOverridablesInContainer,
diff --git a/packages/packages/core/editor-components/src/extended/sync/sanitize-overridable-props.ts b/packages/packages/core/editor-components/src/extended/sync/sanitize-overridable-props.ts
new file mode 100644
index 000000000000..3249e487f6a0
--- /dev/null
+++ b/packages/packages/core/editor-components/src/extended/sync/sanitize-overridable-props.ts
@@ -0,0 +1,32 @@
+import { useEffect } from 'react';
+
+import { useCurrentComponentId, useIsSanitizedComponent, useOverridableProps } from '../../store/store';
+import { filterValidOverridableProps } from '../../utils/filter-valid-overridable-props';
+import { deleteOverridableProp } from '../store/actions/delete-overridable-prop';
+import { updateComponentSanitizedAttribute } from '../store/actions/update-component-sanitized-attribute';
+
+export function SanitizeOverridableProps() {
+ const currentComponentId = useCurrentComponentId();
+ const overridableProps = useOverridableProps( currentComponentId );
+ const isSanitized = useIsSanitizedComponent( currentComponentId, 'overridableProps' );
+
+ useEffect( () => {
+ if ( isSanitized || ! overridableProps || ! currentComponentId ) {
+ return;
+ }
+
+ const filtered = filterValidOverridableProps( overridableProps );
+
+ const propsToDelete = Object.keys( overridableProps.props ?? {} ).filter( ( key ) => ! filtered.props[ key ] );
+
+ if ( propsToDelete.length > 0 ) {
+ propsToDelete.forEach( ( key ) => {
+ deleteOverridableProp( { componentId: currentComponentId, propKey: key, source: 'system' } );
+ } );
+ }
+
+ updateComponentSanitizedAttribute( currentComponentId, 'overridableProps' );
+ }, [ currentComponentId, isSanitized, overridableProps ] );
+
+ return null;
+}
diff --git a/packages/packages/core/editor-components/src/sync/set-component-overridable-props-settings-before-save.ts b/packages/packages/core/editor-components/src/extended/sync/set-component-overridable-props-settings-before-save.ts
similarity index 84%
rename from packages/packages/core/editor-components/src/sync/set-component-overridable-props-settings-before-save.ts
rename to packages/packages/core/editor-components/src/extended/sync/set-component-overridable-props-settings-before-save.ts
index dfb4bbfacfe0..bada11938e27 100644
--- a/packages/packages/core/editor-components/src/sync/set-component-overridable-props-settings-before-save.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/set-component-overridable-props-settings-before-save.ts
@@ -2,8 +2,8 @@ import { type V1Document } from '@elementor/editor-documents';
import { type V1Element } from '@elementor/editor-elements';
import { __getState as getState } from '@elementor/store';
-import { COMPONENT_DOCUMENT_TYPE } from '../components/consts';
-import { selectOverridableProps } from '../store/store';
+import { selectOverridableProps } from '../../store/store';
+import { COMPONENT_DOCUMENT_TYPE } from '../consts';
export const setComponentOverridablePropsSettingsBeforeSave = ( {
container,
diff --git a/packages/packages/core/editor-components/src/sync/update-archived-component-before-save.ts b/packages/packages/core/editor-components/src/extended/sync/update-archived-component-before-save.ts
similarity index 84%
rename from packages/packages/core/editor-components/src/sync/update-archived-component-before-save.ts
rename to packages/packages/core/editor-components/src/extended/sync/update-archived-component-before-save.ts
index 702a4930fd48..04adefa5e06a 100644
--- a/packages/packages/core/editor-components/src/sync/update-archived-component-before-save.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/update-archived-component-before-save.ts
@@ -1,9 +1,9 @@
import { type NotificationData, notify } from '@elementor/editor-notifications';
import { __getState as getState } from '@elementor/store';
-import { apiClient } from '../api';
-import { selectArchivedThisSession } from '../store/store';
-import { type DocumentSaveStatus } from '../types';
+import { apiClient } from '../../api';
+import { selectArchivedThisSession } from '../../store/store';
+import { type DocumentSaveStatus } from '../../types';
const failedNotification = ( message: string ): NotificationData => ( {
type: 'error',
diff --git a/packages/packages/core/editor-components/src/sync/update-component-title-before-save.ts b/packages/packages/core/editor-components/src/extended/sync/update-component-title-before-save.ts
similarity index 74%
rename from packages/packages/core/editor-components/src/sync/update-component-title-before-save.ts
rename to packages/packages/core/editor-components/src/extended/sync/update-component-title-before-save.ts
index 860e01bf72ff..899aef1b7db9 100644
--- a/packages/packages/core/editor-components/src/sync/update-component-title-before-save.ts
+++ b/packages/packages/core/editor-components/src/extended/sync/update-component-title-before-save.ts
@@ -1,8 +1,8 @@
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
-import { apiClient } from '../api';
-import { selectUpdatedComponentNames, slice } from '../store/store';
-import { type DocumentSaveStatus } from '../types';
+import { apiClient } from '../../api';
+import { selectUpdatedComponentNames, slice } from '../../store/store';
+import { type DocumentSaveStatus } from '../../types';
export const updateComponentTitleBeforeSave = async ( status: DocumentSaveStatus ) => {
const updatedComponentNames = selectUpdatedComponentNames( getState() );
diff --git a/packages/packages/core/editor-components/src/utils/__tests__/is-editing-component.test.ts b/packages/packages/core/editor-components/src/extended/utils/__tests__/is-editing-component.test.ts
similarity index 96%
rename from packages/packages/core/editor-components/src/utils/__tests__/is-editing-component.test.ts
rename to packages/packages/core/editor-components/src/extended/utils/__tests__/is-editing-component.test.ts
index b742784b1d30..53a89d758623 100644
--- a/packages/packages/core/editor-components/src/utils/__tests__/is-editing-component.test.ts
+++ b/packages/packages/core/editor-components/src/extended/utils/__tests__/is-editing-component.test.ts
@@ -1,6 +1,6 @@
import { __getStore as getStore } from '@elementor/store';
-import { SLICE_NAME } from '../../store/store';
+import { SLICE_NAME } from '../../../store/store';
import { isEditingComponent } from '../is-editing-component';
jest.mock( '@elementor/store', () => ( {
diff --git a/packages/packages/core/editor-components/src/utils/__tests__/revert-all-overridables.test.ts b/packages/packages/core/editor-components/src/extended/utils/__tests__/revert-all-overridables.test.ts
similarity index 95%
rename from packages/packages/core/editor-components/src/utils/__tests__/revert-all-overridables.test.ts
rename to packages/packages/core/editor-components/src/extended/utils/__tests__/revert-all-overridables.test.ts
index e1a6b7f42ca3..6a045c96b1b4 100644
--- a/packages/packages/core/editor-components/src/utils/__tests__/revert-all-overridables.test.ts
+++ b/packages/packages/core/editor-components/src/extended/utils/__tests__/revert-all-overridables.test.ts
@@ -2,10 +2,10 @@ import { createMockElement, createMockElementData } from 'test-utils';
import { getAllDescendants, getContainer, updateElementSettings, type V1Element } from '@elementor/editor-elements';
import { numberPropTypeUtil } from '@elementor/editor-props';
-import { componentInstanceOverridePropTypeUtil } from '../../prop-types/component-instance-override-prop-type';
-import { componentInstanceOverridesPropTypeUtil } from '../../prop-types/component-instance-overrides-prop-type';
-import { componentInstancePropTypeUtil } from '../../prop-types/component-instance-prop-type';
-import { componentOverridablePropTypeUtil } from '../../prop-types/component-overridable-prop-type';
+import { componentInstanceOverridePropTypeUtil } from '../../../prop-types/component-instance-override-prop-type';
+import { componentInstanceOverridesPropTypeUtil } from '../../../prop-types/component-instance-overrides-prop-type';
+import { componentInstancePropTypeUtil } from '../../../prop-types/component-instance-prop-type';
+import { componentOverridablePropTypeUtil } from '../../../prop-types/component-overridable-prop-type';
import { revertAllOverridablesInContainer, revertAllOverridablesInElementData } from '../revert-overridable-settings';
jest.mock( '@elementor/editor-elements', () => ( {
diff --git a/packages/packages/core/editor-components/src/utils/__tests__/revert-element-overridable-setting.test.ts b/packages/packages/core/editor-components/src/extended/utils/__tests__/revert-element-overridable-setting.test.ts
similarity index 94%
rename from packages/packages/core/editor-components/src/utils/__tests__/revert-element-overridable-setting.test.ts
rename to packages/packages/core/editor-components/src/extended/utils/__tests__/revert-element-overridable-setting.test.ts
index bd28aaa88b64..1e6f9c015f32 100644
--- a/packages/packages/core/editor-components/src/utils/__tests__/revert-element-overridable-setting.test.ts
+++ b/packages/packages/core/editor-components/src/extended/utils/__tests__/revert-element-overridable-setting.test.ts
@@ -2,10 +2,10 @@ import { createMockElement } from 'test-utils';
import { getContainer, getElementSetting, updateElementSettings } from '@elementor/editor-elements';
import { numberPropTypeUtil } from '@elementor/editor-props';
-import { componentInstanceOverridePropTypeUtil } from '../../prop-types/component-instance-override-prop-type';
-import { componentInstanceOverridesPropTypeUtil } from '../../prop-types/component-instance-overrides-prop-type';
-import { componentInstancePropTypeUtil } from '../../prop-types/component-instance-prop-type';
-import { componentOverridablePropTypeUtil } from '../../prop-types/component-overridable-prop-type';
+import { componentInstanceOverridePropTypeUtil } from '../../../prop-types/component-instance-override-prop-type';
+import { componentInstanceOverridesPropTypeUtil } from '../../../prop-types/component-instance-overrides-prop-type';
+import { componentInstancePropTypeUtil } from '../../../prop-types/component-instance-prop-type';
+import { componentOverridablePropTypeUtil } from '../../../prop-types/component-overridable-prop-type';
import { revertElementOverridableSetting } from '../revert-overridable-settings';
jest.mock( '@elementor/editor-elements', () => ( {
diff --git a/packages/packages/core/editor-components/src/utils/is-editing-component.ts b/packages/packages/core/editor-components/src/extended/utils/is-editing-component.ts
similarity index 94%
rename from packages/packages/core/editor-components/src/utils/is-editing-component.ts
rename to packages/packages/core/editor-components/src/extended/utils/is-editing-component.ts
index a68b651909d9..26911fa33624 100644
--- a/packages/packages/core/editor-components/src/utils/is-editing-component.ts
+++ b/packages/packages/core/editor-components/src/extended/utils/is-editing-component.ts
@@ -1,6 +1,6 @@
import { __getStore as getStore } from '@elementor/store';
-import { type ComponentsSlice, selectCurrentComponentId } from '../store/store';
+import { type ComponentsSlice, selectCurrentComponentId } from '../../store/store';
export function isEditingComponent(): boolean {
const state = getStore()?.getState() as ComponentsSlice | undefined;
diff --git a/packages/packages/core/editor-components/src/extended/utils/replace-element-with-component.ts b/packages/packages/core/editor-components/src/extended/utils/replace-element-with-component.ts
new file mode 100644
index 000000000000..66d8f6c78f6e
--- /dev/null
+++ b/packages/packages/core/editor-components/src/extended/utils/replace-element-with-component.ts
@@ -0,0 +1,11 @@
+import { replaceElement, type V1ElementData } from '@elementor/editor-elements';
+
+import { type ComponentInstanceParams, createComponentModel } from '../../utils/create-component-model';
+
+export const replaceElementWithComponent = async ( element: V1ElementData, component: ComponentInstanceParams ) => {
+ return await replaceElement( {
+ currentElement: element,
+ newElement: createComponentModel( component ),
+ withHistory: false,
+ } );
+};
diff --git a/packages/packages/core/editor-components/src/utils/revert-overridable-settings.ts b/packages/packages/core/editor-components/src/extended/utils/revert-overridable-settings.ts
similarity index 93%
rename from packages/packages/core/editor-components/src/utils/revert-overridable-settings.ts
rename to packages/packages/core/editor-components/src/extended/utils/revert-overridable-settings.ts
index a9840f8cb5d2..1bef676c3afb 100644
--- a/packages/packages/core/editor-components/src/utils/revert-overridable-settings.ts
+++ b/packages/packages/core/editor-components/src/extended/utils/revert-overridable-settings.ts
@@ -8,22 +8,22 @@ import {
type V1ElementSettingsProps,
} from '@elementor/editor-elements';
-import { COMPONENT_WIDGET_TYPE } from '../create-component-type';
+import { COMPONENT_WIDGET_TYPE } from '../../create-component-type';
import {
type ComponentInstanceOverrideProp,
componentInstanceOverridePropTypeUtil,
-} from '../prop-types/component-instance-override-prop-type';
+} from '../../prop-types/component-instance-override-prop-type';
import {
componentInstanceOverridesPropTypeUtil,
type ComponentInstanceOverridesPropValue,
-} from '../prop-types/component-instance-overrides-prop-type';
+} from '../../prop-types/component-instance-overrides-prop-type';
import {
type ComponentInstanceProp,
componentInstancePropTypeUtil,
type ComponentInstancePropValue,
-} from '../prop-types/component-instance-prop-type';
-import { componentOverridablePropTypeUtil } from '../prop-types/component-overridable-prop-type';
-import { isComponentInstance } from './is-component-instance';
+} from '../../prop-types/component-instance-prop-type';
+import { componentOverridablePropTypeUtil } from '../../prop-types/component-overridable-prop-type';
+import { isComponentInstance } from '../../utils/is-component-instance';
type RevertSettingsResult = {
hasChanges: boolean;
diff --git a/packages/packages/core/editor-components/src/hooks/__tests__/use-sanitize-overridable-props.test.ts b/packages/packages/core/editor-components/src/hooks/__tests__/use-sanitize-overridable-props.test.ts
index 08f1d7f96373..0effb8e13843 100644
--- a/packages/packages/core/editor-components/src/hooks/__tests__/use-sanitize-overridable-props.test.ts
+++ b/packages/packages/core/editor-components/src/hooks/__tests__/use-sanitize-overridable-props.test.ts
@@ -63,36 +63,55 @@ describe( 'useSanitizeOverridableProps', () => {
dispatch( slice.actions.resetSanitizedComponents() );
} );
- describe( 'caching mechanism', () => {
- it( 'should run filtering logic only once per component', () => {
+ describe( 'filtering', () => {
+ it( 'should filter overridable props when component is not sanitized', () => {
// Arrange
const mockOverridableProps = createMockOverridableProps();
- const mockComponent1 = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
- const mockComponent2 = createMockComponent( MOCK_SECOND_COMPONENT_ID, mockOverridableProps );
- dispatch( slice.actions.load( [ mockComponent1, mockComponent2 ] ) );
+ const mockComponent = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
+ dispatch( slice.actions.load( [ mockComponent ] ) );
mockFilterValidOverridableProps.mockReturnValue( mockOverridableProps );
// Act
- renderHookWithStore( () => useSanitizeOverridableProps( MOCK_COMPONENT_ID ), store );
+ const { result } = renderHookWithStore( () => useSanitizeOverridableProps( MOCK_COMPONENT_ID ), store );
// Assert
- expect( mockFilterValidOverridableProps ).toHaveBeenCalledTimes( 1 );
+ expect( mockFilterValidOverridableProps ).toHaveBeenCalledWith( mockOverridableProps, undefined );
+ expect( result.current ).toBe( mockOverridableProps );
+ } );
+
+ it( 'should pass instanceElementId to filter function', () => {
+ // Arrange
+ const mockOverridableProps = createMockOverridableProps();
+ const mockComponent = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
+ dispatch( slice.actions.load( [ mockComponent ] ) );
+ mockFilterValidOverridableProps.mockReturnValue( mockOverridableProps );
// Act
- renderHookWithStore( () => useSanitizeOverridableProps( MOCK_SECOND_COMPONENT_ID ), store );
+ renderHookWithStore( () => useSanitizeOverridableProps( MOCK_COMPONENT_ID, 'instance-element-1' ), store );
// Assert
- expect( mockFilterValidOverridableProps ).toHaveBeenCalledTimes( 2 );
+ expect( mockFilterValidOverridableProps ).toHaveBeenCalledWith(
+ mockOverridableProps,
+ 'instance-element-1'
+ );
+ } );
+
+ it( 'should filter on every render when not sanitized', () => {
+ // Arrange
+ const mockOverridableProps = createMockOverridableProps();
+ const mockComponent = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
+ dispatch( slice.actions.load( [ mockComponent ] ) );
+ mockFilterValidOverridableProps.mockReturnValue( mockOverridableProps );
// Act
renderHookWithStore( () => useSanitizeOverridableProps( MOCK_COMPONENT_ID ), store );
- renderHookWithStore( () => useSanitizeOverridableProps( MOCK_SECOND_COMPONENT_ID ), store );
+ renderHookWithStore( () => useSanitizeOverridableProps( MOCK_COMPONENT_ID ), store );
// Assert
expect( mockFilterValidOverridableProps ).toHaveBeenCalledTimes( 2 );
} );
- it( 'should re-run filtering logic if sanitized components are reset', () => {
+ it( 'should filter independently per component', () => {
// Arrange
const mockOverridableProps = createMockOverridableProps();
const mockComponent1 = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
@@ -106,17 +125,83 @@ describe( 'useSanitizeOverridableProps', () => {
// Assert
expect( mockFilterValidOverridableProps ).toHaveBeenCalledTimes( 2 );
+ } );
+ } );
+
+ describe( 'caching via isSanitized', () => {
+ it( 'should skip filtering when component is marked as sanitized', () => {
+ // Arrange
+ const mockOverridableProps = createMockOverridableProps();
+ const mockComponent = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
+ dispatch( slice.actions.load( [ mockComponent ] ) );
+ dispatch(
+ slice.actions.updateComponentSanitizedAttribute( {
+ componentId: MOCK_COMPONENT_ID,
+ attribute: 'overridableProps',
+ } )
+ );
// Act
- dispatch( slice.actions.resetSanitizedComponents() );
+ const { result } = renderHookWithStore( () => useSanitizeOverridableProps( MOCK_COMPONENT_ID ), store );
+
+ // Assert
+ expect( mockFilterValidOverridableProps ).not.toHaveBeenCalled();
+ expect( result.current ).toBe( mockOverridableProps );
+ } );
+
+ it( 'should track sanitized state per component', () => {
+ // Arrange
+ const mockOverridableProps = createMockOverridableProps();
+ const mockComponent1 = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
+ const mockComponent2 = createMockComponent( MOCK_SECOND_COMPONENT_ID, mockOverridableProps );
+ dispatch( slice.actions.load( [ mockComponent1, mockComponent2 ] ) );
+ dispatch(
+ slice.actions.updateComponentSanitizedAttribute( {
+ componentId: MOCK_COMPONENT_ID,
+ attribute: 'overridableProps',
+ } )
+ );
+ mockFilterValidOverridableProps.mockReturnValue( mockOverridableProps );
+ // Act
renderHookWithStore( () => useSanitizeOverridableProps( MOCK_COMPONENT_ID ), store );
- renderHookWithStore( () => useSanitizeOverridableProps( MOCK_COMPONENT_ID ), store ); // second invocation should be skipped
+
+ // Assert - component 1 is sanitized, should skip filtering
+ expect( mockFilterValidOverridableProps ).not.toHaveBeenCalled();
+
+ // Act - component 2 is NOT sanitized, should filter
renderHookWithStore( () => useSanitizeOverridableProps( MOCK_SECOND_COMPONENT_ID ), store );
- renderHookWithStore( () => useSanitizeOverridableProps( MOCK_SECOND_COMPONENT_ID ), store ); // second invocation should be skipped
// Assert
- expect( mockFilterValidOverridableProps ).toHaveBeenCalledTimes( 4 );
+ expect( mockFilterValidOverridableProps ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ it( 'should re-run filtering after sanitized state is reset', () => {
+ // Arrange
+ const mockOverridableProps = createMockOverridableProps();
+ const mockComponent = createMockComponent( MOCK_COMPONENT_ID, mockOverridableProps );
+ dispatch( slice.actions.load( [ mockComponent ] ) );
+ dispatch(
+ slice.actions.updateComponentSanitizedAttribute( {
+ componentId: MOCK_COMPONENT_ID,
+ attribute: 'overridableProps',
+ } )
+ );
+ mockFilterValidOverridableProps.mockReturnValue( mockOverridableProps );
+
+ // Act
+ const { unmount } = renderHookWithStore( () => useSanitizeOverridableProps( MOCK_COMPONENT_ID ), store );
+
+ // Assert
+ expect( mockFilterValidOverridableProps ).not.toHaveBeenCalled();
+
+ // Act
+ unmount();
+ dispatch( slice.actions.resetSanitizedComponents() );
+ renderHookWithStore( () => useSanitizeOverridableProps( MOCK_COMPONENT_ID ), store );
+
+ // Assert
+ expect( mockFilterValidOverridableProps ).toHaveBeenCalledTimes( 1 );
} );
} );
diff --git a/packages/packages/core/editor-components/src/hooks/use-component-instance-settings.ts b/packages/packages/core/editor-components/src/hooks/use-component-instance-settings.ts
deleted file mode 100644
index 104aa5cdf4fb..000000000000
--- a/packages/packages/core/editor-components/src/hooks/use-component-instance-settings.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { useElement } from '@elementor/editor-editing-panel';
-import { useElementSetting } from '@elementor/editor-elements';
-
-import {
- componentInstancePropTypeUtil,
- type ComponentInstancePropValue,
-} from '../prop-types/component-instance-prop-type';
-
-export function useComponentInstanceSettings() {
- const { element } = useElement();
-
- const settings = useElementSetting< ComponentInstancePropValue >( element.id, 'component_instance' );
-
- return componentInstancePropTypeUtil.extract( settings );
-}
diff --git a/packages/packages/core/editor-components/src/hooks/use-sanitize-overridable-props.ts b/packages/packages/core/editor-components/src/hooks/use-sanitize-overridable-props.ts
index 4ceb5d987c03..f8697cf07fe5 100644
--- a/packages/packages/core/editor-components/src/hooks/use-sanitize-overridable-props.ts
+++ b/packages/packages/core/editor-components/src/hooks/use-sanitize-overridable-props.ts
@@ -1,14 +1,12 @@
-import { deleteOverridableProp } from '../store/actions/delete-overridable-prop';
-import { updateComponentSanitizedAttribute } from '../store/actions/update-component-sanitized-attribute';
import { useIsSanitizedComponent, useOverridableProps } from '../store/store';
import { type ComponentId, type OverridableProps } from '../types';
import { filterValidOverridableProps } from '../utils/filter-valid-overridable-props';
export function useSanitizeOverridableProps(
componentId: ComponentId | null,
+ instanceElementId?: string
// instanceElementId is used to find the component inner elements,
// and should be passed when editing component instance (not in component edit mode)
- instanceElementId?: string
): OverridableProps | undefined {
const overridableProps = useOverridableProps( componentId );
const isSanitized = useIsSanitizedComponent( componentId, 'overridableProps' );
@@ -21,16 +19,5 @@ export function useSanitizeOverridableProps(
return overridableProps;
}
- const filteredOverridableProps = filterValidOverridableProps( overridableProps, instanceElementId );
-
- const originalPropsArray = Object.entries( overridableProps.props ?? {} );
- const propsToDelete = originalPropsArray.filter( ( [ key ] ) => ! filteredOverridableProps.props[ key ] );
-
- propsToDelete.forEach( ( [ key ] ) => {
- deleteOverridableProp( { componentId, propKey: key, source: 'system' } );
- } );
-
- updateComponentSanitizedAttribute( componentId, 'overridableProps' );
-
- return filteredOverridableProps;
+ return filterValidOverridableProps( overridableProps, instanceElementId );
}
diff --git a/packages/packages/core/editor-components/src/init.ts b/packages/packages/core/editor-components/src/init.ts
index 8719aa62130a..c7176eadde85 100644
--- a/packages/packages/core/editor-components/src/init.ts
+++ b/packages/packages/core/editor-components/src/init.ts
@@ -1,20 +1,13 @@
-import { injectIntoLogic, injectIntoTop } from '@elementor/editor';
+import { injectIntoLogic } from '@elementor/editor';
import {
type CreateTemplatedElementTypeOptions,
registerElementType,
settingsTransformersRegistry,
} from '@elementor/editor-canvas';
-import { registerControlReplacement } from '@elementor/editor-controls';
import { getV1CurrentDocument } from '@elementor/editor-documents';
-import {
- FIELD_TYPE,
- injectIntoPanelHeaderTop,
- registerEditingPanelReplacement,
- registerFieldIndicator,
-} from '@elementor/editor-editing-panel';
+import { registerEditingPanelReplacement } from '@elementor/editor-editing-panel';
import { type V1ElementData } from '@elementor/editor-elements';
import { injectTab } from '@elementor/editor-elements-panel';
-import { __registerPanel as registerPanel } from '@elementor/editor-panels';
import { stylesRepository } from '@elementor/editor-styles-repository';
import { registerDataHook } from '@elementor/editor-v1-adapters';
import { __registerSlice as registerSlice } from '@elementor/store';
@@ -23,55 +16,31 @@ import { __ } from '@wordpress/i18n';
import { componentInstanceTransformer } from './component-instance-transformer';
import { componentOverridableTransformer } from './component-overridable-transformer';
import { componentOverrideTransformer } from './component-override-transformer';
-import { ComponentPanelHeader } from './components/component-panel-header/component-panel-header';
-import { panel as componentPropertiesPanel } from './components/component-properties-panel/component-properties-panel';
import { Components } from './components/components-tab/components';
-import { COMPONENT_DOCUMENT_TYPE, OVERRIDABLE_PROP_REPLACEMENT_ID } from './components/consts';
-import { CreateComponentForm } from './components/create-component-form/create-component-form';
-import { EditComponent } from './components/edit-component/edit-component';
import { openEditModeDialog } from './components/in-edit-mode';
import { InstanceEditingPanel } from './components/instance-editing-panel/instance-editing-panel';
import { LoadTemplateComponents } from './components/load-template-components';
-import { OverridablePropControl } from './components/overridable-props/overridable-prop-control';
-import { OverridablePropIndicator } from './components/overridable-props/overridable-prop-indicator';
import { COMPONENT_WIDGET_TYPE, createComponentType } from './create-component-type';
-import { initMcp } from './mcp';
+import { initExtended } from './extended/init';
import { PopulateStore } from './populate-store';
import { initCircularNestingPrevention } from './prevent-circular-nesting';
-import { initNonAtomicNestingPrevention } from './prevent-non-atomic-nesting';
-import { componentOverridablePropTypeUtil } from './prop-types/component-overridable-prop-type';
import { loadComponentsAssets } from './store/actions/load-components-assets';
import { removeComponentStyles } from './store/actions/remove-component-styles';
import { componentsStylesProvider } from './store/components-styles-provider';
import { slice } from './store/store';
import { beforeSave } from './sync/before-save';
-import { initCleanupOverridablePropsOnDelete } from './sync/cleanup-overridable-props-on-delete';
-import { initHandleComponentEditModeContainer } from './sync/handle-component-edit-mode-container';
import { initLoadComponentDataAfterInstanceAdded } from './sync/load-component-data-after-instance-added';
-import { initRevertOverridablesOnCopyOrDuplicate } from './sync/revert-overridables-on-copy-or-duplicate';
import { type ExtendedWindow } from './types';
-import { onElementDrop } from './utils/tracking';
export function init() {
stylesRepository.register( componentsStylesProvider );
registerSlice( slice );
- registerPanel( componentPropertiesPanel );
registerElementType( COMPONENT_WIDGET_TYPE, ( options: CreateTemplatedElementTypeOptions ) =>
createComponentType( { ...options, showLockedByModal: openEditModeDialog } )
);
- registerDataHook( 'dependency', 'editor/documents/close', ( args ) => {
- const document = getV1CurrentDocument();
- if ( document.config.type === COMPONENT_DOCUMENT_TYPE ) {
- args.mode = 'autosave';
- }
- return true;
- } );
-
- registerDataHook( 'after', 'preview/drop', onElementDrop );
-
( window as unknown as ExtendedWindow ).elementorCommon.__beforeSave = beforeSave;
injectTab( {
@@ -81,26 +50,11 @@ export function init() {
position: 1,
} );
- injectIntoTop( {
- id: 'create-component-popup',
- component: CreateComponentForm,
- } );
-
injectIntoLogic( {
id: 'components-populate-store',
component: PopulateStore,
} );
- injectIntoTop( {
- id: 'edit-component',
- component: EditComponent,
- } );
-
- injectIntoPanelHeaderTop( {
- id: 'component-panel-header',
- component: ComponentPanelHeader,
- } );
-
registerDataHook( 'after', 'editor/documents/attach-preview', async () => {
const { id, config } = getV1CurrentDocument();
@@ -116,19 +70,6 @@ export function init() {
component: LoadTemplateComponents,
} );
- registerFieldIndicator( {
- fieldType: FIELD_TYPE.SETTINGS,
- id: 'component-overridable-prop',
- priority: 1,
- indicator: OverridablePropIndicator,
- } );
-
- registerControlReplacement( {
- id: OVERRIDABLE_PROP_REPLACEMENT_ID,
- component: OverridablePropControl,
- condition: ( { value } ) => componentOverridablePropTypeUtil.isValid( value ),
- } );
-
registerEditingPanelReplacement( {
id: 'component-instance-edit-panel',
condition: ( _, elementType ) => elementType.key === 'e-component',
@@ -139,17 +80,9 @@ export function init() {
settingsTransformersRegistry.register( 'overridable', componentOverridableTransformer );
settingsTransformersRegistry.register( 'override', componentOverrideTransformer );
- initCleanupOverridablePropsOnDelete();
-
- initMcp();
-
initCircularNestingPrevention();
- initNonAtomicNestingPrevention();
-
initLoadComponentDataAfterInstanceAdded();
- initHandleComponentEditModeContainer();
-
- initRevertOverridablesOnCopyOrDuplicate();
+ initExtended();
}
diff --git a/packages/packages/core/editor-components/src/store/actions/delete-overridable-prop.ts b/packages/packages/core/editor-components/src/store/actions/delete-overridable-prop.ts
deleted file mode 100644
index 58908c13b02f..000000000000
--- a/packages/packages/core/editor-components/src/store/actions/delete-overridable-prop.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
-
-import { type ComponentId } from '../../types';
-import { revertElementOverridableSetting } from '../../utils/revert-overridable-settings';
-import { type Source, trackComponentEvent } from '../../utils/tracking';
-import { selectCurrentComponent, selectOverridableProps, slice } from '../store';
-import { removePropFromAllGroups } from '../utils/groups-transformers';
-
-type DeletePropParams = {
- componentId: ComponentId;
- propKey: string;
- source: Source;
-};
-
-export function deleteOverridableProp( { componentId, propKey, source }: DeletePropParams ): void {
- const overridableProps = selectOverridableProps( getState(), componentId );
-
- if ( ! overridableProps ) {
- return;
- }
-
- const prop = overridableProps.props[ propKey ];
-
- if ( ! prop ) {
- return;
- }
-
- revertElementOverridableSetting( prop.elementId, prop.propKey, prop.originValue, propKey );
-
- const { [ propKey ]: removedProp, ...remainingProps } = overridableProps.props;
-
- const updatedGroups = removePropFromAllGroups( overridableProps.groups, propKey );
-
- dispatch(
- slice.actions.setOverridableProps( {
- componentId,
- overridableProps: {
- ...overridableProps,
- props: remainingProps,
- groups: updatedGroups,
- },
- } )
- );
-
- const currentComponent = selectCurrentComponent( getState() );
-
- trackComponentEvent( {
- action: 'propertyRemoved',
- source,
- component_uid: currentComponent?.uid,
- property_id: removedProp.overrideKey,
- property_path: removedProp.propKey,
- property_name: removedProp.label,
- element_type: removedProp.widgetType ?? removedProp.elType,
- } );
-}
diff --git a/packages/packages/core/editor-components/src/sync/before-save.ts b/packages/packages/core/editor-components/src/sync/before-save.ts
index ab86159d2683..2350406b0bc7 100644
--- a/packages/packages/core/editor-components/src/sync/before-save.ts
+++ b/packages/packages/core/editor-components/src/sync/before-save.ts
@@ -2,11 +2,7 @@ import { type V1Document } from '@elementor/editor-documents';
import { type V1Element, type V1ElementData } from '@elementor/editor-elements';
import { type DocumentSaveStatus } from '../types';
-import { createComponentsBeforeSave } from './create-components-before-save';
import { publishDraftComponentsInPageBeforeSave } from './publish-draft-components-in-page-before-save';
-import { setComponentOverridablePropsSettingsBeforeSave } from './set-component-overridable-props-settings-before-save';
-import { updateArchivedComponentBeforeSave } from './update-archived-component-before-save';
-import { updateComponentTitleBeforeSave } from './update-component-title-before-save';
type Options = {
container: V1Element & {
@@ -23,30 +19,5 @@ type Options = {
export const beforeSave = ( { container, status }: Options ) => {
const elements = container?.model.get( 'elements' ).toJSON?.() ?? [];
- return Promise.all( [
- syncComponents( { elements, status } ),
- setComponentOverridablePropsSettingsBeforeSave( { container } ),
- ] );
-};
-
-// These operations run sequentially to prevent race conditions when multiple
-// edits occur on the same component simultaneously.
-// TODO: Consolidate these into a single PUT /components endpoint.
-const syncComponents = async ( { elements, status }: { elements: V1ElementData[]; status: DocumentSaveStatus } ) => {
- // This order is important - first update existing components, then create new components,
- // Since new component validation depends on the existing components (preventing duplicate names).
- await updateExistingComponentsBeforeSave( { elements, status } );
- await createComponentsBeforeSave( { elements, status } );
-};
-
-const updateExistingComponentsBeforeSave = async ( {
- elements,
- status,
-}: {
- elements: V1ElementData[];
- status: DocumentSaveStatus;
-} ) => {
- await updateComponentTitleBeforeSave( status );
- await updateArchivedComponentBeforeSave( status );
- await publishDraftComponentsInPageBeforeSave( { elements, status } );
+ return publishDraftComponentsInPageBeforeSave( { elements, status } );
};
diff --git a/packages/packages/core/editor-components/src/utils/__tests__/filter-valid-overridable-props.test.ts b/packages/packages/core/editor-components/src/utils/__tests__/filter-valid-overridable-props.test.ts
index 9becd6a5b8ef..666e4e3363c2 100644
--- a/packages/packages/core/editor-components/src/utils/__tests__/filter-valid-overridable-props.test.ts
+++ b/packages/packages/core/editor-components/src/utils/__tests__/filter-valid-overridable-props.test.ts
@@ -1,7 +1,6 @@
import { createMockElement } from 'test-utils';
import { type PropValue } from '@elementor/editor-props';
-import { getOverridableProp } from '../../components/overridable-props/utils/get-overridable-prop';
import { componentInstanceOverridePropTypeUtil } from '../../prop-types/component-instance-override-prop-type';
import { componentInstanceOverridesPropTypeUtil } from '../../prop-types/component-instance-overrides-prop-type';
import { componentInstancePropTypeUtil } from '../../prop-types/component-instance-prop-type';
@@ -9,11 +8,13 @@ import { componentOverridablePropTypeUtil } from '../../prop-types/component-ove
import { type OverridableProp, type OverridableProps } from '../../types';
import { filterValidOverridableProps, isExposedPropValid } from '../filter-valid-overridable-props';
import { getContainerByOriginId } from '../get-container-by-origin-id';
+import { getOverridableProp } from '../get-overridable-prop';
jest.mock( '../get-container-by-origin-id', () => ( {
getContainerByOriginId: jest.fn(),
} ) );
-jest.mock( '../../components/overridable-props/utils/get-overridable-prop', () => ( {
+
+jest.mock( '../get-overridable-prop', () => ( {
getOverridableProp: jest.fn(),
} ) );
diff --git a/packages/packages/core/editor-components/src/components/create-component-form/utils/component-form-schema.ts b/packages/packages/core/editor-components/src/utils/component-form-schema.ts
similarity index 100%
rename from packages/packages/core/editor-components/src/components/create-component-form/utils/component-form-schema.ts
rename to packages/packages/core/editor-components/src/utils/component-form-schema.ts
diff --git a/packages/packages/core/editor-components/src/utils/component-name-validation.ts b/packages/packages/core/editor-components/src/utils/component-name-validation.ts
index c85596768dcc..852c8bb5c5c6 100644
--- a/packages/packages/core/editor-components/src/utils/component-name-validation.ts
+++ b/packages/packages/core/editor-components/src/utils/component-name-validation.ts
@@ -1,7 +1,7 @@
import { __getState as getState } from '@elementor/store';
-import { createSubmitComponentSchema } from '../components/create-component-form/utils/component-form-schema';
import { selectComponents } from '../store/store';
+import { createSubmitComponentSchema } from './component-form-schema';
type ValidationResult = { isValid: true; errorMessage: null } | { isValid: false; errorMessage: string };
diff --git a/packages/packages/core/editor-components/src/components/create-component-form/utils/replace-element-with-component.ts b/packages/packages/core/editor-components/src/utils/create-component-model.ts
similarity index 55%
rename from packages/packages/core/editor-components/src/components/create-component-form/utils/replace-element-with-component.ts
rename to packages/packages/core/editor-components/src/utils/create-component-model.ts
index 3440637c3ec6..b5a962107965 100644
--- a/packages/packages/core/editor-components/src/components/create-component-form/utils/replace-element-with-component.ts
+++ b/packages/packages/core/editor-components/src/utils/create-component-model.ts
@@ -1,19 +1,11 @@
-import { replaceElement, type V1ElementData, type V1ElementModelProps } from '@elementor/editor-elements';
+import { type V1ElementModelProps } from '@elementor/editor-elements';
-type ComponentInstanceParams = {
+export type ComponentInstanceParams = {
id?: number;
name: string;
uid: string;
};
-export const replaceElementWithComponent = async ( element: V1ElementData, component: ComponentInstanceParams ) => {
- return await replaceElement( {
- currentElement: element,
- newElement: createComponentModel( component ),
- withHistory: false,
- } );
-};
-
export const createComponentModel = ( component: ComponentInstanceParams ): Omit< V1ElementModelProps, 'id' > => {
return {
elType: 'widget',
diff --git a/packages/packages/core/editor-components/src/utils/expand-navigator.ts b/packages/packages/core/editor-components/src/utils/expand-navigator.ts
deleted file mode 100644
index bf5554d1a812..000000000000
--- a/packages/packages/core/editor-components/src/utils/expand-navigator.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { __privateRunCommand as runCommand } from '@elementor/editor-v1-adapters';
-
-export async function expandNavigator() {
- await runCommand( 'navigator/expand-all' );
-}
diff --git a/packages/packages/core/editor-components/src/utils/filter-valid-overridable-props.ts b/packages/packages/core/editor-components/src/utils/filter-valid-overridable-props.ts
index 23cd5e9976b9..f5d0d2223872 100644
--- a/packages/packages/core/editor-components/src/utils/filter-valid-overridable-props.ts
+++ b/packages/packages/core/editor-components/src/utils/filter-valid-overridable-props.ts
@@ -1,10 +1,10 @@
-import { getOverridableProp } from '../components/overridable-props/utils/get-overridable-prop';
import { type ComponentInstanceOverride } from '../prop-types/component-instance-overrides-prop-type';
import { componentInstanceOverridesPropTypeUtil } from '../prop-types/component-instance-overrides-prop-type';
import { componentInstancePropTypeUtil } from '../prop-types/component-instance-prop-type';
import { componentOverridablePropTypeUtil } from '../prop-types/component-overridable-prop-type';
import { type OverridableProp, type OverridableProps } from '../types';
import { getContainerByOriginId } from './get-container-by-origin-id';
+import { getOverridableProp } from './get-overridable-prop';
import { extractInnerOverrideInfo } from './overridable-props-utils';
export function filterValidOverridableProps(
diff --git a/packages/packages/core/editor-components/src/components/overridable-props/utils/get-overridable-prop.ts b/packages/packages/core/editor-components/src/utils/get-overridable-prop.ts
similarity index 76%
rename from packages/packages/core/editor-components/src/components/overridable-props/utils/get-overridable-prop.ts
rename to packages/packages/core/editor-components/src/utils/get-overridable-prop.ts
index fc2666f33415..f4998f33f091 100644
--- a/packages/packages/core/editor-components/src/components/overridable-props/utils/get-overridable-prop.ts
+++ b/packages/packages/core/editor-components/src/utils/get-overridable-prop.ts
@@ -1,7 +1,7 @@
import { __getState as getState } from '@elementor/store';
-import { selectOverridableProps } from '../../../store/store';
-import { type OverridableProp } from '../../../types';
+import { selectOverridableProps } from '../store/store';
+import { type OverridableProp } from '../types';
export function getOverridableProp( {
componentId,
diff --git a/packages/packages/core/editor-components/src/utils/switch-to-component.ts b/packages/packages/core/editor-components/src/utils/switch-to-component.ts
index e00bdc1ff7a2..c35154553e9c 100644
--- a/packages/packages/core/editor-components/src/utils/switch-to-component.ts
+++ b/packages/packages/core/editor-components/src/utils/switch-to-component.ts
@@ -1,7 +1,6 @@
import { invalidateDocumentData, switchToDocument } from '@elementor/editor-documents';
import { getCurrentDocumentContainer, selectElement } from '@elementor/editor-elements';
-
-import { expandNavigator } from './expand-navigator';
+import { __privateRunCommand as runCommand } from '@elementor/editor-v1-adapters';
export async function switchToComponent(
componentId: number,
@@ -28,6 +27,10 @@ export async function switchToComponent(
}
}
+export async function expandNavigator() {
+ await runCommand( 'navigator/expand-all' );
+}
+
function getSelector( element?: HTMLElement | null, componentInstanceId?: string | null ): string | undefined {
if ( element ) {
return buildUniqueSelector( element );
diff --git a/packages/packages/core/editor-documents/src/sync/__tests__/sync-store.test.ts b/packages/packages/core/editor-documents/src/sync/__tests__/sync-store.test.ts
index 7d76824c8b86..ad4cf2595d07 100644
--- a/packages/packages/core/editor-documents/src/sync/__tests__/sync-store.test.ts
+++ b/packages/packages/core/editor-documents/src/sync/__tests__/sync-store.test.ts
@@ -5,7 +5,7 @@ import { slice } from '../../store';
import { selectActiveDocument } from '../../store/selectors';
import { type Document, type ExitTo, type ExtendedWindow, type V1Document, type V1DocumentsManager } from '../../types';
import { syncStore } from '../index';
-import { getV1DocumentPermalink, getV1DocumentsExitTo } from '../utils';
+import { getV1DocumentPermalink, getV1DocumentsExitTo, getV1DocumentWpPreview } from '../utils';
import { makeDocumentsManager } from './test-utils';
type WindowWithOptionalElementor = Omit< ExtendedWindow, 'elementor' > & {
@@ -56,6 +56,7 @@ describe( '@elementor/editor-documents - Sync Store', () => {
links: {
platformEdit: 'https://localhost/wp-admin/post.php?post=1&action=edit',
permalink: 'https://localhost/?p=1',
+ wpPreview: 'https://localhost/?p=1&preview_id=1&preview_nonce=mock_nonce&preview=true',
},
isDirty: false,
isSaving: false,
@@ -82,6 +83,7 @@ describe( '@elementor/editor-documents - Sync Store', () => {
links: {
platformEdit: 'https://localhost/wp-admin/post.php?post=2&action=edit',
permalink: 'https://localhost/?p=2',
+ wpPreview: 'https://localhost/?p=2&preview_id=2&preview_nonce=mock_nonce&preview=true',
},
isDirty: false,
isSaving: false,
@@ -130,6 +132,7 @@ describe( '@elementor/editor-documents - Sync Store', () => {
links: {
platformEdit: 'https://localhost/wp-admin/post.php?post=2&action=edit',
permalink: 'https://localhost/?p=2',
+ wpPreview: 'https://localhost/?p=2&preview_id=2&preview_nonce=mock_nonce&preview=true',
},
status: {
value: 'publish',
@@ -439,6 +442,7 @@ describe( '@elementor/editor-documents - Sync Store', () => {
const currentDocument = selectActiveDocument( store.getState() );
const platformEdit = getV1DocumentsExitTo( mockDocument );
const permalink = getV1DocumentPermalink( mockDocument );
+ const wpPreview = getV1DocumentWpPreview( mockDocument );
expect( currentDocument ).toEqual< Document >( {
id: 1,
@@ -450,6 +454,7 @@ describe( '@elementor/editor-documents - Sync Store', () => {
links: {
platformEdit,
permalink,
+ wpPreview,
},
status: {
value: 'publish',
diff --git a/packages/packages/core/editor-documents/src/sync/sync-store.ts b/packages/packages/core/editor-documents/src/sync/sync-store.ts
index 7ef05b0ab372..16de781e247a 100644
--- a/packages/packages/core/editor-documents/src/sync/sync-store.ts
+++ b/packages/packages/core/editor-documents/src/sync/sync-store.ts
@@ -12,7 +12,13 @@ import { debounce } from '@elementor/utils';
import { slice } from '../store';
import { selectActiveDocument } from '../store/selectors';
import { type Document } from '../types';
-import { getV1DocumentPermalink, getV1DocumentsExitTo, getV1DocumentsManager, normalizeV1Document } from './utils';
+import {
+ getV1DocumentPermalink,
+ getV1DocumentsExitTo,
+ getV1DocumentsManager,
+ getV1DocumentWpPreview,
+ normalizeV1Document,
+} from './utils';
export function syncStore() {
syncInitialization();
@@ -127,8 +133,9 @@ function syncOnExitToChange() {
const currentDocument = getV1DocumentsManager().getCurrent();
const newExitTo = getV1DocumentsExitTo( currentDocument );
const permalink = getV1DocumentPermalink( currentDocument );
+ const wpPreview = getV1DocumentWpPreview( currentDocument );
- __dispatch( updateActiveDocument( { links: { platformEdit: newExitTo, permalink } } ) );
+ __dispatch( updateActiveDocument( { links: { platformEdit: newExitTo, permalink, wpPreview } } ) );
}, 400 );
listenTo( commandEndEvent( 'document/elements/settings' ), updateExitTo );
diff --git a/packages/packages/core/editor-documents/src/sync/utils.ts b/packages/packages/core/editor-documents/src/sync/utils.ts
index a97d0d3444ca..8c1d96daf877 100644
--- a/packages/packages/core/editor-documents/src/sync/utils.ts
+++ b/packages/packages/core/editor-documents/src/sync/utils.ts
@@ -40,6 +40,10 @@ export function getV1DocumentPermalink( documentData: V1Document ) {
return documentData.config.urls.permalink ?? '';
}
+export function getV1DocumentWpPreview( documentData: V1Document ) {
+ return documentData.config.urls.wp_preview ?? '';
+}
+
export function normalizeV1Document( documentData: V1Document ): Document {
// Draft or autosave.
const isUnpublishedRevision = documentData.config.revisions.current_id !== documentData.id;
@@ -58,6 +62,7 @@ export function normalizeV1Document( documentData: V1Document ): Document {
},
links: {
permalink: getV1DocumentPermalink( documentData ),
+ wpPreview: getV1DocumentWpPreview( documentData ),
platformEdit: exitToUrl,
},
isDirty: documentData.editor.isChanged || isUnpublishedRevision,
diff --git a/packages/packages/core/editor-documents/src/types.ts b/packages/packages/core/editor-documents/src/types.ts
index 6b8b65a1e7bb..b5ea5c55c2d2 100644
--- a/packages/packages/core/editor-documents/src/types.ts
+++ b/packages/packages/core/editor-documents/src/types.ts
@@ -16,6 +16,7 @@ export type Document = {
links: {
platformEdit: string;
permalink: string;
+ wpPreview: string;
};
isDirty: boolean;
isSaving: boolean;
@@ -72,6 +73,7 @@ export type V1Document = {
urls: {
exit_to_dashboard: string;
permalink: string;
+ wp_preview: string;
main_dashboard: string;
all_post_type: string;
};
diff --git a/packages/packages/core/editor-editing-panel/package.json b/packages/packages/core/editor-editing-panel/package.json
index a69eb954a748..760769115f82 100644
--- a/packages/packages/core/editor-editing-panel/package.json
+++ b/packages/packages/core/editor-editing-panel/package.json
@@ -52,7 +52,7 @@
"@elementor/editor-styles-repository": "4.0.0",
"@elementor/editor-ui": "4.0.0",
"@elementor/editor-v1-adapters": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/editor-variables": "4.0.0",
"@elementor/locations": "4.0.0",
"@elementor/menus": "4.0.0",
diff --git a/packages/packages/core/editor-editing-panel/src/components/css-classes/css-class-menu.tsx b/packages/packages/core/editor-editing-panel/src/components/css-classes/css-class-menu.tsx
index 3d1543f1c106..d7827ab10323 100644
--- a/packages/packages/core/editor-editing-panel/src/components/css-classes/css-class-menu.tsx
+++ b/packages/packages/core/editor-editing-panel/src/components/css-classes/css-class-menu.tsx
@@ -25,13 +25,26 @@ type State = {
label: string;
};
-const STATES: State[] = [
+const DEFAULT_PSEUDO_STATES: State[] = [
{ key: 'normal', value: null, label: __( 'normal', 'elementor' ) },
{ key: 'hover', value: 'hover', label: __( 'hover', 'elementor' ) },
{ key: 'focus', value: 'focus', label: __( 'focus', 'elementor' ) },
{ key: 'active', value: 'active', label: __( 'active', 'elementor' ) },
];
+function usePseudoStates(): State[] {
+ const { elementType } = useElement();
+ const { pseudoStates = [] } = elementType;
+
+ const additionalStates: State[] = pseudoStates.map( ( { name, value } ) => ( {
+ key: value as StyleDefinitionStateWithNormal,
+ value: value as StyleDefinitionState,
+ label: name,
+ } ) );
+
+ return [ ...DEFAULT_PSEUDO_STATES, ...additionalStates ];
+}
+
type CssClassMenuProps = {
popupState: PopupState;
anchorEl: HTMLElement | null;
@@ -41,6 +54,7 @@ type CssClassMenuProps = {
export function CssClassMenu( { popupState, anchorEl, fixed }: CssClassMenuProps ) {
const { provider } = useCssClass();
const isLocalStyle = provider ? isElementsStylesProvider( provider ) : true;
+ const pseudoStates = usePseudoStates();
const handleKeyDown = ( e: React.KeyboardEvent< HTMLElement > ) => {
e.stopPropagation();
@@ -69,7 +83,7 @@ export function CssClassMenu( { popupState, anchorEl, fixed }: CssClassMenuProps
{ __( 'States', 'elementor' ) }
- { STATES.map( ( state ) => {
+ { pseudoStates.map( ( state ) => {
return (
{
registerPopoverAction( {
id: 'dynamic-tags',
+ priority: 20,
useProps: usePropDynamicAction,
} );
diff --git a/packages/packages/core/editor-editing-panel/src/reset-style-props.tsx b/packages/packages/core/editor-editing-panel/src/reset-style-props.tsx
index b59d49c4bff2..22eaad8ba0f2 100644
--- a/packages/packages/core/editor-editing-panel/src/reset-style-props.tsx
+++ b/packages/packages/core/editor-editing-panel/src/reset-style-props.tsx
@@ -13,6 +13,7 @@ const { registerAction } = controlActionsMenu;
export function initResetStyleProps() {
registerAction( {
id: 'reset-style-value',
+ priority: 10,
useProps: useResetStyleValueProps,
} );
}
diff --git a/packages/packages/core/editor-global-classes/package.json b/packages/packages/core/editor-global-classes/package.json
index 4a2d92f75963..929589e868ec 100644
--- a/packages/packages/core/editor-global-classes/package.json
+++ b/packages/packages/core/editor-global-classes/package.json
@@ -53,7 +53,7 @@
"@elementor/editor-ui": "4.0.0",
"@elementor/editor-v1-adapters": "4.0.0",
"@elementor/http-client": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/query": "4.0.0",
"@elementor/schema": "4.0.0",
"@elementor/store": "4.0.0",
diff --git a/packages/packages/core/editor-global-classes/src/__tests__/sync-with-document-save.test.ts b/packages/packages/core/editor-global-classes/src/__tests__/sync-with-document-save.test.ts
index 94b475994bbb..3dc1b63c059a 100644
--- a/packages/packages/core/editor-global-classes/src/__tests__/sync-with-document-save.test.ts
+++ b/packages/packages/core/editor-global-classes/src/__tests__/sync-with-document-save.test.ts
@@ -1,6 +1,10 @@
import { createMockStyleDefinition } from 'test-utils';
import { getCurrentUser } from '@elementor/editor-current-user';
-import { __privateRunCommandSync as runCommandSync, registerDataHook } from '@elementor/editor-v1-adapters';
+import {
+ __privateRunCommandSync as runCommandSync,
+ type HookOptions,
+ registerDataHook,
+} from '@elementor/editor-v1-adapters';
import {
__createStore as createStore,
__dispatch as dispatch,
@@ -135,8 +139,15 @@ describe( 'syncWithDocumentSave', () => {
} );
} );
+type AfterHookCallback = (
+ args: Record< string, unknown >,
+ result: unknown,
+ options: HookOptions
+) => unknown | Promise< void >;
+type DependencyHookCallback = ( args: Record< string, unknown >, options: HookOptions ) => boolean;
+
function mockRegisterDataHook() {
- const callbacks = new Map< string, ( args: Record< string, unknown >, result?: unknown ) => unknown >();
+ const callbacks = new Map< string, AfterHookCallback | DependencyHookCallback >();
jest.mocked( registerDataHook ).mockImplementation( ( type, command, callback ) => {
const key = `${ command }-${ type }`;
@@ -157,6 +168,11 @@ function mockRegisterDataHook() {
callbacks.delete( key );
- return callback?.( args, result );
+ switch ( type ) {
+ case 'after':
+ return ( callback as AfterHookCallback )?.( args, result, { commandsCurrentTrace: [] } );
+ case 'dependency':
+ return ( callback as DependencyHookCallback )?.( args, { commandsCurrentTrace: [] } );
+ }
};
}
diff --git a/packages/packages/core/editor-interactions/package.json b/packages/packages/core/editor-interactions/package.json
index d82bc44e0a92..4a30df5061f3 100644
--- a/packages/packages/core/editor-interactions/package.json
+++ b/packages/packages/core/editor-interactions/package.json
@@ -45,7 +45,7 @@
"@elementor/editor-responsive": "4.0.0-524",
"@elementor/editor-ui": "4.0.0",
"@elementor/editor-v1-adapters": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
"@elementor/session": "4.0.0",
"@elementor/ui": "1.36.17",
"@elementor/utils": "4.0.0",
diff --git a/packages/packages/core/editor-interactions/src/__tests__/interaction-details.test.tsx b/packages/packages/core/editor-interactions/src/__tests__/interaction-details.test.tsx
index 3fa05ffb5d64..21edab56bb76 100644
--- a/packages/packages/core/editor-interactions/src/__tests__/interaction-details.test.tsx
+++ b/packages/packages/core/editor-interactions/src/__tests__/interaction-details.test.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
-import { fireEvent, render, screen, within } from '@testing-library/react';
+import { renderWithTheme } from 'test-utils';
+import { fireEvent, screen, within } from '@testing-library/react';
import { Direction } from '../components/controls/direction';
import { Easing } from '../components/controls/easing';
@@ -52,7 +53,7 @@ describe( 'InteractionDetails', () => {
const mockOnPlayInteraction = jest.fn();
const renderInteractionDetails = ( interaction: InteractionItemValue ) => {
- return render(
+ return renderWithTheme(
{
fireEvent.mouseDown( triggerSelect );
// Sanity: core UI enables only these trigger options.
- expect( screen.getByRole( 'option', { name: /page load/i } ) ).toBeInTheDocument();
- expect( screen.getByRole( 'option', { name: /scroll into view/i } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'option', { name: /page load/i, hidden: true } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'option', { name: /scroll into view/i, hidden: true } ) ).toBeInTheDocument();
// Guard: Pro-only trigger should be present but disabled in the core trigger control.
- const scrollOnOption = screen.getByRole( 'option', { name: /while scrolling/i } );
+ const scrollOnOption = screen.getByRole( 'option', { name: /while scrolling/i, hidden: true } );
expect( scrollOnOption ).toBeInTheDocument();
expect( scrollOnOption ).toHaveAttribute( 'aria-disabled', 'true' );
} );
@@ -245,7 +246,7 @@ describe( 'InteractionDetails', () => {
const comboboxes = screen.getAllByRole( 'combobox' );
const triggerSelect = comboboxes[ 0 ];
fireEvent.mouseDown( triggerSelect );
- const scrollInOption = screen.getByRole( 'option', { name: /scroll into view/i } );
+ const scrollInOption = screen.getByRole( 'option', { name: /scroll into view/i, hidden: true } );
fireEvent.click( scrollInOption );
expect( mockOnChange ).toHaveBeenCalledTimes( 1 );
@@ -268,7 +269,7 @@ describe( 'InteractionDetails', () => {
const effectSelect = getEffectCombobox();
fireEvent.mouseDown( effectSelect );
- const slideOption = screen.getByRole( 'option', { name: /slide/i } );
+ const slideOption = screen.getByRole( 'option', { name: /slide/i, hidden: true } );
fireEvent.click( slideOption );
expect( mockOnChange ).toHaveBeenCalledTimes( 1 );
@@ -426,7 +427,7 @@ describe( 'InteractionDetails', () => {
const comboboxes = screen.getAllByRole( 'combobox' );
const triggerSelect = comboboxes[ 0 ];
fireEvent.mouseDown( triggerSelect );
- const scrollInOption = screen.getByRole( 'option', { name: /scroll into view/i } );
+ const scrollInOption = screen.getByRole( 'option', { name: /scroll into view/i, hidden: true } );
fireEvent.click( scrollInOption );
const updatedInteraction = mockOnChange.mock.calls[ 0 ][ 0 ];
@@ -457,7 +458,7 @@ describe( 'InteractionDetails', () => {
const effectSelect = getEffectCombobox();
fireEvent.mouseDown( effectSelect );
- const slideOption = screen.getByRole( 'option', { name: /slide/i } );
+ const slideOption = screen.getByRole( 'option', { name: /slide/i, hidden: true } );
fireEvent.click( slideOption );
const updatedInteraction = mockOnChange.mock.calls[ 0 ][ 0 ];
@@ -481,7 +482,7 @@ describe( 'InteractionDetails', () => {
const effectSelect = getEffectCombobox();
fireEvent.mouseDown( effectSelect );
- const scaleOption = screen.getByRole( 'option', { name: /scale/i } );
+ const scaleOption = screen.getByRole( 'option', { name: /scale/i, hidden: true } );
fireEvent.click( scaleOption );
const updatedInteraction = mockOnChange.mock.calls[ 0 ][ 0 ];
@@ -512,7 +513,7 @@ describe( 'InteractionDetails', () => {
const effectSelect = getEffectCombobox();
fireEvent.mouseDown( effectSelect );
- const slideOption = screen.getByRole( 'option', { name: /slide/i } );
+ const slideOption = screen.getByRole( 'option', { name: /slide/i, hidden: true } );
fireEvent.click( slideOption );
const updatedInteraction = mockOnChange.mock.calls[ 0 ][ 0 ];
diff --git a/packages/packages/core/editor-interactions/src/components/controls/easing.tsx b/packages/packages/core/editor-interactions/src/components/controls/easing.tsx
index cf2b9ae5ce65..b3f0a9e2afb5 100644
--- a/packages/packages/core/editor-interactions/src/components/controls/easing.tsx
+++ b/packages/packages/core/editor-interactions/src/components/controls/easing.tsx
@@ -1,17 +1,12 @@
import * as React from 'react';
-import { type MouseEvent, useRef } from 'react';
-import { MenuListItem } from '@elementor/editor-ui';
-import { MenuSubheader, Select } from '@elementor/ui';
import { __ } from '@wordpress/i18n';
import { type FieldProps } from '../../types';
-import { InteractionsPromotionChip, type InteractionsPromotionChipRef } from '../../ui/interactions-promotion-chip';
+import { PromotionSelect } from '../../ui/promotion-select';
+import { DEFAULT_VALUES } from '../interaction-details';
-const BASE_EASING_OPTIONS = {
+export const EASING_OPTIONS = {
easeIn: __( 'Ease In', 'elementor' ),
-};
-
-const EXTENDED_EASING_OPTIONS = {
easeInOut: __( 'Ease In Out', 'elementor' ),
easeOut: __( 'Ease Out', 'elementor' ),
backIn: __( 'Back In', 'elementor' ),
@@ -20,60 +15,24 @@ const EXTENDED_EASING_OPTIONS = {
linear: __( 'Linear', 'elementor' ),
};
-const DEFAULT_EASING = 'easeIn';
+export const BASE_EASINGS: string[] = [ 'easeIn' ];
export function Easing( {}: FieldProps ) {
- const promotionRef = useRef< InteractionsPromotionChipRef >( null );
- const anchorRef = useRef< HTMLElement >( null );
+ const baseOptions = Object.fromEntries(
+ Object.entries( EASING_OPTIONS ).filter( ( [ key ] ) => BASE_EASINGS.includes( key ) )
+ );
- const baseOptions = Object.entries( BASE_EASING_OPTIONS ).map( ( [ key, label ] ) => ( { key, label } ) );
- const extendedOptions = Object.entries( EXTENDED_EASING_OPTIONS ).map( ( [ key, label ] ) => ( { key, label } ) );
+ const disabledOptions = Object.fromEntries(
+ Object.entries( EASING_OPTIONS ).filter( ( [ key ] ) => ! BASE_EASINGS.includes( key ) )
+ );
return (
-
+
);
}
diff --git a/packages/packages/core/editor-interactions/src/components/controls/effect.tsx b/packages/packages/core/editor-interactions/src/components/controls/effect.tsx
index 862d7dfe83b7..6172e23e33ab 100644
--- a/packages/packages/core/editor-interactions/src/components/controls/effect.tsx
+++ b/packages/packages/core/editor-interactions/src/components/controls/effect.tsx
@@ -1,33 +1,40 @@
import * as React from 'react';
-import { MenuListItem } from '@elementor/editor-ui';
-import { Select, type SelectChangeEvent } from '@elementor/ui';
import { __ } from '@wordpress/i18n';
import { type FieldProps } from '../../types';
+import { PromotionSelect } from '../../ui/promotion-select';
+import { DEFAULT_VALUES } from '../interaction-details';
+
+export const EFFECT_OPTIONS = {
+ fade: __( 'Fade', 'elementor' ),
+ slide: __( 'Slide', 'elementor' ),
+ scale: __( 'Scale', 'elementor' ),
+ custom: __( 'Custom', 'elementor' ),
+};
+
+export const BASE_EFFECTS: string[] = [ 'fade', 'slide', 'scale' ];
export function Effect( { value, onChange }: FieldProps ) {
- const availableEffects = [
- { key: 'fade', label: __( 'Fade', 'elementor' ) },
- { key: 'slide', label: __( 'Slide', 'elementor' ) },
- { key: 'scale', label: __( 'Scale', 'elementor' ) },
- { key: 'custom', label: __( 'Custom', 'elementor' ), disabled: true },
- ];
+ const baseOptions = Object.fromEntries(
+ Object.entries( EFFECT_OPTIONS ).filter( ( [ key ] ) => BASE_EFFECTS.includes( key ) )
+ );
+
+ const disabledOptions = Object.fromEntries(
+ Object.entries( EFFECT_OPTIONS ).filter( ( [ key ] ) => ! BASE_EFFECTS.includes( key ) )
+ );
return (
-
+
);
}
diff --git a/packages/packages/core/editor-interactions/src/components/controls/replay.tsx b/packages/packages/core/editor-interactions/src/components/controls/replay.tsx
index 78c98a2ceb5d..80eb7773d5ee 100644
--- a/packages/packages/core/editor-interactions/src/components/controls/replay.tsx
+++ b/packages/packages/core/editor-interactions/src/components/controls/replay.tsx
@@ -7,6 +7,13 @@ import { __ } from '@wordpress/i18n';
import { type ReplayFieldProps } from '../../types';
import { InteractionsPromotionChip } from '../../ui/interactions-promotion-chip';
+export const REPLAY_OPTIONS = {
+ no: __( 'No', 'elementor' ),
+ yes: __( 'Yes', 'elementor' ),
+};
+
+export const BASE_REPLAY: string[] = [ 'no' ];
+
const OVERLAY_GRID = '1 / 1';
const CHIP_OFFSET = '50%';
@@ -15,14 +22,14 @@ export function Replay( { onChange, anchorRef }: ReplayFieldProps ) {
{
value: false,
disabled: false,
- label: __( 'No', 'elementor' ),
+ label: REPLAY_OPTIONS.no,
renderContent: ( { size } ) => ,
showTooltip: true,
},
{
value: true,
disabled: true,
- label: __( 'Yes', 'elementor' ),
+ label: REPLAY_OPTIONS.yes,
renderContent: ( { size } ) => ,
showTooltip: true,
},
diff --git a/packages/packages/core/editor-interactions/src/components/controls/trigger.tsx b/packages/packages/core/editor-interactions/src/components/controls/trigger.tsx
index b484af751ec0..39b700e69020 100644
--- a/packages/packages/core/editor-interactions/src/components/controls/trigger.tsx
+++ b/packages/packages/core/editor-interactions/src/components/controls/trigger.tsx
@@ -1,11 +1,11 @@
import * as React from 'react';
-import { MenuListItem } from '@elementor/editor-ui';
-import { Select, type SelectChangeEvent } from '@elementor/ui';
import { __ } from '@wordpress/i18n';
import { type FieldProps } from '../../types';
+import { PromotionSelect } from '../../ui/promotion-select';
+import { DEFAULT_VALUES } from '../interaction-details';
-const TRIGGER_OPTIONS = {
+export const TRIGGER_OPTIONS = {
load: __( 'Page load', 'elementor' ),
scrollIn: __( 'Scroll into view', 'elementor' ),
scrollOn: __( 'While scrolling', 'elementor' ),
@@ -13,30 +13,26 @@ const TRIGGER_OPTIONS = {
click: __( 'On click', 'elementor' ),
};
-const SUPPORTED_TRIGGERS = [ 'load', 'scrollIn' ];
+export const BASE_TRIGGERS: string[] = [ 'load', 'scrollIn' ];
export function Trigger( { value, onChange }: FieldProps ) {
- const availableTriggers = Object.entries( TRIGGER_OPTIONS ).map( ( [ key, label ] ) => ( {
- key,
- label,
- disabled: ! SUPPORTED_TRIGGERS.includes( key ),
- } ) );
+ const baseOptions = Object.fromEntries(
+ Object.entries( TRIGGER_OPTIONS ).filter( ( [ key ] ) => BASE_TRIGGERS.includes( key ) )
+ );
+
+ const disabledOptions = Object.fromEntries(
+ Object.entries( TRIGGER_OPTIONS ).filter( ( [ key ] ) => ! BASE_TRIGGERS.includes( key ) )
+ );
return (
-
+
);
}
diff --git a/packages/packages/core/editor-interactions/src/components/field.tsx b/packages/packages/core/editor-interactions/src/components/field.tsx
index b28ff3601de8..71148d90f653 100644
--- a/packages/packages/core/editor-interactions/src/components/field.tsx
+++ b/packages/packages/core/editor-interactions/src/components/field.tsx
@@ -5,7 +5,7 @@ import { Grid } from '@elementor/ui';
export const Field = ( { label, children }: { label: string } & PropsWithChildren ) => {
return (
-
+
{ label }
diff --git a/packages/packages/core/editor-interactions/src/components/interaction-details.tsx b/packages/packages/core/editor-interactions/src/components/interaction-details.tsx
index 07711b38f595..9e8c42ed74a3 100644
--- a/packages/packages/core/editor-interactions/src/components/interaction-details.tsx
+++ b/packages/packages/core/editor-interactions/src/components/interaction-details.tsx
@@ -25,7 +25,7 @@ type InteractionDetailsProps = {
onPlayInteraction: ( interactionId: string ) => void;
};
-const DEFAULT_VALUES = {
+export const DEFAULT_VALUES = {
trigger: 'load',
effect: 'fade',
type: 'in',
diff --git a/packages/packages/core/editor-interactions/src/index.ts b/packages/packages/core/editor-interactions/src/index.ts
index 3f5dce64cb3f..437171ebbb3d 100644
--- a/packages/packages/core/editor-interactions/src/index.ts
+++ b/packages/packages/core/editor-interactions/src/index.ts
@@ -11,4 +11,8 @@ export {
export { ELEMENTS_INTERACTIONS_PROVIDER_KEY_PREFIX } from './providers/document-elements-interactions-provider';
export { init } from './init';
export { registerInteractionsControl } from './interactions-controls-registry';
-export type { InteractionItemPropValue, FieldProps } from './types';
+export type { InteractionItemPropValue, FieldProps, ReplayFieldProps } from './types';
+export { TRIGGER_OPTIONS, BASE_TRIGGERS } from './components/controls/trigger';
+export { EASING_OPTIONS, BASE_EASINGS } from './components/controls/easing';
+export { REPLAY_OPTIONS, BASE_REPLAY } from './components/controls/replay';
+export { EFFECT_OPTIONS, BASE_EFFECTS } from './components/controls/effect';
diff --git a/packages/packages/core/editor-interactions/src/interactions-controls-registry.ts b/packages/packages/core/editor-interactions/src/interactions-controls-registry.ts
index 6bb2da43348c..095e049159d5 100644
--- a/packages/packages/core/editor-interactions/src/interactions-controls-registry.ts
+++ b/packages/packages/core/editor-interactions/src/interactions-controls-registry.ts
@@ -20,7 +20,7 @@ type InteractionsControlType =
type InteractionsControlPropsMap = {
trigger: FieldProps;
effect: FieldProps;
- customEffects?: FieldProps< PropValue >;
+ customEffects: FieldProps< PropValue >;
effectType: FieldProps;
direction: DirectionFieldProps;
duration: FieldProps;
diff --git a/packages/packages/core/editor-interactions/src/providers/document-elements-interactions-provider.ts b/packages/packages/core/editor-interactions/src/providers/document-elements-interactions-provider.ts
index bab15c45ded5..eb8bfbef568c 100644
--- a/packages/packages/core/editor-interactions/src/providers/document-elements-interactions-provider.ts
+++ b/packages/packages/core/editor-interactions/src/providers/document-elements-interactions-provider.ts
@@ -2,7 +2,6 @@ import { getCurrentDocumentId, getElementInteractions, getElements } from '@elem
import { __privateListenTo as listenTo, windowEvent } from '@elementor/editor-v1-adapters';
import { createInteractionsProvider } from '../utils/create-interactions-provider';
-import { normalizeInteractions } from './utils/normalize-interactions';
export const ELEMENTS_INTERACTIONS_PROVIDER_KEY_PREFIX = 'document-elements-interactions-';
@@ -37,12 +36,10 @@ export const documentElementsInteractionsProvider = createInteractionsProvider(
return filtered.map( ( element ) => {
const interactions = getElementInteractions( element.id );
- const normalizedInteractions = normalizeInteractions( interactions );
-
return {
elementId: element.id,
dataId: element.id,
- interactions: normalizedInteractions || { version: 1, items: [] },
+ interactions: interactions || { version: 1, items: [] },
};
} );
},
diff --git a/packages/packages/core/editor-interactions/src/providers/utils/normalize-interactions.ts b/packages/packages/core/editor-interactions/src/providers/utils/normalize-interactions.ts
deleted file mode 100644
index 9b5ab88c2b17..000000000000
--- a/packages/packages/core/editor-interactions/src/providers/utils/normalize-interactions.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { type Unit } from '@elementor/editor-controls';
-import { type ElementInteractions } from '@elementor/editor-elements';
-import {
- isTransformable,
- numberPropTypeUtil,
- type PropValue,
- type SizePropValue,
- type TransformablePropValue,
-} from '@elementor/editor-props';
-
-import { convertTimeUnit } from '../../utils/time-conversion';
-
-type Normalizer = ( value: TransformablePropValue< string > ) => PropValue;
-
-const FIELD_NORMALIZERS: Record< string, Normalizer > = {
- size: ( value ) => {
- const sizeProp = value as SizePropValue;
- const { size, unit } = sizeProp.value;
- const numberPropValue = convertTimeUnit( size as number, unit as Unit, 'ms' );
-
- return numberPropTypeUtil.create( numberPropValue );
- },
-};
-
-export const normalizeInteractions = ( interactions?: ElementInteractions ): ElementInteractions | undefined => {
- if ( ! interactions ) {
- return;
- }
-
- if ( interactions.items.length === 0 ) {
- return interactions;
- }
-
- return {
- ...interactions,
- items: interactions.items.map( normalizeNode ),
- } as ElementInteractions;
-};
-
-const normalizeNode = ( node: PropValue ): PropValue => {
- if ( Array.isArray( node ) ) {
- return node.map( normalizeNode );
- }
-
- if ( node !== null && typeof node === 'object' ) {
- if ( isTransformable( node ) && node.value !== undefined ) {
- const normalizer = FIELD_NORMALIZERS[ node.$$type ];
-
- if ( normalizer ) {
- return normalizer( node as TransformablePropValue< string > );
- }
- }
-
- const typedNode = node as Record< string, unknown >;
- const out: Record< string, PropValue > = {};
-
- for ( const k in typedNode ) {
- out[ k ] = normalizeNode( typedNode[ k ] as PropValue );
- }
-
- return out;
- }
-
- return node;
-};
diff --git a/packages/packages/core/editor-interactions/src/ui/promotion-select.tsx b/packages/packages/core/editor-interactions/src/ui/promotion-select.tsx
new file mode 100644
index 000000000000..57898c01723c
--- /dev/null
+++ b/packages/packages/core/editor-interactions/src/ui/promotion-select.tsx
@@ -0,0 +1,76 @@
+import * as React from 'react';
+import { type MouseEvent, useRef } from 'react';
+import { MenuListItem } from '@elementor/editor-ui';
+import { MenuSubheader, Select, type SelectChangeEvent } from '@elementor/ui';
+import { __ } from '@wordpress/i18n';
+
+import { InteractionsPromotionChip, type InteractionsPromotionChipRef } from './interactions-promotion-chip';
+
+type PromotionSelectProps = {
+ value: string;
+ onChange?: ( value: string ) => void;
+ baseOptions: Record< string, string >;
+ disabledOptions: Record< string, string >;
+ promotionLabel?: string;
+ promotionContent: string;
+ upgradeUrl: string;
+};
+
+export function PromotionSelect( {
+ value,
+ onChange,
+ baseOptions,
+ disabledOptions,
+ promotionLabel,
+ promotionContent,
+ upgradeUrl,
+}: PromotionSelectProps ) {
+ const promotionRef = useRef< InteractionsPromotionChipRef >( null );
+ const anchorRef = useRef< HTMLElement >( null );
+
+ return (
+
+ );
+}
diff --git a/packages/packages/core/editor-site-navigation/package.json b/packages/packages/core/editor-site-navigation/package.json
index 36f36761906f..459017c0db26 100644
--- a/packages/packages/core/editor-site-navigation/package.json
+++ b/packages/packages/core/editor-site-navigation/package.json
@@ -44,7 +44,8 @@
"@elementor/editor-panels": "4.0.0",
"@elementor/editor-v1-adapters": "4.0.0",
"@elementor/env": "4.0.0",
- "@elementor/icons": "^1.63.0",
+ "@elementor/icons": "^1.68.0",
+ "@elementor/events": "4.0.0",
"@elementor/query": "4.0.0",
"@elementor/ui": "1.36.17",
"@wordpress/api-fetch": "^6.42.0",
diff --git a/packages/packages/core/editor-site-navigation/src/components/top-bar/create-post-list-item.tsx b/packages/packages/core/editor-site-navigation/src/components/top-bar/create-post-list-item.tsx
index 2e5d04e0d624..728d15f32fcd 100644
--- a/packages/packages/core/editor-site-navigation/src/components/top-bar/create-post-list-item.tsx
+++ b/packages/packages/core/editor-site-navigation/src/components/top-bar/create-post-list-item.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import { __useNavigateToDocument as useNavigateToDocument } from '@elementor/editor-documents';
+import { useMixpanel } from '@elementor/events';
import { PlusIcon } from '@elementor/icons';
import { CircularProgress, ListItemIcon, ListItemText, MenuItem, type MenuItemProps } from '@elementor/ui';
import { __ } from '@wordpress/i18n';
@@ -15,11 +16,26 @@ export function CreatePostListItem( { closePopup, ...props }: Props ) {
const { create, isLoading } = useCreatePage();
const navigateToDocument = useNavigateToDocument();
const { data: user } = useUser();
+ const { dispatchEvent, config } = useMixpanel();
return (