@elementor/editor-variables 3.33.0-98 → 3.34.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/index.d.mts +11 -3
  2. package/dist/index.d.ts +11 -3
  3. package/dist/index.js +1874 -801
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1819 -737
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +16 -14
  8. package/src/api.ts +24 -0
  9. package/src/batch-operations.ts +86 -0
  10. package/src/components/fields/color-field.tsx +1 -0
  11. package/src/components/fields/font-field.tsx +2 -1
  12. package/src/components/fields/label-field.tsx +42 -6
  13. package/src/components/ui/deleted-variable-alert.tsx +14 -10
  14. package/src/components/ui/{no-variables.tsx → empty-state.tsx} +8 -13
  15. package/src/components/ui/menu-item-content.tsx +14 -11
  16. package/src/components/ui/mismatch-variable-alert.tsx +5 -9
  17. package/src/components/ui/missing-variable-alert.tsx +8 -9
  18. package/src/components/ui/no-search-results.tsx +1 -2
  19. package/src/components/ui/tags/assigned-tag.tsx +6 -3
  20. package/src/components/ui/tags/warning-variable-tag.tsx +44 -0
  21. package/src/components/ui/variable/deleted-variable.tsx +13 -6
  22. package/src/components/ui/variable/mismatch-variable.tsx +11 -4
  23. package/src/components/ui/variable/missing-variable.tsx +2 -2
  24. package/src/components/variable-creation.tsx +10 -3
  25. package/src/components/variable-edit.tsx +11 -12
  26. package/src/components/variable-restore.tsx +3 -2
  27. package/src/components/variables-manager/hooks/use-auto-edit.ts +21 -0
  28. package/src/components/variables-manager/hooks/use-error-navigation.ts +49 -0
  29. package/src/components/variables-manager/hooks/use-variables-manager-state.ts +89 -0
  30. package/src/components/variables-manager/variable-editable-cell.tsx +131 -67
  31. package/src/components/variables-manager/variables-manager-create-menu.tsx +116 -0
  32. package/src/components/variables-manager/variables-manager-panel.tsx +290 -59
  33. package/src/components/variables-manager/variables-manager-table.tsx +111 -14
  34. package/src/components/variables-selection.tsx +61 -15
  35. package/src/controls/variable-control.tsx +1 -1
  36. package/src/hooks/use-prop-variables.ts +11 -8
  37. package/src/hooks/use-variable-bound-prop.ts +42 -0
  38. package/src/index.ts +1 -0
  39. package/src/init.ts +19 -6
  40. package/src/mcp/create-variable-tool.ts +70 -0
  41. package/src/mcp/delete-variable-tool.ts +50 -0
  42. package/src/mcp/index.ts +17 -0
  43. package/src/mcp/list-variables-tool.ts +58 -0
  44. package/src/mcp/update-variable-tool.ts +81 -0
  45. package/src/mcp/variables-resource.ts +28 -0
  46. package/src/register-variable-types.tsx +2 -0
  47. package/src/service.ts +60 -1
  48. package/src/storage.ts +8 -0
  49. package/src/types.ts +1 -0
  50. package/src/utils/filter-by-search.ts +5 -0
  51. package/src/utils/tracking.ts +37 -22
  52. package/src/utils/validations.ts +72 -3
  53. package/src/variables-registry/create-variable-type-registry.ts +10 -1
  54. package/src/variables-registry/variable-type-registry.ts +2 -1
  55. package/src/components/ui/tags/deleted-tag.tsx +0 -37
  56. package/src/components/ui/tags/mismatch-tag.tsx +0 -37
  57. package/src/components/ui/tags/missing-tag.tsx +0 -25
  58. /package/src/components/variables-manager/{variable-edit-menu.tsx → ui/variable-edit-menu.tsx} +0 -0
  59. /package/src/components/variables-manager/{variable-table-cell.tsx → ui/variable-table-cell.tsx} +0 -0
package/src/service.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { __ } from '@wordpress/i18n';
2
2
 
3
3
  import { apiClient } from './api';
