sbuilder-mcp 0.91.0 → 0.93.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 +25 -0
- package/CHANGELOG.vi.md +25 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/dist/catalog/appscaffolds.generated.js +1 -0
- package/dist/catalog/checkout.generated.js +30 -9
- package/dist/catalog/deadkeys.generated.js +3 -3
- package/dist/catalog/elements.generated.js +240 -5
- package/dist/catalog/shapes.generated.js +1 -1
- package/dist/catalog/source.generated.js +2 -2
- package/dist/catalog/storepages.generated.js +9 -0
- package/dist/catalog/translations.generated.js +21 -3
- package/dist/domains/site/findings.js +11 -0
- package/dist/domains/site/record.js +64 -5
- package/dist/domains/site/review.js +131 -9
- package/dist/tools/live.js +32 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@ 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.93.0] - 2026-10-26
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sb_bind` accepts `record.agg.<key>` and `record.money.agg.<key>`, and a text-dataset tile with `recordField: "agg.<key>"` derives its binding the way the platform does.
|
|
13
|
+
- `sb_review` reports `record_aggregate_invalid` for `recordAggregates` that are malformed, which would publish an empty list, or that use `whenNone: "hide"` without an empty state.
|
|
14
|
+
- `sb_review` reports `estimate_invalid` for a form-estimate that does not have exactly one record term, or whose term or rule points at a node of the wrong type.
|
|
15
|
+
- The element catalog gains `form-estimate` and the base-only `recordAggregates` key.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `sb_review` accepts `agg.<key>` and `param.<name>` axes in `record_filter_invalid` when the target list defines them, rejects a param with more than one value, and accepts sort values of `agg.<key>_asc|desc`.
|
|
19
|
+
- `sb_review` reports `record_field_unset` for a tile bound to `agg.<key>` that its list does not define.
|
|
20
|
+
- The API catalog is synced with web_builder v1.0.173 and now covers 124 elements and 95 binding sources.
|
|
21
|
+
|
|
22
|
+
## [0.92.0] - 2026-10-26
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `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.
|
|
26
|
+
- `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.
|
|
27
|
+
- 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_…`.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- `sb_review` no longer reports `no_data_context` or `record_relation_orphan` for nodes outside a list on a record page.
|
|
31
|
+
- `sb_set` no longer writes `optionSource`, `recordLabelField`, `recordParentField` or `recordParentNode` per breakpoint, because they are now base-only config keys.
|
|
32
|
+
- The API catalog is synced with web_builder v1.0.172 and gains the `record` page type, served at `/records/<id>`.
|
|
33
|
+
|
|
9
34
|
## [0.91.0] - 2026-10-23
|
|
10
35
|
|
|
11
36
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,31 @@ 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.93.0] - 2026-10-26
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sb_bind` nhận `record.agg.<key>` và `record.money.agg.<key>`, và tile text-dataset có `recordField: "agg.<key>"` suy ra binding giống nền tảng.
|
|
13
|
+
- `sb_review` báo `record_aggregate_invalid` cho `recordAggregates` sai định dạng (làm danh sách publish rỗng) hoặc dùng `whenNone: "hide"` mà thiếu trạng thái rỗng.
|
|
14
|
+
- `sb_review` báo `estimate_invalid` khi form-estimate không có đúng một term record, hoặc term/rule trỏ vào node sai loại.
|
|
15
|
+
- Catalog element thêm `form-estimate` và khoá base-only `recordAggregates`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `sb_review` chấp nhận axis `agg.<key>` và `param.<name>` trong `record_filter_invalid` khi danh sách đích có định nghĩa, từ chối param có nhiều hơn một giá trị, và chấp nhận giá trị sort `agg.<key>_asc|desc`.
|
|
19
|
+
- `sb_review` báo `record_field_unset` cho tile bind `agg.<key>` mà danh sách của nó không định nghĩa.
|
|
20
|
+
- Catalog API được đồng bộ với web_builder v1.0.173 và nay gồm 124 element và 95 nguồn binding.
|
|
21
|
+
|
|
22
|
+
## [0.92.0] - 2026-10-26
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `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.
|
|
26
|
+
- `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.
|
|
27
|
+
- 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_…`.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- `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.
|
|
31
|
+
- `sb_set` không còn ghi `optionSource`, `recordLabelField`, `recordParentField` hay `recordParentNode` theo breakpoint, vì chúng nay là khoá config base-only.
|
|
32
|
+
- 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>`.
|
|
33
|
+
|
|
9
34
|
## [0.91.0] - 2026-10-23
|
|
10
35
|
|
|
11
36
|
### Added
|
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ in this client: one would put the very key the platform exists to hold back into
|
|
|
119
119
|
| `sb_undo` | Undo / redo this session's writes — page and form edits and `sb_api_call` PUTs; a page is refused if someone else saved it since. The platform's versions and history (`sb_page_version`) outlive this process |
|
|
120
120
|
|
|
121
121
|
Twenty-eight tools, **919 API operations** (344 of the 429 writes carrying a body shape read
|
|
122
|
-
off the handler),
|
|
122
|
+
off the handler), 124 elements, 95 binding sources. `sb_api_find`
|
|
123
123
|
is an index rather than a tool per endpoint, so the tool list stays short while everything
|
|
124
124
|
the platform can do stays reachable — and operations added to the platform arrive with the
|
|
125
125
|
next `npm run codegen`.
|
package/README.vi.md
CHANGED
|
@@ -116,7 +116,7 @@ nhét ngược lại vào mọi bản cài.
|
|
|
116
116
|
| `sb_undo` | Hoàn tác / làm lại các thao tác ghi trong phiên — chỉnh sửa trang, form và PUT qua `sb_api_call`; trang bị từ chối nếu người khác đã lưu sau đó. Version và history của nền tảng (`sb_page_version`) sống lâu hơn process này |
|
|
117
117
|
|
|
118
118
|
Hai mươi tám tool, **919 operation API** (344 trong 429 lệnh ghi có hình dạng body đọc thẳng
|
|
119
|
-
từ handler),
|
|
119
|
+
từ handler), 124 element, 95 nguồn binding. `sb_api_find` là
|
|
120
120
|
một chỉ mục chứ không phải mỗi endpoint một tool, nên danh sách tool vẫn ngắn trong khi mọi
|
|
121
121
|
thứ nền tảng làm được vẫn với tới — và operation mới thêm bên nền tảng sẽ tự có sau lần
|
|
122
122
|
`npm run codegen` kế tiếp.
|
|
@@ -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",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const ELEMENT_SOURCE = {
|
|
2
|
-
"count":
|
|
2
|
+
"count": 124,
|
|
3
3
|
"docSchemaVersion": 2
|
|
4
4
|
};
|
|
5
5
|
export const ELEMENTS = {
|
|
@@ -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%"
|
|
@@ -9837,6 +9846,154 @@ export const ELEMENTS = {
|
|
|
9837
9846
|
"account"
|
|
9838
9847
|
]
|
|
9839
9848
|
},
|
|
9849
|
+
"form-estimate": {
|
|
9850
|
+
"type": "form-estimate",
|
|
9851
|
+
"label": "Estimate",
|
|
9852
|
+
"category": "form",
|
|
9853
|
+
"isContainer": false,
|
|
9854
|
+
"isRootOnly": false,
|
|
9855
|
+
"locked": false,
|
|
9856
|
+
"hideInLayer": false,
|
|
9857
|
+
"childAllows": [],
|
|
9858
|
+
"defaults": {
|
|
9859
|
+
"specials": {
|
|
9860
|
+
"name": "",
|
|
9861
|
+
"label": "Tạm tính",
|
|
9862
|
+
"placeholder": "Chọn phòng và ngày để xem giá dự kiến.",
|
|
9863
|
+
"description": "Giá dự kiến, chưa phải giá giữ chỗ.",
|
|
9864
|
+
"estimateTerms": [],
|
|
9865
|
+
"estimateRules": []
|
|
9866
|
+
},
|
|
9867
|
+
"style": {
|
|
9868
|
+
"width": "100%",
|
|
9869
|
+
"height": "fit-content",
|
|
9870
|
+
"fontSize": "14px",
|
|
9871
|
+
"lineHeight": "150%",
|
|
9872
|
+
"color": "#171717"
|
|
9873
|
+
}
|
|
9874
|
+
},
|
|
9875
|
+
"inspector": [
|
|
9876
|
+
{
|
|
9877
|
+
"tab": "general",
|
|
9878
|
+
"groups": [
|
|
9879
|
+
{
|
|
9880
|
+
"key": "content",
|
|
9881
|
+
"label": "Content",
|
|
9882
|
+
"controls": [
|
|
9883
|
+
"form_estimate_content"
|
|
9884
|
+
]
|
|
9885
|
+
},
|
|
9886
|
+
{
|
|
9887
|
+
"key": "estimate",
|
|
9888
|
+
"label": "Estimate",
|
|
9889
|
+
"controls": [
|
|
9890
|
+
"form_estimate_terms"
|
|
9891
|
+
]
|
|
9892
|
+
},
|
|
9893
|
+
{
|
|
9894
|
+
"key": "estimate_rules",
|
|
9895
|
+
"label": "Stay rules",
|
|
9896
|
+
"controls": [
|
|
9897
|
+
"form_estimate_rules"
|
|
9898
|
+
]
|
|
9899
|
+
},
|
|
9900
|
+
{
|
|
9901
|
+
"key": "typography",
|
|
9902
|
+
"label": "Typography",
|
|
9903
|
+
"controls": [
|
|
9904
|
+
"text_color",
|
|
9905
|
+
"font_family",
|
|
9906
|
+
"font_size",
|
|
9907
|
+
"text_align",
|
|
9908
|
+
"line_height"
|
|
9909
|
+
]
|
|
9910
|
+
},
|
|
9911
|
+
{
|
|
9912
|
+
"key": "size",
|
|
9913
|
+
"label": "Size",
|
|
9914
|
+
"controls": [
|
|
9915
|
+
"width_select",
|
|
9916
|
+
"height_select",
|
|
9917
|
+
"size_bounds"
|
|
9918
|
+
]
|
|
9919
|
+
},
|
|
9920
|
+
{
|
|
9921
|
+
"key": "id_field",
|
|
9922
|
+
"label": "ID field",
|
|
9923
|
+
"controls": [
|
|
9924
|
+
"field_name"
|
|
9925
|
+
]
|
|
9926
|
+
}
|
|
9927
|
+
]
|
|
9928
|
+
},
|
|
9929
|
+
{
|
|
9930
|
+
"tab": "advanced",
|
|
9931
|
+
"groups": [
|
|
9932
|
+
{
|
|
9933
|
+
"key": "spacing",
|
|
9934
|
+
"label": "Spacing",
|
|
9935
|
+
"controls": [
|
|
9936
|
+
"padding_margin"
|
|
9937
|
+
]
|
|
9938
|
+
},
|
|
9939
|
+
{
|
|
9940
|
+
"key": "display",
|
|
9941
|
+
"label": "Display",
|
|
9942
|
+
"controls": [
|
|
9943
|
+
"display"
|
|
9944
|
+
]
|
|
9945
|
+
},
|
|
9946
|
+
{
|
|
9947
|
+
"key": "class_css",
|
|
9948
|
+
"label": "Class",
|
|
9949
|
+
"controls": [
|
|
9950
|
+
"class_css"
|
|
9951
|
+
]
|
|
9952
|
+
}
|
|
9953
|
+
]
|
|
9954
|
+
}
|
|
9955
|
+
],
|
|
9956
|
+
"controls": [
|
|
9957
|
+
"form_estimate_content",
|
|
9958
|
+
"form_estimate_terms",
|
|
9959
|
+
"form_estimate_rules",
|
|
9960
|
+
"text_color",
|
|
9961
|
+
"font_family",
|
|
9962
|
+
"font_size",
|
|
9963
|
+
"text_align",
|
|
9964
|
+
"line_height",
|
|
9965
|
+
"width_select",
|
|
9966
|
+
"height_select",
|
|
9967
|
+
"size_bounds",
|
|
9968
|
+
"field_name",
|
|
9969
|
+
"padding_margin",
|
|
9970
|
+
"display",
|
|
9971
|
+
"class_css"
|
|
9972
|
+
],
|
|
9973
|
+
"description": "A live booking summary inside a form: the price of the record chosen in a data-record select, multiplied by the nights between two date fields and by a number field (\"3.330.000 ₫ × 2 đêm × 2 phòng = 13.320.000 ₫\"), plus the stay rules that go with it — check-in not in the past, check-out after check-in, a maximum number of nights, and guests within the chosen room's capacity. The server recomputes the total from the published record on submit and stores it as an estimate.",
|
|
9974
|
+
"useWhen": [
|
|
9975
|
+
"A booking or quote request whose price is a record (a room type, a tour, a rental) times nights and/or a quantity.",
|
|
9976
|
+
"The visitor should see what their choice costs before sending, and the stay dates need checking."
|
|
9977
|
+
],
|
|
9978
|
+
"avoidWhen": [
|
|
9979
|
+
"The form sells cart products — the order form and its bill already price those.",
|
|
9980
|
+
"The price is not stored on a data record; there is nothing authoritative for the server to recompute from."
|
|
9981
|
+
],
|
|
9982
|
+
"contentTips": [
|
|
9983
|
+
"Point the price at a number column holding WHOLE currency units (1850000, not cents).",
|
|
9984
|
+
"Set the guests and rooms fields to whole numbers with a range, and add a capacity rule so 3 guests cannot book 1 room for 2.",
|
|
9985
|
+
"Keep the note: an estimate is not a held price."
|
|
9986
|
+
],
|
|
9987
|
+
"semantics": [
|
|
9988
|
+
"form",
|
|
9989
|
+
"booking",
|
|
9990
|
+
"estimate",
|
|
9991
|
+
"total",
|
|
9992
|
+
"price",
|
|
9993
|
+
"summary",
|
|
9994
|
+
"nights"
|
|
9995
|
+
]
|
|
9996
|
+
},
|
|
9840
9997
|
"form-segment": {
|
|
9841
9998
|
"type": "form-segment",
|
|
9842
9999
|
"label": "Segment",
|
|
@@ -42909,6 +43066,7 @@ export const ELEMENTS = {
|
|
|
42909
43066
|
"recordFilterField": "",
|
|
42910
43067
|
"recordFilterValue": "",
|
|
42911
43068
|
"recordRelationField": "",
|
|
43069
|
+
"recordAggregates": [],
|
|
42912
43070
|
"itemsPerRow": 4,
|
|
42913
43071
|
"collectionType": "all_products",
|
|
42914
43072
|
"collectionId": "all_products",
|
|
@@ -44016,6 +44174,7 @@ export const ELEMENTS = {
|
|
|
44016
44174
|
"recordFilterField",
|
|
44017
44175
|
"recordFilterValue",
|
|
44018
44176
|
"recordRelationField",
|
|
44177
|
+
"recordAggregates",
|
|
44019
44178
|
"itemsPerRow",
|
|
44020
44179
|
"listNavIcon",
|
|
44021
44180
|
"listNavPosition",
|
|
@@ -44058,6 +44217,7 @@ export const ELEMENTS = {
|
|
|
44058
44217
|
"recordFilterField",
|
|
44059
44218
|
"recordFilterValue",
|
|
44060
44219
|
"recordRelationField",
|
|
44220
|
+
"recordAggregates",
|
|
44061
44221
|
"itemsPerRow",
|
|
44062
44222
|
"listNavIcon",
|
|
44063
44223
|
"listNavPosition",
|
|
@@ -44108,6 +44268,7 @@ export const ELEMENTS = {
|
|
|
44108
44268
|
"recordFilterField",
|
|
44109
44269
|
"recordFilterValue",
|
|
44110
44270
|
"recordRelationField",
|
|
44271
|
+
"recordAggregates",
|
|
44111
44272
|
"itemsPerRow",
|
|
44112
44273
|
"listNavIcon",
|
|
44113
44274
|
"listNavPosition",
|
|
@@ -44160,6 +44321,7 @@ export const ELEMENTS = {
|
|
|
44160
44321
|
"recordFilterField",
|
|
44161
44322
|
"recordFilterValue",
|
|
44162
44323
|
"recordRelationField",
|
|
44324
|
+
"recordAggregates",
|
|
44163
44325
|
"itemsPerRow",
|
|
44164
44326
|
"listNavIcon",
|
|
44165
44327
|
"listNavPosition",
|
|
@@ -44202,6 +44364,7 @@ export const ELEMENTS = {
|
|
|
44202
44364
|
"recordFilterField",
|
|
44203
44365
|
"recordFilterValue",
|
|
44204
44366
|
"recordRelationField",
|
|
44367
|
+
"recordAggregates",
|
|
44205
44368
|
"itemsPerRow",
|
|
44206
44369
|
"listNavIcon",
|
|
44207
44370
|
"listNavPosition",
|
|
@@ -44390,6 +44553,7 @@ export const ELEMENTS = {
|
|
|
44390
44553
|
"recordFilterField",
|
|
44391
44554
|
"recordFilterValue",
|
|
44392
44555
|
"recordRelationField",
|
|
44556
|
+
"recordAggregates",
|
|
44393
44557
|
"itemsPerRow",
|
|
44394
44558
|
"listNavIcon",
|
|
44395
44559
|
"listNavPosition",
|
|
@@ -44502,6 +44666,7 @@ export const ELEMENTS = {
|
|
|
44502
44666
|
"Collection lists: config.collectionListType \"page_collections\" renders THE SUB-COLLECTIONS OF THE COLLECTION THE PAGE IS (the one /collections/{slug} named), so a single collection template draws every collection's browse grid — off a collection template it shows every collection instead; \"all_collections\" renders every collection; \"custom_collections\" renders specials.collectionIds, in that order",
|
|
44503
44667
|
"config.quantity caps how many records render and, under a loadingMode, is the PAGE SIZE; it is BASE-only — one value for every breakpoint, never per-breakpoint. A Data list (datasetSource \"record\") pages at most 50 records per page. config.itemsPerRow is the row size (grid columns / slide page size) and style.gap is the space between cards — both are per-breakpoint",
|
|
44504
44668
|
"A Data list (datasetSource \"record\") nested inside another Data list's row can show ONLY the child records pointing at that row: set config.recordRelationField to the field id (fld_<16hex>) of a \"reference\" field in the nested list's own collection (e.g. RoomType.hotel inside a Hotels list). BASE-only. Such a list is never paged (loadingMode is ignored), its card may also bind the enclosing row's fields, and outside a record-list row it shows its empty state, never all records",
|
|
44669
|
+
"A Data list can carry config.recordAggregates (BASE-only, ≤4): [{key, recordCollectionId (child collection), recordRelationField (child reference field → this collection), op \"min\"|\"max\"|\"count\", recordField (child number/integer field, \"\" for count), where: [{recordField, op:\"gte\", param, per?}], whenNone \"hide\"|\"show\"}]. Each row then binds record.agg.<key> / record.money.agg.<key> (e.g. a hotel's cheapest room that fits: min nightly price where capacity × rooms ≥ guests). Visitors send params as f.record.param.<name>=<int>, filter as f.record.agg.<key>=..3000000, sort as s.<node>=agg.<key>_asc. Not availability",
|
|
44505
44670
|
"config.layout \"grid\" for a multi-row grid, \"slide\" for row-by-row carousel behaviour",
|
|
44506
44671
|
"Navigation and pagination only matter when layout is \"slide\". The arrows exist while config.listNavIcon names an icon (clearing it turns them off); the dots exist while paginationItemWidth and paginationItemHeight are both above 0",
|
|
44507
44672
|
"config.loadingMode is how a GRID list offers the REST of its feed once config.quantity rows are on screen: \"none\" (the default — those rows and nothing more), \"pagination\" (a numbered pager, each page its own URL), \"load_more\" (a button that appends the next page in place) or \"scroll_more\" (the same append, triggered by reaching the end). Set it whenever the listing is a real catalogue rather than a fixed shelf of featured rows",
|
|
@@ -45571,6 +45736,7 @@ export const BINDING_SOURCES = [
|
|
|
45571
45736
|
"site.supportEmail",
|
|
45572
45737
|
"site.supportPhone",
|
|
45573
45738
|
"site.tiktok",
|
|
45739
|
+
"site.timezone",
|
|
45574
45740
|
"site.youtube"
|
|
45575
45741
|
];
|
|
45576
45742
|
export const BOUND_SPECIALS = {
|
|
@@ -45762,6 +45928,67 @@ export const RECORD_BINDING_PROBES = [
|
|
|
45762
45928
|
}
|
|
45763
45929
|
]
|
|
45764
45930
|
},
|
|
45931
|
+
{
|
|
45932
|
+
"type": "text-dataset",
|
|
45933
|
+
"config": {
|
|
45934
|
+
"datasetSource": "record",
|
|
45935
|
+
"recordField": "agg.startPrice",
|
|
45936
|
+
"recordFieldType": "number"
|
|
45937
|
+
},
|
|
45938
|
+
"bindings": [
|
|
45939
|
+
{
|
|
45940
|
+
"id": "bind-text",
|
|
45941
|
+
"source": "record.agg.startPrice",
|
|
45942
|
+
"field": "specials.boundText",
|
|
45943
|
+
"target": {
|
|
45944
|
+
"type": "record",
|
|
45945
|
+
"id": "",
|
|
45946
|
+
"kind": "title"
|
|
45947
|
+
}
|
|
45948
|
+
}
|
|
45949
|
+
]
|
|
45950
|
+
},
|
|
45951
|
+
{
|
|
45952
|
+
"type": "text-dataset",
|
|
45953
|
+
"config": {
|
|
45954
|
+
"datasetSource": "record",
|
|
45955
|
+
"recordField": "agg.startPrice",
|
|
45956
|
+
"recordFieldType": "number",
|
|
45957
|
+
"recordFormat": "money"
|
|
45958
|
+
},
|
|
45959
|
+
"bindings": [
|
|
45960
|
+
{
|
|
45961
|
+
"id": "bind-text",
|
|
45962
|
+
"source": "record.money.agg.startPrice",
|
|
45963
|
+
"field": "specials.boundText",
|
|
45964
|
+
"target": {
|
|
45965
|
+
"type": "record",
|
|
45966
|
+
"id": "",
|
|
45967
|
+
"kind": "title"
|
|
45968
|
+
}
|
|
45969
|
+
}
|
|
45970
|
+
]
|
|
45971
|
+
},
|
|
45972
|
+
{
|
|
45973
|
+
"type": "text-dataset",
|
|
45974
|
+
"config": {
|
|
45975
|
+
"datasetSource": "record",
|
|
45976
|
+
"recordField": "agg.startPrice",
|
|
45977
|
+
"recordFieldType": "richtext"
|
|
45978
|
+
},
|
|
45979
|
+
"bindings": [
|
|
45980
|
+
{
|
|
45981
|
+
"id": "bind-text",
|
|
45982
|
+
"source": "",
|
|
45983
|
+
"field": "specials.boundHtml",
|
|
45984
|
+
"target": {
|
|
45985
|
+
"type": "record",
|
|
45986
|
+
"id": "",
|
|
45987
|
+
"kind": "title"
|
|
45988
|
+
}
|
|
45989
|
+
}
|
|
45990
|
+
]
|
|
45991
|
+
},
|
|
45765
45992
|
{
|
|
45766
45993
|
"type": "text-dataset",
|
|
45767
45994
|
"config": {
|
|
@@ -49348,6 +49575,7 @@ export const ELEMENT_VALUES = {
|
|
|
49348
49575
|
"category",
|
|
49349
49576
|
"manual",
|
|
49350
49577
|
"product",
|
|
49578
|
+
"record",
|
|
49351
49579
|
"tag"
|
|
49352
49580
|
],
|
|
49353
49581
|
"readBy": "OPTION_SOURCES (schema/src/elements/formOptionSource.ts)"
|
|
@@ -49363,6 +49591,7 @@ export const ELEMENT_VALUES = {
|
|
|
49363
49591
|
"category",
|
|
49364
49592
|
"manual",
|
|
49365
49593
|
"product",
|
|
49594
|
+
"record",
|
|
49366
49595
|
"tag"
|
|
49367
49596
|
],
|
|
49368
49597
|
"readBy": "OPTION_SOURCES (schema/src/elements/formOptionSource.ts)"
|
|
@@ -49389,6 +49618,7 @@ export const ELEMENT_VALUES = {
|
|
|
49389
49618
|
"category",
|
|
49390
49619
|
"manual",
|
|
49391
49620
|
"product",
|
|
49621
|
+
"record",
|
|
49392
49622
|
"tag"
|
|
49393
49623
|
],
|
|
49394
49624
|
"readBy": "OPTION_SOURCES (schema/src/elements/formOptionSource.ts)"
|
|
@@ -50186,11 +50416,16 @@ export const BASE_ONLY_CONFIG = [
|
|
|
50186
50416
|
"backgroundSceneUrl",
|
|
50187
50417
|
"backgroundSceneModelUrl",
|
|
50188
50418
|
"rowLimit",
|
|
50419
|
+
"optionSource",
|
|
50420
|
+
"recordAggregates",
|
|
50189
50421
|
"recordCollectionId",
|
|
50190
50422
|
"recordField",
|
|
50191
50423
|
"recordFieldType",
|
|
50192
50424
|
"recordFilterField",
|
|
50193
50425
|
"recordFilterValue",
|
|
50426
|
+
"recordLabelField",
|
|
50427
|
+
"recordParentField",
|
|
50428
|
+
"recordParentNode",
|
|
50194
50429
|
"recordRelationField",
|
|
50195
50430
|
"recordSortDesc",
|
|
50196
50431
|
"recordSortField"
|
|
@@ -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": "e909f77a5b8c33c635657cc906e45b99f6d253d8",
|
|
5
|
+
"committedAt": "2026-10-26T17:18:35+07:00",
|
|
6
6
|
"dirty": false
|
|
7
7
|
};
|
|
@@ -678,6 +678,7 @@ export const PAGE_LAYOUT_SEEDS = {
|
|
|
678
678
|
"recordFilterField": "",
|
|
679
679
|
"recordFilterValue": "",
|
|
680
680
|
"recordRelationField": "",
|
|
681
|
+
"recordAggregates": [],
|
|
681
682
|
"itemsPerRow": 4,
|
|
682
683
|
"collectionType": "all_products",
|
|
683
684
|
"collectionId": "all_products",
|
|
@@ -1624,6 +1625,11 @@ export const PAGE_TYPES = [
|
|
|
1624
1625
|
"ownSlug": false,
|
|
1625
1626
|
"routePattern": "/brands/{slug}"
|
|
1626
1627
|
},
|
|
1628
|
+
{
|
|
1629
|
+
"type": "record",
|
|
1630
|
+
"group": "store",
|
|
1631
|
+
"ownSlug": false
|
|
1632
|
+
},
|
|
1627
1633
|
{
|
|
1628
1634
|
"type": "search",
|
|
1629
1635
|
"group": "main",
|
|
@@ -2962,6 +2968,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
2962
2968
|
"recordFilterField": "",
|
|
2963
2969
|
"recordFilterValue": "",
|
|
2964
2970
|
"recordRelationField": "",
|
|
2971
|
+
"recordAggregates": [],
|
|
2965
2972
|
"itemsPerRow": 4,
|
|
2966
2973
|
"collectionType": "all_products",
|
|
2967
2974
|
"collectionId": "all_products",
|
|
@@ -3683,6 +3690,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
3683
3690
|
"recordFilterField": "",
|
|
3684
3691
|
"recordFilterValue": "",
|
|
3685
3692
|
"recordRelationField": "",
|
|
3693
|
+
"recordAggregates": [],
|
|
3686
3694
|
"itemsPerRow": 4,
|
|
3687
3695
|
"collectionType": "all_products",
|
|
3688
3696
|
"collectionId": "all_products",
|
|
@@ -4413,6 +4421,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
4413
4421
|
"recordFilterField": "",
|
|
4414
4422
|
"recordFilterValue": "",
|
|
4415
4423
|
"recordRelationField": "",
|
|
4424
|
+
"recordAggregates": [],
|
|
4416
4425
|
"itemsPerRow": 4,
|
|
4417
4426
|
"collectionType": "all_products",
|
|
4418
4427
|
"collectionId": "all_products",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-catalog.ts — do not edit by hand.
|
|
2
2
|
// Source: <WB_REPO>/schema/src/elements/translatableFields.ts
|
|
3
3
|
export const TRANSLATION_SOURCE = {
|
|
4
|
-
"elements":
|
|
5
|
-
"pairs":
|
|
6
|
-
"neverKeys":
|
|
4
|
+
"elements": 68,
|
|
5
|
+
"pairs": 190,
|
|
6
|
+
"neverKeys": 182,
|
|
7
7
|
"entityTypes": 12
|
|
8
8
|
};
|
|
9
9
|
/** Every entity type a translation record can name, including "node". */
|
|
@@ -274,6 +274,11 @@ export const TRANSLATABLE_SPECIALS = {
|
|
|
274
274
|
"text",
|
|
275
275
|
"linkText"
|
|
276
276
|
],
|
|
277
|
+
"form-estimate": [
|
|
278
|
+
"label",
|
|
279
|
+
"placeholder",
|
|
280
|
+
"description"
|
|
281
|
+
],
|
|
277
282
|
"form-step-button": [
|
|
278
283
|
"text"
|
|
279
284
|
],
|
|
@@ -407,6 +412,7 @@ export const NEVER_TRANSLATED = [
|
|
|
407
412
|
"displayType",
|
|
408
413
|
"end",
|
|
409
414
|
"endsAt",
|
|
415
|
+
"estimateRules",
|
|
410
416
|
"field",
|
|
411
417
|
"fileFormat",
|
|
412
418
|
"fileLimit",
|
|
@@ -522,6 +528,7 @@ export const NEVER_TRANSLATED = [
|
|
|
522
528
|
"showPercent",
|
|
523
529
|
"showPhone",
|
|
524
530
|
"showPlaceholder",
|
|
531
|
+
"siteTimezone",
|
|
525
532
|
"source",
|
|
526
533
|
"sourceCode",
|
|
527
534
|
"sourceFlashSale",
|
|
@@ -1129,6 +1136,17 @@ export const TRANSLATABLE_FIELDS = {
|
|
|
1129
1136
|
"key": "linkText"
|
|
1130
1137
|
}
|
|
1131
1138
|
],
|
|
1139
|
+
"form-estimate": [
|
|
1140
|
+
{
|
|
1141
|
+
"key": "label"
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
"key": "placeholder"
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
"key": "description"
|
|
1148
|
+
}
|
|
1149
|
+
],
|
|
1132
1150
|
"form-step-button": [
|
|
1133
1151
|
{
|
|
1134
1152
|
"key": "text"
|
|
@@ -120,6 +120,17 @@ 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_aggregate_invalid: 'Fix config.recordAggregates on "<id>" (base): each { key, recordCollectionId (child), recordRelationField ' +
|
|
128
|
+
'(its reference to this list\'s collection), op min|max|count, recordField (child number/integer; "" for count), ' +
|
|
129
|
+
'where: [{ recordField, op: "gte", param, per? }], whenNone hide|show } — and keep the list\'s list-empty.',
|
|
130
|
+
estimate_invalid: 'Point its operands at THIS form\'s nodes: sb_set id "<id>", namespace specials, base, keys { "estimateTerms": ' +
|
|
131
|
+
'[{kind:"record", node:<record form-select>, field:<fld_ price>}, {kind:"nights", start:<date>, end:<date>, unit:"đêm"}, ' +
|
|
132
|
+
'{kind:"number", node:<form-number>, unit:"phòng"}], "estimateRules": [{kind:"dateRange", start, end, notPast:true}, ' +
|
|
133
|
+
'{kind:"maxNights", start, end, max:30}, {kind:"capacity", node, field:<fld_ capacity>, per:<rooms>, count:<guests>}] }.',
|
|
123
134
|
record_filter_invalid: 'Aim it at ONE collection\'s record list(s) and a field: sb_set id "<id>", namespace specials, keys ' +
|
|
124
135
|
'{ "filterSource": "record", "filterAxis": "fld_…", "filterTargets": ["<list id>"], "filterBehavior": "filter" } — ' +
|
|
125
136
|
'axis types text/select/reference/url/date/boolean/integer/number. A sort select takes filterValues ' +
|
|
@@ -12,9 +12,64 @@ export const RECORD_FIELD_ID = /^fld_[0-9a-f]{16}$/;
|
|
|
12
12
|
/**
|
|
13
13
|
* A `record.*` source a context can supply: `record.<fid>`, `record.html.<fid>`,
|
|
14
14
|
* `record.money.<fid>` (a number/integer field formatted in the site's currency),
|
|
15
|
-
*
|
|
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).
|
|
16
17
|
*/
|
|
17
|
-
export const RECORD_SOURCE = /^record\.(?:id|(?:html\.|money\.)?fld_[0-9a-f]{16})$/;
|
|
18
|
+
export const RECORD_SOURCE = /^record\.(?:id|url|(?:html\.|money\.)?fld_[0-9a-f]{16}|(?:money\.)?agg\.[a-z][a-zA-Z0-9_]{0,31})$/;
|
|
19
|
+
/** An aggregate key — recordAggregates.ts RECORD_AGG_KEY. A tile names it as `agg.<key>`. */
|
|
20
|
+
export const RECORD_AGG_KEY = /^[a-z][a-zA-Z0-9_]{0,31}$/;
|
|
21
|
+
const RECORD_PARAM_NAME = /^[a-z][a-z0-9_]{0,23}$/;
|
|
22
|
+
const COLLECTION_ID = /^col_[0-9a-f]{16}$/;
|
|
23
|
+
export const recordAggKeyOf = (axis) => {
|
|
24
|
+
const key = axis.startsWith('agg.') ? axis.slice(4) : '';
|
|
25
|
+
return RECORD_AGG_KEY.test(key) ? key : '';
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* `config.recordAggregates` read the way the platform does
|
|
29
|
+
* (recordAggregates.ts readRecordAggregates): [] for none, null when MALFORMED —
|
|
30
|
+
* the server then refuses the list and it publishes EMPTY.
|
|
31
|
+
*/
|
|
32
|
+
export function readRecordAggregates(raw) {
|
|
33
|
+
if (raw === undefined || raw === null)
|
|
34
|
+
return [];
|
|
35
|
+
if (!Array.isArray(raw) || raw.length > 4)
|
|
36
|
+
return null;
|
|
37
|
+
const s = (v) => (typeof v === 'string' ? v : '');
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const a of raw) {
|
|
41
|
+
if (!a || typeof a !== 'object')
|
|
42
|
+
return null;
|
|
43
|
+
const op = s(a.op);
|
|
44
|
+
const field = s(a.recordField);
|
|
45
|
+
const whenNone = a.whenNone == null || a.whenNone === '' ? 'hide' : s(a.whenNone);
|
|
46
|
+
if (!RECORD_AGG_KEY.test(s(a.key)) ||
|
|
47
|
+
seen.has(s(a.key)) ||
|
|
48
|
+
!['min', 'max', 'count'].includes(op) ||
|
|
49
|
+
!COLLECTION_ID.test(s(a.recordCollectionId)) ||
|
|
50
|
+
!RECORD_FIELD_ID.test(s(a.recordRelationField)) ||
|
|
51
|
+
(op === 'count' ? field !== '' : !RECORD_FIELD_ID.test(field)) ||
|
|
52
|
+
!['hide', 'show'].includes(whenNone)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const where = a.where ?? [];
|
|
56
|
+
if (!Array.isArray(where) || where.length > 4)
|
|
57
|
+
return null;
|
|
58
|
+
const conds = [];
|
|
59
|
+
for (const c of where) {
|
|
60
|
+
const per = s(c?.per);
|
|
61
|
+
if (!c || !RECORD_FIELD_ID.test(s(c.recordField)) || c.op !== 'gte' || !RECORD_PARAM_NAME.test(s(c.param)) || (per !== '' && !RECORD_PARAM_NAME.test(per))) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
conds.push({ param: s(c.param), ...(per ? { per } : {}) });
|
|
65
|
+
}
|
|
66
|
+
seen.add(s(a.key));
|
|
67
|
+
out.push({ key: s(a.key), op, where: conds, whenNone });
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** The visitor params a list's aggregates read (`param.<name>` axes it accepts). */
|
|
72
|
+
export const recordAggregateParams = (aggs) => new Set(aggs.flatMap((a) => a.where.flatMap((c) => (c.per ? [c.param, c.per] : [c.param]))));
|
|
18
73
|
/** The field types `record.money.<fid>` formats — recordContext.ts RECORD_MONEY_FIELD_TYPES. */
|
|
19
74
|
export const RECORD_MONEY_TYPES = new Set(['number', 'integer']);
|
|
20
75
|
/** The only record source `specials.boundHtml` admits — schema/src/rawSinkBindings.ts:102. */
|
|
@@ -34,8 +89,9 @@ export function recordTileBindings(type, config, rows) {
|
|
|
34
89
|
if (!RECORD_TILES.has(type))
|
|
35
90
|
return rows;
|
|
36
91
|
const fid = config.recordField;
|
|
37
|
-
const ok = typeof fid === 'string' && RECORD_FIELD_ID.test(fid);
|
|
38
92
|
const html = type === 'text-dataset' && config.recordFieldType === 'richtext';
|
|
93
|
+
// An aggregate (`agg.<key>`) is a number on the row — datasetBindings.ts recordSource.
|
|
94
|
+
const ok = typeof fid === 'string' && (RECORD_FIELD_ID.test(fid) || (!html && recordAggKeyOf(fid) !== ''));
|
|
39
95
|
// datasetBindings.ts isRecordMoney: a stale recordFormat on any other type is ignored.
|
|
40
96
|
const money = type === 'text-dataset' && config.recordFormat === 'money' && RECORD_MONEY_TYPES.has(String(config.recordFieldType));
|
|
41
97
|
return rows.map((row) => {
|
|
@@ -85,9 +141,12 @@ export function recordScope(nodeAt, id) {
|
|
|
85
141
|
*/
|
|
86
142
|
export function recordFieldProblem(source, scope) {
|
|
87
143
|
if (!scope.length) {
|
|
88
|
-
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.`;
|
|
144
|
+
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.`;
|
|
89
145
|
}
|
|
90
|
-
if (source === 'record.id')
|
|
146
|
+
if (source === 'record.id' || source === 'record.url')
|
|
147
|
+
return undefined;
|
|
148
|
+
// An aggregate is checked against the list's recordAggregates (sb_review), not a field.
|
|
149
|
+
if (/^record\.(?:money\.)?agg\./.test(source))
|
|
91
150
|
return undefined;
|
|
92
151
|
const html = source.startsWith('record.html.');
|
|
93
152
|
const money = source.startsWith('record.money.');
|
|
@@ -7,7 +7,7 @@ import { SEED_COPY_VI, langOf } from './seedcopy.js';
|
|
|
7
7
|
import { HOVER_STATE, hoverHome } from './hover.js';
|
|
8
8
|
import { ELEMENTS, BINDING_SOURCES, BOUND_SPECIALS, FIRST_CHILD_ONLY, SATELLITE_RULES, ELEMENT_SEEDS } from '../../catalog/elements.generated.js';
|
|
9
9
|
import { fill } from './findings.js';
|
|
10
|
-
import { isRecordSource, RECORD_FIELD_ID, RECORD_HTML_SOURCE, RECORD_SOURCE, RECORD_TILES } from './record.js';
|
|
10
|
+
import { isRecordSource, readRecordAggregates, recordAggKeyOf, recordAggregateParams, RECORD_FIELD_ID, RECORD_HTML_SOURCE, RECORD_SOURCE, RECORD_TILES, } from './record.js';
|
|
11
11
|
import { formFieldFindings } from './formdoc.js';
|
|
12
12
|
import { deadNavigation, liveEventTable, PAYLOAD_NEEDS } from './navhref.js';
|
|
13
13
|
import { unknownValueNote, unknownWriteNote, vocabularyFor, vocabularyForWrite } from './vocabulary.js';
|
|
@@ -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,94 @@ 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
|
+
// AGGREGATES OVER A RELATION (web_builder #162, contract §7.5). A malformed
|
|
709
|
+
// definition makes the server refuse the WHOLE list — it publishes empty; a
|
|
710
|
+
// "hide" aggregate drops every row nothing fits, so the list needs its empty state.
|
|
711
|
+
if (type === 'list-dataset' && cfg.datasetSource === 'record' && cfg.recordAggregates !== undefined) {
|
|
712
|
+
const aggs = readRecordAggregates(cfg.recordAggregates);
|
|
713
|
+
const empty = d.nodes[String(cfg.emptyStateId ?? '')]?.data.type === 'list-empty';
|
|
714
|
+
if (aggs === null || (aggs.some((a) => a.whenNone === 'hide') && !empty)) {
|
|
715
|
+
out.push({
|
|
716
|
+
code: 'record_aggregate_invalid',
|
|
717
|
+
nodeId: id,
|
|
718
|
+
type,
|
|
719
|
+
problem: aggs === null
|
|
720
|
+
? 'config.recordAggregates is malformed (a bad key, op, id, condition or more than 4) — the server refuses the list and it publishes EMPTY.'
|
|
721
|
+
: 'An aggregate with whenNone "hide" drops every row nothing fits, and this list has no empty state — a visitor whose search fits nothing sees a blank band.',
|
|
722
|
+
fix: fill('record_aggregate_invalid', { id }),
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// BOOKING ESTIMATE (web_builder #161, contract §7.4): its operands are NODE IDS of
|
|
727
|
+
// this form document. A wrong one leaves the line on its placeholder and the
|
|
728
|
+
// server stores no total — or skips a stay rule — with no error anywhere.
|
|
729
|
+
if (type === 'form-estimate') {
|
|
730
|
+
const sp = (n.specials ?? {});
|
|
731
|
+
const terms = Array.isArray(sp.estimateTerms) ? sp.estimateTerms : [];
|
|
732
|
+
const rules = Array.isArray(sp.estimateRules) ? sp.estimateRules : [];
|
|
733
|
+
const why = [];
|
|
734
|
+
const t = (x) => d.nodes[String(x ?? '')]?.data.type;
|
|
735
|
+
const cfgSrc = (x) => String((d.nodes[String(x ?? '')]?.config?.optionSource ?? '')).trim();
|
|
736
|
+
const isRecordSelect = (x) => t(x) === 'form-select' && cfgSrc(x) === 'record';
|
|
737
|
+
const isDate = (x) => t(x) === 'form-date' || t(x) === 'form-calendar';
|
|
738
|
+
if (terms.filter((x) => x.kind === 'record').length !== 1)
|
|
739
|
+
why.push('it needs exactly ONE "record" term (the chosen room\'s price)');
|
|
740
|
+
for (const x of terms) {
|
|
741
|
+
if (x.kind === 'record' && (!isRecordSelect(x.node) || !RECORD_FIELD_ID.test(String(x.field ?? ''))))
|
|
742
|
+
why.push(`record term: node "${String(x.node ?? '')}" must be a record-sourced form-select and field a fld_ id`);
|
|
743
|
+
if (x.kind === 'nights' && (!isDate(x.start) || !isDate(x.end)))
|
|
744
|
+
why.push('nights term: start and end must be date fields of this form');
|
|
745
|
+
if (x.kind === 'number' && t(x.node) !== 'form-number')
|
|
746
|
+
why.push(`number term: "${String(x.node ?? '')}" is not a form-number`);
|
|
747
|
+
}
|
|
748
|
+
for (const r of rules) {
|
|
749
|
+
if ((r.kind === 'dateRange' || r.kind === 'maxNights') && (!isDate(r.start) || !isDate(r.end)))
|
|
750
|
+
why.push(`${String(r.kind)} rule: start and end must be date fields of this form`);
|
|
751
|
+
if (r.kind === 'capacity' && (!isRecordSelect(r.node) || !RECORD_FIELD_ID.test(String(r.field ?? '')) || t(r.count) !== 'form-number' || (r.per && t(r.per) !== 'form-number')))
|
|
752
|
+
why.push('capacity rule: node must be a record select, field a fld_ id, count (and per) form-number fields');
|
|
753
|
+
}
|
|
754
|
+
if (why.length) {
|
|
755
|
+
out.push({
|
|
756
|
+
code: 'estimate_invalid',
|
|
757
|
+
nodeId: id,
|
|
758
|
+
type,
|
|
759
|
+
problem: `A booking estimate that shows no total or skips a stay rule: ${[...new Set(why)].join('; ')}.`,
|
|
760
|
+
fix: fill('estimate_invalid', { id }),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
673
764
|
// RECORD FILTERS (web_builder PR #157, contract §7.1): a filter/sort control aimed at a
|
|
674
765
|
// record list. Every miss below renders a control that empties the list or narrows nothing.
|
|
675
766
|
{
|
|
@@ -681,11 +772,23 @@ opts = {}) {
|
|
|
681
772
|
});
|
|
682
773
|
const why = [];
|
|
683
774
|
if (sp.filterSource === 'record') {
|
|
684
|
-
|
|
685
|
-
|
|
775
|
+
const cfgOf = (t) => d.nodes[t].config ?? {};
|
|
776
|
+
// web_builder #162: an axis may also be an aggregate (`agg.<key>`, a budget) or a
|
|
777
|
+
// visitor param (`param.<name>`) — both must exist on EVERY targeted list.
|
|
778
|
+
const axis = String(sp.filterAxis ?? '');
|
|
779
|
+
const aggsOf = (t) => readRecordAggregates(cfgOf(t).recordAggregates) ?? [];
|
|
780
|
+
if (axis.startsWith('agg.') || axis.startsWith('param.')) {
|
|
781
|
+
const key = axis.startsWith('agg.') ? recordAggKeyOf(axis) : axis.slice(6);
|
|
782
|
+
const has = (t) => axis.startsWith('agg.') ? aggsOf(t).some((a) => a.key === key) : recordAggregateParams(aggsOf(t)).has(key);
|
|
783
|
+
if (!key || !recordTargets.length || !recordTargets.every(has))
|
|
784
|
+
why.push(`filterAxis "${axis}" names no aggregate or param of its target list's config.recordAggregates`);
|
|
785
|
+
if (axis.startsWith('param.') && sp.filterArity !== undefined && sp.filterArity !== 'single')
|
|
786
|
+
why.push(`a param axis holds ONE value — filterArity "${String(sp.filterArity)}" lets a visitor tick two and empties the list`);
|
|
787
|
+
}
|
|
788
|
+
else if (!RECORD_FIELD_ID.test(axis))
|
|
789
|
+
why.push(`filterAxis "${axis}" is not a field ID`);
|
|
686
790
|
if (targets.length && recordTargets.length !== targets.length)
|
|
687
791
|
why.push('a filterTargets entry is not a record list');
|
|
688
|
-
const cfgOf = (t) => d.nodes[t].config ?? {};
|
|
689
792
|
if (recordTargets.some((t) => String(cfgOf(t).recordRelationField ?? '')))
|
|
690
793
|
why.push('it targets a relation list');
|
|
691
794
|
if (new Set(recordTargets.map((t) => String(cfgOf(t).recordCollectionId ?? ''))).size > 1)
|
|
@@ -696,9 +799,9 @@ opts = {}) {
|
|
|
696
799
|
if (sp.filterSource === 'sort' && recordTargets.length) {
|
|
697
800
|
const bad = (Array.isArray(sp.filterValues) ? sp.filterValues : [])
|
|
698
801
|
.map((v) => String(v?.value ?? ''))
|
|
699
|
-
.filter((v) => !/^fld_[0-9a-f]{16}_(?:asc|desc)$/.test(v));
|
|
802
|
+
.filter((v) => !/^(?:fld_[0-9a-f]{16}|agg\.[a-z][a-zA-Z0-9_]{0,31})_(?:asc|desc)$/.test(v));
|
|
700
803
|
if (bad.length)
|
|
701
|
-
why.push(`sort values ${bad.slice(0, 3).map((v) => `"${v}"`).join(', ')} are not "<fieldId>_asc|desc" — a record list ignores product sort keys`);
|
|
804
|
+
why.push(`sort values ${bad.slice(0, 3).map((v) => `"${v}"`).join(', ')} are not "<fieldId>_asc|desc" or "agg.<key>_asc|desc" — a record list ignores product sort keys`);
|
|
702
805
|
}
|
|
703
806
|
if (why.length) {
|
|
704
807
|
out.push({
|
|
@@ -712,7 +815,25 @@ opts = {}) {
|
|
|
712
815
|
}
|
|
713
816
|
if (RECORD_TILES.has(type) && cfg.datasetSource === 'record') {
|
|
714
817
|
const fid = cfg.recordField;
|
|
715
|
-
|
|
818
|
+
const aggKey = typeof fid === 'string' ? recordAggKeyOf(fid) : '';
|
|
819
|
+
let aggOk = false;
|
|
820
|
+
if (aggKey) {
|
|
821
|
+
let up = n.data.parent;
|
|
822
|
+
while (up && d.nodes[up] && d.nodes[up].data.type !== 'list-dataset')
|
|
823
|
+
up = d.nodes[up].data.parent;
|
|
824
|
+
const lc = up ? d.nodes[up].config : undefined;
|
|
825
|
+
aggOk = (readRecordAggregates(lc?.recordAggregates) ?? []).some((a) => a.key === aggKey);
|
|
826
|
+
}
|
|
827
|
+
if (aggKey && !aggOk) {
|
|
828
|
+
out.push({
|
|
829
|
+
code: 'record_field_unset',
|
|
830
|
+
nodeId: id,
|
|
831
|
+
type,
|
|
832
|
+
problem: `config.recordField "${String(fid)}" names an aggregate its record list does not define in config.recordAggregates — it renders its placeholder on every row.`,
|
|
833
|
+
fix: fill('record_field_unset', { id }),
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
else if (!aggKey && (typeof fid !== 'string' || !RECORD_FIELD_ID.test(fid))) {
|
|
716
837
|
out.push({
|
|
717
838
|
code: 'record_field_unset',
|
|
718
839
|
nodeId: id,
|
|
@@ -730,7 +851,8 @@ opts = {}) {
|
|
|
730
851
|
while (list && d.nodes[list] && d.nodes[list].data.type !== 'list-dataset')
|
|
731
852
|
list = d.nodes[list].data.parent;
|
|
732
853
|
const lcfg = list && d.nodes[list] ? d.nodes[list].config : undefined;
|
|
733
|
-
|
|
854
|
+
// On a record detail page the PAGE supplies the record outside any list.
|
|
855
|
+
if (lcfg?.datasetSource !== 'record' && !(opts.pageType === 'record' && !list)) {
|
|
734
856
|
out.push({
|
|
735
857
|
code: 'no_data_context',
|
|
736
858
|
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.93.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",
|