@elementor/editor-components 3.35.0-410 → 3.35.0-412

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-components",
3
3
  "description": "Elementor editor components",
4
- "version": "3.35.0-410",
4
+ "version": "3.35.0-412",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,30 +40,30 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor": "3.35.0-410",
44
- "@elementor/editor-canvas": "3.35.0-410",
45
- "@elementor/editor-controls": "3.35.0-410",
46
- "@elementor/editor-documents": "3.35.0-410",
47
- "@elementor/editor-editing-panel": "3.35.0-410",
48
- "@elementor/editor-elements": "3.35.0-410",
49
- "@elementor/editor-elements-panel": "3.35.0-410",
50
- "@elementor/editor-mcp": "3.35.0-410",
51
- "@elementor/editor-panels": "3.35.0-410",
52
- "@elementor/editor-props": "3.35.0-410",
53
- "@elementor/editor-styles-repository": "3.35.0-410",
54
- "@elementor/editor-ui": "3.35.0-410",
55
- "@elementor/editor-v1-adapters": "3.35.0-410",
56
- "@elementor/http-client": "3.35.0-410",
43
+ "@elementor/editor": "3.35.0-412",
44
+ "@elementor/editor-canvas": "3.35.0-412",
45
+ "@elementor/editor-controls": "3.35.0-412",
46
+ "@elementor/editor-documents": "3.35.0-412",
47
+ "@elementor/editor-editing-panel": "3.35.0-412",
48
+ "@elementor/editor-elements": "3.35.0-412",
49
+ "@elementor/editor-elements-panel": "3.35.0-412",
50
+ "@elementor/editor-mcp": "3.35.0-412",
51
+ "@elementor/editor-panels": "3.35.0-412",
52
+ "@elementor/editor-props": "3.35.0-412",
53
+ "@elementor/editor-styles-repository": "3.35.0-412",
54
+ "@elementor/editor-ui": "3.35.0-412",
55
+ "@elementor/editor-v1-adapters": "3.35.0-412",
56
+ "@elementor/http-client": "3.35.0-412",
57
57
  "@elementor/icons": "^1.63.0",
58
- "@elementor/mixpanel": "3.35.0-410",
59
- "@elementor/query": "3.35.0-410",
60
- "@elementor/schema": "3.35.0-410",
61
- "@elementor/store": "3.35.0-410",
58
+ "@elementor/mixpanel": "3.35.0-412",
59
+ "@elementor/query": "3.35.0-412",
60
+ "@elementor/schema": "3.35.0-412",
61
+ "@elementor/store": "3.35.0-412",
62
62
  "@elementor/ui": "1.36.17",
63
- "@elementor/utils": "3.35.0-410",
63
+ "@elementor/utils": "3.35.0-412",
64
64
  "@wordpress/i18n": "^5.13.0",
65
- "@elementor/editor-notifications": "3.35.0-410",
66
- "@elementor/editor-current-user": "3.35.0-410"
65
+ "@elementor/editor-notifications": "3.35.0-412",
66
+ "@elementor/editor-current-user": "3.35.0-412"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "react": "^18.3.1",
package/src/api.ts CHANGED
@@ -2,7 +2,12 @@ import { type V1ElementData } from '@elementor/editor-elements';
2
2
  import { ajax } from '@elementor/editor-v1-adapters';
3
3
  import { type HttpResponse, httpService } from '@elementor/http-client';
4
4
 
5
- import { type DocumentSaveStatus, type OverridableProps, type PublishedComponent } from './types';
5
+ import {
6
+ type DocumentSaveStatus,
7
+ type OverridableProps,
8
+ type PublishedComponent,
9
+ type UpdatedComponentName,
10
+ } from './types';
6
11
 
7
12
  const BASE_URL = 'elementor/v1/components';
8
13
 
@@ -104,6 +109,15 @@ export const apiClient = {
104
109
  }
105
110
  )
106
111
  .then( ( res ) => res.data.data ),
112
+ updateComponentTitle: ( updatedComponentNames: UpdatedComponentName[] ) =>
113
+ httpService()
114
+ .post< { data: { failedIds: number[]; successIds: number[]; success: boolean } } >(
115
+ `${ BASE_URL }/update-titles`,
116
+ {
117
+ components: updatedComponentNames,
118
+ }
119
+ )
120
+ .then( ( res ) => res.data.data ),
107
121
  validate: async ( payload: ValidateComponentsPayload ) =>
