webcake-landing-mcp 1.6.1 → 1.6.2

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.
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "v": "1.6.2",
4
+ "d": "27/10/2026",
5
+ "type": "Fixed",
6
+ "en": "expand now syncs a field element's properties.field_default with its specials.field_name, turning it on for the editor's system columns…",
7
+ "vi": "expand nay đồng bộ properties.field_default của field element theo specials.field_name: bật cho các cột hệ thống của editor (phone_number,…"
8
+ },
2
9
  {
3
10
  "v": "1.6.1",
4
11
  "d": "21/10/2026",
@@ -33,12 +40,5 @@
33
40
  "type": "Fixed",
34
41
  "en": "The HTTP server now answers a request carrying an unknown mcp-session-id with 404 instead of 400, so clients re-initialize instead of staying stuck…",
35
42
  "vi": "HTTP server nay trả về 404 (thay vì 400) khi nhận request với mcp-session-id không xác định, giúp client tự re-initialize thay vì bị kẹt ở trạng…"
36
- },
37
- {
38
- "v": "1.5.8",
39
- "d": "13/10/2026",
40
- "type": "Fixed",
41
- "en": "The OAuth authorize endpoint now accepts http://localhost/127.0.0.1/[::1] redirect URIs on any port as long as the scheme, host, path, and query…",
42
- "vi": "Endpoint OAuth authorize nay chấp nhận redirect URI http://localhost/127.0.0.1/[::1] ở bất kỳ port nào miễn là scheme, host, path, và query khớp với…"
43
43
  }
44
44
  ]
@@ -58,7 +58,7 @@ export const FORM = [
58
58
  summary: "Single-line input. specials.field_name is the submitted data column (REQUIRED & unique).",
59
59
  useWhen: "Name/email/phone fields. Set field_type to text/email/phone/number. Must be a DIRECT child of the form to submit.",
60
60
  keySpecials: {
61
- field_name: "REQUIRED unique data key. Special names coerce the input type: 'phone_number'/'recheck_phone_number' → tel, 'email' → email. Phone pattern validation only applies when field_name=='phone_number' AND validate=true.",
61
+ field_name: "REQUIRED unique data key. Use the EXACT system names for standard columns — full_name, first_name, last_name, phone_number, recheck_phone_number, email, address, quantity, coupon, postal_code (never 'phone'/'name'/'sdt'/'ho_ten'): the editor locks those IDs and refuses them when typed, so a field saved as 'phone' can never be renamed to 'phone_number' there. Special names coerce the input type: 'phone_number'/'recheck_phone_number' → tel, 'email' → email. Phone pattern validation only applies when field_name=='phone_number' AND validate=true.",
62
62
  field_placeholder: "placeholder text.",
63
63
  field_type: "text | email | phone | number | postal_code | date. 'postal_code' enables postcode-detect helper and uses condition/pattern (country_default|limit_5|limit_6|custom). 'date' renders a date input.",
64
64
  required: "boolean.",
@@ -99,7 +99,7 @@ export const FORM = [
99
99
  desktop: { styles: { top: 60, left: 20, width: 360, height: 40 } },
100
100
  mobile: { styles: { top: 60, left: 20, width: 360, height: 40 } },
101
101
  },
102
- specials: { field_name: "phone", field_placeholder: "Số điện thoại", field_type: "phone", required: true },
102
+ specials: { field_name: "phone_number", field_placeholder: "Số điện thoại", field_type: "phone", required: true },
103
103
  },
104
104
  },
105
105
  {
@@ -389,3 +389,37 @@ export const FORM = [
389
389
  },
390
390
  },
391
391
  ];
