@mohasinac/appkit 4.45.0 → 4.47.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/dist/.build-stamp.json +4 -4
- package/dist/_internal/server/features/categories/live-counts.d.ts +41 -0
- package/dist/_internal/server/features/categories/live-counts.js +72 -0
- package/dist/_internal/server/features/checkout/expand-order-items.js +17 -0
- package/dist/_internal/server/features/orders/adapters.js +29 -0
- package/dist/_internal/server/features/parts/assembly.d.ts +126 -0
- package/dist/_internal/server/features/parts/assembly.js +246 -0
- package/dist/_internal/server/features/parts/data.d.ts +65 -0
- package/dist/_internal/server/features/parts/data.js +170 -0
- package/dist/_internal/server/features/parts/og.d.ts +34 -0
- package/dist/_internal/server/features/parts/og.js +66 -0
- package/dist/_internal/server/features/search/search-everything.d.ts +75 -0
- package/dist/_internal/server/features/search/search-everything.js +120 -0
- package/dist/_internal/server/features/search/search-sources.d.ts +17 -0
- package/dist/_internal/server/features/search/search-sources.js +138 -0
- package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.d.ts +70 -0
- package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +302 -0
- package/dist/_internal/server/jobs/core/countersReconcile.js +128 -19
- package/dist/_internal/server/jobs/core/jobRunners.js +12 -0
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +73 -3
- package/dist/_internal/server/jobs/core/onProductWrite.js +93 -43
- package/dist/_internal/server/jobs/core/productViewWindows.d.ts +2 -0
- package/dist/_internal/server/jobs/core/productViewWindows.js +173 -0
- package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testDataCleanup.js +27 -1
- package/dist/_internal/server/jobs/handlers/pageViewRollup.d.ts +20 -0
- package/dist/_internal/server/jobs/handlers/pageViewRollup.js +25 -1
- package/dist/_internal/shared/actions/action-registry.js +36 -0
- package/dist/_internal/shared/features/promotions/schema.d.ts +2 -2
- package/dist/_internal/shared/features/shipping/protections.d.ts +93 -1
- package/dist/_internal/shared/features/shipping/protections.js +127 -0
- package/dist/_internal/shared/search/part-aliases.d.ts +44 -0
- package/dist/_internal/shared/search/part-aliases.js +104 -0
- package/dist/constants/api-endpoints.d.ts +18 -12
- package/dist/constants/api-endpoints.js +6 -8
- package/dist/constants/field-names.d.ts +0 -2
- package/dist/constants/field-names.js +0 -2
- package/dist/constants/index.d.ts +1 -1
- package/dist/constants/index.js +1 -1
- package/dist/core/index.d.ts +0 -3
- package/dist/core/index.js +0 -2
- package/dist/errors/messages.d.ts +0 -7
- package/dist/errors/messages.js +0 -8
- package/dist/features/admin/components/AdminSectionsView.js +83 -32
- package/dist/features/admin/components/AdminSiteSettingsView.js +6 -2
- package/dist/features/admin/components/sections/sectionTemplates.d.ts +117 -0
- package/dist/features/admin/components/sections/sectionTemplates.js +213 -0
- package/dist/features/admin/schemas/firestore.d.ts +15 -0
- package/dist/features/admin/types/index.d.ts +0 -1
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +0 -2
- package/dist/features/categories/api/[id]/description-template/route.js +40 -1
- package/dist/features/categories/components/BrandDetailPageView.js +14 -3
- package/dist/features/categories/components/BreadcrumbTrail.d.ts +24 -1
- package/dist/features/categories/components/BreadcrumbTrail.js +45 -4
- package/dist/features/categories/components/CategoryDetailPageView.js +89 -14
- package/dist/features/categories/components/CategoryTemplatePicker.js +9 -1
- package/dist/features/categories/repository/categories.repository.d.ts +65 -0
- package/dist/features/categories/repository/categories.repository.js +168 -22
- package/dist/features/categories/repository/category-counts.d.ts +143 -0
- package/dist/features/categories/repository/category-counts.js +224 -0
- package/dist/features/categories/schemas/category-counts.d.ts +155 -0
- package/dist/features/categories/schemas/category-counts.js +116 -0
- package/dist/features/categories/schemas/firestore.d.ts +0 -2
- package/dist/features/categories/schemas/firestore.js +0 -1
- package/dist/features/categories/utils/breadcrumb-chain.d.ts +194 -0
- package/dist/features/categories/utils/breadcrumb-chain.js +202 -0
- package/dist/features/corporate/schemas/index.d.ts +4 -4
- package/dist/features/orders/schemas/firestore.d.ts +13 -0
- package/dist/features/orders/types/index.d.ts +30 -0
- package/dist/features/parts/components/AssemblyDrawer.d.ts +107 -0
- package/dist/features/parts/components/AssemblyDrawer.js +250 -0
- package/dist/features/parts/components/ModelCompositionPanel.d.ts +37 -0
- package/dist/features/parts/components/ModelCompositionPanel.js +34 -0
- package/dist/features/parts/components/PartDetailPageView.d.ts +48 -0
- package/dist/features/parts/components/PartDetailPageView.js +20 -0
- package/dist/features/parts/components/PartsIndexPageView.d.ts +33 -0
- package/dist/features/parts/components/PartsIndexPageView.js +17 -0
- package/dist/features/parts/schemas/firestore.d.ts +257 -0
- package/dist/features/parts/schemas/firestore.js +181 -0
- package/dist/features/parts/utils/colourway-match.d.ts +62 -0
- package/dist/features/parts/utils/colourway-match.js +98 -0
- package/dist/features/parts/utils/part-breakdown.d.ts +53 -0
- package/dist/features/parts/utils/part-breakdown.js +77 -0
- package/dist/features/products/components/ProductDetailPageView.d.ts +14 -1
- package/dist/features/products/components/ProductDetailPageView.js +22 -3
- package/dist/features/products/components/ProductTabsShell.d.ts +3 -1
- package/dist/features/products/components/ProductTabsShell.js +4 -1
- package/dist/features/products/repository/category-assignment.d.ts +132 -0
- package/dist/features/products/repository/category-assignment.js +304 -0
- package/dist/features/products/repository/products.repository.d.ts +58 -1
- package/dist/features/products/repository/products.repository.js +255 -28
- package/dist/features/products/schemas/firestore.d.ts +147 -6
- package/dist/features/products/schemas/firestore.js +49 -0
- package/dist/features/search/components/SearchResultsPageView.d.ts +33 -0
- package/dist/features/search/components/SearchResultsPageView.js +21 -0
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +17 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +14 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +26 -1
- package/dist/features/seller/components/SellerProductShell.js +30 -1
- package/dist/features/seller/components/SellerProductsView.js +129 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +0 -15
- package/dist/features/tester/seed-data/authored/content-discovery__parts-database.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__parts-database.js +166 -0
- package/dist/features/tester/seed-data/authored/index.js +2 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +28 -1
- package/dist/index.d.ts +11 -16
- package/dist/index.js +26 -18
- package/dist/next/routing/route-map.d.ts +6 -2
- package/dist/next/routing/route-map.js +14 -1
- package/dist/providers/db-firebase/base.repository.d.ts +31 -0
- package/dist/providers/db-firebase/base.repository.js +31 -0
- package/dist/repositories/index.d.ts +1 -2
- package/dist/repositories/index.js +0 -1
- package/dist/seed/_helpers/category-content-generated.d.ts +24 -0
- package/dist/seed/_helpers/category-content-generated.js +1837 -0
- package/dist/seed/_helpers/category-content.d.ts +29 -0
- package/dist/seed/_helpers/category-content.js +98 -1
- package/dist/seed/_helpers/category-forest.js +104 -24
- package/dist/seed/_helpers/category-parts.d.ts +9 -0
- package/dist/seed/_helpers/category-parts.js +4594 -0
- package/dist/seed/carousel-slides-seed-data.js +81 -8
- package/dist/seed/categories-seed-data.js +44 -6
- package/dist/seed/homepage-sections-seed-data.js +170 -9
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +13 -0
- package/dist/seed/model-composition-seed-data.d.ts +15 -0
- package/dist/seed/model-composition-seed-data.js +8918 -0
- package/dist/seed/part-glossary-seed-data.d.ts +15 -0
- package/dist/seed/part-glossary-seed-data.js +10729 -0
- package/dist/seed/products-standard-seed-data.js +52 -0
- package/dist/seed/seed-purpose.d.ts +103 -4
- package/dist/seed/seed-purpose.js +162 -11
- package/dist/seo/json-ld.d.ts +10 -1
- package/dist/seo/json-ld.js +9 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.js +11 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/utils/id-generators.d.ts +30 -0
- package/dist/utils/id-generators.js +31 -1
- package/dist/utils/search-txt-builders.js +22 -0
- package/dist/utils/search-txt.d.ts +22 -0
- package/dist/utils/search-txt.js +17 -0
- package/dist/values/success-messages.d.ts +0 -4
- package/dist/values/success-messages.js +0 -5
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +292 -16
- package/dist/core/copilot-log.repository.d.ts +0 -44
- package/dist/core/copilot-log.repository.js +0 -61
- package/dist/features/copilot/components/AdminCopilotView.d.ts +0 -21
- package/dist/features/copilot/components/AdminCopilotView.js +0 -51
- package/dist/features/copilot/components/index.d.ts +0 -2
- package/dist/features/copilot/components/index.js +0 -1
- package/dist/features/copilot/hooks/useCopilotChat.d.ts +0 -25
- package/dist/features/copilot/hooks/useCopilotChat.js +0 -65
- package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +0 -15
- package/dist/features/copilot/hooks/useCopilotFeedback.js +0 -15
- package/dist/features/copilot/index.d.ts +0 -3
- package/dist/features/copilot/index.js +0 -3
|
@@ -16,6 +16,10 @@ export const PRODUCT_INDEXED_FIELDS = [
|
|
|
16
16
|
"storeId",
|
|
17
17
|
"status",
|
|
18
18
|
"category",
|
|
19
|
+
/* Indexed: `array-contains` on the ASSIGNED list is what the category
|
|
20
|
+
* delete cascade queries to find a product filed here as its SECOND
|
|
21
|
+
* category — `findBy("category", id)` sees only the default leaf. */
|
|
22
|
+
"categoryIds",
|
|
19
23
|
"featured",
|
|
20
24
|
"listingType",
|
|
21
25
|
"isPromoted",
|
|
@@ -74,6 +78,9 @@ export const PRODUCT_PUBLIC_FIELDS = [
|
|
|
74
78
|
"title",
|
|
75
79
|
"description",
|
|
76
80
|
"category",
|
|
81
|
+
/* Public: the chips on a detail page render the assignments. */
|
|
82
|
+
"categoryIds",
|
|
83
|
+
"categoryIdNames",
|
|
77
84
|
"subcategory",
|
|
78
85
|
"brand",
|
|
79
86
|
"price",
|
|
@@ -92,6 +99,35 @@ export const PRODUCT_PUBLIC_FIELDS = [
|
|
|
92
99
|
// Buyer-facing: drives the "Final Sale" card pill and the detail-page
|
|
93
100
|
// Delivery & Returns line. Absent means final sale — read via isFinalSale().
|
|
94
101
|
"finalSale",
|
|
102
|
+
// Buyer-facing by necessity: "lot of 12" is the single most important thing
|
|
103
|
+
// about a lot listing, and a buyer who cannot see it thinks they are bidding
|
|
104
|
+
// on one item.
|
|
105
|
+
"isLot",
|
|
106
|
+
"lotSize",
|
|
107
|
+
/* D13/D14 — buyer-facing by definition: a colourway is WHY someone buys that
|
|
108
|
+
* particular spare, and a unit label is how they tell two identical listings
|
|
109
|
+
* apart. `assembledFrom` is listed too — it names product ids that are
|
|
110
|
+
* already public listings, and "built from these parts" is provenance a
|
|
111
|
+
* buyer should see. */
|
|
112
|
+
"unitLabel",
|
|
113
|
+
"partGlossaryId",
|
|
114
|
+
"colourway",
|
|
115
|
+
"mouldVariant",
|
|
116
|
+
"assembledFrom",
|
|
117
|
+
/* D8 — public because "N people viewed this this week" is the honest social
|
|
118
|
+
* proof H2 is built on, and it is a figure we measured. 🛑 Deliberately NOT
|
|
119
|
+
* in PRODUCT_UPDATABLE_FIELDS: a seller who could write their own view count
|
|
120
|
+
* turns a measurement into a claim. */
|
|
121
|
+
"views7d",
|
|
122
|
+
"views30d",
|
|
123
|
+
/* 🛑 D13/D14. Listed here or an update STRIPS them: the write schema has no
|
|
124
|
+
* `.passthrough()`, so a field absent from this list is dropped before it
|
|
125
|
+
* reaches Firestore while the write still returns 200 (Root Cause #101). */
|
|
126
|
+
"unitLabel",
|
|
127
|
+
"partGlossaryId",
|
|
128
|
+
"colourway",
|
|
129
|
+
"mouldVariant",
|
|
130
|
+
"assembledFrom",
|
|
95
131
|
"listingType",
|
|
96
132
|
"auctionEndDate",
|
|
97
133
|
"startingBid",
|
|
@@ -147,6 +183,12 @@ export const PRODUCT_UPDATABLE_FIELDS = [
|
|
|
147
183
|
"title",
|
|
148
184
|
"description",
|
|
149
185
|
"category",
|
|
186
|
+
/* I1 — the ASSIGNED list. 🛑 Named here or an update STRIPS it while still
|
|
187
|
+
* returning 200 (Root Cause #101), which is exactly how a UI-created
|
|
188
|
+
* product lost its chain. `categorySlugs`/`categoryNames` stay OUT: they
|
|
189
|
+
* are derived, and a caller that could set them could desync the closure
|
|
190
|
+
* from the assignments it is supposed to be the closure OF. */
|
|
191
|
+
"categoryIds",
|
|
150
192
|
"subcategory",
|
|
151
193
|
"brand",
|
|
152
194
|
"price",
|
|
@@ -159,6 +201,13 @@ export const PRODUCT_UPDATABLE_FIELDS = [
|
|
|
159
201
|
"shippingInfo",
|
|
160
202
|
"returnPolicy",
|
|
161
203
|
"finalSale",
|
|
204
|
+
/* 🛑 Listed here or an update STRIPS them. The write schema has no
|
|
205
|
+
* `.passthrough()`, so a field absent from this list is dropped before it
|
|
206
|
+
* reaches Firestore while the write still returns 200 — exactly how a
|
|
207
|
+
* UI-created product lost its `categorySlugs` chain (Root Cause #101) and how
|
|
208
|
+
* `compareAtPrice` is silently discarded today (F1 #25). */
|
|
209
|
+
"isLot",
|
|
210
|
+
"lotSize",
|
|
162
211
|
"pickupAddressId",
|
|
163
212
|
"condition",
|
|
164
213
|
"insurance",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D2 Fix 4 — the cross-resource search RESULTS page.
|
|
3
|
+
*
|
|
4
|
+
* Replaces a pure redirect. Before this, `/search` mapped each of 15
|
|
5
|
+
* `SearchResourceType` values to a browse route and `redirect()`ed, so a buyer
|
|
6
|
+
* searching "dranzer" could not see that we have a product, a category, a blog
|
|
7
|
+
* post and an FAQ about it — the dropdown only chose which single listing page
|
|
8
|
+
* to send them to.
|
|
9
|
+
*
|
|
10
|
+
* Measured on production: "burst" returns 21 hits across SIX groups
|
|
11
|
+
* (listings, categories, brands, stores, articles, FAQs); "zzzznope" and "!!!"
|
|
12
|
+
* return none.
|
|
13
|
+
*
|
|
14
|
+
* 🛑 STILL `noindex`, and that is deliberate rather than inherited. A search
|
|
15
|
+
* results page is the classic crawl trap: one URL per query string, thin
|
|
16
|
+
* content, and no canonical answer to index. The redirect it replaces was
|
|
17
|
+
* `noindex` too, so this changes the page's usefulness to a HUMAN without
|
|
18
|
+
* changing its relationship to a crawler. The `moreHref` links go to the real
|
|
19
|
+
* indexable listing pages.
|
|
20
|
+
*
|
|
21
|
+
* 🛑 NO PRICES ANYWHERE. Prices are hidden from signed-out visitors and this
|
|
22
|
+
* is the one surface where gating every row would be most of the markup.
|
|
23
|
+
* A title plus a link is honest, needs no `<GatedPrice>`, and cannot leak —
|
|
24
|
+
* there is nothing on the row to leak.
|
|
25
|
+
*/
|
|
26
|
+
import React from "react";
|
|
27
|
+
import type { SearchEverythingResult } from "../../../_internal/server/features/search/search-everything";
|
|
28
|
+
export interface SearchResultsPageViewProps {
|
|
29
|
+
result: SearchEverythingResult;
|
|
30
|
+
/** Rendered above the groups — the consumer owns the input control. */
|
|
31
|
+
renderSearchInput?: () => React.ReactNode;
|
|
32
|
+
}
|
|
33
|
+
export declare function SearchResultsPageView({ result, renderSearchInput, }: SearchResultsPageViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Section, Container, Stack, Row, Div, Heading, Text, Span, TextLink } from "../../../ui/index.js";
|
|
3
|
+
export function SearchResultsPageView({ result, renderSearchInput, }) {
|
|
4
|
+
const { q, terms, groups, totalHits } = result;
|
|
5
|
+
const hasQuery = q.trim().length > 0;
|
|
6
|
+
/*
|
|
7
|
+
* 🛑 THREE states, not two, and the middle one is the honest part.
|
|
8
|
+
*
|
|
9
|
+
* no query — prompt, no claim about results
|
|
10
|
+
* query, 0 terms — the query normalised away ("!!!", "—"). Saying
|
|
11
|
+
* "no results for !!!" implies we looked; we could
|
|
12
|
+
* not. A2's bug was the inverse of this: a
|
|
13
|
+
* punctuation-only query returned the ENTIRE
|
|
14
|
+
* published catalogue as search results.
|
|
15
|
+
* query, 0 hits — genuinely nothing matched
|
|
16
|
+
*/
|
|
17
|
+
const normalisedAway = hasQuery && terms.length === 0;
|
|
18
|
+
return (_jsx(Section, { padding: "page", children: _jsx(Container, { children: _jsxs(Stack, { gap: "loose", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, children: hasQuery ? `Search: ${q}` : "Search" }), hasQuery && !normalisedAway && (_jsx(Text, { color: "muted", size: "sm", children: totalHits === 0
|
|
19
|
+
? "Nothing matched."
|
|
20
|
+
: `${totalHits}${groups.some((g) => g.truncated) ? "+" : ""} result${totalHits === 1 ? "" : "s"} across ${groups.length} section${groups.length === 1 ? "" : "s"}.` }))] }), renderSearchInput?.(), !hasQuery && (_jsx(Text, { color: "muted", children: "Search listings, categories, brands, stores, articles and help pages at once." })), normalisedAway && (_jsx(Text, { color: "muted", children: "That search had no searchable words in it. Try a product, part or category name." })), groups.map((group) => (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", wrap: true, gap: "sm", children: [_jsxs(Heading, { level: 2, size: "lg", children: [group.label, group.truncated && (_jsxs(Span, { size: "sm", color: "muted", children: [" ", "(", group.hits.length, "+)"] }))] }), _jsxs(TextLink, { href: group.moreHref, children: ["See all ", group.label.toLowerCase(), " \u2192"] })] }), _jsx(Stack, { gap: "xs", children: group.hits.map((hit) => (_jsxs(Div, { surface: "card", padding: "sm", rounded: "md", children: [_jsx(TextLink, { href: hit.href, children: hit.title }), hit.detail && (_jsx(Text, { size: "xs", color: "muted", children: hit.detail }))] }, `${group.key}-${hit.id}`))) })] }, group.key))), hasQuery && !normalisedAway && totalHits === 0 && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { color: "muted", children: ["No listings, categories, brands, stores, articles or help pages matched \u201C", q, "\u201D."] }), _jsx(TextLink, { href: "/products", children: "Browse all listings \u2192" })] }))] }) }) }));
|
|
21
|
+
}
|
|
@@ -6,5 +6,21 @@ export interface CategoryInlineSelectProps {
|
|
|
6
6
|
disabled?: boolean;
|
|
7
7
|
/** Show the "+ Create new category" option. Defaults to true — sellers can create categories inline. */
|
|
8
8
|
allowCreate?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* I2 — the ADDITIONAL assignments, beyond `value`.
|
|
11
|
+
*
|
|
12
|
+
* 🛑 Omitting this pair keeps the component exactly single-select, which is
|
|
13
|
+
* what every existing call site gets. Multi-assignment is opt-in per surface
|
|
14
|
+
* rather than switched on globally, because `value`/`onChange` is a `string`
|
|
15
|
+
* contract that five call sites rely on and changing it under them is Root
|
|
16
|
+
* Cause #20 (a public prop change without its consumers in the same commit).
|
|
17
|
+
*/
|
|
18
|
+
extraValues?: string[];
|
|
19
|
+
onExtraValuesChange?: (ids: string[]) => void;
|
|
20
|
+
/**
|
|
21
|
+
* How many assignments are allowed in total, `value` included. Defaults to
|
|
22
|
+
* `MAX_CATEGORY_ASSIGNMENTS`, which is what the repository refuses above.
|
|
23
|
+
*/
|
|
24
|
+
maxAssignments?: number;
|
|
9
25
|
}
|
|
10
|
-
export declare function CategoryInlineSelect({ value, onChange, placeholder, disabled, allowCreate, }: CategoryInlineSelectProps): React.JSX.Element;
|
|
26
|
+
export declare function CategoryInlineSelect({ value, onChange, placeholder, disabled, allowCreate, extraValues, onExtraValuesChange, maxAssignments, }: CategoryInlineSelectProps): React.JSX.Element;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
|
|
4
4
|
import { CategoryQuickCreateForm } from "../../admin/components/CategoryQuickCreateForm.js";
|
|
5
5
|
import { apiClient } from "../../../http/index.js";
|
|
6
6
|
import { ADMIN_ENDPOINTS, CATEGORY_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
7
|
+
import { Stack } from "../../../ui/components/Layout.js";
|
|
8
|
+
import { Text } from "../../../ui/components/Typography.js";
|
|
9
|
+
import { MAX_CATEGORY_ASSIGNMENTS } from "../../products/repository/category-assignment.js";
|
|
7
10
|
// Admin: paginated + server-side search via /api/admin/categories (admin/mod only).
|
|
8
11
|
// Response via successResponse({ data: items, total }) → apiClient unwraps to { data, total, ... }.
|
|
9
12
|
async function loadAdminCategoryOptions(query, page) {
|
|
@@ -35,9 +38,17 @@ async function loadPublicCategoryOptions(query, _page) {
|
|
|
35
38
|
const items = q ? all.filter((c) => c.label.toLowerCase().includes(q)) : all;
|
|
36
39
|
return { items, hasMore: false, nextPage: 2 };
|
|
37
40
|
}
|
|
38
|
-
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categories…", disabled, allowCreate = true, }) {
|
|
41
|
+
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categories…", disabled, allowCreate = true, extraValues, onExtraValuesChange, maxAssignments = MAX_CATEGORY_ASSIGNMENTS, }) {
|
|
39
42
|
const loadOptions = allowCreate ? loadAdminCategoryOptions : loadPublicCategoryOptions;
|
|
40
|
-
|
|
43
|
+
const primary = (_jsx(PaginatedSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadOptions, placeholder: placeholder, disabled: disabled, createLabel: "category", drawerTitle: "Create Category", renderCreateForm: allowCreate
|
|
41
44
|
? ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel }))
|
|
42
45
|
: undefined }));
|
|
46
|
+
// Single-select unless the caller opted in — byte-identical to before.
|
|
47
|
+
if (!onExtraValuesChange)
|
|
48
|
+
return primary;
|
|
49
|
+
const extras = extraValues ?? [];
|
|
50
|
+
const remaining = Math.max(0, maxAssignments - 1 - extras.length);
|
|
51
|
+
return (_jsxs(Stack, { gap: "sm", children: [primary, _jsx(PaginatedSelect, { multiple: true, value: extras, onChange: (v) => onExtraValuesChange(Array.isArray(v) ? v : []), loadOptions: loadOptions, placeholder: remaining > 0
|
|
52
|
+
? `Also list under… (up to ${remaining} more)`
|
|
53
|
+
: "No more categories can be added", disabled: disabled || remaining <= 0 }), _jsx(Text, { size: "xs", color: "muted", children: "Optional. Add a category from a DIFFERENT tree \u2014 a franchise, for example. Two categories from the same tree will be refused, because the broader one is already implied by the narrower." })] }));
|
|
43
54
|
}
|
|
@@ -6,6 +6,16 @@ export interface SellerProductDraft {
|
|
|
6
6
|
slug?: string;
|
|
7
7
|
description?: string;
|
|
8
8
|
category?: string;
|
|
9
|
+
/**
|
|
10
|
+
* I2 — the assignment list, DEFAULT FIRST, with `category` mirroring `[0]`.
|
|
11
|
+
*
|
|
12
|
+
* 🛑 Declared on the TYPE as well as the Zod schema. The schema is
|
|
13
|
+
* `.passthrough()`, so an undeclared key survives at runtime — and stays
|
|
14
|
+
* untyped, so the picker could not read `values.categoryIds` at all. Both
|
|
15
|
+
* halves are needed, which is the same trap as a field missing from
|
|
16
|
+
* `PRODUCT_UPDATABLE_FIELDS` (Root Cause #101) one layer out.
|
|
17
|
+
*/
|
|
18
|
+
categoryIds?: string[];
|
|
9
19
|
brand?: string;
|
|
10
20
|
condition?: string;
|
|
11
21
|
tags?: string[];
|
|
@@ -134,10 +144,25 @@ export interface SellerProductShellProps {
|
|
|
134
144
|
onDiscard?: () => void;
|
|
135
145
|
isLoading?: boolean;
|
|
136
146
|
storeSlug?: string;
|
|
137
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Render a custom category selector (e.g. PaginatedSelect with createLabel).
|
|
149
|
+
*
|
|
150
|
+
* I2 — `extraValues`/`onExtraValuesChange` carry the ADDITIONAL assignments.
|
|
151
|
+
* A selector that ignores them stays exactly single-select, which is what
|
|
152
|
+
* every surface except this one does.
|
|
153
|
+
*/
|
|
138
154
|
renderCategorySelector?: (props: {
|
|
139
155
|
value: string;
|
|
140
156
|
onChange: (v: string) => void;
|
|
157
|
+
/**
|
|
158
|
+
* 🛑 OPTIONAL, and that is the whole point. `QuickProductForm` is a second
|
|
159
|
+
* consumer of this slot and making these required broke it immediately —
|
|
160
|
+
* Root Cause #20, a public prop change without its consumers, which the
|
|
161
|
+
* comment above literally warns about. A selector that ignores them stays
|
|
162
|
+
* exactly single-select.
|
|
163
|
+
*/
|
|
164
|
+
extraValues?: string[];
|
|
165
|
+
onExtraValuesChange?: (ids: string[]) => void;
|
|
141
166
|
}) => React.ReactNode;
|
|
142
167
|
/** Render a custom brand selector. */
|
|
143
168
|
renderBrandSelector?: (props: {
|
|
@@ -46,6 +46,15 @@ const sellerProductSchema = z.object({
|
|
|
46
46
|
description: z.string().max(10000).optional().or(z.literal("")),
|
|
47
47
|
price: z.number().min(0).optional(),
|
|
48
48
|
category: z.string().optional(),
|
|
49
|
+
/**
|
|
50
|
+
* I2 — the assignment list, DEFAULT FIRST. `category` mirrors `[0]`.
|
|
51
|
+
*
|
|
52
|
+
* Declared even though this schema is `.passthrough()`, because the draft's
|
|
53
|
+
* TYPE is what the render site reads `values.categoryIds` off — passthrough
|
|
54
|
+
* keeps an undeclared key at runtime and still leaves it untyped, so the
|
|
55
|
+
* picker could not see it.
|
|
56
|
+
*/
|
|
57
|
+
categoryIds: z.array(z.string()).max(4).optional(),
|
|
49
58
|
brand: z.string().optional(),
|
|
50
59
|
isActive: z.boolean().optional(),
|
|
51
60
|
images: z
|
|
@@ -106,7 +115,27 @@ function StepBasic({ values, onChange, renderCategorySelector, renderBrandSelect
|
|
|
106
115
|
condition: CONDITION_OPTIONS.find((o) => o.value === (values.condition ?? "new"))?.label ?? "",
|
|
107
116
|
siteName: "LetItRip",
|
|
108
117
|
};
|
|
109
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsxs(FormGroup, { columns: 2, children: [renderCategorySelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Category" }), renderCategorySelector({
|
|
118
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsxs(FormGroup, { columns: 2, children: [renderCategorySelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Category" }), renderCategorySelector({
|
|
119
|
+
value: values.category ?? "",
|
|
120
|
+
onChange: (v) => onChange({ category: v }),
|
|
121
|
+
/*
|
|
122
|
+
* 🛑 `categoryIds[0]` is the DEFAULT assignment and is already
|
|
123
|
+
* rendered by the primary picker above, so the extras list is
|
|
124
|
+
* everything AFTER it. Passing the whole array would show the
|
|
125
|
+
* primary twice and let a seller "remove" it from a control that
|
|
126
|
+
* does not own it.
|
|
127
|
+
*/
|
|
128
|
+
extraValues: (values.categoryIds ?? []).slice(1),
|
|
129
|
+
onExtraValuesChange: (ids) => onChange({
|
|
130
|
+
/*
|
|
131
|
+
* Rebuilt primary-first on every change, because the server
|
|
132
|
+
* derives the default by DEPTH and reorders the stored array
|
|
133
|
+
* anyway — sending it in a different order would make the
|
|
134
|
+
* form and the saved document disagree until the next load.
|
|
135
|
+
*/
|
|
136
|
+
categoryIds: [values.category ?? "", ...ids].filter(Boolean),
|
|
137
|
+
}),
|
|
138
|
+
})] })) : (_jsx(FormField, { name: "category", label: "Category", type: "text", value: values.category ?? "", onChange: (v) => onChange({ category: v }), placeholder: "e.g. Trading Cards" })), _jsx(FormField, { name: "condition", label: "Condition", type: "select", value: values.condition ?? "new", onChange: (v) => onChange({ condition: v }), options: CONDITION_OPTIONS })] }), renderTemplateSelector?.({
|
|
110
139
|
categoryId: values.category ?? "",
|
|
111
140
|
condition: values.condition ?? "new",
|
|
112
141
|
context: templateContext,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { formatPhysicalLocation } from "../../../utils/physical-location.js";
|
|
4
4
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
5
|
+
import { trackError, ErrorCategory, ErrorSeverity, } from "../../../monitoring/error-tracking.js";
|
|
5
6
|
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
6
7
|
import { SIEVE_OP, Stack, sieveFilter } from "@mohasinac/appkit/client";
|
|
7
8
|
import { LOW_STOCK_THRESHOLD } from "../../store-extensions/constants/alert-metrics.js";
|
|
@@ -9,7 +10,7 @@ import { sortBy } from "@mohasinac/appkit/client";
|
|
|
9
10
|
import { useState, useCallback } from "react";
|
|
10
11
|
import { useEntityDelete } from "../../../react/hooks/useEntityDelete.js";
|
|
11
12
|
import { useActionDispatch } from "../../../react/hooks/use-action-dispatch.js";
|
|
12
|
-
import { Eye, EyeOff, Pencil, Trash2, Printer, MapPin, Boxes } from "lucide-react";
|
|
13
|
+
import { Eye, EyeOff, Pencil, Trash2, Printer, MapPin, Boxes, Wrench, Scissors } from "lucide-react";
|
|
13
14
|
import { PhysicalLocationModal } from "./PhysicalLocationModal.js";
|
|
14
15
|
import { BulkStockModal } from "./BulkStockModal.js";
|
|
15
16
|
import { useUrlTable } from "../../../react/hooks/useUrlTable.js";
|
|
@@ -30,6 +31,7 @@ import { SellerProductsCards } from "./SellerProductsCards.js";
|
|
|
30
31
|
import { SellerProductsFilterFields } from "./SellerProductsFilterDrawer.js";
|
|
31
32
|
import { listingBadgeVariant } from "../../products/utils/listing-badge-variant.js";
|
|
32
33
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
34
|
+
import { AssemblyDrawer, } from "../../parts/components/AssemblyDrawer.js";
|
|
33
35
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
34
36
|
import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers.js";
|
|
35
37
|
const FILTER_KEYS = [
|
|
@@ -148,6 +150,29 @@ const PRODUCT_COLUMNS = [
|
|
|
148
150
|
* Both stock fields have been `canFilter`/`canSort` at the repository layer
|
|
149
151
|
* the whole time; only the view omitted them.
|
|
150
152
|
*/
|
|
153
|
+
/*
|
|
154
|
+
* D8 — views, which were collected on ~30 surfaces and joined to NOTHING.
|
|
155
|
+
* A seller could not see how many people looked at their listing: the metric
|
|
156
|
+
* every competitor platform leads with.
|
|
157
|
+
*
|
|
158
|
+
* 🛑 Three states, and the middle one is the whole reason this is a column
|
|
159
|
+
* rather than a number in a subtitle. `undefined` means NOT YET ROLLED UP —
|
|
160
|
+
* the nightly pass has not run — and renders an em-dash; `0` means measured
|
|
161
|
+
* and nobody looked, and renders `0`. Collapsing them with `?? 0` is exactly
|
|
162
|
+
* what made the admin Top Products table show "0 views" on every row as
|
|
163
|
+
* though it were a measurement (A5).
|
|
164
|
+
*/
|
|
165
|
+
{
|
|
166
|
+
key: "views",
|
|
167
|
+
header: "7d views",
|
|
168
|
+
className: "w-24 text-right",
|
|
169
|
+
render: (row) => {
|
|
170
|
+
if (typeof row.views7d !== "number") {
|
|
171
|
+
return _jsx(Span, { size: "xs", color: "faint", children: "\u2014" });
|
|
172
|
+
}
|
|
173
|
+
return (_jsx(Span, { size: "xs", color: row.views7d > 0 ? "primary" : "muted", numeric: true, children: row.views7d.toLocaleString() }));
|
|
174
|
+
},
|
|
175
|
+
},
|
|
151
176
|
{
|
|
152
177
|
key: "stock",
|
|
153
178
|
header: "Stock",
|
|
@@ -227,6 +252,94 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
227
252
|
const [setLocationOpen, setSetLocationOpen] = useState(false);
|
|
228
253
|
const [selectedIdsForLocation, setSelectedIdsForLocation] = useState([]);
|
|
229
254
|
const [setStockOpen, setSetStockOpen] = useState(false);
|
|
255
|
+
/*
|
|
256
|
+
* ── D13: assembly ──────────────────────────────────────────────────────
|
|
257
|
+
*
|
|
258
|
+
* 🛑 The drawer's data is fetched when it OPENS, not with the page. 50 part
|
|
259
|
+
* listings and a composition per row would be a read nobody asked for on a
|
|
260
|
+
* view that renders 50 rows — and the drawer is used on a handful of them.
|
|
261
|
+
*/
|
|
262
|
+
const [assembleFor, setAssembleFor] = useState(null);
|
|
263
|
+
const [assembleData, setAssembleData] = useState(null);
|
|
264
|
+
const [assembleLoading, setAssembleLoading] = useState(false);
|
|
265
|
+
const openAssembly = useCallback(async (row, direction) => {
|
|
266
|
+
setAssembleFor({ id: row.id, title: row.primary ?? row.id, direction });
|
|
267
|
+
setAssembleData(null);
|
|
268
|
+
setAssembleLoading(true);
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch(SELLER_ENDPOINTS.PRODUCT_ASSEMBLE(row.id));
|
|
271
|
+
const body = (await res.json().catch(() => null));
|
|
272
|
+
if (!res.ok)
|
|
273
|
+
throw new Error(body?.error ?? "Could not load your parts");
|
|
274
|
+
setAssembleData({
|
|
275
|
+
compositions: body?.data?.compositions ?? [],
|
|
276
|
+
options: body?.data?.options ?? [],
|
|
277
|
+
targetAvailable: body?.data?.targetAvailable ?? 0,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
const e = normalizeError(err);
|
|
282
|
+
/*
|
|
283
|
+
* 🛑 Closed and reported, never left open and empty. An empty picker
|
|
284
|
+
* with no explanation is the state in which a seller concludes they
|
|
285
|
+
* hold no parts — which may be false.
|
|
286
|
+
*/
|
|
287
|
+
setAssembleFor(null);
|
|
288
|
+
/*
|
|
289
|
+
* 🛑 AUTHORED copy, not `e.message`. Unlike the 409 (which carries
|
|
290
|
+
* `{needs, has}` and composes a sentence around the real figures),
|
|
291
|
+
* a load failure has no number worth surfacing — so this is one honest
|
|
292
|
+
* line, and the raw text goes to the recorder where it belongs.
|
|
293
|
+
*/
|
|
294
|
+
showToast("Could not load your parts. Please try again.", "error");
|
|
295
|
+
trackError(e.message, ErrorCategory.API, ErrorSeverity.MEDIUM, {
|
|
296
|
+
component: "SellerProductsView",
|
|
297
|
+
action: "openAssembly",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
setAssembleLoading(false);
|
|
302
|
+
}
|
|
303
|
+
}, [showToast]);
|
|
304
|
+
const submitAssembly = useCallback(async (input) => {
|
|
305
|
+
const id = assembleFor?.id;
|
|
306
|
+
if (!id)
|
|
307
|
+
throw new Error("No listing selected");
|
|
308
|
+
const res = await fetch(SELLER_ENDPOINTS.PRODUCT_ASSEMBLE(id), {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: { "Content-Type": "application/json" },
|
|
311
|
+
body: JSON.stringify(input),
|
|
312
|
+
});
|
|
313
|
+
const body = (await res.json().catch(() => null));
|
|
314
|
+
/*
|
|
315
|
+
* 🛑 THROWS with the server's own message, which the drawer renders. A
|
|
316
|
+
* 409 reads "Not enough available: needs 6, has 5" — the one sentence
|
|
317
|
+
* that tells the seller what to do. Swallowing it into a generic failure
|
|
318
|
+
* would turn "restock" into "retry".
|
|
319
|
+
*/
|
|
320
|
+
if (!res.ok) {
|
|
321
|
+
/*
|
|
322
|
+
* 🛑 Carries the structured DETAIL to the drawer and never a rendered
|
|
323
|
+
* sentence. The drawer composes its own copy around `{needs, has}`;
|
|
324
|
+
* `message` here is for the recorder (Root Cause #86 —
|
|
325
|
+
* `audit-raw-error-text` blocks showing a thrown `.message`).
|
|
326
|
+
*/
|
|
327
|
+
const err = new Error(body?.error ?? "assembly failed");
|
|
328
|
+
/*
|
|
329
|
+
* Typed rather than `unknown`: the 409's shape is OURS — the planner
|
|
330
|
+
* throws `{ kind, productId, needs, has }` — so declaring it is what
|
|
331
|
+
* lets the drawer compose a sentence around real figures instead of
|
|
332
|
+
* casting at the boundary (Root Cause #57/#70: a cast at a boundary is
|
|
333
|
+
* where this class of bug lives).
|
|
334
|
+
*/
|
|
335
|
+
err.detail = { code: body?.code, ...(body?.details ?? {}) };
|
|
336
|
+
throw err;
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
unitsBuilt: body?.data?.unitsBuilt ?? input.units,
|
|
340
|
+
consumed: body?.data?.consumed ?? [],
|
|
341
|
+
};
|
|
342
|
+
}, [assembleFor?.id]);
|
|
230
343
|
const [selectedIdsForStock, setSelectedIdsForStock] = useState([]);
|
|
231
344
|
const [stockBusy, setStockBusy] = useState(false);
|
|
232
345
|
/*
|
|
@@ -472,6 +585,9 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
472
585
|
price: priceRaw ? `₹${priceRaw.toLocaleString("en-IN")}` : "—",
|
|
473
586
|
stock: stockLabel || "—",
|
|
474
587
|
stockValue,
|
|
588
|
+
// Carried through ONLY when the document really has it — see the
|
|
589
|
+
// field's own note on undefined-vs-zero.
|
|
590
|
+
...(typeof item.views7d === "number" ? { views7d: item.views7d } : {}),
|
|
475
591
|
physicalLocation: item.physicalLocation &&
|
|
476
592
|
typeof item.physicalLocation.zone === "string"
|
|
477
593
|
? item.physicalLocation
|
|
@@ -524,8 +640,18 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
524
640
|
: `/products/${row.id}`,
|
|
525
641
|
renderRowActions: (row) => {
|
|
526
642
|
const isPublished = (statusOverrides.get(row.id) ?? row.status) === "published";
|
|
527
|
-
return (_jsxs(Row, { gap: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); handleEdit(row); }, "aria-label": ACTIONS.STORE["edit-listing"].ariaLabel, children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleTogglePublish(row); }, "aria-label": isPublished ? "Unpublish" : "Publish", title: isPublished ? "Unpublish" : "Publish", disabled: publishingId === row.id, children: isPublished ? _jsx(EyeOff, { className: "w-4 h-4" }) : _jsx(Eye, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleDuplicate(row); }, "aria-label": "Duplicate listing", title: "Duplicate", children: "\u29C9" }), onDeleteProduct && (_jsx(Button, { variant: "ghost", size: "sm", action: ACTIONS.STORE["delete-listing"], onClick: (e) => { e.stopPropagation(); void handleDelete(row); }, disabled: deletingId === row.id, className: "text-[var(--appkit-color-error)] hover:bg-[var(--appkit-color-border-subtle)]", children: _jsx(Trash2, { className: "w-4 h-4" }) }))] }));
|
|
643
|
+
return (_jsxs(Row, { gap: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); handleEdit(row); }, "aria-label": ACTIONS.STORE["edit-listing"].ariaLabel, children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "assemble"); }, "aria-label": ACTIONS.STORE["assemble-from-parts"].ariaLabel, title: ACTIONS.STORE["assemble-from-parts"].label, disabled: assembleLoading, children: _jsx(Wrench, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "part-out"); }, "aria-label": ACTIONS.STORE["part-out-listing"].ariaLabel, title: ACTIONS.STORE["part-out-listing"].label, disabled: assembleLoading, children: _jsx(Scissors, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleTogglePublish(row); }, "aria-label": isPublished ? "Unpublish" : "Publish", title: isPublished ? "Unpublish" : "Publish", disabled: publishingId === row.id, children: isPublished ? _jsx(EyeOff, { className: "w-4 h-4" }) : _jsx(Eye, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleDuplicate(row); }, "aria-label": "Duplicate listing", title: "Duplicate", children: "\u29C9" }), onDeleteProduct && (_jsx(Button, { variant: "ghost", size: "sm", action: ACTIONS.STORE["delete-listing"], onClick: (e) => { e.stopPropagation(); void handleDelete(row); }, disabled: deletingId === row.id, className: "text-[var(--appkit-color-error)] hover:bg-[var(--appkit-color-border-subtle)]", children: _jsx(Trash2, { className: "w-4 h-4" }) }))] }));
|
|
528
644
|
},
|
|
529
645
|
};
|
|
530
|
-
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), setLocationOpen && (_jsx(PhysicalLocationModal, { count: selectedIdsForLocation.length, onSave: (loc) => handleSetLocation(loc, selectedIdsForLocation), onClose: () => setSetLocationOpen(false) })), setStockOpen && (_jsx(BulkStockModal, { count: selectedIdsForStock.length, busy: stockBusy, onSave: (qty) => handleSetStock(qty, selectedIdsForStock), onClose: () => setSetStockOpen(false) }))
|
|
646
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), setLocationOpen && (_jsx(PhysicalLocationModal, { count: selectedIdsForLocation.length, onSave: (loc) => handleSetLocation(loc, selectedIdsForLocation), onClose: () => setSetLocationOpen(false) })), setStockOpen && (_jsx(BulkStockModal, { count: selectedIdsForStock.length, busy: stockBusy, onSave: (qty) => handleSetStock(qty, selectedIdsForStock), onClose: () => setSetStockOpen(false) })), assembleFor && assembleData && (_jsx(AssemblyDrawer, { isOpen: true,
|
|
647
|
+
/*
|
|
648
|
+
* 🛑 No refetch on close, and that is a stated limitation rather than
|
|
649
|
+
* a silent one: `DataListingView` owns the query and exposes no
|
|
650
|
+
* refetch handle to a consumer. The drawer reports the real
|
|
651
|
+
* before/after per component, so the seller SEES what moved — but the
|
|
652
|
+
* row behind it still shows the pre-build figure until the next
|
|
653
|
+
* navigation. Wiring a refetch means giving `ListingViewConfig` a
|
|
654
|
+
* handle, which is a shared-surface change and its own commit.
|
|
655
|
+
*/
|
|
656
|
+
onClose: () => { setAssembleFor(null); setAssembleData(null); }, direction: assembleFor.direction, targetTitle: assembleFor.title, targetAvailable: assembleData.targetAvailable, compositions: assembleData.compositions, options: assembleData.options, onSubmit: submitAssembly }))] }));
|
|
531
657
|
}
|
|
@@ -424,21 +424,6 @@ export const authored = {
|
|
|
424
424
|
expectedUiState: "The page renders logs, or a readable message naming what is unavailable. It does not show a stack trace, and it does not take the rest of the admin panel with it.",
|
|
425
425
|
endResult: "Read-only; nothing persists.",
|
|
426
426
|
},
|
|
427
|
-
"checklist-admin-site-system-copilot-admin": {
|
|
428
|
-
roles: ["admin"],
|
|
429
|
-
startPage: "/admin",
|
|
430
|
-
steps: [
|
|
431
|
-
"Sign in as admin@letitrip.in / TempPass123!.",
|
|
432
|
-
"Open /admin/copilot and read what it offers.",
|
|
433
|
-
"Read whether it states which model or provider it uses.",
|
|
434
|
-
"Submit a simple request and read the response.",
|
|
435
|
-
"Read whether any error surfaces a provider key or a raw response body.",
|
|
436
|
-
"Read whether the feature declines cleanly when unconfigured.",
|
|
437
|
-
],
|
|
438
|
-
expectedBehaviour: "The feature works or declines cleanly, and never surfaces a provider credential or a raw upstream error to the browser. Every 5xx message is scrubbed before it reaches a client, with the real one kept server-side for the error recorder.",
|
|
439
|
-
expectedUiState: "A response renders, or a readable message names what is missing. No provider key, no raw upstream body, no stack trace.",
|
|
440
|
-
endResult: "Read-only; nothing persists.",
|
|
441
|
-
},
|
|
442
427
|
"checklist-admin-site-system-team-admin": {
|
|
443
428
|
roles: ["admin"],
|
|
444
429
|
startPage: "/admin/team",
|