4
+ import { buildOperationsArray, type OperationResult } from './batch-operations';
4
5
  import { OP_RW, Storage, type TVariablesList } from './storage';
5
6
  import { styleVariablesRepository } from './style-variables-repository';
6
7
  import { type Variable } from './types';
@@ -12,8 +13,12 @@ export const service = {
12
13
  return storage.load();
13
14
  },
14
15
 
16
+ getWatermark: (): number => {
17
+ return storage.state.watermark;
18
+ },
19
+
15
20
  init: () => {
16
- service.load();
21
+ return service.load();
17
22
  },
18
23
 
19
24
  load: () => {
@@ -168,6 +173,60 @@ export const service = {
168
173
  };
169
174
  } );
170
175
  },
176
+
177
+ batchSave: ( originalVariables: TVariablesList, currentVariables: TVariablesList ) => {
178
+ const operations = buildOperationsArray( originalVariables, currentVariables );
179
+ const batchPayload = { operations, watermark: storage.state.watermark };
180
+
181
+ if ( operations.length === 0 ) {
182
+ return Promise.resolve( {
183
+ success: true,
184
+ watermark: storage.state.watermark,
185
+ operations: 0,
186
+ } );
187
+ }
188
+
189
+ return apiClient
190
+ .batch( batchPayload )
191
+ .then( ( response ) => {
192
+ const { success, data: payload } = response.data;
193
+
194
+ if ( ! success ) {
195
+ throw new Error( 'Unexpected response from server' );
196
+ }
197
+
198
+ return payload;
199
+ } )
200
+ .then( ( data ) => {
201
+ const { results, watermark } = data;
202
+
203
+ handleWatermark( OP_RW, watermark );
204
+
205
+ if ( results ) {
206
+ results.forEach( ( result: OperationResult ) => {
207
+ if ( result.variable ) {
208
+ const { id: variableId, ...variableData } = result.variable;
209
+
210
+ if ( result.type === 'create' ) {
211
+ storage.add( variableId, variableData );
212
+ } else {
213
+ storage.update( variableId, variableData );
214
+ }
215
+
216
+ styleVariablesRepository.update( {
217
+ [ variableId ]: variableData,
218
+ } );
219
+ }
220
+ } );
221
+ }
222
+
223
+ return {
224
+ success: true,
225
+ watermark,
226
+ operations: operations.length,
227
+ };
228
+ } );
229
+ },
171
230
  };
172
231
 
