sbuilder-mcp 0.90.0 → 0.92.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/CHANGELOG.md +23 -0
- package/CHANGELOG.vi.md +23 -0
- package/dist/catalog/appscaffolds.generated.js +6 -0
- package/dist/catalog/checkout.generated.js +30 -9
- package/dist/catalog/deadkeys.generated.js +3 -3
- package/dist/catalog/elements.generated.js +100 -6
- package/dist/catalog/overlays.generated.js +5 -0
- package/dist/catalog/shapes.generated.js +1 -1
- package/dist/catalog/source.generated.js +2 -2
- package/dist/catalog/storepages.generated.js +20 -0
- package/dist/domains/site/builder.js +3 -3
- package/dist/domains/site/findings.js +10 -2
- package/dist/domains/site/record.js +16 -7
- package/dist/domains/site/review.js +78 -2
- package/dist/tools/live.js +32 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ All notable changes to this project are documented in this file.
|
|
|
6
6
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.92.0] - 2026-10-26
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sb_bind` accepts `record.url` and, on a page of type `record`, reads the collection linked through page-links so `record.*` bindings work outside any list, refusing `record.url` while the collection has no detail page.
|
|
13
|
+
- `sb_review` reports `record_option_invalid` for form-select, radio and checkbox controls with `optionSource: "record"` that lack a collection, use a label that is not an ID field, or have a parent that is not a record-sourced form-select.
|
|
14
|
+
- Form controls accept `optionSource: "record"` to take their options from a collection, and form-select supports `specials.urlParam` prefill such as `?hotel=rec_…&room=rec_…`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `sb_review` no longer reports `no_data_context` or `record_relation_orphan` for nodes outside a list on a record page.
|
|
18
|
+
- `sb_set` no longer writes `optionSource`, `recordLabelField`, `recordParentField` or `recordParentNode` per breakpoint, because they are now base-only config keys.
|
|
19
|
+
- The API catalog is synced with web_builder v1.0.172 and gains the `record` page type, served at `/records/<id>`.
|
|
20
|
+
|
|
21
|
+
## [0.91.0] - 2026-10-23
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `sb_bind` accepts `record.money.<fid>` for number and integer Collections fields, showing the stored whole-unit value as money in the site's base currency (1850000 renders as 1.850.000 ₫).
|
|
25
|
+
- `sb_review` reports `record_filter_invalid` for filter and sort controls aimed at a record list whose axis is not an ID field, whose target is not a collection record list or is a relation list, whose behaviour is not `filter`, or whose sort value is not `<fieldId>_asc|desc`.
|
|
26
|
+
- Filter controls accept `filterSource: "record"` to filter and sort record lists through the `?f.record.<fld>~<list>=…&s.<list>=<fld>_desc&p.<list>=2` URL contract.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- `sb_set` re-derives a text record tile's binding when `recordFormat`, `recordField` or `recordFieldType` changes, so choosing `recordFormat: "money"` switches it to `record.money.<fid>`.
|
|
30
|
+
- The API catalog is synced with web_builder v1.0.171, and `recordFormat` is now a base-only config key.
|
|
31
|
+
|
|
9
32
|
## [0.90.0] - 2026-10-22
|
|
10
33
|
|
|
11
34
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,29 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
|
|
|
6
6
|
Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.92.0] - 2026-10-26
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sb_bind` nhận `record.url` và, trên trang loại `record`, đọc collection gắn qua page-links để bind `record.*` ngoài mọi danh sách, đồng thời từ chối `record.url` khi collection chưa có trang chi tiết.
|
|
13
|
+
- `sb_review` báo `record_option_invalid` cho control form-select, radio và checkbox có `optionSource: "record"` thiếu collection, dùng nhãn không phải ID field, hoặc có cha không phải form-select lấy từ record.
|
|
14
|
+
- Các control form nhận `optionSource: "record"` để lấy option từ collection, và form-select hỗ trợ điền sẵn qua `specials.urlParam` như `?hotel=rec_…&room=rec_…`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `sb_review` không còn báo `no_data_context` hay `record_relation_orphan` cho node nằm ngoài danh sách trên trang record.
|
|
18
|
+
- `sb_set` không còn ghi `optionSource`, `recordLabelField`, `recordParentField` hay `recordParentNode` theo breakpoint, vì chúng nay là khoá config base-only.
|
|
19
|
+
- Catalog API được đồng bộ với web_builder v1.0.172 và thêm loại trang `record`, phục vụ tại `/records/<id>`.
|
|
20
|
+
|
|
21
|
+
## [0.91.0] - 2026-10-23
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `sb_bind` nhận `record.money.<fid>` cho field number và integer của Collections, hiển thị giá trị đã lưu (đơn vị nguyên) thành tiền theo tiền tệ gốc của site (1850000 hiện là 1.850.000 ₫).
|
|
25
|
+
- `sb_review` báo `record_filter_invalid` cho control lọc và sắp xếp nhắm vào danh sách record mà axis không phải ID field, target không phải danh sách record của collection hoặc là relation list, behaviour khác `filter`, hay giá trị sort không theo `<fieldId>_asc|desc`.
|
|
26
|
+
- Các control lọc nhận `filterSource: "record"` để lọc và sắp xếp danh sách record qua hợp đồng URL `?f.record.<fld>~<list>=…&s.<list>=<fld>_desc&p.<list>=2`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- `sb_set` suy lại binding của tile text record khi `recordFormat`, `recordField` hoặc `recordFieldType` đổi, nên chọn `recordFormat: "money"` chuyển binding sang `record.money.<fid>`.
|
|
30
|
+
- Catalog API được đồng bộ với web_builder v1.0.171, và `recordFormat` nay là khoá config base-only.
|
|
31
|
+
|
|
9
32
|
## [0.90.0] - 2026-10-22
|
|
10
33
|
|
|
11
34
|
### Added
|
|
@@ -53,6 +53,7 @@ export const APP_SCAFFOLDS = {
|
|
|
53
53
|
"kind": "title",
|
|
54
54
|
"recordField": "",
|
|
55
55
|
"recordFieldType": "",
|
|
56
|
+
"recordFormat": "",
|
|
56
57
|
"textValue": "",
|
|
57
58
|
"descriptionDisplayType": "Full",
|
|
58
59
|
"descriptionLines": 3,
|
|
@@ -98,6 +99,7 @@ export const APP_SCAFFOLDS = {
|
|
|
98
99
|
"kind": "summary",
|
|
99
100
|
"recordField": "",
|
|
100
101
|
"recordFieldType": "",
|
|
102
|
+
"recordFormat": "",
|
|
101
103
|
"textValue": "",
|
|
102
104
|
"descriptionDisplayType": "Full",
|
|
103
105
|
"descriptionLines": 3,
|
|
@@ -143,6 +145,7 @@ export const APP_SCAFFOLDS = {
|
|
|
143
145
|
"kind": "price",
|
|
144
146
|
"recordField": "",
|
|
145
147
|
"recordFieldType": "",
|
|
148
|
+
"recordFormat": "",
|
|
146
149
|
"textValue": "",
|
|
147
150
|
"descriptionDisplayType": "Full",
|
|
148
151
|
"descriptionLines": 3,
|
|
@@ -238,6 +241,7 @@ export const APP_SCAFFOLDS = {
|
|
|
238
241
|
"kind": "description",
|
|
239
242
|
"recordField": "",
|
|
240
243
|
"recordFieldType": "",
|
|
244
|
+
"recordFormat": "",
|
|
241
245
|
"textValue": "",
|
|
242
246
|
"descriptionDisplayType": "Full",
|
|
243
247
|
"descriptionLines": 3,
|
|
@@ -571,6 +575,7 @@ export const APP_SCAFFOLDS = {
|
|
|
571
575
|
"kind": "title",
|
|
572
576
|
"recordField": "",
|
|
573
577
|
"recordFieldType": "",
|
|
578
|
+
"recordFormat": "",
|
|
574
579
|
"textValue": "",
|
|
575
580
|
"descriptionDisplayType": "Full",
|
|
576
581
|
"descriptionLines": 3,
|
|
@@ -616,6 +621,7 @@ export const APP_SCAFFOLDS = {
|
|
|
616
621
|
"kind": "price",
|
|
617
622
|
"recordField": "",
|
|
618
623
|
"recordFieldType": "",
|
|
624
|
+
"recordFormat": "",
|
|
619
625
|
"textValue": "",
|
|
620
626
|
"descriptionDisplayType": "Full",
|
|
621
627
|
"descriptionLines": 3,
|
|
@@ -361,7 +361,11 @@ export const CHECKOUT_FORM_DOCUMENT = {
|
|
|
361
361
|
"width": "100%"
|
|
362
362
|
},
|
|
363
363
|
"config": {
|
|
364
|
-
"optionSource": "manual"
|
|
364
|
+
"optionSource": "manual",
|
|
365
|
+
"recordCollectionId": "",
|
|
366
|
+
"recordLabelField": "",
|
|
367
|
+
"recordParentField": "",
|
|
368
|
+
"recordParentNode": ""
|
|
365
369
|
},
|
|
366
370
|
"specials": {
|
|
367
371
|
"name": "shipping_method",
|
|
@@ -372,7 +376,8 @@ export const CHECKOUT_FORM_DOCUMENT = {
|
|
|
372
376
|
"mapTo": "order.shippingMethod",
|
|
373
377
|
"options": [],
|
|
374
378
|
"defaultValue": "",
|
|
375
|
-
"placeholder": "Chọn phương thức vận chuyển"
|
|
379
|
+
"placeholder": "Chọn phương thức vận chuyển",
|
|
380
|
+
"urlParam": ""
|
|
376
381
|
},
|
|
377
382
|
"responsive": {},
|
|
378
383
|
"events": [],
|
|
@@ -466,7 +471,9 @@ export const CHECKOUT_FORM_DOCUMENT = {
|
|
|
466
471
|
},
|
|
467
472
|
"config": {
|
|
468
473
|
"columns": 1,
|
|
469
|
-
"optionSource": "manual"
|
|
474
|
+
"optionSource": "manual",
|
|
475
|
+
"recordCollectionId": "",
|
|
476
|
+
"recordLabelField": ""
|
|
470
477
|
},
|
|
471
478
|
"specials": {
|
|
472
479
|
"name": "xuat_hoa_don_vat",
|
|
@@ -2271,7 +2278,11 @@ export const FORM_TEMPLATES = {
|
|
|
2271
2278
|
"width": "100%"
|
|
2272
2279
|
},
|
|
2273
2280
|
"config": {
|
|
2274
|
-
"optionSource": "manual"
|
|
2281
|
+
"optionSource": "manual",
|
|
2282
|
+
"recordCollectionId": "",
|
|
2283
|
+
"recordLabelField": "",
|
|
2284
|
+
"recordParentField": "",
|
|
2285
|
+
"recordParentNode": ""
|
|
2275
2286
|
},
|
|
2276
2287
|
"specials": {
|
|
2277
2288
|
"name": "shipping_method",
|
|
@@ -2282,7 +2293,8 @@ export const FORM_TEMPLATES = {
|
|
|
2282
2293
|
"mapTo": "order.shippingMethod",
|
|
2283
2294
|
"options": [],
|
|
2284
2295
|
"defaultValue": "",
|
|
2285
|
-
"placeholder": "Chọn phương thức vận chuyển"
|
|
2296
|
+
"placeholder": "Chọn phương thức vận chuyển",
|
|
2297
|
+
"urlParam": ""
|
|
2286
2298
|
},
|
|
2287
2299
|
"responsive": {},
|
|
2288
2300
|
"events": [],
|
|
@@ -2376,7 +2388,9 @@ export const FORM_TEMPLATES = {
|
|
|
2376
2388
|
},
|
|
2377
2389
|
"config": {
|
|
2378
2390
|
"columns": 1,
|
|
2379
|
-
"optionSource": "manual"
|
|
2391
|
+
"optionSource": "manual",
|
|
2392
|
+
"recordCollectionId": "",
|
|
2393
|
+
"recordLabelField": ""
|
|
2380
2394
|
},
|
|
2381
2395
|
"specials": {
|
|
2382
2396
|
"name": "xuat_hoa_don_vat",
|
|
@@ -4199,7 +4213,11 @@ export const FORM_TEMPLATES = {
|
|
|
4199
4213
|
"width": "100%"
|
|
4200
4214
|
},
|
|
4201
4215
|
"config": {
|
|
4202
|
-
"optionSource": "manual"
|
|
4216
|
+
"optionSource": "manual",
|
|
4217
|
+
"recordCollectionId": "",
|
|
4218
|
+
"recordLabelField": "",
|
|
4219
|
+
"recordParentField": "",
|
|
4220
|
+
"recordParentNode": ""
|
|
4203
4221
|
},
|
|
4204
4222
|
"specials": {
|
|
4205
4223
|
"name": "loai_phong",
|
|
@@ -4214,7 +4232,8 @@ export const FORM_TEMPLATES = {
|
|
|
4214
4232
|
"Phòng gia đình"
|
|
4215
4233
|
],
|
|
4216
4234
|
"defaultValue": "",
|
|
4217
|
-
"placeholder": "Chọn loại phòng"
|
|
4235
|
+
"placeholder": "Chọn loại phòng",
|
|
4236
|
+
"urlParam": ""
|
|
4218
4237
|
},
|
|
4219
4238
|
"responsive": {},
|
|
4220
4239
|
"events": [],
|
|
@@ -4422,7 +4441,9 @@ export const FORM_TEMPLATES = {
|
|
|
4422
4441
|
},
|
|
4423
4442
|
"config": {
|
|
4424
4443
|
"columns": 1,
|
|
4425
|
-
"optionSource": "manual"
|
|
4444
|
+
"optionSource": "manual",
|
|
4445
|
+
"recordCollectionId": "",
|
|
4446
|
+
"recordLabelField": ""
|
|
4426
4447
|
},
|
|
4427
4448
|
"specials": {
|
|
4428
4449
|
"name": "muc_do_hai_long",
|
|
@@ -8304,13 +8304,18 @@ export const ELEMENTS = {
|
|
|
8304
8304
|
"Tùy chọn 3"
|
|
8305
8305
|
],
|
|
8306
8306
|
"defaultValue": "",
|
|
8307
|
-
"placeholder": "Chọn một mục"
|
|
8307
|
+
"placeholder": "Chọn một mục",
|
|
8308
|
+
"urlParam": ""
|
|
8308
8309
|
},
|
|
8309
8310
|
"style": {
|
|
8310
8311
|
"width": "100%"
|
|
8311
8312
|
},
|
|
8312
8313
|
"config": {
|
|
8313
|
-
"optionSource": "manual"
|
|
8314
|
+
"optionSource": "manual",
|
|
8315
|
+
"recordCollectionId": "",
|
|
8316
|
+
"recordLabelField": "",
|
|
8317
|
+
"recordParentField": "",
|
|
8318
|
+
"recordParentNode": ""
|
|
8314
8319
|
}
|
|
8315
8320
|
},
|
|
8316
8321
|
"inspector": [
|
|
@@ -8476,7 +8481,9 @@ export const ELEMENTS = {
|
|
|
8476
8481
|
},
|
|
8477
8482
|
"config": {
|
|
8478
8483
|
"columns": 1,
|
|
8479
|
-
"optionSource": "manual"
|
|
8484
|
+
"optionSource": "manual",
|
|
8485
|
+
"recordCollectionId": "",
|
|
8486
|
+
"recordLabelField": ""
|
|
8480
8487
|
},
|
|
8481
8488
|
"style": {
|
|
8482
8489
|
"width": "100%"
|
|
@@ -8967,7 +8974,9 @@ export const ELEMENTS = {
|
|
|
8967
8974
|
},
|
|
8968
8975
|
"config": {
|
|
8969
8976
|
"columns": 1,
|
|
8970
|
-
"optionSource": "manual"
|
|
8977
|
+
"optionSource": "manual",
|
|
8978
|
+
"recordCollectionId": "",
|
|
8979
|
+
"recordLabelField": ""
|
|
8971
8980
|
},
|
|
8972
8981
|
"style": {
|
|
8973
8982
|
"width": "100%"
|
|
@@ -36144,6 +36153,7 @@ export const ELEMENTS = {
|
|
|
36144
36153
|
"kind": "title",
|
|
36145
36154
|
"recordField": "",
|
|
36146
36155
|
"recordFieldType": "",
|
|
36156
|
+
"recordFormat": "",
|
|
36147
36157
|
"textValue": "",
|
|
36148
36158
|
"descriptionDisplayType": "Full",
|
|
36149
36159
|
"descriptionLines": 3,
|
|
@@ -37197,6 +37207,7 @@ export const ELEMENTS = {
|
|
|
37197
37207
|
"datasetSource",
|
|
37198
37208
|
"recordField",
|
|
37199
37209
|
"recordFieldType",
|
|
37210
|
+
"recordFormat",
|
|
37200
37211
|
"textValue",
|
|
37201
37212
|
"descriptionDisplayType",
|
|
37202
37213
|
"descriptionLines",
|
|
@@ -37221,6 +37232,7 @@ export const ELEMENTS = {
|
|
|
37221
37232
|
"datasetSource",
|
|
37222
37233
|
"recordField",
|
|
37223
37234
|
"recordFieldType",
|
|
37235
|
+
"recordFormat",
|
|
37224
37236
|
"textValue",
|
|
37225
37237
|
"descriptionDisplayType",
|
|
37226
37238
|
"descriptionLines",
|
|
@@ -37245,6 +37257,7 @@ export const ELEMENTS = {
|
|
|
37245
37257
|
"datasetSource",
|
|
37246
37258
|
"recordField",
|
|
37247
37259
|
"recordFieldType",
|
|
37260
|
+
"recordFormat",
|
|
37248
37261
|
"textValue",
|
|
37249
37262
|
"descriptionDisplayType",
|
|
37250
37263
|
"descriptionLines",
|
|
@@ -37268,6 +37281,7 @@ export const ELEMENTS = {
|
|
|
37268
37281
|
"datasetSource",
|
|
37269
37282
|
"recordField",
|
|
37270
37283
|
"recordFieldType",
|
|
37284
|
+
"recordFormat",
|
|
37271
37285
|
"textValue",
|
|
37272
37286
|
"descriptionDisplayType",
|
|
37273
37287
|
"descriptionLines",
|
|
@@ -37284,7 +37298,8 @@ export const ELEMENTS = {
|
|
|
37284
37298
|
"key": "record",
|
|
37285
37299
|
"label": "Data",
|
|
37286
37300
|
"controls": [
|
|
37287
|
-
"record_field"
|
|
37301
|
+
"record_field",
|
|
37302
|
+
"record_format"
|
|
37288
37303
|
]
|
|
37289
37304
|
},
|
|
37290
37305
|
{
|
|
@@ -37404,6 +37419,7 @@ export const ELEMENTS = {
|
|
|
37404
37419
|
"datasetSource",
|
|
37405
37420
|
"recordField",
|
|
37406
37421
|
"recordFieldType",
|
|
37422
|
+
"recordFormat",
|
|
37407
37423
|
"textValue",
|
|
37408
37424
|
"descriptionDisplayType",
|
|
37409
37425
|
"descriptionLines",
|
|
@@ -37417,6 +37433,7 @@ export const ELEMENTS = {
|
|
|
37417
37433
|
"collection",
|
|
37418
37434
|
"article",
|
|
37419
37435
|
"record_field",
|
|
37436
|
+
"record_format",
|
|
37420
37437
|
"description_display",
|
|
37421
37438
|
"width_select",
|
|
37422
37439
|
"height_select",
|
|
@@ -37457,7 +37474,8 @@ export const ELEMENTS = {
|
|
|
37457
37474
|
"Set config.kind to \"title\", \"vendor\", or \"description\"",
|
|
37458
37475
|
"For a description, set config.descriptionDisplayType to \"Compact\" and config.descriptionLines to clamp it; enable specials.moreButtonEnabled for the toggle",
|
|
37459
37476
|
"Customise config.moreText / config.lessText and config.icon (the chevron) for the toggle",
|
|
37460
|
-
"Style per state (default / hover / active) via the state switcher"
|
|
37477
|
+
"Style per state (default / hover / active) via the state switcher",
|
|
37478
|
+
"In a record list on a number/integer field, set config.recordFormat to \"money\" to print it in the site currency (record.money.<fieldId>)"
|
|
37461
37479
|
],
|
|
37462
37480
|
"semantics": [
|
|
37463
37481
|
"product",
|
|
@@ -45690,6 +45708,69 @@ export const RECORD_BINDING_PROBES = [
|
|
|
45690
45708
|
}
|
|
45691
45709
|
]
|
|
45692
45710
|
},
|
|
45711
|
+
{
|
|
45712
|
+
"type": "text-dataset",
|
|
45713
|
+
"config": {
|
|
45714
|
+
"datasetSource": "record",
|
|
45715
|
+
"recordField": "fld_0123456789abcdef",
|
|
45716
|
+
"recordFieldType": "integer",
|
|
45717
|
+
"recordFormat": "money"
|
|
45718
|
+
},
|
|
45719
|
+
"bindings": [
|
|
45720
|
+
{
|
|
45721
|
+
"id": "bind-text",
|
|
45722
|
+
"source": "record.money.fld_0123456789abcdef",
|
|
45723
|
+
"field": "specials.boundText",
|
|
45724
|
+
"target": {
|
|
45725
|
+
"type": "record",
|
|
45726
|
+
"id": "",
|
|
45727
|
+
"kind": "title"
|
|
45728
|
+
}
|
|
45729
|
+
}
|
|
45730
|
+
]
|
|
45731
|
+
},
|
|
45732
|
+
{
|
|
45733
|
+
"type": "text-dataset",
|
|
45734
|
+
"config": {
|
|
45735
|
+
"datasetSource": "record",
|
|
45736
|
+
"recordField": "fld_0123456789abcdef",
|
|
45737
|
+
"recordFieldType": "number",
|
|
45738
|
+
"recordFormat": "money"
|
|
45739
|
+
},
|
|
45740
|
+
"bindings": [
|
|
45741
|
+
{
|
|
45742
|
+
"id": "bind-text",
|
|
45743
|
+
"source": "record.money.fld_0123456789abcdef",
|
|
45744
|
+
"field": "specials.boundText",
|
|
45745
|
+
"target": {
|
|
45746
|
+
"type": "record",
|
|
45747
|
+
"id": "",
|
|
45748
|
+
"kind": "title"
|
|
45749
|
+
}
|
|
45750
|
+
}
|
|
45751
|
+
]
|
|
45752
|
+
},
|
|
45753
|
+
{
|
|
45754
|
+
"type": "text-dataset",
|
|
45755
|
+
"config": {
|
|
45756
|
+
"datasetSource": "record",
|
|
45757
|
+
"recordField": "fld_0123456789abcdef",
|
|
45758
|
+
"recordFieldType": "text",
|
|
45759
|
+
"recordFormat": "money"
|
|
45760
|
+
},
|
|
45761
|
+
"bindings": [
|
|
45762
|
+
{
|
|
45763
|
+
"id": "bind-text",
|
|
45764
|
+
"source": "record.fld_0123456789abcdef",
|
|
45765
|
+
"field": "specials.boundText",
|
|
45766
|
+
"target": {
|
|
45767
|
+
"type": "record",
|
|
45768
|
+
"id": "",
|
|
45769
|
+
"kind": "title"
|
|
45770
|
+
}
|
|
45771
|
+
}
|
|
45772
|
+
]
|
|
45773
|
+
},
|
|
45693
45774
|
{
|
|
45694
45775
|
"type": "text-dataset",
|
|
45695
45776
|
"config": {
|
|
@@ -49276,6 +49357,7 @@ export const ELEMENT_VALUES = {
|
|
|
49276
49357
|
"category",
|
|
49277
49358
|
"manual",
|
|
49278
49359
|
"product",
|
|
49360
|
+
"record",
|
|
49279
49361
|
"tag"
|
|
49280
49362
|
],
|
|
49281
49363
|
"readBy": "OPTION_SOURCES (schema/src/elements/formOptionSource.ts)"
|
|
@@ -49291,6 +49373,7 @@ export const ELEMENT_VALUES = {
|
|
|
49291
49373
|
"category",
|
|
49292
49374
|
"manual",
|
|
49293
49375
|
"product",
|
|
49376
|
+
"record",
|
|
49294
49377
|
"tag"
|
|
49295
49378
|
],
|
|
49296
49379
|
"readBy": "OPTION_SOURCES (schema/src/elements/formOptionSource.ts)"
|
|
@@ -49317,6 +49400,7 @@ export const ELEMENT_VALUES = {
|
|
|
49317
49400
|
"category",
|
|
49318
49401
|
"manual",
|
|
49319
49402
|
"product",
|
|
49403
|
+
"record",
|
|
49320
49404
|
"tag"
|
|
49321
49405
|
],
|
|
49322
49406
|
"readBy": "OPTION_SOURCES (schema/src/elements/formOptionSource.ts)"
|
|
@@ -49547,6 +49631,7 @@ export const ELEMENT_VALUES = {
|
|
|
49547
49631
|
"custom",
|
|
49548
49632
|
"price",
|
|
49549
49633
|
"purchase_history",
|
|
49634
|
+
"record",
|
|
49550
49635
|
"search",
|
|
49551
49636
|
"sort",
|
|
49552
49637
|
"tag"
|
|
@@ -49618,6 +49703,7 @@ export const ELEMENT_VALUES = {
|
|
|
49618
49703
|
"custom",
|
|
49619
49704
|
"price",
|
|
49620
49705
|
"purchase_history",
|
|
49706
|
+
"record",
|
|
49621
49707
|
"search",
|
|
49622
49708
|
"sort",
|
|
49623
49709
|
"tag"
|
|
@@ -49689,6 +49775,7 @@ export const ELEMENT_VALUES = {
|
|
|
49689
49775
|
"custom",
|
|
49690
49776
|
"price",
|
|
49691
49777
|
"purchase_history",
|
|
49778
|
+
"record",
|
|
49692
49779
|
"search",
|
|
49693
49780
|
"sort",
|
|
49694
49781
|
"tag"
|
|
@@ -49760,6 +49847,7 @@ export const ELEMENT_VALUES = {
|
|
|
49760
49847
|
"custom",
|
|
49761
49848
|
"price",
|
|
49762
49849
|
"purchase_history",
|
|
49850
|
+
"record",
|
|
49763
49851
|
"search",
|
|
49764
49852
|
"sort",
|
|
49765
49853
|
"tag"
|
|
@@ -49831,6 +49919,7 @@ export const ELEMENT_VALUES = {
|
|
|
49831
49919
|
"custom",
|
|
49832
49920
|
"price",
|
|
49833
49921
|
"purchase_history",
|
|
49922
|
+
"record",
|
|
49834
49923
|
"search",
|
|
49835
49924
|
"sort",
|
|
49836
49925
|
"tag"
|
|
@@ -50091,6 +50180,7 @@ export const BASE_ONLY_CONFIG = [
|
|
|
50091
50180
|
"openItems",
|
|
50092
50181
|
"kind",
|
|
50093
50182
|
"datasetSource",
|
|
50183
|
+
"recordFormat",
|
|
50094
50184
|
"collectionId",
|
|
50095
50185
|
"collectionType",
|
|
50096
50186
|
"quantity",
|
|
@@ -50108,11 +50198,15 @@ export const BASE_ONLY_CONFIG = [
|
|
|
50108
50198
|
"backgroundSceneUrl",
|
|
50109
50199
|
"backgroundSceneModelUrl",
|
|
50110
50200
|
"rowLimit",
|
|
50201
|
+
"optionSource",
|
|
50111
50202
|
"recordCollectionId",
|
|
50112
50203
|
"recordField",
|
|
50113
50204
|
"recordFieldType",
|
|
50114
50205
|
"recordFilterField",
|
|
50115
50206
|
"recordFilterValue",
|
|
50207
|
+
"recordLabelField",
|
|
50208
|
+
"recordParentField",
|
|
50209
|
+
"recordParentNode",
|
|
50116
50210
|
"recordRelationField",
|
|
50117
50211
|
"recordSortDesc",
|
|
50118
50212
|
"recordSortField"
|
|
@@ -363,6 +363,7 @@ export const OVERLAY_SEEDS = {
|
|
|
363
363
|
"kind": "title",
|
|
364
364
|
"recordField": "",
|
|
365
365
|
"recordFieldType": "",
|
|
366
|
+
"recordFormat": "",
|
|
366
367
|
"textValue": "Product name",
|
|
367
368
|
"descriptionDisplayType": "Full",
|
|
368
369
|
"descriptionLines": 3,
|
|
@@ -1354,6 +1355,7 @@ export const OVERLAY_SEEDS = {
|
|
|
1354
1355
|
"kind": "title",
|
|
1355
1356
|
"recordField": "",
|
|
1356
1357
|
"recordFieldType": "",
|
|
1358
|
+
"recordFormat": "",
|
|
1357
1359
|
"textValue": "",
|
|
1358
1360
|
"descriptionDisplayType": "Full",
|
|
1359
1361
|
"descriptionLines": 3,
|
|
@@ -1769,6 +1771,7 @@ export const OVERLAY_SEEDS = {
|
|
|
1769
1771
|
"kind": "description",
|
|
1770
1772
|
"recordField": "",
|
|
1771
1773
|
"recordFieldType": "",
|
|
1774
|
+
"recordFormat": "",
|
|
1772
1775
|
"textValue": "",
|
|
1773
1776
|
"descriptionDisplayType": "Full",
|
|
1774
1777
|
"descriptionLines": 3,
|
|
@@ -2409,6 +2412,7 @@ export const CART_SEEDS = {
|
|
|
2409
2412
|
"kind": "title",
|
|
2410
2413
|
"recordField": "",
|
|
2411
2414
|
"recordFieldType": "",
|
|
2415
|
+
"recordFormat": "",
|
|
2412
2416
|
"textValue": "Product name",
|
|
2413
2417
|
"descriptionDisplayType": "Full",
|
|
2414
2418
|
"descriptionLines": 3,
|
|
@@ -3245,6 +3249,7 @@ export const CART_SEEDS = {
|
|
|
3245
3249
|
"kind": "title",
|
|
3246
3250
|
"recordField": "",
|
|
3247
3251
|
"recordFieldType": "",
|
|
3252
|
+
"recordFormat": "",
|
|
3248
3253
|
"textValue": "Product name",
|
|
3249
3254
|
"descriptionDisplayType": "Full",
|
|
3250
3255
|
"descriptionLines": 3,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-catalog.ts — do not edit by hand.
|
|
2
2
|
// The platform commit every other generated file here describes.
|
|
3
3
|
export const PLATFORM_SOURCE = {
|
|
4
|
-
"commit": "
|
|
5
|
-
"committedAt": "2026-10-
|
|
4
|
+
"commit": "05168e6e00f55abbddc7895a29a15f030ca060fa",
|
|
5
|
+
"committedAt": "2026-10-26T09:25:34+07:00",
|
|
6
6
|
"dirty": false
|
|
7
7
|
};
|
|
@@ -881,6 +881,7 @@ export const PAGE_LAYOUT_SEEDS = {
|
|
|
881
881
|
"kind": "title",
|
|
882
882
|
"recordField": "",
|
|
883
883
|
"recordFieldType": "",
|
|
884
|
+
"recordFormat": "",
|
|
884
885
|
"textValue": "Article title",
|
|
885
886
|
"descriptionDisplayType": "Full",
|
|
886
887
|
"descriptionLines": 3,
|
|
@@ -926,6 +927,7 @@ export const PAGE_LAYOUT_SEEDS = {
|
|
|
926
927
|
"kind": "summary",
|
|
927
928
|
"recordField": "",
|
|
928
929
|
"recordFieldType": "",
|
|
930
|
+
"recordFormat": "",
|
|
929
931
|
"textValue": "Article summary",
|
|
930
932
|
"descriptionDisplayType": "Full",
|
|
931
933
|
"descriptionLines": 3,
|
|
@@ -971,6 +973,7 @@ export const PAGE_LAYOUT_SEEDS = {
|
|
|
971
973
|
"kind": "date",
|
|
972
974
|
"recordField": "",
|
|
973
975
|
"recordFieldType": "",
|
|
976
|
+
"recordFormat": "",
|
|
974
977
|
"textValue": "2026-08-13",
|
|
975
978
|
"descriptionDisplayType": "Full",
|
|
976
979
|
"descriptionLines": 3,
|
|
@@ -1621,6 +1624,11 @@ export const PAGE_TYPES = [
|
|
|
1621
1624
|
"ownSlug": false,
|
|
1622
1625
|
"routePattern": "/brands/{slug}"
|
|
1623
1626
|
},
|
|
1627
|
+
{
|
|
1628
|
+
"type": "record",
|
|
1629
|
+
"group": "store",
|
|
1630
|
+
"ownSlug": false
|
|
1631
|
+
},
|
|
1624
1632
|
{
|
|
1625
1633
|
"type": "search",
|
|
1626
1634
|
"group": "main",
|
|
@@ -2057,6 +2065,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
2057
2065
|
"kind": "title",
|
|
2058
2066
|
"recordField": "",
|
|
2059
2067
|
"recordFieldType": "",
|
|
2068
|
+
"recordFormat": "",
|
|
2060
2069
|
"textValue": "",
|
|
2061
2070
|
"descriptionDisplayType": "Full",
|
|
2062
2071
|
"descriptionLines": 3,
|
|
@@ -2472,6 +2481,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
2472
2481
|
"kind": "description",
|
|
2473
2482
|
"recordField": "",
|
|
2474
2483
|
"recordFieldType": "",
|
|
2484
|
+
"recordFormat": "",
|
|
2475
2485
|
"textValue": "",
|
|
2476
2486
|
"descriptionDisplayType": "Full",
|
|
2477
2487
|
"descriptionLines": 3,
|
|
@@ -2899,6 +2909,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
2899
2909
|
"kind": "title",
|
|
2900
2910
|
"recordField": "",
|
|
2901
2911
|
"recordFieldType": "",
|
|
2912
|
+
"recordFormat": "",
|
|
2902
2913
|
"textValue": "",
|
|
2903
2914
|
"descriptionDisplayType": "Full",
|
|
2904
2915
|
"descriptionLines": 3,
|
|
@@ -3334,6 +3345,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
3334
3345
|
"kind": "title",
|
|
3335
3346
|
"recordField": "",
|
|
3336
3347
|
"recordFieldType": "",
|
|
3348
|
+
"recordFormat": "",
|
|
3337
3349
|
"textValue": "Product name",
|
|
3338
3350
|
"descriptionDisplayType": "Full",
|
|
3339
3351
|
"descriptionLines": 3,
|
|
@@ -4054,6 +4066,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
4054
4066
|
"kind": "title",
|
|
4055
4067
|
"recordField": "",
|
|
4056
4068
|
"recordFieldType": "",
|
|
4069
|
+
"recordFormat": "",
|
|
4057
4070
|
"textValue": "Product name",
|
|
4058
4071
|
"descriptionDisplayType": "Full",
|
|
4059
4072
|
"descriptionLines": 3,
|
|
@@ -4347,6 +4360,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
4347
4360
|
"kind": "title",
|
|
4348
4361
|
"recordField": "",
|
|
4349
4362
|
"recordFieldType": "",
|
|
4363
|
+
"recordFormat": "",
|
|
4350
4364
|
"textValue": "",
|
|
4351
4365
|
"descriptionDisplayType": "Full",
|
|
4352
4366
|
"descriptionLines": 3,
|
|
@@ -4607,6 +4621,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
4607
4621
|
"kind": "title",
|
|
4608
4622
|
"recordField": "",
|
|
4609
4623
|
"recordFieldType": "",
|
|
4624
|
+
"recordFormat": "",
|
|
4610
4625
|
"textValue": "Article title",
|
|
4611
4626
|
"descriptionDisplayType": "Full",
|
|
4612
4627
|
"descriptionLines": 3,
|
|
@@ -4652,6 +4667,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
4652
4667
|
"kind": "summary",
|
|
4653
4668
|
"recordField": "",
|
|
4654
4669
|
"recordFieldType": "",
|
|
4670
|
+
"recordFormat": "",
|
|
4655
4671
|
"textValue": "Article summary",
|
|
4656
4672
|
"descriptionDisplayType": "Full",
|
|
4657
4673
|
"descriptionLines": 3,
|
|
@@ -4697,6 +4713,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
4697
4713
|
"kind": "date",
|
|
4698
4714
|
"recordField": "",
|
|
4699
4715
|
"recordFieldType": "",
|
|
4716
|
+
"recordFormat": "",
|
|
4700
4717
|
"textValue": "2026-08-13",
|
|
4701
4718
|
"descriptionDisplayType": "Full",
|
|
4702
4719
|
"descriptionLines": 3,
|
|
@@ -4967,6 +4984,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
4967
4984
|
"kind": "title",
|
|
4968
4985
|
"recordField": "",
|
|
4969
4986
|
"recordFieldType": "",
|
|
4987
|
+
"recordFormat": "",
|
|
4970
4988
|
"textValue": "",
|
|
4971
4989
|
"descriptionDisplayType": "Full",
|
|
4972
4990
|
"descriptionLines": 3,
|
|
@@ -5012,6 +5030,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
5012
5030
|
"kind": "date",
|
|
5013
5031
|
"recordField": "",
|
|
5014
5032
|
"recordFieldType": "",
|
|
5033
|
+
"recordFormat": "",
|
|
5015
5034
|
"textValue": "",
|
|
5016
5035
|
"descriptionDisplayType": "Full",
|
|
5017
5036
|
"descriptionLines": 3,
|
|
@@ -5057,6 +5076,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
5057
5076
|
"kind": "description",
|
|
5058
5077
|
"recordField": "",
|
|
5059
5078
|
"recordFieldType": "",
|
|
5079
|
+
"recordFormat": "",
|
|
5060
5080
|
"textValue": "",
|
|
5061
5081
|
"descriptionDisplayType": "Full",
|
|
5062
5082
|
"descriptionLines": 3,
|
|
@@ -512,9 +512,9 @@ export function baseOnlyKeys(doc, id, keys, opts) {
|
|
|
512
512
|
* binding is worse.
|
|
513
513
|
*/
|
|
514
514
|
function rebindPatch(doc, id, keys) {
|
|
515
|
-
// `recordField` / `recordFieldType` are a record tile's field
|
|
516
|
-
// (datasetBindings.ts
|
|
517
|
-
const touchesAxis = ['kind', 'datasetSource', 'recordField', 'recordFieldType'].some((k) => k in keys);
|
|
515
|
+
// `recordField` / `recordFieldType` / `recordFormat` are a record tile's field
|
|
516
|
+
// axis (datasetBindings.ts recordSource) — `setRecordField` / `setRecordFormat` heal on them.
|
|
517
|
+
const touchesAxis = ['kind', 'datasetSource', 'recordField', 'recordFieldType', 'recordFormat'].some((k) => k in keys);
|
|
518
518
|
if (!touchesAxis)
|
|
519
519
|
return null;
|
|
520
520
|
const node = doc.node(id);
|
|
@@ -120,6 +120,14 @@ export const FIX = {
|
|
|
120
120
|
'datasetSource is "record" — one card template, rows edited as data. Logic the platform has ' +
|
|
121
121
|
'no native control for yet (visitor filters, computed totals) may stay in code, reading ' +
|
|
122
122
|
'GET /_wb/collections/{slug}/records instead of its own copy.',
|
|
123
|
+
record_option_invalid: 'Point it at a collection and its label column: sb_set id "<id>", namespace config, keys ' +
|
|
124
|
+
'{ "optionSource": "record", "recordCollectionId": "col_…", "recordLabelField": "fld_…" } (base). A dependent ' +
|
|
125
|
+
'select adds "recordParentNode": "<parent form-select id>" and "recordParentField": "<reference fld_… of this ' +
|
|
126
|
+
'collection pointing at the parent\'s>". Values are record ids; prefill with specials.urlParam.',
|
|
127
|
+
record_filter_invalid: 'Aim it at ONE collection\'s record list(s) and a field: sb_set id "<id>", namespace specials, keys ' +
|
|
128
|
+
'{ "filterSource": "record", "filterAxis": "fld_…", "filterTargets": ["<list id>"], "filterBehavior": "filter" } — ' +
|
|
129
|
+
'axis types text/select/reference/url/date/boolean/integer/number. A sort select takes filterValues ' +
|
|
130
|
+
'[{ "value": "fld_…_asc" | "fld_…_desc", "label": "…" }].',
|
|
123
131
|
record_relation_unset: 'Link it to its parent row: sb_set id "<id>", namespace config, keys { "recordRelationField": "fld_…" } — ' +
|
|
124
132
|
'the ID of a "reference" field in THIS list\'s collection that targets the enclosing list\'s collection ' +
|
|
125
133
|
'(GET /api/sites/{siteId}/collections/{id}/fields, refCollectionId).',
|
|
@@ -129,8 +137,8 @@ export const FIX = {
|
|
|
129
137
|
'e.g. a short "Chưa có dữ liệu" line.',
|
|
130
138
|
record_field_unset: 'Name the field by its ID: sb_set id "<id>", namespace config, keys { "recordField": "fld_…", ' +
|
|
131
139
|
'"recordFieldType": "<the field\'s type>" } — ids and types are on GET /api/sites/{siteId}/collections/{id}/fields. ' +
|
|
132
|
-
'A text-dataset shows text/richtext/number/integer/boolean/date/select/url/reference (integer
|
|
133
|
-
'
|
|
140
|
+
'A text-dataset shows text/richtext/number/integer/boolean/date/select/url/reference (a number/integer ' +
|
|
141
|
+
'prints as money with "recordFormat": "money"); a collection-media shows image. A gallery field binds nothing yet.',
|
|
134
142
|
unread_value: 'Write a value the renderer reads: sb_traits_for "<type>" lists them; sb_set id "<id>" ' +
|
|
135
143
|
'with one, at the same breakpoint it is stored at.',
|
|
136
144
|
unknown_icon: 'Write a real icon name: sb_set id "<id>", namespace <ns>, keys { "<key>": "<name>" } — the ' +
|
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
/** `collections.NewID("fld")` — schema/src/elements/recordContext.ts:33. Anything else binds nothing. */
|
|
11
11
|
export const RECORD_FIELD_ID = /^fld_[0-9a-f]{16}$/;
|
|
12
12
|
/**
|
|
13
|
-
* A `record.*` source a context can supply: `record.<fid>`, `record.html.<fid>`,
|
|
14
|
-
* `record.
|
|
13
|
+
* A `record.*` source a context can supply: `record.<fid>`, `record.html.<fid>`,
|
|
14
|
+
* `record.money.<fid>` (a number/integer field formatted in the site's currency),
|
|
15
|
+
* `record.id` — the row's own record id — or `record.url` — `/records/<id>`, emitted
|
|
16
|
+
* only when the row's collection has a published record detail page (recordContext.ts).
|
|
15
17
|
*/
|
|
16
|
-
export const RECORD_SOURCE = /^record\.(?:id|(?:html\.)?fld_[0-9a-f]{16})$/;
|
|
18
|
+
export const RECORD_SOURCE = /^record\.(?:id|url|(?:html\.|money\.)?fld_[0-9a-f]{16})$/;
|
|
19
|
+
/** The field types `record.money.<fid>` formats — recordContext.ts RECORD_MONEY_FIELD_TYPES. */
|
|
20
|
+
export const RECORD_MONEY_TYPES = new Set(['number', 'integer']);
|
|
17
21
|
/** The only record source `specials.boundHtml` admits — schema/src/rawSinkBindings.ts:102. */
|
|
18
22
|
export const RECORD_HTML_SOURCE = /^record\.html\.fld_[0-9a-f]{16}$/;
|
|
19
23
|
/** The tile elements whose binding the record field drives (editor/src/element/recordList.ts TILE_FIELD_TYPES). */
|
|
@@ -33,6 +37,8 @@ export function recordTileBindings(type, config, rows) {
|
|
|
33
37
|
const fid = config.recordField;
|
|
34
38
|
const ok = typeof fid === 'string' && RECORD_FIELD_ID.test(fid);
|
|
35
39
|
const html = type === 'text-dataset' && config.recordFieldType === 'richtext';
|
|
40
|
+
// datasetBindings.ts isRecordMoney: a stale recordFormat on any other type is ignored.
|
|
41
|
+
const money = type === 'text-dataset' && config.recordFormat === 'money' && RECORD_MONEY_TYPES.has(String(config.recordFieldType));
|
|
36
42
|
return rows.map((row) => {
|
|
37
43
|
const b = row;
|
|
38
44
|
if (b.id === 'bind-image')
|
|
@@ -40,7 +46,7 @@ export function recordTileBindings(type, config, rows) {
|
|
|
40
46
|
if (b.id === 'bind-text') {
|
|
41
47
|
return {
|
|
42
48
|
...b,
|
|
43
|
-
source: ok ? `record.${html ? 'html.' : ''}${fid}` : '',
|
|
49
|
+
source: ok ? `record.${html ? 'html.' : money ? 'money.' : ''}${fid}` : '',
|
|
44
50
|
field: html ? 'specials.boundHtml' : 'specials.boundText',
|
|
45
51
|
};
|
|
46
52
|
}
|
|
@@ -80,12 +86,13 @@ export function recordScope(nodeAt, id) {
|
|
|
80
86
|
*/
|
|
81
87
|
export function recordFieldProblem(source, scope) {
|
|
82
88
|
if (!scope.length) {
|
|
83
|
-
return `"${source}" sits in no record-list row — only a list-dataset with datasetSource "record" (or a relation list inside one) supplies a record, so it would render its placeholder.`;
|
|
89
|
+
return `"${source}" sits in no record-list row and this is no record detail page — only a list-dataset with datasetSource "record" (or a relation list inside one), or a page of type "record" attached to a collection, supplies a record, so it would render its placeholder.`;
|
|
84
90
|
}
|
|
85
|
-
if (source === 'record.id')
|
|
91
|
+
if (source === 'record.id' || source === 'record.url')
|
|
86
92
|
return undefined;
|
|
87
93
|
const html = source.startsWith('record.html.');
|
|
88
|
-
const
|
|
94
|
+
const money = source.startsWith('record.money.');
|
|
95
|
+
const fid = source.slice(html ? 12 : money ? 13 : 7);
|
|
89
96
|
const f = scope.flatMap((s) => s.fields).find((x) => x.id === fid);
|
|
90
97
|
if (!f) {
|
|
91
98
|
const avail = scope
|
|
@@ -95,6 +102,8 @@ export function recordFieldProblem(source, scope) {
|
|
|
95
102
|
}
|
|
96
103
|
if (f.type === 'gallery')
|
|
97
104
|
return `${fid} (${f.key}) is a gallery field, which the renderers do not bind yet — it emits nothing.`;
|
|
105
|
+
if (money && !RECORD_MONEY_TYPES.has(f.type))
|
|
106
|
+
return `${fid} (${f.key}) is ${f.type} — record.money. formats only a number or integer field.`;
|
|
98
107
|
if (html && f.type !== 'richtext')
|
|
99
108
|
return `${fid} (${f.key}) is ${f.type}, not richtext — bind it as "record.${fid}".`;
|
|
100
109
|
if (!html && f.type === 'richtext')
|
|
@@ -636,6 +636,9 @@ opts = {}) {
|
|
|
636
636
|
const parentCfg = up && d.nodes[up] ? d.nodes[up].config : undefined;
|
|
637
637
|
const nested = parentCfg?.datasetSource === 'record';
|
|
638
638
|
const rel = String(cfg.recordRelationField ?? '');
|
|
639
|
+
// A record detail page (web_builder #160) is the parent of a relation list
|
|
640
|
+
// placed on it outside any list.
|
|
641
|
+
const pageParent = opts.pageType === 'record' && !up;
|
|
639
642
|
if (nested && !rel) {
|
|
640
643
|
out.push({
|
|
641
644
|
code: 'record_relation_unset',
|
|
@@ -647,7 +650,7 @@ opts = {}) {
|
|
|
647
650
|
fix: fill('record_relation_unset', { id }),
|
|
648
651
|
});
|
|
649
652
|
}
|
|
650
|
-
else if (rel && (!nested || !RECORD_FIELD_ID.test(rel))) {
|
|
653
|
+
else if (rel && ((!nested && !pageParent) || !RECORD_FIELD_ID.test(rel))) {
|
|
651
654
|
out.push({
|
|
652
655
|
code: 'record_relation_orphan',
|
|
653
656
|
nodeId: id,
|
|
@@ -670,6 +673,78 @@ opts = {}) {
|
|
|
670
673
|
});
|
|
671
674
|
}
|
|
672
675
|
}
|
|
676
|
+
// FORM OPTIONS FROM A COLLECTION (web_builder #159, contract §7.2). Every miss renders
|
|
677
|
+
// a choice field with only its placeholder (no authored fallback), or a room list that
|
|
678
|
+
// never narrows — and the server refuses any other answer as not_an_option.
|
|
679
|
+
if (['form-select', 'form-radio', 'form-checkbox'].includes(type) && String(cfg.optionSource ?? '').trim() === 'record') {
|
|
680
|
+
const why = [];
|
|
681
|
+
if (!String(cfg.recordCollectionId ?? '').trim())
|
|
682
|
+
why.push('config.recordCollectionId names no collection');
|
|
683
|
+
if (!RECORD_FIELD_ID.test(String(cfg.recordLabelField ?? '')))
|
|
684
|
+
why.push('config.recordLabelField is not a field ID');
|
|
685
|
+
const pNode = String(cfg.recordParentNode ?? '');
|
|
686
|
+
const pField = String(cfg.recordParentField ?? '');
|
|
687
|
+
if (pNode || pField) {
|
|
688
|
+
const pc = d.nodes[pNode]?.config;
|
|
689
|
+
if (type !== 'form-select')
|
|
690
|
+
why.push('only a form-select narrows by a parent field');
|
|
691
|
+
else if (!pNode || !pField)
|
|
692
|
+
why.push('recordParentNode and recordParentField go together');
|
|
693
|
+
else if (d.nodes[pNode]?.data.type !== 'form-select' || String(pc?.optionSource ?? '').trim() !== 'record')
|
|
694
|
+
why.push(`recordParentNode "${pNode}" is not a record-sourced form-select`);
|
|
695
|
+
else if (!RECORD_FIELD_ID.test(pField))
|
|
696
|
+
why.push(`recordParentField "${pField}" is not a field ID`);
|
|
697
|
+
}
|
|
698
|
+
if (why.length) {
|
|
699
|
+
out.push({
|
|
700
|
+
code: 'record_option_invalid',
|
|
701
|
+
nodeId: id,
|
|
702
|
+
type,
|
|
703
|
+
problem: `A choice field listing records that shows no options or never narrows: ${why.join('; ')}.`,
|
|
704
|
+
fix: fill('record_option_invalid', { id }),
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// RECORD FILTERS (web_builder PR #157, contract §7.1): a filter/sort control aimed at a
|
|
709
|
+
// record list. Every miss below renders a control that empties the list or narrows nothing.
|
|
710
|
+
{
|
|
711
|
+
const sp = (n.specials ?? {});
|
|
712
|
+
const targets = Array.isArray(sp.filterTargets) ? sp.filterTargets.map(String) : [];
|
|
713
|
+
const recordTargets = targets.filter((t) => {
|
|
714
|
+
const c = d.nodes[t]?.config;
|
|
715
|
+
return d.nodes[t]?.data.type === 'list-dataset' && c?.datasetSource === 'record';
|
|
716
|
+
});
|
|
717
|
+
const why = [];
|
|
718
|
+
if (sp.filterSource === 'record') {
|
|
719
|
+
if (!RECORD_FIELD_ID.test(String(sp.filterAxis ?? '')))
|
|
720
|
+
why.push(`filterAxis "${String(sp.filterAxis ?? '')}" is not a field ID`);
|
|
721
|
+
if (targets.length && recordTargets.length !== targets.length)
|
|
722
|
+
why.push('a filterTargets entry is not a record list');
|
|
723
|
+
const cfgOf = (t) => d.nodes[t].config ?? {};
|
|
724
|
+
if (recordTargets.some((t) => String(cfgOf(t).recordRelationField ?? '')))
|
|
725
|
+
why.push('it targets a relation list');
|
|
726
|
+
if (new Set(recordTargets.map((t) => String(cfgOf(t).recordCollectionId ?? ''))).size > 1)
|
|
727
|
+
why.push('its targets read different collections');
|
|
728
|
+
if (sp.filterBehavior !== undefined && sp.filterBehavior !== 'filter')
|
|
729
|
+
why.push(`filterBehavior "${String(sp.filterBehavior)}" — a record filter only narrows ("filter")`);
|
|
730
|
+
}
|
|
731
|
+
if (sp.filterSource === 'sort' && recordTargets.length) {
|
|
732
|
+
const bad = (Array.isArray(sp.filterValues) ? sp.filterValues : [])
|
|
733
|
+
.map((v) => String(v?.value ?? ''))
|
|
734
|
+
.filter((v) => !/^fld_[0-9a-f]{16}_(?:asc|desc)$/.test(v));
|
|
735
|
+
if (bad.length)
|
|
736
|
+
why.push(`sort values ${bad.slice(0, 3).map((v) => `"${v}"`).join(', ')} are not "<fieldId>_asc|desc" — a record list ignores product sort keys`);
|
|
737
|
+
}
|
|
738
|
+
if (why.length) {
|
|
739
|
+
out.push({
|
|
740
|
+
code: 'record_filter_invalid',
|
|
741
|
+
nodeId: id,
|
|
742
|
+
type,
|
|
743
|
+
problem: `A record ${sp.filterSource === 'sort' ? 'sort' : 'filter'} control that narrows nothing or empties its list: ${why.join('; ')}.`,
|
|
744
|
+
fix: fill('record_filter_invalid', { id }),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
673
748
|
if (RECORD_TILES.has(type) && cfg.datasetSource === 'record') {
|
|
674
749
|
const fid = cfg.recordField;
|
|
675
750
|
if (typeof fid !== 'string' || !RECORD_FIELD_ID.test(fid)) {
|
|
@@ -690,7 +765,8 @@ opts = {}) {
|
|
|
690
765
|
while (list && d.nodes[list] && d.nodes[list].data.type !== 'list-dataset')
|
|
691
766
|
list = d.nodes[list].data.parent;
|
|
692
767
|
const lcfg = list && d.nodes[list] ? d.nodes[list].config : undefined;
|
|
693
|
-
|
|
768
|
+
// On a record detail page the PAGE supplies the record outside any list.
|
|
769
|
+
if (lcfg?.datasetSource !== 'record' && !(opts.pageType === 'record' && !list)) {
|
|
694
770
|
out.push({
|
|
695
771
|
code: 'no_data_context',
|
|
696
772
|
nodeId: id,
|
package/dist/tools/live.js
CHANGED
|
@@ -745,21 +745,23 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
745
745
|
// `force` gets past it, recorded under `forced`.
|
|
746
746
|
if (RECORD_SOURCE.test(source)) {
|
|
747
747
|
const scope = recordScope((x) => (d.has(x) ? d.node(x) : undefined), id);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
748
|
+
const open = session.page();
|
|
749
|
+
const site = siteFor(ctx, open?.siteId);
|
|
750
|
+
const get = async (path) => request({ base: ctx.base, method: 'GET', path: `/api/sites/${encodeURIComponent(site)}${path}`, token: siteToken(ctx), fetchImpl: ctx.fetchImpl });
|
|
751
751
|
let withFields;
|
|
752
752
|
try {
|
|
753
|
+
// A RECORD DETAIL PAGE (web_builder #160) supplies its record outside any list:
|
|
754
|
+
// the collections attached to it are the scope.
|
|
755
|
+
if (!scope.length && open) {
|
|
756
|
+
const links = (await get(`/page-links?pageId=${encodeURIComponent(open.pageId)}&linkType=recordCollection`));
|
|
757
|
+
for (const l of links.links ?? [])
|
|
758
|
+
if (l.linkId)
|
|
759
|
+
scope.push(l.linkId);
|
|
760
|
+
}
|
|
753
761
|
withFields = await Promise.all(scope.map(async (cid) => {
|
|
754
762
|
if (!cid)
|
|
755
763
|
return { collectionId: '(none set)', fields: [] };
|
|
756
|
-
const got = (await
|
|
757
|
-
base: ctx.base,
|
|
758
|
-
method: 'GET',
|
|
759
|
-
path: `/api/sites/${encodeURIComponent(site)}/collections/${encodeURIComponent(cid)}/fields`,
|
|
760
|
-
token: siteToken(ctx),
|
|
761
|
-
fetchImpl: ctx.fetchImpl,
|
|
762
|
-
}));
|
|
764
|
+
const got = (await get(`/collections/${encodeURIComponent(cid)}/fields`));
|
|
763
765
|
return { collectionId: cid, fields: got.fields ?? [] };
|
|
764
766
|
}));
|
|
765
767
|
}
|
|
@@ -776,6 +778,26 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
776
778
|
if (why)
|
|
777
779
|
throw new Error(`sbuilder: ${why}`);
|
|
778
780
|
});
|
|
781
|
+
// record.url resolves only for a collection with an attached record page.
|
|
782
|
+
const cid = scope[0];
|
|
783
|
+
if (source === 'record.url' && cid) {
|
|
784
|
+
let attached = true;
|
|
785
|
+
try {
|
|
786
|
+
await get(`/page-links/recordCollection/${encodeURIComponent(cid)}`);
|
|
787
|
+
}
|
|
788
|
+
catch (e) {
|
|
789
|
+
if (e.status !== 404)
|
|
790
|
+
throw e;
|
|
791
|
+
attached = false;
|
|
792
|
+
}
|
|
793
|
+
soft(guard, () => {
|
|
794
|
+
if (!attached) {
|
|
795
|
+
throw new Error(`sbuilder: collection ${cid} has no record detail page, so record.url is never emitted and the link ` +
|
|
796
|
+
'stays a plain button. Create a page of type "record", attach it with PUT ' +
|
|
797
|
+
`/api/sites/{siteId}/page-links/recordCollection/${cid} {pageId}, and publish it.`);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
}
|
|
779
801
|
}
|
|
780
802
|
}
|
|
781
803
|
const forced = guard.forced.length ? { forced: guard.forced } : {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.92.0",
|
|
4
4
|
"description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/sbuilder-mcp",
|
|
6
6
|
"type": "module",
|