@elementor/editor-global-classes 3.33.0-116 → 3.33.0-118

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-global-classes",
3
- "version": "3.33.0-116",
3
+ "version": "3.33.0-118",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -39,22 +39,22 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "3.33.0-116",
43
- "@elementor/editor-current-user": "3.33.0-116",
44
- "@elementor/editor-documents": "3.33.0-116",
45
- "@elementor/editor-editing-panel": "3.33.0-116",
46
- "@elementor/editor-panels": "3.33.0-116",
47
- "@elementor/editor-props": "3.33.0-116",
48
- "@elementor/editor-styles": "3.33.0-116",
49
- "@elementor/editor-styles-repository": "3.33.0-116",
50
- "@elementor/editor-ui": "3.33.0-116",
51
- "@elementor/editor-v1-adapters": "3.33.0-116",
52
- "@elementor/http-client": "3.33.0-116",
42
+ "@elementor/editor": "3.33.0-118",
43
+ "@elementor/editor-current-user": "3.33.0-118",
44
+ "@elementor/editor-documents": "3.33.0-118",
45
+ "@elementor/editor-editing-panel": "3.33.0-118",
46
+ "@elementor/editor-panels": "3.33.0-118",
47
+ "@elementor/editor-props": "3.33.0-118",
48
+ "@elementor/editor-styles": "3.33.0-118",
49
+ "@elementor/editor-styles-repository": "3.33.0-118",
50
+ "@elementor/editor-ui": "3.33.0-118",
51
+ "@elementor/editor-v1-adapters": "3.33.0-118",
52
+ "@elementor/http-client": "3.33.0-118",
53
53
  "@elementor/icons": "1.46.0",
54
- "@elementor/query": "3.33.0-116",
55
- "@elementor/store": "3.33.0-116",
54
+ "@elementor/query": "3.33.0-118",
55
+ "@elementor/store": "3.33.0-118",
56
56
  "@elementor/ui": "1.36.12",
57
- "@elementor/utils": "3.33.0-116",
57
+ "@elementor/utils": "3.33.0-118",
58
58
  "@wordpress/i18n": "^5.13.0"
59
59
  },
60
60
  "peerDependencies": {
package/src/api.ts CHANGED
@@ -49,3 +49,7 @@ export const apiClient = {
49
49
  },
50
50
  } ),
51
51
  };
