@elementor/editor-global-classes 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.
- package/dist/index.js +1000 -430
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +940 -366
- package/dist/index.mjs.map +1 -1
- package/package.json +21 -18
- package/src/api.ts +4 -0
- package/src/components/class-manager/class-manager-button.tsx +15 -1
- package/src/components/class-manager/class-manager-panel.tsx +2 -2
- package/src/components/class-manager/delete-class.ts +9 -3
- package/src/components/class-manager/delete-confirmation-dialog.tsx +2 -2
- package/src/components/class-manager/duplicate-label-dialog.tsx +159 -0
- package/src/components/class-manager/global-classes-list.tsx +53 -22
- package/src/components/convert-local-class-to-global-class.tsx +7 -0
- package/src/components/css-class-usage/components/css-class-usage-popover.tsx +10 -1
- package/src/components/css-class-usage/components/css-class-usage-trigger.tsx +22 -7
- package/src/components/search-and-filter/components/filter/active-filters.tsx +8 -0
- package/src/components/search-and-filter/components/filter/clear-icon-button.tsx +12 -3
- package/src/components/search-and-filter/components/filter/css-class-filter.tsx +10 -0
- package/src/components/search-and-filter/components/filter/filter-list.tsx +7 -0
- package/src/components/search-and-filter/components/search/class-manager-search.tsx +6 -0
- package/src/components/search-and-filter/context.tsx +12 -2
- package/src/errors.ts +5 -0
- package/src/global-classes-styles-provider.ts +13 -3
- package/src/hooks/use-css-class-by-id.ts +8 -0
- package/src/hooks/use-prefetch-css-class-usage.ts +6 -0
- package/src/init.ts +14 -5
- package/src/mcp-integration/classes-resource.ts +20 -0
- package/src/mcp-integration/index.ts +15 -0
- package/src/mcp-integration/mcp-apply-unapply-global-classes.ts +117 -0
- package/src/mcp-integration/mcp-get-global-class-usages.ts +72 -0
- package/src/save-global-classes.tsx +55 -0
- package/src/store.ts +32 -1
- package/src/sync-with-document-save.ts +9 -6
- package/src/sync-with-document.tsx +19 -0
- package/src/utils/tracking.ts +204 -0
- package/src/components/class-manager/save-changes-dialog.tsx +0 -92
- package/src/save-global-classes.ts +0 -42
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { type StyleDefinition, type StyleDefinitionID } from '@elementor/editor-styles';
|
|
2
|
+
import { getMixpanel } from '@elementor/mixpanel';
|
|
3
|
+
import { __getState as getState } from '@elementor/store';
|
|
4
|
+
|
|
5
|
+
import { fetchCssClassUsage } from '../../service/css-class-usage-service';
|
|
6
|
+
import { GlobalClassTrackingError } from '../errors';
|
|
7
|
+
import { type FilterKey } from '../hooks/use-filtered-css-class-usage';
|
|
8
|
+
import { selectClass } from '../store';
|
|
9
|
+
|
|
10
|
+
type EventMap = {
|
|
11
|
+
classCreated: {
|
|
12
|
+
source?: 'created' | 'converted';
|
|
13
|
+
classId: StyleDefinitionID;
|
|
14
|
+
classTitle?: string;
|
|
15
|
+
};
|
|
16
|
+
classDeleted: {
|
|
17
|
+
classId: StyleDefinitionID;
|
|
18
|
+
runAction?: () => void;
|
|
19
|
+
};
|
|
20
|
+
classRenamed: {
|
|
21
|
+
classId: StyleDefinitionID;
|
|
22
|
+
oldValue: string;
|
|
23
|
+
newValue: string;
|
|
24
|
+
source: 'class-manager' | 'style-tab';
|
|
25
|
+
};
|
|
26
|
+
classApplied: {
|
|
27
|
+
classId: StyleDefinitionID;
|
|
28
|
+
classTitle: string;
|
|
29
|
+
totalInstancesAfterApply: number;
|
|
30
|
+
};
|
|
31
|
+
classRemoved: {
|
|
32
|
+
classId: StyleDefinitionID;
|
|
33
|
+
classTitle: string;
|
|
34
|
+
};
|
|
35
|
+
classStyled: {
|
|
36
|
+
classId: StyleDefinitionID;
|
|
37
|
+
classTitle: string;
|
|
38
|
+
classType: 'global' | 'local';
|
|
39
|
+
};
|
|
40
|
+
classManagerOpened: {
|
|
41
|
+
source: 'style-panel';
|
|
42
|
+
};
|
|
43
|
+
classManagerSearched: Record< string, never >;
|
|
44
|
+
classManagerFiltersOpened: Record< string, never >;
|
|
45
|
+
classManagerFilterUsed: {
|
|
46
|
+
action: 'apply' | 'remove';
|
|
47
|
+
type: FilterKey;
|
|
48
|
+
trigger: 'menu' | 'header';
|
|
49
|
+
};
|
|
50
|
+
classManagerFilterCleared: {
|
|
51
|
+
trigger: 'menu' | 'header';
|
|
52
|
+
};
|
|
53
|
+
classManagerReorder: {
|
|
54
|
+
classId: StyleDefinitionID;
|
|
55
|
+
classTitle: string;
|
|
56
|
+
};
|
|
57
|
+
classPublishConflict: {
|
|
58
|
+
numOfConflicts: number;
|
|
59
|
+
};
|
|
60
|
+
classUsageHovered: {
|
|
61
|
+
classId: string;
|
|
62
|
+
usage: number;
|
|
63
|
+
};
|
|
64
|
+
classUsageClicked: {
|
|
65
|
+
classId: StyleDefinitionID;
|
|
66
|
+
};
|
|
67
|
+
classUsageLocate: {
|
|
68
|
+
classId: StyleDefinitionID;
|
|
69
|
+
};
|
|
70
|
+
classStateClicked: {
|
|
71
|
+
classId: StyleDefinitionID | null;
|
|
72
|
+
type: string;
|
|
73
|
+
source: 'global' | 'local';
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type TrackingEvent = {
|
|
78
|
+
[ K in keyof EventMap ]: { event: K } & EventMap[ K ];
|
|
79
|
+
}[ keyof EventMap ];
|
|
80
|
+
|
|
81
|
+
type TrackingEventWithComputed = TrackingEvent & {
|
|
82
|
+
classTitle?: string;
|
|
83
|
+
totalInstancesAfterApply?: number;
|
|
84
|
+
totalInstances?: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const trackGlobalClasses = async ( payload: TrackingEvent ) => {
|
|
88
|
+
const { runAction } = payload as TrackingEventWithComputed & { runAction?: () => void };
|
|
89
|
+
const data = await getSanitizedData( payload );
|
|
90
|
+
if ( data ) {
|
|
91
|
+
track( data );
|
|
92
|
+
if ( data.event === 'classCreated' && 'classId' in data ) {
|
|
93
|
+
fireClassApplied( data.classId as StyleDefinitionID );
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
runAction?.();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const fireClassApplied = async ( classId: StyleDefinitionID ) => {
|
|
100
|
+
const appliedInfo = await getAppliedInfo( classId );
|
|
101
|
+
track( {
|
|
102
|
+
event: 'classApplied',
|
|
103
|
+
classId,
|
|
104
|
+
...appliedInfo,
|
|
105
|
+
totalInstancesAfterApply: 1,
|
|
106
|
+
} );
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const getSanitizedData = async ( payload: TrackingEvent ): Promise< Record< string, unknown > | undefined > => {
|
|
110
|
+
switch ( payload.event ) {
|
|
111
|
+
case 'classApplied':
|
|
112
|
+
if ( 'classId' in payload && payload.classId ) {
|
|
113
|
+
const appliedInfo = await getAppliedInfo( payload.classId );
|
|
114
|
+
return { ...payload, ...appliedInfo };
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
case 'classRemoved':
|
|
118
|
+
if ( 'classId' in payload && payload.classId ) {
|
|
119
|
+
const deleteInfo = getRemovedInfo( payload.classId );
|
|
120
|
+
return { ...payload, ...deleteInfo };
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
case 'classDeleted':
|
|
124
|
+
if ( 'classId' in payload && payload.classId ) {
|
|
125
|
+
const deleteInfo = await trackDeleteClass( payload.classId );
|
|
126
|
+
return { ...payload, ...deleteInfo };
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
case 'classCreated':
|
|
130
|
+
if ( 'source' in payload && payload.source !== 'created' ) {
|
|
131
|
+
if ( 'classId' in payload && payload.classId ) {
|
|
132
|
+
return { ...payload, classTitle: getCssClass( payload.classId ).label };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return payload;
|
|
136
|
+
case 'classStateClicked':
|
|
137
|
+
if ( 'classId' in payload && payload.classId ) {
|
|
138
|
+
return { ...payload, classTitle: getCssClass( payload.classId ).label };
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
return payload;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const track = ( data: Record< string, unknown > ) => {
|
|
147
|
+
const { dispatchEvent, config } = getMixpanel();
|
|
148
|
+
if ( ! config?.names?.global_classes?.[ data.event as keyof EventMap ] ) {
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
console.error( 'Global class tracking event not found', { event: data.event } );
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const name = config.names.global_classes[ data.event as keyof EventMap ];
|
|
155
|
+
const { event, ...eventData } = data;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
dispatchEvent?.( name, {
|
|
159
|
+
event,
|
|
160
|
+
...eventData,
|
|
161
|
+
} );
|
|
162
|
+
} catch ( error ) {
|
|
163
|
+
throw new GlobalClassTrackingError( { cause: error } );
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const extractCssClassData = ( classId: StyleDefinitionID ) => {
|
|
168
|
+
const cssClass: StyleDefinition = getCssClass( classId );
|
|
169
|
+
const classTitle = cssClass.label;
|
|
170
|
+
|
|
171
|
+
return { classTitle };
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const getCssClass = ( classId: StyleDefinitionID ) => {
|
|
175
|
+
const cssClass = selectClass( getState(), classId );
|
|
176
|
+
if ( ! cssClass ) {
|
|
177
|
+
throw new Error( `CSS class with ID ${ classId } not found` );
|
|
178
|
+
}
|
|
179
|
+
return cssClass;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const trackDeleteClass = async ( classId: StyleDefinitionID ) => {
|
|
183
|
+
const totalInstances = await getTotalInstancesByCssClassID( classId );
|
|
184
|
+
const classTitle = getCssClass( classId ).label;
|
|
185
|
+
return { totalInstances, classTitle };
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const getTotalInstancesByCssClassID = async ( classId: StyleDefinitionID ) => {
|
|
189
|
+
const cssClassUsage = await fetchCssClassUsage();
|
|
190
|
+
return cssClassUsage[ classId ]?.total ?? 1;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const getAppliedInfo = async ( classId: StyleDefinitionID ) => {
|
|
194
|
+
const { classTitle } = extractCssClassData( classId );
|
|
195
|
+
const totalInstancesAfterApply = ( await getTotalInstancesByCssClassID( classId ) ) + 1;
|
|
196
|
+
return { classTitle, totalInstancesAfterApply };
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const getRemovedInfo = ( classId: StyleDefinitionID ) => {
|
|
200
|
+
const { classTitle } = extractCssClassData( classId );
|
|
201
|
+
return {
|
|
202
|
+
classTitle,
|
|
203
|
+
};
|
|
204
|
+
};
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { AlertTriangleFilledIcon } from '@elementor/icons';
|
|
4
|
-
import {
|
|
5
|
-
Button,
|
|
6
|
-
Dialog,
|
|
7
|
-
DialogActions,
|
|
8
|
-
DialogContent,
|
|
9
|
-
DialogContentText,
|
|
10
|
-
type DialogContentTextProps,
|
|
11
|
-
type DialogProps,
|
|
12
|
-
DialogTitle,
|
|
13
|
-
} from '@elementor/ui';
|
|
14
|
-
|
|
15
|
-
const TITLE_ID = 'save-changes-dialog';
|
|
16
|
-
|
|
17
|
-
const SaveChangesDialog = ( { children, onClose }: Pick< DialogProps, 'children' | 'onClose' > ) => (
|
|
18
|
-
<Dialog open onClose={ onClose } aria-labelledby={ TITLE_ID } maxWidth="xs">
|
|
19
|
-
{ children }
|
|
20
|
-
</Dialog>
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const SaveChangesDialogTitle = ( { children }: React.PropsWithChildren ) => (
|
|
24
|
-
<DialogTitle id={ TITLE_ID } display="flex" alignItems="center" gap={ 1 } sx={ { lineHeight: 1 } }>
|
|
25
|
-
<AlertTriangleFilledIcon color="secondary" />
|
|
26
|
-
{ children }
|
|
27
|
-
</DialogTitle>
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
const SaveChangesDialogContent = ( { children }: React.PropsWithChildren ) => (
|
|
31
|
-
<DialogContent>{ children }</DialogContent>
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const SaveChangesDialogContentText = ( props: DialogContentTextProps ) => (
|
|
35
|
-
<DialogContentText variant="body2" color="textPrimary" display="flex" flexDirection="column" { ...props } />
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
type Action = {
|
|
39
|
-
label: string;
|
|
40
|
-
action: () => void | Promise< void >;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
type ConfirmationDialogActionsProps = {
|
|
44
|
-
actions: {
|
|
45
|
-
cancel?: Action;
|
|
46
|
-
confirm: Action;
|
|
47
|
-
discard?: Action;
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
const SaveChangesDialogActions = ( { actions }: ConfirmationDialogActionsProps ) => {
|
|
51
|
-
const [ isConfirming, setIsConfirming ] = useState( false );
|
|
52
|
-
const { cancel, confirm, discard } = actions;
|
|
53
|
-
|
|
54
|
-
const onConfirm = async () => {
|
|
55
|
-
setIsConfirming( true );
|
|
56
|
-
await confirm.action();
|
|
57
|
-
setIsConfirming( false );
|
|
58
|
-
};
|
|
59
|
-
return (
|
|
60
|
-
<DialogActions>
|
|
61
|
-
{ cancel && (
|
|
62
|
-
<Button variant="text" color="secondary" onClick={ cancel.action }>
|
|
63
|
-
{ cancel.label }
|
|
64
|
-
</Button>
|
|
65
|
-
) }
|
|
66
|
-
{ discard && (
|
|
67
|
-
<Button variant="text" color="secondary" onClick={ discard.action }>
|
|
68
|
-
{ discard.label }
|
|
69
|
-
</Button>
|
|
70
|
-
) }
|
|
71
|
-
<Button variant="contained" color="secondary" onClick={ onConfirm } loading={ isConfirming }>
|
|
72
|
-
{ confirm.label }
|
|
73
|
-
</Button>
|
|
74
|
-
</DialogActions>
|
|
75
|
-
);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
SaveChangesDialog.Title = SaveChangesDialogTitle;
|
|
79
|
-
SaveChangesDialog.Content = SaveChangesDialogContent;
|
|
80
|
-
SaveChangesDialog.ContentText = SaveChangesDialogContentText;
|
|
81
|
-
SaveChangesDialog.Actions = SaveChangesDialogActions;
|
|
82
|
-
|
|
83
|
-
const useDialog = () => {
|
|
84
|
-
const [ isOpen, setIsOpen ] = useState( false );
|
|
85
|
-
|
|
86
|
-
const open = () => setIsOpen( true );
|
|
87
|
-
const close = () => setIsOpen( false );
|
|
88
|
-
|
|
89
|
-
return { isOpen, open, close };
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export { SaveChangesDialog, useDialog };
|
|
@@ -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
|
-
}
|