mcp-magento-cms 1.0.4 → 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.
@@ -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', { page });
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}`, { page: { ...page, id: Number(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
- return this.post('/cmsBlock', { block });
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: { ...block, id: Number(blockId) },
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
  }
@@ -1,93 +1,73 @@
1
1
  /**
2
- * Page Builder simple text: Row (contained) → Text
3
- * Matches Brainvire_CmsContentStudio SimpleTextComposer.
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
- function escapeHtml(value) {
7
- return String(value)
8
- .replace(/&/g, '&')
9
- .replace(/</g, '&lt;')
10
- .replace(/>/g, '&gt;')
11
- .replace(/"/g, '&quot;')
12
- .replace(/'/g, '&#039;');
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
- function generateStyleId(prefix) {
36
- const random = [...Array(12)]
37
- .map(() => Math.floor(Math.random() * 16).toString(16))
38
- .join('');
39
- return `${prefix}${random}`.toUpperCase();
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 inner = normalizeInnerHtml(htmlContent);
48
- const rowStyleId = generateStyleId('R');
49
- const textStyleId = generateStyleId('T');
50
-
51
- return (
52
- `<div data-content-type="row" data-appearance="contained" data-element="main">` +
53
- `<div data-enable-parallax="0" data-parallax-speed="0.5" data-background-images="{}"` +
54
- ` data-background-type="image" data-video-loop="true" data-video-play-only-visible="true"` +
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 chunks = [];
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) chunks.push(`<h2>${escapeHtml(headline)}</h2>`);
85
- if (subheadline) chunks.push(`<p><strong>${escapeHtml(subheadline)}</strong></p>`);
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
- chunks.push(looksLikeHtml(content) ? content : plainTextToParagraphs(content));
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 composePageBuilderSimpleText(chunks.length ? chunks.join('') : '<p></p>');
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
  }