@spree/dashboard-core 0.13.1 → 1.0.0-beta.1

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
@@ -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 'lucide-react'
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
- toast.error(t('admin.components.file_upload.invalid_type', { accept }))
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
- toast.error(
150
- t('admin.components.file_upload.upload_failed', {
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 bg-muted/40 px-4 py-8 text-center transition-colors hover:bg-muted',
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 shadow-xs">
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 'lucide-react'
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 { toast } from 'sonner'
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: ImportType
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: Import) => void
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({ type, subject, onCreated, label }: ImportButtonProps) {
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
- toast.error(
98
- t('admin.components.import_button.failed', {
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
- toast.error(
111
- t('admin.components.import_button.template_failed', {
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
- toast.error(
123
- t('admin.components.import_button.example_failed', {
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
- size="sm"
135
- variant="outline"
136
- className="h-[2.125rem]"
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 className="size-4" />
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
  >