@elementor/editor-canvas 4.2.0-923 → 4.2.0-925
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 +196 -56
- package/dist/index.mjs +159 -19
- package/package.json +18 -18
- package/src/legacy/__tests__/init-legacy-views.test.ts +66 -0
- package/src/legacy/init-legacy-views.ts +20 -10
- package/src/mcp/canvas-mcp.ts +5 -0
- package/src/mcp/resources/__tests__/dynamic-tags-resource.test.ts +85 -0
- package/src/mcp/resources/dynamic-tags-resource.ts +52 -0
- package/src/mcp/tools/build-composition/prompt.ts +8 -0
- package/src/mcp/tools/build-composition/tool.ts +2 -0
- package/src/mcp/tools/configure-element/prompt.ts +16 -0
- package/src/mcp/tools/configure-element/tool.ts +2 -0
- package/src/mcp/utils/__tests__/resolve-dynamic-tag.test.ts +102 -0
- package/src/mcp/utils/do-update-element-property.ts +5 -1
- package/src/mcp/utils/resolve-dynamic-tag.ts +102 -0
|
@@ -10,6 +10,7 @@ import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
|
10
10
|
|
|
11
11
|
import { CompositionBuilder } from '../../../composition-builder/composition-builder';
|
|
12
12
|
import { AVAILABLE_WIDGETS_URI_V4 } from '../../resources/available-widgets-resource';
|
|
13
|
+
import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
|
|
13
14
|
import { BEST_PRACTICES_URI, STYLE_SCHEMA_URI, WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
|
|
14
15
|
import { isWidgetAvailableForLLM } from '../../utils/element-data-util';
|
|
15
16
|
import { getCompositionTargetContainer } from '../../utils/get-composition-target-container';
|
|
@@ -45,6 +46,7 @@ export const initBuildCompositionsTool = ( reg: MCPRegistryEntry ) => {
|
|
|
45
46
|
{ description: 'Global Variables', uri: 'elementor://global-variables' },
|
|
46
47
|
{ description: 'Styles best practices', uri: BEST_PRACTICES_URI },
|
|
47
48
|
{ description: 'Available widgets for this tool', uri: AVAILABLE_WIDGETS_URI_V4 },
|
|
49
|
+
{ description: 'Dynamic tags catalog', uri: DYNAMIC_TAGS_URI },
|
|
48
50
|
],
|
|
49
51
|
outputSchema,
|
|
50
52
|
handler: async ( rawParams ) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toolPrompts } from '@elementor/editor-mcp';
|
|
2
2
|
|
|
3
|
+
import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
|
|
3
4
|
import { STYLE_SCHEMA_URI, WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
|
|
4
5
|
|
|
5
6
|
export const CONFIGURE_ELEMENT_GUIDE_URI = 'elementor://canvas/tools/configure-element-guide';
|
|
@@ -59,6 +60,21 @@ For styleProperties, use the style schema provided, as it also uses the PropType
|
|
|
59
60
|
For all non-primitive types, provide the key property as defined in the schema as $$type in the generated object, as it is MANDATORY for parsing.
|
|
60
61
|
|
|
61
62
|
Use the EXACT "PROP-TYPE" Schema given, and ALWAYS include the "key" property from the original configuration for every property you are changing.
|
|
63
|
+
|
|
64
|
+
# Dynamic tags
|
|
65
|
+
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.
|
|
66
|
+
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.
|
|
67
|
+
1. Read the [${ DYNAMIC_TAGS_URI }] resource for each allowed tag's settings schema.
|
|
68
|
+
2. Provide, at that node:
|
|
69
|
+
{
|
|
70
|
+
"$$type": "dynamic",
|
|
71
|
+
"value": {
|
|
72
|
+
"name": "<allowed tag name>",
|
|
73
|
+
"settings": { /* strictly per the tag's settings schema */ }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
Image example: { "$$type": "image", "value": { "src": { "$$type": "dynamic", "value": { "name": "<image tag>", "settings": { ... } } } } }
|
|
77
|
+
Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only when the tag has no settings.
|
|
62
78
|
` );
|
|
63
79
|
|
|
64
80
|
configureElementToolPrompt.parameter( 'elementId', 'The ID of the element to configure. MANDATORY.' );
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getWidgetsCache } from '@elementor/editor-elements';
|
|
2
2
|
import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
3
3
|
|
|
4
|
+
import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
|
|
4
5
|
import { STYLE_SCHEMA_URI, WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
|
|
5
6
|
import { doUpdateElementProperty } from '../../utils/do-update-element-property';
|
|
6
7
|
import { validateInput } from '../../utils/validate-input';
|
|
@@ -32,6 +33,7 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
32
33
|
{ description: 'Widgets schema', uri: WIDGET_SCHEMA_URI },
|
|
33
34
|
{ description: 'Styles schema', uri: STYLE_SCHEMA_URI },
|
|
34
35
|
{ description: 'Configure element guide', uri: CONFIGURE_ELEMENT_GUIDE_URI },
|
|
36
|
+
{ description: 'Dynamic tags catalog', uri: DYNAMIC_TAGS_URI },
|
|
35
37
|
],
|
|
36
38
|
handler: ( { elementId, propertiesToChange, elementType, stylePropertiesToChange } ) => {
|
|
37
39
|
const widgetData = getWidgetsCache()?.[ elementType ];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { getElementorConfig } from '@elementor/editor-v1-adapters';
|
|
2
|
+
|
|
3
|
+
import { dynamicTagLLMResolver, getDynamicTagNamesByCategories } from '../resolve-dynamic-tag';
|
|
4
|
+
|
|
5
|
+
jest.mock( '@elementor/editor-v1-adapters', () => ( {
|
|
6
|
+
getElementorConfig: jest.fn(),
|
|
7
|
+
} ) );
|
|
8
|
+
|
|
9
|
+
const mockedGetElementorConfig = getElementorConfig as jest.MockedFunction< typeof getElementorConfig >;
|
|
10
|
+
|
|
11
|
+
const givenTags = () =>
|
|
12
|
+
mockedGetElementorConfig.mockReturnValue( {
|
|
13
|
+
atomicDynamicTags: {
|
|
14
|
+
tags: {
|
|
15
|
+
'post-custom-field': {
|
|
16
|
+
name: 'post-custom-field',
|
|
17
|
+
label: 'Post Custom Field',
|
|
18
|
+
group: 'post',
|
|
19
|
+
categories: [ 'text', 'url' ],
|
|
20
|
+
props_schema: {
|
|
21
|
+
key: { kind: 'plain', key: 'string', default: '' },
|
|
22
|
+
before: { kind: 'plain', key: 'string', default: 'prefix' },
|
|
23
|
+
fallback: { kind: 'plain', key: 'string', default: '' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
'site-title': {
|
|
27
|
+
name: 'site-title',
|
|
28
|
+
label: 'Site Title',
|
|
29
|
+
group: 'site',
|
|
30
|
+
categories: [ 'text' ],
|
|
31
|
+
props_schema: {},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
} as never );
|
|
36
|
+
|
|
37
|
+
describe( 'resolve-dynamic-tag', () => {
|
|
38
|
+
beforeEach( () => {
|
|
39
|
+
jest.clearAllMocks();
|
|
40
|
+
givenTags();
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
describe( 'getDynamicTagNamesByCategories', () => {
|
|
44
|
+
it( 'returns only the tags whose categories intersect the prop categories', () => {
|
|
45
|
+
// Act
|
|
46
|
+
const names = getDynamicTagNamesByCategories( [ 'url' ] );
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect( names ).toEqual( [ 'post-custom-field' ] );
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
it( 'returns an empty list when no categories are requested', () => {
|
|
53
|
+
// Act & Assert
|
|
54
|
+
expect( getDynamicTagNamesByCategories( [] ) ).toEqual( [] );
|
|
55
|
+
} );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
describe( 'dynamicTagLLMResolver', () => {
|
|
59
|
+
it( 'fills the group from the registry and wraps provided scalar settings', () => {
|
|
60
|
+
// Act
|
|
61
|
+
const resolved = dynamicTagLLMResolver( {
|
|
62
|
+
name: 'post-custom-field',
|
|
63
|
+
settings: { key: 'price' },
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
// Assert
|
|
67
|
+
expect( resolved ).toEqual( {
|
|
68
|
+
$$type: 'dynamic',
|
|
69
|
+
value: {
|
|
70
|
+
name: 'post-custom-field',
|
|
71
|
+
group: 'post',
|
|
72
|
+
settings: {
|
|
73
|
+
key: { $$type: 'string', value: 'price' },
|
|
74
|
+
before: { $$type: 'string', value: 'prefix' },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
} );
|
|
78
|
+
} );
|
|
79
|
+
|
|
80
|
+
it( 'keeps already-wrapped settings values untouched', () => {
|
|
81
|
+
// Act
|
|
82
|
+
const resolved = dynamicTagLLMResolver( {
|
|
83
|
+
name: 'post-custom-field',
|
|
84
|
+
settings: { key: { $$type: 'string', value: 'price' } },
|
|
85
|
+
} ) as { value: { settings: Record< string, unknown > } };
|
|
86
|
+
|
|
87
|
+
// Assert
|
|
88
|
+
expect( resolved.value.settings.key ).toEqual( { $$type: 'string', value: 'price' } );
|
|
89
|
+
} );
|
|
90
|
+
|
|
91
|
+
it( 'returns a structurally complete value for unknown tags', () => {
|
|
92
|
+
// Act
|
|
93
|
+
const resolved = dynamicTagLLMResolver( { name: 'does-not-exist' } );
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
expect( resolved ).toEqual( {
|
|
97
|
+
$$type: 'dynamic',
|
|
98
|
+
value: { name: 'does-not-exist', group: '', settings: {} },
|
|
99
|
+
} );
|
|
100
|
+
} );
|
|
101
|
+
} );
|
|
102
|
+
} );
|
|
@@ -12,6 +12,7 @@ import { type Utils as IUtils } from '@elementor/editor-variables';
|
|
|
12
12
|
import { type z } from '@elementor/schema';
|
|
13
13
|
|
|
14
14
|
import { mergeCustomCssText, readStoredCustomCssText } from './merge-custom-css';
|
|
15
|
+
import { DYNAMIC_PROP_TYPE_KEY, dynamicTagLLMResolver } from './resolve-dynamic-tag';
|
|
15
16
|
|
|
16
17
|
// TODO: see https://elementor.atlassian.net/browse/ED-22513 for better cross-module access
|
|
17
18
|
type XElementor = z.infer< z.ZodAny >;
|
|
@@ -34,7 +35,10 @@ export function resolvePropValue( value: unknown, forceKey?: string ): PropValue
|
|
|
34
35
|
.Utils as typeof IUtils;
|
|
35
36
|
return Schema.adjustLlmPropValueSchema( value as PropValue, {
|
|
36
37
|
forceKey,
|
|
37
|
-
transformers:
|
|
38
|
+
transformers: {
|
|
39
|
+
...Utils.globalVariablesLLMResolvers,
|
|
40
|
+
[ DYNAMIC_PROP_TYPE_KEY ]: dynamicTagLLMResolver,
|
|
41
|
+
},
|
|
38
42
|
} );
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type PropType, type PropValue } from '@elementor/editor-props';
|
|
2
|
+
import { getElementorConfig } from '@elementor/editor-v1-adapters';
|
|
3
|
+
|
|
4
|
+
export const DYNAMIC_PROP_TYPE_KEY = 'dynamic';
|
|
5
|
+
|
|
6
|
+
// `fallback` is a generic render-time default added to every tag; it is noise for configuration.
|
|
7
|
+
export const OMITTED_DYNAMIC_SETTING_KEYS = [ 'fallback' ] as const;
|
|
8
|
+
|
|
9
|
+
type SettingPropType = PropType & { key?: string };
|
|
10
|
+
|
|
11
|
+
export type AtomicDynamicTag = {
|
|
12
|
+
name: string;
|
|
13
|
+
label: string;
|
|
14
|
+
group: string;
|
|
15
|
+
categories: string[];
|
|
16
|
+
props_schema: Record< string, SettingPropType >;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type LlmDynamicValue = {
|
|
20
|
+
name?: string;
|
|
21
|
+
settings?: Record< string, unknown >;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const getAtomicDynamicTags = (): Record< string, AtomicDynamicTag > => {
|
|
25
|
+
const config = getElementorConfig() as { atomicDynamicTags?: { tags?: Record< string, AtomicDynamicTag > } };
|
|
26
|
+
return config.atomicDynamicTags?.tags ?? {};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getDynamicTagNamesByCategories = ( categories: string[] ): string[] => {
|
|
30
|
+
if ( ! categories.length ) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const wanted = new Set( categories );
|
|
34
|
+
return Object.values( getAtomicDynamicTags() )
|
|
35
|
+
.filter( ( tag ) => tag.categories?.some( ( category ) => wanted.has( category ) ) )
|
|
36
|
+
.map( ( tag ) => tag.name );
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Reconstructs an intact dynamic PropValue from whatever the LLM produced: the authoritative `group`
|
|
40
|
+
// is taken from the registry (never trusted from the model) and `settings` are rebuilt strictly from
|
|
41
|
+
// the tag's props schema (provided values are wrapped, omitted ones fall back to their defaults).
|
|
42
|
+
export const dynamicTagLLMResolver = ( value: unknown ): PropValue => {
|
|
43
|
+
const input = ( value ?? {} ) as LlmDynamicValue;
|
|
44
|
+
const tag = input.name ? getAtomicDynamicTags()[ input.name ] : undefined;
|
|
45
|
+
|
|
46
|
+
if ( ! tag ) {
|
|
47
|
+
return {
|
|
48
|
+
$$type: DYNAMIC_PROP_TYPE_KEY,
|
|
49
|
+
value: { name: input.name ?? '', group: '', settings: {} },
|
|
50
|
+
} as PropValue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
$$type: DYNAMIC_PROP_TYPE_KEY,
|
|
55
|
+
value: {
|
|
56
|
+
name: tag.name,
|
|
57
|
+
group: tag.group,
|
|
58
|
+
settings: buildStrictSettings( tag.props_schema ?? {}, input.settings ?? {} ),
|
|
59
|
+
},
|
|
60
|
+
} as PropValue;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const buildStrictSettings = (
|
|
64
|
+
schema: Record< string, SettingPropType >,
|
|
65
|
+
provided: Record< string, unknown >
|
|
66
|
+
): Record< string, unknown > => {
|
|
67
|
+
const settings: Record< string, unknown > = {};
|
|
68
|
+
|
|
69
|
+
for ( const [ key, propType ] of Object.entries( schema ) ) {
|
|
70
|
+
if ( ( OMITTED_DYNAMIC_SETTING_KEYS as readonly string[] ).includes( key ) ) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const resolved =
|
|
75
|
+
provided[ key ] !== undefined
|
|
76
|
+
? wrapSettingValue( provided[ key ], propType )
|
|
77
|
+
: defaultSettingValue( propType );
|
|
78
|
+
|
|
79
|
+
if ( resolved !== undefined && resolved !== null ) {
|
|
80
|
+
settings[ key ] = resolved;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return settings;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const wrapSettingValue = ( raw: unknown, propType: SettingPropType ): unknown => {
|
|
88
|
+
if ( raw !== null && typeof raw === 'object' ) {
|
|
89
|
+
return raw;
|
|
90
|
+
}
|
|
91
|
+
return propType.key ? { $$type: propType.key, value: raw } : raw;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const defaultSettingValue = ( propType: SettingPropType ): unknown => {
|
|
95
|
+
if ( propType.initial_value !== null && propType.initial_value !== undefined ) {
|
|
96
|
+
return propType.initial_value;
|
|
97
|
+
}
|
|
98
|
+
if ( propType.default !== null && propType.default !== undefined ) {
|
|
99
|
+
return wrapSettingValue( propType.default, propType );
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
};
|