@spree/dashboard-ui 0.13.1 → 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,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dashboard's icon set.
|
|
3
|
+
*
|
|
4
|
+
* Every icon the dashboard renders is re-exported from here rather than
|
|
5
|
+
* imported from `lucide-react` directly, so the icon set is swappable in one
|
|
6
|
+
* file instead of across hundreds. Adding an icon means adding a line here.
|
|
7
|
+
*
|
|
8
|
+
* `LucideIcon` is the icon *type* the nav registries and the plugin API are
|
|
9
|
+
* written against; it still comes straight from `lucide-react`.
|
|
10
|
+
*/
|
|
11
|
+
export {
|
|
12
|
+
AlertTriangleIcon,
|
|
13
|
+
ArchiveIcon,
|
|
14
|
+
ArrowLeftIcon,
|
|
15
|
+
ArrowLeftRightIcon,
|
|
16
|
+
ArrowUpDownIcon,
|
|
17
|
+
BadgeCheckIcon,
|
|
18
|
+
BanIcon,
|
|
19
|
+
BanknoteIcon,
|
|
20
|
+
BarChart3Icon as AnalyticsIcon,
|
|
21
|
+
BoldIcon,
|
|
22
|
+
BookOpenIcon,
|
|
23
|
+
BoxesIcon,
|
|
24
|
+
BracesIcon,
|
|
25
|
+
Building2Icon,
|
|
26
|
+
CalendarIcon,
|
|
27
|
+
CalendarOffIcon,
|
|
28
|
+
ChartColumnIcon,
|
|
29
|
+
CheckCircle2Icon,
|
|
30
|
+
CheckCircleIcon,
|
|
31
|
+
CheckIcon,
|
|
32
|
+
ChevronDownIcon,
|
|
33
|
+
ChevronLeftIcon,
|
|
34
|
+
ChevronRightIcon,
|
|
35
|
+
ChevronsLeftIcon,
|
|
36
|
+
ChevronsRightIcon,
|
|
37
|
+
ChevronsUpDownIcon,
|
|
38
|
+
CircleCheckIcon,
|
|
39
|
+
CircleDotIcon,
|
|
40
|
+
CircleIcon,
|
|
41
|
+
ClipboardCheckIcon,
|
|
42
|
+
ClockIcon,
|
|
43
|
+
Columns3Icon,
|
|
44
|
+
CommandIcon,
|
|
45
|
+
CopyIcon,
|
|
46
|
+
CornerDownLeftIcon,
|
|
47
|
+
CreditCardIcon,
|
|
48
|
+
DownloadIcon,
|
|
49
|
+
EllipsisVerticalIcon,
|
|
50
|
+
ExternalLinkIcon,
|
|
51
|
+
EyeIcon,
|
|
52
|
+
EyeOffIcon,
|
|
53
|
+
FileArchiveIcon,
|
|
54
|
+
FileAudioIcon,
|
|
55
|
+
FileCodeIcon,
|
|
56
|
+
FileIcon,
|
|
57
|
+
FileImageIcon,
|
|
58
|
+
FileSpreadsheetIcon,
|
|
59
|
+
FileTextIcon,
|
|
60
|
+
FileVideoIcon,
|
|
61
|
+
FilmIcon,
|
|
62
|
+
FilterIcon,
|
|
63
|
+
FolderMinusIcon,
|
|
64
|
+
FolderPlusIcon,
|
|
65
|
+
FolderTreeIcon,
|
|
66
|
+
GiftIcon,
|
|
67
|
+
GlobeIcon,
|
|
68
|
+
GlobeLockIcon,
|
|
69
|
+
GripVerticalIcon,
|
|
70
|
+
HandCoinsIcon,
|
|
71
|
+
HomeIcon,
|
|
72
|
+
ImageIcon,
|
|
73
|
+
ImagePlusIcon,
|
|
74
|
+
InboxIcon,
|
|
75
|
+
InfoIcon,
|
|
76
|
+
ItalicIcon,
|
|
77
|
+
KeyRoundIcon,
|
|
78
|
+
LanguagesIcon,
|
|
79
|
+
LayersIcon,
|
|
80
|
+
LibraryIcon,
|
|
81
|
+
LinkIcon,
|
|
82
|
+
ListChecksIcon,
|
|
83
|
+
ListFilter,
|
|
84
|
+
ListIcon,
|
|
85
|
+
ListOrderedIcon,
|
|
86
|
+
Loader2,
|
|
87
|
+
Loader2Icon,
|
|
88
|
+
Locate,
|
|
89
|
+
LockIcon,
|
|
90
|
+
LogOutIcon,
|
|
91
|
+
MailIcon,
|
|
92
|
+
MapIcon,
|
|
93
|
+
MapPinIcon,
|
|
94
|
+
Maximize,
|
|
95
|
+
MenuIcon,
|
|
96
|
+
MessageCircleIcon,
|
|
97
|
+
Minus,
|
|
98
|
+
MinusIcon,
|
|
99
|
+
MonitorIcon,
|
|
100
|
+
MoonStarIcon,
|
|
101
|
+
MoreHorizontalIcon,
|
|
102
|
+
MoveDownIcon,
|
|
103
|
+
MoveUpIcon,
|
|
104
|
+
OctagonXIcon,
|
|
105
|
+
PackageCheckIcon,
|
|
106
|
+
PackageIcon,
|
|
107
|
+
PackageXIcon,
|
|
108
|
+
PanelLeftIcon,
|
|
109
|
+
PaperclipIcon,
|
|
110
|
+
PauseIcon,
|
|
111
|
+
PencilIcon,
|
|
112
|
+
PercentIcon,
|
|
113
|
+
PhoneIcon,
|
|
114
|
+
PlayIcon,
|
|
115
|
+
PlugIcon,
|
|
116
|
+
Plus,
|
|
117
|
+
PlusIcon,
|
|
118
|
+
PrinterIcon,
|
|
119
|
+
QuoteIcon,
|
|
120
|
+
RadioTowerIcon,
|
|
121
|
+
ReceiptTextIcon,
|
|
122
|
+
RedoIcon,
|
|
123
|
+
RepeatIcon,
|
|
124
|
+
RotateCcwIcon,
|
|
125
|
+
RotateCcwIcon as ReturnRequestIcon,
|
|
126
|
+
ScrollTextIcon,
|
|
127
|
+
SearchIcon,
|
|
128
|
+
SendIcon,
|
|
129
|
+
SettingsIcon,
|
|
130
|
+
ShapesIcon,
|
|
131
|
+
ShieldAlertIcon,
|
|
132
|
+
ShieldCheckIcon,
|
|
133
|
+
ShieldIcon,
|
|
134
|
+
ShoppingCartIcon,
|
|
135
|
+
SlidersHorizontalIcon,
|
|
136
|
+
SparklesIcon,
|
|
137
|
+
SplitIcon,
|
|
138
|
+
StarIcon,
|
|
139
|
+
StoreIcon,
|
|
140
|
+
StrikethroughIcon,
|
|
141
|
+
SunIcon,
|
|
142
|
+
TableIcon,
|
|
143
|
+
TagIcon,
|
|
144
|
+
TagsIcon,
|
|
145
|
+
TicketPercentIcon,
|
|
146
|
+
Trash2Icon,
|
|
147
|
+
TrashIcon,
|
|
148
|
+
TrendingDownIcon,
|
|
149
|
+
TrendingUpIcon,
|
|
150
|
+
TriangleAlertIcon,
|
|
151
|
+
TruckIcon,
|
|
152
|
+
Undo2Icon,
|
|
153
|
+
UndoIcon,
|
|
154
|
+
UploadCloudIcon,
|
|
155
|
+
UploadIcon,
|
|
156
|
+
UserIcon,
|
|
157
|
+
UserMinusIcon,
|
|
158
|
+
UserPlusIcon,
|
|
159
|
+
UsersIcon,
|
|
160
|
+
UsersRoundIcon,
|
|
161
|
+
WarehouseIcon,
|
|
162
|
+
WebhookIcon,
|
|
163
|
+
X,
|
|
164
|
+
XCircleIcon,
|
|
165
|
+
XIcon,
|
|
166
|
+
ZapIcon,
|
|
167
|
+
} from 'lucide-react'
|
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
SheetTitle,
|
|
9
9
|
useCopyToClipboard,
|
|
10
10
|
} from '@spree/dashboard-ui'
|
|
11
|
+
import type { ComponentProps } from 'react'
|
|
12
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
13
|
+
import { useTranslation } from 'react-i18next'
|
|
11
14
|
import {
|
|
12
15
|
CheckIcon,
|
|
13
16
|
ChevronDownIcon,
|
|
@@ -17,10 +20,7 @@ import {
|
|
|
17
20
|
Loader2Icon,
|
|
18
21
|
RotateCcwIcon,
|
|
19
22
|
XIcon,
|
|
20
|
-
} from '
|
|
21
|
-
import type { ComponentProps } from 'react'
|
|
22
|
-
import { useEffect, useMemo, useState } from 'react'
|
|
23
|
-
import { useTranslation } from 'react-i18next'
|
|
23
|
+
} from './icons'
|
|
24
24
|
import { type JsonValueLinkResolver, JsonValueView } from './json-value-view'
|
|
25
25
|
|
|
26
26
|
// Re-export so existing imports of `JsonValueLinkResolver` from this module
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { Controller, type UseFormReturn } from 'react-hook-form'
|
|
3
|
+
import { useTranslation } from 'react-i18next'
|
|
4
|
+
import { Button } from '../ui/button'
|
|
5
|
+
import {
|
|
6
|
+
Combobox,
|
|
7
|
+
ComboboxButtonTrigger,
|
|
8
|
+
ComboboxContent,
|
|
9
|
+
ComboboxEmpty,
|
|
10
|
+
ComboboxItem,
|
|
11
|
+
ComboboxList,
|
|
12
|
+
ComboboxSearch,
|
|
13
|
+
ComboboxTriggerPlaceholder,
|
|
14
|
+
ComboboxValue,
|
|
15
|
+
} from '../ui/combobox'
|
|
16
|
+
import {
|
|
17
|
+
Dialog,
|
|
18
|
+
DialogBody,
|
|
19
|
+
DialogContent,
|
|
20
|
+
DialogDescription,
|
|
21
|
+
DialogFooter,
|
|
22
|
+
DialogHeader,
|
|
23
|
+
DialogTitle,
|
|
24
|
+
} from '../ui/dialog'
|
|
25
|
+
import { Field, FieldError, FieldLabel } from '../ui/field'
|
|
26
|
+
import { Input } from '../ui/input'
|
|
27
|
+
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from '../ui/input-group'
|
|
28
|
+
|
|
29
|
+
/** The form shape both panels bind for a label bought elsewhere. */
|
|
30
|
+
export type LabelUploadFields = {
|
|
31
|
+
tracking_number: string
|
|
32
|
+
carrier: string
|
|
33
|
+
cost: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Records a label the merchant bought elsewhere — postage from a carrier's
|
|
38
|
+
* own site, a 3PL's PDF — so the parcel still has its file, its tracking
|
|
39
|
+
* number and what it cost, with no carrier account connected.
|
|
40
|
+
*
|
|
41
|
+
* Shared by the operator's order page and the seller's; on the seller branch
|
|
42
|
+
* it is the only way a label arrives, since buying one runs through the
|
|
43
|
+
* marketplace's carrier account.
|
|
44
|
+
*
|
|
45
|
+
* The file field comes in as a slot: uploading needs an API client, which is
|
|
46
|
+
* each panel's own.
|
|
47
|
+
*/
|
|
48
|
+
export function LabelUploadDialog({
|
|
49
|
+
form,
|
|
50
|
+
open,
|
|
51
|
+
onOpenChange,
|
|
52
|
+
fileField,
|
|
53
|
+
carrierKeys,
|
|
54
|
+
carrierLabel,
|
|
55
|
+
currency,
|
|
56
|
+
onSubmit,
|
|
57
|
+
pending = false,
|
|
58
|
+
}: {
|
|
59
|
+
form: UseFormReturn<LabelUploadFields>
|
|
60
|
+
open: boolean
|
|
61
|
+
onOpenChange: (open: boolean) => void
|
|
62
|
+
fileField: ReactNode
|
|
63
|
+
carrierKeys: string[]
|
|
64
|
+
carrierLabel: (key: string) => string
|
|
65
|
+
/** Shown beside the cost, when the caller knows which currency it is in. */
|
|
66
|
+
currency?: string
|
|
67
|
+
onSubmit: (values: LabelUploadFields) => void | Promise<void>
|
|
68
|
+
pending?: boolean
|
|
69
|
+
}) {
|
|
70
|
+
const { t } = useTranslation()
|
|
71
|
+
const { errors } = form.formState
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
75
|
+
<DialogContent>
|
|
76
|
+
<form
|
|
77
|
+
onSubmit={(event) => {
|
|
78
|
+
// The order page renders its cards inside their own forms — without
|
|
79
|
+
// stopping the bubble the browser submits the outer one.
|
|
80
|
+
form.handleSubmit(onSubmit)(event)
|
|
81
|
+
event.stopPropagation()
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<DialogHeader>
|
|
85
|
+
<DialogTitle>{t('admin.orders.detail.fulfillments.upload_label_title')}</DialogTitle>
|
|
86
|
+
<DialogDescription>
|
|
87
|
+
{t('admin.orders.detail.fulfillments.upload_label_description')}
|
|
88
|
+
</DialogDescription>
|
|
89
|
+
</DialogHeader>
|
|
90
|
+
|
|
91
|
+
<DialogBody className="flex flex-col gap-4">
|
|
92
|
+
{errors.root?.message && (
|
|
93
|
+
<p className="text-destructive text-sm" role="alert">
|
|
94
|
+
{errors.root.message}
|
|
95
|
+
</p>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
<Field>
|
|
99
|
+
<FieldLabel>{t('admin.orders.detail.fulfillments.upload_label_file')}</FieldLabel>
|
|
100
|
+
{fileField}
|
|
101
|
+
</Field>
|
|
102
|
+
|
|
103
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
104
|
+
<Field>
|
|
105
|
+
<FieldLabel htmlFor="label-tracking-number">
|
|
106
|
+
{t('admin.orders.fulfill.tracking_label')}
|
|
107
|
+
</FieldLabel>
|
|
108
|
+
<Input
|
|
109
|
+
id="label-tracking-number"
|
|
110
|
+
placeholder={t('admin.orders.fulfill.tracking_placeholder')}
|
|
111
|
+
aria-invalid={!!errors.tracking_number}
|
|
112
|
+
{...form.register('tracking_number')}
|
|
113
|
+
/>
|
|
114
|
+
<FieldError errors={[errors.tracking_number]} />
|
|
115
|
+
</Field>
|
|
116
|
+
|
|
117
|
+
<Field>
|
|
118
|
+
<FieldLabel htmlFor="label-cost">
|
|
119
|
+
{t('admin.orders.detail.fulfillments.upload_label_cost')}
|
|
120
|
+
</FieldLabel>
|
|
121
|
+
<InputGroup>
|
|
122
|
+
{currency && (
|
|
123
|
+
<InputGroupAddon>
|
|
124
|
+
<InputGroupText>{currency}</InputGroupText>
|
|
125
|
+
</InputGroupAddon>
|
|
126
|
+
)}
|
|
127
|
+
<InputGroupInput
|
|
128
|
+
id="label-cost"
|
|
129
|
+
type="number"
|
|
130
|
+
step="0.01"
|
|
131
|
+
min="0"
|
|
132
|
+
{...form.register('cost')}
|
|
133
|
+
/>
|
|
134
|
+
</InputGroup>
|
|
135
|
+
<FieldError errors={[errors.cost]} />
|
|
136
|
+
</Field>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<Field>
|
|
140
|
+
<FieldLabel htmlFor="label-carrier">
|
|
141
|
+
{t('admin.orders.detail.fulfillments.carrier_label')}
|
|
142
|
+
</FieldLabel>
|
|
143
|
+
<Controller
|
|
144
|
+
control={form.control}
|
|
145
|
+
name="carrier"
|
|
146
|
+
render={({ field }) => (
|
|
147
|
+
<Combobox
|
|
148
|
+
items={carrierKeys}
|
|
149
|
+
value={field.value}
|
|
150
|
+
onValueChange={(value: string | null) => field.onChange(value ?? '')}
|
|
151
|
+
itemToStringLabel={carrierLabel}
|
|
152
|
+
>
|
|
153
|
+
<ComboboxButtonTrigger id="label-carrier" onBlur={field.onBlur}>
|
|
154
|
+
{field.value ? (
|
|
155
|
+
<ComboboxValue />
|
|
156
|
+
) : (
|
|
157
|
+
<ComboboxTriggerPlaceholder>
|
|
158
|
+
{t('admin.orders.detail.fulfillments.carrier_auto')}
|
|
159
|
+
</ComboboxTriggerPlaceholder>
|
|
160
|
+
)}
|
|
161
|
+
</ComboboxButtonTrigger>
|
|
162
|
+
<ComboboxContent>
|
|
163
|
+
<ComboboxSearch
|
|
164
|
+
placeholder={t('admin.orders.detail.fulfillments.carrier_search')}
|
|
165
|
+
/>
|
|
166
|
+
<ComboboxEmpty>{t('admin.common.no_results')}</ComboboxEmpty>
|
|
167
|
+
<ComboboxList>
|
|
168
|
+
{(value: string) => (
|
|
169
|
+
<ComboboxItem key={value} value={value}>
|
|
170
|
+
{carrierLabel(value)}
|
|
171
|
+
</ComboboxItem>
|
|
172
|
+
)}
|
|
173
|
+
</ComboboxList>
|
|
174
|
+
</ComboboxContent>
|
|
175
|
+
</Combobox>
|
|
176
|
+
)}
|
|
177
|
+
/>
|
|
178
|
+
</Field>
|
|
179
|
+
</DialogBody>
|
|
180
|
+
|
|
181
|
+
<DialogFooter>
|
|
182
|
+
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
183
|
+
{t('admin.actions.cancel')}
|
|
184
|
+
</Button>
|
|
185
|
+
<Button type="submit" disabled={pending}>
|
|
186
|
+
{pending
|
|
187
|
+
? t('admin.actions.saving')
|
|
188
|
+
: t('admin.orders.detail.fulfillments.upload_label')}
|
|
189
|
+
</Button>
|
|
190
|
+
</DialogFooter>
|
|
191
|
+
</form>
|
|
192
|
+
</DialogContent>
|
|
193
|
+
</Dialog>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
DropdownMenuItem,
|
|
4
|
-
DropdownMenuSub,
|
|
5
|
-
DropdownMenuSubContent,
|
|
6
|
-
DropdownMenuSubTrigger,
|
|
7
|
-
} from '../ui/dropdown-menu'
|
|
1
|
+
import { ChevronsUpDownIcon } from './icons'
|
|
8
2
|
|
|
9
3
|
interface LanguageMenuItemsProps {
|
|
10
|
-
/** Translated "Language" label for the
|
|
4
|
+
/** Translated "Language" label for the row (passed by the app). */
|
|
11
5
|
label: string
|
|
12
6
|
/** Admin UI languages available to switch to ({ code, name } pairs). */
|
|
13
7
|
locales: ReadonlyArray<{ code: string; name: string }>
|
|
@@ -17,9 +11,15 @@ interface LanguageMenuItemsProps {
|
|
|
17
11
|
}
|
|
18
12
|
|
|
19
13
|
/**
|
|
20
|
-
* Admin UI language picker rendered inside the user dropdown
|
|
21
|
-
*
|
|
22
|
-
*
|
|
14
|
+
* Admin UI language picker rendered inside the user dropdown's Preferences
|
|
15
|
+
* section, as a native `<select>`.
|
|
16
|
+
*
|
|
17
|
+
* Native rather than the design system's `Select`: a menu nested inside a menu
|
|
18
|
+
* is awkward to drive with a pointer and stacks two portal layers, while the
|
|
19
|
+
* platform control opens as its own popup outside the dropdown entirely — and
|
|
20
|
+
* gives long locale lists native scrolling and type-ahead for free. The
|
|
21
|
+
* `<select>` is transparent and overlaid on the styled pill, so the row still
|
|
22
|
+
* matches the Theme row above it.
|
|
23
23
|
*
|
|
24
24
|
* Headless: the label, locale list, and change handler are passed in, so this
|
|
25
25
|
* component imports no i18n/data runtime. Renders nothing when fewer than two
|
|
@@ -31,24 +31,31 @@ export function LanguageMenuItems({ label, locales, value, onSelect }: LanguageM
|
|
|
31
31
|
const current = locales.find((l) => l.code === value)
|
|
32
32
|
|
|
33
33
|
return (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
34
|
+
// Not a DropdownMenuItem: the row is a container for the select, and menu
|
|
35
|
+
// items steal the pointer and close the menu on click.
|
|
36
|
+
<div className="flex items-center justify-between gap-2 rounded-lg px-2 py-1.5">
|
|
37
|
+
<span className="text-sm text-foreground">{label}</span>
|
|
38
|
+
{/* The select is transparent and stretched over the pill, so the hit area
|
|
39
|
+
tracks the pill's width whatever the locale name is. `focus-within`
|
|
40
|
+
carries the focus ring, since `opacity-0` hides the native one. */}
|
|
41
|
+
<span className="relative inline-flex rounded-lg focus-within:ring-2 focus-within:ring-ring">
|
|
42
|
+
<span className="pointer-events-none inline-flex items-center gap-1.5 rounded-lg border border-border bg-card px-2 py-1 text-xs font-normal text-foreground">
|
|
43
|
+
{current?.name ?? value}
|
|
44
|
+
<ChevronsUpDownIcon className="size-3 text-muted-foreground" />
|
|
45
|
+
</span>
|
|
46
|
+
<select
|
|
47
|
+
aria-label={label}
|
|
48
|
+
value={value}
|
|
49
|
+
onChange={(event) => onSelect(event.target.value)}
|
|
50
|
+
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
|
51
|
+
>
|
|
52
|
+
{locales.map((locale) => (
|
|
53
|
+
<option key={locale.code} value={locale.code}>
|
|
54
|
+
{locale.name}
|
|
55
|
+
</option>
|
|
56
|
+
))}
|
|
57
|
+
</select>
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
53
60
|
)
|
|
54
61
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { BracesIcon } from 'lucide-react'
|
|
2
1
|
import { Fragment } from 'react'
|
|
3
2
|
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card'
|
|
4
3
|
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../../ui/empty'
|
|
4
|
+
import { BracesIcon } from '../icons'
|
|
5
5
|
|
|
6
6
|
interface MetadataCardProps {
|
|
7
7
|
/**
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { Controller, type UseFormReturn } from 'react-hook-form'
|
|
3
|
+
import { useTranslation } from 'react-i18next'
|
|
4
|
+
import { Button } from '../ui/button'
|
|
5
|
+
import { Checkbox } from '../ui/checkbox'
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogBody,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from '../ui/dialog'
|
|
15
|
+
import { Field, FieldLabel } from '../ui/field'
|
|
16
|
+
import { Textarea } from '../ui/textarea'
|
|
17
|
+
|
|
18
|
+
/** The form shape both panels bind for a cancellation. */
|
|
19
|
+
export type CancelOrderFields = {
|
|
20
|
+
cancel_reason_id: string
|
|
21
|
+
cancel_note: string
|
|
22
|
+
refund_payments: boolean
|
|
23
|
+
notify_customer: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Cancels an order, asking why first.
|
|
28
|
+
*
|
|
29
|
+
* A dialog rather than a bare confirm, because a cancellation carries a
|
|
30
|
+
* reason the merchant wants recorded — this doubles as the confirmation, so
|
|
31
|
+
* there is no second dialog in front of it.
|
|
32
|
+
*
|
|
33
|
+
* `showRefund` is off by default: releasing an unclaimed authorization happens
|
|
34
|
+
* either way, but handing back money already taken is not every surface's
|
|
35
|
+
* decision. On the seller branch the endpoint accepts no refund argument at
|
|
36
|
+
* all, so showing the switch would offer something that would be refused.
|
|
37
|
+
*/
|
|
38
|
+
export function OrderCancelDialog({
|
|
39
|
+
form,
|
|
40
|
+
reasonField,
|
|
41
|
+
showRefund = false,
|
|
42
|
+
showNotify = true,
|
|
43
|
+
onSubmit,
|
|
44
|
+
onClose,
|
|
45
|
+
pending = false,
|
|
46
|
+
}: {
|
|
47
|
+
form: UseFormReturn<CancelOrderFields>
|
|
48
|
+
/** The reason picker, wired to whichever vocabulary the caller reads. */
|
|
49
|
+
reasonField?: ReactNode
|
|
50
|
+
showRefund?: boolean
|
|
51
|
+
showNotify?: boolean
|
|
52
|
+
onSubmit: (values: CancelOrderFields) => void | Promise<void>
|
|
53
|
+
onClose: () => void
|
|
54
|
+
pending?: boolean
|
|
55
|
+
}) {
|
|
56
|
+
const { t } = useTranslation()
|
|
57
|
+
const { errors } = form.formState
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Dialog open onOpenChange={(next) => !next && onClose()}>
|
|
61
|
+
<DialogContent>
|
|
62
|
+
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
63
|
+
<DialogHeader>
|
|
64
|
+
<DialogTitle>{t('admin.pages.orders.detail.dialogs.cancel_title')}</DialogTitle>
|
|
65
|
+
<DialogDescription>{t('admin.orders.detail.cancel.description')}</DialogDescription>
|
|
66
|
+
</DialogHeader>
|
|
67
|
+
|
|
68
|
+
<DialogBody>
|
|
69
|
+
{errors.root?.message && (
|
|
70
|
+
<p className="text-destructive text-sm" role="alert">
|
|
71
|
+
{errors.root.message}
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<div className="flex flex-col gap-4">
|
|
76
|
+
{reasonField}
|
|
77
|
+
|
|
78
|
+
<Field>
|
|
79
|
+
<FieldLabel htmlFor="order-cancel-note">
|
|
80
|
+
{t('admin.orders.detail.cancel.note_label')}
|
|
81
|
+
</FieldLabel>
|
|
82
|
+
<Textarea
|
|
83
|
+
id="order-cancel-note"
|
|
84
|
+
rows={3}
|
|
85
|
+
placeholder={t('admin.orders.detail.cancel.note_placeholder')}
|
|
86
|
+
{...form.register('cancel_note')}
|
|
87
|
+
/>
|
|
88
|
+
</Field>
|
|
89
|
+
|
|
90
|
+
{showRefund && (
|
|
91
|
+
<Controller
|
|
92
|
+
control={form.control}
|
|
93
|
+
name="refund_payments"
|
|
94
|
+
render={({ field }) => (
|
|
95
|
+
<label
|
|
96
|
+
htmlFor="order-cancel-refund"
|
|
97
|
+
className="flex cursor-pointer items-start gap-2 text-sm"
|
|
98
|
+
>
|
|
99
|
+
<Checkbox
|
|
100
|
+
id="order-cancel-refund"
|
|
101
|
+
checked={field.value}
|
|
102
|
+
onCheckedChange={(checked) => field.onChange(checked === true)}
|
|
103
|
+
/>
|
|
104
|
+
<span>
|
|
105
|
+
{t('admin.orders.detail.cancel.refund_payments')}
|
|
106
|
+
<span className="block text-muted-foreground">
|
|
107
|
+
{t('admin.orders.detail.cancel.refund_payments_help')}
|
|
108
|
+
</span>
|
|
109
|
+
</span>
|
|
110
|
+
</label>
|
|
111
|
+
)}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{showNotify && (
|
|
116
|
+
<Controller
|
|
117
|
+
control={form.control}
|
|
118
|
+
name="notify_customer"
|
|
119
|
+
render={({ field }) => (
|
|
120
|
+
<label
|
|
121
|
+
htmlFor="order-cancel-notify"
|
|
122
|
+
className="flex cursor-pointer items-center gap-2 text-sm"
|
|
123
|
+
>
|
|
124
|
+
<Checkbox
|
|
125
|
+
id="order-cancel-notify"
|
|
126
|
+
checked={field.value}
|
|
127
|
+
onCheckedChange={(checked) => field.onChange(checked === true)}
|
|
128
|
+
/>
|
|
129
|
+
{t('admin.orders.detail.cancel.notify_customer')}
|
|
130
|
+
</label>
|
|
131
|
+
)}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</DialogBody>
|
|
136
|
+
|
|
137
|
+
<DialogFooter>
|
|
138
|
+
<Button type="button" variant="outline" onClick={onClose} disabled={pending}>
|
|
139
|
+
{t('admin.orders.detail.cancel.keep_order')}
|
|
140
|
+
</Button>
|
|
141
|
+
<Button type="submit" variant="destructive" disabled={pending}>
|
|
142
|
+
{t('admin.pages.orders.detail.actions.cancel')}
|
|
143
|
+
</Button>
|
|
144
|
+
</DialogFooter>
|
|
145
|
+
</form>
|
|
146
|
+
</DialogContent>
|
|
147
|
+
</Dialog>
|
|
148
|
+
)
|
|
149
|
+
}
|