392
+ /**
393
+ * The editor's SYSTEM form columns — the `default: true` fields of its "add
394
+ * field" panel (landing_page_backend assets/editor/main/panels/FormPanel.vue).
395
+ * The editor creates them with `properties.field_default = true`, which LOCKS
396
+ * the ID box (FieldTrait.vue `disabled`), and it refuses these names when typed
397
+ * (FieldTrait.vue `defaultFieldName` appends a random suffix). So a field must
398
+ * be BORN with the canonical name: a field saved as "phone" can never become
399
+ * "phone_number" in the editor, and only the canonical names get the editor's
400
+ * extra behaviour (phone validation, address autocomplete, quantity, coupon…).
401
+ */
402
+ export const SYSTEM_FIELD_NAMES = new Set([
403
+ "full_name", "first_name", "last_name", "phone_number", "recheck_phone_number",
404
+ "email", "quantity", "coupon", "address", "postal_code", "country_select",
405
+ "verify_code", "access_key", "province_id/district_id/commune_id",
406
+ ]);
407
+ /** Common hand-made spellings of a system column (slugged: lowercase, no accents, `_`). */
408
+ const SYSTEM_FIELD_ALIASES = {
409
+ phone: "phone_number", phonenumber: "phone_number", phone_no: "phone_number", tel: "phone_number",
410
+ telephone: "phone_number", mobile: "phone_number", sdt: "phone_number", so_dt: "phone_number",
411
+ so_dien_thoai: "phone_number", sodienthoai: "phone_number", dien_thoai: "phone_number", dienthoai: "phone_number",
412
+ name: "full_name", fullname: "full_name", your_name: "full_name", customer_name: "full_name",
413
+ ho_ten: "full_name", hoten: "full_name", ho_va_ten: "full_name", ten: "full_name",
414
+ firstname: "first_name", lastname: "last_name",
415
+ mail: "email", e_mail: "email", email_address: "email",
416
+ dia_chi: "address", diachi: "address",
417
+ so_luong: "quantity", soluong: "quantity", qty: "quantity",
418
+ ma_giam_gia: "coupon", voucher: "coupon",
419
+ };
420
+ /** The system column a non-canonical field_name was clearly meant to be, else undefined. */
421
+ export function systemFieldAlias(fieldName) {
422
+ const slug = fieldName.normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/gi, "d")
423
+ .toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
424
+ return SYSTEM_FIELD_ALIASES[slug];
425
+ }
@@ -2,6 +2,7 @@ import { CANVAS, EVENT_TRIGGERS, CLICK_ACTIONS, HOVER_ACTIONS, SUCCESS_ACTIONS,
2
2
  import { GENERATION_GUIDE } from "./guide.js";
3
3
  import { INSTRUCTIONS } from "./instructions.js";
4
4
  import { LIBRARY, ELEMENT_TYPES, CONTAINER_TYPES, FIELD_TYPES, createElement } from "./elements/index.js";
5
+ import { SYSTEM_FIELD_NAMES } from "./elements/form.js";
5
6
  import { createPageSource } from "./page.js";
6
7
  import { canvasToPageSource } from "./canvas-to-source.js";
7
8
  import { validatePage, coercePage, pageSchema } from "./validate.js";
@@ -389,6 +390,32 @@ function normalizeSettings(source) {
389
390
  ws.desktop ??= CANVAS.desktopWidth;
390
391
  ws.mobile ??= CANVAS.mobileWidth;
391
392
  }
393
+ // ---------------------------------------------------------------------------
394
+ // form-field lock normalization: the editor locks a field's ID box when
395
+ // `properties.field_default` is true (FieldTrait.vue `disabled`) and only ever
396
+ // sets it on its SYSTEM columns (phone_number, full_name, email…). A model that
397
+ // copies `field_default:true` onto a custom field ("message", "note") leaves
398
+ // the user an ID they can NEVER change; a system column without it is editable
399
+ // in a way the editor itself never allows. So the flag follows the name:
400
+ // on for system names, off for everything else. Deterministic + idempotent,
401
+ // so the expand(compact(x)) == expand(x) invariant holds.
402
+ // ---------------------------------------------------------------------------
403
+ /** Walk a tree node and sync properties.field_default with the field_name in-place (mutates). */
404
+ function normalizeFormFields(node) {
405
+ if (!node || typeof node !== "object")
406
+ return;
407
+ const fn = node.specials?.field_name;
408
+ if (FIELD_TYPES.has(node.type) && typeof fn === "string" && node.properties && typeof node.properties === "object") {
409
+ if (SYSTEM_FIELD_NAMES.has(fn))
410
+ node.properties.field_default = true;
411
+ else
412
+ delete node.properties.field_default;
413
+ }
414
+ if (Array.isArray(node.children)) {
415
+ for (const child of node.children)
416
+ normalizeFormFields(child);
417
+ }
418
+ }
392
419
  /** Apply all post-expand normalizations to every node in a page source. */
393
420
  function normalizeSource(source) {
394
421
  if (!source || typeof source !== "object")
@@ -405,6 +432,7 @@ function normalizeSource(source) {
405
432
  // url() layer in the same value.
406
433
  normalizeColors(node);
407
434
  normalizeBackgrounds(node);
435
+ normalizeFormFields(node);
408
436
  }
409
437
  }
410
438
  }
@@ -9,6 +9,7 @@
9
9
  import { readFileSync } from "node:fs";
10
10
  import Ajv2020Module from "ajv/dist/2020.js";
11
11
  import { CONTAINER_TYPES, FIELD_TYPES } from "./elements/index.js";
