sbuilder-mcp 0.69.0 → 0.71.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 +18 -0
- package/CHANGELOG.vi.md +18 -0
- package/dist/catalog/api.generated.js +60 -0
- package/dist/catalog/deadkeys.generated.js +2 -2
- package/dist/catalog/search.js +8 -1
- package/dist/catalog/shapes.generated.js +11 -1
- package/dist/catalog/source.generated.js +2 -2
- package/dist/catalog/translations.generated.js +19 -8
- package/dist/domains/site/cartlang.js +15 -0
- package/dist/domains/site/readiness-fetch.js +2 -0
- package/dist/domains/site/readiness.js +29 -3
- package/dist/domains/site/translate.js +12 -0
- package/dist/tools/overlay.js +23 -14
- package/dist/tools/store.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ 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.71.0] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sb_review`'s `catalogue` readiness gap now reports "N products and none active" separately from an empty catalogue, for a store whose products were all created without `status` and stored as drafts.
|
|
13
|
+
- `sb_api_find`'s `product_traps` gains a `status` trap: a product created without `status` is stored as "draft" — the storefront never lists it and the checkout refuses it as a line — even though the write still answers 201.
|
|
14
|
+
- The translation call sheet (via `sb_api_find`) gains `review_gate`, explaining that `PUT /api/v1/translations` defaults a row's `source` to "machine", which is stored and counted but never shown on the storefront, in search, in slug routing, or in shopper mail until `POST /api/sites/{siteId}/translations/review` (or its `/bulk` form) approves it.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- The generated catalog was regenerated against the platform (`web_builder` `84f1f10bc`): flash sale campaign listing operations gain query parameters `q`, `status`, `limit`, and `offset`; the order/checkout request shapes now catalog a `locale` field that snapshots the shopper's storefront language at checkout; `slug` is now catalogued as a translatable field on products, categories, articles, blog categories, courses, and pages; and `review` is no longer a translatable entity type.
|
|
18
|
+
|
|
19
|
+
## [0.70.0] - 2026-09-26
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `sb_review` warns once per site (`cartDrawerThumbnail`) when a cart drawer seeded before the platform fixed its seed renders a line's thumbnail as a gallery (feature image plus a thumbs strip) squeezed into a 64px line, and `sb_store` action `cart` with `relocalize: true` now also fixes it, setting that thumbnail to a single image through the open page's save.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `sb_store` action `cart`'s `relocalize` no longer requires `settings.locale` to be set when the only thing left to fix is a gallery thumbnail; it still refuses when there is neither a locale word nor a thumbnail to relocalize.
|
|
26
|
+
|
|
9
27
|
## [0.69.0] - 2026-09-26
|
|
10
28
|
|
|
11
29
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,24 @@ 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.71.0] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Gap readiness `catalogue` của `sb_review` giờ báo riêng "có N sản phẩm, không cái nào active" thay vì gộp chung với catalogue rỗng, dành cho store có toàn bộ sản phẩm được tạo mà không kèm `status` nên bị lưu thành draft.
|
|
13
|
+
- `product_traps` của `sb_api_find` có thêm bẫy `status`: một sản phẩm tạo không kèm `status` sẽ bị lưu thành "draft" — storefront không liệt kê và checkout từ chối nó như một dòng hàng — dù request ghi vẫn trả về 201.
|
|
14
|
+
- Call sheet bản dịch (qua `sb_api_find`) có thêm `review_gate`, giải thích rằng `PUT /api/v1/translations` mặc định `source` của một dòng là "machine", dòng này được lưu và tính vào tiến độ nhưng không hiện ở storefront, trong tìm kiếm, trong slug routing hay trong mail gửi khách cho tới khi `POST /api/sites/{siteId}/translations/review` (hoặc bản `/bulk`) duyệt nó.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Catalog được sinh ra đã được làm mới dựa trên nền tảng (`web_builder` `84f1f10bc`): các operation liệt kê flash sale campaign có thêm tham số truy vấn `q`, `status`, `limit`, `offset`; request shape của order/checkout giờ catalog thêm field `locale` lưu lại ngôn ngữ storefront của khách tại thời điểm checkout; `slug` giờ được catalog là field dịch được trên product, category, article, blog category, course và page; và `review` không còn là một entity type dịch được nữa.
|
|
18
|
+
|
|
19
|
+
## [0.70.0] - 2026-09-26
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `sb_review` cảnh báo một lần cho mỗi site (`cartDrawerThumbnail`) khi cart drawer được gieo trước khi nền tảng sửa lại seed hiện thumbnail của một line dưới dạng gallery (ảnh chính cộng dải ảnh thu nhỏ) bị nhồi vào một line cao 64px, và action `cart` của `sb_store` với `relocalize: true` giờ cũng sửa luôn, đặt thumbnail đó thành một ảnh duy nhất thông qua lần lưu của trang đang mở.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `relocalize` của action `cart` trong `sb_store` không còn yêu cầu `settings.locale` phải được đặt khi thứ duy nhất cần sửa là thumbnail dạng gallery; nó vẫn từ chối khi không có cả từ ngôn ngữ lẫn thumbnail nào cần relocalize.
|
|
26
|
+
|
|
9
27
|
## [0.69.0] - 2026-09-26
|
|
10
28
|
|
|
11
29
|
### Added
|
|
@@ -8050,6 +8050,34 @@ export const API_OPERATIONS = [
|
|
|
8050
8050
|
"type": "string",
|
|
8051
8051
|
"description": "Site ID"
|
|
8052
8052
|
},
|
|
8053
|
+
{
|
|
8054
|
+
"name": "q",
|
|
8055
|
+
"in": "query",
|
|
8056
|
+
"required": false,
|
|
8057
|
+
"type": "string",
|
|
8058
|
+
"description": "Case-insensitive substring of the campaign name (GET only)"
|
|
8059
|
+
},
|
|
8060
|
+
{
|
|
8061
|
+
"name": "status",
|
|
8062
|
+
"in": "query",
|
|
8063
|
+
"required": false,
|
|
8064
|
+
"type": "string",
|
|
8065
|
+
"description": "off|scheduled|running|ended — derived status, evaluated now (GET only)"
|
|
8066
|
+
},
|
|
8067
|
+
{
|
|
8068
|
+
"name": "limit",
|
|
8069
|
+
"in": "query",
|
|
8070
|
+
"required": false,
|
|
8071
|
+
"type": "integer",
|
|
8072
|
+
"description": "Page size, default 50, max 200 (GET only)"
|
|
8073
|
+
},
|
|
8074
|
+
{
|
|
8075
|
+
"name": "offset",
|
|
8076
|
+
"in": "query",
|
|
8077
|
+
"required": false,
|
|
8078
|
+
"type": "integer",
|
|
8079
|
+
"description": "Page offset, default 0 (GET only)"
|
|
8080
|
+
},
|
|
8053
8081
|
{
|
|
8054
8082
|
"name": "flashSale",
|
|
8055
8083
|
"in": "body",
|
|
@@ -8078,6 +8106,34 @@ export const API_OPERATIONS = [
|
|
|
8078
8106
|
"type": "string",
|
|
8079
8107
|
"description": "Site ID"
|
|
8080
8108
|
},
|
|
8109
|
+
{
|
|
8110
|
+
"name": "q",
|
|
8111
|
+
"in": "query",
|
|
8112
|
+
"required": false,
|
|
8113
|
+
"type": "string",
|
|
8114
|
+
"description": "Case-insensitive substring of the campaign name (GET only)"
|
|
8115
|
+
},
|
|
8116
|
+
{
|
|
8117
|
+
"name": "status",
|
|
8118
|
+
"in": "query",
|
|
8119
|
+
"required": false,
|
|
8120
|
+
"type": "string",
|
|
8121
|
+
"description": "off|scheduled|running|ended — derived status, evaluated now (GET only)"
|
|
8122
|
+
},
|
|
8123
|
+
{
|
|
8124
|
+
"name": "limit",
|
|
8125
|
+
"in": "query",
|
|
8126
|
+
"required": false,
|
|
8127
|
+
"type": "integer",
|
|
8128
|
+
"description": "Page size, default 50, max 200 (GET only)"
|
|
8129
|
+
},
|
|
8130
|
+
{
|
|
8131
|
+
"name": "offset",
|
|
8132
|
+
"in": "query",
|
|
8133
|
+
"required": false,
|
|
8134
|
+
"type": "integer",
|
|
8135
|
+
"description": "Page offset, default 0 (GET only)"
|
|
8136
|
+
},
|
|
8081
8137
|
{
|
|
8082
8138
|
"name": "flashSale",
|
|
8083
8139
|
"in": "body",
|
|
@@ -18416,6 +18472,10 @@ export const API_DEFINITIONS = {
|
|
|
18416
18472
|
"$ref": "#/definitions/github_com_webbuilder_server_internal_orders.OrderItem"
|
|
18417
18473
|
}
|
|
18418
18474
|
},
|
|
18475
|
+
"locale": {
|
|
18476
|
+
"description": "Locale is the language the SHOPPER was reading the storefront in at\ncheckout — a SNAPSHOT, for the same reason every other checkout-time fact\non this struct is one: a merchant who later reorders or trims the site's\nlanguage list must not have an already-placed order start mailing its\nbuyer in a different language than the one they bought in.\n\n\"\" means the site's DEFAULT locale — every order placed before this field\nexisted, every order typed by staff, and every order placed through a\nsurface that has not been taught to send one yet. It is never trusted\noutright: Checkout.Create normalises it against the site's published\nlocales (SiteLocales port) before it is stored, so a stale or tampered\nclaim can only ever fall back to \"\", never choose an unpublished\nlanguage. See Checkout.normalizeLocale.",
|
|
18477
|
+
"type": "string"
|
|
18478
|
+
},
|
|
18419
18479
|
"note": {
|
|
18420
18480
|
"type": "string"
|
|
18421
18481
|
},
|
package/dist/catalog/search.js
CHANGED
|
@@ -352,12 +352,19 @@ export function describeOperation(op) {
|
|
|
352
352
|
'/apps/blocks again for what it contributed.',
|
|
353
353
|
};
|
|
354
354
|
}
|
|
355
|
-
// FILLING A CATALOGUE WAS REACHABLE AND
|
|
355
|
+
// FILLING A CATALOGUE WAS REACHABLE AND SIX FACTS ABOUT IT WERE NOT, each
|
|
356
356
|
// failing silently. Attached to the call sheet for the same reason the
|
|
357
357
|
// translation table is: this is where the agent is when it decides what to
|
|
358
358
|
// send, on both credential surfaces a product can be created or replaced on.
|
|
359
359
|
if (PRODUCT_WRITE_IDS.has(op.id)) {
|
|
360
360
|
out.product_traps = {
|
|
361
|
+
// THE STATUS NOBODY SENDS. An empty Status becomes ProductStatusDraft
|
|
362
|
+
// (products/product.go), the storefront lists only "active", and since
|
|
363
|
+
// web_builder b4d60a862 the checkout REFUSES any other status as a line.
|
|
364
|
+
// The write answers 201 either way.
|
|
365
|
+
status: 'Send status: "active". A product created without one is stored as "draft" — the ' +
|
|
366
|
+
'storefront never lists it and the checkout refuses it — and the write still ' +
|
|
367
|
+
'answers 201. PATCH is 405: to activate later, PUT the whole object back.',
|
|
361
368
|
price: 'Price lives on the VARIANT, not the product — variants[].priceCents. A product ' +
|
|
362
369
|
'posted with no variant renders a catalogue entry nobody can buy.',
|
|
363
370
|
// MINOR UNITS, SAID WHERE THE NUMBER IS BEING WRITTEN.
|
|
@@ -4,7 +4,7 @@ export const SHAPE_SOURCE = {
|
|
|
4
4
|
"fromHandlers": 197,
|
|
5
5
|
"fromSwaggerOnly": 0,
|
|
6
6
|
"withReadOnly": 28,
|
|
7
|
-
"structsRead":
|
|
7
|
+
"structsRead": 1977
|
|
8
8
|
};
|
|
9
9
|
export const REQUEST_SHAPES = {
|
|
10
10
|
"post:/api/sites/{id}/ai-credits/purchase": {
|
|
@@ -4736,6 +4736,11 @@ export const REQUEST_SHAPES = {
|
|
|
4736
4736
|
"name": "currency",
|
|
4737
4737
|
"type": "string"
|
|
4738
4738
|
},
|
|
4739
|
+
{
|
|
4740
|
+
"name": "locale",
|
|
4741
|
+
"type": "string",
|
|
4742
|
+
"note": "Locale is the language the SHOPPER was reading the storefront in at checkout — a SNAPSHOT, for the same reason every other checkout-time fact on this struct is one: a merchant who later reorders or trims the site's language list must not…"
|
|
4743
|
+
},
|
|
4739
4744
|
{
|
|
4740
4745
|
"name": "discountCode",
|
|
4741
4746
|
"type": "string",
|
|
@@ -5047,6 +5052,11 @@ export const REQUEST_SHAPES = {
|
|
|
5047
5052
|
"name": "currency",
|
|
5048
5053
|
"type": "string"
|
|
5049
5054
|
},
|
|
5055
|
+
{
|
|
5056
|
+
"name": "locale",
|
|
5057
|
+
"type": "string",
|
|
5058
|
+
"note": "Locale is the language the SHOPPER was reading the storefront in at checkout — a SNAPSHOT, for the same reason every other checkout-time fact on this struct is one: a merchant who later reorders or trims the site's language list must not…"
|
|
5059
|
+
},
|
|
5050
5060
|
{
|
|
5051
5061
|
"name": "discountCode",
|
|
5052
5062
|
"type": "string",
|
|
@@ -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-09-
|
|
4
|
+
"commit": "84f1f10bcbef0748d0b18e19283ccb0458599c1b",
|
|
5
|
+
"committedAt": "2026-09-26T21:25:06+07:00",
|
|
6
6
|
"dirty": false
|
|
7
7
|
};
|
|
@@ -4,7 +4,7 @@ export const TRANSLATION_SOURCE = {
|
|
|
4
4
|
"elements": 66,
|
|
5
5
|
"pairs": 180,
|
|
6
6
|
"neverKeys": 178,
|
|
7
|
-
"entityTypes":
|
|
7
|
+
"entityTypes": 11
|
|
8
8
|
};
|
|
9
9
|
/** Every entity type a translation record can name, including "node". */
|
|
10
10
|
export const TRANSLATION_ENTITY_TYPES = [
|
|
@@ -13,7 +13,6 @@ export const TRANSLATION_ENTITY_TYPES = [
|
|
|
13
13
|
"article",
|
|
14
14
|
"blogCategory",
|
|
15
15
|
"course",
|
|
16
|
-
"review",
|
|
17
16
|
"courseSection",
|
|
18
17
|
"courseLesson",
|
|
19
18
|
"node",
|
|
@@ -546,6 +545,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
|
|
|
546
545
|
"key": "attributes",
|
|
547
546
|
"list": "attributes"
|
|
548
547
|
},
|
|
548
|
+
{
|
|
549
|
+
"key": "slug"
|
|
550
|
+
},
|
|
549
551
|
{
|
|
550
552
|
"key": "seoTitle"
|
|
551
553
|
},
|
|
@@ -570,6 +572,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
|
|
|
570
572
|
"key": "description",
|
|
571
573
|
"html": true
|
|
572
574
|
},
|
|
575
|
+
{
|
|
576
|
+
"key": "slug"
|
|
577
|
+
},
|
|
573
578
|
{
|
|
574
579
|
"key": "seoTitle"
|
|
575
580
|
},
|
|
@@ -601,6 +606,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
|
|
|
601
606
|
{
|
|
602
607
|
"key": "tags"
|
|
603
608
|
},
|
|
609
|
+
{
|
|
610
|
+
"key": "slug"
|
|
611
|
+
},
|
|
604
612
|
{
|
|
605
613
|
"key": "seoTitle"
|
|
606
614
|
},
|
|
@@ -624,6 +632,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
|
|
|
624
632
|
{
|
|
625
633
|
"key": "description"
|
|
626
634
|
},
|
|
635
|
+
{
|
|
636
|
+
"key": "slug"
|
|
637
|
+
},
|
|
627
638
|
{
|
|
628
639
|
"key": "seoTitle"
|
|
629
640
|
},
|
|
@@ -666,6 +677,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
|
|
|
666
677
|
{
|
|
667
678
|
"key": "tags"
|
|
668
679
|
},
|
|
680
|
+
{
|
|
681
|
+
"key": "slug"
|
|
682
|
+
},
|
|
669
683
|
{
|
|
670
684
|
"key": "seoTitle"
|
|
671
685
|
},
|
|
@@ -682,12 +696,6 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
|
|
|
682
696
|
"key": "ogDesc"
|
|
683
697
|
}
|
|
684
698
|
],
|
|
685
|
-
"review": [
|
|
686
|
-
{
|
|
687
|
-
"key": "reply",
|
|
688
|
-
"html": true
|
|
689
|
-
}
|
|
690
|
-
],
|
|
691
699
|
"courseSection": [
|
|
692
700
|
{
|
|
693
701
|
"key": "title"
|
|
@@ -713,6 +721,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
|
|
|
713
721
|
},
|
|
714
722
|
{
|
|
715
723
|
"key": "ogDesc"
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
"key": "slug"
|
|
716
727
|
}
|
|
717
728
|
],
|
|
718
729
|
"uiString": [
|
|
@@ -56,3 +56,18 @@ export function cartRelocalize(doc, rootId, locale) {
|
|
|
56
56
|
});
|
|
57
57
|
return out;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* A CART LINE THUMBNAIL SEEDED AS A GALLERY. Drawers seeded before
|
|
61
|
+
* web_builder `3e5efbb52` carry a childless `media-dataset` with no `layout`,
|
|
62
|
+
* which renders the feature image PLUS a thumbs strip inside a 64px line. The
|
|
63
|
+
* seed now says `single`; a layout the merchant chose explicitly is theirs.
|
|
64
|
+
*/
|
|
65
|
+
export function cartGalleryThumbs(doc, rootId) {
|
|
66
|
+
const out = [];
|
|
67
|
+
walk(doc, rootId, (n) => {
|
|
68
|
+
const layout = n.config?.layout;
|
|
69
|
+
if (n.data.type === 'media-dataset' && !(n.data.nodes ?? []).length && (layout === undefined || layout === 'bottom'))
|
|
70
|
+
out.push(n.id);
|
|
71
|
+
});
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
@@ -104,6 +104,8 @@ export async function gatherReadiness(ctx, siteId, pageNodes, openPageId) {
|
|
|
104
104
|
? {
|
|
105
105
|
active: rows.filter((p) => (p.status ?? 'active') === 'active').length,
|
|
106
106
|
purchasable: rows.filter((p) => (p.status ?? 'active') === 'active' && (p.priceCents ?? 0) > 0).length,
|
|
107
|
+
// Drafts and archived: what a catalogue built without status:"active" is.
|
|
108
|
+
inactive: rows.filter((p) => (p.status ?? 'active') !== 'active').length,
|
|
107
109
|
}
|
|
108
110
|
: null;
|
|
109
111
|
// A page an entity links to is that entity's own; only the shared template
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* fetched reports nothing rather than a gap the store may not have. A warning
|
|
17
17
|
* that fires on a correctly built site is one the reader learns to ignore.
|
|
18
18
|
*/
|
|
19
|
-
import { cartRelocalize, cartSeedLocale } from './cartlang.js';
|
|
19
|
+
import { cartGalleryThumbs, cartRelocalize, cartSeedLocale } from './cartlang.js';
|
|
20
20
|
/** The purchase binding an Add-to-cart / Buy-now button carries. */
|
|
21
21
|
const PRODUCT_ACTION_BINDING_ID = 'bind-product-action';
|
|
22
22
|
/** Element types that only make sense in a store. */
|
|
@@ -280,6 +280,15 @@ export function readinessGaps(input) {
|
|
|
280
280
|
});
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
+
if (input.cartOverlay?.root_node_id && cartGalleryThumbs(input.cartOverlay, input.cartOverlay.root_node_id).length) {
|
|
284
|
+
gaps.push({
|
|
285
|
+
id: 'cartDrawerThumbnail',
|
|
286
|
+
draft: false,
|
|
287
|
+
problem: 'The cart drawer\'s line thumbnail is a GALLERY (feature image plus a thumbs strip) squeezed ' +
|
|
288
|
+
'into a 64px line — a drawer seeded before the platform fixed its seed.',
|
|
289
|
+
fix: 'Open any page of this site, then sb_store action:"cart" relocalize:true dry_run:false — it sets that thumbnail to a single image.',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
283
292
|
if (!isStore(input))
|
|
284
293
|
return gaps;
|
|
285
294
|
if (pages && !published(pages, 'checkout')) {
|
|
@@ -354,14 +363,31 @@ export function readinessGaps(input) {
|
|
|
354
363
|
// NOTHING TO SELL. Checked before the delivery option, because a shipping
|
|
355
364
|
// method for an empty catalogue is furniture.
|
|
356
365
|
if (input.products) {
|
|
357
|
-
if (input.products.active === 0) {
|
|
366
|
+
if (input.products.active === 0 && (input.products.inactive ?? 0) > 0) {
|
|
367
|
+
// PRODUCTS EXIST AND NONE IS ACTIVE. A product created without a status is
|
|
368
|
+
// stored as "draft" (products.go: an empty Status becomes ProductStatusDraft),
|
|
369
|
+
// the storefront lists only "active", and since web_builder b4d60a862 the
|
|
370
|
+
// checkout refuses any other status outright — so a catalogue built through
|
|
371
|
+
// the API without saying status:"active" is a store with nothing to buy.
|
|
372
|
+
gaps.push({
|
|
373
|
+
id: 'catalogue',
|
|
374
|
+
draft: false,
|
|
375
|
+
problem: `The store has ${input.products.inactive} products and none is active — a product ` +
|
|
376
|
+
'created without status is stored as "draft", which the storefront never lists and ' +
|
|
377
|
+
'the checkout refuses. Every product list renders its empty state.',
|
|
378
|
+
fix: 'PUT /api/v1/products/{id} with the whole object read back and status: "active" ' +
|
|
379
|
+
'(PATCH is 405). Send status: "active" on every POST /api/v1/products from now on.',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
else if (input.products.active === 0) {
|
|
358
383
|
gaps.push({
|
|
359
384
|
id: 'catalogue',
|
|
360
385
|
draft: false,
|
|
361
386
|
problem: 'The store has no active products. Every product list on the site renders its empty ' +
|
|
362
387
|
'state, the product template has nothing to bind to, and there is nothing to add to a ' +
|
|
363
388
|
'cart — on a site that otherwise reports ready.',
|
|
364
|
-
fix: 'Create products (sb_api_find "create product")
|
|
389
|
+
fix: 'Create products (sb_api_find "create product") with status: "active" — one created ' +
|
|
390
|
+
'without a status is a draft. priceCents is minor units — VND × 100.',
|
|
365
391
|
});
|
|
366
392
|
}
|
|
367
393
|
else if (input.products.purchasable === 0) {
|
|
@@ -68,5 +68,17 @@ export function translationCallSheet() {
|
|
|
68
68
|
'that element allows. Translating any OTHER special BREAKS the render rather than ' +
|
|
69
69
|
'degrading it: `name` is a lucide icon id, `src` a URL, `filterSource` a registry id the ' +
|
|
70
70
|
'renderer switches on. Never walk a document translating every string you find.',
|
|
71
|
+
// THE REVIEW GATE (web_builder c6184c085). The storefront, search, slug routing
|
|
72
|
+
// and shopper mail read only rows whose source is "human". The key surface
|
|
73
|
+
// defaults source to "machine" — deliberately, so an app cannot mislabel an
|
|
74
|
+
// unread value as reviewed — so a translation written there is stored,
|
|
75
|
+
// counted by /progress, and never SHOWN until a review call approves it.
|
|
76
|
+
review_gate: 'Only rows with source "human" are served — on the storefront, in search, in slug ' +
|
|
77
|
+
'routing and in shopper mail. PUT /api/v1/translations defaults source to "machine": ' +
|
|
78
|
+
'the row is stored and counted by GET /api/v1/translations/progress, and shown to ' +
|
|
79
|
+
'nobody until POST /api/sites/{siteId}/translations/review {locale, entityType, ' +
|
|
80
|
+
'entityId, field} or …/review/bulk approves it (session credential). Send ' +
|
|
81
|
+
'"source": "human" only when a person read the text; the site-scoped PUT ' +
|
|
82
|
+
'/api/sites/{siteId}/translations already defaults to "human".',
|
|
71
83
|
};
|
|
72
84
|
}
|
package/dist/tools/overlay.js
CHANGED
|
@@ -57,7 +57,7 @@ import { siteToken } from './credentialpick.js';
|
|
|
57
57
|
import { setKeys } from '../domains/site/builder.js';
|
|
58
58
|
import { CART_SEEDS, OVERLAY_SEEDS } from '../catalog/overlays.generated.js';
|
|
59
59
|
import { withFreshIds } from '../domains/site/ids.js';
|
|
60
|
-
import { cartRelocalize, cartSeedLocale } from '../domains/site/cartlang.js';
|
|
60
|
+
import { cartGalleryThumbs, cartRelocalize, cartSeedLocale } from '../domains/site/cartlang.js';
|
|
61
61
|
const KIND_NAME = {
|
|
62
62
|
popup: 'Pop-up',
|
|
63
63
|
quickview: 'Quick view',
|
|
@@ -363,29 +363,34 @@ export async function relocalizeCartDrawer(ctx, session, siteId, dryRun) {
|
|
|
363
363
|
throw new Error('sbuilder: this site has no cart drawer — sb_store action:"cart" without relocalize creates one.');
|
|
364
364
|
const settings = (await get(`/api/sites/${site}/settings`));
|
|
365
365
|
const raw = settings.settings?.locale;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
const language = cartSeedLocale(raw);
|
|
366
|
+
// No locale means no language to move the WORDS to; the thumbnail needs none.
|
|
367
|
+
const language = typeof raw === 'string' && raw ? cartSeedLocale(raw) : null;
|
|
370
368
|
const open = session.peek() ? session.location() : null;
|
|
371
369
|
const composed = open?.siteId === siteId
|
|
372
370
|
? Object.values(session.current().doc.nodes).find((n) => n.specials?.overlayId === cart.id)
|
|
373
371
|
: undefined;
|
|
374
|
-
const
|
|
375
|
-
?
|
|
372
|
+
const target = composed
|
|
373
|
+
? { doc: session.current().doc, root: composed.id }
|
|
376
374
|
: cart.document?.root_node_id
|
|
377
|
-
?
|
|
378
|
-
:
|
|
379
|
-
|
|
375
|
+
? { doc: cart.document, root: cart.document.root_node_id }
|
|
376
|
+
: null;
|
|
377
|
+
const changes = target && language ? cartRelocalize(target.doc, target.root, language) : [];
|
|
378
|
+
const thumbnails = target ? cartGalleryThumbs(target.doc, target.root) : [];
|
|
379
|
+
if (!language && thumbnails.length === 0) {
|
|
380
|
+
throw new Error('sbuilder: settings.locale is unset, so there is no site language to relocalize to — set it with sb_theme locale first.');
|
|
381
|
+
}
|
|
382
|
+
const thumbNote = thumbnails.length ? { thumbnails, thumbnails_note: 'These line thumbnails become a single image (config.layout "single").' } : {};
|
|
383
|
+
if (dryRun || changes.length + thumbnails.length === 0) {
|
|
380
384
|
return {
|
|
381
385
|
...(dryRun ? { dry_run: true } : {}),
|
|
382
386
|
overlay_id: cart.id,
|
|
383
387
|
language,
|
|
384
388
|
changes,
|
|
385
|
-
...
|
|
386
|
-
|
|
389
|
+
...thumbNote,
|
|
390
|
+
...(changes.length + thumbnails.length === 0
|
|
391
|
+
? { note: 'Nothing to change — no seed word from another language and no gallery thumbnail is left in the drawer.' }
|
|
387
392
|
: {
|
|
388
|
-
plan: redact([{ step: 1, what: '
|
|
393
|
+
plan: redact([{ step: 1, what: 'apply those changes to the composed drawer and save the open page', method: 'PUT', path: `/api/sites/${site}/pages/{open page}/source` }]),
|
|
389
394
|
note: 'Nothing was sent. Re-call with dry_run:false; it writes through the open page\'s save.',
|
|
390
395
|
}),
|
|
391
396
|
};
|
|
@@ -393,11 +398,15 @@ export async function relocalizeCartDrawer(ctx, session, siteId, dryRun) {
|
|
|
393
398
|
if (!composed) {
|
|
394
399
|
throw new Error(`sbuilder: the drawer is written through a page save, so relocalize needs a page of site "${siteId}" open — sb_page_open one, then re-run.`);
|
|
395
400
|
}
|
|
396
|
-
await session.applyAndSave(
|
|
401
|
+
await session.applyAndSave([
|
|
402
|
+
...changes.map((c) => ({ op: 'set', path: ['nodes', c.node_id, 'specials', c.key], value: c.to })),
|
|
403
|
+
...thumbnails.map((id) => ({ op: 'set', path: ['nodes', id, 'config', 'layout'], value: 'single' })),
|
|
404
|
+
]);
|
|
397
405
|
return {
|
|
398
406
|
overlay_id: cart.id,
|
|
399
407
|
language,
|
|
400
408
|
changes,
|
|
409
|
+
...thumbNote,
|
|
401
410
|
next: 'Publish the pages: the drawer reaches a shopper through each published page.',
|
|
402
411
|
};
|
|
403
412
|
}
|
package/dist/tools/store.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THE STORE FLOWS THAT MUST BE ORDERED.
|
|
3
3
|
*
|
|
4
|
-
* `sb_review` names
|
|
4
|
+
* `sb_review` names its readiness gaps (`ReadinessGapId`). Most are one
|
|
5
5
|
* call each — a delivery option, a payment gateway, a product, a page of the
|
|
6
6
|
* right type — because `REQUEST_SHAPES` tells the caller what those calls take.
|
|
7
7
|
* The ones that are not are the flows in this file: each is several writes in
|
|
@@ -533,7 +533,8 @@ export function registerStoreTools(server, ctx, session) {
|
|
|
533
533
|
.boolean()
|
|
534
534
|
.optional()
|
|
535
535
|
.describe('action:"cart" — rewrite an EXISTING drawer\'s seed words that are in another language ' +
|
|
536
|
-
'to the site locale\'s (edited text untouched)
|
|
536
|
+
'to the site locale\'s (edited text untouched), and turn a gallery line thumbnail into a ' +
|
|
537
|
+
'single image; needs a page of the site open'),
|
|
537
538
|
dry_run: z.boolean().optional(),
|
|
538
539
|
},
|
|
539
540
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.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",
|