@elementor/editor-components 3.33.0-99 → 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 (53) hide show
  1. package/dist/index.js +1860 -123
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +1863 -110
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +21 -11
  6. package/src/api.ts +57 -11
  7. package/src/component-instance-transformer.ts +24 -0
  8. package/src/component-overridable-transformer.ts +28 -0
  9. package/src/components/components-tab/component-search.tsx +32 -0
  10. package/src/components/components-tab/components-item.tsx +67 -0
  11. package/src/components/components-tab/components-list.tsx +141 -0
  12. package/src/components/components-tab/components.tsx +17 -0
  13. package/src/components/components-tab/loading-components.tsx +43 -0
  14. package/src/components/components-tab/search-provider.tsx +38 -0
  15. package/src/components/consts.ts +1 -0
  16. package/src/components/create-component-form/create-component-form.tsx +109 -100
  17. package/src/components/create-component-form/utils/get-component-event-data.ts +54 -0
  18. package/src/components/create-component-form/utils/replace-element-with-component.ts +28 -10
  19. package/src/components/edit-component/component-modal.tsx +134 -0
  20. package/src/components/edit-component/edit-component.tsx +134 -0
  21. package/src/components/in-edit-mode.tsx +43 -0
  22. package/src/components/overridable-props/indicator.tsx +81 -0
  23. package/src/components/overridable-props/overridable-prop-form.tsx +98 -0
  24. package/src/components/overridable-props/overridable-prop-indicator.tsx +128 -0
  25. package/src/components/overridable-props/utils/get-overridable-prop.ts +20 -0
  26. package/src/create-component-type.ts +194 -0
  27. package/src/hooks/use-canvas-document.ts +6 -0
  28. package/src/hooks/use-components.ts +6 -9
  29. package/src/hooks/use-element-rect.ts +81 -0
  30. package/src/init.ts +82 -3
  31. package/src/mcp/index.ts +14 -0
  32. package/src/mcp/save-as-component-tool.ts +92 -0
  33. package/src/populate-store.ts +12 -0
  34. package/src/prop-types/component-overridable-prop-type.ts +17 -0
  35. package/src/store/actions.ts +21 -0
  36. package/src/store/components-styles-provider.ts +24 -0
  37. package/src/store/create-unpublished-component.ts +40 -0
  38. package/src/store/load-components-assets.ts +26 -0
  39. package/src/store/load-components-styles.ts +44 -0
  40. package/src/store/remove-component-styles.ts +9 -0
  41. package/src/store/set-overridable-prop.ts +161 -0
  42. package/src/store/store.ts +168 -0
  43. package/src/store/thunks.ts +10 -0
  44. package/src/sync/before-save.ts +15 -0
  45. package/src/sync/create-components-before-save.ts +108 -0
  46. package/src/sync/update-components-before-save.ts +36 -0
  47. package/src/types.ts +91 -0
  48. package/src/utils/component-document-data.ts +19 -0
  49. package/src/utils/get-component-ids.ts +36 -0
  50. package/src/utils/get-container-for-new-element.ts +49 -0
  51. package/src/utils/tracking.ts +47 -0
  52. package/src/components/components-tab.tsx +0 -6
  53. package/src/hooks/use-create-component.ts +0 -13
