@spree/dashboard-ui 0.13.0 → 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.
- package/README.md +1 -1
- package/package.json +15 -9
- package/src/hooks/use-prefers-reduced-motion.ts +26 -0
- package/src/hooks/use-scrolled.ts +34 -14
- package/src/index.ts +39 -1
- package/src/lib/date-range-presets.ts +79 -0
- package/src/lib/same-rich-text.ts +145 -0
- package/src/spree/address-block.tsx +5 -4
- package/src/spree/address-book-row.tsx +96 -0
- package/src/spree/address-map.tsx +62 -0
- package/src/spree/back-button.tsx +14 -9
- package/src/spree/bulk-dialog.tsx +2 -2
- package/src/spree/bulk-price-table.tsx +145 -14
- package/src/spree/bulk-variants-table.tsx +434 -0
- package/src/spree/calculator-summary.tsx +5 -1
- package/src/spree/claim-resolve-dialog.tsx +219 -0
- package/src/spree/confirm-dialog.tsx +1 -1
- package/src/spree/copy-to-clipboard-button.tsx +1 -1
- package/src/spree/data-grid/cells.tsx +199 -25
- package/src/spree/data-grid/context.ts +6 -0
- package/src/spree/data-grid/data-grid.tsx +149 -7
- package/src/spree/data-grid/helpers.ts +6 -2
- package/src/spree/data-grid/index.ts +9 -1
- package/src/spree/data-grid/use-data-grid-keyboard.ts +4 -2
- package/src/spree/delivery-form-dialog.tsx +166 -0
- package/src/spree/drag-handle.tsx +2 -2
- package/src/spree/editable-note-card.tsx +103 -0
- package/src/spree/external-link.tsx +56 -0
- package/src/spree/file-type-icon.tsx +68 -0
- package/src/spree/form-actions.tsx +2 -3
- package/src/spree/form-section.tsx +46 -0
- package/src/spree/fulfill-items-form.tsx +282 -0
- package/src/spree/fulfillment-edit-dialog.tsx +211 -0
- package/src/spree/fulfillment-item-list.tsx +61 -0
- package/src/spree/fulfillment-panel.tsx +53 -0
- package/src/spree/icons.ts +167 -0
- package/src/spree/json-preview-drawer.tsx +4 -4
- package/src/spree/label-upload-dialog.tsx +195 -0
- package/src/spree/language-menu-items.tsx +37 -30
- package/src/spree/metadata/metadata-card.tsx +1 -1
- package/src/spree/order-cancel-dialog.tsx +149 -0
- package/src/spree/post-sale-create-dialogs.tsx +253 -0
- package/src/spree/post-sale-fields.tsx +133 -0
- package/src/spree/product-membership-list.tsx +453 -0
- package/src/spree/products/status-card.tsx +79 -0
- package/src/spree/quantity-tier-editor.tsx +162 -0
- package/src/spree/resource-combobox.tsx +12 -1
- package/src/spree/resource-name-cell.tsx +21 -4
- package/src/spree/return-dialogs.tsx +238 -0
- package/src/spree/route-error-boundary.tsx +1 -1
- package/src/spree/row-actions.tsx +2 -2
- package/src/spree/search-input.tsx +76 -0
- package/src/spree/secret-input.tsx +26 -40
- package/src/spree/tax-identifiers-card.tsx +418 -0
- package/src/spree/theme-toggle.tsx +36 -23
- package/src/spree/wizard.tsx +254 -0
- package/src/styles.css +750 -75
- package/src/ui/accordion.tsx +83 -0
- package/src/ui/alert.tsx +79 -0
- package/src/ui/attachment.tsx +1 -1
- package/src/ui/badge.tsx +198 -56
- package/src/ui/breadcrumb.tsx +1 -1
- package/src/ui/button.tsx +22 -10
- package/src/ui/calendar.tsx +1 -1
- package/src/ui/card.tsx +52 -6
- package/src/ui/checkbox.tsx +13 -5
- package/src/ui/combobox.tsx +125 -6
- package/src/ui/command.tsx +149 -23
- package/src/ui/context-menu.tsx +99 -0
- package/src/ui/data-table.tsx +448 -9
- package/src/ui/date-picker.tsx +18 -3
- package/src/ui/date-range-picker.tsx +47 -60
- package/src/ui/dialog.tsx +6 -6
- package/src/ui/dropdown-menu.tsx +18 -11
- package/src/ui/field.tsx +10 -2
- package/src/ui/input-group.tsx +1 -1
- package/src/ui/input.tsx +1 -1
- package/src/ui/map.tsx +2110 -0
- package/src/ui/media-preview.tsx +105 -0
- package/src/ui/pagination.tsx +19 -11
- package/src/ui/popover.tsx +4 -2
- package/src/ui/progress.tsx +5 -2
- package/src/ui/radio-group.tsx +4 -4
- package/src/ui/rich-text-editor.tsx +214 -88
- package/src/ui/scroll-area.tsx +65 -0
- package/src/ui/select.tsx +17 -10
- package/src/ui/separator.tsx +1 -1
- package/src/ui/sheet.tsx +49 -7
- package/src/ui/sidebar.tsx +138 -37
- package/src/ui/skeleton.tsx +3 -1
- package/src/ui/tabs.tsx +84 -0
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/thumbnail.tsx +80 -0
- package/src/ui/toast.tsx +112 -0
- package/src/ui/tooltip.tsx +3 -1
- package/src/ui/sonner.tsx +0 -47
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { type ReactNode, useState } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { Button } from '../ui/button'
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogBody,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from '../ui/dialog'
|
|
12
|
+
import { Field, FieldLabel } from '../ui/field'
|
|
13
|
+
import { Input } from '../ui/input'
|
|
14
|
+
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from '../ui/input-group'
|
|
15
|
+
import { Textarea } from '../ui/textarea'
|
|
16
|
+
import {
|
|
17
|
+
type PostSaleSelection,
|
|
18
|
+
type PostSaleUnit,
|
|
19
|
+
QuantityPicker,
|
|
20
|
+
selectedUnits,
|
|
21
|
+
} from './post-sale-fields'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Opens a return: which units are coming back, why, and any note.
|
|
25
|
+
*
|
|
26
|
+
* `extraFields` is where a surface adds what only it can offer — the
|
|
27
|
+
* operator's shelf picker, for instance — so the shape stays one component.
|
|
28
|
+
* Shared by the operator's order page and the seller's.
|
|
29
|
+
*/
|
|
30
|
+
export function CreateReturnDialog({
|
|
31
|
+
units,
|
|
32
|
+
reasonField,
|
|
33
|
+
extraFields,
|
|
34
|
+
onClose,
|
|
35
|
+
onSubmit,
|
|
36
|
+
pending = false,
|
|
37
|
+
}: {
|
|
38
|
+
units: PostSaleUnit[]
|
|
39
|
+
/** The reason picker, wired to whichever vocabulary the caller reads. */
|
|
40
|
+
reasonField?: ReactNode
|
|
41
|
+
extraFields?: ReactNode
|
|
42
|
+
onClose: () => void
|
|
43
|
+
onSubmit: (params: {
|
|
44
|
+
items: Array<{ fulfillment_item_id: string; quantity: number }>
|
|
45
|
+
memo?: string
|
|
46
|
+
}) => void
|
|
47
|
+
pending?: boolean
|
|
48
|
+
}) {
|
|
49
|
+
const { t } = useTranslation()
|
|
50
|
+
const [selection, setSelection] = useState<PostSaleSelection>({})
|
|
51
|
+
const [memo, setMemo] = useState('')
|
|
52
|
+
|
|
53
|
+
const chosen = selectedUnits(selection)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
|
57
|
+
<DialogContent>
|
|
58
|
+
<DialogHeader>
|
|
59
|
+
<DialogTitle>{t('admin.pages.orders.detail.returns.create_title')}</DialogTitle>
|
|
60
|
+
</DialogHeader>
|
|
61
|
+
<DialogBody className="flex flex-col gap-4">
|
|
62
|
+
<QuantityPicker units={units} selection={selection} onChange={setSelection} />
|
|
63
|
+
{reasonField}
|
|
64
|
+
{extraFields}
|
|
65
|
+
<Field>
|
|
66
|
+
<FieldLabel htmlFor="return-memo">
|
|
67
|
+
{t('admin.pages.orders.detail.returns.memo')}
|
|
68
|
+
</FieldLabel>
|
|
69
|
+
<Textarea
|
|
70
|
+
id="return-memo"
|
|
71
|
+
value={memo}
|
|
72
|
+
onChange={(event) => setMemo(event.target.value)}
|
|
73
|
+
/>
|
|
74
|
+
</Field>
|
|
75
|
+
</DialogBody>
|
|
76
|
+
<DialogFooter>
|
|
77
|
+
<Button type="button" variant="outline" onClick={onClose} disabled={pending}>
|
|
78
|
+
{t('admin.actions.cancel')}
|
|
79
|
+
</Button>
|
|
80
|
+
<Button
|
|
81
|
+
type="button"
|
|
82
|
+
disabled={chosen.length === 0 || pending}
|
|
83
|
+
onClick={() =>
|
|
84
|
+
onSubmit({
|
|
85
|
+
items: chosen.map(([id, quantity]) => ({
|
|
86
|
+
fulfillment_item_id: id,
|
|
87
|
+
quantity,
|
|
88
|
+
})),
|
|
89
|
+
memo: memo.trim() || undefined,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
{t('admin.pages.orders.detail.returns.actions.create')}
|
|
94
|
+
</Button>
|
|
95
|
+
</DialogFooter>
|
|
96
|
+
</DialogContent>
|
|
97
|
+
</Dialog>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A line the claim can name, with what one unit of it cost. */
|
|
102
|
+
export type ClaimableLine = PostSaleUnit & {
|
|
103
|
+
/** Unit price, used to default the refund to what was actually paid. */
|
|
104
|
+
price?: string | null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Reports a problem with a delivery: which lines, how many, and what to give
|
|
109
|
+
* back for each.
|
|
110
|
+
*
|
|
111
|
+
* A claim is about what was bought rather than what shipped — goods that
|
|
112
|
+
* never arrived are exactly what one is for — so the lines come from the
|
|
113
|
+
* order, not from its fulfillments.
|
|
114
|
+
*/
|
|
115
|
+
export function CreateClaimDialog({
|
|
116
|
+
lines,
|
|
117
|
+
currencySymbol,
|
|
118
|
+
reasonField,
|
|
119
|
+
onClose,
|
|
120
|
+
onSubmit,
|
|
121
|
+
pending = false,
|
|
122
|
+
}: {
|
|
123
|
+
lines: ClaimableLine[]
|
|
124
|
+
currencySymbol: string
|
|
125
|
+
reasonField?: ReactNode
|
|
126
|
+
onClose: () => void
|
|
127
|
+
onSubmit: (params: {
|
|
128
|
+
items: Array<{
|
|
129
|
+
line_item_id: string
|
|
130
|
+
quantity: number
|
|
131
|
+
description?: string
|
|
132
|
+
refund_amount?: string
|
|
133
|
+
}>
|
|
134
|
+
memo?: string
|
|
135
|
+
}) => void
|
|
136
|
+
pending?: boolean
|
|
137
|
+
}) {
|
|
138
|
+
const { t } = useTranslation()
|
|
139
|
+
const [selection, setSelection] = useState<PostSaleSelection>({})
|
|
140
|
+
const [amounts, setAmounts] = useState<Record<string, string>>({})
|
|
141
|
+
const [memo, setMemo] = useState('')
|
|
142
|
+
|
|
143
|
+
const chosen = selectedUnits(selection)
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
|
147
|
+
<DialogContent>
|
|
148
|
+
<DialogHeader>
|
|
149
|
+
<DialogTitle>{t('admin.pages.orders.detail.claims.create_title')}</DialogTitle>
|
|
150
|
+
</DialogHeader>
|
|
151
|
+
<DialogBody className="flex flex-col gap-4">
|
|
152
|
+
<div className="flex flex-col gap-2">
|
|
153
|
+
{lines.map((line) => {
|
|
154
|
+
const chosenQuantity = selection[line.id] ?? 0
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
key={line.id}
|
|
159
|
+
className="flex flex-col gap-2 rounded-lg border border-border-subtle p-3"
|
|
160
|
+
>
|
|
161
|
+
<div className="flex items-center justify-between gap-4">
|
|
162
|
+
<span className="min-w-0 truncate text-sm">{line.label}</span>
|
|
163
|
+
<Input
|
|
164
|
+
type="number"
|
|
165
|
+
min={0}
|
|
166
|
+
max={line.quantity}
|
|
167
|
+
className="w-20"
|
|
168
|
+
value={chosenQuantity}
|
|
169
|
+
aria-label={line.label}
|
|
170
|
+
onChange={(event) => {
|
|
171
|
+
const quantity = Math.max(
|
|
172
|
+
0,
|
|
173
|
+
Math.min(Number(event.target.value), line.quantity),
|
|
174
|
+
)
|
|
175
|
+
setSelection({ ...selection, [line.id]: quantity })
|
|
176
|
+
// Default the refund to what was paid for those units;
|
|
177
|
+
// the merchant can still overwrite it.
|
|
178
|
+
if (quantity > 0 && !amounts[line.id]) {
|
|
179
|
+
const unitPrice = Number(line.price)
|
|
180
|
+
if (Number.isFinite(unitPrice)) {
|
|
181
|
+
setAmounts((current) => ({
|
|
182
|
+
...current,
|
|
183
|
+
[line.id]: (unitPrice * quantity).toFixed(2),
|
|
184
|
+
}))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
{chosenQuantity > 0 && (
|
|
191
|
+
<Field>
|
|
192
|
+
<FieldLabel htmlFor={`claim-amount-${line.id}`}>
|
|
193
|
+
{t('admin.pages.orders.detail.claims.refund_amount')}
|
|
194
|
+
</FieldLabel>
|
|
195
|
+
<InputGroup>
|
|
196
|
+
<InputGroupAddon>
|
|
197
|
+
<InputGroupText>{currencySymbol}</InputGroupText>
|
|
198
|
+
</InputGroupAddon>
|
|
199
|
+
<InputGroupInput
|
|
200
|
+
id={`claim-amount-${line.id}`}
|
|
201
|
+
type="number"
|
|
202
|
+
step="0.01"
|
|
203
|
+
min="0"
|
|
204
|
+
value={amounts[line.id] ?? ''}
|
|
205
|
+
onChange={(event) =>
|
|
206
|
+
setAmounts({ ...amounts, [line.id]: event.target.value })
|
|
207
|
+
}
|
|
208
|
+
/>
|
|
209
|
+
</InputGroup>
|
|
210
|
+
</Field>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
})}
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{reasonField}
|
|
218
|
+
<Field>
|
|
219
|
+
<FieldLabel htmlFor="claim-memo">
|
|
220
|
+
{t('admin.pages.orders.detail.returns.memo')}
|
|
221
|
+
</FieldLabel>
|
|
222
|
+
<Textarea
|
|
223
|
+
id="claim-memo"
|
|
224
|
+
value={memo}
|
|
225
|
+
onChange={(event) => setMemo(event.target.value)}
|
|
226
|
+
/>
|
|
227
|
+
</Field>
|
|
228
|
+
</DialogBody>
|
|
229
|
+
<DialogFooter>
|
|
230
|
+
<Button type="button" variant="outline" onClick={onClose} disabled={pending}>
|
|
231
|
+
{t('admin.actions.cancel')}
|
|
232
|
+
</Button>
|
|
233
|
+
<Button
|
|
234
|
+
type="button"
|
|
235
|
+
disabled={chosen.length === 0 || pending}
|
|
236
|
+
onClick={() =>
|
|
237
|
+
onSubmit({
|
|
238
|
+
items: chosen.map(([id, quantity]) => ({
|
|
239
|
+
line_item_id: id,
|
|
240
|
+
quantity,
|
|
241
|
+
refund_amount: amounts[id] || undefined,
|
|
242
|
+
})),
|
|
243
|
+
memo: memo.trim() || undefined,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
>
|
|
247
|
+
{t('admin.pages.orders.detail.claims.actions.create')}
|
|
248
|
+
</Button>
|
|
249
|
+
</DialogFooter>
|
|
250
|
+
</DialogContent>
|
|
251
|
+
</Dialog>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next'
|
|
2
|
+
import { Field, FieldLabel } from '../ui/field'
|
|
3
|
+
import { Input } from '../ui/input'
|
|
4
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
|
5
|
+
|
|
6
|
+
/** A unit on the order that a return, exchange or claim can name. */
|
|
7
|
+
export type PostSaleUnit = {
|
|
8
|
+
id: string
|
|
9
|
+
label: string
|
|
10
|
+
quantity: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** How many of each unit a record covers; zero means "not included". */
|
|
14
|
+
export type PostSaleSelection = Record<string, number>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Picks how many of each unit a post-sale record covers.
|
|
18
|
+
*
|
|
19
|
+
* Shared by the operator's order page and the seller's, so opening a return
|
|
20
|
+
* looks the same on both sides of a marketplace.
|
|
21
|
+
*/
|
|
22
|
+
export function QuantityPicker({
|
|
23
|
+
units,
|
|
24
|
+
selection,
|
|
25
|
+
onChange,
|
|
26
|
+
}: {
|
|
27
|
+
units: PostSaleUnit[]
|
|
28
|
+
selection: PostSaleSelection
|
|
29
|
+
onChange: (selection: PostSaleSelection) => void
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<div className="flex flex-col gap-2">
|
|
33
|
+
{units.map((unit) => (
|
|
34
|
+
<div
|
|
35
|
+
key={unit.id}
|
|
36
|
+
className="flex items-center justify-between gap-4 rounded-lg border border-border-subtle p-3"
|
|
37
|
+
>
|
|
38
|
+
<span className="min-w-0 truncate text-sm">{unit.label}</span>
|
|
39
|
+
<Input
|
|
40
|
+
type="number"
|
|
41
|
+
min={0}
|
|
42
|
+
max={unit.quantity}
|
|
43
|
+
className="w-20"
|
|
44
|
+
value={selection[unit.id] ?? 0}
|
|
45
|
+
aria-label={unit.label}
|
|
46
|
+
onChange={(event) =>
|
|
47
|
+
onChange({
|
|
48
|
+
...selection,
|
|
49
|
+
[unit.id]: Math.max(0, Math.min(Number(event.target.value), unit.quantity)),
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The units a record actually covers, as id/quantity pairs. */
|
|
60
|
+
export function selectedUnits(selection: PostSaleSelection): Array<[string, number]> {
|
|
61
|
+
return Object.entries(selection).filter(([, quantity]) => quantity > 0)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One entry of a merchant's reason vocabulary. */
|
|
65
|
+
export type ReasonOption = {
|
|
66
|
+
id: string
|
|
67
|
+
name: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reason dropdown, shared by every surface that records why something
|
|
72
|
+
* happened. Optional by design — the API accepts a record without a reason,
|
|
73
|
+
* and a merchant mid-return should not be blocked because nobody has curated
|
|
74
|
+
* the list yet.
|
|
75
|
+
*
|
|
76
|
+
* With no `emptyOptionLabel` the field hides itself when the vocabulary is
|
|
77
|
+
* empty, which suits a form that has other fields to fall back on. Passing one
|
|
78
|
+
* keeps the field visible with an explicit "none" choice at the top, for a
|
|
79
|
+
* surface where the reason is the point and its absence has to be sayable.
|
|
80
|
+
*
|
|
81
|
+
* The reasons come from the caller: each panel reads its own endpoint, and
|
|
82
|
+
* which vocabulary a surface offers is not this component's business.
|
|
83
|
+
*/
|
|
84
|
+
export function ReasonField({
|
|
85
|
+
id,
|
|
86
|
+
reasons,
|
|
87
|
+
value,
|
|
88
|
+
onChange,
|
|
89
|
+
label,
|
|
90
|
+
emptyOptionLabel,
|
|
91
|
+
}: {
|
|
92
|
+
/** Scopes the field id, since a page can show more than one of these. */
|
|
93
|
+
id: string
|
|
94
|
+
reasons: ReasonOption[]
|
|
95
|
+
value: string
|
|
96
|
+
onChange: (value: string) => void
|
|
97
|
+
label?: string
|
|
98
|
+
emptyOptionLabel?: string
|
|
99
|
+
}) {
|
|
100
|
+
const { t } = useTranslation()
|
|
101
|
+
|
|
102
|
+
if (reasons.length === 0 && emptyOptionLabel === undefined) return null
|
|
103
|
+
|
|
104
|
+
// Base UI's <Select> needs `items` to resolve the trigger label; without it
|
|
105
|
+
// the closed trigger renders the raw id (see CLAUDE.md).
|
|
106
|
+
const options = [
|
|
107
|
+
...(emptyOptionLabel === undefined ? [] : [{ value: '', label: emptyOptionLabel }]),
|
|
108
|
+
...reasons.map((reason) => ({ value: reason.id, label: reason.name })),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Field>
|
|
113
|
+
<FieldLabel htmlFor={`reason-${id}`}>
|
|
114
|
+
{label ?? t('admin.pages.orders.detail.returns.reason')}
|
|
115
|
+
</FieldLabel>
|
|
116
|
+
<Select items={options} value={value} onValueChange={(next) => onChange(next ?? '')}>
|
|
117
|
+
<SelectTrigger id={`reason-${id}`}>
|
|
118
|
+
{/* An explicit empty choice IS the label for "none", so it must show
|
|
119
|
+
rather than fall through to the placeholder Base UI renders for
|
|
120
|
+
an unset value. */}
|
|
121
|
+
<SelectValue placeholder={emptyOptionLabel ?? t('admin.common.select_placeholder')} />
|
|
122
|
+
</SelectTrigger>
|
|
123
|
+
<SelectContent>
|
|
124
|
+
{options.map((option) => (
|
|
125
|
+
<SelectItem key={option.value} value={option.value}>
|
|
126
|
+
{option.label}
|
|
127
|
+
</SelectItem>
|
|
128
|
+
))}
|
|
129
|
+
</SelectContent>
|
|
130
|
+
</Select>
|
|
131
|
+
</Field>
|
|
132
|
+
)
|
|
133
|
+
}
|