@elementor/editor-canvas 4.2.0-931 → 4.2.0-932
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.d.mts +18 -7
- package/dist/index.d.ts +18 -7
- package/dist/index.js +243 -282
- package/dist/index.mjs +227 -266
- package/package.json +19 -18
- package/src/composition-builder/composition-builder.ts +2 -9
- package/src/index.ts +7 -1
- package/src/mcp/canvas-mcp.ts +3 -0
- package/src/mcp/mcp-description.ts +9 -13
- package/src/mcp/resources/widgets-schema-resource.ts +1 -41
- package/src/mcp/tools/build-composition/prompt.ts +12 -13
- package/src/mcp/tools/build-composition/schema.ts +5 -12
- package/src/mcp/tools/build-composition/tool.ts +25 -3
- package/src/mcp/tools/configure-element/prompt.ts +22 -44
- package/src/mcp/tools/configure-element/schema.ts +12 -6
- package/src/mcp/tools/configure-element/tool.ts +43 -38
- package/src/mcp/utils/__tests__/convert-css-to-atomic.test.ts +109 -0
- package/src/mcp/utils/__tests__/do-update-element-property.test.ts +39 -1
- package/src/mcp/utils/convert-css-to-atomic.ts +39 -0
- package/src/mcp/utils/do-update-element-property.ts +3 -0
|
@@ -2,7 +2,8 @@ import { getWidgetsCache } from '@elementor/editor-elements';
|
|
|
2
2
|
import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
3
3
|
|
|
4
4
|
import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
|
|
5
|
-
import {
|
|
5
|
+
import { WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
|
|
6
|
+
import { convertCssToAtomic } from '../../utils/convert-css-to-atomic';
|
|
6
7
|
import { doUpdateElementProperty } from '../../utils/do-update-element-property';
|
|
7
8
|
import { validateInput } from '../../utils/validate-input';
|
|
8
9
|
import { CONFIGURE_ELEMENT_GUIDE_URI, generatePrompt } from './prompt';
|
|
@@ -31,11 +32,10 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
31
32
|
outputSchema,
|
|
32
33
|
requiredResources: [
|
|
33
34
|
{ description: 'Widgets schema', uri: WIDGET_SCHEMA_URI },
|
|
34
|
-
{ description: 'Styles schema', uri: STYLE_SCHEMA_URI },
|
|
35
35
|
{ description: 'Configure element guide', uri: CONFIGURE_ELEMENT_GUIDE_URI },
|
|
36
36
|
{ description: 'Dynamic tags catalog', uri: DYNAMIC_TAGS_URI },
|
|
37
37
|
],
|
|
38
|
-
handler: ( { elementId, propertiesToChange, elementType,
|
|
38
|
+
handler: async ( { elementId, propertiesToChange, elementType, style } ) => {
|
|
39
39
|
const widgetData = getWidgetsCache()?.[ elementType ];
|
|
40
40
|
if ( ! widgetData ) {
|
|
41
41
|
throw new Error(
|
|
@@ -49,21 +49,12 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
49
49
|
}
|
|
50
50
|
const toUpdate = Object.entries( propertiesToChange );
|
|
51
51
|
const { valid, errors } = validateInput.validatePropSchema( elementType, propertiesToChange );
|
|
52
|
-
const { valid: stylesValid, errors: stylesErrors } = validateInput.validateStyles(
|
|
53
|
-
stylePropertiesToChange || {}
|
|
54
|
-
);
|
|
55
52
|
if ( ! valid ) {
|
|
56
53
|
const errorMessage = `Failed to configure element "${ elementId }" due to invalid properties: ${ errors?.join(
|
|
57
54
|
'\n- '
|
|
58
55
|
) }`;
|
|
59
56
|
throw new Error( errorMessage );
|
|
60
57
|
}
|
|
61
|
-
if ( ! stylesValid ) {
|
|
62
|
-
const errorMessage = `Failed to configure element "${ elementId }" due to invalid style properties: ${ stylesErrors?.join(
|
|
63
|
-
'\n- '
|
|
64
|
-
) }`;
|
|
65
|
-
throw new Error( errorMessage );
|
|
66
|
-
}
|
|
67
58
|
for ( const [ propertyName, propertyValue ] of toUpdate ) {
|
|
68
59
|
try {
|
|
69
60
|
doUpdateElementProperty( {
|
|
@@ -83,27 +74,7 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
83
74
|
throw new Error( errorMessage );
|
|
84
75
|
}
|
|
85
76
|
}
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
doUpdateElementProperty( {
|
|
89
|
-
elementId,
|
|
90
|
-
elementType,
|
|
91
|
-
propertyName: '_styles',
|
|
92
|
-
propertyValue: {
|
|
93
|
-
[ stylePropertyName ]: stylePropertyValue,
|
|
94
|
-
},
|
|
95
|
-
} );
|
|
96
|
-
} catch ( error ) {
|
|
97
|
-
const errorMessage = createUpdateErrorMessage( {
|
|
98
|
-
propertyName: `(style) ${ stylePropertyName }`,
|
|
99
|
-
elementId,
|
|
100
|
-
elementType,
|
|
101
|
-
propertyType: 'style',
|
|
102
|
-
error: error as Error,
|
|
103
|
-
} );
|
|
104
|
-
throw new Error( errorMessage );
|
|
105
|
-
}
|
|
106
|
-
}
|
|
77
|
+
await applyStyleFromCss( { elementId, elementType, style } );
|
|
107
78
|
return {
|
|
108
79
|
success: true,
|
|
109
80
|
};
|
|
@@ -111,6 +82,44 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
111
82
|
} );
|
|
112
83
|
};
|
|
113
84
|
|
|
85
|
+
async function applyStyleFromCss( opts: {
|
|
86
|
+
elementId: string;
|
|
87
|
+
elementType: string;
|
|
88
|
+
style: Record< string, string | null >;
|
|
89
|
+
} ) {
|
|
90
|
+
const { elementId, elementType, style } = opts;
|
|
91
|
+
if ( ! style || Object.keys( style ).length === 0 ) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const { props, customCss } = await convertCssToAtomic( style );
|
|
95
|
+
const styleValue: Record< string, unknown > = { ...props };
|
|
96
|
+
if ( customCss ) {
|
|
97
|
+
styleValue.custom_css = customCss;
|
|
98
|
+
}
|
|
99
|
+
if ( Object.keys( styleValue ).length === 0 ) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
doUpdateElementProperty( {
|
|
104
|
+
elementId,
|
|
105
|
+
elementType,
|
|
106
|
+
propertyName: '_styles',
|
|
107
|
+
propertyValue: styleValue,
|
|
108
|
+
customCssWriteMode: 'merge-with-stored',
|
|
109
|
+
} );
|
|
110
|
+
} catch ( error ) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
createUpdateErrorMessage( {
|
|
113
|
+
propertyName: '(style)',
|
|
114
|
+
elementId,
|
|
115
|
+
elementType,
|
|
116
|
+
propertyType: 'style',
|
|
117
|
+
error: error as Error,
|
|
118
|
+
} )
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
function createUpdateErrorMessage( opts: {
|
|
115
124
|
propertyName: string;
|
|
116
125
|
elementId: string;
|
|
@@ -129,11 +138,7 @@ Check the element's PropType schema at the resource [${ WIDGET_SCHEMA_URI.replac
|
|
|
129
138
|
) }] for type "${ elementType }" to ensure the property exists and the value matches the expected PropType.
|
|
130
139
|
Now that you have this information, ensure you have the schema and try again.`
|
|
131
140
|
: `
|
|
132
|
-
|
|
133
|
-
'{category}',
|
|
134
|
-
propertyName
|
|
135
|
-
) }] at editor-canvas__elementor://styles/schema/{category} to ensure the style property exists and the value matches the expected PropType.
|
|
136
|
-
`
|
|
141
|
+
Provide styling as raw CSS via the "style" parameter (a flat map of CSS property → value). Declarations that cannot be converted are stored as the element custom CSS.`
|
|
137
142
|
};
|
|
138
143
|
}`;
|
|
139
144
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { httpService } from '@elementor/http-client';
|
|
2
|
+
|
|
3
|
+
import { convertCssToAtomic, convertStyleBlocksToAtomic } from '../convert-css-to-atomic';
|
|
4
|
+
|
|
5
|
+
jest.mock( '@elementor/http-client' );
|
|
6
|
+
|
|
7
|
+
const mockedHttpService = jest.mocked( httpService );
|
|
8
|
+
|
|
9
|
+
const mockPost = ( responseData: unknown ) => {
|
|
10
|
+
const post = jest.fn().mockResolvedValue( { data: { data: responseData, meta: {} } } );
|
|
11
|
+
mockedHttpService.mockReturnValue( { post } as never );
|
|
12
|
+
return post;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe( 'convertCssToAtomic', () => {
|
|
16
|
+
it( 'posts a single named block as a declaration map and unwraps its result', async () => {
|
|
17
|
+
// Arrange.
|
|
18
|
+
const post = mockPost( {
|
|
19
|
+
default: { props: { color: { $$type: 'color', value: 'red' } }, customCss: 'gap: 4px;' },
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
// Act.
|
|
23
|
+
const result = await convertCssToAtomic( { color: 'red', gap: '4px' } );
|
|
24
|
+
|
|
25
|
+
// Assert.
|
|
26
|
+
expect( post ).toHaveBeenCalledWith( 'elementor/v1/css-to-atomic', {
|
|
27
|
+
blocks: { default: { color: 'red', gap: '4px' } },
|
|
28
|
+
} );
|
|
29
|
+
expect( result ).toEqual( {
|
|
30
|
+
props: { color: { $$type: 'color', value: 'red' } },
|
|
31
|
+
customCss: 'gap: 4px;',
|
|
32
|
+
} );
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
it( 'forwards null declarations untouched so the server can emit reset props', async () => {
|
|
36
|
+
// Arrange.
|
|
37
|
+
const post = mockPost( {
|
|
38
|
+
default: { props: { color: null }, customCss: '' },
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
// Act.
|
|
42
|
+
const result = await convertCssToAtomic( { color: null } );
|
|
43
|
+
|
|
44
|
+
// Assert.
|
|
45
|
+
expect( post ).toHaveBeenCalledWith( 'elementor/v1/css-to-atomic', {
|
|
46
|
+
blocks: { default: { color: null } },
|
|
47
|
+
} );
|
|
48
|
+
expect( result ).toEqual( { props: { color: null }, customCss: '' } );
|
|
49
|
+
} );
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
describe( 'convertStyleBlocksToAtomic', () => {
|
|
53
|
+
it( 'posts every named declaration map and returns the named results map', async () => {
|
|
54
|
+
// Arrange.
|
|
55
|
+
const post = mockPost( {
|
|
56
|
+
'el-1': { props: {}, customCss: 'color: red;' },
|
|
57
|
+
'el-2': { props: {}, customCss: 'gap: 4px;' },
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
// Act.
|
|
61
|
+
const results = await convertStyleBlocksToAtomic( {
|
|
62
|
+
'el-1': { color: 'red' },
|
|
63
|
+
'el-2': { gap: '4px' },
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
// Assert.
|
|
67
|
+
expect( post ).toHaveBeenCalledWith( 'elementor/v1/css-to-atomic', {
|
|
68
|
+
blocks: { 'el-1': { color: 'red' }, 'el-2': { gap: '4px' } },
|
|
69
|
+
} );
|
|
70
|
+
expect( results ).toEqual( {
|
|
71
|
+
'el-1': { props: {}, customCss: 'color: red;' },
|
|
72
|
+
'el-2': { props: {}, customCss: 'gap: 4px;' },
|
|
73
|
+
} );
|
|
74
|
+
} );
|
|
75
|
+
|
|
76
|
+
it( 'posts plaintext css text blocks and returns the named results map', async () => {
|
|
77
|
+
// Arrange.
|
|
78
|
+
const post = mockPost( {
|
|
79
|
+
default: { props: { display: { $$type: 'string', value: 'flex' } }, customCss: '' },
|
|
80
|
+
hover: { props: { opacity: { $$type: 'string', value: '0.85' } }, customCss: '' },
|
|
81
|
+
} );
|
|
82
|
+
|
|
83
|
+
// Act.
|
|
84
|
+
const results = await convertStyleBlocksToAtomic( {
|
|
85
|
+
default: 'display: flex;',
|
|
86
|
+
hover: 'opacity: 0.85;',
|
|
87
|
+
} );
|
|
88
|
+
|
|
89
|
+
// Assert.
|
|
90
|
+
expect( post ).toHaveBeenCalledWith( 'elementor/v1/css-to-atomic', {
|
|
91
|
+
blocks: { default: 'display: flex;', hover: 'opacity: 0.85;' },
|
|
92
|
+
} );
|
|
93
|
+
expect( results ).toEqual( {
|
|
94
|
+
default: { props: { display: { $$type: 'string', value: 'flex' } }, customCss: '' },
|
|
95
|
+
hover: { props: { opacity: { $$type: 'string', value: '0.85' } }, customCss: '' },
|
|
96
|
+
} );
|
|
97
|
+
} );
|
|
98
|
+
|
|
99
|
+
it( 'posts an empty blocks map when given no style blocks', async () => {
|
|
100
|
+
// Arrange.
|
|
101
|
+
const post = mockPost( {} );
|
|
102
|
+
|
|
103
|
+
// Act.
|
|
104
|
+
await convertStyleBlocksToAtomic( {} );
|
|
105
|
+
|
|
106
|
+
// Assert.
|
|
107
|
+
expect( post ).toHaveBeenCalledWith( 'elementor/v1/css-to-atomic', { blocks: {} } );
|
|
108
|
+
} );
|
|
109
|
+
} );
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
updateElementStyle,
|
|
6
6
|
} from '@elementor/editor-elements';
|
|
7
7
|
import { Schema } from '@elementor/editor-props';
|
|
8
|
-
import { getVariantByMeta } from '@elementor/editor-styles';
|
|
8
|
+
import { getStylesSchema, getVariantByMeta } from '@elementor/editor-styles';
|
|
9
9
|
import { __privateRunCommandSync } from '@elementor/editor-v1-adapters';
|
|
10
10
|
|
|
11
11
|
import { doUpdateElementProperty } from '../do-update-element-property';
|
|
@@ -215,4 +215,42 @@ describe( 'doUpdateElementProperty', () => {
|
|
|
215
215
|
} )
|
|
216
216
|
);
|
|
217
217
|
} );
|
|
218
|
+
|
|
219
|
+
it( 'passes a null style value through unresolved so the editor resets the prop to default', () => {
|
|
220
|
+
// Arrange
|
|
221
|
+
jest.mocked( getStylesSchema ).mockReturnValue( {
|
|
222
|
+
color: { key: 'colorPropKey', kind: 'plain' },
|
|
223
|
+
} as never );
|
|
224
|
+
jest.mocked( getElementStyles ).mockReturnValue( {
|
|
225
|
+
[ LOCAL_STYLE_ID ]: {
|
|
226
|
+
id: LOCAL_STYLE_ID,
|
|
227
|
+
label: 'local',
|
|
228
|
+
type: 'class',
|
|
229
|
+
variants: [],
|
|
230
|
+
},
|
|
231
|
+
} );
|
|
232
|
+
jest.mocked( getVariantByMeta ).mockReturnValue( {
|
|
233
|
+
meta: { breakpoint: 'desktop', state: null },
|
|
234
|
+
props: {},
|
|
235
|
+
custom_css: null,
|
|
236
|
+
} );
|
|
237
|
+
|
|
238
|
+
// Act
|
|
239
|
+
doUpdateElementProperty( {
|
|
240
|
+
elementId: ELEMENT_ID,
|
|
241
|
+
elementType: ELEMENT_TYPE,
|
|
242
|
+
propertyName: '_styles',
|
|
243
|
+
propertyValue: {
|
|
244
|
+
color: null,
|
|
245
|
+
},
|
|
246
|
+
} );
|
|
247
|
+
|
|
248
|
+
// Assert
|
|
249
|
+
expect( Schema.adjustLlmPropValueSchema ).not.toHaveBeenCalled();
|
|
250
|
+
expect( updateElementStyle ).toHaveBeenCalledWith(
|
|
251
|
+
expect.objectContaining( {
|
|
252
|
+
props: { color: null },
|
|
253
|
+
} )
|
|
254
|
+
);
|
|
255
|
+
} );
|
|
218
256
|
} );
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type PropValue } from '@elementor/editor-props';
|
|
2
|
+
import { type HttpResponse, httpService } from '@elementor/http-client';
|
|
3
|
+
|
|
4
|
+
const CSS_TO_ATOMIC_URL = 'elementor/v1/css-to-atomic';
|
|
5
|
+
const SINGLE_BLOCK_KEY = 'default';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A flat CSS property→value map. A null value is an explicit reset: the server emits it as a null
|
|
9
|
+
* prop so the editor restores the property to its default.
|
|
10
|
+
*/
|
|
11
|
+
export type StyleDeclarations = Record< string, string | null >;
|
|
12
|
+
|
|
13
|
+
export type StyleBlock = StyleDeclarations | string;
|
|
14
|
+
|
|
15
|
+
export type CssConversionResult = {
|
|
16
|
+
props: Record< string, PropValue | null >;
|
|
17
|
+
customCss: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const convertBlocks = async (
|
|
21
|
+
blocks: Record< string, StyleBlock >
|
|
22
|
+
): Promise< Record< string, CssConversionResult > > => {
|
|
23
|
+
const { data } = await httpService().post< HttpResponse< Record< string, CssConversionResult > > >(
|
|
24
|
+
CSS_TO_ATOMIC_URL,
|
|
25
|
+
{ blocks }
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return data.data;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const convertStyleBlocksToAtomic = async (
|
|
32
|
+
styleByName: Record< string, StyleBlock >
|
|
33
|
+
): Promise< Record< string, CssConversionResult > > => convertBlocks( styleByName );
|
|
34
|
+
|
|
35
|
+
export const convertCssToAtomic = async ( style: StyleDeclarations ): Promise< CssConversionResult > => {
|
|
36
|
+
const results = await convertStyleBlocksToAtomic( { [ SINGLE_BLOCK_KEY ]: style } );
|
|
37
|
+
|
|
38
|
+
return results[ SINGLE_BLOCK_KEY ];
|
|
39
|
+
};
|
|
@@ -61,6 +61,9 @@ export const doUpdateElementProperty = ( params: OwnParams ) => {
|
|
|
61
61
|
if ( ! propKey && kind !== 'union' ) {
|
|
62
62
|
throw new Error( `_styles property ${ key } is not supported.` );
|
|
63
63
|
}
|
|
64
|
+
if ( val === null ) {
|
|
65
|
+
return [ key, null ];
|
|
66
|
+
}
|
|
64
67
|
return [ key, resolvePropValue( val, propKey ) ];
|
|
65
68
|
} )
|
|
66
69
|
);
|