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.
package/dist/changelog.json
CHANGED
|
@@ -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: "
|
|
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.
|
|
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",
|