@spree/seller-dashboard 1.0.0-beta.2 → 1.0.0-beta.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 +5 -5
- package/src/api-client.ts +5 -1
- package/src/components/account-dialog.tsx +270 -0
- package/src/components/seller-switcher.tsx +1 -1
- package/src/hooks/use-account.ts +49 -0
- package/src/i18n.ts +28 -1
- package/src/index.ts +20 -5
- package/src/locales/ar.json +867 -0
- package/src/locales/de.json +855 -0
- package/src/locales/en.json +14 -0
- package/src/locales/fr.json +855 -0
- package/src/locales/pl.json +861 -0
- package/src/locales/zh-CN.json +852 -0
- package/src/pages/order.tsx +3 -1
- package/src/pages/seller-picker.tsx +1 -1
- package/src/pages/team.tsx +34 -14
- package/src/panel-chrome.tsx +18 -2
- package/src/routes/_authenticated/index.tsx +1 -1
- package/src/schemas/account.ts +49 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spree/seller-dashboard",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Spree marketplace seller panel — the seller-facing counterpart to the admin dashboard.",
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
"@tanstack/router-plugin": "^1.167.35",
|
|
46
46
|
"@tanstack/virtual-file-routes": "^1.162.0",
|
|
47
47
|
"i18next": "^26.2.0",
|
|
48
|
-
"lucide-react": "^
|
|
48
|
+
"lucide-react": "^1.47.0",
|
|
49
49
|
"react-hook-form": "^7.65.0",
|
|
50
50
|
"react-i18next": "^17.0.8",
|
|
51
51
|
"zod": "^4.4.3",
|
|
52
|
-
"@spree/dashboard-core": "1.0.0-beta.
|
|
53
|
-
"@spree/
|
|
54
|
-
"@spree/
|
|
52
|
+
"@spree/dashboard-core": "1.0.0-beta.4",
|
|
53
|
+
"@spree/dashboard-ui": "1.0.0-beta.4",
|
|
54
|
+
"@spree/seller-sdk": "1.0.0-beta.3"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"react": "^19.0.0",
|
package/src/api-client.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function createSellerApiClient({
|
|
|
56
56
|
// and 403 them against a seller they may hold no role on.
|
|
57
57
|
clearTenant: forgetSeller,
|
|
58
58
|
fetchPermissions: async () => {
|
|
59
|
-
const response = await sellerClient().me()
|
|
59
|
+
const response = await sellerClient().me.get()
|
|
60
60
|
|
|
61
61
|
// Both shapes, exactly as the operator's dashboard receives them. The
|
|
62
62
|
// framework's `<Can>` reads CanCanCan rules; the key list is what the
|
|
@@ -64,6 +64,10 @@ export function createSellerApiClient({
|
|
|
64
64
|
// did) left every `<Can>` on the panel answering false — silently.
|
|
65
65
|
return { rules: response.permissions ?? [], keys: response.permission_keys ?? [] }
|
|
66
66
|
},
|
|
67
|
+
// The account menu's language switcher and the profile dialog both write
|
|
68
|
+
// through this, so neither reaches for the Admin API from a panel holding
|
|
69
|
+
// only a seller credential.
|
|
70
|
+
updateAccount: (params) => sellerClient().me.update(params),
|
|
67
71
|
// The shared address form reads countries through the registered client,
|
|
68
72
|
// so it works in a panel that has no admin credential.
|
|
69
73
|
listCountries: () => sellerClient().countries.list(),
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
|
+
import {
|
|
3
|
+
ImageUploadField,
|
|
4
|
+
i18n,
|
|
5
|
+
mapSpreeErrorsToForm,
|
|
6
|
+
switchLocale,
|
|
7
|
+
useAuth,
|
|
8
|
+
} from '@spree/dashboard-core'
|
|
9
|
+
import {
|
|
10
|
+
Button,
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogBody,
|
|
13
|
+
DialogContent,
|
|
14
|
+
DialogDescription,
|
|
15
|
+
DialogFooter,
|
|
16
|
+
DialogHeader,
|
|
17
|
+
DialogTitle,
|
|
18
|
+
ErrorState,
|
|
19
|
+
Field,
|
|
20
|
+
FieldError,
|
|
21
|
+
FieldLabel,
|
|
22
|
+
Input,
|
|
23
|
+
Select,
|
|
24
|
+
SelectContent,
|
|
25
|
+
SelectItem,
|
|
26
|
+
SelectTrigger,
|
|
27
|
+
SelectValue,
|
|
28
|
+
Skeleton,
|
|
29
|
+
toastManager,
|
|
30
|
+
} from '@spree/dashboard-ui'
|
|
31
|
+
import type { MeResponse } from '@spree/seller-sdk'
|
|
32
|
+
import { useEffect, useMemo } from 'react'
|
|
33
|
+
import { Controller, useForm } from 'react-hook-form'
|
|
34
|
+
import { useTranslation } from 'react-i18next'
|
|
35
|
+
import { useAccount, useUpdateAccount } from '../hooks/use-account'
|
|
36
|
+
import { getAvailableUiLocales } from '../i18n'
|
|
37
|
+
import {
|
|
38
|
+
type AccountFormValues,
|
|
39
|
+
accountFormSchema,
|
|
40
|
+
accountToForm,
|
|
41
|
+
accountToParams,
|
|
42
|
+
} from '../schemas/account'
|
|
43
|
+
|
|
44
|
+
// The language the panel is currently displaying — persisted in localStorage
|
|
45
|
+
// and applied by i18next at boot. Used as the fallback when the account has no
|
|
46
|
+
// saved `selected_locale`, so the picker reflects what the person actually
|
|
47
|
+
// sees (and a save persists it) rather than starting empty and matching no
|
|
48
|
+
// option.
|
|
49
|
+
function currentUiLocale(): string {
|
|
50
|
+
const available = getAvailableUiLocales().map((locale) => locale.code)
|
|
51
|
+
const active = i18n.resolvedLanguage ?? i18n.language
|
|
52
|
+
return active && available.includes(active) ? active : 'en'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Edit-account dialog for the signed-in person — their name, photo and the
|
|
57
|
+
* panel's language. Opened from the sidebar's account menu; it has no page of
|
|
58
|
+
* its own.
|
|
59
|
+
*
|
|
60
|
+
* Not to be confused with the profile page, which is the seller business they
|
|
61
|
+
* act for. This is theirs, and follows them between sellers.
|
|
62
|
+
*/
|
|
63
|
+
export function AccountDialog({
|
|
64
|
+
open,
|
|
65
|
+
onOpenChange,
|
|
66
|
+
}: {
|
|
67
|
+
open: boolean
|
|
68
|
+
onOpenChange: (open: boolean) => void
|
|
69
|
+
}) {
|
|
70
|
+
const { t } = useTranslation()
|
|
71
|
+
// Mounted on every page of the panel; don't fetch until it opens.
|
|
72
|
+
const { data: me, isLoading, error, refetch } = useAccount(open)
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
76
|
+
<DialogContent>
|
|
77
|
+
<DialogHeader>
|
|
78
|
+
<DialogTitle>{t('account.title')}</DialogTitle>
|
|
79
|
+
<DialogDescription>{t('account.subtitle')}</DialogDescription>
|
|
80
|
+
</DialogHeader>
|
|
81
|
+
{error ? (
|
|
82
|
+
<DialogBody>
|
|
83
|
+
<ErrorState
|
|
84
|
+
title={t('account.load_failed_title')}
|
|
85
|
+
description={error instanceof Error ? error.message : undefined}
|
|
86
|
+
onRetry={() => refetch()}
|
|
87
|
+
/>
|
|
88
|
+
</DialogBody>
|
|
89
|
+
) : isLoading || !me ? (
|
|
90
|
+
<DialogBody className="flex flex-col gap-4">
|
|
91
|
+
<Skeleton className="h-16 w-16 rounded-md" />
|
|
92
|
+
<Skeleton className="h-9 w-full" />
|
|
93
|
+
<Skeleton className="h-9 w-full" />
|
|
94
|
+
<Skeleton className="h-9 w-full" />
|
|
95
|
+
</DialogBody>
|
|
96
|
+
) : (
|
|
97
|
+
// Mount the form only once the account has loaded, so `useForm`
|
|
98
|
+
// initializes with concrete string defaults — that keeps the inputs
|
|
99
|
+
// and the Select controlled from their first render.
|
|
100
|
+
<AccountForm me={me} onOpenChange={onOpenChange} />
|
|
101
|
+
)}
|
|
102
|
+
</DialogContent>
|
|
103
|
+
</Dialog>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function AccountForm({
|
|
108
|
+
me,
|
|
109
|
+
onOpenChange,
|
|
110
|
+
}: {
|
|
111
|
+
me: MeResponse
|
|
112
|
+
onOpenChange: (open: boolean) => void
|
|
113
|
+
}) {
|
|
114
|
+
const { t } = useTranslation()
|
|
115
|
+
const { updateUser } = useAuth()
|
|
116
|
+
const updateMutation = useUpdateAccount()
|
|
117
|
+
|
|
118
|
+
const form = useForm<AccountFormValues>({
|
|
119
|
+
resolver: zodResolver(accountFormSchema),
|
|
120
|
+
defaultValues: accountToForm(me, currentUiLocale()),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Re-baseline from the refetched account after a save (to surface the
|
|
124
|
+
// newly-persisted photo as the server image), unless there are unsaved
|
|
125
|
+
// edits in flight.
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (form.formState.isDirty) return
|
|
128
|
+
form.reset(accountToForm(me, currentUiLocale()))
|
|
129
|
+
}, [me, form])
|
|
130
|
+
|
|
131
|
+
// Release the picked photo's object URL when it is replaced or the dialog
|
|
132
|
+
// unmounts. `ImageUploadField` hands the blob URL to the form, so the form
|
|
133
|
+
// owns revoking it — otherwise a `reset` (the re-baseline above) or a close
|
|
134
|
+
// drops the reference without freeing the blob. Double revokes are harmless.
|
|
135
|
+
const avatarPreviewUrl = form.watch('avatar_preview_url')
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!avatarPreviewUrl) return
|
|
138
|
+
return () => URL.revokeObjectURL(avatarPreviewUrl)
|
|
139
|
+
}, [avatarPreviewUrl])
|
|
140
|
+
|
|
141
|
+
const onSubmit = async (values: AccountFormValues) => {
|
|
142
|
+
try {
|
|
143
|
+
const updated = await updateMutation.mutateAsync(accountToParams(values))
|
|
144
|
+
// Reflect the new name and photo in the auth context (the sidebar's
|
|
145
|
+
// account row) straight away, rather than waiting for a token refresh.
|
|
146
|
+
updateUser(updated.user)
|
|
147
|
+
toastManager.add({ type: 'success', title: t('account.saved') })
|
|
148
|
+
// Closing unmounts the form, so the avatar state needs no preserving
|
|
149
|
+
// across the save — reopening re-hydrates from the mutation response
|
|
150
|
+
// that `useUpdateAccount` writes into the cache.
|
|
151
|
+
form.reset({ ...values, avatar_signed_id: null })
|
|
152
|
+
onOpenChange(false)
|
|
153
|
+
// A changed language takes effect by reloading in it.
|
|
154
|
+
const code = values.selected_locale
|
|
155
|
+
if (code && code !== i18n.language) switchLocale(code)
|
|
156
|
+
} catch (err) {
|
|
157
|
+
// Anything that is not a field error has already been toasted by the
|
|
158
|
+
// mutation hook.
|
|
159
|
+
mapSpreeErrorsToForm(err, form.setError)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The panel's own shipped locale bundles, not the backend — those are what
|
|
164
|
+
// it can actually display. Hidden when fewer than two are installed, since
|
|
165
|
+
// there is then nothing to choose.
|
|
166
|
+
const localeOptions = useMemo(
|
|
167
|
+
() => getAvailableUiLocales().map((locale) => ({ value: locale.code, label: locale.name })),
|
|
168
|
+
[],
|
|
169
|
+
)
|
|
170
|
+
const showLanguagePicker = localeOptions.length >= 2
|
|
171
|
+
|
|
172
|
+
const { errors, isDirty, isSubmitting } = form.formState
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="contents">
|
|
176
|
+
<DialogBody className="flex flex-col gap-4">
|
|
177
|
+
{errors.root?.message && (
|
|
178
|
+
<p className="text-destructive text-sm" role="alert">
|
|
179
|
+
{errors.root.message}
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
182
|
+
<ImageUploadField
|
|
183
|
+
square
|
|
184
|
+
serverUrl={me.user.avatar_url}
|
|
185
|
+
label={t('account.avatar')}
|
|
186
|
+
help={t('account.avatar_help')}
|
|
187
|
+
value={{
|
|
188
|
+
signedId: form.watch('avatar_signed_id'),
|
|
189
|
+
previewUrl: form.watch('avatar_preview_url'),
|
|
190
|
+
cleared: form.watch('avatar_cleared'),
|
|
191
|
+
}}
|
|
192
|
+
onChange={(next) => {
|
|
193
|
+
form.setValue('avatar_signed_id', next.signedId, { shouldDirty: true })
|
|
194
|
+
form.setValue('avatar_preview_url', next.previewUrl, { shouldDirty: true })
|
|
195
|
+
form.setValue('avatar_cleared', next.cleared, { shouldDirty: true })
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
<Field>
|
|
199
|
+
<FieldLabel htmlFor="account-email">{t('account.email')}</FieldLabel>
|
|
200
|
+
{/* Email is identity-bound; PATCH /me does not accept it. */}
|
|
201
|
+
<Input id="account-email" type="email" value={me.user.email} disabled />
|
|
202
|
+
</Field>
|
|
203
|
+
<Field>
|
|
204
|
+
<FieldLabel htmlFor="account-first-name">{t('account.first_name')}</FieldLabel>
|
|
205
|
+
<Input
|
|
206
|
+
id="account-first-name"
|
|
207
|
+
aria-invalid={!!errors.first_name || undefined}
|
|
208
|
+
{...form.register('first_name')}
|
|
209
|
+
/>
|
|
210
|
+
<FieldError errors={[errors.first_name]} />
|
|
211
|
+
</Field>
|
|
212
|
+
<Field>
|
|
213
|
+
<FieldLabel htmlFor="account-last-name">{t('account.last_name')}</FieldLabel>
|
|
214
|
+
<Input
|
|
215
|
+
id="account-last-name"
|
|
216
|
+
aria-invalid={!!errors.last_name || undefined}
|
|
217
|
+
{...form.register('last_name')}
|
|
218
|
+
/>
|
|
219
|
+
<FieldError errors={[errors.last_name]} />
|
|
220
|
+
</Field>
|
|
221
|
+
{showLanguagePicker && (
|
|
222
|
+
<Controller
|
|
223
|
+
name="selected_locale"
|
|
224
|
+
control={form.control}
|
|
225
|
+
render={({ field, fieldState }) => (
|
|
226
|
+
<Field>
|
|
227
|
+
<FieldLabel htmlFor="account-language">{t('account.language')}</FieldLabel>
|
|
228
|
+
<Select
|
|
229
|
+
items={localeOptions as never}
|
|
230
|
+
value={field.value}
|
|
231
|
+
onValueChange={field.onChange}
|
|
232
|
+
>
|
|
233
|
+
<SelectTrigger
|
|
234
|
+
id="account-language"
|
|
235
|
+
aria-invalid={!!fieldState.error || undefined}
|
|
236
|
+
>
|
|
237
|
+
<SelectValue />
|
|
238
|
+
</SelectTrigger>
|
|
239
|
+
<SelectContent>
|
|
240
|
+
{localeOptions.map((option) => (
|
|
241
|
+
<SelectItem key={option.value} value={option.value}>
|
|
242
|
+
{option.label}
|
|
243
|
+
</SelectItem>
|
|
244
|
+
))}
|
|
245
|
+
</SelectContent>
|
|
246
|
+
</Select>
|
|
247
|
+
<FieldError errors={[fieldState.error]} />
|
|
248
|
+
</Field>
|
|
249
|
+
)}
|
|
250
|
+
/>
|
|
251
|
+
)}
|
|
252
|
+
</DialogBody>
|
|
253
|
+
<DialogFooter>
|
|
254
|
+
<Button
|
|
255
|
+
type="button"
|
|
256
|
+
variant="outline"
|
|
257
|
+
onClick={() => onOpenChange(false)}
|
|
258
|
+
disabled={isSubmitting}
|
|
259
|
+
>
|
|
260
|
+
{t('common.cancel')}
|
|
261
|
+
</Button>
|
|
262
|
+
{/* Gated on `isDirty`: without it a pristine Save sends a PATCH and
|
|
263
|
+
toasts success having changed nothing. */}
|
|
264
|
+
<Button type="submit" disabled={!isDirty || isSubmitting}>
|
|
265
|
+
{isSubmitting ? t('account.saving') : t('common.save')}
|
|
266
|
+
</Button>
|
|
267
|
+
</DialogFooter>
|
|
268
|
+
</form>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
@@ -42,7 +42,7 @@ export function SellerSwitcher() {
|
|
|
42
42
|
// loaded by the page behind this.
|
|
43
43
|
const { data: me, isLoading } = useQuery({
|
|
44
44
|
queryKey: ['seller', 'me'],
|
|
45
|
-
queryFn: () => sellerClient().me(),
|
|
45
|
+
queryFn: () => sellerClient().me.get(),
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
const { data: profile } = useQuery({
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useResourceMutation } from '@spree/dashboard-core'
|
|
2
|
+
import type { AccountUpdateParams, MeResponse } from '@spree/seller-sdk'
|
|
3
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
4
|
+
import { useTranslation } from 'react-i18next'
|
|
5
|
+
import { sellerClient } from '../api-client'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The signed-in person's own account.
|
|
9
|
+
*
|
|
10
|
+
* Keyed without the seller, unlike almost everything else in this panel: the
|
|
11
|
+
* account is the same record whichever seller they are acting for, so scoping
|
|
12
|
+
* it per seller would refetch it on every switch and cache the same answer
|
|
13
|
+
* several times over.
|
|
14
|
+
*
|
|
15
|
+
* @param enabled Defer the request until it is needed. The dialog is mounted
|
|
16
|
+
* on every page of the panel but only fetches once it opens.
|
|
17
|
+
*/
|
|
18
|
+
export function useAccount(enabled = true) {
|
|
19
|
+
return useQuery({
|
|
20
|
+
queryKey: ACCOUNT_KEY,
|
|
21
|
+
queryFn: () => sellerClient().me.get(),
|
|
22
|
+
enabled,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useUpdateAccount() {
|
|
27
|
+
const { t } = useTranslation()
|
|
28
|
+
const queryClient = useQueryClient()
|
|
29
|
+
|
|
30
|
+
return useResourceMutation<MeResponse, Error, AccountUpdateParams>({
|
|
31
|
+
mutationFn: (params) => sellerClient().me.update(params),
|
|
32
|
+
// The person is also a row on the team page, so their own name and photo
|
|
33
|
+
// there go stale on a self-edit unless that list is refetched. The hook
|
|
34
|
+
// injects the seller at position 1, which is what the team page keys on.
|
|
35
|
+
invalidate: [['seller', 'team']],
|
|
36
|
+
successMessage: false, // the dialog toasts success itself
|
|
37
|
+
// Field-level 422s never toast — the dialog shows them inline — so this
|
|
38
|
+
// covers everything else: a refusal, a server error, a lost connection.
|
|
39
|
+
errorMessage: t('account.save_failed'),
|
|
40
|
+
// Write the response into the cache rather than leaning on invalidation
|
|
41
|
+
// alone: the query is disabled while the dialog is closed, and an
|
|
42
|
+
// invalidated query with no enabled observer goes stale without
|
|
43
|
+
// refetching — so reopening would render the pre-save account, flashing
|
|
44
|
+
// back a photo that was just removed.
|
|
45
|
+
onSuccess: (updated) => queryClient.setQueryData(ACCOUNT_KEY, updated),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ACCOUNT_KEY = ['seller-account'] as const
|
package/src/i18n.ts
CHANGED
|
@@ -10,7 +10,7 @@ import '@spree/dashboard-core/lib/i18n'
|
|
|
10
10
|
// The instance comes from core too, not a bare `i18next` import: a bundled
|
|
11
11
|
// build can resolve a second copy of the package, and `addResourceBundle` on
|
|
12
12
|
// that one throws. Core re-exports the object it initialised for exactly this.
|
|
13
|
-
import { i18n } from '@spree/dashboard-core'
|
|
13
|
+
import { i18n, localesFromBundlePaths, setUiLocales } from '@spree/dashboard-core'
|
|
14
14
|
import en from './locales/en.json'
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -23,4 +23,31 @@ import en from './locales/en.json'
|
|
|
23
23
|
*/
|
|
24
24
|
i18n.addResourceBundle('en', 'translation', en, true, true)
|
|
25
25
|
|
|
26
|
+
// Every other language, imported EAGERLY so the active one registers
|
|
27
|
+
// synchronously at module load — before any table or registry module
|
|
28
|
+
// evaluates its `i18n.t(...)` labels. Switching language reloads the page, so
|
|
29
|
+
// only the booted language's strings are ever resolved; eager keeps that
|
|
30
|
+
// resolution synchronous and flash-free.
|
|
31
|
+
const panelLocales = import.meta.glob<{ default: Record<string, unknown> }>(
|
|
32
|
+
['./locales/*.json', '!./locales/en.json'],
|
|
33
|
+
{ eager: true },
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
for (const [path, mod] of Object.entries(panelLocales)) {
|
|
37
|
+
const code = path.replace('./locales/', '').replace('.json', '')
|
|
38
|
+
i18n.addResourceBundle(code, 'translation', mod.default, true, true)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setUiLocales(getAvailableUiLocales().map(({ code }) => code))
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The languages this panel can display, as `{ code, name }` pairs for the
|
|
45
|
+
* account menu and the account dialog. Derived from the locale bundles the
|
|
46
|
+
* panel ships — not from the API, which knows nothing about what the client
|
|
47
|
+
* can render.
|
|
48
|
+
*/
|
|
49
|
+
export function getAvailableUiLocales(): Array<{ code: string; name: string }> {
|
|
50
|
+
return localesFromBundlePaths(Object.keys(panelLocales))
|
|
51
|
+
}
|
|
52
|
+
|
|
26
53
|
export default i18n
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,26 @@ import './i18n'
|
|
|
9
9
|
import './nav/default'
|
|
10
10
|
import './nav/settings'
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
export * from '@spree/dashboard-core'
|
|
13
|
+
export {
|
|
14
|
+
type DateRange,
|
|
15
|
+
ResourceCombobox,
|
|
16
|
+
type ResourceComboboxProps,
|
|
17
|
+
ResourceMultiAutocomplete,
|
|
18
|
+
type ResourceMultiAutocompleteProps,
|
|
19
|
+
Slot,
|
|
20
|
+
StatusCard,
|
|
21
|
+
} from '@spree/dashboard-core'
|
|
22
|
+
// The single import a marketplace needs: the framework
|
|
23
|
+
// (`@spree/dashboard-core`) and the design system (`@spree/dashboard-ui`),
|
|
24
|
+
// re-exported so a host writing its own pages never has to work out which
|
|
25
|
+
// package an export lives in. Both stay importable directly.
|
|
26
|
+
//
|
|
27
|
+
// `export *` from two modules that both declare a name is an error (TS2308),
|
|
28
|
+
// resolved by naming the winner explicitly above. The framework's version
|
|
29
|
+
// wins: it fetches its own data, where the design system's takes everything
|
|
30
|
+
// as props. Import from `@spree/dashboard-ui` for the presentational half.
|
|
31
|
+
export * from '@spree/dashboard-ui'
|
|
17
32
|
|
|
18
33
|
export {
|
|
19
34
|
createSellerApiClient,
|