@mohasinac/appkit 4.57.0 → 4.58.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/beys/data.d.ts +44 -0
- package/dist/_internal/server/features/beys/data.js +107 -0
- package/dist/_internal/server/features/beys/og.d.ts +42 -0
- package/dist/_internal/server/features/beys/og.js +89 -0
- package/dist/_internal/server/features/parts/data.js +3 -2
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +86 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +12 -0
- package/dist/constants/api-endpoints.js +10 -0
- package/dist/constants/field-names.d.ts +46 -0
- package/dist/constants/field-names.js +46 -0
- package/dist/features/admin/components/AdminBeysView.d.ts +27 -0
- package/dist/features/admin/components/AdminBeysView.js +178 -0
- package/dist/features/admin/components/AdminPartsView.d.ts +27 -0
- package/dist/features/admin/components/AdminPartsView.js +160 -0
- package/dist/features/admin/components/OwnerQuickActions.d.ts +19 -2
- package/dist/features/admin/components/OwnerQuickActions.js +31 -3
- package/dist/features/admin/components/RecordOwnerQuickActions.d.ts +31 -0
- package/dist/features/admin/components/RecordOwnerQuickActions.js +128 -0
- package/dist/features/admin/components/ReferenceRecordEditor.d.ts +45 -0
- package/dist/features/admin/components/ReferenceRecordEditor.js +153 -0
- package/dist/features/admin/schemas/firestore.d.ts +1 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +15 -2
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +12 -1
- package/dist/features/beys/components/BeyDetailPageView.d.ts +50 -0
- package/dist/features/beys/components/BeyDetailPageView.js +98 -0
- package/dist/features/beys/schemas/firestore.d.ts +417 -0
- package/dist/features/beys/schemas/firestore.js +65 -0
- package/dist/features/blog/components/BlogFeaturedCard.js +15 -2
- package/dist/features/blog/components/BlogPostView.js +2 -2
- package/dist/features/categories/components/BrandDetailPageView.js +4 -2
- package/dist/features/categories/components/BundleDetailView.js +2 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +36 -3
- package/dist/features/categories/components/CategoryGrid.js +24 -2
- package/dist/features/categories/components/FranchiseDetailPageView.js +2 -3
- package/dist/features/events/components/EventCard.js +15 -2
- package/dist/features/grouped/components/GroupedListingDetailView.js +2 -3
- package/dist/features/homepage/components/CustomCardsSection.js +67 -50
- package/dist/features/homepage/components/ShopByCategorySection.js +23 -10
- package/dist/features/homepage/schemas/index.d.ts +10 -10
- package/dist/features/parts/components/PartDetailPageView.js +5 -3
- package/dist/features/parts/components/PartsSearchBox.js +79 -4
- package/dist/features/parts/schemas/admin-edit.d.ts +81 -0
- package/dist/features/parts/schemas/admin-edit.js +89 -0
- package/dist/features/parts/schemas/firestore.d.ts +2 -1
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +15 -2
- package/dist/features/products/components/MarketplaceBundleCard.js +15 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +15 -2
- package/dist/features/products/components/ProductGrid.js +18 -2
- package/dist/features/products/schemas/firestore.d.ts +18 -0
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +10 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +53 -5
- package/dist/features/seller/components/SellerProductShell.d.ts +39 -0
- package/dist/features/seller/components/SellerProductShell.js +31 -3
- package/dist/index.d.ts +8 -0
- package/dist/index.js +13 -0
- package/dist/next/routing/route-map.d.ts +13 -1
- package/dist/next/routing/route-map.js +37 -0
- package/dist/schemas/registry.d.ts +6 -6
- package/dist/seed/_helpers/category-parts-glossary-map.js +38 -38
- package/dist/seed/_helpers/category-parts.js +39 -39
- package/dist/seed/bey-products-seed-data.d.ts +18 -0
- package/dist/seed/bey-products-seed-data.js +18011 -0
- package/dist/seed/beys/chunk0.d.ts +17 -0
- package/dist/seed/beys/chunk0.js +36733 -0
- package/dist/seed/beys/chunk1.d.ts +17 -0
- package/dist/seed/beys/chunk1.js +35728 -0
- package/dist/seed/beys/chunk2.d.ts +17 -0
- package/dist/seed/beys/chunk2.js +35857 -0
- package/dist/seed/beys/chunk3.d.ts +17 -0
- package/dist/seed/beys/chunk3.js +34476 -0
- package/dist/seed/beys-seed-data.d.ts +18 -0
- package/dist/seed/beys-seed-data.js +11 -0
- package/dist/seed/homepage-sections-seed-data.js +16 -0
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +3 -0
- package/dist/seed/media-assets-seed-data.js +4 -4
- package/dist/seed/products-catalogue/chunk0.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk0.js +7262 -0
- package/dist/seed/products-catalogue/chunk1.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk1.js +7290 -0
- package/dist/seed/products-catalogue/chunk10.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk10.js +7290 -0
- package/dist/seed/products-catalogue/chunk11.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk11.js +7005 -0
- package/dist/seed/products-catalogue/chunk12.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk12.js +7002 -0
- package/dist/seed/products-catalogue/chunk13.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk13.js +7002 -0
- package/dist/seed/products-catalogue/chunk14.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk14.js +7002 -0
- package/dist/seed/products-catalogue/chunk15.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk15.js +7002 -0
- package/dist/seed/products-catalogue/chunk16.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk16.js +7002 -0
- package/dist/seed/products-catalogue/chunk17.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk17.js +7002 -0
- package/dist/seed/products-catalogue/chunk18.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk18.js +4398 -0
- package/dist/seed/products-catalogue/chunk2.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk2.js +7268 -0
- package/dist/seed/products-catalogue/chunk3.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk3.js +7264 -0
- package/dist/seed/products-catalogue/chunk4.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk4.js +7268 -0
- package/dist/seed/products-catalogue/chunk5.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk5.js +7258 -0
- package/dist/seed/products-catalogue/chunk6.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk6.js +7268 -0
- package/dist/seed/products-catalogue/chunk7.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk7.js +7311 -0
- package/dist/seed/products-catalogue/chunk8.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk8.js +7304 -0
- package/dist/seed/products-catalogue/chunk9.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk9.js +7309 -0
- package/dist/seed/products-catalogue-seed-data.d.ts +2 -22
- package/dist/seed/products-catalogue-seed-data.js +58 -86382
- package/dist/seed/seed-purpose.js +9 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +8 -0
- package/dist/styles.css +468 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/PosterCard.d.ts +99 -0
- package/dist/ui/components/PosterCard.js +109 -0
- package/dist/ui/components/PosterCard.style.css +466 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +108 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* AdminBeysView — browse and correct the generated Beyblade database.
|
|
5
|
+
*
|
|
6
|
+
* ── 🛑 WHY THIS IS A CORRECTION SURFACE, NOT A CRUD SCREEN ────────────────
|
|
7
|
+
*
|
|
8
|
+
* `beys` is 1,988 rows built by committed generators from a crawled corpus. So
|
|
9
|
+
* there is no "Add Beyblade" button: a row authored here would have no source,
|
|
10
|
+
* would vanish on the next regeneration, and would make the claim "this dataset
|
|
11
|
+
* rebuilds from `appkit/src/seed/` alone" false. Adding one is a change to the
|
|
12
|
+
* generator.
|
|
13
|
+
*
|
|
14
|
+
* What this surface IS for is the thing the corpus demonstrably needs — fixing
|
|
15
|
+
* a wrong extraction. On mode data alone the source was wrong five times in one
|
|
16
|
+
* session: an inverted tip transition, a mechanism attributed to the wrong
|
|
17
|
+
* part, a part with no modes at all, and two misexpanded abbreviations. A
|
|
18
|
+
* correction made here survives the next reseed, field by field
|
|
19
|
+
* (`adminEditedFields`).
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:beys
|
|
22
|
+
* @tag layer:feature
|
|
23
|
+
* @tag access:client
|
|
24
|
+
* @tag consumers:app/[locale]/admin/beys/page.tsx
|
|
25
|
+
*/
|
|
26
|
+
import React from "react";
|
|
27
|
+
import { Heading, ListingLayout, Stack, Text } from "../../../ui/index.js";
|
|
28
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
29
|
+
import { BEY_FIELDS } from "../../../constants/field-names.js";
|
|
30
|
+
import { sortBy } from "../../../constants/sort.js";
|
|
31
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
32
|
+
import { DataListingView } from "./DataListingView.js";
|
|
33
|
+
/**
|
|
34
|
+
* 🛑 Rows are read from WHICHEVER envelope shape arrives.
|
|
35
|
+
*
|
|
36
|
+
* `apiClient.get()` unwraps `{success, data}` and returns `data` itself, so an
|
|
37
|
+
* endpoint that puts rows directly in `data` hands `mapRows` an ARRAY while one
|
|
38
|
+
* that nests them hands it an object. Reading `.data` off an array yields
|
|
39
|
+
* undefined — which is exactly how the categories listing rendered "No
|
|
40
|
+
* categories found" against a 200 carrying 58 rows. This route returns the
|
|
41
|
+
* object shape, and tolerating both is what keeps that from mattering.
|
|
42
|
+
*/
|
|
43
|
+
function extractRows(response) {
|
|
44
|
+
if (Array.isArray(response))
|
|
45
|
+
return response;
|
|
46
|
+
if (!response || typeof response !== "object")
|
|
47
|
+
return [];
|
|
48
|
+
const obj = response;
|
|
49
|
+
if (Array.isArray(obj.data))
|
|
50
|
+
return obj.data;
|
|
51
|
+
return Array.isArray(obj.items) ? obj.items : [];
|
|
52
|
+
}
|
|
53
|
+
function extractTotal(response) {
|
|
54
|
+
if (Array.isArray(response) || !response || typeof response !== "object")
|
|
55
|
+
return undefined;
|
|
56
|
+
const total = response.total;
|
|
57
|
+
return typeof total === "number" ? total : undefined;
|
|
58
|
+
}
|
|
59
|
+
/*
|
|
60
|
+
* audit-create-affordance-ok: this listing is deliberately inbound-only.
|
|
61
|
+
* The 1,988 rows are produced by committed generators from a crawled
|
|
62
|
+
* corpus, so there is no POST endpoint and no create route key to point a
|
|
63
|
+
* button at — a Beyblade authored by hand would have no source, would vanish
|
|
64
|
+
* on the next regeneration, and would make "this dataset rebuilds from
|
|
65
|
+
* appkit/src/seed/ alone" false. Adding one is a change to the generator.
|
|
66
|
+
* The surface exists to CORRECT a wrong extraction and to REMOVE a row that
|
|
67
|
+
* should not exist, which are the two operations the data actually needs.
|
|
68
|
+
*/
|
|
69
|
+
const COLUMNS = [
|
|
70
|
+
{
|
|
71
|
+
key: "primary",
|
|
72
|
+
header: "Beyblade",
|
|
73
|
+
sortable: true,
|
|
74
|
+
render: (row) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", children: row.secondary })] })),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "weight",
|
|
78
|
+
header: "Mass",
|
|
79
|
+
className: "w-28",
|
|
80
|
+
render: (row) => (_jsx(Text, { size: "sm", color: "muted", children: row.weight })),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
key: "edited",
|
|
84
|
+
header: "Corrected",
|
|
85
|
+
className: "w-28",
|
|
86
|
+
/*
|
|
87
|
+
* 🛑 Shown as a COLUMN, because a hand-corrected row behaves differently
|
|
88
|
+
* from its neighbours — the generator no longer owns the fields somebody
|
|
89
|
+
* fixed. Leaving that invisible is how a later maintainer concludes the
|
|
90
|
+
* generator is producing a value it is actually being prevented from
|
|
91
|
+
* writing. Text, not a colour-only dot (colour is never the only signal).
|
|
92
|
+
*/
|
|
93
|
+
render: (row) => row.edited ? (_jsx(Text, { size: "xs", weight: "medium", children: "By hand" })) : (_jsx(Text, { size: "xs", color: "faint", children: "Generated" })),
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
const ADMIN_BEYS_CONFIG = {
|
|
97
|
+
portal: "admin",
|
|
98
|
+
title: "Beyblades",
|
|
99
|
+
subtitle: "The generated Beyblade database. Corrections made here are kept when the database is regenerated.",
|
|
100
|
+
searchPlaceholder: "Search Beyblades by name — Dranzer F, Variares…",
|
|
101
|
+
/*
|
|
102
|
+
* 🛑 `searchCommit: "enter"`, not debounce. The route's search is an indexed
|
|
103
|
+
* PREFIX range, so a partial word is a different query rather than a
|
|
104
|
+
* narrowing one — debouncing would fire a range query per pause and show
|
|
105
|
+
* confusing intermediate results. Enter also makes the prefix-only behaviour
|
|
106
|
+
* legible: you typed a prefix and asked for it.
|
|
107
|
+
*/
|
|
108
|
+
searchCommit: "enter",
|
|
109
|
+
/*
|
|
110
|
+
* 🛑 DECLARED, so the placeholder can be CHECKED rather than trusted.
|
|
111
|
+
* The route matches exactly one field — an indexed prefix range on
|
|
112
|
+
* `name` — and the placeholder must not imply more. It is a prefix and
|
|
113
|
+
* it is case-sensitive; `mode` says so where a reader will see it.
|
|
114
|
+
*/
|
|
115
|
+
search: {
|
|
116
|
+
placeholder: "Search Beyblades by name — a prefix, case-sensitive",
|
|
117
|
+
fields: ["name"],
|
|
118
|
+
},
|
|
119
|
+
emptyLabel: "No Beyblade matches that name",
|
|
120
|
+
filterKeys: [],
|
|
121
|
+
/*
|
|
122
|
+
* 🛑 Through `sortBy`, not a bare "name". The route currently ALWAYS
|
|
123
|
+
* orders by name and ignores the `sorts` param, so this is the only
|
|
124
|
+
* option — but writing it as a literal is what lets a field rename
|
|
125
|
+
* leave a sort string that silently sorts nothing.
|
|
126
|
+
*/
|
|
127
|
+
defaultSort: sortBy(BEY_FIELDS.NAME, "ASC"),
|
|
128
|
+
pageSize: 25,
|
|
129
|
+
queryKey: ["admin", "beys"],
|
|
130
|
+
endpoint: ADMIN_ENDPOINTS.BEYS,
|
|
131
|
+
sortOptions: [{ value: sortBy(BEY_FIELDS.NAME, "ASC"), label: "Name A-Z" }],
|
|
132
|
+
columns: COLUMNS,
|
|
133
|
+
buildFilters: () => undefined,
|
|
134
|
+
getTotal: (response, rows) => extractTotal(response) ?? rows.length,
|
|
135
|
+
mapRows: (response) => extractRows(response).map((raw) => {
|
|
136
|
+
const r = raw;
|
|
137
|
+
/*
|
|
138
|
+
* 🛑 `≈` when the mass was SUMMED from the parts rather than stated by
|
|
139
|
+
* the source. 93% of beys have a mass but only 248 state it, and a
|
|
140
|
+
* derived figure shown as an exact one is the easiest way for this
|
|
141
|
+
* dataset to start lying — the same marking the detail page and the OG
|
|
142
|
+
* card use.
|
|
143
|
+
*/
|
|
144
|
+
const summed = typeof r.weightGFromParts === "number" ? r.weightGFromParts : null;
|
|
145
|
+
const weight = typeof r.weightG === "number"
|
|
146
|
+
? `${r.weightG} g`
|
|
147
|
+
: summed !== null
|
|
148
|
+
? `≈ ${Math.round(summed * 10) / 10} g`
|
|
149
|
+
: "—";
|
|
150
|
+
return {
|
|
151
|
+
id: String(r.id ?? ""),
|
|
152
|
+
primary: String(r.name ?? r.id ?? "Unnamed Beyblade"),
|
|
153
|
+
/*
|
|
154
|
+
* System first: it is the one line a collector uses to place a bey in
|
|
155
|
+
* time, and it is present on 94% of them. The product code follows
|
|
156
|
+
* because two vendor releases of one model differ by little else.
|
|
157
|
+
*/
|
|
158
|
+
secondary: [r.system, Array.isArray(r.productCodes) ? r.productCodes[0] : null]
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
.join(" · ") || "—",
|
|
161
|
+
weight,
|
|
162
|
+
edited: r.adminEdited === true,
|
|
163
|
+
};
|
|
164
|
+
}),
|
|
165
|
+
/*
|
|
166
|
+
* 🛑 `rowHrefTemplate`, not `rowHref`. Verified against the interface
|
|
167
|
+
* rather than guessed — there is no `rowHref` field, and an unknown key on
|
|
168
|
+
* an object literal typed by an interface is a compile error I could not
|
|
169
|
+
* have seen under this session's no-build cap.
|
|
170
|
+
*/
|
|
171
|
+
rowHrefTemplate: (row) => String(ROUTES.ADMIN.BEYS_EDIT(row.id)),
|
|
172
|
+
};
|
|
173
|
+
export function AdminBeysView({ children, ...props }) {
|
|
174
|
+
if (React.Children.count(children) > 0) {
|
|
175
|
+
return (_jsxs(ListingLayout, { portal: "admin", ...props, children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Beyblades" }), children] }));
|
|
176
|
+
}
|
|
177
|
+
return (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Beyblades" }), _jsx(DataListingView, { config: ADMIN_BEYS_CONFIG })] }));
|
|
178
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdminPartsView — browse and correct the generated part glossary.
|
|
3
|
+
*
|
|
4
|
+
* ── 🛑 WHY THIS IS A CORRECTION SURFACE, NOT A CRUD SCREEN ────────────────
|
|
5
|
+
*
|
|
6
|
+
* `partGlossary` is 2,488 rows built by committed generators from a crawled
|
|
7
|
+
* corpus. So there is no "Add part" button: a row authored here would have no
|
|
8
|
+
* source, would vanish on the next regeneration, and would make the claim
|
|
9
|
+
* "this dataset rebuilds from `appkit/src/seed/` alone" false. Adding a part is
|
|
10
|
+
* a change to the generator.
|
|
11
|
+
*
|
|
12
|
+
* What this surface IS for is the thing the corpus demonstrably needs — fixing
|
|
13
|
+
* a wrong extraction. On mode data alone the source was wrong five times in one
|
|
14
|
+
* session: an inverted tip transition, a mechanism attributed to the wrong
|
|
15
|
+
* part, a part with no modes at all, and two misexpanded abbreviations. A
|
|
16
|
+
* correction made here survives the next reseed, field by field
|
|
17
|
+
* (`adminEditedFields`).
|
|
18
|
+
*
|
|
19
|
+
* @tag domain:parts
|
|
20
|
+
* @tag layer:feature
|
|
21
|
+
* @tag access:client
|
|
22
|
+
* @tag consumers:app/[locale]/admin/parts/page.tsx
|
|
23
|
+
*/
|
|
24
|
+
import React from "react";
|
|
25
|
+
import type { ListingLayoutProps } from "../../../ui";
|
|
26
|
+
export type AdminPartsViewProps = ListingLayoutProps;
|
|
27
|
+
export declare function AdminPartsView({ children, ...props }: AdminPartsViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* AdminPartsView — browse and correct the generated part glossary.
|
|
5
|
+
*
|
|
6
|
+
* ── 🛑 WHY THIS IS A CORRECTION SURFACE, NOT A CRUD SCREEN ────────────────
|
|
7
|
+
*
|
|
8
|
+
* `partGlossary` is 2,488 rows built by committed generators from a crawled
|
|
9
|
+
* corpus. So there is no "Add part" button: a row authored here would have no
|
|
10
|
+
* source, would vanish on the next regeneration, and would make the claim
|
|
11
|
+
* "this dataset rebuilds from `appkit/src/seed/` alone" false. Adding a part is
|
|
12
|
+
* a change to the generator.
|
|
13
|
+
*
|
|
14
|
+
* What this surface IS for is the thing the corpus demonstrably needs — fixing
|
|
15
|
+
* a wrong extraction. On mode data alone the source was wrong five times in one
|
|
16
|
+
* session: an inverted tip transition, a mechanism attributed to the wrong
|
|
17
|
+
* part, a part with no modes at all, and two misexpanded abbreviations. A
|
|
18
|
+
* correction made here survives the next reseed, field by field
|
|
19
|
+
* (`adminEditedFields`).
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:parts
|
|
22
|
+
* @tag layer:feature
|
|
23
|
+
* @tag access:client
|
|
24
|
+
* @tag consumers:app/[locale]/admin/parts/page.tsx
|
|
25
|
+
*/
|
|
26
|
+
import React from "react";
|
|
27
|
+
import { Heading, ListingLayout, Stack, Text } from "../../../ui/index.js";
|
|
28
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
29
|
+
import { PART_GLOSSARY_FIELDS } from "../../../constants/field-names.js";
|
|
30
|
+
import { sortBy } from "../../../constants/sort.js";
|
|
31
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
32
|
+
import { DataListingView } from "./DataListingView.js";
|
|
33
|
+
/**
|
|
34
|
+
* 🛑 Rows are read from WHICHEVER envelope shape arrives.
|
|
35
|
+
*
|
|
36
|
+
* `apiClient.get()` unwraps `{success, data}` and returns `data` itself, so an
|
|
37
|
+
* endpoint that puts rows directly in `data` hands `mapRows` an ARRAY while one
|
|
38
|
+
* that nests them hands it an object. Reading `.data` off an array yields
|
|
39
|
+
* undefined — which is exactly how the categories listing rendered "No
|
|
40
|
+
* categories found" against a 200 carrying 58 rows. This route returns the
|
|
41
|
+
* object shape, and tolerating both is what keeps that from mattering.
|
|
42
|
+
*/
|
|
43
|
+
function extractRows(response) {
|
|
44
|
+
if (Array.isArray(response))
|
|
45
|
+
return response;
|
|
46
|
+
if (!response || typeof response !== "object")
|
|
47
|
+
return [];
|
|
48
|
+
const obj = response;
|
|
49
|
+
if (Array.isArray(obj.data))
|
|
50
|
+
return obj.data;
|
|
51
|
+
return Array.isArray(obj.items) ? obj.items : [];
|
|
52
|
+
}
|
|
53
|
+
function extractTotal(response) {
|
|
54
|
+
if (Array.isArray(response) || !response || typeof response !== "object")
|
|
55
|
+
return undefined;
|
|
56
|
+
const total = response.total;
|
|
57
|
+
return typeof total === "number" ? total : undefined;
|
|
58
|
+
}
|
|
59
|
+
/*
|
|
60
|
+
* audit-create-affordance-ok: this listing is deliberately inbound-only.
|
|
61
|
+
* The 2,488 rows are produced by committed generators from a crawled
|
|
62
|
+
* corpus, so there is no POST endpoint and no create route key to point a
|
|
63
|
+
* button at — a part authored by hand would have no source, would vanish
|
|
64
|
+
* on the next regeneration, and would make "this dataset rebuilds from
|
|
65
|
+
* appkit/src/seed/ alone" false. Adding one is a change to the generator.
|
|
66
|
+
* The surface exists to CORRECT a wrong extraction and to REMOVE a row that
|
|
67
|
+
* should not exist, which are the two operations the data actually needs.
|
|
68
|
+
*/
|
|
69
|
+
const COLUMNS = [
|
|
70
|
+
{
|
|
71
|
+
key: "primary",
|
|
72
|
+
header: "Part",
|
|
73
|
+
sortable: true,
|
|
74
|
+
render: (row) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", children: row.secondary })] })),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "weight",
|
|
78
|
+
header: "Weight",
|
|
79
|
+
className: "w-24",
|
|
80
|
+
render: (row) => (_jsx(Text, { size: "sm", color: "muted", children: row.weight })),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
key: "edited",
|
|
84
|
+
header: "Corrected",
|
|
85
|
+
className: "w-28",
|
|
86
|
+
/*
|
|
87
|
+
* 🛑 Shown as a COLUMN, because a hand-corrected row behaves differently
|
|
88
|
+
* from its neighbours — the generator no longer owns the fields somebody
|
|
89
|
+
* fixed. Leaving that invisible is how a later maintainer concludes the
|
|
90
|
+
* generator is producing a value it is actually being prevented from
|
|
91
|
+
* writing. Text, not a colour-only dot (colour is never the only signal).
|
|
92
|
+
*/
|
|
93
|
+
render: (row) => row.edited ? (_jsx(Text, { size: "xs", weight: "medium", children: "By hand" })) : (_jsx(Text, { size: "xs", color: "faint", children: "Generated" })),
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
const ADMIN_PARTS_CONFIG = {
|
|
97
|
+
portal: "admin",
|
|
98
|
+
title: "Parts",
|
|
99
|
+
subtitle: "The generated part glossary. Corrections made here are kept when the database is regenerated.",
|
|
100
|
+
searchPlaceholder: "Search parts by name — Rubber Flat, Dranzer…",
|
|
101
|
+
/*
|
|
102
|
+
* 🛑 `searchCommit: "enter"`, not debounce. The route's search is an indexed
|
|
103
|
+
* PREFIX range, so a partial word is a different query rather than a
|
|
104
|
+
* narrowing one — debouncing would fire a range query per pause and show
|
|
105
|
+
* confusing intermediate results. Enter also makes the prefix-only behaviour
|
|
106
|
+
* legible: you typed a prefix and asked for it.
|
|
107
|
+
*/
|
|
108
|
+
searchCommit: "enter",
|
|
109
|
+
/*
|
|
110
|
+
* 🛑 DECLARED, so the placeholder can be CHECKED rather than trusted.
|
|
111
|
+
* The route matches exactly one field — an indexed prefix range on
|
|
112
|
+
* `name` — and the placeholder must not imply more. It is a prefix and
|
|
113
|
+
* it is case-sensitive; `mode` says so where a reader will see it.
|
|
114
|
+
*/
|
|
115
|
+
search: {
|
|
116
|
+
placeholder: "Search parts by name — a prefix, case-sensitive",
|
|
117
|
+
fields: ["name"],
|
|
118
|
+
},
|
|
119
|
+
emptyLabel: "No part matches that name",
|
|
120
|
+
filterKeys: [],
|
|
121
|
+
/*
|
|
122
|
+
* 🛑 Through `sortBy`, not a bare "name". The route currently ALWAYS
|
|
123
|
+
* orders by name and ignores the `sorts` param, so this is the only
|
|
124
|
+
* option — but writing it as a literal is what lets a field rename
|
|
125
|
+
* leave a sort string that silently sorts nothing.
|
|
126
|
+
*/
|
|
127
|
+
defaultSort: sortBy(PART_GLOSSARY_FIELDS.NAME, "ASC"),
|
|
128
|
+
pageSize: 25,
|
|
129
|
+
queryKey: ["admin", "parts"],
|
|
130
|
+
endpoint: ADMIN_ENDPOINTS.PARTS,
|
|
131
|
+
sortOptions: [{ value: sortBy(PART_GLOSSARY_FIELDS.NAME, "ASC"), label: "Name A-Z" }],
|
|
132
|
+
columns: COLUMNS,
|
|
133
|
+
buildFilters: () => undefined,
|
|
134
|
+
getTotal: (response, rows) => extractTotal(response) ?? rows.length,
|
|
135
|
+
mapRows: (response) => extractRows(response).map((raw) => {
|
|
136
|
+
const r = raw;
|
|
137
|
+
const weight = typeof r.weightG === "number" ? `${r.weightG} g` : "—";
|
|
138
|
+
return {
|
|
139
|
+
id: String(r.id ?? ""),
|
|
140
|
+
primary: String(r.name ?? r.id ?? "Unnamed part"),
|
|
141
|
+
/* The classification is what disambiguates a bare code like "105". */
|
|
142
|
+
secondary: [r.classification, r.code].filter(Boolean).join(" · ") || "—",
|
|
143
|
+
weight,
|
|
144
|
+
edited: r.adminEdited === true,
|
|
145
|
+
};
|
|
146
|
+
}),
|
|
147
|
+
/*
|
|
148
|
+
* 🛑 `rowHrefTemplate`, not `rowHref`. Verified against the interface
|
|
149
|
+
* rather than guessed — there is no `rowHref` field, and an unknown key on
|
|
150
|
+
* an object literal typed by an interface is a compile error I could not
|
|
151
|
+
* have seen under this session's no-build cap.
|
|
152
|
+
*/
|
|
153
|
+
rowHrefTemplate: (row) => String(ROUTES.ADMIN.PARTS_EDIT(row.id)),
|
|
154
|
+
};
|
|
155
|
+
export function AdminPartsView({ children, ...props }) {
|
|
156
|
+
if (React.Children.count(children) > 0) {
|
|
157
|
+
return (_jsxs(ListingLayout, { portal: "admin", ...props, children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Parts" }), children] }));
|
|
158
|
+
}
|
|
159
|
+
return (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Parts" }), _jsx(DataListingView, { config: ADMIN_PARTS_CONFIG })] }));
|
|
160
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Which registry bucket supplies the labels and the confirm copy. */
|
|
2
|
-
export type OwnerQuickActionsResource = "PRODUCT" | "CATEGORY" | "BRAND" | "FRANCHISE" | "BUNDLE" | "GROUP" | "SUBLISTING" | "EVENT" | "BLOG" | "REVIEW" | "STORE" | "USER" | "CAROUSEL" | "SECTION";
|
|
2
|
+
export type OwnerQuickActionsResource = "PRODUCT" | "CATEGORY" | "BRAND" | "FRANCHISE" | "PART" | "BEY" | "BUNDLE" | "GROUP" | "SUBLISTING" | "EVENT" | "BLOG" | "REVIEW" | "STORE" | "USER" | "CAROUSEL" | "SECTION";
|
|
3
3
|
export interface OwnerQuickActionsProps {
|
|
4
4
|
resource: OwnerQuickActionsResource;
|
|
5
5
|
/** Where the pencil goes — that record's dashboard editor. */
|
|
@@ -24,6 +24,23 @@ export interface OwnerQuickActionsProps {
|
|
|
24
24
|
* `detail` sits in a header row beside the title.
|
|
25
25
|
*/
|
|
26
26
|
placement?: "card" | "detail";
|
|
27
|
+
/**
|
|
28
|
+
* 🛑 Set this whenever `placement="detail"` sits on a DARK cover hero.
|
|
29
|
+
*
|
|
30
|
+
* `detail` otherwise renders the `ghost` variant, which is
|
|
31
|
+
* `color: var(--appkit-color-zinc-600)` — a dark grey that is correct on a
|
|
32
|
+
* page-surface header row and invisible on a cover photo. Every detail view
|
|
33
|
+
* that has a hero already computes a `hasCover` flag and flips each of its
|
|
34
|
+
* siblings to `color="inverse"`; this toolbar was the one element in that
|
|
35
|
+
* block that did not, so the pencil rendered near-black on near-black and
|
|
36
|
+
* read as "admins get no edit button" (Root Cause #67 — a theme-relative ink
|
|
37
|
+
* on a theme-invariant dark surface).
|
|
38
|
+
*
|
|
39
|
+
* Pass the SAME flag the rest of the hero branches on, not a literal `true`:
|
|
40
|
+
* a coverless hero falls back to the page surface, where the scrim's white
|
|
41
|
+
* ink would be invisible in the mirror-image way.
|
|
42
|
+
*/
|
|
43
|
+
onMedia?: boolean;
|
|
27
44
|
className?: string;
|
|
28
45
|
}
|
|
29
|
-
export declare function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement, className, }: OwnerQuickActionsProps): import("react").JSX.Element | null;
|
|
46
|
+
export declare function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement, onMedia, className, }: OwnerQuickActionsProps): import("react").JSX.Element | null;
|
|
@@ -57,6 +57,15 @@ import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
|
57
57
|
const RESOURCE_LEAVES = {
|
|
58
58
|
PRODUCT: { bucket: "PRODUCT", edit: "edit", remove: "delete", duplicate: "duplicate" },
|
|
59
59
|
CATEGORY: { bucket: "CATEGORY", edit: "edit", remove: "delete" },
|
|
60
|
+
/*
|
|
61
|
+
* Both get a bin. Unlike the three pencil-only resources below, the blast
|
|
62
|
+
* radius of removing one reference row is SMALL and bounded: a bey keeps
|
|
63
|
+
* its parts list (`partName` sits beside `partId` for exactly this), a
|
|
64
|
+
* boxed set keeps its contents, and a category page keeps its listings.
|
|
65
|
+
* Nothing cascades, and the committed seed can put the row back.
|
|
66
|
+
*/
|
|
67
|
+
PART: { bucket: "PART", edit: "edit", remove: "delete" },
|
|
68
|
+
BEY: { bucket: "BEY", edit: "edit", remove: "delete" },
|
|
60
69
|
BRAND: { bucket: "BRAND", edit: "edit", remove: "delete" },
|
|
61
70
|
BUNDLE: { bucket: "BUNDLE", edit: "edit", remove: "delete" },
|
|
62
71
|
GROUP: { bucket: "GROUP", edit: "edit", remove: "delete" },
|
|
@@ -93,7 +102,7 @@ const RESOURCE_LEAVES = {
|
|
|
93
102
|
USER: { bucket: "ADMIN", edit: "manage-user" },
|
|
94
103
|
SECTION: { bucket: "ADMIN", edit: "edit-section" },
|
|
95
104
|
};
|
|
96
|
-
export function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement = "card", className = "", }) {
|
|
105
|
+
export function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement = "card", onMedia = false, className = "", }) {
|
|
97
106
|
const session = useOptionalSession();
|
|
98
107
|
const isOwnerOrAdmin = useIsOwner(ownerId);
|
|
99
108
|
const isAdmin = useIsAdmin();
|
|
@@ -164,6 +173,25 @@ export function OwnerQuickActions({ resource, editHref, onDelete, onClone, owner
|
|
|
164
173
|
* resource, not to each of its call sites, or the next call site re-opens it.
|
|
165
174
|
*/
|
|
166
175
|
const showDelete = Boolean(onDelete) && Boolean(leaves.remove);
|
|
176
|
+
/*
|
|
177
|
+
* 🛑 The scrim is for ANY dark backdrop, not only a card.
|
|
178
|
+
*
|
|
179
|
+
* `detail` used to hard-map to `ghost` (`color: var(--appkit-color-zinc-600)`),
|
|
180
|
+
* which is right on a page-surface header row and invisible on a cover photo
|
|
181
|
+
* — and every detail view with a hero puts this toolbar inside that hero,
|
|
182
|
+
* beside siblings it has already flipped to `color="inverse"`. The pencil
|
|
183
|
+
* therefore rendered near-black on near-black and read as "admins get no
|
|
184
|
+
* edit button at all". See `onMedia` above and Root Cause #67.
|
|
185
|
+
*/
|
|
186
|
+
const onDark = placement === "card" || onMedia;
|
|
187
|
+
const neutralVariant = onDark ? "scrim" : "ghost";
|
|
188
|
+
/*
|
|
189
|
+
* Danger ink is dropped on a dark backdrop for the same reason: `danger`
|
|
190
|
+
* resolves to a theme-relative red that disappears against the hero. The bin
|
|
191
|
+
* keeps its confirm dialog, which is where the destructive intent is
|
|
192
|
+
* actually communicated — colour was never the safeguard.
|
|
193
|
+
*/
|
|
194
|
+
const dangerVariant = onDark ? "scrim" : "danger";
|
|
167
195
|
return (_jsxs(_Fragment, { children: [_jsxs(Row, { gap: "xs", align: "center", className: className,
|
|
168
196
|
/*
|
|
169
197
|
* Stops the click reaching an ancestor `<Link>` or the card's own
|
|
@@ -172,7 +200,7 @@ export function OwnerQuickActions({ resource, editHref, onDelete, onClone, owner
|
|
|
172
200
|
*/
|
|
173
201
|
onClick: (e) => {
|
|
174
202
|
e.stopPropagation();
|
|
175
|
-
}, children: [_jsx(IconButton, { "aria-label": editDef?.ariaLabel ?? editDef?.label ?? "Edit", size: "touch", variant:
|
|
203
|
+
}, children: [_jsx(IconButton, { "aria-label": editDef?.ariaLabel ?? editDef?.label ?? "Edit", size: "touch", variant: neutralVariant, icon: _jsx(Icon, { name: "edit" }), onClick: () => {
|
|
176
204
|
/*
|
|
177
205
|
* `window.location`, not `next/link`. This component is rendered
|
|
178
206
|
* inside cards whose whole body is already an anchor, and nesting
|
|
@@ -183,7 +211,7 @@ export function OwnerQuickActions({ resource, editHref, onDelete, onClone, owner
|
|
|
183
211
|
if (typeof window !== "undefined")
|
|
184
212
|
window.location.assign(editHref);
|
|
185
213
|
} }), showClone && (_jsx(IconButton, { "aria-label": (leaves.duplicate ? bucket[leaves.duplicate]?.ariaLabel ?? bucket[leaves.duplicate]?.label : undefined) ||
|
|
186
|
-
"Duplicate", size: "touch", variant:
|
|
214
|
+
"Duplicate", size: "touch", variant: neutralVariant, icon: _jsx(Icon, { name: "duplicate" }), onClick: () => void onClone?.() })), showDelete && (_jsx(IconButton, { "aria-label": deleteDef?.ariaLabel ?? deleteDef?.label ?? "Delete", size: "touch", variant: dangerVariant, icon: _jsx(Icon, { name: "delete" }), onClick: () => setConfirmOpen(true) }))] }), showDelete && (_jsx(ConfirmDeleteModal, { isOpen: confirmOpen, onClose: () => setConfirmOpen(false), onConfirm: handleConfirmedDelete, isDeleting: deleting, title: confirmation?.title ?? "Delete this record?", message: recordLabel && confirmation
|
|
187
215
|
? `${confirmation.body}\n\nDeleting: ${recordLabel}`
|
|
188
216
|
: confirmation?.body, confirmText: confirmation?.confirmLabel ?? deleteDef?.label ?? "Delete", variant: "danger" }))] }));
|
|
189
217
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The admin-owned resources this wrapper can delete.
|
|
3
|
+
*
|
|
4
|
+
* 🛑 Deliberately NOT every `OwnerQuickActionsResource`. `STORE`, `USER` and
|
|
5
|
+
* `SECTION` omit a `remove` leaf in `RESOURCE_LEAVES` because their blast
|
|
6
|
+
* radius is far wider than the page a bin would sit on, and `REVIEW` has no
|
|
7
|
+
* admin edit route to send the pencil to. Adding one here without adding its
|
|
8
|
+
* leaf there still renders no bin — the resource owns that decision, which is
|
|
9
|
+
* the point of gating it in two places.
|
|
10
|
+
*/
|
|
11
|
+
export type RecordQuickActionResource = "CATEGORY" | "BRAND" | "BUNDLE" | "GROUP" | "EVENT" | "BLOG" | "FRANCHISE" | "SUBLISTING" | "PART" | "BEY";
|
|
12
|
+
export interface RecordOwnerQuickActionsProps {
|
|
13
|
+
resource: RecordQuickActionResource;
|
|
14
|
+
/** The record's document id — what the endpoint and the editor are keyed on. */
|
|
15
|
+
recordId: string;
|
|
16
|
+
/** Names the record in the delete confirmation. */
|
|
17
|
+
recordLabel?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Forwarded to `OwnerQuickActions` so a store-owned record (a grouped
|
|
20
|
+
* listing, a store-authored bundle) stays visible to the seller who owns it
|
|
21
|
+
* and not only to an admin. `SessionUser.storeId` holds the store SLUG, not
|
|
22
|
+
* the Auth UID — two different id spaces, hence two props.
|
|
23
|
+
*/
|
|
24
|
+
ownerId?: string;
|
|
25
|
+
storeId?: string;
|
|
26
|
+
placement?: "card" | "detail";
|
|
27
|
+
/** Pass the hero's own `hasCover` flag. See `OwnerQuickActions.onMedia`. */
|
|
28
|
+
onMedia?: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function RecordOwnerQuickActions({ resource, recordId, recordLabel, ownerId, storeId, placement, onMedia, className, }: RecordOwnerQuickActionsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The client half of `OwnerQuickActions` for every admin-owned collection.
|
|
5
|
+
*
|
|
6
|
+
* ## 🛑 Why a wrapper is REQUIRED, not a convenience
|
|
7
|
+
*
|
|
8
|
+
* `OwnerQuickActions` takes `onDelete` / `onClone` as FUNCTIONS, and every
|
|
9
|
+
* detail view that renders it (`CategoryDetailPageView`, `BrandDetailPageView`,
|
|
10
|
+
* `BundleDetailView`, …) is a Server Component. A closure cannot cross that
|
|
11
|
+
* boundary — React throws
|
|
12
|
+
*
|
|
13
|
+
* Event handlers cannot be passed to Client Component props.
|
|
14
|
+
*
|
|
15
|
+
* at render. `ProductOwnerQuickActions` records that this exact mistake took
|
|
16
|
+
* down every `/products/[slug]` page in production, with an opaque digest per
|
|
17
|
+
* handler.
|
|
18
|
+
*
|
|
19
|
+
* That is why ten detail views were passing `editHref` and nothing else: the
|
|
20
|
+
* bin and the clone are rendered behind `{showDelete && …}` / `{showClone && …}`,
|
|
21
|
+
* so with no handler they silently do not exist. It read as "admins get no
|
|
22
|
+
* delete button", and the cause was a serialization boundary, not a permission.
|
|
23
|
+
*
|
|
24
|
+
* ## 🛑 Why ONE wrapper and not one per resource
|
|
25
|
+
*
|
|
26
|
+
* `ProductOwnerQuickActions` is resource-specific because products are the one
|
|
27
|
+
* collection a SELLER can also own, so its endpoints live under
|
|
28
|
+
* `SELLER_ENDPOINTS` and its clone has a bespoke redirect to the copy's editor.
|
|
29
|
+
* Everything here is admin-owned and differs only by three strings, so eight
|
|
30
|
+
* near-identical files would trip the Duplication Framework's Rule of Three
|
|
31
|
+
* immediately. One table, one component.
|
|
32
|
+
*
|
|
33
|
+
* ## Why the redirect lands on the ADMIN list
|
|
34
|
+
*
|
|
35
|
+
* You deleted the record from its own public page, so that page is now a 404 —
|
|
36
|
+
* staying is not an option and the public index tells you nothing about whether
|
|
37
|
+
* the delete worked. The admin list is where the absence is verifiable.
|
|
38
|
+
* `ProductOwnerQuickActions` differs deliberately: a seller may delete a
|
|
39
|
+
* product and has no admin list to land on.
|
|
40
|
+
*
|
|
41
|
+
* @tag domain:admin
|
|
42
|
+
* @tag layer:components
|
|
43
|
+
* @tag access:client
|
|
44
|
+
*/
|
|
45
|
+
import { useCallback } from "react";
|
|
46
|
+
import { OwnerQuickActions } from "./OwnerQuickActions.js";
|
|
47
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
48
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
49
|
+
const WIRING = {
|
|
50
|
+
CATEGORY: {
|
|
51
|
+
endpoint: ADMIN_ENDPOINTS.CATEGORY_BY_ID,
|
|
52
|
+
editHref: (id) => String(ROUTES.ADMIN.CATEGORIES_EDIT(id)),
|
|
53
|
+
afterDelete: String(ROUTES.ADMIN.CATEGORIES),
|
|
54
|
+
},
|
|
55
|
+
PART: {
|
|
56
|
+
endpoint: ADMIN_ENDPOINTS.PART_BY_ID,
|
|
57
|
+
editHref: (id) => String(ROUTES.ADMIN.PARTS_EDIT(id)),
|
|
58
|
+
afterDelete: String(ROUTES.ADMIN.PARTS),
|
|
59
|
+
},
|
|
60
|
+
BEY: {
|
|
61
|
+
endpoint: ADMIN_ENDPOINTS.BEY_BY_ID,
|
|
62
|
+
editHref: (id) => String(ROUTES.ADMIN.BEYS_EDIT(id)),
|
|
63
|
+
afterDelete: String(ROUTES.ADMIN.BEYS),
|
|
64
|
+
},
|
|
65
|
+
BRAND: {
|
|
66
|
+
endpoint: ADMIN_ENDPOINTS.BRAND_BY_ID,
|
|
67
|
+
editHref: (id) => String(ROUTES.ADMIN.BRANDS_EDIT(id)),
|
|
68
|
+
afterDelete: String(ROUTES.ADMIN.BRANDS),
|
|
69
|
+
},
|
|
70
|
+
BUNDLE: {
|
|
71
|
+
endpoint: ADMIN_ENDPOINTS.BUNDLE_BY_ID,
|
|
72
|
+
editHref: (id) => String(ROUTES.ADMIN.BUNDLES_EDIT(id)),
|
|
73
|
+
afterDelete: String(ROUTES.ADMIN.BUNDLES),
|
|
74
|
+
},
|
|
75
|
+
GROUP: {
|
|
76
|
+
endpoint: ADMIN_ENDPOINTS.GROUPED_LISTING_BY_ID,
|
|
77
|
+
editHref: (id) => String(ROUTES.ADMIN.GROUPED_LISTINGS_EDIT(id)),
|
|
78
|
+
afterDelete: String(ROUTES.ADMIN.GROUPED_LISTINGS),
|
|
79
|
+
},
|
|
80
|
+
EVENT: {
|
|
81
|
+
endpoint: ADMIN_ENDPOINTS.EVENT_BY_ID,
|
|
82
|
+
editHref: (id) => String(ROUTES.ADMIN.EVENTS_EDIT(id)),
|
|
83
|
+
afterDelete: String(ROUTES.ADMIN.EVENTS),
|
|
84
|
+
},
|
|
85
|
+
BLOG: {
|
|
86
|
+
endpoint: ADMIN_ENDPOINTS.BLOG_BY_ID,
|
|
87
|
+
editHref: (id) => String(ROUTES.ADMIN.BLOG_EDIT(id)),
|
|
88
|
+
afterDelete: String(ROUTES.ADMIN.BLOG),
|
|
89
|
+
},
|
|
90
|
+
/*
|
|
91
|
+
* A franchise row IS a category (`categoryType: "franchise"`), so it shares
|
|
92
|
+
* the category endpoint and editor — the same reasoning `RESOURCE_LEAVES`
|
|
93
|
+
* uses to give it the category copy. Only the landing page differs.
|
|
94
|
+
*/
|
|
95
|
+
FRANCHISE: {
|
|
96
|
+
endpoint: ADMIN_ENDPOINTS.CATEGORY_BY_ID,
|
|
97
|
+
editHref: (id) => String(ROUTES.ADMIN.CATEGORIES_EDIT(id)),
|
|
98
|
+
afterDelete: String(ROUTES.ADMIN.CATEGORIES),
|
|
99
|
+
},
|
|
100
|
+
SUBLISTING: {
|
|
101
|
+
endpoint: ADMIN_ENDPOINTS.SUBLISTING_CATEGORY_BY_ID,
|
|
102
|
+
editHref: (id) => String(ROUTES.ADMIN.SUBLISTING_CATEGORIES_EDIT(id)),
|
|
103
|
+
afterDelete: String(ROUTES.ADMIN.SUBLISTING_CATEGORIES),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
export function RecordOwnerQuickActions({ resource, recordId, recordLabel, ownerId, storeId, placement = "detail", onMedia, className, }) {
|
|
107
|
+
const wiring = WIRING[resource];
|
|
108
|
+
/*
|
|
109
|
+
* 🛑 THROWS on a failed DELETE rather than returning quietly.
|
|
110
|
+
*
|
|
111
|
+
* `OwnerQuickActions.handleConfirmedDelete` catches and toasts, and success
|
|
112
|
+
* navigates away — so a swallowed failure is indistinguishable from "it
|
|
113
|
+
* worked but the page did not move", and the obvious user response is to
|
|
114
|
+
* press the button again. Returning early on `!res.ok` is the bug
|
|
115
|
+
* `ProductOwnerQuickActions` documents having had.
|
|
116
|
+
*/
|
|
117
|
+
const handleDelete = useCallback(async () => {
|
|
118
|
+
const res = await fetch(wiring.endpoint(recordId), { method: "DELETE" });
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
const body = (await res.json().catch(() => null));
|
|
121
|
+
throw Object.assign(new Error("Delete failed"), { code: body?.code });
|
|
122
|
+
}
|
|
123
|
+
if (typeof window !== "undefined") {
|
|
124
|
+
window.location.assign(wiring.afterDelete);
|
|
125
|
+
}
|
|
126
|
+
}, [wiring, recordId]);
|
|
127
|
+
return (_jsx(OwnerQuickActions, { resource: resource, placement: placement, onMedia: onMedia, className: className, recordLabel: recordLabel, ownerId: ownerId, storeId: storeId, editHref: wiring.editHref(recordId), onDelete: handleDelete }));
|
|
128
|
+
}
|