173
232
  const handleWatermark = ( operation: string, newWatermark: number ) => {
package/src/storage.ts CHANGED
@@ -2,6 +2,7 @@ export type TVariable = {
2
2
  type: string;
3
3
  label: string;
4
4
  value: string;
5
+ order?: number;
5
6
  deleted?: boolean;
6
7
  deleted_at?: string;
7
8
  };
@@ -20,6 +21,10 @@ export class Storage {
20
21
  variables: TVariablesList;
21
22
  };
22
23
 
24
+ notifyChange() {
25
+ window.dispatchEvent( new Event( 'variables:updated' ) );
26
+ }
27
+
23
28
  constructor() {
24
29
  this.state = {
25
30
  watermark: -1,
@@ -43,18 +48,21 @@ export class Storage {
43
48
 
44
49
  localStorage.setItem( STORAGE_WATERMARK_KEY, this.state.watermark.toString() );
45
50
  localStorage.setItem( STORAGE_KEY, JSON.stringify( this.state.variables ) );
51
+ this.notifyChange();
46
52
  }
47
53
 
48
54
  add( id: string, variable: TVariable ) {
49
55
  this.load();
50
56
  this.state.variables[ id ] = variable;
51
57
  localStorage.setItem( STORAGE_KEY, JSON.stringify( this.state.variables ) );
58
+ this.notifyChange();
52
59
  }
53
60
 
54
61
  update( id: string, variable: TVariable ) {
55
62
  this.load();
56
63
  this.state.variables[ id ] = variable;
57
64
  localStorage.setItem( STORAGE_KEY, JSON.stringify( this.state.variables ) );
65
+ this.notifyChange();
58
66
  }
59
67
 
60
68
  watermark( watermark: number ) {
package/src/types.ts CHANGED
@@ -22,4 +22,5 @@ export type NormalizedVariable = {
22
22
  key: string;
23
23
  label: string;
24
24
  value: string;
25
+ order?: number;
25
26
  };
@@ -0,0 +1,5 @@
1
+ export function filterBySearch< T extends { label: string } >( variables: T[], searchValue: string ): T[] {
2
+ const lowerSearchValue = searchValue.toLowerCase();
3
+
4
+ return variables.filter( ( variable ) => variable.label.toLowerCase().includes( lowerSearchValue ) );
5
+ }
@@ -1,3 +1,5 @@
1
+ import { getMixpanel } from '@elementor/mixpanel';
2
+
1
3
  type VariableEventData = {
2
4
  varType: string;
3
5
  controlPath: string;
@@ -5,35 +7,48 @@ type VariableEventData = {
5
7
  };
6
8
 
7
9
  export const trackVariableEvent = ( { varType, controlPath, action }: VariableEventData ) => {
8
- const extendedWindow = window as unknown as Window & {
9
- elementorCommon?: {
10
- eventsManager?: {
11
- dispatchEvent: ( name: string, data: Record< string, string > ) => void;
12
- config?: {
13
- locations: Record< string, string >;
14
- secondaryLocations: Record< string, string >;
15
- names: {
16
- variables?: Record< string, string >;
17
- };
18
- triggers: Record< string, string >;
19
- elements?: Record< string, string >;
20
- };
21
- };
22
- };
23
- };
24
-
25
- const config = extendedWindow?.elementorCommon?.eventsManager?.config;
10
+ const { dispatchEvent, config } = getMixpanel();
26
11
  if ( ! config?.names?.variables?.[ action ] ) {
27
12
  return;
28
13
  }
29
14
 
30
15
  const name = config.names.variables[ action ];
31
- extendedWindow.elementorCommon?.eventsManager?.dispatchEvent( name, {
32
- location: config.locations.variables,
33
- secondaryLocation: config.secondaryLocations.variablesPopover,
34
- trigger: config.triggers.click,
16
+ dispatchEvent?.( name, {
17
+ location: config?.locations?.variables || '',
18
+ secondaryLocation: config?.secondaryLocations?.variablesPopover || '',
19
+ trigger: config?.triggers?.click || '',
35
20
  var_type: varType,
36
21
  control_path: controlPath,
37
22
  action_type: name,
38
23
  } );
39
24
  };
25
+
26
+ type VariablesManagerEventData = {
27
+ action: 'openManager' | 'add' | 'saveChanges' | 'delete';
28
+ varType?: string;
29
+ controlPath?: string;
30
+ };
31
+
32
+ export const trackVariablesManagerEvent = ( { action, varType, controlPath }: VariablesManagerEventData ) => {
33
+ const { dispatchEvent, config } = getMixpanel();
34
+ if ( ! config?.names?.variables?.[ action ] ) {
35
+ return;
36
+ }
37
+
38
+ const name = config.names.variables[ action ];
39
+ const eventData: Record< string, string > = {
40
+ location: config?.locations?.variablesManager || '',
41
+ trigger: config?.triggers?.click || '',
42
+ action_type: name,
43
+ };
44
+
45
+ if ( varType ) {
46
+ eventData.var_type = varType;
47
+ }
48
+
49
+ if ( controlPath ) {
50
+ eventData.style_control_path = controlPath;
51
+ }
52
+
53
+ dispatchEvent?.( name, eventData );
54
+ };
@@ -1,4 +1,8 @@
1
- import { __ } from '@wordpress/i18n';
1
+ import type * as React from 'react';
2
+ import { AlertTriangleFilledIcon, InfoCircleFilledIcon } from '@elementor/icons';
3
+ import { __, sprintf } from '@wordpress/i18n';
4
+
5
+ import { type TVariable, type TVariablesList } from '../storage';
2
6
 
3
7
  export const ERROR_MESSAGES = {
4
8
  MISSING_VARIABLE_NAME: __( 'Give your variable a name.', 'elementor' ),
@@ -8,21 +12,49 @@ export const ERROR_MESSAGES = {
8
12
  VARIABLE_LABEL_MAX_LENGTH: __( 'Keep names up to 50 characters.', 'elementor' ),
9
13
  DUPLICATED_LABEL: __( 'This variable name already exists. Please choose a unique name.', 'elementor' ),
10
14
  UNEXPECTED_ERROR: __( 'There was a glitch. Try saving your variable again.', 'elementor' ),
15
+ BATCH: {
16
+ DUPLICATED_LABELS: ( count: number, name: string ) =>
17
+ // eslint-disable-next-line @wordpress/i18n-translator-comments
18
+ sprintf( __( 'We found %1$d duplicated %2$s.', 'elementor' ), count, name ),
19
+ UNEXPECTED_ERROR: __( 'There was a glitch.', 'elementor' ),
20
+ DUPLICATED_LABEL_ACTION: __( 'Take me there', 'elementor' ),
21
+ DUPLICATED_LABEL_ACTION_MESSAGE: __( 'Please rename the variables.', 'elementor' ),
22
+ UNEXPECTED_ERROR_ACTION_MESSAGE: __( 'Try saving your variables again.', 'elementor' ),
23
+ },
11
24
  } as const;
12
25
 
13
26
  export const VARIABLE_LABEL_MAX_LENGTH = 50;
14
27
 
15
- type ErrorResponse = {
28
+ type BatchErrorData = {
29
+ [ id: string ]: {
30
+ status?: number;
31
+ message?: string;
32
+ };
33
+ };
34
+ export type ErrorResponse = {
16
35
  response?: {
17
36
  data?: {
18
37
  code?: string;
38
+ data?: BatchErrorData;
19
39
  };
20
40
  };
21
41
  };
22
42
 
43
+ export type ErrorAction = {
44
+ label?: string;
45
+ message?: string;
46
+ callback?: () => void;
47
+ data?: {
48
+ duplicatedIds?: string[];
49
+ };
50
+ };
51
+
23
52
  export type MappedError = {
24
53
  field: string;
25
54
  message: string;
55
+ action?: ErrorAction;
56
+ severity?: 'error' | 'secondary';
57
+ IconComponent?: React.ElementType;
26
58
  };
27
59
 
28
60
  export const mapServerError = ( error: ErrorResponse ): MappedError | undefined => {
@@ -33,10 +65,43 @@ export const mapServerError = ( error: ErrorResponse ): MappedError | undefined
33
65
  };
34
66
  }
35
67
 
68
+ if ( error?.response?.data?.code === 'batch_duplicated_label' ) {
69
+ const errorData = error?.response?.data?.data ?? {};
70
+ const count = Object.keys( errorData ).length;
71
+ const name = count === 1 ? 'name' : 'names';
72
+ const duplicatedIds = Object.keys( errorData );
73
+
74
+ return {
75
+ field: 'label',
76
+ message: ERROR_MESSAGES.BATCH.DUPLICATED_LABELS( count, name ),
77
+ severity: 'error',
78
+ IconComponent: AlertTriangleFilledIcon,
79
+ action: {
80
+ label: ERROR_MESSAGES.BATCH.DUPLICATED_LABEL_ACTION,
81
+ message: ERROR_MESSAGES.BATCH.DUPLICATED_LABEL_ACTION_MESSAGE,
82
+ data: {
83
+ duplicatedIds,
84
+ },
85
+ },
86
+ };
87
+ }
88
+
89
+ if ( error?.response?.data?.code === 'batch_operation_failed' ) {
90
+ return {
91
+ field: 'label',
92
+ message: ERROR_MESSAGES.BATCH.UNEXPECTED_ERROR,
93
+ severity: 'secondary',
94
+ IconComponent: InfoCircleFilledIcon,
95
+ action: {
96
+ message: ERROR_MESSAGES.BATCH.UNEXPECTED_ERROR_ACTION_MESSAGE,
97
+ },
98
+ };
99
+ }
100
+
36
101
  return undefined;
37
102
  };
38
103
 
39
- export const validateLabel = ( name: string ): string => {
104
+ export const validateLabel = ( name: string, variables?: TVariablesList ): string => {
40
105
  if ( ! name.trim() ) {
41
106
  return ERROR_MESSAGES.MISSING_VARIABLE_NAME;
42
107
  }
@@ -55,6 +120,10 @@ export const validateLabel = ( name: string ): string => {
55
120
  return ERROR_MESSAGES.VARIABLE_LABEL_MAX_LENGTH;
56
121
  }
57
122
 
123
+ if ( Object.values( variables ?? {} ).some( ( variable: TVariable ) => variable.label === name ) ) {
124
+ return ERROR_MESSAGES.DUPLICATED_LABEL;
125
+ }
126
+
58
127
  return '';
59
128
  };
60
129
 
@@ -19,6 +19,7 @@ export type ValueFieldProps = {
19
19
  onChange: ( value: string ) => void;
20
20
  onValidationChange?: ( value: string ) => void;
21
21
  propType?: PropType;
22
+ error?: { value: string; message: string };
22
23
  };
23
24
 
24
25
  type FallbackPropTypeUtil = ReturnType< typeof createPropUtils >;
@@ -26,8 +27,9 @@ type FallbackPropTypeUtil = ReturnType< typeof createPropUtils >;
26
27
  type VariableTypeOptions = {
27
28
  icon: ForwardRefExoticComponent< Omit< SvgIconProps, 'ref' > & RefAttributes< SVGSVGElement > >;
28
29
  startIcon?: ( { value }: { value: string } ) => JSX.Element;
29
- valueField: ( { value, onChange, onValidationChange, propType }: ValueFieldProps ) => JSX.Element;
30
+ valueField: ( { value, onChange, onValidationChange, propType, error }: ValueFieldProps ) => JSX.Element;
30
31
  variableType: string;
32
+ defaultValue?: string;
31
33
  fallbackPropTypeUtil: FallbackPropTypeUtil;
32
34
  propTypeUtil: PropTypeUtil< string, string >;
33
35
  selectionFilter?: ( variables: NormalizedVariable[], propType: PropType ) => NormalizedVariable[];
@@ -46,6 +48,7 @@ export function createVariableTypeRegistry() {
46
48
  valueField,
47
49
  propTypeUtil,
48
50
  variableType,
51
+ defaultValue,
49
52
  selectionFilter,
50
53
  valueTransformer,
51
54
  fallbackPropTypeUtil,
@@ -72,6 +75,7 @@ export function createVariableTypeRegistry() {
72
75
  valueField,
73
76
  propTypeUtil,
74
77
  variableType,
78
+ defaultValue,
75
79
  selectionFilter,
76
80
  valueTransformer,
77
81
  fallbackPropTypeUtil,
@@ -94,6 +98,10 @@ export function createVariableTypeRegistry() {
94
98
  return variableTypes[ key ];
95
99
  };
96
100
 
101
+ const getVariableTypes = () => {
102
+ return variableTypes;
103
+ };
104
+
97
105
  const hasVariableType = ( key: string ) => {
98
106
  return key in variableTypes;
99
107
  };
@@ -101,6 +109,7 @@ export function createVariableTypeRegistry() {
101
109
  return {
102
110
  registerVariableType,
103
111
  getVariableType,
112
+ getVariableTypes,
104
113
  hasVariableType,
105
114
  };
106
115
  }
@@ -1,3 +1,4 @@
1
1
  import { createVariableTypeRegistry } from './create-variable-type-registry';
2
2
 
3
- export const { registerVariableType, getVariableType, hasVariableType } = createVariableTypeRegistry();
3
+ export const { registerVariableType, getVariableType, getVariableTypes, hasVariableType } =
4
+ createVariableTypeRegistry();
@@ -1,37 +0,0 @@
1
- import * as React from 'react';
2
- import { AlertTriangleFilledIcon } from '@elementor/icons';
3
- import { Box, Chip, type ChipProps, type Theme, Tooltip, Typography } from '@elementor/ui';
4
- import { __ } from '@wordpress/i18n';
5
-
6
- export const DeletedTag = React.forwardRef< HTMLDivElement, ChipProps >( ( { label, onClick, ...props }, ref ) => {
7
- return (
8
- <Chip
9
- ref={ ref }
10
- size="tiny"
11
- color="warning"
12
- shape="rounded"
13
- variant="standard"
14
- onClick={ onClick }
15
- icon={ <AlertTriangleFilledIcon /> }
16
- label={
17
- <Tooltip title={ label } placement="top">
18
- <Box sx={ { display: 'flex', gap: 0.5, alignItems: 'center' } }>
19
- <Typography variant="caption" noWrap>
20
- { label }
21
- </Typography>
22
- <Typography variant="caption" noWrap sx={ { textOverflow: 'initial', overflow: 'visible' } }>
23
- ({ __( 'deleted', 'elementor' ) })
24
- </Typography>
25
- </Box>
26
- </Tooltip>
27
- }
28
- sx={ {
29
- height: ( theme: Theme ) => theme.spacing( 3.5 ),
30
- borderRadius: ( theme: Theme ) => theme.spacing( 1 ),
31
- justifyContent: 'flex-start',
32
- width: '100%',
33
- } }
34
- { ...props }
35
- />
36
- );
37
- } );
@@ -1,37 +0,0 @@
1
- import * as React from 'react';
2
- import { AlertTriangleFilledIcon } from '@elementor/icons';
3
- import { Box, Chip, type ChipProps, type Theme, Tooltip, Typography } from '@elementor/ui';
4
- import { __ } from '@wordpress/i18n';
5
-
6
- export const MismatchTag = React.forwardRef< HTMLDivElement, ChipProps >( ( { label, onClick, ...props }, ref ) => {
7
- return (
8
- <Chip
9
- ref={ ref }
10
- size="tiny"
11
- color="warning"
12
- shape="rounded"
13
- variant="standard"
14
- onClick={ onClick }
15
- icon={ <AlertTriangleFilledIcon /> }
16
- label={
17
- <Tooltip title={ label } placement="top">
18
- <Box sx={ { display: 'flex', gap: 0.5, alignItems: 'center' } }>
19
- <Typography variant="caption" noWrap>
20
- { label }
21
- </Typography>
22
- <Typography variant="caption" noWrap sx={ { textOverflow: 'initial', overflow: 'visible' } }>
23
- ({ __( 'changed', 'elementor' ) })
24
- </Typography>
25
- </Box>
26
- </Tooltip>
27
- }
28
- sx={ {
29
- height: ( theme: Theme ) => theme.spacing( 3.5 ),
30
- borderRadius: ( theme: Theme ) => theme.spacing( 1 ),
31
- justifyContent: 'flex-start',
32
- width: '100%',
33
- } }
34
- { ...props }
35
- />
36
- );
37
- } );
@@ -1,25 +0,0 @@
1
- import * as React from 'react';
2
- import { AlertTriangleFilledIcon } from '@elementor/icons';
3
- import { Chip, type ChipProps, type Theme } from '@elementor/ui';
4
-
5
- export const MissingTag = React.forwardRef< HTMLDivElement, ChipProps >( ( { label, onClick, ...props }, ref ) => {
6
- return (
7
- <Chip
8
- ref={ ref }
9
- size="tiny"
10
- color="warning"
11
- shape="rounded"
12
- variant="standard"
13
- onClick={ onClick }
14
- icon={ <AlertTriangleFilledIcon /> }
15
- label={ label }
16
- sx={ {
17
- height: ( theme: Theme ) => theme.spacing( 3.5 ),
18
- borderRadius: ( theme: Theme ) => theme.spacing( 1 ),
19
- justifyContent: 'flex-start',
20
- width: '100%',
21
- } }
22
- { ...props }
23
- />
24
- );
25
- } );