@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.
Files changed (125) hide show
  1. package/README.md +4 -0
  2. package/dist/vite/index.js +17 -3
  3. package/package.json +7 -7
  4. package/src/api-client.ts +643 -0
  5. package/src/components/address-form-dialog.test.ts +25 -0
  6. package/src/components/address-form-dialog.tsx +203 -79
  7. package/src/components/app-shell.tsx +171 -0
  8. package/src/components/app-sidebar.tsx +184 -27
  9. package/src/components/bulk-action-bar.tsx +89 -157
  10. package/src/components/country-combobox.tsx +69 -34
  11. package/src/components/country-state-fields.tsx +43 -11
  12. package/src/components/currency-select.tsx +81 -66
  13. package/src/components/export-button.tsx +62 -5
  14. package/src/components/file-upload-field.tsx +32 -10
  15. package/src/components/import-button.tsx +52 -27
  16. package/src/components/import-wizard-dialog.tsx +710 -0
  17. package/src/components/locale-select.tsx +70 -57
  18. package/src/components/media-picker-sheet.tsx +307 -0
  19. package/src/components/mobile-top-bar.tsx +75 -0
  20. package/src/components/nav-main.tsx +197 -27
  21. package/src/components/page-header.tsx +203 -61
  22. package/src/components/page-tabs.tsx +4 -1
  23. package/src/components/preferences-form.tsx +98 -4
  24. package/src/components/resource-combobox.tsx +52 -4
  25. package/src/components/resource-picker-sheet.tsx +209 -73
  26. package/src/components/resource-table.tsx +383 -127
  27. package/src/components/route-announcer.tsx +64 -0
  28. package/src/components/section-heading.tsx +95 -0
  29. package/src/components/settings-sidebar.tsx +290 -60
  30. package/src/components/sheet-top-bar.tsx +44 -0
  31. package/src/components/sidebar-search.tsx +83 -0
  32. package/src/components/sidebar-user.tsx +213 -0
  33. package/src/components/skip-link.tsx +36 -0
  34. package/src/components/store-date-picker.tsx +6 -2
  35. package/src/components/store-switcher.tsx +19 -14
  36. package/src/components/table-toolbar.tsx +1357 -440
  37. package/src/components/tag-combobox.tsx +12 -4
  38. package/src/components/top-bar-breadcrumbs.tsx +235 -0
  39. package/src/components/wizard-close-guard.test.ts +54 -0
  40. package/src/hooks/use-auto-collapse-sidebar.ts +57 -0
  41. package/src/hooks/use-command-palette.tsx +13 -0
  42. package/src/hooks/use-countries.test.ts +56 -0
  43. package/src/hooks/use-countries.ts +51 -5
  44. package/src/hooks/use-direct-upload.ts +16 -3
  45. package/src/hooks/use-export.ts +51 -16
  46. package/src/hooks/use-import.ts +241 -16
  47. package/src/hooks/use-package-types.ts +132 -0
  48. package/src/hooks/use-resource-mutation.ts +74 -13
  49. package/src/hooks/use-stock-locations.ts +131 -0
  50. package/src/hooks/use-switch-admin-locale.ts +2 -2
  51. package/src/hooks/use-visual-viewport-offset.ts +61 -0
  52. package/src/index.ts +45 -1
  53. package/src/lib/cancel-order-form.ts +27 -0
  54. package/src/lib/create-action-registry.ts +278 -0
  55. package/src/lib/date-presets.test.ts +86 -0
  56. package/src/lib/date-presets.ts +98 -0
  57. package/src/lib/delivery-form.ts +15 -0
  58. package/src/lib/docs.ts +32 -0
  59. package/src/lib/download.ts +13 -1
  60. package/src/lib/filters-to-ransack.test.ts +113 -0
  61. package/src/lib/filters-to-ransack.ts +22 -1
  62. package/src/lib/form-errors.ts +122 -10
  63. package/src/lib/form-mappers.ts +76 -0
  64. package/src/lib/fulfillment-items.test.ts +213 -0
  65. package/src/lib/fulfillment-items.ts +171 -0
  66. package/src/lib/i18n.ts +19 -11
  67. package/src/lib/import-types.ts +25 -0
  68. package/src/lib/label-upload-form.ts +22 -0
  69. package/src/lib/nav-registry.ts +71 -3
  70. package/src/lib/permissions.ts +46 -1
  71. package/src/lib/progress.ts +18 -0
  72. package/src/lib/query-keys.ts +13 -13
  73. package/src/lib/sample-csv.ts +9 -6
  74. package/src/lib/search-registry.ts +6 -2
  75. package/src/lib/settings-nav-registry.ts +104 -11
  76. package/src/lib/table-registry.ts +50 -0
  77. package/src/lib/type-labels.test.ts +68 -0
  78. package/src/lib/type-labels.ts +78 -0
  79. package/src/locales/ar.json +1889 -517
  80. package/src/locales/de.json +1884 -517
  81. package/src/locales/en.json +1886 -519
  82. package/src/locales/fr.json +1884 -517
  83. package/src/locales/pl.json +1913 -517
  84. package/src/locales/zh-CN.json +1883 -517
  85. package/src/pages/package-types.tsx +484 -0
  86. package/src/pages/settings-index.tsx +112 -0
  87. package/src/pages/stock-locations.tsx +674 -0
  88. package/src/plugin.ts +29 -0
  89. package/src/products/add-video-dialog.tsx +100 -0
  90. package/src/products/bulk-variants-dialog.tsx +301 -0
  91. package/src/products/carton-size.test.ts +51 -0
  92. package/src/products/carton-size.ts +20 -0
  93. package/src/products/currency-parts.ts +21 -0
  94. package/src/products/inventory-section.tsx +259 -0
  95. package/src/products/media-edit-sheet.tsx +336 -0
  96. package/src/products/normalize-customs.test.ts +39 -0
  97. package/src/products/normalize-customs.ts +27 -0
  98. package/src/products/normalize-money.test.ts +58 -0
  99. package/src/products/normalize-money.ts +73 -0
  100. package/src/products/normalize-quantity.test.ts +32 -0
  101. package/src/products/normalize-quantity.ts +31 -0
  102. package/src/products/product-bulk-price-editor.tsx +165 -0
  103. package/src/products/product-form-cards.tsx +1167 -0
  104. package/src/products/product-form-mapping.ts +212 -0
  105. package/src/products/product-schema.ts +318 -0
  106. package/src/products/product-types.ts +130 -0
  107. package/src/products/use-product-form-data.ts +273 -0
  108. package/src/products/variant-edit-sheet.tsx +725 -0
  109. package/src/products/variants-matrix.test.ts +154 -0
  110. package/src/products/variants-matrix.ts +244 -0
  111. package/src/products/variants-options-builder.tsx +571 -0
  112. package/src/products/variants-section.tsx +535 -0
  113. package/src/products/video-url.test.ts +71 -0
  114. package/src/products/video-url.ts +83 -0
  115. package/src/providers/auth-provider.tsx +70 -12
  116. package/src/providers/permission-provider.tsx +68 -28
  117. package/src/providers/sticky-header-provider.tsx +134 -0
  118. package/src/providers/store-provider.tsx +36 -3
  119. package/src/providers/tenant-provider.tsx +33 -0
  120. package/src/schemas/package-type.ts +95 -0
  121. package/src/schemas/stock-location.ts +126 -0
  122. package/src/tables/package-types.tsx +74 -0
  123. package/src/tables/stock-locations.tsx +124 -0
  124. package/src/vite/index.ts +40 -7
  125. 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
+ }