52
+
53
+ export const API_ERROR_CODES = {
54
+ DUPLICATED_LABEL: 'DUPLICATED_LABEL',
55
+ };
@@ -0,0 +1,159 @@
1
+ import * as React from 'react';
2
+ import { closeDialog, EllipsisWithTooltip } from '@elementor/editor-ui';
3
+ import { InfoCircleFilledIcon } from '@elementor/icons';
4
+ import {
5
+ Alert,
6
+ Box,
7
+ Button,
8
+ DialogActions,
9
+ DialogContent,
10
+ DialogHeader,
11
+ Divider,
12
+ Icon,
13
+ Stack,
14
+ Typography,
15
+ } from '@elementor/ui';
16
+ import { __ } from '@wordpress/i18n';
17
+
18
+ import { type ModifiedLabels } from '../../store';
19
+
20
+ const DUP_PREFIX = 'DUP_';
21
+
22
+ export const DuplicateLabelDialog = ( {
23
+ modifiedLabels,
24
+ onApprove,
25
+ }: {
26
+ modifiedLabels: ModifiedLabels;
27
+ onApprove?: () => void;
28
+ } ) => {
29
+ const handleButtonClick = () => {
30
+ localStorage.setItem( 'elementor-global-classes-search', DUP_PREFIX );
31
+ onApprove?.();
32
+ closeDialog();
33
+ };
34
+
35
+ return (
36
+ <>
37
+ <DialogHeader logo={ false }>
38
+ <Box display="flex" alignItems="center" gap={ 1 }>
39
+ <Icon color="secondary">
40
+ <InfoCircleFilledIcon fontSize="medium" />
41
+ </Icon>
42
+ <Typography variant="subtitle1">
43
+ { __( "We've published your page and updated class names.", 'elementor' ) }
44
+ </Typography>
45
+ </Box>
46
+ </DialogHeader>
47
+ <DialogContent>
48
+ <Stack spacing={ 2 } direction="column">
49
+ <Typography variant="body2">
50
+ { __(
51
+ 'Some new classes used the same names as existing ones. To prevent conflicts, we added the prefix',
52
+ 'elementor'
53
+ ) }
54
+ <strong> { DUP_PREFIX }</strong>
55
+ </Typography>
56
+
57
+ <Box>
58
+ <Box
59
+ sx={ {
60
+ width: '100%',
61
+ display: 'flex',
62
+ gap: 2,
63
+ alignItems: 'flex-start',
64
+ } }
65
+ >
66
+ <Typography
67
+ variant="subtitle2"
68
+ sx={ {
69
+ fontWeight: 'bold',
70
+ flex: 1,
71
+ flexShrink: 1,
72
+ flexGrow: 1,
73
+ minWidth: 0,
74
+ } }
75
+ >
76
+ { __( 'Before', 'elementor' ) }
77
+ </Typography>
78
+ <Typography
79
+ variant="subtitle2"
80
+ sx={ {
81
+ minWidth: '200px',
82
+ fontWeight: 'bold',
83
+ flexShrink: 0,
84
+ flexGrow: 0,
85
+ width: '200px',
86
+ maxWidth: '200px',
87
+ } }
88
+ >
89
+ { __( 'After', 'elementor' ) }
90
+ </Typography>
91
+ </Box>
92
+ <Divider sx={ { mt: 0.5, mb: 0.5 } } />
93
+ <Stack direction="column" gap={ 0.5 } sx={ { pb: 2 } }>
94
+ { Object.values( modifiedLabels ).map( ( { original, modified }, index ) => (
95
+ <Box
96
+ key={ index }
97
+ sx={ {
98
+ width: '100%',
99
+ display: 'flex',
100
+ gap: 2,
101
+ alignItems: 'flex-start',
102
+ } }
103
+ >
104
+ <Box
105
+ sx={ {
106
+ flex: 1,
107
+ flexShrink: 1,
108
+ flexGrow: 1,
109
+ minWidth: 0,
110
+ } }
111
+ >
112
+ <EllipsisWithTooltip title={ original }>
113
+ <Typography variant="body2" sx={ { color: 'text.secondary' } }>
114
+ { original }
115
+ </Typography>
116
+ </EllipsisWithTooltip>
117
+ </Box>
118
+ <Box
119
+ sx={ {
120
+ minWidth: '200px',
121
+ flexShrink: 0,
122
+ flexGrow: 0,
123
+ width: '200px',
124
+ maxWidth: '200px',
125
+ } }
126
+ >
127
+ <EllipsisWithTooltip title={ modified }>
128
+ <Typography variant="body2" sx={ { color: 'text.primary' } }>
129
+ { modified }
130
+ </Typography>
131
+ </EllipsisWithTooltip>
132
+ </Box>
133
+ </Box>
134
+ ) ) }
135
+ </Stack>
136
+ <Box>
137
+ <Alert severity="info" size="small" color="secondary">
138
+ <strong>{ __( 'Your designs and classes are safe.', 'elementor' ) }</strong>
139
+ { __(
140
+ 'Only the prefixes were added.Find them in Class Manager by searching',
141
+ 'elementor'
142
+ ) }
143
+ <strong>{ DUP_PREFIX }</strong>
144
+ </Alert>
145
+ </Box>
146
+ </Box>
147
+ </Stack>
148
+ </DialogContent>
149
+ <DialogActions>
150
+ <Button color="secondary" variant="text" onClick={ handleButtonClick }>
151
+ { __( 'Go to Class Manager', 'elementor' ) }
152
+ </Button>
153
+ <Button color="secondary" variant="contained" onClick={ closeDialog }>
154
+ { __( 'Done', 'elementor' ) }
155
+ </Button>
156
+ </DialogActions>
157
+ </>
158
+ );
159
+ };
@@ -35,9 +35,19 @@ const INIT_CHECKED_FILTERS: CheckedFilters = {
35
35
 
36
36
  export const SearchAndFilterProvider = ( { children }: React.PropsWithChildren ) => {
37
37
  const [ filters, setFilters ] = React.useState< CheckedFilters >( INIT_CHECKED_FILTERS );
38
+
39
+ const getInitialSearchValue = () => {
40
+ const storedValue = localStorage.getItem( 'elementor-global-classes-search' );
41
+ if ( storedValue ) {
42
+ localStorage.removeItem( 'elementor-global-classes-search' );
43
+ return storedValue;
44
+ }
45
+ return '';
46
+ };
47
+
38
48
  const { debouncedValue, inputValue, handleChange } = useDebounceState( {
39
49
  delay: 300,
40
- initialValue: '',
50
+ initialValue: getInitialSearchValue(),
41
51
  } );
42
52
 
43
53
  const onClearSearch = () => {
@@ -0,0 +1,8 @@
1
+ import { type StyleDefinitionID } from '@elementor/editor-styles';
2
+ import { __useSelector as useSelector, type SliceState } from '@elementor/store';
3
+
4
+ import { selectClass, type slice } from '../store';
5
+
6
+ export const useCssClassById = ( id: StyleDefinitionID ) => {
7
+ return useSelector( ( state: SliceState< typeof slice > ) => selectClass( state, id ) );
8
+ };
package/src/index.ts CHANGED
@@ -1,2 +1 @@
1
1
  export { init } from './init';
2
- export { ClearIconButton } from './components/search-and-filter/components/filter/clear-icon-button';
package/src/init.ts CHANGED
@@ -6,7 +6,6 @@ import {
6
6
  } from '@elementor/editor-editing-panel';
7
7
  import { __registerPanel as registerPanel } from '@elementor/editor-panels';
8
8
  import { stylesRepository } from '@elementor/editor-styles-repository';
9
- import { __privateListenTo as listenTo, v1ReadyEvent } from '@elementor/editor-v1-adapters';
10
9
  import { __registerSlice as registerSlice } from '@elementor/store';
11
10
 
12
11
  import { ClassManagerButton } from './components/class-manager/class-manager-button';
@@ -15,7 +14,7 @@ import { ConvertLocalClassToGlobalClass } from './components/convert-local-class
15
14
  import { PopulateStore } from './components/populate-store';
16
15
  import { GLOBAL_CLASSES_PROVIDER_KEY, globalClassesStylesProvider } from './global-classes-styles-provider';
17
16
  import { slice } from './store';
18
- import { syncWithDocumentSave } from './sync-with-document-save';
17
+ import { SyncWithDocumentSave } from './sync-with-document';
19
18
 
20
19
  export function init() {
21
20
  registerSlice( slice );
@@ -28,6 +27,11 @@ export function init() {
28
27
  component: PopulateStore,
29
28
  } );
30
29
 
30
+ injectIntoLogic( {
31
+ id: 'global-classes-sync-with-document',
32
+ component: SyncWithDocumentSave,
33
+ } );
34
+
31
35
  injectIntoCssClassConvert( {
32
36
  id: 'global-classes-convert-from-local-class',
33
37
  component: ConvertLocalClassToGlobalClass,
@@ -42,8 +46,4 @@ export function init() {
42
46
  name: 'global',
43
47
  getThemeColor: ( theme ) => theme.palette.global.dark,
44
48
  } );
45
-
46
- listenTo( v1ReadyEvent(), () => {
47
- syncWithDocumentSave();
48
- } );
49
49
  }
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import { openDialog } from '@elementor/editor-ui';
3
+ import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
4
+ import { hash } from '@elementor/utils';
5
+
6
+ import { API_ERROR_CODES, apiClient, type ApiContext } from './api';
7
+ import { DuplicateLabelDialog } from './components/class-manager/duplicate-label-dialog';
8
+ import { type GlobalClasses, selectData, selectFrontendInitialData, selectPreviewInitialData, slice } from './store';
9
+
10
+ type Options = {
11
+ context: ApiContext;
12
+ onApprove?: () => void;
13
+ };
14
+
15
+ export async function saveGlobalClasses( { context, onApprove }: Options ) {
16
+ const state = selectData( getState() );
17
+ const apiAction = context === 'preview' ? apiClient.saveDraft : apiClient.publish;
18
+ const currentContext = context === 'preview' ? selectPreviewInitialData : selectFrontendInitialData;
19
+ const response = await apiAction( {
20
+ items: state.items,
21
+ order: state.order,
22
+ changes: calculateChanges( state, currentContext( getState() ) ),
23
+ } );
24
+
25
+ dispatch( slice.actions.reset( { context } ) );
26
+ if ( response?.data?.data?.code === API_ERROR_CODES.DUPLICATED_LABEL ) {
27
+ dispatch( slice.actions.updateMultiple( response.data.data.modifiedLabels ) );
28
+ openDialog( {
29
+ component: (
30
+ <DuplicateLabelDialog
31
+ modifiedLabels={ response.data.data.modifiedLabels || [] }
32
+ onApprove={ onApprove }
33
+ />
34
+ ),
35
+ } );
36
+ }
37
+ }
38
+
39
+ function calculateChanges( state: GlobalClasses, initialData: GlobalClasses ) {
40
+ const stateIds = Object.keys( state.items );
41
+ const initialDataIds = Object.keys( initialData.items );
42
+
43
+ return {
44
+ added: stateIds.filter( ( id ) => ! initialDataIds.includes( id ) ),
45
+ deleted: initialDataIds.filter( ( id ) => ! stateIds.includes( id ) ),
46
+ modified: stateIds.filter( ( id ) => {
47
+ return id in initialData.items && hash( state.items[ id ] ) !== hash( initialData.items[ id ] );
48
+ } ),
49
+ };
50
+ }
package/src/store.ts CHANGED
@@ -32,6 +32,13 @@ type GlobalClassesState = {
32
32
  isDirty: boolean;
33
33
  };
34
34
 
35
+ export type ModifiedLabels = {
36
+ [ id: string ]: {
37
+ original: string;
38
+ modified: string;
39
+ };
40
+ };
41
+
35
42
  const localHistory = SnapshotHistory.get< GlobalClasses >( 'global-classes' );
36
43
 
37
44
  const initialState: GlobalClassesState = {
@@ -108,6 +115,14 @@ export const slice = createSlice( {
108
115
  state.isDirty = true;
109
116
  },
110
117
 
118
+ updateMultiple( state, { payload }: PayloadAction< ModifiedLabels > ) {
119
+ localHistory.next( state.data );
120
+ Object.entries( payload ).forEach( ( [ id, { modified } ] ) => {
121
+ state.data.items[ id ].label = modified;
122
+ } );
123
+
124
+ state.isDirty = false;
125
+ },
111
126
  updateProps(
112
127
  state,
113
128
  {
@@ -7,10 +7,10 @@ import { UPDATE_CLASS_CAPABILITY_KEY } from './capabilities';
7
7
  import { saveGlobalClasses } from './save-global-classes';
8
8
  import { selectIsDirty } from './store';
9
9
 
10
- export function syncWithDocumentSave() {
10
+ export function syncWithDocumentSave( panelActions?: { open: () => void } ) {
11
11
  const unsubscribe = syncDirtyState();
12
12
 
13
- bindSaveAction();
13
+ bindSaveAction( panelActions );
14
14
 
15
15
  return unsubscribe;
16
16
  }
@@ -25,7 +25,7 @@ function syncDirtyState() {
25
25
  } );
26
26
  }
27
27
 
28
- function bindSaveAction() {
28
+ function bindSaveAction( panelActions?: { open: () => void } ) {
29
29
  registerDataHook( 'after', 'document/save/save', ( args ) => {
30
30
  const user = getCurrentUser();
31
31
 
@@ -35,8 +35,9 @@ function bindSaveAction() {
35
35
  return;
36
36
  }
37
37
 
38
- return saveGlobalClasses( {
38
+ saveGlobalClasses( {
39
39
  context: args.status === 'publish' ? 'frontend' : 'preview',
40
+ onApprove: panelActions?.open,
40
41
  } );
41
42
  } );
42
43
  }
@@ -0,0 +1,20 @@
1
+ import { useEffect } from 'react';
2
+ import { __privateListenTo as listenTo, v1ReadyEvent } from '@elementor/editor-v1-adapters';
3
+
4
+ import { usePanelActions } from './components/class-manager/class-manager-panel';
5
+ import { syncWithDocumentSave } from './sync-with-document-save';
6
+
7
+ export function SyncWithDocumentSave() {
8
+ const panelActions = usePanelActions();
9
+
10
+ useEffect( () => {
11
+ listenTo( v1ReadyEvent(), () => {
12
+ syncWithDocumentSave( panelActions );
13
+ } );
14
+
15
+ // eslint-disable-next-line react-compiler/react-compiler
16
+ // eslint-disable-next-line react-hooks/exhaustive-deps
17
+ }, [] );
18
+
19
+ return null;
20
+ }
@@ -1,42 +0,0 @@
1
- import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
2
- import { hash } from '@elementor/utils';
3
-
4
- import { apiClient, type ApiContext } from './api';
5
- import { type GlobalClasses, selectData, selectFrontendInitialData, selectPreviewInitialData, slice } from './store';
6
-
7
- type Options = {
8
- context: ApiContext;
9
- };
10
-
11
- export async function saveGlobalClasses( { context }: Options ) {
12
- const state = selectData( getState() );
13
-
14
- if ( context === 'preview' ) {
15
- await apiClient.saveDraft( {
16
- items: state.items,
17
- order: state.order,
18
- changes: calculateChanges( state, selectPreviewInitialData( getState() ) ),
19
- } );
20
- } else {
21
- await apiClient.publish( {
22
- items: state.items,
23
- order: state.order,
24
- changes: calculateChanges( state, selectFrontendInitialData( getState() ) ),
25
- } );
26
- }
27
-
28
- dispatch( slice.actions.reset( { context } ) );
29
- }
30
-
31
- function calculateChanges( state: GlobalClasses, initialData: GlobalClasses ) {
32
- const stateIds = Object.keys( state.items );
33
- const initialDataIds = Object.keys( initialData.items );
34
-
35
- return {
36
- added: stateIds.filter( ( id ) => ! initialDataIds.includes( id ) ),
37
- deleted: initialDataIds.filter( ( id ) => ! stateIds.includes( id ) ),
38
- modified: stateIds.filter( ( id ) => {
39
- return id in initialData.items && hash( state.items[ id ] ) !== hash( initialData.items[ id ] );
40
- } ),
41
- };
42
- }