@elementor/editor-canvas 4.3.0-999 → 4.4.0-1064
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 +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +457 -371
- package/dist/index.mjs +438 -348
- package/package.json +20 -20
- package/src/init-settings-transformers.ts +4 -0
- package/src/init-style-transformers.ts +2 -2
- package/src/init.tsx +5 -4
- package/src/legacy/__tests__/create-templated-element-type.test.ts +50 -0
- package/src/legacy/__tests__/list-type.test.ts +89 -0
- package/src/legacy/create-nested-templated-element-type.ts +3 -1
- package/src/legacy/create-templated-element-type.ts +5 -2
- package/src/legacy/list-type.ts +63 -0
- package/src/legacy/replacements/inline-editing/__tests__/inline-editing-eligibility.test.ts +7 -7
- package/src/legacy/replacements/inline-editing/inline-editing-elements.tsx +22 -9
- package/src/legacy/replacements/inline-editing/inline-editing-eligibility.ts +12 -3
- package/src/legacy/tabs-model-extensions.ts +2 -5
- package/src/legacy/twig-rendering-utils.ts +11 -2
- package/src/legacy/types.ts +4 -0
- package/src/mcp/canvas-mcp.ts +2 -4
- package/src/mcp/mcp-description.ts +18 -30
- package/src/mcp/resources/__tests__/available-widgets-resource.test.ts +22 -13
- package/src/mcp/resources/__tests__/dynamic-tags-resource.test.ts +22 -30
- package/src/mcp/resources/__tests__/widgets-schema-resource.test.ts +11 -6
- package/src/mcp/resources/available-widgets-resource.ts +13 -10
- package/src/mcp/resources/dynamic-tags-resource.ts +6 -15
- package/src/mcp/resources/widgets-schema-resource.ts +8 -5
- package/src/mcp/tools/configure-element/__tests__/tool.test.ts +130 -0
- package/src/mcp/tools/configure-element/prompt.ts +3 -6
- package/src/mcp/tools/configure-element/schema.ts +6 -0
- package/src/mcp/tools/configure-element/tool.ts +11 -1
- package/src/mcp/tools/get-page-structure/tool.ts +73 -0
- package/src/mcp/utils/__tests__/do-update-element-property.test.ts +27 -1
- package/src/mcp/utils/do-update-element-property.ts +18 -6
- package/src/mcp/utils/get-mcp-error-message.ts +16 -0
- package/src/renderers/__tests__/compute-html-tag.test.ts +55 -0
- package/src/renderers/__tests__/create-dom-renderer.test.ts +60 -0
- package/src/renderers/__tests__/fixtures/html-tag-computer-cases.json +87 -0
- package/src/renderers/compute-html-tag.ts +73 -0
- package/src/renderers/create-dom-renderer.ts +13 -22
- package/src/transformers/settings/escaped-html-transformer.ts +6 -0
- package/src/transformers/shared/__tests__/icon-transformer.test.ts +74 -0
- package/src/transformers/shared/icon-transformer.ts +122 -0
- package/src/transformers/shared/process-svg-content.ts +26 -0
- package/src/transformers/shared/svg-src-transformer.ts +1 -26
- package/src/utils/__tests__/sanitize-escaped-html.test.ts +105 -0
- package/src/utils/sanitize-escaped-html.ts +32 -0
- package/src/mcp/tools/build-composition/tool.ts +0 -133
- package/src/mcp/tools/get-element-config/tool.ts +0 -114
|
@@ -3,7 +3,7 @@ import { WIDGET_SCHEMA_URI } from './resources/widgets-schema-resource';
|
|
|
3
3
|
const ELEMENT_SCHEMA_URI = WIDGET_SCHEMA_URI.replace( '{widgetType}', 'element-schema' );
|
|
4
4
|
|
|
5
5
|
export const mcpDescription = `Elementor Canvas MCP
|
|
6
|
-
This MCP enables
|
|
6
|
+
This MCP enables configuration and styling of existing V4 elements on the Elementor canvas using the configure-element tool.
|
|
7
7
|
|
|
8
8
|
# Core Concepts
|
|
9
9
|
|
|
@@ -22,66 +22,54 @@ The \`$$type\` defines how Elementor interprets the value. Providing the correct
|
|
|
22
22
|
- **Global Classes**: Reusable style sets that can be applied to elements (\`elementor://global-classes\`)
|
|
23
23
|
- **Widget Schemas**: Configuration options for each widget type (\`${ WIDGET_SCHEMA_URI }\`)
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# Configuring Elements with configure-element
|
|
26
26
|
|
|
27
|
-
The \`
|
|
27
|
+
The \`configure-element\` tool updates settings and styles on existing V4 elements. Read the configure-element guide resource before use.
|
|
28
28
|
|
|
29
29
|
## Complete Workflow
|
|
30
30
|
|
|
31
31
|
### 1. Parse User Requirements
|
|
32
|
-
Understand what needs to
|
|
32
|
+
Understand what needs to change: content, settings, or styling on existing elements.
|
|
33
33
|
|
|
34
34
|
### 2. Check Global Resources FIRST
|
|
35
|
-
Always check existing resources before
|
|
35
|
+
Always check existing resources before styling:
|
|
36
36
|
- List \`elementor://global-variables\` for available variables (colors, sizes, fonts)
|
|
37
37
|
- List \`elementor://global-classes\` for available style sets
|
|
38
38
|
- **Always prefer using existing global resources over creating inline styles**
|
|
39
39
|
|
|
40
40
|
### 3. Retrieve Widget Schemas
|
|
41
|
-
For each
|
|
41
|
+
For each element you will configure:
|
|
42
42
|
- List \`${ WIDGET_SCHEMA_URI }\` to see available widgets
|
|
43
43
|
- Retrieve configuration schema from \`${ ELEMENT_SCHEMA_URI }\` for each widget
|
|
44
|
-
- Check the \`llm_guidance\` property for container nesting, \`default_styles\`, and \`default_settings\`
|
|
44
|
+
- Check the \`llm_guidance\` property for container nesting, \`default_styles\`, and \`default_settings\`
|
|
45
45
|
|
|
46
|
-
### 4.
|
|
47
|
-
|
|
48
|
-
- Each element must have a unique \`configuration-id\` attribute
|
|
49
|
-
- No text nodes, classes, or IDs in XML - structure only
|
|
50
|
-
- Example:
|
|
51
|
-
\`\`\`xml
|
|
52
|
-
<e-container configuration-id="container-1">
|
|
53
|
-
<e-heading configuration-id="heading-1" />
|
|
54
|
-
<e-text configuration-id="text-1" />
|
|
55
|
-
</e-container>
|
|
56
|
-
\`\`\`
|
|
46
|
+
### 4. Get Current Element State
|
|
47
|
+
Use page structure and element configuration resources to find element IDs and current values.
|
|
57
48
|
|
|
58
|
-
### 5. Create
|
|
59
|
-
Map
|
|
49
|
+
### 5. Create propertiesToChange
|
|
50
|
+
Map property names to PropValues using the widget schema:
|
|
60
51
|
- Use correct \`$$type\` matching the widget's schema
|
|
61
52
|
- Use global variables in PropValues where applicable
|
|
62
53
|
- Example:
|
|
63
54
|
\`\`\`json
|
|
64
55
|
{
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
"tag": { "$$type": "string", "value": "h1" }
|
|
68
|
-
}
|
|
56
|
+
"text": { "$$type": "string", "value": "Welcome" },
|
|
57
|
+
"tag": { "$$type": "string", "value": "h1" }
|
|
69
58
|
}
|
|
70
59
|
\`\`\`
|
|
71
60
|
|
|
72
61
|
### 6. Create style
|
|
73
|
-
|
|
62
|
+
Provide raw CSS declarations (property → value strings). The server converts them to native styles and stores any unconvertible declarations as the element custom CSS.
|
|
74
63
|
- Example:
|
|
75
64
|
\`\`\`json
|
|
76
65
|
{
|
|
77
|
-
"
|
|
78
|
-
|
|
66
|
+
"color": "#1a1a1a",
|
|
67
|
+
"font-size": "2rem"
|
|
79
68
|
}
|
|
80
69
|
\`\`\`
|
|
81
70
|
|
|
82
|
-
### 7. Execute
|
|
83
|
-
Call the tool with
|
|
84
|
-
At the response you will also find llm_instructions for you to do afterwards, read and follow them!
|
|
71
|
+
### 7. Execute configure-element
|
|
72
|
+
Call the tool with elementId, elementType, propertiesToChange, and style as needed.
|
|
85
73
|
|
|
86
74
|
## Key Points
|
|
87
75
|
|
|
@@ -28,14 +28,16 @@ describe( 'available-widgets-resource', () => {
|
|
|
28
28
|
jest.clearAllMocks();
|
|
29
29
|
} );
|
|
30
30
|
|
|
31
|
-
it( 'fetches all widgets via list-
|
|
31
|
+
it( 'fetches all widgets via list-widget-schemas with summary=true', async () => {
|
|
32
32
|
// Arrange
|
|
33
33
|
const post = jest.fn().mockResolvedValue( {
|
|
34
34
|
data: {
|
|
35
|
-
data:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
data: {
|
|
36
|
+
widgets: [
|
|
37
|
+
{ type: 'e-heading', description: 'A heading widget' },
|
|
38
|
+
{ type: 'e-button', description: 'A button widget' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
39
41
|
},
|
|
40
42
|
} );
|
|
41
43
|
mockedHttpService.mockReturnValue( { post } as never );
|
|
@@ -45,16 +47,21 @@ describe( 'available-widgets-resource', () => {
|
|
|
45
47
|
const result = await allHandler();
|
|
46
48
|
|
|
47
49
|
// Assert
|
|
48
|
-
expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
|
|
50
|
+
expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
|
|
51
|
+
tool: 'list-widget-schemas',
|
|
52
|
+
input: { summary: true },
|
|
53
|
+
} );
|
|
49
54
|
expect( JSON.parse( result.contents[ 0 ].text ) ).toEqual( [
|
|
50
|
-
{ type: 'e-heading',
|
|
51
|
-
{ type: '
|
|
55
|
+
{ type: 'e-heading', description: 'A heading widget' },
|
|
56
|
+
{ type: 'e-button', description: 'A button widget' },
|
|
52
57
|
] );
|
|
53
58
|
} );
|
|
54
59
|
|
|
55
|
-
it( 'fetches
|
|
60
|
+
it( 'fetches v4 widgets via list-widget-schemas with summary=true', async () => {
|
|
56
61
|
// Arrange
|
|
57
|
-
const post = jest.fn().mockResolvedValue( {
|
|
62
|
+
const post = jest.fn().mockResolvedValue( {
|
|
63
|
+
data: { data: { widgets: [ { type: 'e-heading', description: 'A heading widget' } ] } },
|
|
64
|
+
} );
|
|
58
65
|
mockedHttpService.mockReturnValue( { post } as never );
|
|
59
66
|
const { v4Handler } = captureHandlers();
|
|
60
67
|
|
|
@@ -63,10 +70,12 @@ describe( 'available-widgets-resource', () => {
|
|
|
63
70
|
|
|
64
71
|
// Assert
|
|
65
72
|
expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
|
|
66
|
-
tool: 'list-
|
|
67
|
-
input: {
|
|
73
|
+
tool: 'list-widget-schemas',
|
|
74
|
+
input: { summary: true },
|
|
68
75
|
} );
|
|
69
|
-
expect( JSON.parse( result.contents[ 0 ].text ) ).toEqual( [
|
|
76
|
+
expect( JSON.parse( result.contents[ 0 ].text ) ).toEqual( [
|
|
77
|
+
{ type: 'e-heading', description: 'A heading widget' },
|
|
78
|
+
] );
|
|
70
79
|
} );
|
|
71
80
|
|
|
72
81
|
it( 'exposes the expected resource URIs', () => {
|
|
@@ -19,7 +19,7 @@ const captureHandler = (): ResourceHandler => {
|
|
|
19
19
|
const readCatalog = async () => {
|
|
20
20
|
const handler = captureHandler();
|
|
21
21
|
const result = await handler( new URL( DYNAMIC_TAGS_URI ) );
|
|
22
|
-
return
|
|
22
|
+
return result.contents[ 0 ].text;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
describe( 'dynamic-tags-resource', () => {
|
|
@@ -27,31 +27,9 @@ describe( 'dynamic-tags-resource', () => {
|
|
|
27
27
|
jest.clearAllMocks();
|
|
28
28
|
} );
|
|
29
29
|
|
|
30
|
-
it( 'returns the
|
|
30
|
+
it( 'returns the json string fetched from the server', async () => {
|
|
31
31
|
// Arrange
|
|
32
|
-
const
|
|
33
|
-
data: {
|
|
34
|
-
data: [
|
|
35
|
-
{
|
|
36
|
-
name: 'post-custom-field',
|
|
37
|
-
label: 'Post Custom Field',
|
|
38
|
-
categories: [ 'text', 'url' ],
|
|
39
|
-
settings: { key: { mocked: true }, before: { mocked: true } },
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
} );
|
|
44
|
-
mockedHttpService.mockReturnValue( { post } as never );
|
|
45
|
-
|
|
46
|
-
// Act
|
|
47
|
-
const catalog = await readCatalog();
|
|
48
|
-
|
|
49
|
-
// Assert
|
|
50
|
-
expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
|
|
51
|
-
tool: 'list-dynamic-tags',
|
|
52
|
-
input: {},
|
|
53
|
-
} );
|
|
54
|
-
expect( catalog ).toEqual( [
|
|
32
|
+
const serverJson = JSON.stringify( [
|
|
55
33
|
{
|
|
56
34
|
name: 'post-custom-field',
|
|
57
35
|
label: 'Post Custom Field',
|
|
@@ -59,17 +37,31 @@ describe( 'dynamic-tags-resource', () => {
|
|
|
59
37
|
settings: { key: { mocked: true }, before: { mocked: true } },
|
|
60
38
|
},
|
|
61
39
|
] );
|
|
40
|
+
|
|
41
|
+
const get = jest.fn().mockResolvedValue( {
|
|
42
|
+
data: { data: serverJson },
|
|
43
|
+
} );
|
|
44
|
+
mockedHttpService.mockReturnValue( { get } as never );
|
|
45
|
+
|
|
46
|
+
// Act
|
|
47
|
+
const text = await readCatalog();
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
expect( get ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
|
|
51
|
+
params: { uri: DYNAMIC_TAGS_URI },
|
|
52
|
+
} );
|
|
53
|
+
expect( text ).toBe( serverJson );
|
|
62
54
|
} );
|
|
63
55
|
|
|
64
|
-
it( 'returns an empty
|
|
56
|
+
it( 'returns an empty json array string when the server returns no data', async () => {
|
|
65
57
|
// Arrange
|
|
66
|
-
const
|
|
67
|
-
mockedHttpService.mockReturnValue( {
|
|
58
|
+
const get = jest.fn().mockResolvedValue( { data: {} } );
|
|
59
|
+
mockedHttpService.mockReturnValue( { get } as never );
|
|
68
60
|
|
|
69
61
|
// Act
|
|
70
|
-
const
|
|
62
|
+
const text = await readCatalog();
|
|
71
63
|
|
|
72
64
|
// Assert
|
|
73
|
-
expect(
|
|
65
|
+
expect( text ).toBe( '[]' );
|
|
74
66
|
} );
|
|
75
67
|
} );
|
|
@@ -40,14 +40,16 @@ describe( 'widgets-schema-resource', () => {
|
|
|
40
40
|
jest.clearAllMocks();
|
|
41
41
|
} );
|
|
42
42
|
|
|
43
|
-
it( 'lists widget types fetched from the server via
|
|
43
|
+
it( 'lists widget types fetched from the server via list-widget-schemas with summary=true', async () => {
|
|
44
44
|
// Arrange
|
|
45
45
|
const post = jest.fn().mockResolvedValue( {
|
|
46
46
|
data: {
|
|
47
|
-
data:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
data: {
|
|
48
|
+
widgets: [
|
|
49
|
+
{ type: 'e-heading', description: 'A heading widget' },
|
|
50
|
+
{ type: 'e-button', description: 'A button widget' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
51
53
|
},
|
|
52
54
|
} );
|
|
53
55
|
mockedHttpService.mockReturnValue( { post } as never );
|
|
@@ -57,7 +59,10 @@ describe( 'widgets-schema-resource', () => {
|
|
|
57
59
|
const result = await list();
|
|
58
60
|
|
|
59
61
|
// Assert
|
|
60
|
-
expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
|
|
62
|
+
expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
|
|
63
|
+
tool: 'list-widget-schemas',
|
|
64
|
+
input: { summary: true },
|
|
65
|
+
} );
|
|
61
66
|
expect( result.resources ).toEqual( [
|
|
62
67
|
{ uri: 'elementor://widgets/schema/e-heading', name: 'Widget schema for e-heading' },
|
|
63
68
|
{ uri: 'elementor://widgets/schema/e-button', name: 'Widget schema for e-button' },
|
|
@@ -8,21 +8,24 @@ export const AVAILABLE_WIDGETS_URI_V4 = 'elementor://context/available-widgets/v
|
|
|
8
8
|
|
|
9
9
|
type WidgetSummary = {
|
|
10
10
|
type: string;
|
|
11
|
-
version: 'v3' | 'v4';
|
|
12
11
|
description?: string;
|
|
13
12
|
};
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
type WidgetSummaryResponse = {
|
|
15
|
+
widgets: WidgetSummary[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const fetchWidgets = async (): Promise< WidgetSummary[] > => {
|
|
19
|
+
const { data } = await httpService().post< HttpResponse< WidgetSummaryResponse > >( MCP_PROXY_URL, {
|
|
20
|
+
tool: 'list-widget-schemas',
|
|
21
|
+
input: { summary: true },
|
|
19
22
|
} );
|
|
20
23
|
|
|
21
|
-
return data.data ?? [];
|
|
24
|
+
return data.data?.widgets ?? [];
|
|
22
25
|
};
|
|
23
26
|
|
|
24
|
-
const buildContents = async ( uri: string
|
|
25
|
-
const widgets = await fetchWidgets(
|
|
27
|
+
const buildContents = async ( uri: string ) => {
|
|
28
|
+
const widgets = await fetchWidgets();
|
|
26
29
|
|
|
27
30
|
return {
|
|
28
31
|
contents: [
|
|
@@ -44,14 +47,14 @@ export const initAvailableWidgetsResource = ( reg: MCPRegistryEntry ) => {
|
|
|
44
47
|
{
|
|
45
48
|
description: 'All registered v4 version widgets',
|
|
46
49
|
},
|
|
47
|
-
async () => buildContents( AVAILABLE_WIDGETS_URI_V4
|
|
50
|
+
async () => buildContents( AVAILABLE_WIDGETS_URI_V4 )
|
|
48
51
|
);
|
|
49
52
|
|
|
50
53
|
resource(
|
|
51
54
|
'available-widgets',
|
|
52
55
|
AVAILABLE_WIDGETS_URI,
|
|
53
56
|
{
|
|
54
|
-
description: 'All registered widget types with
|
|
57
|
+
description: 'All registered v4 widget types with description.',
|
|
55
58
|
},
|
|
56
59
|
async () => buildContents( AVAILABLE_WIDGETS_URI )
|
|
57
60
|
);
|
|
@@ -5,20 +5,13 @@ export const DYNAMIC_TAGS_URI = 'elementor://dynamic-tags';
|
|
|
5
5
|
|
|
6
6
|
const MCP_PROXY_URL = 'elementor/v1/mcp-proxy';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
settings: Record< string, unknown >;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const fetchDynamicTags = async (): Promise< DynamicTagEntry[] > => {
|
|
16
|
-
const { data } = await httpService().post< HttpResponse< DynamicTagEntry[] > >( MCP_PROXY_URL, {
|
|
17
|
-
tool: 'list-dynamic-tags',
|
|
18
|
-
input: {},
|
|
8
|
+
// NOTE: JSON-encoded array of dynamic tag entries; shape defined by List_Dynamic_Tags_Ability::execute() in modules/mcp/abilities/list-dynamic-tags-ability.php
|
|
9
|
+
const fetchDynamicTags = async (): Promise< string > => {
|
|
10
|
+
const { data } = await httpService().get< HttpResponse< string > >( MCP_PROXY_URL, {
|
|
11
|
+
params: { uri: DYNAMIC_TAGS_URI },
|
|
19
12
|
} );
|
|
20
13
|
|
|
21
|
-
return data.data ?? [];
|
|
14
|
+
return data.data ?? '[]';
|
|
22
15
|
};
|
|
23
16
|
|
|
24
17
|
export const initDynamicTagsResource = ( reg: MCPRegistryEntry ) => {
|
|
@@ -35,14 +28,12 @@ export const initDynamicTagsResource = ( reg: MCPRegistryEntry ) => {
|
|
|
35
28
|
mimeType: 'application/json',
|
|
36
29
|
},
|
|
37
30
|
async ( uri: URL ) => {
|
|
38
|
-
const tags = await fetchDynamicTags();
|
|
39
|
-
|
|
40
31
|
return {
|
|
41
32
|
contents: [
|
|
42
33
|
{
|
|
43
34
|
uri: uri.href,
|
|
44
35
|
mimeType: 'application/json',
|
|
45
|
-
text:
|
|
36
|
+
text: await fetchDynamicTags(),
|
|
46
37
|
},
|
|
47
38
|
],
|
|
48
39
|
};
|
|
@@ -13,17 +13,20 @@ const MCP_PROXY_URL = 'elementor/v1/mcp-proxy';
|
|
|
13
13
|
|
|
14
14
|
type WidgetSummary = {
|
|
15
15
|
type: string;
|
|
16
|
-
version: 'v3' | 'v4';
|
|
17
16
|
description?: string;
|
|
18
17
|
};
|
|
19
18
|
|
|
19
|
+
type WidgetSummaryResponse = {
|
|
20
|
+
widgets: WidgetSummary[];
|
|
21
|
+
};
|
|
22
|
+
|
|
20
23
|
const listWidgetTypes = async (): Promise< string[] > => {
|
|
21
|
-
const { data } = await httpService().post< HttpResponse<
|
|
22
|
-
tool: 'list-
|
|
23
|
-
input: {},
|
|
24
|
+
const { data } = await httpService().post< HttpResponse< WidgetSummaryResponse > >( MCP_PROXY_URL, {
|
|
25
|
+
tool: 'list-widget-schemas',
|
|
26
|
+
input: { summary: true },
|
|
24
27
|
} );
|
|
25
28
|
|
|
26
|
-
return ( data.data ?? [] ).map( ( widget ) => widget.type );
|
|
29
|
+
return ( data.data?.widgets ?? [] ).map( ( widget ) => widget.type );
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
const fetchWidgetSchema = async ( widgetType: string ): Promise< Record< string, unknown > > => {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { getContainer, getWidgetsCache } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
import { doUpdateElementProperty, UnsupportedPropertyError } from '../../../utils/do-update-element-property';
|
|
4
|
+
import { initConfigureElementTool } from '../tool';
|
|
5
|
+
|
|
6
|
+
jest.mock( '@elementor/editor-elements', () => ( {
|
|
7
|
+
getContainer: jest.fn(),
|
|
8
|
+
getWidgetsCache: jest.fn(),
|
|
9
|
+
} ) );
|
|
10
|
+
|
|
11
|
+
jest.mock( '@elementor/editor-mcp', () => ( {
|
|
12
|
+
dispatchMcpStylesAppliedEvent: jest.fn(),
|
|
13
|
+
} ) );
|
|
14
|
+
|
|
15
|
+
jest.mock( '@elementor/editor-props', () => ( {
|
|
16
|
+
Schema: { isPropKeyConfigurable: () => true },
|
|
17
|
+
} ) );
|
|
18
|
+
|
|
19
|
+
jest.mock( '../prompt', () => ( {
|
|
20
|
+
CONFIGURE_ELEMENT_GUIDE_URI: 'configure-element-guide-uri',
|
|
21
|
+
generatePrompt: () => '',
|
|
22
|
+
} ) );
|
|
23
|
+
|
|
24
|
+
jest.mock( '../../../resources/widgets-schema-resource', () => ( {
|
|
25
|
+
WIDGET_SCHEMA_URI: 'elementor://widgets/schema/{widgetType}',
|
|
26
|
+
} ) );
|
|
27
|
+
|
|
28
|
+
jest.mock( '../../../resources/dynamic-tags-resource', () => ( {
|
|
29
|
+
DYNAMIC_TAGS_URI: 'elementor://dynamic-tags',
|
|
30
|
+
} ) );
|
|
31
|
+
|
|
32
|
+
jest.mock( '../../../utils/resolve-canonical-prop-name', () => ( {
|
|
33
|
+
resolveCanonicalPropKeys: ( _elementType: string, props: Record< string, unknown > ) => props,
|
|
34
|
+
} ) );
|
|
35
|
+
|
|
36
|
+
jest.mock( '../../../utils/convert-css-to-atomic', () => ( {
|
|
37
|
+
convertCssToAtomic: jest.fn().mockResolvedValue( { props: {}, customCss: '' } ),
|
|
38
|
+
} ) );
|
|
39
|
+
|
|
40
|
+
jest.mock( '../../../utils/do-update-element-property', () => ( {
|
|
41
|
+
...jest.requireActual( '../../../utils/do-update-element-property' ),
|
|
42
|
+
doUpdateElementProperty: jest.fn(),
|
|
43
|
+
} ) );
|
|
44
|
+
|
|
45
|
+
const ELEMENT_ID = 'el-1';
|
|
46
|
+
const ELEMENT_TYPE = 'e-youtube';
|
|
47
|
+
|
|
48
|
+
type Handler = ( params: {
|
|
49
|
+
elementId: string;
|
|
50
|
+
elementType: string;
|
|
51
|
+
propertiesToChange: Record< string, unknown >;
|
|
52
|
+
style: Record< string, string | null >;
|
|
53
|
+
} ) => Promise< { success: boolean; warnings?: string } >;
|
|
54
|
+
|
|
55
|
+
const getHandler = (): Handler => {
|
|
56
|
+
const addTool = jest.fn();
|
|
57
|
+
initConfigureElementTool( { addTool, resource: jest.fn() } as never );
|
|
58
|
+
return addTool.mock.calls[ 0 ][ 0 ].handler;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe( 'configure-element handler unsupported props', () => {
|
|
62
|
+
beforeEach( () => {
|
|
63
|
+
jest.mocked( getWidgetsCache ).mockReturnValue( {
|
|
64
|
+
[ ELEMENT_TYPE ]: { atomic_props_schema: { title: { key: 'title' } } },
|
|
65
|
+
} as never );
|
|
66
|
+
jest.mocked( getContainer ).mockReturnValue( {
|
|
67
|
+
settings: { get: () => ELEMENT_TYPE },
|
|
68
|
+
} as never );
|
|
69
|
+
jest.mocked( doUpdateElementProperty ).mockReset();
|
|
70
|
+
} );
|
|
71
|
+
|
|
72
|
+
it( 'skips an unsupported prop, reports it in warnings, and still applies supported props', async () => {
|
|
73
|
+
// Arrange
|
|
74
|
+
jest.mocked( doUpdateElementProperty ).mockImplementation( ( { propertyName } ) => {
|
|
75
|
+
if ( 'link' === propertyName ) {
|
|
76
|
+
throw new UnsupportedPropertyError( ELEMENT_TYPE, 'link', [ 'title' ] );
|
|
77
|
+
}
|
|
78
|
+
} );
|
|
79
|
+
const handler = getHandler();
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
const result = await handler( {
|
|
83
|
+
elementId: ELEMENT_ID,
|
|
84
|
+
elementType: ELEMENT_TYPE,
|
|
85
|
+
propertiesToChange: { title: { $$type: 'string', value: 'Hi' }, link: { $$type: 'link', value: {} } },
|
|
86
|
+
style: {},
|
|
87
|
+
} );
|
|
88
|
+
|
|
89
|
+
// Assert
|
|
90
|
+
expect( result.success ).toBe( true );
|
|
91
|
+
expect( result.warnings ).toContain( 'link' );
|
|
92
|
+
expect( doUpdateElementProperty ).toHaveBeenCalledTimes( 2 );
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
it( 'returns no warnings when every prop is supported', async () => {
|
|
96
|
+
// Arrange
|
|
97
|
+
jest.mocked( doUpdateElementProperty ).mockImplementation( () => undefined );
|
|
98
|
+
const handler = getHandler();
|
|
99
|
+
|
|
100
|
+
// Act
|
|
101
|
+
const result = await handler( {
|
|
102
|
+
elementId: ELEMENT_ID,
|
|
103
|
+
elementType: ELEMENT_TYPE,
|
|
104
|
+
propertiesToChange: { title: { $$type: 'string', value: 'Hi' } },
|
|
105
|
+
style: {},
|
|
106
|
+
} );
|
|
107
|
+
|
|
108
|
+
// Assert
|
|
109
|
+
expect( result.success ).toBe( true );
|
|
110
|
+
expect( result.warnings ).toBeUndefined();
|
|
111
|
+
} );
|
|
112
|
+
|
|
113
|
+
it( 'rethrows non-UnsupportedPropertyError failures', async () => {
|
|
114
|
+
// Arrange
|
|
115
|
+
jest.mocked( doUpdateElementProperty ).mockImplementation( () => {
|
|
116
|
+
throw new Error( 'Invalid PropValue' );
|
|
117
|
+
} );
|
|
118
|
+
const handler = getHandler();
|
|
119
|
+
|
|
120
|
+
// Act & Assert
|
|
121
|
+
await expect(
|
|
122
|
+
handler( {
|
|
123
|
+
elementId: ELEMENT_ID,
|
|
124
|
+
elementType: ELEMENT_TYPE,
|
|
125
|
+
propertiesToChange: { title: { $$type: 'string', value: 'Hi' } },
|
|
126
|
+
style: {},
|
|
127
|
+
} )
|
|
128
|
+
).rejects.toThrow( /Invalid PropValue/ );
|
|
129
|
+
} );
|
|
130
|
+
} );
|
|
@@ -50,8 +50,6 @@ For all non-primitive entries in \`propertiesToChange\`, provide the schema \`ke
|
|
|
50
50
|
|
|
51
51
|
Use the EXACT PropType schema given, and ALWAYS include the \`key\` from the schema for every property you are changing in \`propertiesToChange\`.
|
|
52
52
|
|
|
53
|
-
Check \`llm_guidance.default_settings\` in the widget schema — include a key in \`propertiesToChange\` only when the user explicitly asks to change it.
|
|
54
|
-
|
|
55
53
|
# Dynamic tags
|
|
56
54
|
A value can be made dynamic wherever its schema exposes a variant with "$$type": "dynamic". This may be the property root OR a NESTED field: for example an image is made dynamic on its "src" (the root stays "image"), NOT on the whole "image" value.
|
|
57
55
|
Put the dynamic object EXACTLY at the node whose schema offers the "dynamic" variant, in place of the static variant. The variant's "name" enumerates the tags allowed at that node.
|
|
@@ -82,7 +80,7 @@ Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only
|
|
|
82
80
|
|
|
83
81
|
configureElementToolPrompt.parameter(
|
|
84
82
|
'style',
|
|
85
|
-
'A flat map of raw CSS declarations (property → value), e.g. { "line-height": "1.25rem", "color": "var(--primary-text, #000)" }. Set a value to null to reset that property to its default. OPTIONAL.'
|
|
83
|
+
'A flat map of raw CSS declarations (property → value), e.g. { "line-height": "1.25rem", "color": "var(--primary-text, #000)" }. font-family must be a single Google Font name or a var(--label) — no fallback stacks. Set a value to null to reset that property to its default. OPTIONAL.'
|
|
86
84
|
);
|
|
87
85
|
|
|
88
86
|
configureElementToolPrompt.example( `
|
|
@@ -117,11 +115,10 @@ Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only
|
|
|
117
115
|
V4 only: If MCP fails, give manual steps using V4 UI.
|
|
118
116
|
|
|
119
117
|
V4 Editor structure:
|
|
120
|
-
Panel tabs: General (→ Settings section: ID, Tag, Link), Style, Interactions.
|
|
118
|
+
Panel tabs: General (→ Settings section: ID, Tag, and Link where the widget supports it), Style, Interactions.
|
|
121
119
|
NO Advanced tab. Never mention Advanced tab.
|
|
120
|
+
Note: \`link\` is valid only when the element's PropType schema (which you must already have) includes a \`link\` property. Sending \`link\` to a widget whose schema lacks it is skipped and reported in the response \`warnings\` (other changes still apply) and the link is lost.
|
|
122
121
|
` );
|
|
123
122
|
|
|
124
123
|
return configureElementToolPrompt.prompt();
|
|
125
124
|
};
|
|
126
|
-
|
|
127
|
-
export const CONFIGURE_ELEMENT_GUIDE_TEXT = generatePrompt();
|
|
@@ -36,4 +36,10 @@ export const outputSchema = {
|
|
|
36
36
|
.describe(
|
|
37
37
|
'Whether the configuration change was successful, only if propertyName and propertyValue are provided'
|
|
38
38
|
),
|
|
39
|
+
warnings: z
|
|
40
|
+
.string()
|
|
41
|
+
.describe(
|
|
42
|
+
'Non-fatal notices. Present when some props were skipped because they are not in the element schema (e.g. a "link" on a widget with no link prop). Other changes were still applied.'
|
|
43
|
+
)
|
|
44
|
+
.optional(),
|
|
39
45
|
};
|
|
@@ -6,7 +6,7 @@ import { type PropValue, Schema } from '@elementor/editor-props';
|
|
|
6
6
|
import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
|
|
7
7
|
import { WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
|
|
8
8
|
import { convertCssToAtomic } from '../../utils/convert-css-to-atomic';
|
|
9
|
-
import { doUpdateElementProperty } from '../../utils/do-update-element-property';
|
|
9
|
+
import { doUpdateElementProperty, UnsupportedPropertyError } from '../../utils/do-update-element-property';
|
|
10
10
|
import { resolveCanonicalPropKeys } from '../../utils/resolve-canonical-prop-name';
|
|
11
11
|
import { CONFIGURE_ELEMENT_GUIDE_URI, generatePrompt } from './prompt';
|
|
12
12
|
import { inputSchema as schema, outputSchema } from './schema';
|
|
@@ -61,6 +61,7 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
61
61
|
}
|
|
62
62
|
const propertiesToUpdate = resolveCanonicalPropKeys( elementType, propertiesToChange );
|
|
63
63
|
const toUpdate = Object.entries( propertiesToUpdate );
|
|
64
|
+
const skippedProps: string[] = [];
|
|
64
65
|
for ( const [ propertyName, propertyValue ] of toUpdate as [ string, PropValue ][] ) {
|
|
65
66
|
if ( ! Schema.isPropKeyConfigurable( propertyName ) ) {
|
|
66
67
|
throw new Error( `Not allowed to update ${ propertyName }` );
|
|
@@ -73,6 +74,10 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
73
74
|
propertyValue,
|
|
74
75
|
} );
|
|
75
76
|
} catch ( error ) {
|
|
77
|
+
if ( error instanceof UnsupportedPropertyError ) {
|
|
78
|
+
skippedProps.push( error.propertyName );
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
76
81
|
const errorMessage = createUpdateErrorMessage( {
|
|
77
82
|
propertyName,
|
|
78
83
|
elementId,
|
|
@@ -86,6 +91,11 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
86
91
|
await applyStyleFromCss( { elementId, elementType, style } );
|
|
87
92
|
return {
|
|
88
93
|
success: true,
|
|
94
|
+
warnings: skippedProps.length
|
|
95
|
+
? `Skipped unsupported props (not in the "${ elementType }" schema; other changes were applied): ${ skippedProps.join(
|
|
96
|
+
', '
|
|
97
|
+
) }.`
|
|
98
|
+
: undefined,
|
|
89
99
|
};
|
|
90
100
|
},
|
|
91
101
|
} );
|