@spree/dashboard-core 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 +4 -0
- package/dist/vite/index.js +17 -3
- package/package.json +7 -7
- package/src/api-client.ts +643 -0
- package/src/components/address-form-dialog.test.ts +25 -0
- package/src/components/address-form-dialog.tsx +203 -79
- package/src/components/app-shell.tsx +171 -0
- package/src/components/app-sidebar.tsx +184 -27
- package/src/components/bulk-action-bar.tsx +89 -157
- package/src/components/country-combobox.tsx +69 -34
- package/src/components/country-state-fields.tsx +43 -11
- package/src/components/currency-select.tsx +81 -66
- package/src/components/export-button.tsx +62 -5
- package/src/components/file-upload-field.tsx +32 -10
- package/src/components/import-button.tsx +97 -36
- package/src/components/import-wizard-dialog.tsx +710 -0
- package/src/components/locale-select.tsx +70 -57
- package/src/components/media-picker-sheet.tsx +307 -0
- package/src/components/mobile-top-bar.tsx +75 -0
- package/src/components/nav-main.tsx +197 -27
- package/src/components/page-header.tsx +203 -61
- package/src/components/page-tabs.tsx +4 -1
- package/src/components/preferences-form.tsx +98 -4
- package/src/components/resource-combobox.tsx +52 -4
- package/src/components/resource-picker-sheet.tsx +209 -73
- package/src/components/resource-table.tsx +425 -131
- package/src/components/route-announcer.tsx +64 -0
- package/src/components/section-heading.tsx +95 -0
- package/src/components/settings-sidebar.tsx +290 -60
- package/src/components/sheet-top-bar.tsx +44 -0
- package/src/components/sidebar-search.tsx +83 -0
- package/src/components/sidebar-user.tsx +213 -0
- package/src/components/skip-link.tsx +36 -0
- package/src/components/store-date-picker.tsx +6 -2
- package/src/components/store-switcher.tsx +63 -22
- package/src/components/table-toolbar.tsx +1373 -442
- package/src/components/tag-combobox.tsx +12 -4
- package/src/components/top-bar-breadcrumbs.tsx +235 -0
- package/src/components/wizard-close-guard.test.ts +54 -0
- package/src/hooks/use-auto-collapse-sidebar.ts +57 -0
- package/src/hooks/use-command-palette.tsx +13 -0
- package/src/hooks/use-countries.test.ts +56 -0
- package/src/hooks/use-countries.ts +51 -5
- package/src/hooks/use-direct-upload.ts +16 -3
- package/src/hooks/use-export.ts +51 -16
- package/src/hooks/use-import.ts +261 -14
- package/src/hooks/use-package-types.ts +132 -0
- package/src/hooks/use-resource-mutation.ts +74 -13
- package/src/hooks/use-stock-locations.ts +131 -0
- package/src/hooks/use-switch-admin-locale.ts +2 -2
- package/src/hooks/use-visual-viewport-offset.ts +61 -0
- package/src/index.ts +45 -1
- package/src/lib/cancel-order-form.ts +27 -0
- package/src/lib/create-action-registry.ts +278 -0
- package/src/lib/date-presets.test.ts +86 -0
- package/src/lib/date-presets.ts +98 -0
- package/src/lib/delivery-form.ts +15 -0
- package/src/lib/docs.ts +32 -0
- package/src/lib/download.ts +13 -1
- package/src/lib/filters-to-ransack.test.ts +113 -0
- package/src/lib/filters-to-ransack.ts +22 -1
- package/src/lib/form-errors.ts +122 -10
- package/src/lib/form-mappers.ts +76 -0
- package/src/lib/fulfillment-items.test.ts +213 -0
- package/src/lib/fulfillment-items.ts +171 -0
- package/src/lib/i18n.ts +19 -11
- package/src/lib/import-types.ts +25 -0
- package/src/lib/label-upload-form.ts +22 -0
- package/src/lib/nav-registry.ts +71 -3
- package/src/lib/permissions.ts +46 -1
- package/src/lib/progress.ts +18 -0
- package/src/lib/query-keys.ts +13 -13
- package/src/lib/sample-csv.ts +21 -0
- package/src/lib/search-registry.ts +6 -2
- package/src/lib/settings-nav-registry.ts +104 -11
- package/src/lib/storefront.ts +19 -0
- package/src/lib/table-registry.ts +56 -0
- package/src/lib/type-labels.test.ts +68 -0
- package/src/lib/type-labels.ts +78 -0
- package/src/locales/ar.json +1889 -516
- package/src/locales/de.json +1883 -515
- package/src/locales/en.json +1886 -516
- package/src/locales/fr.json +1884 -516
- package/src/locales/pl.json +1913 -516
- package/src/locales/zh-CN.json +1883 -516
- package/src/pages/package-types.tsx +484 -0
- package/src/pages/settings-index.tsx +112 -0
- package/src/pages/stock-locations.tsx +674 -0
- package/src/plugin.ts +29 -0
- package/src/products/add-video-dialog.tsx +100 -0
- package/src/products/bulk-variants-dialog.tsx +301 -0
- package/src/products/carton-size.test.ts +51 -0
- package/src/products/carton-size.ts +20 -0
- package/src/products/currency-parts.ts +21 -0
- package/src/products/inventory-section.tsx +259 -0
- package/src/products/media-edit-sheet.tsx +336 -0
- package/src/products/normalize-customs.test.ts +39 -0
- package/src/products/normalize-customs.ts +27 -0
- package/src/products/normalize-money.test.ts +58 -0
- package/src/products/normalize-money.ts +73 -0
- package/src/products/normalize-quantity.test.ts +32 -0
- package/src/products/normalize-quantity.ts +31 -0
- package/src/products/product-bulk-price-editor.tsx +165 -0
- package/src/products/product-form-cards.tsx +1167 -0
- package/src/products/product-form-mapping.ts +212 -0
- package/src/products/product-schema.ts +318 -0
- package/src/products/product-types.ts +130 -0
- package/src/products/use-product-form-data.ts +273 -0
- package/src/products/variant-edit-sheet.tsx +725 -0
- package/src/products/variants-matrix.test.ts +154 -0
- package/src/products/variants-matrix.ts +244 -0
- package/src/products/variants-options-builder.tsx +571 -0
- package/src/products/variants-section.tsx +535 -0
- package/src/products/video-url.test.ts +71 -0
- package/src/products/video-url.ts +83 -0
- package/src/providers/auth-provider.tsx +70 -12
- package/src/providers/permission-provider.tsx +68 -28
- package/src/providers/sticky-header-provider.tsx +134 -0
- package/src/providers/store-provider.tsx +36 -3
- package/src/providers/tenant-provider.tsx +33 -0
- package/src/schemas/package-type.ts +95 -0
- package/src/schemas/stock-location.ts +126 -0
- package/src/tables/package-types.tsx +74 -0
- package/src/tables/stock-locations.tsx +124 -0
- package/src/vite/index.ts +40 -7
- package/src/components/top-bar.tsx +0 -210
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Combobox,
|
|
3
|
+
ComboboxButtonTrigger,
|
|
3
4
|
ComboboxChip,
|
|
4
5
|
ComboboxChips,
|
|
5
6
|
ComboboxChipsInput,
|
|
6
7
|
ComboboxContent,
|
|
7
8
|
ComboboxEmpty,
|
|
8
|
-
ComboboxInput,
|
|
9
9
|
ComboboxItem,
|
|
10
10
|
ComboboxList,
|
|
11
|
+
ComboboxSearch,
|
|
12
|
+
ComboboxTriggerPlaceholder,
|
|
11
13
|
ComboboxValue,
|
|
12
14
|
CountryFlag,
|
|
13
|
-
InputGroupAddon,
|
|
14
15
|
useComboboxAnchor,
|
|
15
16
|
} from '@spree/dashboard-ui'
|
|
16
|
-
import { useMemo, useState } from 'react'
|
|
17
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
17
18
|
import { useTranslation } from 'react-i18next'
|
|
18
19
|
import { useCountries } from '../hooks/use-countries'
|
|
19
20
|
import { useDisplayName } from '../hooks/use-display-name'
|
|
@@ -23,11 +24,19 @@ export type CountryOption = { iso: string; iso3: string; name: string }
|
|
|
23
24
|
/**
|
|
24
25
|
* Resolves a country's display name in the admin UI language (keyed on the
|
|
25
26
|
* stable ISO code), falling back to the API's backend-locale `name` when the
|
|
26
|
-
* runtime lacks coverage.
|
|
27
|
+
* runtime lacks coverage. Exported so every country-labelled surface
|
|
28
|
+
* (pickers, grid cells) reads the same.
|
|
29
|
+
*
|
|
30
|
+
* The resolver is referentially stable across renders — callers feed it into
|
|
31
|
+
* `useMemo` deps, and a fresh closure each render would rebuild every derived
|
|
32
|
+
* option list (and, downstream, every grid column) on each keystroke.
|
|
27
33
|
*/
|
|
28
|
-
function useCountryDisplayName() {
|
|
34
|
+
export function useCountryDisplayName() {
|
|
29
35
|
const displayName = useDisplayName('region')
|
|
30
|
-
return (
|
|
36
|
+
return useCallback(
|
|
37
|
+
(country: CountryOption) => displayName(country.iso) ?? country.name,
|
|
38
|
+
[displayName],
|
|
39
|
+
)
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
/**
|
|
@@ -79,32 +88,51 @@ function CountryRow({ country, label }: { country: CountryOption; label: string
|
|
|
79
88
|
)
|
|
80
89
|
}
|
|
81
90
|
|
|
91
|
+
/** Maps the cached country list onto the flat option shape both pickers use. */
|
|
92
|
+
function useCountryOptions(): CountryOption[] {
|
|
93
|
+
const { countries } = useCountries()
|
|
94
|
+
return useMemo(
|
|
95
|
+
() => countries.map((c) => ({ iso: c.iso, iso3: c.iso3, name: c.name })),
|
|
96
|
+
[countries],
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
82
100
|
/**
|
|
83
101
|
* Searchable single-select country picker. Value is the 2-letter ISO code
|
|
84
|
-
* (so the field round-trips cleanly with the Spree API).
|
|
85
|
-
*
|
|
102
|
+
* (so the field round-trips cleanly with the Spree API).
|
|
103
|
+
*
|
|
104
|
+
* The field itself is a button showing the selected flag + name, with the
|
|
105
|
+
* search box inside the dropdown — see `<ComboboxButtonTrigger>` for why a
|
|
106
|
+
* text input is the wrong trigger for an address-adjacent picker.
|
|
86
107
|
*/
|
|
87
108
|
export function CountryCombobox({
|
|
109
|
+
id,
|
|
88
110
|
value,
|
|
89
111
|
onValueChange,
|
|
112
|
+
onBlur,
|
|
90
113
|
placeholder,
|
|
114
|
+
searchPlaceholder,
|
|
115
|
+
invalid,
|
|
91
116
|
disabled = false,
|
|
92
117
|
}: {
|
|
118
|
+
/** Forwarded to the trigger so a `<FieldLabel htmlFor>` can target it. */
|
|
119
|
+
id?: string
|
|
93
120
|
value: string | null | undefined
|
|
94
121
|
onValueChange: (iso: string) => void
|
|
122
|
+
/** Forwarded to the trigger so RHF's `<Controller>` can track touched state. */
|
|
123
|
+
onBlur?: () => void
|
|
124
|
+
/** Trigger text while nothing is selected. */
|
|
95
125
|
placeholder?: string
|
|
126
|
+
/** Text in the dropdown's search box. */
|
|
127
|
+
searchPlaceholder?: string
|
|
128
|
+
invalid?: boolean
|
|
96
129
|
disabled?: boolean
|
|
97
130
|
}) {
|
|
98
131
|
const { t } = useTranslation()
|
|
99
|
-
const
|
|
132
|
+
const items = useCountryOptions()
|
|
100
133
|
const countryName = useCountryDisplayName()
|
|
101
134
|
const filter = useCountryFilter()
|
|
102
135
|
|
|
103
|
-
const items: CountryOption[] = useMemo(
|
|
104
|
-
() => countries.map((c) => ({ iso: c.iso, iso3: c.iso3, name: c.name })),
|
|
105
|
-
[countries],
|
|
106
|
-
)
|
|
107
|
-
|
|
108
136
|
// The Combobox holds the selected object internally; we adapt to a flat ISO
|
|
109
137
|
// string at the boundary so callers don't have to thread the option shape.
|
|
110
138
|
const selected = useMemo(() => items.find((c) => c.iso === value) ?? null, [items, value])
|
|
@@ -121,21 +149,29 @@ export function CountryCombobox({
|
|
|
121
149
|
// "Allemagne", "Germany", "DE", or "DEU" all find the country.
|
|
122
150
|
filter={filter}
|
|
123
151
|
>
|
|
124
|
-
<
|
|
125
|
-
|
|
152
|
+
<ComboboxButtonTrigger
|
|
153
|
+
id={id}
|
|
154
|
+
onBlur={onBlur}
|
|
126
155
|
disabled={disabled}
|
|
156
|
+
aria-invalid={invalid || undefined}
|
|
127
157
|
>
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
Hidden while empty so the input doesn't lurch when typing a new
|
|
131
|
-
search query. */}
|
|
132
|
-
{selected?.iso && (
|
|
133
|
-
<InputGroupAddon align="inline-start">
|
|
158
|
+
{selected ? (
|
|
159
|
+
<>
|
|
134
160
|
<CountryFlag iso={selected.iso} />
|
|
135
|
-
|
|
161
|
+
<span className="truncate">{countryName(selected)}</span>
|
|
162
|
+
</>
|
|
163
|
+
) : (
|
|
164
|
+
<ComboboxTriggerPlaceholder>
|
|
165
|
+
{placeholder ?? t('admin.components.country_combobox.placeholder')}
|
|
166
|
+
</ComboboxTriggerPlaceholder>
|
|
136
167
|
)}
|
|
137
|
-
</
|
|
168
|
+
</ComboboxButtonTrigger>
|
|
138
169
|
<ComboboxContent>
|
|
170
|
+
<ComboboxSearch
|
|
171
|
+
placeholder={
|
|
172
|
+
searchPlaceholder ?? t('admin.components.country_combobox.search_placeholder')
|
|
173
|
+
}
|
|
174
|
+
/>
|
|
139
175
|
<ComboboxEmpty>{t('admin.components.country_combobox.empty')}</ComboboxEmpty>
|
|
140
176
|
<ComboboxList>
|
|
141
177
|
{(country: CountryOption) => (
|
|
@@ -152,9 +188,13 @@ export function CountryCombobox({
|
|
|
152
188
|
/**
|
|
153
189
|
* Searchable multi-select country picker. Value is an array of 2-letter ISO
|
|
154
190
|
* codes. Renders a flag + name per chip and `<CountryRow>` per dropdown row;
|
|
155
|
-
* filters the cached `useCountries()` list client-side
|
|
156
|
-
*
|
|
191
|
+
* filters the cached `useCountries()` list client-side (localized name, raw
|
|
192
|
+
* name, and both ISO codes). Reused anywhere the admin picks multiple
|
|
157
193
|
* countries (promotion Country rule, market/zone editors, …).
|
|
194
|
+
*
|
|
195
|
+
* Keeps the chips-with-inline-input shape rather than a button trigger: the
|
|
196
|
+
* chips are the selection display, and typing beside them is how a
|
|
197
|
+
* multi-select is expected to work.
|
|
158
198
|
*/
|
|
159
199
|
export function CountryMultiCombobox({
|
|
160
200
|
value,
|
|
@@ -172,17 +212,12 @@ export function CountryMultiCombobox({
|
|
|
172
212
|
disabled?: boolean
|
|
173
213
|
}) {
|
|
174
214
|
const { t } = useTranslation()
|
|
175
|
-
const
|
|
215
|
+
const items = useCountryOptions()
|
|
176
216
|
const countryName = useCountryDisplayName()
|
|
177
217
|
const filter = useCountryFilter()
|
|
178
218
|
const anchorRef = useComboboxAnchor()
|
|
179
219
|
const [inputValue, setInputValue] = useState('')
|
|
180
220
|
|
|
181
|
-
const items: CountryOption[] = useMemo(
|
|
182
|
-
() => countries.map((c) => ({ iso: c.iso, iso3: c.iso3, name: c.name })),
|
|
183
|
-
[countries],
|
|
184
|
-
)
|
|
185
|
-
|
|
186
221
|
// The Combobox holds full option objects; selection round-trips as ISO
|
|
187
222
|
// strings. Selected ISOs not yet present in `items` (list still loading)
|
|
188
223
|
// fall back to a stub so chips don't vanish on first render.
|
|
@@ -218,13 +253,13 @@ export function CountryMultiCombobox({
|
|
|
218
253
|
}
|
|
219
254
|
</ComboboxValue>
|
|
220
255
|
<ComboboxChipsInput
|
|
221
|
-
placeholder={placeholder ?? t('admin.
|
|
256
|
+
placeholder={placeholder ?? t('admin.components.country_combobox.search_placeholder')}
|
|
222
257
|
value={inputValue}
|
|
223
258
|
onChange={(e) => setInputValue((e.target as HTMLInputElement).value)}
|
|
224
259
|
/>
|
|
225
260
|
</ComboboxChips>
|
|
226
261
|
<ComboboxContent anchor={anchorRef}>
|
|
227
|
-
<ComboboxEmpty>{emptyText ?? t('admin.
|
|
262
|
+
<ComboboxEmpty>{emptyText ?? t('admin.components.country_combobox.empty')}</ComboboxEmpty>
|
|
228
263
|
<ComboboxList>
|
|
229
264
|
{(country: CountryOption) => (
|
|
230
265
|
<ComboboxItem key={country.iso} value={country}>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { State } from '@spree/admin-sdk'
|
|
2
2
|
import {
|
|
3
3
|
Combobox,
|
|
4
|
+
ComboboxButtonTrigger,
|
|
4
5
|
ComboboxContent,
|
|
5
6
|
ComboboxEmpty,
|
|
6
|
-
ComboboxInput,
|
|
7
7
|
ComboboxItem,
|
|
8
8
|
ComboboxList,
|
|
9
|
+
ComboboxSearch,
|
|
10
|
+
ComboboxTriggerPlaceholder,
|
|
9
11
|
} from '@spree/dashboard-ui'
|
|
10
12
|
import { useMemo } from 'react'
|
|
11
13
|
import { useTranslation } from 'react-i18next'
|
|
@@ -18,11 +20,11 @@ type StateOption = { abbr: string; name: string }
|
|
|
18
20
|
* when the country doesn't enumerate them (free-text region); `statesRequired`
|
|
19
21
|
* tells callers whether to fall back to a plain text input.
|
|
20
22
|
*/
|
|
21
|
-
export function useCountryStates(
|
|
23
|
+
export function useCountryStates(countryCode: string | null | undefined) {
|
|
22
24
|
const { countries } = useCountries()
|
|
23
25
|
const country = useMemo(
|
|
24
|
-
() => countries.find((c) => c.iso ===
|
|
25
|
-
[countries,
|
|
26
|
+
() => countries.find((c) => c.iso === countryCode) ?? null,
|
|
27
|
+
[countries, countryCode],
|
|
26
28
|
)
|
|
27
29
|
return {
|
|
28
30
|
states: ((country?.states ?? []) as StateOption[]).filter((s) => Boolean(s.abbr)),
|
|
@@ -35,23 +37,39 @@ export function useCountryStates(countryIso: string | null | undefined) {
|
|
|
35
37
|
* abbreviation (e.g. "CA"). Callers should hide this and render a free-text
|
|
36
38
|
* Input when `useCountryStates(...).states` is empty.
|
|
37
39
|
*
|
|
38
|
-
*
|
|
40
|
+
* Shares the country picker's button-trigger shape — the two sit side by side
|
|
41
|
+
* in every address form, so they have to look alike, and the state field is
|
|
42
|
+
* exactly as prone to being buried by the browser's saved-address panel.
|
|
43
|
+
*
|
|
44
|
+
* Keyed on `countryCode` so the internal state is reset when the country
|
|
39
45
|
* changes — prevents a stale highlight from a previous country.
|
|
40
46
|
*/
|
|
41
47
|
export function StateCombobox({
|
|
42
|
-
|
|
48
|
+
id,
|
|
49
|
+
countryCode,
|
|
43
50
|
states,
|
|
44
51
|
value,
|
|
45
52
|
onValueChange,
|
|
53
|
+
onBlur,
|
|
46
54
|
placeholder,
|
|
55
|
+
searchPlaceholder,
|
|
56
|
+
invalid,
|
|
47
57
|
disabled = false,
|
|
48
58
|
}: {
|
|
49
|
-
|
|
59
|
+
/** Forwarded to the trigger so a `<FieldLabel htmlFor>` can target it. */
|
|
60
|
+
id?: string
|
|
61
|
+
countryCode: string | null | undefined
|
|
50
62
|
/** State list for the active country (typically from `useCountryStates`). */
|
|
51
63
|
states: Pick<State, 'abbr' | 'name'>[]
|
|
52
64
|
value: string | null | undefined
|
|
53
65
|
onValueChange: (abbr: string) => void
|
|
66
|
+
/** Forwarded to the trigger so RHF's `<Controller>` can track touched state. */
|
|
67
|
+
onBlur?: () => void
|
|
68
|
+
/** Trigger text while nothing is selected. */
|
|
54
69
|
placeholder?: string
|
|
70
|
+
/** Text in the dropdown's search box. */
|
|
71
|
+
searchPlaceholder?: string
|
|
72
|
+
invalid?: boolean
|
|
55
73
|
disabled?: boolean
|
|
56
74
|
}) {
|
|
57
75
|
const { t } = useTranslation()
|
|
@@ -60,18 +78,32 @@ export function StateCombobox({
|
|
|
60
78
|
|
|
61
79
|
return (
|
|
62
80
|
<Combobox
|
|
63
|
-
key={
|
|
81
|
+
key={countryCode ?? 'no-country'}
|
|
64
82
|
items={items}
|
|
65
83
|
value={selected}
|
|
66
84
|
onValueChange={(s: StateOption | null) => onValueChange(s?.abbr ?? '')}
|
|
67
85
|
itemToStringLabel={(s: StateOption | null) => s?.name ?? ''}
|
|
68
86
|
itemToStringValue={(s: StateOption | null) => s?.abbr ?? ''}
|
|
87
|
+
disabled={disabled}
|
|
69
88
|
>
|
|
70
|
-
<
|
|
71
|
-
|
|
89
|
+
<ComboboxButtonTrigger
|
|
90
|
+
id={id}
|
|
91
|
+
onBlur={onBlur}
|
|
72
92
|
disabled={disabled}
|
|
73
|
-
|
|
93
|
+
aria-invalid={invalid || undefined}
|
|
94
|
+
>
|
|
95
|
+
{selected ? (
|
|
96
|
+
<span className="truncate">{selected.name}</span>
|
|
97
|
+
) : (
|
|
98
|
+
<ComboboxTriggerPlaceholder>
|
|
99
|
+
{placeholder ?? t('admin.components.state_combobox.placeholder')}
|
|
100
|
+
</ComboboxTriggerPlaceholder>
|
|
101
|
+
)}
|
|
102
|
+
</ComboboxButtonTrigger>
|
|
74
103
|
<ComboboxContent>
|
|
104
|
+
<ComboboxSearch
|
|
105
|
+
placeholder={searchPlaceholder ?? t('admin.components.state_combobox.search_placeholder')}
|
|
106
|
+
/>
|
|
75
107
|
<ComboboxEmpty>{t('admin.components.state_combobox.empty')}</ComboboxEmpty>
|
|
76
108
|
<ComboboxList>
|
|
77
109
|
{(state: StateOption) => (
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Combobox,
|
|
3
|
+
ComboboxButtonTrigger,
|
|
3
4
|
ComboboxContent,
|
|
4
5
|
ComboboxEmpty,
|
|
5
|
-
ComboboxInput,
|
|
6
6
|
ComboboxItem,
|
|
7
7
|
ComboboxList,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
SelectItem,
|
|
11
|
-
SelectTrigger,
|
|
12
|
-
SelectValue,
|
|
8
|
+
ComboboxSearch,
|
|
9
|
+
ComboboxTriggerPlaceholder,
|
|
13
10
|
} from '@spree/dashboard-ui'
|
|
14
11
|
import { useMemo, useState } from 'react'
|
|
15
12
|
import { useTranslation } from 'react-i18next'
|
|
16
13
|
import { useDisplayName } from '../hooks/use-display-name'
|
|
17
|
-
import {
|
|
14
|
+
import { useOptionalStore } from '../providers/store-provider'
|
|
18
15
|
|
|
19
16
|
/**
|
|
20
17
|
* Every ISO 4217 currency code the runtime knows about — the active-currency
|
|
@@ -33,6 +30,10 @@ export const ALL_CURRENCY_CODES: string[] = (() => {
|
|
|
33
30
|
})()
|
|
34
31
|
|
|
35
32
|
interface CurrencySelectProps {
|
|
33
|
+
/** Classes for the trigger — used to match a neighbouring control's height. */
|
|
34
|
+
triggerClassName?: string
|
|
35
|
+
/** `sm` matches the toolbar controls beside it — see `SelectTrigger`. */
|
|
36
|
+
triggerSize?: 'sm' | 'default'
|
|
36
37
|
/** ID for the trigger — paired with the parent `<FieldLabel htmlFor>`. */
|
|
37
38
|
id?: string
|
|
38
39
|
/** Submit name — a hidden input mirrors the value so plain `FormData` works. */
|
|
@@ -47,18 +48,24 @@ interface CurrencySelectProps {
|
|
|
47
48
|
* Currency codes to pick from. Defaults to the current store's
|
|
48
49
|
* `supported_currencies`. Pass `ALL_CURRENCY_CODES` (or any custom list) for
|
|
49
50
|
* contexts where the merchant chooses a currency the store doesn't yet
|
|
50
|
-
* support — e.g. a market's currency.
|
|
51
|
-
* combobox automatically.
|
|
51
|
+
* support — e.g. a market's currency.
|
|
52
52
|
*/
|
|
53
53
|
options?: string[]
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Announces the field as required to assistive technology (`aria-required`).
|
|
56
|
+
* It does not enforce anything: the trigger is a button, so there is no
|
|
57
|
+
* native constraint validation behind it. The consuming form owns
|
|
58
|
+
* enforcement.
|
|
59
|
+
*/
|
|
55
60
|
required?: boolean
|
|
56
61
|
disabled?: boolean
|
|
62
|
+
/** Text in the dropdown's search box. */
|
|
63
|
+
searchPlaceholder?: string
|
|
64
|
+
invalid?: boolean
|
|
65
|
+
/** Forwarded to the trigger so RHF's `<Controller>` can track touched state. */
|
|
66
|
+
onBlur?: () => void
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
/** Above this many options a plain `<Select>` is unwieldy — switch to search. */
|
|
60
|
-
const SEARCHABLE_THRESHOLD = 12
|
|
61
|
-
|
|
62
69
|
/**
|
|
63
70
|
* Picker for a currency code. Defaults to the current store's
|
|
64
71
|
* `supported_currencies` (and the store's `default_currency`), so callers
|
|
@@ -66,7 +73,11 @@ const SEARCHABLE_THRESHOLD = 12
|
|
|
66
73
|
* Pass `options` (e.g. `ALL_CURRENCY_CODES`) when the merchant may pick any
|
|
67
74
|
* currency, such as a market's currency. Each option reads `CODE — Full Name`
|
|
68
75
|
* (e.g. `USD — US Dollar`), with the name localized to the admin UI language.
|
|
69
|
-
*
|
|
76
|
+
*
|
|
77
|
+
* Always searchable, whatever the list length. The picker used to fall back to
|
|
78
|
+
* a plain `<Select>` under a dozen options, which meant the same field behaved
|
|
79
|
+
* differently from one store to the next — typing to filter worked on a store
|
|
80
|
+
* with many currencies and did nothing on a store with three.
|
|
70
81
|
*/
|
|
71
82
|
export function CurrencySelect({
|
|
72
83
|
id,
|
|
@@ -77,9 +88,22 @@ export function CurrencySelect({
|
|
|
77
88
|
options,
|
|
78
89
|
required,
|
|
79
90
|
disabled,
|
|
91
|
+
searchPlaceholder,
|
|
92
|
+
invalid,
|
|
93
|
+
onBlur,
|
|
94
|
+
triggerClassName,
|
|
95
|
+
triggerSize,
|
|
80
96
|
}: CurrencySelectProps) {
|
|
81
97
|
const { t } = useTranslation()
|
|
82
|
-
|
|
98
|
+
// Optional: the operator's dashboard always has a store, the seller panel
|
|
99
|
+
// has none — its tenant is a seller and the store is derived server-side.
|
|
100
|
+
// Without one the picker renders from the `options` its caller passes,
|
|
101
|
+
// rather than refusing to render at all. It deliberately does NOT fall back
|
|
102
|
+
// to every ISO code: a currency the store does not trade in saves fine and
|
|
103
|
+
// then quotes nothing.
|
|
104
|
+
const store = useOptionalStore()
|
|
105
|
+
const currencies = store?.currencies ?? []
|
|
106
|
+
const defaultCurrency = store?.defaultCurrency ?? ''
|
|
83
107
|
const [internalValue, setInternalValue] = useState(defaultValue ?? defaultCurrency)
|
|
84
108
|
const isControlled = controlledValue !== undefined
|
|
85
109
|
// Controlled callers that pass an empty value still see the store default
|
|
@@ -120,62 +144,53 @@ export function CurrencySelect({
|
|
|
120
144
|
|
|
121
145
|
const hiddenInput = name ? <input type="hidden" name={name} value={value} /> : null
|
|
122
146
|
|
|
123
|
-
if (items.length > SEARCHABLE_THRESHOLD) {
|
|
124
|
-
return (
|
|
125
|
-
<>
|
|
126
|
-
{hiddenInput}
|
|
127
|
-
<Combobox
|
|
128
|
-
items={items}
|
|
129
|
-
value={value}
|
|
130
|
-
onValueChange={(next: string | null) => handleChange(next ?? '')}
|
|
131
|
-
itemToStringLabel={(code: string | null) => (code ? renderOption(code) : '')}
|
|
132
|
-
itemToStringValue={(code: string | null) => code ?? ''}
|
|
133
|
-
filter={filter}
|
|
134
|
-
disabled={disabled}
|
|
135
|
-
>
|
|
136
|
-
<ComboboxInput
|
|
137
|
-
id={id}
|
|
138
|
-
aria-required={required}
|
|
139
|
-
placeholder={t('admin.components.currency_select.placeholder')}
|
|
140
|
-
disabled={disabled}
|
|
141
|
-
/>
|
|
142
|
-
<ComboboxContent>
|
|
143
|
-
<ComboboxEmpty>{t('admin.components.currency_select.empty')}</ComboboxEmpty>
|
|
144
|
-
<ComboboxList>
|
|
145
|
-
{(code: string) => (
|
|
146
|
-
<ComboboxItem key={code} value={code}>
|
|
147
|
-
{renderOption(code)}
|
|
148
|
-
</ComboboxItem>
|
|
149
|
-
)}
|
|
150
|
-
</ComboboxList>
|
|
151
|
-
</ComboboxContent>
|
|
152
|
-
</Combobox>
|
|
153
|
-
</>
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
147
|
return (
|
|
158
148
|
<>
|
|
159
149
|
{/* Hidden input keeps the parent `<form>` submit / FormData path working
|
|
160
150
|
without each caller having to thread the value through state. */}
|
|
161
151
|
{hiddenInput}
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<
|
|
172
|
-
{
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
152
|
+
<Combobox
|
|
153
|
+
items={items}
|
|
154
|
+
value={value}
|
|
155
|
+
onValueChange={(next: string | null) => handleChange(next ?? '')}
|
|
156
|
+
itemToStringLabel={(code: string | null) => (code ? renderOption(code) : '')}
|
|
157
|
+
itemToStringValue={(code: string | null) => code ?? ''}
|
|
158
|
+
filter={filter}
|
|
159
|
+
disabled={disabled}
|
|
160
|
+
>
|
|
161
|
+
<ComboboxButtonTrigger
|
|
162
|
+
id={id}
|
|
163
|
+
size={triggerSize}
|
|
164
|
+
className={triggerClassName}
|
|
165
|
+
onBlur={onBlur}
|
|
166
|
+
disabled={disabled}
|
|
167
|
+
aria-required={required}
|
|
168
|
+
aria-invalid={invalid || undefined}
|
|
169
|
+
>
|
|
170
|
+
{value ? (
|
|
171
|
+
<span className="truncate">{renderOption(value)}</span>
|
|
172
|
+
) : (
|
|
173
|
+
<ComboboxTriggerPlaceholder>
|
|
174
|
+
{t('admin.components.currency_select.placeholder')}
|
|
175
|
+
</ComboboxTriggerPlaceholder>
|
|
176
|
+
)}
|
|
177
|
+
</ComboboxButtonTrigger>
|
|
178
|
+
<ComboboxContent>
|
|
179
|
+
<ComboboxSearch
|
|
180
|
+
placeholder={
|
|
181
|
+
searchPlaceholder ?? t('admin.components.currency_select.search_placeholder')
|
|
182
|
+
}
|
|
183
|
+
/>
|
|
184
|
+
<ComboboxEmpty>{t('admin.components.currency_select.empty')}</ComboboxEmpty>
|
|
185
|
+
<ComboboxList>
|
|
186
|
+
{(code: string) => (
|
|
187
|
+
<ComboboxItem key={code} value={code}>
|
|
188
|
+
{renderOption(code)}
|
|
189
|
+
</ComboboxItem>
|
|
190
|
+
)}
|
|
191
|
+
</ComboboxList>
|
|
192
|
+
</ComboboxContent>
|
|
193
|
+
</Combobox>
|
|
179
194
|
</>
|
|
180
195
|
)
|
|
181
196
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ExportType } from '@spree/admin-sdk'
|
|
2
1
|
import {
|
|
3
2
|
Button,
|
|
4
3
|
Dialog,
|
|
@@ -16,8 +15,8 @@ import {
|
|
|
16
15
|
RadioGroup,
|
|
17
16
|
RadioGroupItem,
|
|
18
17
|
} from '@spree/dashboard-ui'
|
|
18
|
+
import { DownloadIcon, FilterIcon, GlobeIcon } from '@spree/dashboard-ui/icons'
|
|
19
19
|
import type { TFunction } from 'i18next'
|
|
20
|
-
import { DownloadIcon, FilterIcon, GlobeIcon } from 'lucide-react'
|
|
21
20
|
import { useState } from 'react'
|
|
22
21
|
import { useTranslation } from 'react-i18next'
|
|
23
22
|
import { useExport } from '../hooks/use-export'
|
|
@@ -25,14 +24,61 @@ import { filtersToRansack } from '../lib/filters-to-ransack'
|
|
|
25
24
|
import type { ResourceActionsContext } from './resource-table'
|
|
26
25
|
|
|
27
26
|
interface ExportButtonProps extends ResourceActionsContext {
|
|
28
|
-
/**
|
|
29
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Which dataset to export. A plain string rather than either SDK's union:
|
|
29
|
+
* the operator's registry and a seller's allowlist are different sets, and
|
|
30
|
+
* the server is what validates the value in both panels.
|
|
31
|
+
*/
|
|
32
|
+
type: string
|
|
30
33
|
/** Label shown on the button. Defaults to the translated "Export" action. */
|
|
31
34
|
label?: string
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
type Selection = 'filtered' | 'all'
|
|
35
38
|
|
|
39
|
+
interface ExportRecordButtonProps {
|
|
40
|
+
/** Which dataset to export; see `ExportButton`. */
|
|
41
|
+
type: string
|
|
42
|
+
/** Ransack filter naming the records, as `search_params` on the export. */
|
|
43
|
+
searchParams: Record<string, unknown>
|
|
44
|
+
/** Label shown on the button. Defaults to the translated "Export" action. */
|
|
45
|
+
label?: string
|
|
46
|
+
/** Optional size for the button. Defaults to "sm". */
|
|
47
|
+
size?: 'sm' | 'default' | 'lg'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Export from a record's own page — one price list's prices, one promotion's
|
|
52
|
+
* coupon codes. The page has already decided what the file holds, so one
|
|
53
|
+
* click queues it; the filtered-or-all dialog is the table's concern.
|
|
54
|
+
*/
|
|
55
|
+
export function ExportRecordButton({ type, searchParams, label, size }: ExportRecordButtonProps) {
|
|
56
|
+
const { t } = useTranslation()
|
|
57
|
+
const exportMutation = useExport()
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Button
|
|
61
|
+
type="button"
|
|
62
|
+
size={size ?? 'sm'}
|
|
63
|
+
variant="outline"
|
|
64
|
+
onClick={() =>
|
|
65
|
+
exportMutation.mutate({
|
|
66
|
+
type,
|
|
67
|
+
record_selection: 'filtered',
|
|
68
|
+
search_params: searchParams,
|
|
69
|
+
results_url: window.location.href,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
disabled={exportMutation.isPending}
|
|
73
|
+
>
|
|
74
|
+
<DownloadIcon className="size-4" />
|
|
75
|
+
{exportMutation.isPending
|
|
76
|
+
? t('admin.actions.exporting')
|
|
77
|
+
: (label ?? t('admin.actions.export'))}
|
|
78
|
+
</Button>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
36
82
|
export function ExportButton({
|
|
37
83
|
type,
|
|
38
84
|
label,
|
|
@@ -59,16 +105,27 @@ export function ExportButton({
|
|
|
59
105
|
type,
|
|
60
106
|
record_selection: selection,
|
|
61
107
|
search_params: selection === 'filtered' ? search_params : undefined,
|
|
108
|
+
// Where the done email sends the user back to, for an export too slow to
|
|
109
|
+
// wait on. Read off the live location rather than built from a
|
|
110
|
+
// configured host: a panel may be mounted under a path (`/sellers`,
|
|
111
|
+
// `/dashboard`) as easily as on a host of its own, and only the page
|
|
112
|
+
// itself knows which. The store's allowed origins decide whether the
|
|
113
|
+
// server trusts it.
|
|
114
|
+
results_url: window.location.href,
|
|
62
115
|
})
|
|
63
116
|
setOpen(false)
|
|
64
117
|
}
|
|
65
118
|
|
|
66
119
|
return (
|
|
67
120
|
<>
|
|
121
|
+
{/* Desktop only: picking a dataset, waiting on the job and opening
|
|
122
|
+
the file is desk work, and the button is one of the things that
|
|
123
|
+
pushed the toolbar onto a third row on a phone. */}
|
|
68
124
|
<Button
|
|
125
|
+
type="button"
|
|
69
126
|
size="sm"
|
|
70
127
|
variant="outline"
|
|
71
|
-
className="h-[2.125rem]"
|
|
128
|
+
className="hidden h-[2.125rem] lg:inline-flex"
|
|
72
129
|
onClick={() => setOpen(true)}
|
|
73
130
|
disabled={exportMutation.isPending}
|
|
74
131
|
>
|