@spree/dashboard-core 0.13.1 → 1.0.0-beta.2
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/README.md +4 -0
- package/dist/vite/index.js +17 -3
- package/package.json +7 -7
- package/src/api-client.ts +643 -0
- package/src/components/address-form-dialog.test.ts +25 -0
- package/src/components/address-form-dialog.tsx +203 -79
- package/src/components/app-shell.tsx +171 -0
- package/src/components/app-sidebar.tsx +184 -27
- package/src/components/bulk-action-bar.tsx +89 -157
- package/src/components/country-combobox.tsx +69 -34
- package/src/components/country-state-fields.tsx +43 -11
- package/src/components/currency-select.tsx +81 -66
- package/src/components/export-button.tsx +62 -5
- package/src/components/file-upload-field.tsx +32 -10
- package/src/components/import-button.tsx +52 -27
- package/src/components/import-wizard-dialog.tsx +710 -0
- package/src/components/locale-select.tsx +70 -57
- package/src/components/media-picker-sheet.tsx +307 -0
- package/src/components/mobile-top-bar.tsx +75 -0
- package/src/components/nav-main.tsx +197 -27
- package/src/components/page-header.tsx +203 -61
- package/src/components/page-tabs.tsx +4 -1
- package/src/components/preferences-form.tsx +98 -4
- package/src/components/resource-combobox.tsx +52 -4
- package/src/components/resource-picker-sheet.tsx +209 -73
- package/src/components/resource-table.tsx +383 -127
- package/src/components/route-announcer.tsx +64 -0
- package/src/components/section-heading.tsx +95 -0
- package/src/components/settings-sidebar.tsx +290 -60
- package/src/components/sheet-top-bar.tsx +44 -0
- package/src/components/sidebar-search.tsx +83 -0
- package/src/components/sidebar-user.tsx +213 -0
- package/src/components/skip-link.tsx +36 -0
- package/src/components/store-date-picker.tsx +6 -2
- package/src/components/store-switcher.tsx +19 -14
- package/src/components/table-toolbar.tsx +1357 -440
- package/src/components/tag-combobox.tsx +12 -4
- package/src/components/top-bar-breadcrumbs.tsx +235 -0
- package/src/components/wizard-close-guard.test.ts +54 -0
- package/src/hooks/use-auto-collapse-sidebar.ts +57 -0
- package/src/hooks/use-command-palette.tsx +13 -0
- package/src/hooks/use-countries.test.ts +56 -0
- package/src/hooks/use-countries.ts +51 -5
- package/src/hooks/use-direct-upload.ts +16 -3
- package/src/hooks/use-export.ts +51 -16
- package/src/hooks/use-import.ts +241 -16
- package/src/hooks/use-package-types.ts +132 -0
- package/src/hooks/use-resource-mutation.ts +74 -13
- package/src/hooks/use-stock-locations.ts +131 -0
- package/src/hooks/use-switch-admin-locale.ts +2 -2
- package/src/hooks/use-visual-viewport-offset.ts +61 -0
- package/src/index.ts +45 -1
- package/src/lib/cancel-order-form.ts +27 -0
- package/src/lib/create-action-registry.ts +278 -0
- package/src/lib/date-presets.test.ts +86 -0
- package/src/lib/date-presets.ts +98 -0
- package/src/lib/delivery-form.ts +15 -0
- package/src/lib/docs.ts +32 -0
- package/src/lib/download.ts +13 -1
- package/src/lib/filters-to-ransack.test.ts +113 -0
- package/src/lib/filters-to-ransack.ts +22 -1
- package/src/lib/form-errors.ts +122 -10
- package/src/lib/form-mappers.ts +76 -0
- package/src/lib/fulfillment-items.test.ts +213 -0
- package/src/lib/fulfillment-items.ts +171 -0
- package/src/lib/i18n.ts +19 -11
- package/src/lib/import-types.ts +25 -0
- package/src/lib/label-upload-form.ts +22 -0
- package/src/lib/nav-registry.ts +71 -3
- package/src/lib/permissions.ts +46 -1
- package/src/lib/progress.ts +18 -0
- package/src/lib/query-keys.ts +13 -13
- package/src/lib/sample-csv.ts +9 -6
- package/src/lib/search-registry.ts +6 -2
- package/src/lib/settings-nav-registry.ts +104 -11
- package/src/lib/table-registry.ts +50 -0
- package/src/lib/type-labels.test.ts +68 -0
- package/src/lib/type-labels.ts +78 -0
- package/src/locales/ar.json +1889 -517
- package/src/locales/de.json +1884 -517
- package/src/locales/en.json +1886 -519
- package/src/locales/fr.json +1884 -517
- package/src/locales/pl.json +1913 -517
- package/src/locales/zh-CN.json +1883 -517
- package/src/pages/package-types.tsx +484 -0
- package/src/pages/settings-index.tsx +112 -0
- package/src/pages/stock-locations.tsx +674 -0
- package/src/plugin.ts +29 -0
- package/src/products/add-video-dialog.tsx +100 -0
- package/src/products/bulk-variants-dialog.tsx +301 -0
- package/src/products/carton-size.test.ts +51 -0
- package/src/products/carton-size.ts +20 -0
- package/src/products/currency-parts.ts +21 -0
- package/src/products/inventory-section.tsx +259 -0
- package/src/products/media-edit-sheet.tsx +336 -0
- package/src/products/normalize-customs.test.ts +39 -0
- package/src/products/normalize-customs.ts +27 -0
- package/src/products/normalize-money.test.ts +58 -0
- package/src/products/normalize-money.ts +73 -0
- package/src/products/normalize-quantity.test.ts +32 -0
- package/src/products/normalize-quantity.ts +31 -0
- package/src/products/product-bulk-price-editor.tsx +165 -0
- package/src/products/product-form-cards.tsx +1167 -0
- package/src/products/product-form-mapping.ts +212 -0
- package/src/products/product-schema.ts +318 -0
- package/src/products/product-types.ts +130 -0
- package/src/products/use-product-form-data.ts +273 -0
- package/src/products/variant-edit-sheet.tsx +725 -0
- package/src/products/variants-matrix.test.ts +154 -0
- package/src/products/variants-matrix.ts +244 -0
- package/src/products/variants-options-builder.tsx +571 -0
- package/src/products/variants-section.tsx +535 -0
- package/src/products/video-url.test.ts +71 -0
- package/src/products/video-url.ts +83 -0
- package/src/providers/auth-provider.tsx +70 -12
- package/src/providers/permission-provider.tsx +68 -28
- package/src/providers/sticky-header-provider.tsx +134 -0
- package/src/providers/store-provider.tsx +36 -3
- package/src/providers/tenant-provider.tsx +33 -0
- package/src/schemas/package-type.ts +95 -0
- package/src/schemas/stock-location.ts +126 -0
- package/src/tables/package-types.tsx +74 -0
- package/src/tables/stock-locations.tsx +124 -0
- package/src/vite/index.ts +40 -7
- package/src/components/top-bar.tsx +0 -209
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import type {
|
|
3
|
+
PanelCollection,
|
|
4
|
+
PanelDeliveryProfile,
|
|
5
|
+
PanelMarket,
|
|
6
|
+
PanelNamedRecord,
|
|
7
|
+
PanelOptionType,
|
|
8
|
+
PanelPackageType,
|
|
9
|
+
PanelProductType,
|
|
10
|
+
} from '../api-client'
|
|
11
|
+
import { getApiClient } from '../api-client'
|
|
12
|
+
import { useResourceKey } from '../lib/query-keys'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The reference data the product form's pickers read.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately separate from the operator dashboard's own `useOptionTypes`
|
|
18
|
+
* and friends, which back its management pages and carry the full CRUD each
|
|
19
|
+
* resource needs. These read through the registered panel client, so the same
|
|
20
|
+
* cards work in a seller's panel against the Seller API — and answer empty
|
|
21
|
+
* when a panel registers no such resource, which is how a card knows to hide.
|
|
22
|
+
*
|
|
23
|
+
* Keys go through `useResourceKey`, so they carry the tenant — the store on the
|
|
24
|
+
* operator's panel, the seller on a seller's. Flat keys would serve one
|
|
25
|
+
* tenant's categories to the next within the stale window, and a save from that
|
|
26
|
+
* form would write ids belonging to somebody else.
|
|
27
|
+
*
|
|
28
|
+
* They are also keyed UNDER the resource they mirror — `['package-types',
|
|
29
|
+
* tenant, 'panel-form', ...]`, not `['panel-form-package-types', tenant]` — so
|
|
30
|
+
* that the `invalidate: [['package-types']]` a management page already declares
|
|
31
|
+
* sweeps them by prefix. Keyed to one side, a carton created in settings stayed
|
|
32
|
+
* invisible to this form for the whole stale window below, and the section that
|
|
33
|
+
* only renders once a carton exists went on claiming there were none.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const FIVE_MINUTES = 1000 * 60 * 5
|
|
37
|
+
|
|
38
|
+
export function useFormOptionTypes(enabled = true) {
|
|
39
|
+
const client = getApiClient()
|
|
40
|
+
|
|
41
|
+
return useQuery({
|
|
42
|
+
queryKey: useResourceKey('option-types', 'panel-form'),
|
|
43
|
+
queryFn: async (): Promise<{ data: PanelOptionType[] }> =>
|
|
44
|
+
(await client.optionTypes?.list({ limit: 100 })) ?? { data: [] },
|
|
45
|
+
enabled: enabled && Boolean(client.optionTypes),
|
|
46
|
+
staleTime: FIVE_MINUTES,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function useFormCategories(enabled = true) {
|
|
51
|
+
const client = getApiClient()
|
|
52
|
+
|
|
53
|
+
return useQuery({
|
|
54
|
+
queryKey: useResourceKey('categories', 'panel-form'),
|
|
55
|
+
queryFn: async (): Promise<{ data: PanelNamedRecord[] }> =>
|
|
56
|
+
(await client.categories?.list({ limit: 100 })) ?? { data: [] },
|
|
57
|
+
enabled: enabled && Boolean(client.categories),
|
|
58
|
+
staleTime: FIVE_MINUTES,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function useFormCollections(enabled = true) {
|
|
63
|
+
const client = getApiClient()
|
|
64
|
+
|
|
65
|
+
return useQuery({
|
|
66
|
+
queryKey: useResourceKey('collections', 'panel-form'),
|
|
67
|
+
queryFn: async (): Promise<{ data: PanelCollection[] }> =>
|
|
68
|
+
(await client.collections?.list({ limit: 100 })) ?? { data: [] },
|
|
69
|
+
enabled: enabled && Boolean(client.collections),
|
|
70
|
+
staleTime: FIVE_MINUTES,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useFormProductTypes(enabled = true) {
|
|
75
|
+
const client = getApiClient()
|
|
76
|
+
|
|
77
|
+
return useQuery({
|
|
78
|
+
queryKey: useResourceKey('product-types', 'panel-form'),
|
|
79
|
+
queryFn: async (): Promise<{ data: PanelProductType[] }> =>
|
|
80
|
+
(await client.productTypes?.list({ limit: 100 })) ?? { data: [] },
|
|
81
|
+
enabled: enabled && Boolean(client.productTypes),
|
|
82
|
+
staleTime: FIVE_MINUTES,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One product type, for the option types and custom fields it seeds. */
|
|
87
|
+
export function useFormProductType(id?: string) {
|
|
88
|
+
const client = getApiClient()
|
|
89
|
+
|
|
90
|
+
return useQuery({
|
|
91
|
+
queryKey: useResourceKey('product-types', 'panel-form', id),
|
|
92
|
+
queryFn: () => client.productTypes?.get(id as string) as Promise<PanelProductType>,
|
|
93
|
+
enabled: Boolean(id) && Boolean(client.productTypes),
|
|
94
|
+
staleTime: FIVE_MINUTES,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Option types named by a product type, resolved from the full list rather
|
|
100
|
+
* than fetched by id — the list is already cached and small enough that a
|
|
101
|
+
* second round trip buys nothing.
|
|
102
|
+
*/
|
|
103
|
+
export function useFormOptionTypesByIds(ids?: string[]) {
|
|
104
|
+
const { data, ...rest } = useFormOptionTypes(Boolean(ids?.length))
|
|
105
|
+
const wanted = new Set(ids ?? [])
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
...rest,
|
|
109
|
+
data: ids?.length
|
|
110
|
+
? { data: (data?.data ?? []).filter((type) => wanted.has(type.id)) }
|
|
111
|
+
: undefined,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function useFormTaxCategories() {
|
|
116
|
+
const client = getApiClient()
|
|
117
|
+
|
|
118
|
+
return useQuery({
|
|
119
|
+
queryKey: useResourceKey('tax-categories', 'panel-form'),
|
|
120
|
+
queryFn: async (): Promise<{ data: PanelNamedRecord[] }> =>
|
|
121
|
+
(await client.taxCategories?.list({ limit: 100 })) ?? { data: [] },
|
|
122
|
+
enabled: Boolean(client.taxCategories),
|
|
123
|
+
staleTime: FIVE_MINUTES,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function useFormDeliveryProfiles() {
|
|
128
|
+
const client = getApiClient()
|
|
129
|
+
|
|
130
|
+
return useQuery({
|
|
131
|
+
queryKey: useResourceKey('delivery-profiles', 'panel-form'),
|
|
132
|
+
queryFn: async (): Promise<{ data: PanelDeliveryProfile[] }> =>
|
|
133
|
+
(await client.deliveryProfiles?.list({ limit: 100 })) ?? { data: [] },
|
|
134
|
+
enabled: Boolean(client.deliveryProfiles),
|
|
135
|
+
staleTime: FIVE_MINUTES,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Carton package types, plus whether this panel manages them at all.
|
|
141
|
+
*
|
|
142
|
+
* `supported` is what separates "this panel has no such page" (a seller's,
|
|
143
|
+
* where the packing card should not appear) from "the store has not created a
|
|
144
|
+
* carton yet" (an operator's, where it appears disabled and says where to go).
|
|
145
|
+
*/
|
|
146
|
+
export function useFormCartonPackageTypes() {
|
|
147
|
+
const client = getApiClient()
|
|
148
|
+
const supported = Boolean(client.packageTypes)
|
|
149
|
+
|
|
150
|
+
const query = useQuery({
|
|
151
|
+
queryKey: useResourceKey('package-types', 'panel-form', 'cartons'),
|
|
152
|
+
queryFn: async (): Promise<{ data: PanelPackageType[] }> =>
|
|
153
|
+
(await client.packageTypes?.list({ limit: 100, kind_eq: 'carton' })) ?? { data: [] },
|
|
154
|
+
enabled: supported,
|
|
155
|
+
staleTime: FIVE_MINUTES,
|
|
156
|
+
// The seller endpoint returns their own cartons before the
|
|
157
|
+
// marketplace's, so the 100-row ceiling cuts the shared vocabulary rather
|
|
158
|
+
// than the seller's own measurements. Ordered there rather than here
|
|
159
|
+
// because the page is taken before anything client-side could reorder it.
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return { ...query, supported }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Removing a file already on the product. A panel whose client registers no
|
|
167
|
+
* deletion answers a no-op mutation, so the gallery still drops the row from
|
|
168
|
+
* the form and the save reconciles it.
|
|
169
|
+
*/
|
|
170
|
+
export function useFormDeleteProductMedia(productId: string) {
|
|
171
|
+
const client = getApiClient()
|
|
172
|
+
const queryClient = useQueryClient()
|
|
173
|
+
// Resolved here rather than in the callback: it is a hook. No `panel-form`
|
|
174
|
+
// segment on this one — it must match the key the gallery reads, which is
|
|
175
|
+
// the operator dashboard's own `useProductMedia`.
|
|
176
|
+
const mediaKey = useResourceKey('products', productId, 'media')
|
|
177
|
+
|
|
178
|
+
return useMutation({
|
|
179
|
+
mutationFn: async (mediaId: string) => {
|
|
180
|
+
await client.deleteProductMedia?.(productId, mediaId)
|
|
181
|
+
},
|
|
182
|
+
onSuccess: () => {
|
|
183
|
+
void queryClient.invalidateQueries({ queryKey: mediaKey })
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function useCreateOptionType() {
|
|
189
|
+
const client = getApiClient()
|
|
190
|
+
const queryClient = useQueryClient()
|
|
191
|
+
const optionTypesKey = useResourceKey('option-types', 'panel-form')
|
|
192
|
+
|
|
193
|
+
return useMutation({
|
|
194
|
+
mutationFn: (params: { name: string; label: string } & Record<string, unknown>) =>
|
|
195
|
+
client.optionTypes?.create?.(params) as Promise<PanelOptionType>,
|
|
196
|
+
onSuccess: () => {
|
|
197
|
+
void queryClient.invalidateQueries({ queryKey: optionTypesKey })
|
|
198
|
+
},
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function useUpdateOptionType(id: string) {
|
|
203
|
+
const client = getApiClient()
|
|
204
|
+
const queryClient = useQueryClient()
|
|
205
|
+
const optionTypesKey = useResourceKey('option-types', 'panel-form')
|
|
206
|
+
|
|
207
|
+
return useMutation({
|
|
208
|
+
mutationFn: (
|
|
209
|
+
params: { option_values?: Array<{ name: string; label: string }> } & Record<string, unknown>,
|
|
210
|
+
) => client.optionTypes?.update?.(id, params) as Promise<PanelOptionType>,
|
|
211
|
+
onSuccess: () => {
|
|
212
|
+
void queryClient.invalidateQueries({ queryKey: optionTypesKey })
|
|
213
|
+
},
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Props for the shared multi-select pickers, reading through the registered
|
|
219
|
+
* client so the same card works in either panel.
|
|
220
|
+
*/
|
|
221
|
+
export function categoryAutocompleteProps(queryKey: string) {
|
|
222
|
+
const client = getApiClient()
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
queryKey,
|
|
226
|
+
search: async (query: string) =>
|
|
227
|
+
(await client.categories?.list({ name_cont: query, limit: 20 })) ?? { data: [] },
|
|
228
|
+
hydrate: async (ids: string[]) =>
|
|
229
|
+
(await client.categories?.list({ id_in: ids, limit: ids.length })) ?? { data: [] },
|
|
230
|
+
getOptionLabel: (category: PanelNamedRecord & { pretty_name?: string }) =>
|
|
231
|
+
category.pretty_name ?? category.name ?? category.id,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function collectionAutocompleteProps(queryKey: string) {
|
|
236
|
+
const client = getApiClient()
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
queryKey,
|
|
240
|
+
search: async (query: string) =>
|
|
241
|
+
(await client.collections?.list({ name_cont: query, limit: 20 })) ?? { data: [] },
|
|
242
|
+
hydrate: async (ids: string[]) =>
|
|
243
|
+
(await client.collections?.list({ id_in: ids, limit: ids.length })) ?? { data: [] },
|
|
244
|
+
getOptionLabel: (collection: PanelCollection) => collection.name ?? collection.id,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Resolves a currency to the locale its amounts are typed and shown in, so a
|
|
250
|
+
* EUR field accepts `1.234,56` and normalizes it to `1234.56`. Mirrors the
|
|
251
|
+
* operator dashboard's `useCurrencyLocale`, reading through the registered
|
|
252
|
+
* client so the shared price editor works in either panel; a panel with no
|
|
253
|
+
* markets answers undefined and callers fall back to the UI language.
|
|
254
|
+
*/
|
|
255
|
+
export function useFormCurrencyLocale(): (currency: string | undefined) => string | undefined {
|
|
256
|
+
const client = getApiClient()
|
|
257
|
+
|
|
258
|
+
const { data } = useQuery({
|
|
259
|
+
queryKey: useResourceKey('markets', 'panel-form'),
|
|
260
|
+
queryFn: async (): Promise<{ data: PanelMarket[] }> =>
|
|
261
|
+
(await client.markets?.list({ limit: 100 })) ?? { data: [] },
|
|
262
|
+
enabled: Boolean(client.markets),
|
|
263
|
+
staleTime: FIVE_MINUTES,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const markets = data?.data ?? []
|
|
267
|
+
|
|
268
|
+
return (currency) => {
|
|
269
|
+
if (!currency) return undefined
|
|
270
|
+
|
|
271
|
+
return markets.find((market) => market.currency === currency)?.default_locale ?? undefined
|
|
272
|
+
}
|
|
273
|
+
}
|