PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.7.0
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.7.0
2.7.0 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.4.0 2.3.3 2.3.2 2.3.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 All 129 releases
← All changes | includes/abilities/templates/wdesignkit-save-template.php +122 -9 2.5.5 → 2.7.0 View file →
@@ -9,12 +9,12 @@
9 9 exit();
10 10 }
11 11
12 12 wp_register_ability('wdesignkit/save-template', [
13 - 'label' => __('Save WDesignKit Template', 'sprout-mcp'),
13 + 'label' => __('Save WDesignKit Template', 'wdesignkit'),
14 14 'description' => __(
15 15 'Saves a page builder layout to the user\'s WDesignKit cloud library as a new template. Provide the full builder data payload (JSON string for Gutenberg, base64-decoded Elementor export for Elementor) plus the source post_id so any nxt-* custom meta is captured alongside it.',
16 - 'sprout-mcp',
16 + 'wdesignkit',
17 17 ),
18 18 'category' => 'wdesignkit',
19 19 'input_schema' => [
20 20 'type' => 'object',
@@ -39,8 +39,13 @@
39 39 'type' => [
40 40 'type' => 'string',
41 41 'description' => 'Template type (e.g. "page", "section", "block").',
42 42 ],
43 + 'plugins' => [
44 + 'type' => 'array',
45 + 'description' => 'Optional list of cloud plugin IDs (integers) this template requires — populates the template\'s plugins_id so the library shows its plugin dependencies. Get the IDs from wdesignkit/get-template-plugins.',
46 + 'items' => ['type' => 'integer'],
47 + ],
43 48 ],
44 49 'required' => ['builder', 'data'],
45 50 'additionalProperties' => false,
46 51 ],
@@ -52,10 +57,11 @@
52 57 'response' => ['type' => ['object', 'array']],
53 58 ],
54 59 ],
55 60 'execute_callback' => 'wdesignkit_mcp_save_template',
56 - 'permission_callback' => 'sprout_mcp_permission_callback',
61 + 'permission_callback' => 'wdesignkit_mcp_permission_callback',
57 62 'meta' => [
63 + 'public' => true,
58 64 'show_in_rest' => true,
59 65 'mcp' => ['public' => true],
60 66 'annotations' => [
61 67 'instructions' => implode("\n", [
@@ -84,8 +90,14 @@
84 90 $post_id = sanitize_text_field((string) ($input['post_id'] ?? ''));
85 91 $name = sanitize_text_field((string) ($input['name'] ?? ''));
86 92 $type = sanitize_text_field((string) ($input['type'] ?? ''));
87 93
94 + // Map the caller's friendly type to the literal value the cloud stores and filters on
95 + // ('section' / 'pagetemplate'). "block" has no confirmed cloud equivalent, so it passes
96 + // through unmapped rather than being silently rewritten to something wrong.
97 + $cloud_type_map = ['page' => 'pagetemplate', 'section' => 'section'];
98 + $cloud_type = $cloud_type_map[$type] ?? $type;
99 +
88 100 if (!in_array($builder, ['elementor', 'gutenberg'], true)) {
89 101 return ['success' => false, 'message' => 'builder must be "elementor" or "gutenberg".'];
90 102 }
91 103 if ($data === '') {
@@ -91,9 +103,73 @@
91 103 if ($data === '') {
92 104 return ['success' => false, 'message' => 'data is required — pass the full builder export payload.'];
93 105 }
94 106
107 + // Wrap the payload in the envelope the cloud, the importer and the editor JS all expect —
108 + // the same one the Save Template UI builds in main_save_template.js:
109 + // { file_type, title, page_id, content, el_type, settings } with the layout in `content`.
110 + //
111 + // Sending the raw layout as `data` instead broke both builders, in different ways:
112 + // Elementor — _elementor_data decodes to a LIST, and adding the string key 'custom_meta' to
113 + // a list makes wp_json_encode() emit an object, so the indices became "0","1",…
114 + // Gutenberg — block markup is a STRING, so json_decode() returned null, the is_array()
115 + // branch was skipped, and the markup was stored bare with no envelope at all.
116 + // The editor then runs JSON.parse() on it and throws.
117 + // Building the envelope first fixes both: custom_meta now goes onto an OBJECT, where an extra
118 + // key cannot reshape `content`.
119 + //
120 + // el_type is left empty — the UI derives it from the Elementor preview DOM, which has no
121 + // server-side equivalent.
122 + if ('elementor' === $builder) {
123 + $content = json_decode($data, true);
124 + if (!is_array($content)) {
125 + return [
126 + 'success' => false,
127 + 'message' => 'data is not valid Elementor JSON — pass the _elementor_data export (a JSON array), not markup.',
128 + ];
129 + }
130 + } else {
131 + // Gutenberg content is serialized block markup; keep the string unless block JSON was sent.
132 + $decoded = json_decode($data, true);
133 + $content = is_array($decoded) ? $decoded : $data;
134 +
135 + }
136 +
137 + $settings = [];
95 138 if ($post_id !== '') {
139 + $page_settings = get_post_meta((int) $post_id, '_elementor_page_settings', true);
140 + if (is_array($page_settings) && !empty($page_settings)) {
141 + $settings = $page_settings;
142 + }
143 + }
144 +
145 + $envelope = [
146 + 'file_type' => 'elementor' === $builder ? 'elementor' : 'wp_block',
147 + 'title' => $name,
148 + 'page_id' => $post_id !== '' ? (int) $post_id : 0,
149 + 'content' => $content,
150 + 'el_type' => '',
151 + 'settings' => $settings,
152 + ];
153 +
154 + // The Plus Addons resolves its global button styles, radii and shadows from the kit by
155 + // `_id`. Without those definitions travelling with the template, an imported section keeps
156 + // the reference but loses the styling. Scoped to what this layout actually references, so a
157 + // section carries its own globals rather than a copy of the whole kit.
158 + $tp_globals = ('elementor' === $builder && is_array($content)) ? wdesignkit_mcp_tp_globals($content) : [];
159 + if (!empty($tp_globals)) {
160 + $envelope['tp_globals'] = $tp_globals;
161 +
162 + // A Plus global can reference an Elementor global colour or font of its own. Those refs
163 + // live in the lists rather than the layout, so they need capturing separately or the
164 + // button style arrives pointing at an id the destination kit does not have.
165 + $tp_refs = wdesignkit_mcp_tp_global_refs($tp_globals);
166 + if (!empty($tp_refs['color']) || !empty($tp_refs['typography'])) {
167 + $envelope['tp_global_refs'] = $tp_refs;
168 + }
169 + }
170 +
171 + if ($post_id !== '') {
96 172 $custom_fields = [];
97 173 foreach ((array) get_post_custom($post_id) as $key => $value) {
98 174 if (is_string($key) && str_contains($key, 'nxt-')) {
99 175 $custom_fields[$key] = $value;
@@ -100,24 +176,61 @@
100 176 }
101 177 }
102 178
103 179 if (!empty($custom_fields)) {
104 - $decoded = json_decode($data, true);
105 - if (is_array($decoded)) {
106 - $decoded['custom_meta'] = $custom_fields;
107 - $data = wp_json_encode($decoded);
108 - }
180 + $envelope['custom_meta'] = $custom_fields;
109 181 }
110 182 }
111 183
184 + $data = wp_json_encode($envelope);
185 +
112 186 $args = [
113 187 'token' => $auth['token'],
114 188 'data' => $data,
115 189 'post_id' => $post_id,
116 190 'builder' => $builder,
191 + // The cloud SetSaveTemplate endpoint reads the display name from 'title'
192 + // (request->get('title')) and ignores 'name' — sending only 'name' is why
193 + // templates saved via this ability came back with a blank title. Send 'title'
194 + // (keep 'name' too, harmlessly, for any other consumer).
195 + 'title' => $name,
117 196 'name' => $name,
118 - 'type' => $type,
197 + // The cloud stores the template's type from 'template_type', NOT 'type':
198 + // $TemplateType = $request->get('template_type');
199 + // 'type' => !empty($TemplateType) ? $TemplateType : 'pagetemplate',
200 + // (WdkitPluginController::SetSaveTemplate lines 171 / 252). Sending only 'type' meant
201 + // $TemplateType was always empty, so EVERY template saved through this ability was
202 + // stored as 'pagetemplate' regardless of the caller's input — a section saved here
203 + // could never be found by list-templates(type: 'section'). Send 'template_type' too.
204 + //
205 + // The cloud's own queries match the literal values 'section' and 'pagetemplate'
206 + // (see the whereIn at SetSaveTemplate line 574), not 'page'/'block', so map first.
207 + 'type' => $type,
208 + 'template_type' => $cloud_type,
119 209 ];
210 +
211 + // Optional: record which cloud plugin IDs this template requires. The cloud
212 + // SetSaveTemplate endpoint reads 'plugins' as a JSON string of integer plugin IDs
213 + // and stores them as the template's plugins_id (previously always empty because the
214 + // ability had no way to supply them).
215 + $plugins_in = $input['plugins'] ?? [];
216 + if (is_string($plugins_in)) {
217 + $decoded = json_decode($plugins_in, true);
218 + $plugins_in = is_array($decoded) ? $decoded : [];
219 + }
220 + if (is_array($plugins_in)) {
221 + $plugin_ids = [];
222 + foreach ($plugins_in as $pid) {
223 + $pid = (int) $pid;
224 + if ($pid > 0) {
225 + $plugin_ids[] = $pid;
226 + }
227 + }
228 + if (!empty($plugin_ids)) {
229 + // Sent as a JSON string: the cloud json_decodes 'plugins' server-side.
230 + $args['plugins'] = wp_json_encode(array_values(array_unique($plugin_ids)));
231 + }
232 + }
120 233
121 234 $response = wdesignkit_mcp_template_cloud_call('save_template', $args, 'json');
122 235
123 236 return [