@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/src/pages/order.tsx
CHANGED
|
@@ -37,10 +37,12 @@ export function OrderPage() {
|
|
|
37
37
|
if (isError) return <RetryableError onRetry={() => refetch()} />
|
|
38
38
|
if (!order) return <CenteredMessage>{t('orders.not_found')}</CenteredMessage>
|
|
39
39
|
|
|
40
|
-
const cancelable = order.status !== 'canceled'
|
|
41
40
|
// Nothing can come back from an order that never went out, so the post-sale
|
|
42
41
|
// cards would only render empty with a disabled button.
|
|
43
42
|
const placed = !!order.completed_at
|
|
43
|
+
// Only a placed order can be cancelled — offering it on a draft gives the
|
|
44
|
+
// seller an action the backend always refuses.
|
|
45
|
+
const cancelable = placed && order.status !== 'canceled'
|
|
44
46
|
|
|
45
47
|
return (
|
|
46
48
|
<>
|
|
@@ -15,7 +15,7 @@ export function SellerPicker() {
|
|
|
15
15
|
const { t } = useTranslation()
|
|
16
16
|
const { data, isLoading, error } = useQuery({
|
|
17
17
|
queryKey: ['seller', 'me'],
|
|
18
|
-
queryFn: () => sellerClient().me(),
|
|
18
|
+
queryFn: () => sellerClient().me.get(),
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
if (isLoading) return <CenteredMessage>{t('common.loading')}</CenteredMessage>
|
package/src/pages/team.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Can, getInitials, PageHeader, Slot } from '@spree/dashboard-core'
|
|
1
|
+
import { Can, getInitials, PageHeader, Slot, usePermissions } from '@spree/dashboard-core'
|
|
2
2
|
import {
|
|
3
3
|
Avatar,
|
|
4
4
|
AvatarFallback,
|
|
@@ -283,6 +283,7 @@ function InvitationRow({ invitation }: { invitation: Invitation }) {
|
|
|
283
283
|
const queryClient = useQueryClient()
|
|
284
284
|
const confirm = useConfirm()
|
|
285
285
|
const { copy } = useCopyToClipboard()
|
|
286
|
+
const { permissions } = usePermissions()
|
|
286
287
|
|
|
287
288
|
const invalidate = () =>
|
|
288
289
|
queryClient.invalidateQueries({ queryKey: ['seller', sellerId, 'invitations'] })
|
|
@@ -313,20 +314,37 @@ function InvitationRow({ invitation }: { invitation: Invitation }) {
|
|
|
313
314
|
}),
|
|
314
315
|
})
|
|
315
316
|
|
|
317
|
+
// The link carries the invitation's token, so it is fetched on demand
|
|
318
|
+
// rather than shipped with the listing.
|
|
319
|
+
const acceptanceLink = useMutation({
|
|
320
|
+
mutationFn: (id: string) => sellerClient().invitations.acceptanceLink(id),
|
|
321
|
+
})
|
|
322
|
+
|
|
316
323
|
async function handleCopyLink() {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
324
|
+
try {
|
|
325
|
+
const { acceptance_url } = await acceptanceLink.mutateAsync(invitation.id)
|
|
326
|
+
// Path-only when no panel origin is configured; resolve against wherever
|
|
327
|
+
// this panel is actually mounted. `BASE_URL` matters: served at /sellers
|
|
328
|
+
// (the single-node topology), origin alone would produce a link that
|
|
329
|
+
// misses the mount and 404s.
|
|
330
|
+
const url = acceptance_url.startsWith('/')
|
|
331
|
+
? new URL(
|
|
332
|
+
`.${acceptance_url}`,
|
|
333
|
+
`${window.location.origin}${import.meta.env.BASE_URL}`,
|
|
334
|
+
).toString()
|
|
335
|
+
: acceptance_url
|
|
336
|
+
|
|
337
|
+
if (!(await copy(url))) {
|
|
338
|
+
toastManager.add({ type: 'error', title: t('common.error') })
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
toastManager.add({ type: 'success', title: t('team.messages.link_copied') })
|
|
342
|
+
} catch (err) {
|
|
343
|
+
toastManager.add({
|
|
344
|
+
type: 'error',
|
|
345
|
+
title: err instanceof Error ? err.message : t('common.error'),
|
|
346
|
+
})
|
|
347
|
+
}
|
|
330
348
|
}
|
|
331
349
|
|
|
332
350
|
async function handleRevoke() {
|
|
@@ -371,6 +389,8 @@ function InvitationRow({ invitation }: { invitation: Invitation }) {
|
|
|
371
389
|
key: 'copy',
|
|
372
390
|
label: t('team.actions.copy_link'),
|
|
373
391
|
icon: <LinkIcon className="size-4" />,
|
|
392
|
+
visible: permissions.can('update', 'seller_profile'),
|
|
393
|
+
disabled: acceptanceLink.isPending,
|
|
374
394
|
onSelect: handleCopyLink,
|
|
375
395
|
},
|
|
376
396
|
{
|
package/src/panel-chrome.tsx
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { AppShell, AppShellProvider, TenantProvider } from '@spree/dashboard-core'
|
|
2
2
|
import { useParams, useRouterState } from '@tanstack/react-router'
|
|
3
|
-
import type
|
|
3
|
+
import { type ReactNode, useState } from 'react'
|
|
4
|
+
import { AccountDialog } from './components/account-dialog'
|
|
4
5
|
import { SellerSwitcher } from './components/seller-switcher'
|
|
6
|
+
import { getAvailableUiLocales } from './i18n'
|
|
7
|
+
|
|
8
|
+
// Derived once from the shipped locale bundles — stable for the panel's life.
|
|
9
|
+
const UI_LOCALES = getAvailableUiLocales()
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* The panel's frame: the nav rail, and the page as a sheet beside it.
|
|
@@ -20,6 +25,10 @@ export function PanelChrome({ children }: { children: ReactNode }) {
|
|
|
20
25
|
const inSettings = useRouterState({
|
|
21
26
|
select: (state) => state.location.pathname.startsWith(`/${sellerId}/settings`),
|
|
22
27
|
})
|
|
28
|
+
// The account is edited in a dialog rather than on a page, so the frame owns
|
|
29
|
+
// its open state — the trigger sits in the sidebar's account menu, which is
|
|
30
|
+
// mounted here and stays put across route changes.
|
|
31
|
+
const [accountOpen, setAccountOpen] = useState(false)
|
|
23
32
|
|
|
24
33
|
return (
|
|
25
34
|
// Scopes every shared query key to this seller, the way `StoreProvider`
|
|
@@ -27,7 +36,14 @@ export function PanelChrome({ children }: { children: ReactNode }) {
|
|
|
27
36
|
// survive a switch and be shown under the next seller.
|
|
28
37
|
<TenantProvider id={sellerId}>
|
|
29
38
|
<AppShellProvider>
|
|
30
|
-
<AppShell
|
|
39
|
+
<AppShell
|
|
40
|
+
tenantId={sellerId}
|
|
41
|
+
sidebarHeader={<SellerSwitcher />}
|
|
42
|
+
inSettings={inSettings}
|
|
43
|
+
uiLocales={UI_LOCALES}
|
|
44
|
+
onEditProfile={() => setAccountOpen(true)}
|
|
45
|
+
>
|
|
46
|
+
<AccountDialog open={accountOpen} onOpenChange={setAccountOpen} />
|
|
31
47
|
{children}
|
|
32
48
|
</AppShell>
|
|
33
49
|
</AppShellProvider>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { accountFormToParams, isUiLocale } from '@spree/dashboard-core'
|
|
2
|
+
import type { AccountUpdateParams, MeResponse } from '@spree/seller-sdk'
|
|
3
|
+
import { z } from 'zod/v4'
|
|
4
|
+
|
|
5
|
+
// The signed-in person's own account (PATCH /me) — not the seller business
|
|
6
|
+
// they act for, which is `pages/profile.tsx`. Every field is optional: the
|
|
7
|
+
// panel asks for a name but does not require one.
|
|
8
|
+
//
|
|
9
|
+
// The panel's languages are NOT listed here. They are derived at render time
|
|
10
|
+
// from the locale bundles the panel ships (see `getAvailableUiLocales`), which
|
|
11
|
+
// is what it can actually display — the API knows nothing about that.
|
|
12
|
+
//
|
|
13
|
+
// The avatar is a small state machine: untouched (omit on save), uploaded
|
|
14
|
+
// (send the signed id), or cleared (send null to purge). `avatar_signed_id`
|
|
15
|
+
// carries a freshly direct-uploaded blob, `avatar_preview_url` a transient
|
|
16
|
+
// object URL for the just-picked file, and `avatar_cleared` flags a removal of
|
|
17
|
+
// the persisted photo.
|
|
18
|
+
export const accountFormSchema = z.object({
|
|
19
|
+
first_name: z.string().nullable().optional(),
|
|
20
|
+
last_name: z.string().nullable().optional(),
|
|
21
|
+
selected_locale: z.string().nullable().optional(),
|
|
22
|
+
avatar_signed_id: z.string().nullable(),
|
|
23
|
+
avatar_preview_url: z.string().nullable(),
|
|
24
|
+
avatar_cleared: z.boolean(),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export type AccountFormValues = z.infer<typeof accountFormSchema>
|
|
28
|
+
|
|
29
|
+
/** Hydrate the form from the current account (the `/me` response). */
|
|
30
|
+
export function accountToForm(me: MeResponse, fallbackLocale: string): AccountFormValues {
|
|
31
|
+
return {
|
|
32
|
+
first_name: me.user.first_name ?? '',
|
|
33
|
+
last_name: me.user.last_name ?? '',
|
|
34
|
+
// A language saved from another panel may not be one this panel ships;
|
|
35
|
+
// preselecting it would show a raw code and save it straight back.
|
|
36
|
+
selected_locale:
|
|
37
|
+
me.user.selected_locale && isUiLocale(me.user.selected_locale)
|
|
38
|
+
? me.user.selected_locale
|
|
39
|
+
: fallbackLocale,
|
|
40
|
+
avatar_signed_id: null,
|
|
41
|
+
avatar_preview_url: null,
|
|
42
|
+
avatar_cleared: false,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Map the form to the PATCH /me params (drops the frontend-only fields). */
|
|
47
|
+
export function accountToParams(values: AccountFormValues): AccountUpdateParams {
|
|
48
|
+
return accountFormToParams(values)
|
|
49
|
+
}
|