@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.
- package/package.json +10 -7
- package/src/api-client.ts +76 -7
- package/src/components/address-form-dialog.tsx +2 -2
- package/src/components/country-combobox.tsx +1 -2
- package/src/components/currency-amounts-field.tsx +93 -0
- package/src/components/currency-select.tsx +6 -7
- package/src/components/file-upload-field.tsx +1 -0
- package/src/components/locale-select.tsx +22 -23
- package/src/components/nav-main.tsx +7 -2
- package/src/components/preferences-form.tsx +2 -8
- package/src/components/settings-sidebar.tsx +8 -1
- package/src/components/slot.tsx +21 -1
- package/src/components/table-toolbar.tsx +102 -39
- package/src/hooks/use-display-name.ts +1 -18
- package/src/hooks/use-file-upload.ts +2 -1
- package/src/hooks/use-import.ts +3 -2
- package/src/index.ts +17 -3
- package/src/lib/currencies.ts +2 -34
- package/src/lib/currency-amounts.test.ts +147 -0
- package/src/lib/currency-amounts.ts +172 -0
- package/src/lib/currency-parts.ts +2 -21
- package/src/lib/filters-to-ransack.ts +4 -1
- package/src/lib/form-fields-registry.ts +23 -1
- package/src/lib/form-mappers.ts +1 -1
- package/src/lib/formatters.ts +6 -10
- package/src/lib/fulfillment-items.ts +11 -0
- package/src/lib/i18n.ts +1 -12
- package/src/lib/iso-codes.ts +9 -26
- package/src/lib/money.ts +9 -99
- package/src/lib/normalize-money.ts +2 -85
- package/src/lib/return-refunds.ts +1 -2
- package/src/lib/route-registry.ts +29 -0
- package/src/lib/settings-nav-registry.ts +6 -1
- package/src/lib/store-config.test.ts +93 -0
- package/src/lib/store-config.ts +76 -0
- package/src/lib/table-registry.ts +16 -5
- package/src/locales/ar.json +51 -0
- package/src/locales/de.json +51 -0
- package/src/locales/en.json +51 -0
- package/src/locales/es.json +51 -0
- package/src/locales/fr.json +51 -0
- package/src/locales/pl.json +51 -0
- package/src/locales/zh-CN.json +51 -0
- package/src/pages/package-types.tsx +50 -11
- package/src/pages/settings-index.tsx +4 -1
- package/src/pages/stock-locations.tsx +31 -6
- package/src/products/assignable-variants.ts +35 -0
- package/src/products/bulk-variants-dialog.tsx +27 -28
- package/src/products/digital-asset-edit-sheet.tsx +192 -0
- package/src/products/digital-asset-provider-sheet.tsx +88 -0
- package/src/products/digital-assets-card.tsx +451 -0
- package/src/products/media-edit-sheet.tsx +68 -20
- package/src/products/normalize-quantity.ts +2 -3
- package/src/products/product-bulk-price-editor.tsx +1 -3
- package/src/products/product-form-cards.tsx +67 -37
- package/src/products/product-form-mapping.ts +44 -5
- package/src/products/product-schema.test.ts +8 -0
- package/src/products/product-schema.ts +14 -7
- package/src/products/product-types.ts +1 -1
- package/src/products/provider-settings-fields.tsx +110 -0
- package/src/products/sheet-save-handlers.ts +37 -0
- package/src/products/use-digital-assets.ts +66 -0
- package/src/products/variant-edit-sheet.tsx +21 -11
- package/src/products/variants-section.tsx +4 -1
- package/src/providers/store-provider.tsx +31 -7
- package/src/schemas/package-type.ts +14 -12
- package/src/schemas/stock-location.ts +2 -3
- package/src/tables/stock-locations.tsx +5 -8
- package/src/lib/money.test.ts +0 -63
- 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
|
+
}
|