@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
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
Field,
|
|
11
11
|
FieldDescription,
|
|
12
12
|
FieldLabel,
|
|
13
|
+
toastManager,
|
|
13
14
|
} from '@spree/dashboard-ui'
|
|
14
|
-
import { FileIcon, UploadCloudIcon, XIcon } from '
|
|
15
|
+
import { FileIcon, UploadCloudIcon, XIcon } from '@spree/dashboard-ui/icons'
|
|
15
16
|
import { type ReactNode, useState } from 'react'
|
|
16
17
|
import { useTranslation } from 'react-i18next'
|
|
17
|
-
import { toast } from 'sonner'
|
|
18
18
|
import { useDirectUpload } from '../hooks/use-direct-upload'
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -43,7 +43,9 @@ export const EMPTY_FILE_UPLOAD_VALUE: FileUploadValue = {
|
|
|
43
43
|
export function formatFileSize(bytes: number): string {
|
|
44
44
|
if (bytes < 1024) return `${bytes} B`
|
|
45
45
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
46
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
46
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
47
|
+
// Digital products are sold in gigabytes — sample packs, video, disk images.
|
|
48
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export interface FileUploadFieldProps {
|
|
@@ -72,6 +74,18 @@ export interface FileUploadFieldProps {
|
|
|
72
74
|
disabled?: boolean
|
|
73
75
|
/** Re-wrap the picked file before upload (e.g. force a content type). */
|
|
74
76
|
transformFile?: (file: File) => File
|
|
77
|
+
/**
|
|
78
|
+
* Upload to private storage rather than the public bucket. Required for
|
|
79
|
+
* anything served through an authorized endpoint — attaching a signed id
|
|
80
|
+
* never moves a blob between services, so this has to be right up front.
|
|
81
|
+
*/
|
|
82
|
+
private?: boolean
|
|
83
|
+
/**
|
|
84
|
+
* Called with true while a picked file is uploading. A consumer whose save
|
|
85
|
+
* sends `signedId` must disable it meanwhile — the id does not exist until
|
|
86
|
+
* the upload resolves, so an early submit silently drops the file.
|
|
87
|
+
*/
|
|
88
|
+
onUploadingChange?: (uploading: boolean) => void
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
/**
|
|
@@ -97,9 +111,11 @@ export function FileUploadField({
|
|
|
97
111
|
mediaClassName,
|
|
98
112
|
disabled = false,
|
|
99
113
|
transformFile,
|
|
114
|
+
private: privateStorage = false,
|
|
115
|
+
onUploadingChange,
|
|
100
116
|
}: FileUploadFieldProps) {
|
|
101
117
|
const { t } = useTranslation()
|
|
102
|
-
const directUpload = useDirectUpload()
|
|
118
|
+
const directUpload = useDirectUpload({ private: privateStorage })
|
|
103
119
|
const [pending, setPending] = useState<File | null>(null)
|
|
104
120
|
|
|
105
121
|
const isImage = variant === 'image'
|
|
@@ -126,12 +142,16 @@ export function FileUploadField({
|
|
|
126
142
|
async function handlePicked(picked: File) {
|
|
127
143
|
if (disabled || pending) return
|
|
128
144
|
if (!matchesAccept(picked)) {
|
|
129
|
-
|
|
145
|
+
toastManager.add({
|
|
146
|
+
type: 'error',
|
|
147
|
+
title: t('admin.components.file_upload.invalid_type', { accept }),
|
|
148
|
+
})
|
|
130
149
|
return
|
|
131
150
|
}
|
|
132
151
|
|
|
133
152
|
const file = transformFile ? transformFile(picked) : picked
|
|
134
153
|
setPending(file)
|
|
154
|
+
onUploadingChange?.(true)
|
|
135
155
|
try {
|
|
136
156
|
const result = await directUpload.mutateAsync(file)
|
|
137
157
|
// Revoke a preview we're replacing in-place; the object URL otherwise
|
|
@@ -146,13 +166,15 @@ export function FileUploadField({
|
|
|
146
166
|
byteSize: file.size,
|
|
147
167
|
})
|
|
148
168
|
} catch (err) {
|
|
149
|
-
|
|
150
|
-
|
|
169
|
+
toastManager.add({
|
|
170
|
+
type: 'error',
|
|
171
|
+
title: t('admin.components.file_upload.upload_failed', {
|
|
151
172
|
message: err instanceof Error ? err.message : String(err),
|
|
152
173
|
}),
|
|
153
|
-
)
|
|
174
|
+
})
|
|
154
175
|
} finally {
|
|
155
176
|
setPending(null)
|
|
177
|
+
onUploadingChange?.(false)
|
|
156
178
|
}
|
|
157
179
|
}
|
|
158
180
|
|
|
@@ -191,7 +213,7 @@ export function FileUploadField({
|
|
|
191
213
|
Enter/Space; the label paints a focus ring via has-[:focus-visible]. */}
|
|
192
214
|
<label
|
|
193
215
|
className={cn(
|
|
194
|
-
'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-border border-dashed
|
|
216
|
+
'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-border border-dashed px-4 py-8 text-center transition-colors hover:bg-accent/50',
|
|
195
217
|
'has-[:focus-visible]:border-ring has-[:focus-visible]:ring-[3px] has-[:focus-visible]:ring-ring/50',
|
|
196
218
|
(disabled || !!pending) && 'pointer-events-none opacity-60',
|
|
197
219
|
)}
|
|
@@ -206,7 +228,7 @@ export function FileUploadField({
|
|
|
206
228
|
<span className="text-muted-foreground text-sm">
|
|
207
229
|
{dropLabel ?? t('admin.components.file_upload.drop_label')}
|
|
208
230
|
</span>
|
|
209
|
-
<span className="mt-1 inline-flex h-8 items-center rounded-md border border-border bg-background px-3 font-medium text-sm
|
|
231
|
+
<span className="mt-1 inline-flex h-8 items-center rounded-md border border-border bg-background px-3 font-medium text-sm hover:bg-accent">
|
|
210
232
|
{browseLabel ?? t('admin.components.file_upload.browse')}
|
|
211
233
|
</span>
|
|
212
234
|
<input
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Import, ImportType } from '@spree/admin-sdk'
|
|
2
1
|
import {
|
|
3
2
|
Button,
|
|
4
3
|
Field,
|
|
@@ -14,12 +13,14 @@ import {
|
|
|
14
13
|
SheetFooter,
|
|
15
14
|
SheetHeader,
|
|
16
15
|
SheetTitle,
|
|
16
|
+
toastManager,
|
|
17
17
|
} from '@spree/dashboard-ui'
|
|
18
|
-
import { DownloadIcon, FileSpreadsheetIcon, UploadIcon } from '
|
|
18
|
+
import { DownloadIcon, FileSpreadsheetIcon, UploadIcon } from '@spree/dashboard-ui/icons'
|
|
19
19
|
import { useState } from 'react'
|
|
20
20
|
import { useTranslation } from 'react-i18next'
|
|
21
|
-
import {
|
|
21
|
+
import type { PanelImport } from '../api-client'
|
|
22
22
|
import {
|
|
23
|
+
type ImportCreateExtras,
|
|
23
24
|
useCreateImport,
|
|
24
25
|
useDownloadImportExample,
|
|
25
26
|
useDownloadImportTemplate,
|
|
@@ -40,7 +41,7 @@ type Delimiter = (typeof DELIMITERS)[number]['value']
|
|
|
40
41
|
|
|
41
42
|
interface ImportButtonProps {
|
|
42
43
|
/** Which dataset to import. Server validates against `Spree::Import.available_types`. */
|
|
43
|
-
type:
|
|
44
|
+
type: string
|
|
44
45
|
/**
|
|
45
46
|
* CanCanCan subject gating the button — the *imported resource*
|
|
46
47
|
* (e.g. `Subject.Product`), mirroring the server's `write_<resource>`
|
|
@@ -52,9 +53,24 @@ interface ImportButtonProps {
|
|
|
52
53
|
* wizard — typically a full-window dialog driven by an `?import=` search
|
|
53
54
|
* param.
|
|
54
55
|
*/
|
|
55
|
-
onCreated: (imp:
|
|
56
|
+
onCreated: (imp: PanelImport) => void
|
|
56
57
|
/** Label shown on the button. Defaults to the translated "Import" action. */
|
|
57
58
|
label?: string
|
|
59
|
+
/** Optional variant for the button. Defaults to "outline". */
|
|
60
|
+
variant?: 'default' | 'outline' | 'ghost'
|
|
61
|
+
/** Optional size for the button. Defaults to "sm". */
|
|
62
|
+
size?: 'sm' | 'default' | 'lg'
|
|
63
|
+
/**
|
|
64
|
+
* Whatever else the type needs at create — a parent record's id, say. The
|
|
65
|
+
* button then belongs on that record's page, and the sheet asks nothing.
|
|
66
|
+
*/
|
|
67
|
+
params?: ImportCreateExtras
|
|
68
|
+
/**
|
|
69
|
+
* Path the import-done email links back to, relative to this panel's origin.
|
|
70
|
+
* Defaults to the operator dashboard's imports view under the current
|
|
71
|
+
* tenant; a panel filing that page elsewhere passes its own.
|
|
72
|
+
*/
|
|
73
|
+
resultsPath?: string
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
/**
|
|
@@ -63,7 +79,16 @@ interface ImportButtonProps {
|
|
|
63
79
|
* `FileUploadField`; submitting creates the import from the signed blob id
|
|
64
80
|
* and hands it to `onCreated`.
|
|
65
81
|
*/
|
|
66
|
-
export function ImportButton({
|
|
82
|
+
export function ImportButton({
|
|
83
|
+
type,
|
|
84
|
+
subject,
|
|
85
|
+
onCreated,
|
|
86
|
+
label,
|
|
87
|
+
variant,
|
|
88
|
+
size,
|
|
89
|
+
params,
|
|
90
|
+
resultsPath,
|
|
91
|
+
}: ImportButtonProps) {
|
|
67
92
|
const { t } = useTranslation()
|
|
68
93
|
const [open, setOpen] = useState(false)
|
|
69
94
|
const [file, setFile] = useState<FileUploadValue>(EMPTY_FILE_UPLOAD_VALUE)
|
|
@@ -87,18 +112,19 @@ export function ImportButton({ type, subject, onCreated, label }: ImportButtonPr
|
|
|
87
112
|
if (!file.signedId || createImport.isPending) return
|
|
88
113
|
|
|
89
114
|
createImport.mutate(
|
|
90
|
-
{ type, signedId: file.signedId, preferredDelimiter: delimiter },
|
|
115
|
+
{ type, signedId: file.signedId, preferredDelimiter: delimiter, params, resultsPath },
|
|
91
116
|
{
|
|
92
117
|
onSuccess: (imp) => {
|
|
93
118
|
handleOpenChange(false)
|
|
94
119
|
onCreated(imp)
|
|
95
120
|
},
|
|
96
121
|
onError: (err) => {
|
|
97
|
-
|
|
98
|
-
|
|
122
|
+
toastManager.add({
|
|
123
|
+
type: 'error',
|
|
124
|
+
title: t('admin.components.import_button.failed', {
|
|
99
125
|
message: err instanceof Error ? err.message : String(err),
|
|
100
126
|
}),
|
|
101
|
-
)
|
|
127
|
+
})
|
|
102
128
|
},
|
|
103
129
|
},
|
|
104
130
|
)
|
|
@@ -107,11 +133,12 @@ export function ImportButton({ type, subject, onCreated, label }: ImportButtonPr
|
|
|
107
133
|
function handleTemplateDownload() {
|
|
108
134
|
downloadTemplate.mutate(type, {
|
|
109
135
|
onError: (err) => {
|
|
110
|
-
|
|
111
|
-
|
|
136
|
+
toastManager.add({
|
|
137
|
+
type: 'error',
|
|
138
|
+
title: t('admin.components.import_button.template_failed', {
|
|
112
139
|
message: err instanceof Error ? err.message : String(err),
|
|
113
140
|
}),
|
|
114
|
-
)
|
|
141
|
+
})
|
|
115
142
|
},
|
|
116
143
|
})
|
|
117
144
|
}
|
|
@@ -119,25 +146,29 @@ export function ImportButton({ type, subject, onCreated, label }: ImportButtonPr
|
|
|
119
146
|
function handleExampleDownload() {
|
|
120
147
|
downloadExample.mutate(type, {
|
|
121
148
|
onError: (err) => {
|
|
122
|
-
|
|
123
|
-
|
|
149
|
+
toastManager.add({
|
|
150
|
+
type: 'error',
|
|
151
|
+
title: t('admin.components.import_button.example_failed', {
|
|
124
152
|
message: err instanceof Error ? err.message : String(err),
|
|
125
153
|
}),
|
|
126
|
-
)
|
|
154
|
+
})
|
|
127
155
|
},
|
|
128
156
|
})
|
|
129
157
|
}
|
|
130
158
|
|
|
131
159
|
return (
|
|
132
160
|
<Can I="create" a={subject}>
|
|
161
|
+
{/* Desktop only: the wizard needs a file from disk and column
|
|
162
|
+
mapping across a wide grid, neither of which works on a phone. */}
|
|
133
163
|
<Button
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
164
|
+
type="button"
|
|
165
|
+
size={size ?? 'sm'}
|
|
166
|
+
variant={variant ?? 'outline'}
|
|
167
|
+
className="hidden h-[2.125rem] lg:inline-flex"
|
|
137
168
|
onClick={() => setOpen(true)}
|
|
138
169
|
disabled={createImport.isPending}
|
|
139
170
|
>
|
|
140
|
-
<UploadIcon
|
|
171
|
+
<UploadIcon />
|
|
141
172
|
{label ?? t('admin.actions.import')}
|
|
142
173
|
</Button>
|
|
143
174
|
|
|
@@ -212,17 +243,11 @@ export function ImportButton({ type, subject, onCreated, label }: ImportButtonPr
|
|
|
212
243
|
</div>
|
|
213
244
|
|
|
214
245
|
<SheetFooter>
|
|
215
|
-
<Button
|
|
216
|
-
type="button"
|
|
217
|
-
variant="outline"
|
|
218
|
-
size="sm"
|
|
219
|
-
onClick={() => handleOpenChange(false)}
|
|
220
|
-
>
|
|
246
|
+
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}>
|
|
221
247
|
{t('admin.actions.cancel')}
|
|
222
248
|
</Button>
|
|
223
249
|
<Button
|
|
224
250
|
type="button"
|
|
225
|
-
size="sm"
|
|
226
251
|
onClick={handleSubmit}
|
|
227
252
|
disabled={!file.signedId || createImport.isPending}
|
|
228
253
|
>
|