108
122
  await httpService()
109
123
  .post< HttpResponse< ValidateComponentsResponse > >( `${ BASE_URL }/create-validate`, payload )
@@ -2,11 +2,12 @@ import * as React from 'react';
2
2
  import { useSuppressedMessage } from '@elementor/editor-current-user';
3
3
  import { getV1DocumentsManager } from '@elementor/editor-documents';
4
4
  import { ArrowLeftIcon, ComponentsFilledIcon } from '@elementor/icons';
5
+ import { __getState as getState } from '@elementor/store';
5
6
  import { Box, Divider, IconButton, Stack, Tooltip, Typography } from '@elementor/ui';
6
7
  import { __ } from '@wordpress/i18n';
7
8
 
8
9
  import { useNavigateBack } from '../../hooks/use-navigate-back';
9
- import { useCurrentComponentId } from '../../store/store';
10
+ import { type ComponentsSlice, SLICE_NAME, useCurrentComponentId } from '../../store/store';
10
11
  import { usePanelActions } from '../component-properties-panel/component-properties-panel';
11
12
  import { ComponentIntroduction } from '../components-tab/component-introduction';
12
13
  import { ComponentsBadge } from './component-badge';
@@ -69,7 +70,15 @@ export const ComponentPanelHeader = () => {
69
70
  );
70
71
  };
71
72
 
