sbuilder-mcp 0.92.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 +13 -0
- package/CHANGELOG.vi.md +13 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/dist/catalog/appscaffolds.generated.js +1 -0
- package/dist/catalog/deadkeys.generated.js +3 -3
- package/dist/catalog/elements.generated.js +220 -1
- package/dist/catalog/shapes.generated.js +1 -1
- package/dist/catalog/source.generated.js +2 -2
- package/dist/catalog/storepages.generated.js +4 -0
- package/dist/catalog/translations.generated.js +21 -3
- package/dist/domains/site/findings.js +7 -0
- package/dist/domains/site/record.js +60 -2
- package/dist/domains/site/review.js +93 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ 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
|
+
|
|
9
22
|
## [0.92.0] - 2026-10-26
|
|
10
23
|
|
|
11
24
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,19 @@ 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
|
+
|
|
9
22
|
## [0.92.0] - 2026-10-26
|
|
10
23
|
|
|
11
24
|
### 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.
|
|
@@ -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 = {
|
|
@@ -9846,6 +9846,154 @@ export const ELEMENTS = {
|
|
|
9846
9846
|
"account"
|
|
9847
9847
|
]
|
|
9848
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
|
+
},
|
|
9849
9997
|
"form-segment": {
|
|
9850
9998
|
"type": "form-segment",
|
|
9851
9999
|
"label": "Segment",
|
|
@@ -42918,6 +43066,7 @@ export const ELEMENTS = {
|
|
|
42918
43066
|
"recordFilterField": "",
|
|
42919
43067
|
"recordFilterValue": "",
|
|
42920
43068
|
"recordRelationField": "",
|
|
43069
|
+
"recordAggregates": [],
|
|
42921
43070
|
"itemsPerRow": 4,
|
|
42922
43071
|
"collectionType": "all_products",
|
|
42923
43072
|
"collectionId": "all_products",
|
|
@@ -44025,6 +44174,7 @@ export const ELEMENTS = {
|
|
|
44025
44174
|
"recordFilterField",
|
|
44026
44175
|
"recordFilterValue",
|
|
44027
44176
|
"recordRelationField",
|
|
44177
|
+
"recordAggregates",
|
|
44028
44178
|
"itemsPerRow",
|
|
44029
44179
|
"listNavIcon",
|
|
44030
44180
|
"listNavPosition",
|
|
@@ -44067,6 +44217,7 @@ export const ELEMENTS = {
|
|
|
44067
44217
|
"recordFilterField",
|
|
44068
44218
|
"recordFilterValue",
|
|
44069
44219
|
"recordRelationField",
|
|
44220
|
+
"recordAggregates",
|
|
44070
44221
|
"itemsPerRow",
|
|
44071
44222
|
"listNavIcon",
|
|
44072
44223
|
"listNavPosition",
|
|
@@ -44117,6 +44268,7 @@ export const ELEMENTS = {
|
|
|
44117
44268
|
"recordFilterField",
|
|
44118
44269
|
"recordFilterValue",
|
|
44119
44270
|
"recordRelationField",
|
|
44271
|
+
"recordAggregates",
|
|
44120
44272
|
"itemsPerRow",
|
|
44121
44273
|
"listNavIcon",
|
|
44122
44274
|
"listNavPosition",
|
|
@@ -44169,6 +44321,7 @@ export const ELEMENTS = {
|
|
|
44169
44321
|
"recordFilterField",
|
|
44170
44322
|
"recordFilterValue",
|
|
44171
44323
|
"recordRelationField",
|
|
44324
|
+
"recordAggregates",
|
|
44172
44325
|
"itemsPerRow",
|
|
44173
44326
|
"listNavIcon",
|
|
44174
44327
|
"listNavPosition",
|
|
@@ -44211,6 +44364,7 @@ export const ELEMENTS = {
|
|
|
44211
44364
|
"recordFilterField",
|
|
44212
44365
|
"recordFilterValue",
|
|
44213
44366
|
"recordRelationField",
|
|
44367
|
+
"recordAggregates",
|
|
44214
44368
|
"itemsPerRow",
|
|
44215
44369
|
"listNavIcon",
|
|
44216
44370
|
"listNavPosition",
|
|
@@ -44399,6 +44553,7 @@ export const ELEMENTS = {
|
|
|
44399
44553
|
"recordFilterField",
|
|
44400
44554
|
"recordFilterValue",
|
|
44401
44555
|
"recordRelationField",
|
|
44556
|
+
"recordAggregates",
|
|
44402
44557
|
"itemsPerRow",
|
|
44403
44558
|
"listNavIcon",
|
|
44404
44559
|
"listNavPosition",
|
|
@@ -44511,6 +44666,7 @@ export const ELEMENTS = {
|
|
|
44511
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",
|
|
44512
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",
|
|
44513
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",
|
|
44514
44670
|
"config.layout \"grid\" for a multi-row grid, \"slide\" for row-by-row carousel behaviour",
|
|
44515
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",
|
|
44516
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",
|
|
@@ -45580,6 +45736,7 @@ export const BINDING_SOURCES = [
|
|
|
45580
45736
|
"site.supportEmail",
|
|
45581
45737
|
"site.supportPhone",
|
|
45582
45738
|
"site.tiktok",
|
|
45739
|
+
"site.timezone",
|
|
45583
45740
|
"site.youtube"
|
|
45584
45741
|
];
|
|
45585
45742
|
export const BOUND_SPECIALS = {
|
|
@@ -45771,6 +45928,67 @@ export const RECORD_BINDING_PROBES = [
|
|
|
45771
45928
|
}
|
|
45772
45929
|
]
|
|
45773
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
|
+
},
|
|
45774
45992
|
{
|
|
45775
45993
|
"type": "text-dataset",
|
|
45776
45994
|
"config": {
|
|
@@ -50199,6 +50417,7 @@ export const BASE_ONLY_CONFIG = [
|
|
|
50199
50417
|
"backgroundSceneModelUrl",
|
|
50200
50418
|
"rowLimit",
|
|
50201
50419
|
"optionSource",
|
|
50420
|
+
"recordAggregates",
|
|
50202
50421
|
"recordCollectionId",
|
|
50203
50422
|
"recordField",
|
|
50204
50423
|
"recordFieldType",
|
|
@@ -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",
|
|
@@ -2967,6 +2968,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
2967
2968
|
"recordFilterField": "",
|
|
2968
2969
|
"recordFilterValue": "",
|
|
2969
2970
|
"recordRelationField": "",
|
|
2971
|
+
"recordAggregates": [],
|
|
2970
2972
|
"itemsPerRow": 4,
|
|
2971
2973
|
"collectionType": "all_products",
|
|
2972
2974
|
"collectionId": "all_products",
|
|
@@ -3688,6 +3690,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
3688
3690
|
"recordFilterField": "",
|
|
3689
3691
|
"recordFilterValue": "",
|
|
3690
3692
|
"recordRelationField": "",
|
|
3693
|
+
"recordAggregates": [],
|
|
3691
3694
|
"itemsPerRow": 4,
|
|
3692
3695
|
"collectionType": "all_products",
|
|
3693
3696
|
"collectionId": "all_products",
|
|
@@ -4418,6 +4421,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
4418
4421
|
"recordFilterField": "",
|
|
4419
4422
|
"recordFilterValue": "",
|
|
4420
4423
|
"recordRelationField": "",
|
|
4424
|
+
"recordAggregates": [],
|
|
4421
4425
|
"itemsPerRow": 4,
|
|
4422
4426
|
"collectionType": "all_products",
|
|
4423
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"
|
|
@@ -124,6 +124,13 @@ export const FIX = {
|
|
|
124
124
|
'{ "optionSource": "record", "recordCollectionId": "col_…", "recordLabelField": "fld_…" } (base). A dependent ' +
|
|
125
125
|
'select adds "recordParentNode": "<parent form-select id>" and "recordParentField": "<reference fld_… of this ' +
|
|
126
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>}] }.',
|
|
127
134
|
record_filter_invalid: 'Aim it at ONE collection\'s record list(s) and a field: sb_set id "<id>", namespace specials, keys ' +
|
|
128
135
|
'{ "filterSource": "record", "filterAxis": "fld_…", "filterTargets": ["<list id>"], "filterBehavior": "filter" } — ' +
|
|
129
136
|
'axis types text/select/reference/url/date/boolean/integer/number. A sort select takes filterValues ' +
|
|
@@ -15,7 +15,61 @@ export const RECORD_FIELD_ID = /^fld_[0-9a-f]{16}$/;
|
|
|
15
15
|
* `record.id` — the row's own record id — or `record.url` — `/records/<id>`, emitted
|
|
16
16
|
* only when the row's collection has a published record detail page (recordContext.ts).
|
|
17
17
|
*/
|
|
18
|
-
export const RECORD_SOURCE = /^record\.(?:id|url|(?: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]))));
|
|
19
73
|
/** The field types `record.money.<fid>` formats — recordContext.ts RECORD_MONEY_FIELD_TYPES. */
|
|
20
74
|
export const RECORD_MONEY_TYPES = new Set(['number', 'integer']);
|
|
21
75
|
/** The only record source `specials.boundHtml` admits — schema/src/rawSinkBindings.ts:102. */
|
|
@@ -35,8 +89,9 @@ export function recordTileBindings(type, config, rows) {
|
|
|
35
89
|
if (!RECORD_TILES.has(type))
|
|
36
90
|
return rows;
|
|
37
91
|
const fid = config.recordField;
|
|
38
|
-
const ok = typeof fid === 'string' && RECORD_FIELD_ID.test(fid);
|
|
39
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) !== ''));
|
|
40
95
|
// datasetBindings.ts isRecordMoney: a stale recordFormat on any other type is ignored.
|
|
41
96
|
const money = type === 'text-dataset' && config.recordFormat === 'money' && RECORD_MONEY_TYPES.has(String(config.recordFieldType));
|
|
42
97
|
return rows.map((row) => {
|
|
@@ -90,6 +145,9 @@ export function recordFieldProblem(source, scope) {
|
|
|
90
145
|
}
|
|
91
146
|
if (source === 'record.id' || source === 'record.url')
|
|
92
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))
|
|
150
|
+
return undefined;
|
|
93
151
|
const html = source.startsWith('record.html.');
|
|
94
152
|
const money = source.startsWith('record.money.');
|
|
95
153
|
const fid = source.slice(html ? 12 : money ? 13 : 7);
|
|
@@ -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';
|
|
@@ -705,6 +705,62 @@ opts = {}) {
|
|
|
705
705
|
});
|
|
706
706
|
}
|
|
707
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
|
+
}
|
|
708
764
|
// RECORD FILTERS (web_builder PR #157, contract §7.1): a filter/sort control aimed at a
|
|
709
765
|
// record list. Every miss below renders a control that empties the list or narrows nothing.
|
|
710
766
|
{
|
|
@@ -716,11 +772,23 @@ opts = {}) {
|
|
|
716
772
|
});
|
|
717
773
|
const why = [];
|
|
718
774
|
if (sp.filterSource === 'record') {
|
|
719
|
-
|
|
720
|
-
|
|
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`);
|
|
721
790
|
if (targets.length && recordTargets.length !== targets.length)
|
|
722
791
|
why.push('a filterTargets entry is not a record list');
|
|
723
|
-
const cfgOf = (t) => d.nodes[t].config ?? {};
|
|
724
792
|
if (recordTargets.some((t) => String(cfgOf(t).recordRelationField ?? '')))
|
|
725
793
|
why.push('it targets a relation list');
|
|
726
794
|
if (new Set(recordTargets.map((t) => String(cfgOf(t).recordCollectionId ?? ''))).size > 1)
|
|
@@ -731,9 +799,9 @@ opts = {}) {
|
|
|
731
799
|
if (sp.filterSource === 'sort' && recordTargets.length) {
|
|
732
800
|
const bad = (Array.isArray(sp.filterValues) ? sp.filterValues : [])
|
|
733
801
|
.map((v) => String(v?.value ?? ''))
|
|
734
|
-
.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));
|
|
735
803
|
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`);
|
|
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`);
|
|
737
805
|
}
|
|
738
806
|
if (why.length) {
|
|
739
807
|
out.push({
|
|
@@ -747,7 +815,25 @@ opts = {}) {
|
|
|
747
815
|
}
|
|
748
816
|
if (RECORD_TILES.has(type) && cfg.datasetSource === 'record') {
|
|
749
817
|
const fid = cfg.recordField;
|
|
750
|
-
|
|
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))) {
|
|
751
837
|
out.push({
|
|
752
838
|
code: 'record_field_unset',
|
|
753
839
|
nodeId: id,
|
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",
|