@spree/dashboard-ui 1.0.0-rc.2 → 1.0.0-rc.4
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 +23 -21
- package/src/index.ts +2 -1
- package/src/lib/validation-messages.ts +12 -0
- package/src/spree/address-book-row.tsx +37 -51
- package/src/spree/bulk-price-table.tsx +17 -11
- package/src/spree/bulk-variants-table.tsx +12 -5
- package/src/spree/calculator-summary.tsx +39 -34
- package/src/spree/claim-resolve-dialog.tsx +23 -17
- package/src/spree/data-grid/data-grid.tsx +23 -13
- package/src/spree/data-grid/index.ts +1 -1
- package/src/spree/fulfillment-item-list.tsx +10 -9
- package/src/spree/icons.ts +4 -0
- package/src/spree/label-upload-dialog.tsx +1 -1
- package/src/spree/post-sale-create-dialogs.tsx +7 -14
- package/src/spree/post-sale-fields.tsx +22 -20
- package/src/spree/quantity-tier-editor.tsx +1 -1
- package/src/spree/resource-combobox.tsx +20 -4
- package/src/spree/return-dialogs.tsx +4 -45
- package/src/spree/secret-input.tsx +29 -13
- package/src/spree/tax-identifiers-card.tsx +69 -76
- package/src/spree/wizard.tsx +1 -7
- package/src/ui/dialog.tsx +11 -2
- package/src/ui/item.tsx +188 -0
- package/src/ui/toast.tsx +99 -45
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spree/dashboard-ui",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.4",
|
|
5
5
|
"description": "Spree Dashboard design system — shadcn primitives, headless composed components, and design tokens. Source-only package; consumers compile via their own Vite/Tailwind setup. Developer Preview.",
|
|
6
6
|
"author": "Vendo Connect Inc.",
|
|
7
7
|
"homepage": "https://spreecommerce.org",
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
"LICENSE"
|
|
38
38
|
],
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@tanstack/react-hotkeys": "^0.10.0",
|
|
41
40
|
"@tanstack/react-router": "^1.169.2",
|
|
42
41
|
"i18next": "^26.2.0",
|
|
43
42
|
"react": "^19.2.6",
|
|
@@ -60,37 +59,40 @@
|
|
|
60
59
|
"@dnd-kit/core": "6.3.1",
|
|
61
60
|
"@dnd-kit/sortable": "10.0.0",
|
|
62
61
|
"@dnd-kit/utilities": "3.2.2",
|
|
63
|
-
"@fontsource-variable/geist": "5.
|
|
64
|
-
"@fontsource-variable/geist-mono": "5.
|
|
65
|
-
"@tanstack/react-
|
|
66
|
-
"@
|
|
67
|
-
"@tiptap/
|
|
68
|
-
"@tiptap/extension-
|
|
69
|
-
"@tiptap/extension-
|
|
70
|
-
"@tiptap/
|
|
71
|
-
"@tiptap/
|
|
72
|
-
"@tiptap/
|
|
62
|
+
"@fontsource-variable/geist": "5.3.0",
|
|
63
|
+
"@fontsource-variable/geist-mono": "5.3.0",
|
|
64
|
+
"@tanstack/react-hotkeys": "^0.14.3",
|
|
65
|
+
"@tanstack/react-table": "9.3.1",
|
|
66
|
+
"@tiptap/core": "3.32.0",
|
|
67
|
+
"@tiptap/extension-bubble-menu": "3.32.0",
|
|
68
|
+
"@tiptap/extension-floating-menu": "3.32.0",
|
|
69
|
+
"@tiptap/extension-image": "3.32.0",
|
|
70
|
+
"@tiptap/extension-link": "3.32.0",
|
|
71
|
+
"@tiptap/extension-placeholder": "3.32.0",
|
|
72
|
+
"@tiptap/pm": "3.32.0",
|
|
73
|
+
"@tiptap/react": "3.32.0",
|
|
74
|
+
"@tiptap/starter-kit": "3.32.0",
|
|
73
75
|
"@uiw/react-json-view": "1.12.2",
|
|
74
76
|
"class-variance-authority": "0.7.1",
|
|
75
77
|
"clsx": "2.1.1",
|
|
76
78
|
"cmdk": "1.1.1",
|
|
77
|
-
"date-fns": "4.
|
|
79
|
+
"date-fns": "4.4.0",
|
|
78
80
|
"date-fns-tz": "3.2.0",
|
|
79
|
-
"flag-icons": "7.
|
|
80
|
-
"lucide-react": "1.
|
|
81
|
-
"react-colorful": "5.
|
|
82
|
-
"react-day-picker": "
|
|
81
|
+
"flag-icons": "7.6.0",
|
|
82
|
+
"lucide-react": "1.58.0",
|
|
83
|
+
"react-colorful": "5.8.1",
|
|
84
|
+
"react-day-picker": "10.0.2",
|
|
83
85
|
"recharts": "3.10.1",
|
|
84
|
-
"shadcn": "4.
|
|
85
|
-
"tailwind-merge": "3.
|
|
86
|
+
"shadcn": "4.22.0",
|
|
87
|
+
"tailwind-merge": "3.7.0",
|
|
86
88
|
"tw-animate-css": "1.4.0"
|
|
87
89
|
},
|
|
88
90
|
"devDependencies": {
|
|
89
91
|
"@types/react": "^19.2.14",
|
|
90
92
|
"@types/react-dom": "^19.2.3",
|
|
91
93
|
"tailwindcss": "^4.2.4",
|
|
92
|
-
"typescript": "~
|
|
93
|
-
"vite": "^8.
|
|
94
|
+
"typescript": "~7.0.2",
|
|
95
|
+
"vite": "^8.3.4"
|
|
94
96
|
},
|
|
95
97
|
"scripts": {
|
|
96
98
|
"lint": "biome check src",
|
package/src/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ export {
|
|
|
26
26
|
} from './lib/date-range-presets'
|
|
27
27
|
export { sameRichText, shouldEmitRichTextChange } from './lib/same-rich-text'
|
|
28
28
|
export { cn } from './lib/utils'
|
|
29
|
-
export { requiredMessage } from './lib/validation-messages'
|
|
29
|
+
export { decimalPlacesMessage, requiredMessage } from './lib/validation-messages'
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
// Spree composed components — headless, accept data via props
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
@@ -117,6 +117,7 @@ export * from './ui/empty'
|
|
|
117
117
|
export * from './ui/field'
|
|
118
118
|
export * from './ui/input'
|
|
119
119
|
export * from './ui/input-group'
|
|
120
|
+
export * from './ui/item'
|
|
120
121
|
export * from './ui/label'
|
|
121
122
|
export * from './ui/media-preview'
|
|
122
123
|
export * from './ui/pagination'
|
|
@@ -17,3 +17,15 @@ import i18n from 'i18next'
|
|
|
17
17
|
export function requiredMessage(field: string): () => string {
|
|
18
18
|
return () => i18n.t('admin.validation.required', { field: i18n.t(`admin.fields.${field}.label`) })
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Lazy "<Field> can have at most <count> decimal places" message, for amounts
|
|
23
|
+
* and rates the server keeps to a fixed number of decimals.
|
|
24
|
+
*/
|
|
25
|
+
export function decimalPlacesMessage(field: string, count: number): () => string {
|
|
26
|
+
return () =>
|
|
27
|
+
i18n.t('admin.validation.too_many_decimals', {
|
|
28
|
+
field: i18n.t(`admin.fields.${field}.label`),
|
|
29
|
+
count,
|
|
30
|
+
})
|
|
31
|
+
}
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { useTranslation } from 'react-i18next'
|
|
2
2
|
import { Badge } from '../ui/badge'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
DropdownMenu,
|
|
6
|
-
DropdownMenuContent,
|
|
7
|
-
DropdownMenuItem,
|
|
8
|
-
DropdownMenuTrigger,
|
|
9
|
-
} from '../ui/dropdown-menu'
|
|
3
|
+
import { Item, ItemActions, ItemContent, ItemTitle } from '../ui/item'
|
|
10
4
|
import { AddressBlock, type AddressBlockValue } from './address-block'
|
|
11
|
-
import {
|
|
5
|
+
import { RowActions } from './row-actions'
|
|
12
6
|
|
|
13
7
|
/** What every address book row needs, whoever owns the book. */
|
|
14
8
|
export interface AddressBookEntry extends AddressBlockValue {
|
|
@@ -45,52 +39,44 @@ export function AddressBookRow({
|
|
|
45
39
|
const { t } = useTranslation()
|
|
46
40
|
|
|
47
41
|
return (
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
42
|
+
<Item variant="outline" className="items-start">
|
|
43
|
+
<ItemContent className="min-w-0">
|
|
44
|
+
{(address.label || address.is_default_billing || address.is_default_shipping) && (
|
|
45
|
+
<ItemTitle className="flex-wrap">
|
|
46
|
+
{address.label}
|
|
47
|
+
{address.is_default_billing && (
|
|
48
|
+
<Badge variant="outline">{t('admin.address_book.default_billing')}</Badge>
|
|
49
|
+
)}
|
|
50
|
+
{address.is_default_shipping && (
|
|
51
|
+
<Badge variant="outline">{t('admin.address_book.default_shipping')}</Badge>
|
|
52
|
+
)}
|
|
53
|
+
</ItemTitle>
|
|
54
|
+
)}
|
|
61
55
|
<AddressBlock address={address} />
|
|
62
|
-
</
|
|
56
|
+
</ItemContent>
|
|
63
57
|
|
|
64
58
|
{canEdit && (
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
</DropdownMenuItem>
|
|
86
|
-
)}
|
|
87
|
-
<DropdownMenuItem variant="destructive" onClick={onRemove}>
|
|
88
|
-
<TrashIcon className="size-4" />
|
|
89
|
-
{t('admin.actions.delete')}
|
|
90
|
-
</DropdownMenuItem>
|
|
91
|
-
</DropdownMenuContent>
|
|
92
|
-
</DropdownMenu>
|
|
59
|
+
<ItemActions>
|
|
60
|
+
<RowActions
|
|
61
|
+
actions={[
|
|
62
|
+
{ key: 'edit', onSelect: onEdit },
|
|
63
|
+
{
|
|
64
|
+
key: 'set_default_billing',
|
|
65
|
+
label: t('admin.address_book.set_default_billing'),
|
|
66
|
+
visible: !!onSetDefaultBilling && !address.is_default_billing,
|
|
67
|
+
onSelect: () => onSetDefaultBilling?.(),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: 'set_default_shipping',
|
|
71
|
+
label: t('admin.address_book.set_default_shipping'),
|
|
72
|
+
visible: !!onSetDefaultShipping && !address.is_default_shipping,
|
|
73
|
+
onSelect: () => onSetDefaultShipping?.(),
|
|
74
|
+
},
|
|
75
|
+
{ key: 'delete', destructive: true, onSelect: onRemove },
|
|
76
|
+
]}
|
|
77
|
+
/>
|
|
78
|
+
</ItemActions>
|
|
93
79
|
)}
|
|
94
|
-
</
|
|
80
|
+
</Item>
|
|
95
81
|
)
|
|
96
82
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import type { ColumnDef } from '@tanstack/react-table'
|
|
2
1
|
import { PlusIcon, XIcon } from 'lucide-react'
|
|
3
2
|
import { useMemo } from 'react'
|
|
3
|
+
import { Alert, AlertDescription } from '../ui/alert'
|
|
4
4
|
import { Button } from '../ui/button'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DataGrid,
|
|
7
|
+
type DataGridColumn,
|
|
8
|
+
DecimalCell,
|
|
9
|
+
editableRowIndex,
|
|
10
|
+
MoneyCell,
|
|
11
|
+
ReadOnlyCell,
|
|
12
|
+
} from './data-grid'
|
|
13
|
+
import { CircleAlertIcon } from './icons'
|
|
6
14
|
import { SearchInput } from './search-input'
|
|
7
15
|
|
|
8
16
|
export interface BulkPriceRow {
|
|
@@ -131,7 +139,7 @@ export function BulkPriceTable({
|
|
|
131
139
|
isLoading,
|
|
132
140
|
error,
|
|
133
141
|
}: BulkPriceTableProps) {
|
|
134
|
-
const columns = useMemo<
|
|
142
|
+
const columns = useMemo<DataGridColumn<BulkPriceRow>[]>(
|
|
135
143
|
() => [
|
|
136
144
|
{
|
|
137
145
|
id: 'variant',
|
|
@@ -203,7 +211,7 @@ export function BulkPriceTable({
|
|
|
203
211
|
/>
|
|
204
212
|
)
|
|
205
213
|
},
|
|
206
|
-
} satisfies
|
|
214
|
+
} satisfies DataGridColumn<BulkPriceRow>,
|
|
207
215
|
]
|
|
208
216
|
: []),
|
|
209
217
|
{
|
|
@@ -291,7 +299,7 @@ export function BulkPriceTable({
|
|
|
291
299
|
</Button>
|
|
292
300
|
)
|
|
293
301
|
},
|
|
294
|
-
} satisfies
|
|
302
|
+
} satisfies DataGridColumn<BulkPriceRow>,
|
|
295
303
|
]
|
|
296
304
|
: []),
|
|
297
305
|
],
|
|
@@ -312,12 +320,10 @@ export function BulkPriceTable({
|
|
|
312
320
|
// table in a card does.
|
|
313
321
|
<div className="flex h-full flex-col gap-3">
|
|
314
322
|
{error && (
|
|
315
|
-
<
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
>
|
|
319
|
-
{error}
|
|
320
|
-
</div>
|
|
323
|
+
<Alert variant="destructive" className="mx-3 w-auto shrink-0">
|
|
324
|
+
<CircleAlertIcon />
|
|
325
|
+
<AlertDescription>{error}</AlertDescription>
|
|
326
|
+
</Alert>
|
|
321
327
|
)}
|
|
322
328
|
{showToolbar && (
|
|
323
329
|
// Always-mounted toolbar. Conditionally rendering the search input
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import type { ColumnDef } from '@tanstack/react-table'
|
|
2
1
|
import { useMemo } from 'react'
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DataGrid,
|
|
4
|
+
type DataGridColumn,
|
|
5
|
+
DecimalCell,
|
|
6
|
+
ReadOnlyCell,
|
|
7
|
+
SelectCell,
|
|
8
|
+
SwitchCell,
|
|
9
|
+
TextCell,
|
|
10
|
+
} from './data-grid'
|
|
4
11
|
|
|
5
12
|
export interface BulkVariantsRow {
|
|
6
13
|
id: string
|
|
@@ -139,7 +146,7 @@ export function BulkVariantsTable({
|
|
|
139
146
|
cartonOptions,
|
|
140
147
|
onChange,
|
|
141
148
|
}: BulkVariantsTableProps) {
|
|
142
|
-
const columns = useMemo<
|
|
149
|
+
const columns = useMemo<DataGridColumn<BulkVariantsRow>[]>(() => {
|
|
143
150
|
// Keyboard column coordinates, fixed per column: variant label is
|
|
144
151
|
// read-only (unregistered), then sku=1, barcode=2, weight=3, unit=4,
|
|
145
152
|
// height=5, width=6, depth=7, unit=8, preorderable=9, backorder=10,
|
|
@@ -148,7 +155,7 @@ export function BulkVariantsTable({
|
|
|
148
155
|
const numericHeader = (label: string) => () => <span className="block text-right">{label}</span>
|
|
149
156
|
const rowLabel = (r: BulkVariantsRow) => r.variantLabel ?? labels.variantDefault
|
|
150
157
|
|
|
151
|
-
const defs:
|
|
158
|
+
const defs: DataGridColumn<BulkVariantsRow>[] = [
|
|
152
159
|
{
|
|
153
160
|
id: 'variant',
|
|
154
161
|
header: labels.variant,
|
|
@@ -182,7 +189,7 @@ export function BulkVariantsTable({
|
|
|
182
189
|
/>
|
|
183
190
|
),
|
|
184
191
|
},
|
|
185
|
-
...DECIMAL_COLUMNS.map<
|
|
192
|
+
...DECIMAL_COLUMNS.map<DataGridColumn<BulkVariantsRow>>(({ field, col }) => ({
|
|
186
193
|
id: field,
|
|
187
194
|
header: numericHeader(labels[field]),
|
|
188
195
|
cell: ({ row }) => (
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* model (delivery methods, tax rates, …).
|
|
6
6
|
*
|
|
7
7
|
* The expected payload mirrors the admin API's calculator embed: a
|
|
8
|
-
* `{ label, preferences,
|
|
8
|
+
* `{ label, preferences, schema }` triple, `schema` being the JSON Schema of
|
|
9
|
+
* the calculator's preferences. The component is
|
|
9
10
|
* tolerant of partial input — missing schema or preferences just fall
|
|
10
11
|
* back to the label alone.
|
|
11
12
|
*
|
|
@@ -22,7 +23,14 @@ export interface CalculatorPayload {
|
|
|
22
23
|
/** STI class name (e.g. "Spree::Calculator::FlatRate"). Used as a label fallback. */
|
|
23
24
|
type?: string | null
|
|
24
25
|
preferences?: Record<string, unknown> | null
|
|
25
|
-
|
|
26
|
+
schema?: { properties?: Record<string, CalculatorPropertySchema> } | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The part of a preference's JSON Schema the summary reads. */
|
|
30
|
+
interface CalculatorPropertySchema {
|
|
31
|
+
type?: string | string[]
|
|
32
|
+
format?: string
|
|
33
|
+
pattern?: string
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
interface CalculatorSummaryProps {
|
|
@@ -60,14 +68,14 @@ export function formatCalculatorSummary(
|
|
|
60
68
|
calculator.label?.trim() ||
|
|
61
69
|
(calculator.type ? (calculator.type.split('::').pop() ?? calculator.type) : '')
|
|
62
70
|
|
|
63
|
-
const
|
|
71
|
+
const properties = Object.entries(calculator.schema?.properties ?? {})
|
|
64
72
|
const prefs = calculator.preferences
|
|
65
73
|
|
|
66
|
-
if (!
|
|
74
|
+
if (!properties.length || !prefs) return label || null
|
|
67
75
|
|
|
68
76
|
const currency = (typeof prefs.currency === 'string' && prefs.currency) || 'USD'
|
|
69
|
-
const details =
|
|
70
|
-
.map((
|
|
77
|
+
const details = properties
|
|
78
|
+
.map(([key, property]) => formatField(key, property, prefs[key], currency))
|
|
71
79
|
.filter((s): s is string => s !== null)
|
|
72
80
|
.join(', ')
|
|
73
81
|
|
|
@@ -76,50 +84,47 @@ export function formatCalculatorSummary(
|
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
/**
|
|
79
|
-
* Formats one preference value
|
|
80
|
-
* carry richer field metadata (no `display_format`, no `unit`), so we
|
|
81
|
-
* fall back to key-name heuristics that match the Ruby preference
|
|
82
|
-
* declarations in `Spree::Calculator::*`.
|
|
87
|
+
* Formats one preference value from its schema.
|
|
83
88
|
*
|
|
84
|
-
* -
|
|
85
|
-
* - `*_percent`
|
|
86
|
-
* -
|
|
89
|
+
* - money → currency-formatted.
|
|
90
|
+
* - `*_percent` values → `N%`.
|
|
91
|
+
* - currencies → omitted (folded into the money formatting).
|
|
87
92
|
* - booleans → humanized key when true; skipped when false.
|
|
88
|
-
* - lists (tiers) → their length;
|
|
93
|
+
* - lists (tiers) → their length; objects → omitted.
|
|
89
94
|
* - everything else → `key: value`.
|
|
90
95
|
*/
|
|
91
96
|
function formatField(
|
|
92
|
-
|
|
97
|
+
key: string,
|
|
98
|
+
property: CalculatorPropertySchema,
|
|
93
99
|
value: unknown,
|
|
94
100
|
currency: string,
|
|
95
101
|
): string | null {
|
|
96
102
|
if (value === null || value === undefined || value === '') return null
|
|
97
|
-
if (
|
|
103
|
+
if (property.format === 'currency') return null
|
|
104
|
+
if (property.format === 'money') return formatMoney(value, currency)
|
|
105
|
+
if (/(?:^|_)percent$/.test(key)) return `${value}%`
|
|
98
106
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return `${humanize(
|
|
105
|
-
}
|
|
106
|
-
if (field.type === 'boolean') {
|
|
107
|
-
return value ? humanize(field.key) : null
|
|
108
|
-
}
|
|
109
|
-
if (field.type === 'array') {
|
|
110
|
-
return Array.isArray(value) && value.length ? `${humanize(field.key)}: ${value.length}` : null
|
|
107
|
+
const type = (Array.isArray(property.type) ? property.type : [property.type]).find(
|
|
108
|
+
(t) => t !== 'null',
|
|
109
|
+
)
|
|
110
|
+
if (type === 'boolean') return value ? humanize(key) : null
|
|
111
|
+
if (type === 'array') {
|
|
112
|
+
return Array.isArray(value) && value.length ? `${humanize(key)}: ${value.length}` : null
|
|
111
113
|
}
|
|
112
|
-
if (
|
|
113
|
-
return `${humanize(
|
|
114
|
+
if (type === 'object') return null
|
|
115
|
+
return `${humanize(key)}: ${value}`
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
function formatMoney(value: unknown, currency: string): string {
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
+
const amount = String(value)
|
|
120
|
+
if (!/^-?\d+(\.\d+)?$/.test(amount)) return amount
|
|
119
121
|
try {
|
|
120
|
-
|
|
122
|
+
// `Intl` reads a numeric string exactly; its ES2022 typings only list numbers.
|
|
123
|
+
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(
|
|
124
|
+
amount as unknown as number,
|
|
125
|
+
)
|
|
121
126
|
} catch {
|
|
122
|
-
return `${
|
|
127
|
+
return `${amount} ${currency}`
|
|
123
128
|
}
|
|
124
129
|
}
|
|
125
130
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../ui/dialog'
|
|
12
12
|
import { Field, FieldLabel } from '../ui/field'
|
|
13
13
|
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from '../ui/input-group'
|
|
14
|
+
import { Item, ItemActions, ItemContent, ItemTitle } from '../ui/item'
|
|
14
15
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
|
15
16
|
import { Switch } from '../ui/switch'
|
|
16
17
|
import type { RefundMethod } from './return-dialogs'
|
|
@@ -93,8 +94,11 @@ export function ClaimResolveDialog({
|
|
|
93
94
|
|
|
94
95
|
// Refunding nothing, or replacing nothing, is what the server rejects —
|
|
95
96
|
// say so here instead of letting the request fail.
|
|
97
|
+
// The amount is the canonical decimal string a number input yields, so
|
|
98
|
+
// "above zero" is unsigned digits with a non-zero digit among them.
|
|
99
|
+
const positiveAmount = /^(\d+(\.\d+)?|\.\d+)$/.test(amount) && /[1-9]/.test(amount)
|
|
96
100
|
const ready =
|
|
97
|
-
(!refunding ||
|
|
101
|
+
(!refunding || positiveAmount) && (!sendingReplacement || chosenReplacements.length > 0)
|
|
98
102
|
|
|
99
103
|
return (
|
|
100
104
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
|
@@ -138,7 +142,7 @@ export function ClaimResolveDialog({
|
|
|
138
142
|
<InputGroupInput
|
|
139
143
|
id="claim-resolve-amount"
|
|
140
144
|
type="number"
|
|
141
|
-
step="
|
|
145
|
+
step="any"
|
|
142
146
|
min="0"
|
|
143
147
|
value={amount}
|
|
144
148
|
onChange={(event) => setAmount(event.target.value)}
|
|
@@ -175,21 +179,23 @@ export function ClaimResolveDialog({
|
|
|
175
179
|
{t('admin.pages.orders.detail.claims.replacement_items')}
|
|
176
180
|
</span>
|
|
177
181
|
{lines.map((line) => (
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
182
|
+
<Item key={line.id} variant="outline" className="flex-nowrap border-border-subtle">
|
|
183
|
+
<ItemContent className="min-w-0">
|
|
184
|
+
<ItemTitle className="block truncate font-normal">
|
|
185
|
+
{line.label}
|
|
186
|
+
<span className="text-muted-foreground"> ×{line.quantity}</span>
|
|
187
|
+
</ItemTitle>
|
|
188
|
+
</ItemContent>
|
|
189
|
+
<ItemActions>
|
|
190
|
+
<Switch
|
|
191
|
+
aria-label={line.label}
|
|
192
|
+
checked={replacing[line.id] ?? false}
|
|
193
|
+
onCheckedChange={(checked) =>
|
|
194
|
+
setReplacing({ ...replacing, [line.id]: checked })
|
|
195
|
+
}
|
|
196
|
+
/>
|
|
197
|
+
</ItemActions>
|
|
198
|
+
</Item>
|
|
193
199
|
))}
|
|
194
200
|
</div>
|
|
195
201
|
)}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ColumnDef,
|
|
2
3
|
flexRender,
|
|
3
|
-
getCoreRowModel,
|
|
4
4
|
type Row,
|
|
5
|
+
type RowData,
|
|
5
6
|
type Table,
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
tableFeatures,
|
|
8
|
+
useTable,
|
|
8
9
|
} from '@tanstack/react-table'
|
|
9
10
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
10
11
|
import { cn } from '../../lib/utils'
|
|
@@ -14,9 +15,18 @@ import type { CellCoords, CellKey, CellRegistration, RenderSectionHeader } from
|
|
|
14
15
|
import { cellKey } from './types'
|
|
15
16
|
import { useDataGridKeyboard } from './use-data-grid-keyboard'
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
// The grid renders rows as given — no sorting, filtering or column state — so
|
|
19
|
+
// it needs only the table's core features.
|
|
20
|
+
const dataGridFeatures = tableFeatures({})
|
|
21
|
+
type DataGridFeatures = typeof dataGridFeatures
|
|
22
|
+
|
|
23
|
+
/** A column of a `DataGrid`. */
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: each column reads a value of its own type
|
|
25
|
+
export type DataGridColumn<T extends RowData> = ColumnDef<DataGridFeatures, T, any>
|
|
26
|
+
|
|
27
|
+
interface DataGridProps<T extends RowData> {
|
|
18
28
|
rows: T[]
|
|
19
|
-
columns:
|
|
29
|
+
columns: DataGridColumn<T>[]
|
|
20
30
|
getRowId: (row: T) => string
|
|
21
31
|
/** Optional grouping. Return a node spanning all columns to render this row
|
|
22
32
|
* as a (non-editable) section header. Return null to render the row as
|
|
@@ -39,7 +49,7 @@ interface DataGridProps<T> {
|
|
|
39
49
|
scrollable?: boolean
|
|
40
50
|
}
|
|
41
51
|
|
|
42
|
-
export function DataGrid<T>({
|
|
52
|
+
export function DataGrid<T extends RowData>({
|
|
43
53
|
rows,
|
|
44
54
|
columns,
|
|
45
55
|
getRowId,
|
|
@@ -48,11 +58,11 @@ export function DataGrid<T>({
|
|
|
48
58
|
'aria-label': ariaLabel,
|
|
49
59
|
scrollable = false,
|
|
50
60
|
}: DataGridProps<T>) {
|
|
51
|
-
const table =
|
|
61
|
+
const table = useTable({
|
|
62
|
+
features: dataGridFeatures,
|
|
52
63
|
data: rows,
|
|
53
64
|
columns,
|
|
54
65
|
getRowId,
|
|
55
|
-
getCoreRowModel: getCoreRowModel(),
|
|
56
66
|
})
|
|
57
67
|
|
|
58
68
|
return (
|
|
@@ -66,14 +76,14 @@ export function DataGrid<T>({
|
|
|
66
76
|
)
|
|
67
77
|
}
|
|
68
78
|
|
|
69
|
-
function DataGridShell<T>({
|
|
79
|
+
function DataGridShell<T extends RowData>({
|
|
70
80
|
table,
|
|
71
81
|
renderSectionHeader,
|
|
72
82
|
className,
|
|
73
83
|
ariaLabel,
|
|
74
84
|
scrollable,
|
|
75
85
|
}: {
|
|
76
|
-
table: Table<T>
|
|
86
|
+
table: Table<DataGridFeatures, T>
|
|
77
87
|
renderSectionHeader?: RenderSectionHeader<T>
|
|
78
88
|
className?: string
|
|
79
89
|
ariaLabel?: string
|
|
@@ -324,12 +334,12 @@ function DataGridShell<T>({
|
|
|
324
334
|
)
|
|
325
335
|
}
|
|
326
336
|
|
|
327
|
-
function DataGridRow<T>({
|
|
337
|
+
function DataGridRow<T extends RowData>({
|
|
328
338
|
row,
|
|
329
339
|
renderSectionHeader,
|
|
330
340
|
columnCount,
|
|
331
341
|
}: {
|
|
332
|
-
row: Row<T>
|
|
342
|
+
row: Row<DataGridFeatures, T>
|
|
333
343
|
renderSectionHeader?: RenderSectionHeader<T>
|
|
334
344
|
columnCount: number
|
|
335
345
|
}) {
|
|
@@ -345,7 +355,7 @@ function DataGridRow<T>({
|
|
|
345
355
|
}
|
|
346
356
|
return (
|
|
347
357
|
<tr>
|
|
348
|
-
{row.
|
|
358
|
+
{row.getAllCells().map((cell) => (
|
|
349
359
|
<td key={cell.id} className="h-9 p-0 align-middle">
|
|
350
360
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
351
361
|
</td>
|
|
@@ -8,6 +8,6 @@ export {
|
|
|
8
8
|
TextCell,
|
|
9
9
|
} from './cells'
|
|
10
10
|
export { useDataGridContext } from './context'
|
|
11
|
-
export { DataGrid } from './data-grid'
|
|
11
|
+
export { DataGrid, type DataGridColumn } from './data-grid'
|
|
12
12
|
export { editableRowIndex } from './helpers'
|
|
13
13
|
export type { CellCoords, RenderSectionHeader } from './types'
|