72
- function getComponentName() {
73
+ function getComponentName(): string {
74
+ const state = getState() as ComponentsSlice;
75
+ const path = state[ SLICE_NAME ].path;
76
+ const { instanceTitle } = path.at( -1 ) ?? {};
77
+
78
+ if ( instanceTitle ) {
79
+ return instanceTitle;
80
+ }
81
+
73
82
  const documentsManager = getV1DocumentsManager();
74
83
  const currentDocument = documentsManager.getCurrent();
75
84
 
@@ -1,7 +1,8 @@
1
1
  import * as React from 'react';
2
+ import { useRef } from 'react';
2
3
  import { endDragElementFromPanel, startDragElementFromPanel } from '@elementor/editor-canvas';
3
4
  import { dropElement, type DropElementParams, type V1ElementData } from '@elementor/editor-elements';
4
- import { EllipsisWithTooltip, MenuListItem } from '@elementor/editor-ui';
5
+ import { EditableField, EllipsisWithTooltip, MenuListItem, useEditable, WarningInfotip } from '@elementor/editor-ui';
5
6
  import { ComponentsIcon, DotsVerticalIcon } from '@elementor/icons';
6
7
  import {
7
8
  bindMenu,
@@ -12,6 +13,8 @@ import {
12
13
  ListItemIcon,
13
14
  Menu,
14
15
  Stack,
16
+ styled,
17
+ type Theme,
15
18
  Typography,
16
19
  usePopupState,
17
20
  } from '@elementor/ui';
@@ -20,14 +23,29 @@ import { __ } from '@wordpress/i18n';
20
23
  import { archiveComponent } from '../../store/actions/archive-component';
21
24
  import { loadComponentsAssets } from '../../store/actions/load-components-assets';
22
25
  import { type Component } from '../../types';
26
+ import { validateComponentName } from '../../utils/component-name-validation';
23
27
  import { getContainerForNewElement } from '../../utils/get-container-for-new-element';
24
28
  import { createComponentModel } from '../create-component-form/utils/replace-element-with-component';
25
29
 
26
30
  type ComponentItemProps = {
27
31
  component: Omit< Component, 'id' > & { id?: number };
32
+ renameComponent: ( newName: string ) => void;
28
33
  };
29
34
 
30
- export const ComponentItem = ( { component }: ComponentItemProps ) => {
35
+ export const ComponentItem = ( { component, renameComponent }: ComponentItemProps ) => {
36
+ const itemRef = useRef< HTMLElement >( null );
37
+
38
+ const {
39
+ ref: editableRef,
40
+ isEditing,
41
+ openEditMode,
42
+ error,
43
+ getProps: getEditableProps,
44
+ } = useEditable( {
45
+ value: component.name,
46
+ onSubmit: renameComponent,
47
+ validation: validateComponentTitle,
48
+ } );
31
49
  const componentModel = createComponentModel( component );
32
50
 
33
51
  const popupState = usePopupState( {
@@ -57,48 +75,66 @@ export const ComponentItem = ( { component }: ComponentItemProps ) => {
57
75
 
58
76
  return (
59
77
  <Stack>
60
- <ListItemButton
61
- draggable
62
- onDragStart={ ( event: React.DragEvent ) => startDragElementFromPanel( componentModel, event ) }
63
- onDragEnd={ handleDragEnd }
64
- shape="rounded"
65
- sx={ {
66
- border: 'solid 1px',
67
- borderColor: 'divider',
68
- py: 0.5,
69
- px: 1,
70
- display: 'flex',
71
- width: '100%',
72
- alignItems: 'center',
73
- gap: 1,
74
- } }
78
+ <WarningInfotip
79
+ open={ Boolean( error ) }
80
+ text={ error ?? '' }
81
+ placement="bottom"
82
+ width={ itemRef.current?.getBoundingClientRect().width }
83
+ offset={ [ 0, -15 ] }
75
84
  >
76
- <Box
77
- onClick={ handleClick }
85
+ <ListItemButton
86
+ draggable
87
+ onDragStart={ ( event: React.DragEvent ) => startDragElementFromPanel( componentModel, event ) }
88
+ onDragEnd={ handleDragEnd }
89
+ shape="rounded"
90
+ ref={ itemRef }
78
91
  sx={ {
92
+ border: 'solid 1px',
93
+ borderColor: 'divider',
94
+ py: 0.5,
95
+ px: 1,
79
96
  display: 'flex',
97
+ width: '100%',
80
98
  alignItems: 'center',
81
99
  gap: 1,
82
- minWidth: 0,
83
- flexGrow: 1,
84
100
  } }
85
101
  >
86
- <ListItemIcon size="tiny">
87
- <ComponentsIcon fontSize="tiny" />
88
- </ListItemIcon>
89
- <Box display="flex" flex={ 1 } minWidth={ 0 } flexGrow={ 1 }>
90
- <EllipsisWithTooltip
91
- title={ component.name }
92
- as={ Typography }
93
- variant="caption"
94
- color="text.primary"
95
- />
102
+ <Box
103
+ display="flex"
104
+ alignItems="center"
105
+ gap={ 1 }
106
+ minWidth={ 0 }
107
+ flexGrow={ 1 }
108
+ onClick={ handleClick }
109
+ >
110
+ <ListItemIcon size="tiny">
111
+ <ComponentsIcon fontSize="tiny" />
112
+ </ListItemIcon>
113
+ <Indicator isActive={ isEditing } isError={ !! error }>
114
+ <Box display="flex" flex={ 1 } minWidth={ 0 } flexGrow={ 1 }>
115
+ { isEditing ? (
116
+ <EditableField
117
+ ref={ editableRef }
118
+ as={ Typography }
119
+ variant="caption"
120
+ { ...getEditableProps() }
121
+ />
122
+ ) : (
123
+ <EllipsisWithTooltip
124
+ title={ component.name }
125
+ as={ Typography }
126
+ variant="caption"
127
+ color="text.primary"
128
+ />
129
+ ) }
130
+ </Box>
131
+ </Indicator>
96
132
  </Box>
97
- </Box>
98
- <IconButton size="tiny" { ...bindTrigger( popupState ) } aria-label="More actions">
99
- <DotsVerticalIcon fontSize="tiny" />
100
- </IconButton>
101
- </ListItemButton>
133
+ <IconButton size="tiny" { ...bindTrigger( popupState ) } aria-label="More actions">
134
+ <DotsVerticalIcon fontSize="tiny" />
135
+ </IconButton>
136
+ </ListItemButton>
137
+ </WarningInfotip>
102
138
  <Menu
103
139
  { ...bindMenu( popupState ) }
104
140
  anchorOrigin={ {
@@ -110,8 +146,19 @@ export const ComponentItem = ( { component }: ComponentItemProps ) => {
110
146
  horizontal: 'right',
111
147
  } }
112
148
  >
149
+ <MenuListItem
150
+ sx={ { minWidth: '160px' } }
151
+ onClick={ () => {
152
+ popupState.close();
153
+ openEditMode();
154
+ } }
155
+ >
156
+ { __( 'Rename', 'elementor' ) }
157
+ </MenuListItem>
113
158
  <MenuListItem sx={ { minWidth: '160px' } } onClick={ handleArchiveClick }>
114
- { __( 'Archive', 'elementor' ) }
159
+ <Typography variant="caption" sx={ { color: 'error.light' } }>
160
+ { __( 'Archive', 'elementor' ) }
161
+ </Typography>
115
162
  </MenuListItem>
116
163
  </Menu>
117
164
  </Stack>
@@ -133,3 +180,38 @@ const addComponentToPage = ( model: DropElementParams[ 'model' ] ) => {
133
180
  options: { ...options, useHistory: false, scrollIntoView: true },
134
181
  } );
135
182
  };
183
+
184
+ const validateComponentTitle = ( newTitle: string ) => {
185
+ const result = validateComponentName( newTitle );
186
+
187
+ if ( ! result.errorMessage ) {
188
+ return null;
189
+ }
190
+
191
+ return result.errorMessage;
192
+ };
193
+
194
+ const Indicator = styled( Box, {
195
+ shouldForwardProp: ( prop ) => prop !== 'isActive' && prop !== 'isError',
196
+ } )( ( { theme, isActive, isError } ) => ( {
197
+ display: 'flex',
198
+ width: '100%',
199
+ flexGrow: 1,
200
+ borderRadius: theme.spacing( 0.5 ),
201
+ border: getIndicatorBorder( { isActive, isError, theme } ),
202
+ padding: `0 ${ theme.spacing( 1 ) }`,
203
+ marginLeft: isActive ? theme.spacing( 1 ) : 0,
204
+ minWidth: 0,
205
+ } ) );
206
+
207
+ const getIndicatorBorder = ( { isActive, isError, theme }: { isActive: boolean; isError: boolean; theme: Theme } ) => {
208
+ if ( isError ) {
209
+ return `2px solid ${ theme.palette.error.main }`;
210
+ }
211
+
212
+ if ( isActive ) {
213
+ return `2px solid ${ theme.palette.secondary.main }`;
214
+ }
215
+
216
+ return 'none';
217
+ };
@@ -4,6 +4,7 @@ import { Box, Divider, Icon, Link, List, Stack, Typography } from '@elementor/ui
4
4
  import { __ } from '@wordpress/i18n';
5
5
 
6
6
  import { useComponents } from '../../hooks/use-components';
7
+ import { renameComponent } from '../../store/actions/rename-component';
7
8
  import { ComponentItem } from './components-item';
8
9
  import { LoadingComponents } from './loading-components';
9
10
  import { useSearch } from './search-provider';
@@ -25,7 +26,13 @@ export function ComponentsList() {
25
26
  return (
26
27
  <List sx={ { display: 'flex', flexDirection: 'column', gap: 1, px: 2 } }>
27
28
  { components.map( ( component ) => (
28
- <ComponentItem key={ component.uid } component={ component } />
29
+ <ComponentItem
30
+ key={ component.uid }
31
+ component={ component }
32
+ renameComponent={ ( newName ) => {
33
+ renameComponent( component.uid, newName );
34
+ } }
35
+ />
29
36
  ) ) }
30
37
  </List>
31
38
  );
@@ -1,21 +1,19 @@
1
1
  import { z } from '@elementor/schema';
2
2
  import { __ } from '@wordpress/i18n';
3
3
 
4
- const MIN_NAME_LENGTH = 2;
4
+ export const MIN_NAME_LENGTH = 2;
5
5
  const MAX_NAME_LENGTH = 50;
6
6
 
7
+ const baseComponentSchema = z
8
+ .string()
9
+ .trim()
10
+ .max( MAX_NAME_LENGTH, __( 'Component name is too long. Please keep it under 50 characters.', 'elementor' ) );
11
+
7
12
  export const createBaseComponentSchema = ( existingNames: string[] ) => {
8
13
  return z.object( {
9
- componentName: z
10
- .string()
11
- .trim()
12
- .max(
13
- MAX_NAME_LENGTH,
14
- __( 'Component name is too long. Please keep it under 50 characters.', 'elementor' )
15
- )
16
- .refine( ( value ) => ! existingNames.includes( value ), {
17
- message: __( 'Component name already exists', 'elementor' ),
18
- } ),
14
+ componentName: baseComponentSchema.refine( ( value ) => ! existingNames.includes( value ), {
15
+ message: __( 'Component name already exists', 'elementor' ),
16
+ } ),
19
17
  } );
20
18
  };
21
19
 
@@ -34,7 +34,6 @@ export const createComponentModel = ( component: ComponentInstanceParams ): Omit
34
34
  overridable_props: component.overridableProps,
35
35
  },
36
36
  editor_settings: {
37
- title: component.name,
38
37
  component_uid: component.uid,
39
38
  },
40
39
  };
@@ -73,15 +73,45 @@ function getUpdatedComponentPath( path: ComponentsPathItem[], nextDocument: V1Do
73
73
  return path.slice( 0, componentIndex + 1 );
74
74
  }
75
75
 
76
+ const instanceId = nextDocument?.container.view?.el?.dataset.id;
77
+ const instanceTitle = getInstanceTitle( instanceId, path );
78
+
76
79
  return [
77
80
  ...path,
78
81
  {
79
- instanceId: nextDocument?.container.view?.el?.dataset.id,
82
+ instanceId,
83
+ instanceTitle,
80
84
  componentId: nextDocument.id,
81
85
  },
82
86
  ];
83
87
  }
84
88
 
89
+ function getInstanceTitle( instanceId: string | undefined, path: ComponentsPathItem[] ): string | undefined {
90
+ if ( ! instanceId ) {
91
+ return undefined;
92
+ }
93
+
94
+ const documentsManager = getV1DocumentsManager();
95
+ const parentDocId = path.at( -1 )?.componentId ?? documentsManager.getInitialId();
96
+ const parentDoc = documentsManager.get( parentDocId );
97
+
98
+ type EditorSettings = { title?: string };
99
+ type ContainerWithChildren = V1Element & {
100
+ children?: {
101
+ findRecursive?: ( predicate: ( child: V1Element ) => boolean ) => V1Element | undefined;
102
+ };
103
+ };
104
+
105
+ const parentContainer = parentDoc?.container as unknown as ContainerWithChildren | undefined;
106
+ const widget = parentContainer?.children?.findRecursive?.(
107
+ ( container: V1Element ) => container.id === instanceId
108
+ );
109
+
110
+ const editorSettings = widget?.model?.get?.( 'editor_settings' ) as EditorSettings | undefined;
111
+
112
+ return editorSettings?.title;
113
+ }
114
+
85
115
  function getComponentDOMElement( id: V1Document[ 'id' ] | undefined ) {
86
116
  if ( ! id ) {
87
117
  return null;
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  type BackboneModel,
3
+ type BackboneModelConstructor,
3
4
  type CreateTemplatedElementTypeOptions,
4
5
  createTemplatedElementView,
5
6
  type ElementModel,
@@ -8,10 +9,12 @@ import {
8
9
  type LegacyWindow,
9
10
  } from '@elementor/editor-canvas';
10
11
  import { getCurrentDocument } from '@elementor/editor-documents';
12
+ import { __getState as getState } from '@elementor/store';
11
13
  import { __ } from '@wordpress/i18n';
12
14
 
13
15
  import { apiClient } from './api';
14
16
  import { type ComponentInstanceProp } from './prop-types/component-instance-prop-type';
17
+ import { type ComponentsSlice, selectComponentByUid } from './store/store';
15
18
  import { type ExtendedWindow } from './types';
16
19
  import { switchToComponent } from './utils/switch-to-component';
17
20
  import { trackComponentEvent } from './utils/tracking';
@@ -36,6 +39,18 @@ type ContextMenuGroup = {
36
39
  actions: ContextMenuAction[];
37
40
  };
38
41
 
42
+ type ComponentModel = ElementModel & {
43
+ componentId?: number | string;
44
+ };
45
+
46
+ type ComponentModelInstance = BackboneModel< ComponentModel > & {
47
+ trigger: ( event: string, ...args: unknown[] ) => void;
48
+ getTitle: () => string;
49
+ getComponentId: () => number | null;
50
+ getComponentName: () => string;
51
+ getComponentUid: () => string | null;
52
+ };
53
+
39
54
  export const COMPONENT_WIDGET_TYPE = 'e-component';
40
55
 
41
56
  const updateGroups = ( groups: ContextMenuGroup[], config: ContextMenuGroupConfig ): ContextMenuGroup[] => {
@@ -64,8 +79,9 @@ export function createComponentType(
64
79
  options: CreateTemplatedElementTypeOptions & { showLockedByModal?: ( lockedBy: string ) => void }
65
80
  ): typeof ElementType {
66
81
  const legacyWindow = window as unknown as LegacyWindow;
82
+ const WidgetType = legacyWindow.elementor.modules.elements.types.Widget;
67
83
 
68
- return class extends legacyWindow.elementor.modules.elements.types.Widget {
84
+ return class extends WidgetType {
69
85
  getType() {
70
86
  return options.type;
71
87
  }
@@ -73,6 +89,10 @@ export function createComponentType(
73
89
  getView() {
74
90
  return createComponentView( { ...options } );
75
91
  }
92
+
93
+ getModel(): BackboneModelConstructor< ComponentModel > {
94
+ return createComponentModel();
95
+ }
76
96
  };
77
97
  }
78
98
 
@@ -253,3 +273,67 @@ function setInactiveRecursively( model: BackboneModel< ElementModel > ) {
253
273
  } );
254
274
  }
255
275
  }
276
+
277
+ function createComponentModel(): BackboneModelConstructor< ComponentModel > {
278
+ const legacyWindow = window as unknown as LegacyWindow;
279
+ const WidgetType = legacyWindow.elementor.modules.elements.types.Widget;
280
+ const widgetTypeInstance = new WidgetType() as unknown as BackboneModelConstructor< ElementModel >;
281
+ const BaseWidgetModel = widgetTypeInstance.getModel();
282
+
283
+ return BaseWidgetModel.extend( {
284
+ initialize( this: ComponentModelInstance, attributes: unknown, options: unknown ): void {
285
+ BaseWidgetModel.prototype.initialize.call( this, attributes, options );
286
+
287
+ const componentInstance = this.get( 'settings' )?.get( 'component_instance' ) as
288
+ | ComponentInstanceProp
289
+ | undefined;
290
+ if ( componentInstance?.value ) {
291
+ const componentId = componentInstance.value.component_id?.value;
292
+ if ( componentId && typeof componentId === 'number' ) {
293
+ this.set( 'componentId', componentId );
294
+ }
295
+ }
296
+ },
297
+
298
+ getTitle( this: ComponentModelInstance ): string {
299
+ const editorSettings = this.get( 'editor_settings' ) as
300
+ | {
301
+ title?: string;
302
+ component_uid?: string;
303
+ }
304
+ | undefined;
305
+
306
+ const instanceTitle = editorSettings?.title;
307
+ if ( instanceTitle ) {
308
+ return instanceTitle;
309
+ }
310
+
311
+ const componentUid = editorSettings?.component_uid;
312
+ if ( componentUid ) {
313
+ const component = selectComponentByUid( getState() as ComponentsSlice, componentUid );
314
+ if ( component?.name ) {
315
+ return component.name;
316
+ }
317
+ }
318
+
319
+ return ( window as unknown as LegacyWindow ).elementor.getElementData( this ).title;
320
+ },
321
+
322
+ getComponentId( this: ComponentModelInstance ): number | null {
323
+ return ( this.get( 'componentId' ) as number | undefined ) || null;
324
+ },
325
+
326
+ getComponentName( this: ComponentModelInstance ): string {
327
+ return this.getTitle();
328
+ },
329
+
330
+ getComponentUid( this: ComponentModelInstance ): string | null {
331
+ const editorSettings = this.get( 'editor_settings' ) as
332
+ | {
333
+ component_uid?: string;
334
+ }
335
+ | undefined;
336
+ return editorSettings?.component_uid || null;
337
+ },
338
+ } );
339
+ }
@@ -0,0 +1,7 @@
1
+ import { __dispatch as dispatch } from '@elementor/store';
2
+
3
+ import { slice } from '../store';
4
+
5
+ export const renameComponent = ( componentUid: string, newName: string ) => {
6
+ dispatch( slice.actions.rename( { componentUid, name: newName } ) );
7
+ };
@@ -30,12 +30,14 @@ type ComponentsState = {
30
30
  archivedData: PublishedComponent[];
31
31
  path: ComponentsPathItem[];
32
32
  currentComponentId: V1Document[ 'id' ] | null;
33
+ updatedComponentNames: Record< number, string >;
33
34
  };
34
35
 
35
36
  export type ComponentsSlice = SliceState< typeof slice >;
36
37
 
37
38
  export type ComponentsPathItem = {
38
39
  instanceId?: string;
40
+ instanceTitle?: string;
39
41
  componentId: V1Document[ 'id' ];
40
42
  };
41
43
 
@@ -48,6 +50,7 @@ export const initialState: ComponentsState = {
48
50
  archivedData: [],
49
51
  path: [],
50
52
  currentComponentId: null,
53
+ updatedComponentNames: {},
51
54
  };
52
55
 
53
56
  export const SLICE_NAME = 'components';
@@ -111,6 +114,20 @@ export const slice = createSlice( {
111
114
 
112
115
  component.overridableProps = payload.overridableProps;
113
116
  },
117
+ rename: ( state, { payload }: PayloadAction< { componentUid: string; name: string } > ) => {
118
+ const component = state.data.find( ( comp ) => comp.uid === payload.componentUid );
119
+
120
+ if ( ! component ) {
121
+ return;
122
+ }
123
+ if ( component.id ) {
124
+ state.updatedComponentNames[ component.id ] = payload.name;
125
+ }
126
+ component.name = payload.name;
127
+ },
128
+ cleanUpdatedComponentNames: ( state ) => {
129
+ state.updatedComponentNames = {};
130
+ },
114
131
  },
115
132
  extraReducers: ( builder ) => {
116
133
  builder.addCase( loadComponents.fulfilled, ( state, { payload }: PayloadAction< GetComponentResponse > ) => {
@@ -140,6 +157,10 @@ export const useComponent = ( componentId: ComponentId | null ) => {
140
157
  return useSelector( ( state: ComponentsSlice ) => ( componentId ? selectComponent( state, componentId ) : null ) );
141
158
  };
142
159
 
160
+ export const selectComponentByUid = ( state: ComponentsSlice, componentUid: string ) =>
161
+ state[ SLICE_NAME ].data.find( ( component ) => component.uid === componentUid ) ??
162
+ state[ SLICE_NAME ].unpublishedData.find( ( component ) => component.uid === componentUid );
163
+
143
164
  export const selectComponents = createSelector(
144
165
  selectData,
145
166
  selectUnpublishedData,
@@ -209,3 +230,11 @@ export const selectArchivedComponents = createSelector(
209
230
  selectArchivedData,
210
231
  ( archivedData: PublishedComponent[] ) => archivedData
211
232
  );
233
+ export const selectUpdatedComponentNames = createSelector(
234
+ ( state: ComponentsSlice ) => state[ SLICE_NAME ].updatedComponentNames,
235
+ ( updatedComponentNames ) =>
236
+ Object.entries( updatedComponentNames ).map( ( [ componentId, title ] ) => ( {
237
+ componentId: Number( componentId ),
238
+ title,
239
+ } ) )
240
+ );
@@ -5,6 +5,7 @@ import { type DocumentSaveStatus } from '../types';
5
5
  import { createComponentsBeforeSave } from './create-components-before-save';
6
6
  import { setComponentOverridablePropsSettingsBeforeSave } from './set-component-overridable-props-settings-before-save';
7
7
  import { updateArchivedComponentBeforeSave } from './update-archived-component-before-save';
8
+ import { updateComponentTitleBeforeSave } from './update-component-title-before-save';
8
9
  import { updateComponentsBeforeSave } from './update-components-before-save';
9
10
 
10
11
  type Options = {
@@ -27,5 +28,6 @@ export const beforeSave = ( { container, status }: Options ) => {
27
28
  createComponentsBeforeSave( { elements, status } ),
28
29
  updateComponentsBeforeSave( { elements, status } ),
29
30
  setComponentOverridablePropsSettingsBeforeSave( { container } ),
31
+ updateComponentTitleBeforeSave(),
30
32
  ] );
31
33
  };
@@ -0,0 +1,18 @@
1
+ import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
2
+
3
+ import { apiClient } from '../api';
4
+ import { selectUpdatedComponentNames, slice } from '../store/store';
5
+
6
+ export const updateComponentTitleBeforeSave = async () => {
7
+ const updatedComponentNames = selectUpdatedComponentNames( getState() );
8
+
9
+ if ( ! updatedComponentNames.length ) {
10
+ return;
11
+ }
12
+
13
+ const result = await apiClient.updateComponentTitle( updatedComponentNames );
14
+
15
+ if ( result.failedIds.length === 0 ) {
16
+ dispatch( slice.actions.cleanUpdatedComponentNames() );
17
+ }
18
+ };