mcp-magento-cms 1.0.3 → 1.2.0
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/README.md +173 -82
- package/package.json +4 -3
- package/src/assignStores.js +150 -0
- package/src/index.js +840 -89
- package/src/magentoClient.js +95 -4
- package/src/pageBuilder.js +66 -68
- package/src/pageBuilderElements.js +789 -0
- package/src/pageBuilderRepair.js +266 -0
- package/src/pageBuilderRules.js +127 -0
- package/src/pageBuilderValidate.js +164 -0
- package/src/projectRules.js +95 -0
package/src/magentoClient.js
CHANGED
|
@@ -141,25 +141,116 @@ export class MagentoClient {
|
|
|
141
141
|
return this.get(`/cmsPage/${pageId}`);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Magento 2.4.8 cmsPage REST uses `active` (not `is_active`) and rejects
|
|
146
|
+
* store_id / unknown fields with "%fieldName is not supported".
|
|
147
|
+
* Accept either name from callers; always send `active` on the wire.
|
|
148
|
+
*/
|
|
149
|
+
normalizePagePayload(page, { requireId } = {}) {
|
|
150
|
+
const allowed = [
|
|
151
|
+
'title',
|
|
152
|
+
'identifier',
|
|
153
|
+
'content',
|
|
154
|
+
'content_heading',
|
|
155
|
+
'page_layout',
|
|
156
|
+
'meta_title',
|
|
157
|
+
'meta_keywords',
|
|
158
|
+
'meta_description',
|
|
159
|
+
'active',
|
|
160
|
+
'custom_theme',
|
|
161
|
+
'custom_root_template',
|
|
162
|
+
'layout_update_xml',
|
|
163
|
+
'custom_theme_from',
|
|
164
|
+
'custom_theme_to',
|
|
165
|
+
];
|
|
166
|
+
const payload = {};
|
|
167
|
+
if (requireId != null) payload.id = Number(requireId);
|
|
168
|
+
for (const key of allowed) {
|
|
169
|
+
if (page[key] !== undefined) payload[key] = page[key];
|
|
170
|
+
}
|
|
171
|
+
if (page.is_active !== undefined && payload.active === undefined) {
|
|
172
|
+
payload.active = page.is_active;
|
|
173
|
+
}
|
|
174
|
+
if (page.active !== undefined) payload.active = page.active;
|
|
175
|
+
return payload;
|
|
176
|
+
}
|
|
177
|
+
|
|
144
178
|
async createPage(page) {
|
|
145
|
-
return this.post('/cmsPage', {
|
|
179
|
+
return this.post('/cmsPage', {
|
|
180
|
+
page: this.normalizePagePayload(page),
|
|
181
|
+
});
|
|
146
182
|
}
|
|
147
183
|
|
|
148
184
|
async updatePage(pageId, page) {
|
|
149
|
-
return this.put(`/cmsPage/${pageId}`, {
|
|
185
|
+
return this.put(`/cmsPage/${pageId}`, {
|
|
186
|
+
page: this.normalizePagePayload(page, { requireId: pageId }),
|
|
187
|
+
});
|
|
150
188
|
}
|
|
151
189
|
|
|
152
190
|
async getBlock(blockId) {
|
|
153
191
|
return this.get(`/cmsBlock/${blockId}`);
|
|
154
192
|
}
|
|
155
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Magento 2.4.8 cmsBlock REST uses `active` (not `is_active`), same as cmsPage.
|
|
196
|
+
* Accept either name from callers; always send `active` on the wire.
|
|
197
|
+
*/
|
|
198
|
+
normalizeBlockPayload(block, { requireId } = {}) {
|
|
199
|
+
const allowed = ['title', 'identifier', 'content', 'active'];
|
|
200
|
+
const payload = {};
|
|
201
|
+
if (requireId != null) payload.id = Number(requireId);
|
|
202
|
+
for (const key of allowed) {
|
|
203
|
+
if (block[key] !== undefined) payload[key] = block[key];
|
|
204
|
+
}
|
|
205
|
+
if (block.is_active !== undefined && payload.active === undefined) {
|
|
206
|
+
payload.active = block.is_active;
|
|
207
|
+
}
|
|
208
|
+
if (block.active !== undefined) payload.active = block.active;
|
|
209
|
+
return payload;
|
|
210
|
+
}
|
|
211
|
+
|
|
156
212
|
async createBlock(block) {
|
|
157
|
-
|
|
213
|
+
// Magento REST rejects unsupported fields (e.g. store_id / is_active) on cmsBlock POST.
|
|
214
|
+
return this.post('/cmsBlock', {
|
|
215
|
+
block: this.normalizeBlockPayload(block),
|
|
216
|
+
});
|
|
158
217
|
}
|
|
159
218
|
|
|
160
219
|
async updateBlock(blockId, block) {
|
|
161
220
|
return this.put(`/cmsBlock/${blockId}`, {
|
|
162
|
-
block:
|
|
221
|
+
block: this.normalizeBlockPayload(block, { requireId: blockId }),
|
|
163
222
|
});
|
|
164
223
|
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Search CMS pages by identifier (exact or like).
|
|
227
|
+
* @param {string} identifier
|
|
228
|
+
* @param {'eq'|'like'} [conditionType]
|
|
229
|
+
*/
|
|
230
|
+
async searchPagesByIdentifier(identifier, conditionType = 'eq') {
|
|
231
|
+
const value =
|
|
232
|
+
conditionType === 'like' ? `%${identifier}%` : identifier;
|
|
233
|
+
const q =
|
|
234
|
+
`/cmsPage/search?searchCriteria[filterGroups][0][filters][0][field]=identifier` +
|
|
235
|
+
`&searchCriteria[filterGroups][0][filters][0][value]=${encodeURIComponent(value)}` +
|
|
236
|
+
`&searchCriteria[filterGroups][0][filters][0][conditionType]=${conditionType}` +
|
|
237
|
+
`&searchCriteria[pageSize]=20`;
|
|
238
|
+
return this.get(q);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Search CMS blocks by identifier (exact or like).
|
|
243
|
+
* @param {string} identifier
|
|
244
|
+
* @param {'eq'|'like'} [conditionType]
|
|
245
|
+
*/
|
|
246
|
+
async searchBlocksByIdentifier(identifier, conditionType = 'eq') {
|
|
247
|
+
const value =
|
|
248
|
+
conditionType === 'like' ? `%${identifier}%` : identifier;
|
|
249
|
+
const q =
|
|
250
|
+
`/cmsBlock/search?searchCriteria[filterGroups][0][filters][0][field]=identifier` +
|
|
251
|
+
`&searchCriteria[filterGroups][0][filters][0][value]=${encodeURIComponent(value)}` +
|
|
252
|
+
`&searchCriteria[filterGroups][0][filters][0][conditionType]=${conditionType}` +
|
|
253
|
+
`&searchCriteria[pageSize]=20`;
|
|
254
|
+
return this.get(q);
|
|
255
|
+
}
|
|
165
256
|
}
|
package/src/pageBuilder.js
CHANGED
|
@@ -1,93 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Page Builder
|
|
3
|
-
*
|
|
2
|
+
* Page Builder composition API (project-agnostic).
|
|
3
|
+
* - composeFromFields / composePageBuilderSimpleText: Row → Text (legacy simple)
|
|
4
|
+
* - composeDocument / composeElement: full native content-type tree
|
|
5
|
+
* See pageBuilderElements.js for all Magento_PageBuilder types.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
function looksLikeHtml(value) {
|
|
16
|
-
return /<[^>]+>/.test(value);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function plainTextToParagraphs(text) {
|
|
20
|
-
const parts = String(text)
|
|
21
|
-
.split(/\r\n|\r|\n/)
|
|
22
|
-
.map((l) => l.trim())
|
|
23
|
-
.filter(Boolean)
|
|
24
|
-
.map((l) => `<p>${escapeHtml(l)}</p>`);
|
|
25
|
-
return parts.length ? parts.join('') : '<p></p>';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function normalizeInnerHtml(htmlContent) {
|
|
29
|
-
const t = String(htmlContent || '').trim();
|
|
30
|
-
if (!t) return '<p></p>';
|
|
31
|
-
if (!looksLikeHtml(t)) return plainTextToParagraphs(t);
|
|
32
|
-
return t;
|
|
33
|
-
}
|
|
8
|
+
import {
|
|
9
|
+
composeDocument,
|
|
10
|
+
composeElement,
|
|
11
|
+
StyleBag,
|
|
12
|
+
normalizeHtml,
|
|
13
|
+
escapeHtml,
|
|
14
|
+
PAGEBUILDER_ELEMENT_TYPES,
|
|
15
|
+
describeElements,
|
|
16
|
+
} from './pageBuilderElements.js';
|
|
34
17
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
18
|
+
export {
|
|
19
|
+
composeDocument,
|
|
20
|
+
composeElement,
|
|
21
|
+
StyleBag,
|
|
22
|
+
normalizeHtml,
|
|
23
|
+
escapeHtml,
|
|
24
|
+
PAGEBUILDER_ELEMENT_TYPES,
|
|
25
|
+
describeElements,
|
|
26
|
+
};
|
|
41
27
|
|
|
42
28
|
/**
|
|
43
29
|
* @param {string} htmlContent
|
|
44
30
|
* @returns {string}
|
|
45
31
|
*/
|
|
46
32
|
export function composePageBuilderSimpleText(htmlContent) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
` data-video-lazy-load="true" data-video-fallback-src="" data-element="inner"` +
|
|
56
|
-
` data-pb-style="${rowStyleId}"` +
|
|
57
|
-
` style="justify-content: flex-start; display: flex; flex-direction: column;` +
|
|
58
|
-
` background-position: left top; background-size: cover; background-repeat: no-repeat;` +
|
|
59
|
-
` background-attachment: scroll; border-style: none; border-width: 1px; border-radius: 0px;` +
|
|
60
|
-
` margin: 0px 0px 10px; padding: 10px;">` +
|
|
61
|
-
`<div data-content-type="text" data-appearance="default" data-element="main"` +
|
|
62
|
-
` data-pb-style="${textStyleId}" class="bypass-html-filter"` +
|
|
63
|
-
` style="border-style: none; border-width: 1px; border-radius: 0px; margin: 0px; padding: 0px;">` +
|
|
64
|
-
`${inner}` +
|
|
65
|
-
`</div></div></div>` +
|
|
66
|
-
`<style>` +
|
|
67
|
-
`#html-body [data-pb-style=${rowStyleId}]{justify-content:flex-start;display:flex;flex-direction:column;` +
|
|
68
|
-
`background-position:left top;background-size:cover;background-repeat:no-repeat;` +
|
|
69
|
-
`background-attachment:scroll}` +
|
|
70
|
-
`#html-body [data-pb-style=${textStyleId}]{border-style:none}` +
|
|
71
|
-
`</style>`
|
|
72
|
-
);
|
|
33
|
+
const doc = composeDocument([
|
|
34
|
+
{
|
|
35
|
+
type: 'row',
|
|
36
|
+
appearance: 'contained',
|
|
37
|
+
children: [{ type: 'text', html: htmlContent }],
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
return doc.html;
|
|
73
41
|
}
|
|
74
42
|
|
|
75
43
|
/**
|
|
76
44
|
* @param {{ content?: string, headline?: string, subheadline?: string }} fields
|
|
77
45
|
*/
|
|
78
46
|
export function composeFromFields(fields = {}) {
|
|
79
|
-
const
|
|
47
|
+
const children = [];
|
|
80
48
|
const headline = (fields.headline || '').trim();
|
|
81
49
|
const subheadline = (fields.subheadline || '').trim();
|
|
82
50
|
const content = (fields.content || fields.source_content || '').trim();
|
|
83
51
|
|
|
84
|
-
if (headline)
|
|
85
|
-
|
|
52
|
+
if (headline) {
|
|
53
|
+
children.push({ type: 'heading', text: headline, heading_type: 'h2' });
|
|
54
|
+
}
|
|
55
|
+
if (subheadline) {
|
|
56
|
+
children.push({
|
|
57
|
+
type: 'text',
|
|
58
|
+
html: `<p><strong>${escapeHtml(subheadline)}</strong></p>`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
86
61
|
if (content) {
|
|
87
|
-
|
|
62
|
+
children.push({ type: 'text', html: normalizeHtml(content) });
|
|
63
|
+
}
|
|
64
|
+
if (!children.length) {
|
|
65
|
+
children.push({ type: 'text', html: '<p></p>' });
|
|
88
66
|
}
|
|
89
67
|
|
|
90
|
-
return
|
|
68
|
+
return composeDocument([
|
|
69
|
+
{ type: 'row', appearance: 'contained', children },
|
|
70
|
+
]).html;
|
|
91
71
|
}
|
|
92
72
|
|
|
93
73
|
export function generateIdentifier(title) {
|
|
@@ -99,10 +79,28 @@ export function generateIdentifier(title) {
|
|
|
99
79
|
return value || `page-${Date.now()}`;
|
|
100
80
|
}
|
|
101
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Lightweight check that HTML looks like Page Builder master format.
|
|
84
|
+
* Prefer validatePageBuilderContent for stage-safety.
|
|
85
|
+
*/
|
|
102
86
|
export function validatePageBuilderHtml(html) {
|
|
103
87
|
return (
|
|
104
88
|
typeof html === 'string' &&
|
|
105
89
|
html.includes('data-content-type="row"') &&
|
|
106
|
-
html.includes('data-content-type="text"')
|
|
90
|
+
(html.includes('data-content-type="text"') ||
|
|
91
|
+
html.includes('data-content-type="heading"') ||
|
|
92
|
+
html.includes('data-content-type="banner"') ||
|
|
93
|
+
html.includes('data-content-type="html"') ||
|
|
94
|
+
html.includes('data-content-type="image"') ||
|
|
95
|
+
html.includes('data-content-type="columns"') ||
|
|
96
|
+
html.includes('data-content-type="column-group"') ||
|
|
97
|
+
html.includes('data-content-type="buttons"') ||
|
|
98
|
+
html.includes('data-content-type="slider"') ||
|
|
99
|
+
html.includes('data-content-type="tabs"') ||
|
|
100
|
+
html.includes('data-content-type="products"') ||
|
|
101
|
+
html.includes('data-content-type="block"') ||
|
|
102
|
+
html.includes('data-content-type="video"') ||
|
|
103
|
+
html.includes('data-content-type="divider"') ||
|
|
104
|
+
html.includes('data-content-type="map"'))
|
|
107
105
|
);
|
|
108
106
|
}
|