12
+ import { systemFieldAlias } from "./elements/form.js";
12
13
  import { ANIMATABLE_TYPES, ANIMATION_NAMES } from "./vocab.js";
13
14
  import { estTextHeightPx, measureTextBlock } from "./text-metrics.js";
14
15
  import { isColorKey, isUnparseableByEditor } from "./color.js";
@@ -316,6 +317,12 @@ export function validatePage(input) {
316
317
  if (!fn || typeof fn !== "string" || fn.trim() === "") {
317
318
  warnings.push(`${path} (${type}): form input should have a unique specials.field_name.`);
318
319
  }
320
+ else {
321
+ const canonical = systemFieldAlias(fn);
322
+ if (canonical && canonical !== fn) {
323
+ warnings.push(`${path} (${type}): field_name "${fn}" should be the system column "${canonical}" — the Webcake editor reserves system names (typing "${canonical}" there gets a random suffix), so the user can NEVER rename this field to "${canonical}" later, and only "${canonical}" gets the editor's built-in behaviour. Rename it now (also update any {{${fn}}} placeholder). On an EXISTING page this changes the submitted data column — ask the user first.`);
324
+ }
325
+ }
319
326
  // `field_placeholder` is the ONLY placeholder key the renderer reads. A stray
320
327
  // `placeholder` renders blank; a select with no field_placeholder crashes the
321
328
  // published renderer (unescapeHTML(undefined)).
package/dist/smoke.js CHANGED
@@ -187,6 +187,36 @@ console.log("== expand: relocates misplaced responsive.<bp>.animation into confi
187
187
  const kept = landingDomain.expand(both).page[0].children[0].responsive.desktop;
188
188
  check("explicit config.animation wins over the stray key", kept.config.animation.name === "zoomIn" && kept.animation === undefined, kept.config);
189
189
  }
190
+ console.log("== form fields: field_default follows the system field_name; alias names warn ==");
191
+ {
192
+ // The editor locks a field's ID when properties.field_default is true and
193
+ // refuses system names when typed — so the lock must match the name, and a
194
+ // hand-made "phone" must be flagged before it is saved un-renameable.
195
+ const mk = (fields) => ({
196
+ page: [{ id: "s1", type: "section", responsive: { desktop: { styles: { height: 400 } }, mobile: { styles: { height: 400 } } },
197
+ children: [{ id: "f1", type: "form",
198
+ responsive: { desktop: { styles: { top: 10, left: 10, width: 400, height: 300 } }, mobile: { styles: { top: 10, left: 10, width: 400, height: 300 } } },
199
+ children: fields.map((f, i) => ({ id: `i${i}`, type: f.type ?? "input", properties: f.properties,
200
+ responsive: { desktop: { styles: { top: i * 50, left: 0, width: 300, height: 40 } }, mobile: { styles: { top: i * 50, left: 0, width: 300, height: 40 } } },
201
+ specials: { field_name: f.name, field_placeholder: "x" } })) }] }],
202
+ popup: [], settings: {},
203
+ });
204
+ const ex = landingDomain.expand(mk([
205
+ { name: "phone_number" },
206
+ { name: "message", properties: { field_default: true } },
207
+ { name: "phone" },
208
+ ]));
209
+ const kids = ex.page[0].children[0].children;
210
+ check("system field gets field_default:true", kids[0].properties.field_default === true, kids[0].properties);
211
+ check("custom field never locked", kids[1].properties.field_default === undefined, kids[1].properties);
212
+ check("field_default normalization idempotent", JSON.stringify(landingDomain.expand(ex)) === JSON.stringify(ex));
213
+ const w = validatePage(ex).warnings;
214
+ check("alias 'phone' warns → phone_number", w.some((m) => m.includes('"phone"') && m.includes('"phone_number"')), w);
215
+ check("canonical/custom names don't warn", !w.some((m) => m.includes('"message" should') || m.includes('"phone_number" should')), w);
216
+ const vi = landingDomain.expand(mk([{ name: "Số điện thoại" }, { name: "Họ tên" }]));
217
+ const wv = validatePage(vi).warnings;
218
+ check("Vietnamese aliases warn", wv.some((m) => m.includes('"phone_number"')) && wv.some((m) => m.includes('"full_name"')), wv);
219
+ }
190
220
  console.log("== validate: accepts JSON string input ==");
191
221
  const r3 = validatePage(JSON.stringify(good));
192
222
  check("string input parsed & valid", r3.valid, r3.errors);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-landing-mcp",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "MCP server exposing Webcake landing-page element schemas + AI usage hints, and persisting LLM-generated page sources to a Webcake backend.",
5
5
  "mcpName": "io.github.vuluu2k/webcake-landing-mcp",
6
6
  "type": "module",