@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
|
+
import { Item, ItemActions, ItemContent, ItemDescription, ItemTitle } from '../ui/item'
|
|
2
3
|
import { Thumbnail } from '../ui/thumbnail'
|
|
3
4
|
import { PackageIcon } from './icons'
|
|
4
5
|
|
|
@@ -30,17 +31,17 @@ export type FulfillmentItemRowData = {
|
|
|
30
31
|
*/
|
|
31
32
|
function ItemRow({ row }: { row: FulfillmentItemRowData }) {
|
|
32
33
|
return (
|
|
33
|
-
<
|
|
34
|
+
<Item className="flex-nowrap gap-3 rounded-none border-0 p-3">
|
|
34
35
|
<Thumbnail src={row.thumbnailUrl} fallback={<PackageIcon />} />
|
|
35
36
|
|
|
36
|
-
<
|
|
37
|
-
<
|
|
37
|
+
<ItemContent className="min-w-0 gap-0">
|
|
38
|
+
<ItemTitle className="block truncate">{row.name}</ItemTitle>
|
|
38
39
|
{row.optionsText && (
|
|
39
|
-
<
|
|
40
|
+
<ItemDescription className="line-clamp-1 text-xs">{row.optionsText}</ItemDescription>
|
|
40
41
|
)}
|
|
41
|
-
</
|
|
42
|
+
</ItemContent>
|
|
42
43
|
|
|
43
|
-
<
|
|
44
|
+
<ItemActions className="min-w-0 shrink flex-col items-end gap-0 text-right text-muted-foreground text-sm">
|
|
44
45
|
{row.displayPrice ? (
|
|
45
46
|
<span className="whitespace-nowrap">
|
|
46
47
|
{row.displayPrice} × {row.quantity}
|
|
@@ -48,9 +49,9 @@ function ItemRow({ row }: { row: FulfillmentItemRowData }) {
|
|
|
48
49
|
) : (
|
|
49
50
|
<span className="whitespace-nowrap">× {row.quantity}</span>
|
|
50
51
|
)}
|
|
51
|
-
{row.priceSource && <div className="truncate text-xs">{row.priceSource}</div>}
|
|
52
|
-
</
|
|
53
|
-
</
|
|
52
|
+
{row.priceSource && <div className="max-w-full truncate text-xs">{row.priceSource}</div>}
|
|
53
|
+
</ItemActions>
|
|
54
|
+
</Item>
|
|
54
55
|
)
|
|
55
56
|
}
|
|
56
57
|
|
package/src/spree/icons.ts
CHANGED
|
@@ -18,11 +18,14 @@ export {
|
|
|
18
18
|
BanIcon,
|
|
19
19
|
BanknoteIcon,
|
|
20
20
|
BarChart3Icon as AnalyticsIcon,
|
|
21
|
+
BellIcon,
|
|
21
22
|
BoldIcon,
|
|
22
23
|
BookOpenIcon,
|
|
24
|
+
BotIcon,
|
|
23
25
|
BoxesIcon,
|
|
24
26
|
BracesIcon,
|
|
25
27
|
Building2Icon,
|
|
28
|
+
CalculatorIcon,
|
|
26
29
|
CalendarIcon,
|
|
27
30
|
CalendarOffIcon,
|
|
28
31
|
ChartColumnIcon,
|
|
@@ -35,6 +38,7 @@ export {
|
|
|
35
38
|
ChevronsLeftIcon,
|
|
36
39
|
ChevronsRightIcon,
|
|
37
40
|
ChevronsUpDownIcon,
|
|
41
|
+
CircleAlertIcon,
|
|
38
42
|
CircleCheckIcon,
|
|
39
43
|
CircleDotIcon,
|
|
40
44
|
CircleIcon,
|
|
@@ -100,18 +100,11 @@ export function CreateReturnDialog({
|
|
|
100
100
|
|
|
101
101
|
/** A line the claim can name, with what the customer paid for it. */
|
|
102
102
|
export type ClaimableLine = PostSaleUnit & {
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
/** What the customer paid for `quantity` units of the line, or blank when that is unknown. */
|
|
110
|
-
function paidForUnits(line: ClaimableLine, quantity: number): string {
|
|
111
|
-
const paid = Number(line.discountedAmount) + Number(line.additionalTaxTotal ?? 0)
|
|
112
|
-
if (quantity <= 0 || line.discountedAmount == null || !Number.isFinite(paid)) return ''
|
|
113
|
-
|
|
114
|
-
return ((paid * quantity) / line.quantity).toFixed(2)
|
|
103
|
+
/**
|
|
104
|
+
* What the customer paid, tax included, for that many units of the line, as
|
|
105
|
+
* a decimal string — the default refund. Blank when it is unknown.
|
|
106
|
+
*/
|
|
107
|
+
paidFor?: (quantity: number) => string
|
|
115
108
|
}
|
|
116
109
|
|
|
117
110
|
/**
|
|
@@ -189,7 +182,7 @@ export function CreateClaimDialog({
|
|
|
189
182
|
if (!typedAmounts[line.id]) {
|
|
190
183
|
setAmounts((current) => ({
|
|
191
184
|
...current,
|
|
192
|
-
[line.id]:
|
|
185
|
+
[line.id]: quantity > 0 ? (line.paidFor?.(quantity) ?? '') : '',
|
|
193
186
|
}))
|
|
194
187
|
}
|
|
195
188
|
}}
|
|
@@ -207,7 +200,7 @@ export function CreateClaimDialog({
|
|
|
207
200
|
<InputGroupInput
|
|
208
201
|
id={`claim-amount-${line.id}`}
|
|
209
202
|
type="number"
|
|
210
|
-
step="
|
|
203
|
+
step="any"
|
|
211
204
|
min="0"
|
|
212
205
|
value={amounts[line.id] ?? ''}
|
|
213
206
|
onChange={(event) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useTranslation } from 'react-i18next'
|
|
2
2
|
import { Field, FieldLabel } from '../ui/field'
|
|
3
3
|
import { Input } from '../ui/input'
|
|
4
|
+
import { Item, ItemActions, ItemContent, ItemTitle } from '../ui/item'
|
|
4
5
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
|
5
6
|
|
|
6
7
|
/** A unit on the order that a return, exchange or claim can name. */
|
|
@@ -31,26 +32,27 @@ export function QuantityPicker({
|
|
|
31
32
|
return (
|
|
32
33
|
<div className="flex flex-col gap-2">
|
|
33
34
|
{units.map((unit) => (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
35
|
+
<Item key={unit.id} variant="outline" className="flex-nowrap border-border-subtle">
|
|
36
|
+
<ItemContent className="min-w-0">
|
|
37
|
+
<ItemTitle className="block truncate font-normal">{unit.label}</ItemTitle>
|
|
38
|
+
</ItemContent>
|
|
39
|
+
<ItemActions>
|
|
40
|
+
<Input
|
|
41
|
+
type="number"
|
|
42
|
+
min={0}
|
|
43
|
+
max={unit.quantity}
|
|
44
|
+
className="w-20"
|
|
45
|
+
value={selection[unit.id] ?? 0}
|
|
46
|
+
aria-label={unit.label}
|
|
47
|
+
onChange={(event) =>
|
|
48
|
+
onChange({
|
|
49
|
+
...selection,
|
|
50
|
+
[unit.id]: Math.max(0, Math.min(Number(event.target.value), unit.quantity)),
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
/>
|
|
54
|
+
</ItemActions>
|
|
55
|
+
</Item>
|
|
54
56
|
))}
|
|
55
57
|
</div>
|
|
56
58
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Combobox,
|
|
4
4
|
ComboboxContent,
|
|
@@ -84,14 +84,30 @@ export function ResourceCombobox<T extends ComboboxOption>({
|
|
|
84
84
|
disabled,
|
|
85
85
|
id,
|
|
86
86
|
}: ResourceComboboxProps<T>) {
|
|
87
|
-
|
|
87
|
+
// The picked record is remembered so it stays selected while `items` is
|
|
88
|
+
// between fetches. Without it, the selection drops out of the list for a
|
|
89
|
+
// moment, Base UI reads that as the value being cleared, empties the input,
|
|
90
|
+
// and the field flickers between labels until the queries settle.
|
|
91
|
+
const [picked, setPicked] = useState<T | null>(null)
|
|
92
|
+
const selected =
|
|
93
|
+
items.find((r) => r.id === value) ?? (picked && picked.id === value ? picked : null)
|
|
88
94
|
|
|
89
95
|
return (
|
|
90
96
|
<Combobox
|
|
91
97
|
items={items}
|
|
92
98
|
value={selected}
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
// Compared by id: a refetch hands back equal records as new objects.
|
|
100
|
+
isItemEqualToValue={(item: T, current: T) => item.id === current.id}
|
|
101
|
+
onValueChange={(record: T | null) => {
|
|
102
|
+
setPicked(record)
|
|
103
|
+
onChange(record?.id, record)
|
|
104
|
+
}}
|
|
105
|
+
// Picking an option writes its label into the input. That is not a
|
|
106
|
+
// search, so the query goes back to the empty list instead of searching
|
|
107
|
+
// for the full label of what was just chosen.
|
|
108
|
+
onInputValueChange={(next, details) =>
|
|
109
|
+
onInputChange?.(details.reason === 'item-press' ? '' : next)
|
|
110
|
+
}
|
|
95
111
|
itemToStringLabel={(record: T | null) => (record ? getOptionLabel(record) : '')}
|
|
96
112
|
itemToStringValue={(record: T | null) => record?.id ?? ''}
|
|
97
113
|
// Filtering is server-side: the caller's `onInputChange` already narrows
|
|
@@ -139,49 +139,6 @@ export function ReturnReceiveDialog({
|
|
|
139
139
|
)
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
/** True when a received return is owed nothing, so completing it moves no money. */
|
|
143
|
-
export function returnOwesNothing(refundableTotal: string): boolean {
|
|
144
|
-
return Number(refundableTotal) === 0
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export type ReturnRefundFigures = {
|
|
148
|
-
status: string
|
|
149
|
-
refund_total: string
|
|
150
|
-
display_refund_total: string
|
|
151
|
-
refunded_total: string
|
|
152
|
-
display_refunded_total: string
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* What a return's card reports: what it is owed until money goes back, then
|
|
157
|
-
* what actually went back — which a merchant keeping a restocking fee makes
|
|
158
|
-
* less than it was owed, so that case also names the full amount. Money that
|
|
159
|
-
* went back counts even before the return is marked refunded: a refund that
|
|
160
|
-
* succeeded on one payment and was declined on the next leaves it received.
|
|
161
|
-
*/
|
|
162
|
-
export function returnRefundSummary(
|
|
163
|
-
returnRecord: ReturnRefundFigures,
|
|
164
|
-
):
|
|
165
|
-
| { kind: 'owed'; amount: string }
|
|
166
|
-
| { kind: 'refunded'; amount: string }
|
|
167
|
-
| { kind: 'refunded_short'; amount: string; total: string } {
|
|
168
|
-
const refunded = Number(returnRecord.refunded_total)
|
|
169
|
-
|
|
170
|
-
if (returnRecord.status !== 'refunded' && refunded === 0) {
|
|
171
|
-
return { kind: 'owed', amount: returnRecord.display_refund_total }
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (refunded < Number(returnRecord.refund_total)) {
|
|
175
|
-
return {
|
|
176
|
-
kind: 'refunded_short',
|
|
177
|
-
amount: returnRecord.display_refunded_total,
|
|
178
|
-
total: returnRecord.display_refund_total,
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return { kind: 'refunded', amount: returnRecord.display_refunded_total }
|
|
183
|
-
}
|
|
184
|
-
|
|
185
142
|
/**
|
|
186
143
|
* Gives the money back: how much, and by what means. A return owed nothing
|
|
187
144
|
* (a free gift sent back) is completed instead, with nothing to choose.
|
|
@@ -193,6 +150,7 @@ export function returnRefundSummary(
|
|
|
193
150
|
*/
|
|
194
151
|
export function ReturnRefundDialog({
|
|
195
152
|
refundableTotal,
|
|
153
|
+
owesNothing,
|
|
196
154
|
refundTaxTotal,
|
|
197
155
|
currencySymbol,
|
|
198
156
|
onClose,
|
|
@@ -200,6 +158,8 @@ export function ReturnRefundDialog({
|
|
|
200
158
|
pending = false,
|
|
201
159
|
}: {
|
|
202
160
|
refundableTotal: string
|
|
161
|
+
/** True when the return is owed nothing, so it is completed rather than refunded. */
|
|
162
|
+
owesNothing: boolean
|
|
203
163
|
refundTaxTotal?: string
|
|
204
164
|
currencySymbol: string
|
|
205
165
|
onClose: () => void
|
|
@@ -209,7 +169,6 @@ export function ReturnRefundDialog({
|
|
|
209
169
|
const { t } = useTranslation()
|
|
210
170
|
const [refundMethod, setRefundMethod] = useState<RefundMethod>('original_payment')
|
|
211
171
|
const [amount, setAmount] = useState(refundableTotal)
|
|
212
|
-
const owesNothing = returnOwesNothing(refundableTotal)
|
|
213
172
|
|
|
214
173
|
const methodOptions = [
|
|
215
174
|
{
|
|
@@ -250,7 +209,7 @@ export function ReturnRefundDialog({
|
|
|
250
209
|
<InputGroupInput
|
|
251
210
|
id="refund-amount"
|
|
252
211
|
type="number"
|
|
253
|
-
step="
|
|
212
|
+
step="any"
|
|
254
213
|
min="0"
|
|
255
214
|
value={amount}
|
|
256
215
|
onChange={(event) => setAmount(event.target.value)}
|
|
@@ -2,7 +2,8 @@ import { useState } from 'react'
|
|
|
2
2
|
import { useTranslation } from 'react-i18next'
|
|
3
3
|
import { Button } from '../ui/button'
|
|
4
4
|
import { Field, FieldLabel } from '../ui/field'
|
|
5
|
-
import {
|
|
5
|
+
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '../ui/input-group'
|
|
6
|
+
import { EyeIcon, EyeOffIcon } from './icons'
|
|
6
7
|
|
|
7
8
|
// Server masks stored secrets as `••••…` on read so admins can see "yes,
|
|
8
9
|
// something is stored" without exposing the actual value. Detecting that
|
|
@@ -35,7 +36,9 @@ interface SecretInputProps {
|
|
|
35
36
|
* mask (`••••3K9z`) and renders as a read-only badge with a "Replace"
|
|
36
37
|
* button — never as an input, and never with a reveal toggle, because the
|
|
37
38
|
* server does not send the secret and there is nothing to unhide. Replace
|
|
38
|
-
* switches to an empty password input so the admin opts in to rotation;
|
|
39
|
+
* switches to an empty password input so the admin opts in to rotation; that
|
|
40
|
+
* input does carry a reveal toggle, since what it hides is what the admin is
|
|
41
|
+
* typing right now;
|
|
39
42
|
* Cancel reverts to the badge so the next save preserves the existing
|
|
40
43
|
* secret via the backend's masked round-trip guard.
|
|
41
44
|
*
|
|
@@ -57,6 +60,7 @@ export function SecretInput({
|
|
|
57
60
|
// after `onChange('')` has cleared the parent value.
|
|
58
61
|
const [pendingMask, setPendingMask] = useState<string | null>(null)
|
|
59
62
|
const replacing = pendingMask !== null
|
|
63
|
+
const [revealed, setRevealed] = useState(false)
|
|
60
64
|
|
|
61
65
|
// A stored secret always renders as the badge: the server sent a mask,
|
|
62
66
|
// not the value, so there is nothing a reveal toggle could show.
|
|
@@ -93,17 +97,29 @@ export function SecretInput({
|
|
|
93
97
|
<Field>
|
|
94
98
|
<FieldLabel htmlFor={id}>{label}</FieldLabel>
|
|
95
99
|
<div className="flex items-center gap-2">
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
<InputGroup className="flex-1">
|
|
101
|
+
<InputGroupInput
|
|
102
|
+
id={id}
|
|
103
|
+
type={revealed ? 'text' : 'password'}
|
|
104
|
+
autoComplete="new-password"
|
|
105
|
+
spellCheck={false}
|
|
106
|
+
placeholder={
|
|
107
|
+
replacing ? t('admin.components.secret_input.new_value_placeholder') : placeholder
|
|
108
|
+
}
|
|
109
|
+
value={(value as string) ?? ''}
|
|
110
|
+
onChange={(e) => onChange(e.target.value)}
|
|
111
|
+
/>
|
|
112
|
+
<InputGroupAddon align="inline-end">
|
|
113
|
+
<InputGroupButton
|
|
114
|
+
size="icon-xs"
|
|
115
|
+
aria-label={revealed ? t('admin.actions.hide') : t('admin.actions.show')}
|
|
116
|
+
aria-pressed={revealed}
|
|
117
|
+
onClick={() => setRevealed((visible) => !visible)}
|
|
118
|
+
>
|
|
119
|
+
{revealed ? <EyeOffIcon /> : <EyeIcon />}
|
|
120
|
+
</InputGroupButton>
|
|
121
|
+
</InputGroupAddon>
|
|
122
|
+
</InputGroup>
|
|
107
123
|
{replacing && (
|
|
108
124
|
<Button
|
|
109
125
|
type="button"
|
|
@@ -15,17 +15,20 @@ import {
|
|
|
15
15
|
ComboboxInput,
|
|
16
16
|
ComboboxItem,
|
|
17
17
|
ComboboxList,
|
|
18
|
-
DropdownMenu,
|
|
19
|
-
DropdownMenuContent,
|
|
20
|
-
DropdownMenuItem,
|
|
21
|
-
DropdownMenuTrigger,
|
|
22
18
|
Field,
|
|
23
19
|
FieldDescription,
|
|
24
20
|
FieldError,
|
|
25
21
|
FieldGroup,
|
|
26
22
|
FieldLabel,
|
|
27
23
|
Input,
|
|
24
|
+
Item,
|
|
25
|
+
ItemActions,
|
|
26
|
+
ItemContent,
|
|
27
|
+
ItemDescription,
|
|
28
|
+
ItemGroup,
|
|
29
|
+
ItemTitle,
|
|
28
30
|
RelativeTime,
|
|
31
|
+
RowActions,
|
|
29
32
|
Sheet,
|
|
30
33
|
SheetContent,
|
|
31
34
|
SheetDescription,
|
|
@@ -35,7 +38,7 @@ import {
|
|
|
35
38
|
StatusBadge,
|
|
36
39
|
useConfirm,
|
|
37
40
|
} from '../index'
|
|
38
|
-
import { BadgeCheckIcon,
|
|
41
|
+
import { BadgeCheckIcon, PlusIcon } from './icons'
|
|
39
42
|
|
|
40
43
|
/** One selectable registration kind. */
|
|
41
44
|
interface KindOption {
|
|
@@ -153,81 +156,71 @@ export function TaxIdentifiersCard({
|
|
|
153
156
|
<p className="text-muted-foreground text-sm">{t('admin.tax_identifiers.empty')}</p>
|
|
154
157
|
</CardContent>
|
|
155
158
|
) : (
|
|
156
|
-
<CardContent
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
key={identifier.id}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
defaultValue: identifier.kind,
|
|
168
|
-
})}
|
|
169
|
-
</Badge>
|
|
170
|
-
{identifier.validation_status ? (
|
|
171
|
-
<StatusBadge
|
|
172
|
-
status={identifier.validation_status}
|
|
173
|
-
label={t(
|
|
174
|
-
`admin.tax_identifiers.validation_status.${identifier.validation_status}`,
|
|
175
|
-
{ defaultValue: identifier.validation_status },
|
|
176
|
-
)}
|
|
177
|
-
/>
|
|
178
|
-
) : (
|
|
179
|
-
<Badge variant="secondary">
|
|
180
|
-
{t('admin.tax_identifiers.validation_status.not_checked')}
|
|
159
|
+
<CardContent>
|
|
160
|
+
<ItemGroup className="gap-3">
|
|
161
|
+
{identifiers.map((identifier) => (
|
|
162
|
+
<Item key={identifier.id} role="listitem" variant="outline" className="items-start">
|
|
163
|
+
<ItemContent className="min-w-0">
|
|
164
|
+
<ItemTitle className="flex-wrap">
|
|
165
|
+
{identifier.value}
|
|
166
|
+
<Badge variant="outline">
|
|
167
|
+
{t(`admin.tax_identifiers.kinds.${identifier.kind}`, {
|
|
168
|
+
defaultValue: identifier.kind,
|
|
169
|
+
})}
|
|
181
170
|
</Badge>
|
|
171
|
+
{identifier.validation_status ? (
|
|
172
|
+
<StatusBadge
|
|
173
|
+
status={identifier.validation_status}
|
|
174
|
+
label={t(
|
|
175
|
+
`admin.tax_identifiers.validation_status.${identifier.validation_status}`,
|
|
176
|
+
{ defaultValue: identifier.validation_status },
|
|
177
|
+
)}
|
|
178
|
+
/>
|
|
179
|
+
) : (
|
|
180
|
+
<Badge variant="secondary">
|
|
181
|
+
{t('admin.tax_identifiers.validation_status.not_checked')}
|
|
182
|
+
</Badge>
|
|
183
|
+
)}
|
|
184
|
+
</ItemTitle>
|
|
185
|
+
{identifier.validated_at && (
|
|
186
|
+
<ItemDescription className="text-xs">
|
|
187
|
+
<RelativeTime
|
|
188
|
+
iso={identifier.validated_at}
|
|
189
|
+
prefix={t('admin.tax_identifiers.checked_prefix')}
|
|
190
|
+
/>
|
|
191
|
+
</ItemDescription>
|
|
192
|
+
)}
|
|
193
|
+
{!identifier.validatable && (
|
|
194
|
+
<ItemDescription className="text-xs">
|
|
195
|
+
{t('admin.tax_identifiers.not_validatable')}
|
|
196
|
+
</ItemDescription>
|
|
182
197
|
)}
|
|
183
|
-
</
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
198
|
+
</ItemContent>
|
|
199
|
+
|
|
200
|
+
{canEdit && (
|
|
201
|
+
<ItemActions>
|
|
202
|
+
<RowActions
|
|
203
|
+
actions={[
|
|
204
|
+
{
|
|
205
|
+
key: 'validate',
|
|
206
|
+
label: t('admin.tax_identifiers.validate_action'),
|
|
207
|
+
icon: <BadgeCheckIcon className="size-4" />,
|
|
208
|
+
disabled: !identifier.validatable || mutations.isValidating,
|
|
209
|
+
onSelect: () => mutations.validate(identifier.id).catch(() => undefined),
|
|
210
|
+
},
|
|
211
|
+
{ key: 'edit', onSelect: () => setEditing(identifier) },
|
|
212
|
+
{
|
|
213
|
+
key: 'delete',
|
|
214
|
+
destructive: true,
|
|
215
|
+
onSelect: () => handleDelete(identifier),
|
|
216
|
+
},
|
|
217
|
+
]}
|
|
189
218
|
/>
|
|
190
|
-
</
|
|
191
|
-
)}
|
|
192
|
-
{!identifier.validatable && (
|
|
193
|
-
<span className="text-muted-foreground text-xs">
|
|
194
|
-
{t('admin.tax_identifiers.not_validatable')}
|
|
195
|
-
</span>
|
|
219
|
+
</ItemActions>
|
|
196
220
|
)}
|
|
197
|
-
</
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
<DropdownMenu>
|
|
201
|
-
<DropdownMenuTrigger asChild>
|
|
202
|
-
<Button variant="ghost" size="icon-xs">
|
|
203
|
-
<EllipsisVerticalIcon className="size-4" />
|
|
204
|
-
<span className="sr-only">{t('admin.actions.actions_menu')}</span>
|
|
205
|
-
</Button>
|
|
206
|
-
</DropdownMenuTrigger>
|
|
207
|
-
<DropdownMenuContent align="end">
|
|
208
|
-
<DropdownMenuItem
|
|
209
|
-
disabled={!identifier.validatable || mutations.isValidating}
|
|
210
|
-
onClick={() => mutations.validate(identifier.id).catch(() => undefined)}
|
|
211
|
-
>
|
|
212
|
-
<BadgeCheckIcon className="size-4" />
|
|
213
|
-
{t('admin.tax_identifiers.validate_action')}
|
|
214
|
-
</DropdownMenuItem>
|
|
215
|
-
<DropdownMenuItem onClick={() => setEditing(identifier)}>
|
|
216
|
-
<PencilIcon className="size-4" />
|
|
217
|
-
{t('admin.actions.edit')}
|
|
218
|
-
</DropdownMenuItem>
|
|
219
|
-
<DropdownMenuItem
|
|
220
|
-
className="text-destructive focus:text-destructive"
|
|
221
|
-
onClick={() => handleDelete(identifier)}
|
|
222
|
-
>
|
|
223
|
-
<TrashIcon className="size-4" />
|
|
224
|
-
{t('admin.actions.delete')}
|
|
225
|
-
</DropdownMenuItem>
|
|
226
|
-
</DropdownMenuContent>
|
|
227
|
-
</DropdownMenu>
|
|
228
|
-
)}
|
|
229
|
-
</div>
|
|
230
|
-
))}
|
|
221
|
+
</Item>
|
|
222
|
+
))}
|
|
223
|
+
</ItemGroup>
|
|
231
224
|
</CardContent>
|
|
232
225
|
)}
|
|
233
226
|
|
package/src/spree/wizard.tsx
CHANGED
|
@@ -188,13 +188,7 @@ export function Wizard({
|
|
|
188
188
|
}}
|
|
189
189
|
modal
|
|
190
190
|
>
|
|
191
|
-
<DialogContent
|
|
192
|
-
// Edge-to-edge minus a gutter, like the import wizard and the bulk
|
|
193
|
-
// price editor — every inset/translate/max is overridden.
|
|
194
|
-
className="!inset-3 !w-auto !max-w-none !translate-x-0 !translate-y-0 flex flex-col p-0"
|
|
195
|
-
style={{ maxHeight: 'none' }}
|
|
196
|
-
showCloseButton={false}
|
|
197
|
-
>
|
|
191
|
+
<DialogContent fullScreen showCloseButton={false}>
|
|
198
192
|
<DialogHeader className="h-14 shrink-0 flex-row items-center gap-3 space-y-0 border-b border-border-subtle bg-background px-4 py-0">
|
|
199
193
|
<div className="flex min-w-0 items-center gap-3">
|
|
200
194
|
<DialogTitle className="truncate text-base">{title}</DialogTitle>
|
package/src/ui/dialog.tsx
CHANGED
|
@@ -95,20 +95,29 @@ function DialogContent({
|
|
|
95
95
|
className,
|
|
96
96
|
children,
|
|
97
97
|
showCloseButton = true,
|
|
98
|
+
fullScreen = false,
|
|
98
99
|
...props
|
|
99
100
|
}: React.ComponentProps<typeof DialogPrimitive.Popup> & {
|
|
100
101
|
showCloseButton?: boolean
|
|
102
|
+
/**
|
|
103
|
+
* Fills the window minus a small gutter, over an opaque overlay — for
|
|
104
|
+
* wizards and editors that replace the page rather than sit on top of it.
|
|
105
|
+
* A see-through overlay would let the page's own panel edges show around
|
|
106
|
+
* the dialog as a second border.
|
|
107
|
+
*/
|
|
108
|
+
fullScreen?: boolean
|
|
101
109
|
}) {
|
|
102
110
|
return (
|
|
103
111
|
<DialogPortal>
|
|
104
|
-
<DialogOverlay />
|
|
112
|
+
<DialogOverlay className={cn(fullScreen && 'bg-background dark:bg-background')} />
|
|
105
113
|
<DialogPrimitive.Popup
|
|
106
114
|
data-slot="dialog-content"
|
|
107
115
|
className={cn(
|
|
108
116
|
'fixed top-1/2 left-1/2 z-50 flex w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border border-border bg-card text-foreground text-sm shadow-lg duration-100 outline-none sm:max-w-[min(500px,calc(100%-2rem))] data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 transition-[opacity,transform,translate,scale]',
|
|
117
|
+
fullScreen && 'inset-3 w-auto max-w-none translate-x-0 translate-y-0 sm:max-w-none',
|
|
109
118
|
className,
|
|
110
119
|
)}
|
|
111
|
-
style={{ maxHeight: '90vh' }}
|
|
120
|
+
style={fullScreen ? undefined : { maxHeight: '90vh' }}
|
|
112
121
|
{...props}
|
|
113
122
|
>
|
|
114
123
|
{children}
|