@spree/dashboard-core 1.0.0-rc.4 → 1.0.0-rc.6

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 (70) hide show
  1. package/package.json +10 -7
  2. package/src/api-client.ts +76 -7
  3. package/src/components/address-form-dialog.tsx +2 -2
  4. package/src/components/country-combobox.tsx +1 -2
  5. package/src/components/currency-amounts-field.tsx +93 -0
  6. package/src/components/currency-select.tsx +6 -7
  7. package/src/components/file-upload-field.tsx +1 -0
  8. package/src/components/locale-select.tsx +22 -23
  9. package/src/components/nav-main.tsx +7 -2
  10. package/src/components/preferences-form.tsx +2 -8
  11. package/src/components/settings-sidebar.tsx +8 -1
  12. package/src/components/slot.tsx +21 -1
  13. package/src/components/table-toolbar.tsx +102 -39
  14. package/src/hooks/use-display-name.ts +1 -18
  15. package/src/hooks/use-file-upload.ts +2 -1
  16. package/src/hooks/use-import.ts +3 -2
  17. package/src/index.ts +17 -3
  18. package/src/lib/currencies.ts +2 -34
  19. package/src/lib/currency-amounts.test.ts +147 -0
  20. package/src/lib/currency-amounts.ts +172 -0
  21. package/src/lib/currency-parts.ts +2 -21
  22. package/src/lib/filters-to-ransack.ts +4 -1
  23. package/src/lib/form-fields-registry.ts +23 -1
  24. package/src/lib/form-mappers.ts +1 -1
  25. package/src/lib/formatters.ts +6 -10
  26. package/src/lib/fulfillment-items.ts +11 -0
  27. package/src/lib/i18n.ts +1 -12
  28. package/src/lib/iso-codes.ts +9 -26
  29. package/src/lib/money.ts +9 -99
  30. package/src/lib/normalize-money.ts +2 -85
  31. package/src/lib/return-refunds.ts +1 -2
  32. package/src/lib/route-registry.ts +29 -0
  33. package/src/lib/settings-nav-registry.ts +6 -1
  34. package/src/lib/store-config.test.ts +93 -0
  35. package/src/lib/store-config.ts +76 -0
  36. package/src/lib/table-registry.ts +16 -5
  37. package/src/locales/ar.json +51 -0
  38. package/src/locales/de.json +51 -0
  39. package/src/locales/en.json +51 -0
  40. package/src/locales/es.json +51 -0
  41. package/src/locales/fr.json +51 -0
  42. package/src/locales/pl.json +51 -0
  43. package/src/locales/zh-CN.json +51 -0
  44. package/src/pages/package-types.tsx +50 -11
  45. package/src/pages/settings-index.tsx +4 -1
  46. package/src/pages/stock-locations.tsx +31 -6
  47. package/src/products/assignable-variants.ts +35 -0
  48. package/src/products/bulk-variants-dialog.tsx +27 -28
  49. package/src/products/digital-asset-edit-sheet.tsx +192 -0
  50. package/src/products/digital-asset-provider-sheet.tsx +88 -0
  51. package/src/products/digital-assets-card.tsx +451 -0
  52. package/src/products/media-edit-sheet.tsx +68 -20
  53. package/src/products/normalize-quantity.ts +2 -3
  54. package/src/products/product-bulk-price-editor.tsx +1 -3
  55. package/src/products/product-form-cards.tsx +67 -37
  56. package/src/products/product-form-mapping.ts +44 -5
  57. package/src/products/product-schema.test.ts +8 -0
  58. package/src/products/product-schema.ts +14 -7
  59. package/src/products/product-types.ts +1 -1
  60. package/src/products/provider-settings-fields.tsx +110 -0
  61. package/src/products/sheet-save-handlers.ts +37 -0
  62. package/src/products/use-digital-assets.ts +66 -0
  63. package/src/products/variant-edit-sheet.tsx +21 -11
  64. package/src/products/variants-section.tsx +4 -1
  65. package/src/providers/store-provider.tsx +31 -7
  66. package/src/schemas/package-type.ts +14 -12
  67. package/src/schemas/stock-location.ts +2 -3
  68. package/src/tables/stock-locations.tsx +5 -8
  69. package/src/lib/money.test.ts +0 -63
  70. package/src/lib/normalize-money.test.ts +0 -67