@@ -0,0 +1,21 @@
1
+ import { type V1Document } from '@elementor/editor-documents';
2
+ import { __getStore as getStore } from '@elementor/store';
3
+
4
+ import { type ComponentsPathItem, slice } from './store';
5
+
6
+ export function updateCurrentComponent( {
7
+ path,
8
+ currentComponentId,
9
+ }: {
10
+ path: ComponentsPathItem[];
11
+ currentComponentId: V1Document[ 'id' ] | null;
12
+ } ) {
13
+ const dispatch = getStore()?.dispatch;
14
+
15
+ if ( ! dispatch ) {
16
+ return;
17
+ }
18
+
19
+ dispatch( slice.actions.setPath( path ) );
20
+ dispatch( slice.actions.setCurrentComponentId( currentComponentId ) );
21
+ }
@@ -0,0 +1,24 @@
1
+ import { createStylesProvider } from '@elementor/editor-styles-repository';
2
+ import { __getState as getState, __subscribeWithSelector as subscribeWithSelector } from '@elementor/store';
3
+
4
+ import { selectFlatStyles, SLICE_NAME } from './store';
5
+
6
+ export const componentsStylesProvider = createStylesProvider( {
7
+ key: 'components-styles',
8
+ priority: 100,
9
+ subscribe: ( cb ) =>
10
+ subscribeWithSelector(
11
+ ( state ) => state[ SLICE_NAME ],
12
+ () => {
13
+ cb();
14
+ }
15
+ ),
16
+ actions: {
17
+ all: () => {
18
+ return selectFlatStyles( getState() );
19
+ },
20
+ get: ( id ) => {
21
+ return selectFlatStyles( getState() ).find( ( style ) => style.id === id ) ?? null;
22
+ },
23
+ },
24
+ } );
@@ -0,0 +1,40 @@
1
+ import { type V1ElementData } from '@elementor/editor-elements';
2
+ import { __privateRunCommand as runCommand } from '@elementor/editor-v1-adapters';
3
+ import { __dispatch as dispatch } from '@elementor/store';
4
+ import { generateUniqueId } from '@elementor/utils';
5
+
6
+ import { type ComponentEventData } from '../components/create-component-form/utils/get-component-event-data';
7
+ import { replaceElementWithComponent } from '../components/create-component-form/utils/replace-element-with-component';
8
+ import { trackComponentEvent } from '../utils/tracking';
9
+ import { slice } from './store';
10
+
11
+ export function createUnpublishedComponent(
12
+ name: string,
13
+ element: V1ElementData,
14
+ eventData: ComponentEventData | null
15
+ ) {
16
+ const uid = generateUniqueId( 'component' );
17
+ const componentBase = { uid, name };
18
+
19
+ dispatch(
20
+ slice.actions.addUnpublished( {
21
+ ...componentBase,
22
+ elements: [ element ],
23
+ } )
24
+ );
25
+
26
+ dispatch( slice.actions.addCreatedThisSession( uid ) );
27
+
28
+ replaceElementWithComponent( element, componentBase );
29
+
30
+ trackComponentEvent( {
31
+ action: 'created',
32
+ component_uid: uid,
33
+ component_name: name,
34
+ ...eventData,
35
+ } );
36
+
37
+ runCommand( 'document/save/auto' );
38
+
39
+ return uid;
40
+ }
@@ -0,0 +1,26 @@
1
+ import { isDocumentDirty, setDocumentModifiedStatus } from '@elementor/editor-documents';
2
+ import { type V1ElementData } from '@elementor/editor-elements';
3
+
4
+ import { getComponentDocumentData } from '../utils/component-document-data';
5
+ import { getComponentIds } from '../utils/get-component-ids';
6
+ import { loadComponentsStyles } from './load-components-styles';
7
+
8
+ export async function loadComponentsAssets( elements: V1ElementData[] ) {
9
+ const componentIds = await getComponentIds( elements );
10
+
11
+ updateDocumentState( componentIds );
12
+
13
+ return loadComponentsStyles( componentIds );
14
+ }
15
+
16
+ async function updateDocumentState( componentIds: number[] ) {
17
+ const components = ( await Promise.all( componentIds.map( getComponentDocumentData ) ) ).filter(
18
+ ( document ) => !! document
19
+ );
20
+
21
+ const isDrafted = components.some( isDocumentDirty );
22
+
23
+ if ( isDrafted ) {
24
+ setDocumentModifiedStatus( true );
25
+ }
26
+ }
@@ -0,0 +1,44 @@
1
+ import { type V1ElementData } from '@elementor/editor-elements';
2
+ import { type StyleDefinition } from '@elementor/editor-styles';
3
+ import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
4
+
5
+ import { apiClient } from '../api';
6
+ import { type ComponentId } from '../types';
7
+ import { selectStyles, slice } from './store';
8
+
9
+ export async function loadComponentsStyles( componentIds: number[] ) {
10
+ if ( ! componentIds.length ) {
11
+ return;
12
+ }
13
+
14
+ const knownComponents = selectStyles( getState() );
15
+ const unknownComponentIds = componentIds.filter( ( id ) => ! knownComponents[ id ] );
16
+
17
+ if ( ! unknownComponentIds.length ) {
18
+ return;
19
+ }
20
+
21
+ addComponentStyles( unknownComponentIds );
22
+ }
23
+
24
+ async function addComponentStyles( ids: ComponentId[] ) {
25
+ const newComponents = await loadStyles( ids );
26
+
27
+ addStyles( newComponents );
28
+ }
29
+
30
+ async function loadStyles( ids: number[] ): Promise< [ number, V1ElementData ][] > {
31
+ return Promise.all( ids.map( async ( id ) => [ id, await apiClient.getComponentConfig( id ) ] ) );
32
+ }
33
+
34
+ function addStyles( data: ( readonly [ ComponentId, V1ElementData ] )[] ) {
35
+ const styles = Object.fromEntries(
36
+ data.map( ( [ componentId, componentData ] ) => [ componentId, extractStyles( componentData ) ] )
37
+ );
38
+
39
+ dispatch( slice.actions.addStyles( styles ) );
40
+ }
41
+
42
+ function extractStyles( element: V1ElementData ): Array< StyleDefinition > {
43
+ return [ ...Object.values( element.styles ?? {} ), ...( element.elements ?? [] ).flatMap( extractStyles ) ];
44
+ }
@@ -0,0 +1,9 @@
1
+ import { __dispatch as dispatch } from '@elementor/store';
2
+
3
+ import { apiClient } from '../api';
4
+ import { slice } from './store';
5
+
6
+ export function removeComponentStyles( id: number ) {
7
+ apiClient.invalidateComponentConfigCache( id );
8
+ dispatch( slice.actions.removeStyles( { id } ) );
9
+ }
@@ -0,0 +1,161 @@
1
+ import { type PropValue } from '@elementor/editor-props';
2
+ import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
3
+ import { generateUniqueId } from '@elementor/utils';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { type OverridableProp, type OverridableProps, type OverridablePropsGroup } from '../types';
7
+ import { selectOverridableProps, slice } from './store';
8
+
9
+ type Props = {
10
+ componentId: number;
11
+ overrideKey: string | null;
12
+ elementId: string;
13
+ label: string;
14
+ groupId: string | null;
15
+ propKey: string;
16
+ elType: string;
17
+ widgetType: string;
18
+ originValue: PropValue;
19
+ };
20
+ export function setOverridableProp( {
21
+ componentId,
22
+ overrideKey,
23
+ elementId,
24
+ label,
25
+ groupId,
26
+ propKey,
27
+ elType,
28
+ widgetType,
29
+ originValue,
30
+ }: Props ): OverridableProp | undefined {
31
+ const overridableProps = selectOverridableProps( getState(), componentId );
32
+
33
+ if ( ! overridableProps ) {
34
+ return;
35
+ }
36
+
37
+ const existingOverridableProp = overrideKey ? overridableProps.props[ overrideKey ] : null;
38
+
39
+ const { props: existingProps, groups: existingGroups } = { ...overridableProps };
40
+
41
+ const { groups: updatedGroups, currentGroupId } = getUpdatedGroups(
42
+ existingGroups,
43
+ groupId || existingOverridableProp?.groupId
44
+ );
45
+
46
+ const overridableProp = {
47
+ overrideKey: existingOverridableProp?.overrideKey || generateUniqueId( 'prop' ),
48
+ label,
49
+ elementId,
50
+ propKey,
51
+ widgetType,
52
+ elType,
53
+ originValue,
54
+ groupId: currentGroupId,
55
+ };
56
+
57
+ const props = {
58
+ ...existingProps,
59
+ [ overridableProp.overrideKey ]: overridableProp,
60
+ };
61
+
62
+ const groups = {
63
+ items: {
64
+ ...updatedGroups.items,
65
+ [ currentGroupId ]: getGroupWithProp( updatedGroups, currentGroupId, overridableProp ),
66
+ },
67
+ order: updatedGroups.order.includes( currentGroupId )
68
+ ? updatedGroups.order
69
+ : [ ...updatedGroups.order, currentGroupId ],
70
+ };
71
+
72
+ const isChangingGroups = existingOverridableProp && existingOverridableProp.groupId !== currentGroupId;
73
+
74
+ if ( isChangingGroups ) {
75
+ groups.items[ existingOverridableProp.groupId ] = getGroupWithoutProp(
76
+ updatedGroups,
77
+ existingOverridableProp.groupId,
78
+ overridableProp
79
+ );
80
+ }
81
+
82
+ dispatch(
83
+ slice.actions.setOverridableProps( {
84
+ componentId,
85
+ overridableProps: {
86
+ props,
87
+ groups,
88
+ },
89
+ } )
90
+ );
91
+
92
+ return overridableProp;
93
+ }
94
+
95
+ type UpdatedGroups = { groups: OverridableProps[ 'groups' ]; currentGroupId: string };
96
+
97
+ function getUpdatedGroups( groups: OverridableProps[ 'groups' ], groupId: string | undefined ): UpdatedGroups {
98
+ if ( ! groupId ) {
99
+ // use first existing group
100
+ if ( groups.order.length > 0 ) {
101
+ return { groups, currentGroupId: groups.order[ 0 ] };
102
+ }
103
+
104
+ // create the first group (default)
105
+ return addNewGroup( groups );
106
+ }
107
+
108
+ if ( ! groups.items[ groupId ] ) {
109
+ // fallback - if for any reason there's no such group - create it
110
+ return addNewGroup( groups, groupId );
111
+ }
112
+
113
+ // use the existing group
114
+ return { groups, currentGroupId: groupId };
115
+ }
116
+
117
+ function addNewGroup( groups: OverridableProps[ 'groups' ], groupId?: string | undefined ): UpdatedGroups {
118
+ const currentGroupId = groupId || generateUniqueId( 'group' );
119
+ const updatedGroups = {
120
+ ...groups,
121
+ items: {
122
+ ...groups.items,
123
+ [ currentGroupId ]: {
124
+ id: currentGroupId,
125
+ label: __( 'Default', 'elementor' ),
126
+ props: [],
127
+ },
128
+ },
129
+ order: [ ...groups.order, currentGroupId ],
130
+ };
131
+
132
+ return { groups: updatedGroups, currentGroupId };
133
+ }
134
+
135
+ function getGroupWithProp(
136
+ groups: OverridableProps[ 'groups' ],
137
+ groupId: string,
138
+ overridableProp: OverridableProp
139
+ ): OverridablePropsGroup {
140
+ const group: OverridablePropsGroup = { ...groups.items[ groupId ] };
141
+
142
+ if ( ! group.props.includes( overridableProp.overrideKey ) ) {
143
+ group.props = [ ...group.props, overridableProp.overrideKey ];
144
+ }
145
+
146
+ return group;
147
+ }
148
+
149
+ function getGroupWithoutProp(
150
+ groups: OverridableProps[ 'groups' ],
151
+ groupId: string,
152
+ overridableProp: OverridableProp
153
+ ): OverridablePropsGroup {
154
+ const group = { ...groups.items[ groupId ] };
155
+
156
+ if ( group ) {
157
+ group.props = group.props.filter( ( key ) => key !== overridableProp.overrideKey );
158
+ }
159
+
160
+ return group;
161
+ }
@@ -0,0 +1,168 @@
1
+ import { type V1Document } from '@elementor/editor-documents';
2
+ import {
3
+ __createSelector as createSelector,
4
+ __createSlice as createSlice,
5
+ type PayloadAction,
6
+ type SliceState,
7
+ } from '@elementor/store';
8
+
9
+ import {
10
+ type Component,
11
+ type ComponentId,
12
+ type OverridableProps,
13
+ type PublishedComponent,
14
+ type StylesDefinition,
15
+ type UnpublishedComponent,
16
+ } from '../types';
17
+ import { loadComponents } from './thunks';
18
+
19
+ type GetComponentResponse = PublishedComponent[];
20
+
21
+ type Status = 'idle' | 'pending' | 'error';
22
+
23
+ type ComponentsState = {
24
+ data: PublishedComponent[];
25
+ unpublishedData: UnpublishedComponent[];
26
+ loadStatus: Status;
27
+ styles: StylesDefinition;
28
+ createdThisSession: Component[ 'uid' ][];
29
+ path: ComponentsPathItem[];
30
+ currentComponentId: V1Document[ 'id' ] | null;
31
+ };
32
+
33
+ type ComponentsSlice = SliceState< typeof slice >;
34
+
35
+ export type ComponentsPathItem = {
36
+ instanceId?: string;
37
+ componentId: V1Document[ 'id' ];
38
+ };
39
+
40
+ export const initialState: ComponentsState = {
41
+ data: [],
42
+ unpublishedData: [],
43
+ loadStatus: 'idle',
44
+ styles: {},
45
+ createdThisSession: [],
46
+ path: [],
47
+ currentComponentId: null,
48
+ };
49
+
50
+ export const SLICE_NAME = 'components';
51
+
52
+ export const slice = createSlice( {
53
+ name: SLICE_NAME,
54
+ initialState,
55
+ reducers: {
56
+ add: ( state, { payload }: PayloadAction< PublishedComponent | PublishedComponent[] > ) => {
57
+ if ( Array.isArray( payload ) ) {
58
+ state.data = [ ...state.data, ...payload ];
59
+ } else {
60
+ state.data.unshift( payload );
61
+ }
62
+ },
63
+ load: ( state, { payload }: PayloadAction< PublishedComponent[] > ) => {
64
+ state.data = payload;
65
+ },
66
+ addUnpublished: ( state, { payload }: PayloadAction< UnpublishedComponent > ) => {
67
+ state.unpublishedData.unshift( payload );
68
+ },
69
+ resetUnpublished: ( state ) => {
70
+ state.unpublishedData = [];
71
+ },
72
+ removeStyles( state, { payload }: PayloadAction< { id: ComponentId } > ) {
73
+ const { [ payload.id ]: _, ...rest } = state.styles;
74
+
75
+ state.styles = rest;
76
+ },
77
+ addStyles: ( state, { payload } ) => {
78
+ state.styles = { ...state.styles, ...payload };
79
+ },
80
+ addCreatedThisSession: ( state, { payload }: PayloadAction< string > ) => {
81
+ state.createdThisSession.push( payload );
82
+ },
83
+ setCurrentComponentId: ( state, { payload }: PayloadAction< V1Document[ 'id' ] | null > ) => {
84
+ state.currentComponentId = payload;
85
+ },
86
+ setPath: ( state, { payload }: PayloadAction< ComponentsPathItem[] > ) => {
87
+ state.path = payload;
88
+ },
89
+ setOverridableProps: (
90
+ state,
91
+ { payload }: PayloadAction< { componentId: ComponentId; overridableProps: OverridableProps } >
92
+ ) => {
93
+ const component = state.data.find( ( comp ) => comp.id === payload.componentId );
94
+
95
+ if ( ! component ) {
96
+ return;
97
+ }
98
+
99
+ component.overridableProps = payload.overridableProps;
100
+ },
101
+ },
102
+ extraReducers: ( builder ) => {
103
+ builder.addCase( loadComponents.fulfilled, ( state, { payload }: PayloadAction< GetComponentResponse > ) => {
104
+ state.data = payload;
105
+ state.loadStatus = 'idle';
106
+ } );
107
+ builder.addCase( loadComponents.pending, ( state ) => {
108
+ state.loadStatus = 'pending';
109
+ } );
110
+ builder.addCase( loadComponents.rejected, ( state ) => {
111
+ state.loadStatus = 'error';
112
+ } );
113
+ },
114
+ } );
115
+
116
+ const selectData = ( state: ComponentsSlice ) => state[ SLICE_NAME ].data;
117
+ const selectLoadStatus = ( state: ComponentsSlice ) => state[ SLICE_NAME ].loadStatus;
118
+ const selectStylesDefinitions = ( state: ComponentsSlice ) => state[ SLICE_NAME ].styles ?? {};
119
+ const selectUnpublishedData = ( state: ComponentsSlice ) => state[ SLICE_NAME ].unpublishedData;
120
+ const getCreatedThisSession = ( state: ComponentsSlice ) => state[ SLICE_NAME ].createdThisSession;
121
+ const getPath = ( state: ComponentsSlice ) => state[ SLICE_NAME ].path;
122
+ const getCurrentComponentId = ( state: ComponentsSlice ) => state[ SLICE_NAME ].currentComponentId;
123
+ const selectComponent = ( state: ComponentsSlice, componentId: ComponentId ) =>
124
+ state[ SLICE_NAME ].data.find( ( component ) => component.id === componentId );
125
+
126
+ export const selectComponents = createSelector(
127
+ selectData,
128
+ selectUnpublishedData,
129
+ ( data: PublishedComponent[], unpublishedData: UnpublishedComponent[] ) => [
130
+ ...unpublishedData.map( ( item ) => ( { uid: item.uid, name: item.name } ) ),
131
+ ...data,
132
+ ]
133
+ );
134
+ export const selectUnpublishedComponents = createSelector(
135
+ selectUnpublishedData,
136
+ ( unpublishedData: UnpublishedComponent[] ) => unpublishedData
137
+ );
138
+ export const selectLoadIsPending = createSelector( selectLoadStatus, ( status ) => status === 'pending' );
139
+ export const selectLoadIsError = createSelector( selectLoadStatus, ( status ) => status === 'error' );
140
+ export const selectStyles = ( state: ComponentsSlice ) => state[ SLICE_NAME ].styles ?? {};
141
+ export const selectFlatStyles = createSelector( selectStylesDefinitions, ( data ) => Object.values( data ).flat() );
142
+ export const selectCreatedThisSession = createSelector(
143
+ getCreatedThisSession,
144
+ ( createdThisSession ) => createdThisSession
145
+ );
146
+
147
+ const DEFAULT_OVERRIDABLE_PROPS: OverridableProps = {
148
+ props: {},
149
+ groups: {
150
+ items: {},
151
+ order: [],
152
+ },
153
+ };
154
+
155
+ export const selectOverridableProps = createSelector(
156
+ selectComponent,
157
+ ( component: PublishedComponent | undefined ) => {
158
+ if ( ! component ) {
159
+ return undefined;
160
+ }
161
+ return component.overridableProps ?? DEFAULT_OVERRIDABLE_PROPS;
162
+ }
163
+ );
164
+ export const selectPath = createSelector( getPath, ( path ) => path );
165
+ export const selectCurrentComponentId = createSelector(
166
+ getCurrentComponentId,
167
+ ( currentComponentId ) => currentComponentId
168
+ );
@@ -0,0 +1,10 @@
1
+ import { __createAsyncThunk as createAsyncThunk } from '@elementor/store';
2
+
3
+ import { apiClient } from '../api';
4
+
5
+ const loadComponents = createAsyncThunk( 'components/load', async () => {
6
+ const response = await apiClient.get();
7
+ return response;
8
+ } );
9
+
10
+ export { loadComponents };
@@ -0,0 +1,15 @@
1
+ import { type Container, type DocumentSaveStatus } from '../types';
2
+ import { createComponentsBeforeSave } from './create-components-before-save';
3
+ import { updateComponentsBeforeSave } from './update-components-before-save';
4
+
5
+ type Options = {
6
+ container: Container;
7
+ status: DocumentSaveStatus;
8
+ };
9
+
10
+ export const beforeSave = ( { container, status }: Options ) => {
11
+ return Promise.all( [
12
+ createComponentsBeforeSave( { container, status } ),
13
+ updateComponentsBeforeSave( { container, status } ),
14
+ ] );
15
+ };
@@ -0,0 +1,108 @@
1
+ import { updateElementSettings, type V1ElementData } from '@elementor/editor-elements';
2
+ import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
3
+
4
+ import { apiClient } from '../api';
5
+ import { selectUnpublishedComponents, slice } from '../store/store';
6
+ import {
7
+ type ComponentInstancePropValue,
8
+ type Container,
9
+ type DocumentSaveStatus,
10
+ type UnpublishedComponent,
11
+ } from '../types';
12
+
13
+ export async function createComponentsBeforeSave( {
14
+ container,
15
+ status,
16
+ }: {
17
+ container: Container;
18
+ status: DocumentSaveStatus;
19
+ } ) {
20
+ const unpublishedComponents = selectUnpublishedComponents( getState() );
21
+
22
+ if ( ! unpublishedComponents.length ) {
23
+ return;
24
+ }
25
+
26
+ try {
27
+ const uidToComponentId = await createComponents( unpublishedComponents, status );
28
+
29
+ const elements = container.model.get( 'elements' ).toJSON();
30
+ updateComponentInstances( elements, uidToComponentId );
31
+
32
+ dispatch(
33
+ slice.actions.add(
34
+ unpublishedComponents.map( ( component ) => ( {
35
+ id: uidToComponentId.get( component.uid ) as number,
36
+ name: component.name,
37
+ uid: component.uid,
38
+ } ) )
39
+ )
40
+ );
41
+ dispatch( slice.actions.resetUnpublished() );
42
+ } catch ( error ) {
43
+ throw new Error( `Failed to publish components and update component instances: ${ error }` );
44
+ }
45
+ }
46
+
47
+ async function createComponents(
48
+ components: UnpublishedComponent[],
49
+ status: DocumentSaveStatus
50
+ ): Promise< Map< string, number > > {
51
+ const response = await apiClient.create( {
52
+ status,
53
+ items: components.map( ( component ) => ( {
54
+ uid: component.uid,
55
+ title: component.name,
56
+ elements: component.elements,
57
+ } ) ),
58
+ } );
59
+
60
+ const map = new Map< string, number >();
61
+
62
+ Object.entries( response ).forEach( ( [ key, value ] ) => {
63
+ map.set( key, value );
64
+ } );
65
+
66
+ return map;
67
+ }
68
+
69
+ function updateComponentInstances( elements: V1ElementData[], uidToComponentId: Map< string, number > ): void {
70
+ elements.forEach( ( element ) => {
71
+ const { shouldUpdate, newComponentId } = shouldUpdateElement( element, uidToComponentId );
72
+ if ( shouldUpdate ) {
73
+ updateElementComponentId( element.id, newComponentId );
74
+ }
75
+
76
+ if ( element.elements ) {
77
+ updateComponentInstances( element.elements, uidToComponentId );
78
+ }
79
+ } );
80
+ }
81
+
82
+ function shouldUpdateElement(
83
+ element: V1ElementData,
84
+ uidToComponentId: Map< string, number >
85
+ ): { shouldUpdate: true; newComponentId: number } | { shouldUpdate: false; newComponentId: null } {
86
+ if ( element.widgetType === 'e-component' ) {
87
+ const currentComponentId = ( element.settings?.component_instance as ComponentInstancePropValue< string > )
88
+ ?.value?.component_id;
89
+
90
+ if ( currentComponentId && uidToComponentId.has( currentComponentId ) ) {
91
+ return { shouldUpdate: true, newComponentId: uidToComponentId.get( currentComponentId ) as number };
92
+ }
93
+ }
94
+ return { shouldUpdate: false, newComponentId: null };
95
+ }
96
+
97
+ function updateElementComponentId( elementId: string, componentId: number ): void {
98
+ updateElementSettings( {
99
+ id: elementId,
100
+ props: {
101
+ component_instance: {
102
+ $$type: 'component-instance',
103
+ value: { component_id: componentId },
104
+ },
105
+ },
106
+ withHistory: false,
107
+ } );
108
+ }
@@ -0,0 +1,36 @@
1
+ import { isDocumentDirty } from '@elementor/editor-documents';
2
+
3
+ import { apiClient } from '../api';
4
+ import { type Container, type DocumentSaveStatus } from '../types';
5
+ import { getComponentDocumentData, invalidateComponentDocumentData } from '../utils/component-document-data';
6
+ import { getComponentIds } from '../utils/get-component-ids';
7
+
8
+ type Options = {
9
+ status: DocumentSaveStatus;
10
+ container: Container;
11
+ };
12
+
13
+ export async function updateComponentsBeforeSave( { status, container }: Options ) {
14
+ if ( status !== 'publish' ) {
15
+ return;
16
+ }
17
+
18
+ const elements = container.model.get( 'elements' ).toJSON();
19
+
20
+ const componentIds = await getComponentIds( elements );
21
+
22
+ const componentDocumentData = await Promise.all( componentIds.map( getComponentDocumentData ) );
23
+
24
+ const draftIds = componentDocumentData
25
+ .filter( ( document ) => !! document )
26
+ .filter( isDocumentDirty )
27
+ .map( ( document ) => document.id );
28
+
29
+ if ( draftIds.length === 0 ) {
30
+ return;
31
+ }
32
+
33
+ await apiClient.updateStatuses( draftIds, 'publish' );
34
+
35
+ draftIds.forEach( ( id ) => invalidateComponentDocumentData( id ) );
36
+ }