@@ -0,0 +1,192 @@
1
+ import type { DigitalAssetProviderSettingField } from '@spree/admin-sdk'
2
+ import {
3
+ Button,
4
+ Field,
5
+ FieldLabel,
6
+ Input,
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ Sheet,
13
+ SheetContent,
14
+ SheetFooter,
15
+ SheetHeader,
16
+ SheetTitle,
17
+ } from '@spree/dashboard-ui'
18
+ import { useEffect, useState } from 'react'
19
+ import { useTranslation } from 'react-i18next'
20
+ import type { PanelDigitalAsset } from '../api-client'
21
+ import { variantLabel } from './assignable-variants'
22
+ import type { PanelVariant } from './product-types'
23
+ import { ProviderSettingsFields } from './provider-settings-fields'
24
+ import { useUpdateDigitalAsset } from './use-digital-assets'
25
+
26
+ interface Props {
27
+ productId: string
28
+ asset: PanelDigitalAsset | null
29
+ variants?: PanelVariant[]
30
+ /** Fields for a provider-backed asset; empty for an uploaded file. */
31
+ settingsSchema?: DigitalAssetProviderSettingField[]
32
+ open: boolean
33
+ onOpenChange: (open: boolean) => void
34
+ }
35
+
36
+ /** Blank means "use the store's setting", so an empty box is a real value. */
37
+ function toInput(value: number | null | undefined): string {
38
+ return value == null ? '' : String(value)
39
+ }
40
+
41
+ function parseLimit(raw: string): number | null | undefined {
42
+ if (raw.trim() === '') return null
43
+ const parsed = Number(raw)
44
+ return Number.isInteger(parsed) && parsed >= 1 ? parsed : undefined
45
+ }
46
+
47
+ export function DigitalAssetEditSheet({
48
+ productId,
49
+ asset,
50
+ variants,
51
+ settingsSchema = [],
52
+ open,
53
+ onOpenChange,
54
+ }: Props) {
55
+ const { t } = useTranslation()
56
+ const updateAsset = useUpdateDigitalAsset(productId)
57
+
58
+ const [clicks, setClicks] = useState('')
59
+ const [days, setDays] = useState('')
60
+ const [variantId, setVariantId] = useState('')
61
+ const [settings, setSettings] = useState<Record<string, unknown>>({})
62
+ const [error, setError] = useState<string | null>(null)
63
+
64
+ // Re-seed whenever a different file is opened, so the sheet never shows the
65
+ // previous row's values.
66
+ useEffect(() => {
67
+ if (!asset) return
68
+ setClicks(toInput(asset.authorized_clicks))
69
+ setDays(toInput(asset.authorized_days))
70
+ setVariantId(asset.variant_id ?? '')
71
+ setSettings(asset.provider_settings ?? {})
72
+ setError(null)
73
+ }, [asset])
74
+
75
+ if (!asset) return null
76
+
77
+ const hasVariants = (variants?.length ?? 0) > 1
78
+
79
+ async function handleSave() {
80
+ const parsedClicks = parseLimit(clicks)
81
+ const parsedDays = parseLimit(days)
82
+
83
+ if (parsedClicks === undefined || parsedDays === undefined) {
84
+ setError(t('admin.digital_assets.limit_invalid'))
85
+ return
86
+ }
87
+
88
+ setError(null)
89
+ try {
90
+ await updateAsset.mutateAsync({
91
+ id: asset!.id,
92
+ authorized_clicks: parsedClicks,
93
+ authorized_days: parsedDays,
94
+ ...(hasVariants && variantId ? { variant_id: variantId } : {}),
95
+ ...(settingsSchema.length > 0 ? { provider_settings: settings } : {}),
96
+ })
97
+ onOpenChange(false)
98
+ } catch (err) {
99
+ // useResourceMutation suppresses the toast for a 422, so surface the
100
+ // reason inline and keep the sheet open with the entered values.
101
+ setError(err instanceof Error ? err.message : t('admin.errors.unexpected'))
102
+ }
103
+ }
104
+
105
+ return (
106
+ <Sheet open={open} onOpenChange={onOpenChange}>
107
+ <SheetContent side="right" className="flex flex-col">
108
+ <SheetHeader>
109
+ <SheetTitle>
110
+ {asset.filename ?? asset.provider_name ?? t('admin.digital_assets.untitled')}
111
+ </SheetTitle>
112
+ </SheetHeader>
113
+
114
+ <div className="flex flex-1 flex-col gap-6 overflow-y-auto p-4">
115
+ <ProviderSettingsFields
116
+ schema={settingsSchema}
117
+ values={settings}
118
+ onChange={setSettings}
119
+ />
120
+
121
+ {hasVariants && (
122
+ <Field>
123
+ <FieldLabel htmlFor="digital-asset-variant">
124
+ {t('admin.digital_assets.columns.variant')}
125
+ </FieldLabel>
126
+ <Select value={variantId} onValueChange={setVariantId}>
127
+ <SelectTrigger id="digital-asset-variant">
128
+ <SelectValue>
129
+ {(value) => {
130
+ const variant = variants?.find((v) => v.id === value)
131
+ return variant
132
+ ? variantLabel(variant)
133
+ : t('admin.digital_assets.all_variants')
134
+ }}
135
+ </SelectValue>
136
+ </SelectTrigger>
137
+ <SelectContent>
138
+ {variants?.map((variant) => (
139
+ <SelectItem key={variant.id} value={variant.id}>
140
+ {variantLabel(variant)}
141
+ </SelectItem>
142
+ ))}
143
+ </SelectContent>
144
+ </Select>
145
+ </Field>
146
+ )}
147
+
148
+ <Field>
149
+ <FieldLabel htmlFor="digital-asset-clicks">
150
+ {t('admin.fields.store.digital_asset_authorized_clicks.label')}
151
+ </FieldLabel>
152
+ <Input
153
+ id="digital-asset-clicks"
154
+ type="number"
155
+ min={1}
156
+ value={clicks}
157
+ placeholder={String(asset.effective_authorized_clicks)}
158
+ onChange={(e) => setClicks(e.target.value)}
159
+ />
160
+ </Field>
161
+
162
+ <Field>
163
+ <FieldLabel htmlFor="digital-asset-days">
164
+ {t('admin.fields.store.digital_asset_authorized_days.label')}
165
+ </FieldLabel>
166
+ <Input
167
+ id="digital-asset-days"
168
+ type="number"
169
+ min={1}
170
+ value={days}
171
+ placeholder={String(asset.effective_authorized_days)}
172
+ onChange={(e) => setDays(e.target.value)}
173
+ />
174
+ </Field>
175
+
176
+ <p className="text-muted-foreground text-xs">{t('admin.digital_assets.limits_help')}</p>
177
+
178
+ {error && <p className="text-destructive text-sm">{error}</p>}
179
+ </div>
180
+
181
+ <SheetFooter>
182
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
183
+ {t('admin.actions.cancel')}
184
+ </Button>
185
+ <Button type="button" onClick={handleSave} disabled={updateAsset.isPending}>
186
+ {updateAsset.isPending ? t('admin.actions.saving') : t('admin.actions.save')}
187
+ </Button>
188
+ </SheetFooter>
189
+ </SheetContent>
190
+ </Sheet>
191
+ )
192
+ }
@@ -0,0 +1,88 @@
1
+ import type { DigitalAssetProvider } from '@spree/admin-sdk'
2
+ import {
3
+ Button,
4
+ Sheet,
5
+ SheetContent,
6
+ SheetFooter,
7
+ SheetHeader,
8
+ SheetTitle,
9
+ } from '@spree/dashboard-ui'
10
+ import { useEffect, useState } from 'react'
11
+ import { useTranslation } from 'react-i18next'
12
+ import { defaultProviderSettings, ProviderSettingsFields } from './provider-settings-fields'
13
+ import { useCreateDigitalAsset } from './use-digital-assets'
14
+
15
+ interface Props {
16
+ productId: string
17
+ /** The picked source, or null when the sheet is closed. */
18
+ provider: DigitalAssetProvider | null
19
+ onOpenChange: (open: boolean) => void
20
+ onCreated: () => void
21
+ }
22
+
23
+ /**
24
+ * Captures a provider-backed asset's settings before creating it. Only opened
25
+ * for a provider that declares fields; a settings-free provider creates in one
26
+ * click from the source menu without this sheet.
27
+ */
28
+ export function DigitalAssetProviderSheet({ productId, provider, onOpenChange, onCreated }: Props) {
29
+ const { t } = useTranslation()
30
+ const createAsset = useCreateDigitalAsset(productId)
31
+ const [values, setValues] = useState<Record<string, unknown>>({})
32
+ const [error, setError] = useState<string | null>(null)
33
+
34
+ // Re-seed with the provider's defaults each time a source is picked.
35
+ useEffect(() => {
36
+ if (provider) {
37
+ setValues(defaultProviderSettings(provider.settings_schema))
38
+ setError(null)
39
+ }
40
+ }, [provider])
41
+
42
+ if (!provider) return null
43
+
44
+ async function handleCreate() {
45
+ setError(null)
46
+ try {
47
+ await createAsset.mutateAsync({
48
+ provider_type: provider!.type,
49
+ provider_settings: values,
50
+ })
51
+ onCreated()
52
+ onOpenChange(false)
53
+ } catch (err) {
54
+ // useResourceMutation suppresses the toast for a 422, so surface the
55
+ // reason inline and keep the sheet open with the entered values.
56
+ setError(err instanceof Error ? err.message : t('admin.errors.unexpected'))
57
+ }
58
+ }
59
+
60
+ return (
61
+ <Sheet open={Boolean(provider)} onOpenChange={onOpenChange}>
62
+ <SheetContent side="right" className="flex flex-col">
63
+ <SheetHeader>
64
+ <SheetTitle>{provider.name}</SheetTitle>
65
+ </SheetHeader>
66
+
67
+ <div className="flex flex-1 flex-col gap-6 overflow-y-auto p-4">
68
+ <ProviderSettingsFields
69
+ schema={provider.settings_schema}
70
+ values={values}
71
+ onChange={setValues}
72
+ />
73
+
74
+ {error && <p className="text-destructive text-sm">{error}</p>}
75
+ </div>
76
+
77
+ <SheetFooter>
78
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
79
+ {t('admin.actions.cancel')}
80
+ </Button>
81
+ <Button type="button" onClick={handleCreate} disabled={createAsset.isPending}>
82
+ {createAsset.isPending ? t('admin.actions.saving') : t('admin.actions.add')}
83
+ </Button>
84
+ </SheetFooter>
85
+ </SheetContent>
86
+ </Sheet>
87
+ )
88
+ }