@spree/seller-dashboard 6.0.0 → 6.0.2

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.
@@ -1,17 +1,4 @@
1
1
  {
2
- "accept_invitation": {
3
- "failed": "无法接受该邀请。邀请可能已过期,或密码有误。",
4
- "first_name": "名字",
5
- "invalid_link": "此链接缺少令牌。请从邀请邮件中打开链接。",
6
- "last_name": "姓氏",
7
- "not_found": "此邀请已失效。请让邀请你的人重新发送一份。",
8
- "password": "密码",
9
- "password_confirmation": "确认密码",
10
- "submit": "接受邀请",
11
- "submitting": "正在接受…",
12
- "subtitle": "你以 {{email}} 的身份受邀。设置密码即可完成。",
13
- "title": "加入团队"
14
- },
15
2
  "account": {
16
3
  "avatar": "照片",
17
4
  "avatar_help": "在面板中显示在您的姓名旁边。方形图片效果最佳。",
@@ -1,26 +1,14 @@
1
- import { useAuth } from '@spree/dashboard-core'
2
- import { Button, Field, FieldError, FieldGroup, FieldLabel, Input } from '@spree/dashboard-ui'
3
- import { useQuery } from '@tanstack/react-query'
1
+ import { InvitationAcceptance } from '@spree/dashboard-core'
4
2
  import { useNavigate } from '@tanstack/react-router'
5
- import { useState } from 'react'
6
- import { useForm } from 'react-hook-form'
7
- import { useTranslation } from 'react-i18next'
8
3
  import { sellerClient } from '../api-client'
9
- import { CenteredMessage } from '../components/centered-message'
10
-
11
- interface AcceptValues {
12
- password: string
13
- password_confirmation: string
14
- first_name: string
15
- last_name: string
16
- }
17
4
 
18
5
  /**
19
6
  * Joining a seller's team from the emailed link.
20
7
  *
21
- * The invitation id and token in the URL are the whole credential — nobody is
22
- * signed in yet. Accepting returns a seller session, so the new member lands
23
- * in the panel rather than being bounced to a login form.
8
+ * The screens are the framework's, shared with the operator's dashboard; what
9
+ * is the panel's own is which API answers the lookup and where an accepted
10
+ * member lands. That is the seller they joined: the layout under that route
11
+ * loads the member's permissions for it.
24
12
  */
25
13
  export function AcceptInvitationPage({
26
14
  invitationId,
@@ -29,98 +17,31 @@ export function AcceptInvitationPage({
29
17
  invitationId: string
30
18
  token?: string
31
19
  }) {
32
- const { t } = useTranslation()
33
- const { acceptInvitation } = useAuth()
34
20
  const navigate = useNavigate()
35
- const [error, setError] = useState<string | null>(null)
36
-
37
- const {
38
- data: invitation,
39
- isLoading,
40
- error: lookupError,
41
- } = useQuery({
42
- queryKey: ['seller', 'invitation', invitationId],
43
- queryFn: () => sellerClient().auth.lookupInvitation(invitationId, token ?? ''),
44
- enabled: Boolean(token),
45
- retry: false,
46
- })
47
-
48
- const form = useForm<AcceptValues>({
49
- defaultValues: { password: '', password_confirmation: '', first_name: '', last_name: '' },
50
- })
51
-
52
- if (!token) return <CenteredMessage>{t('accept_invitation.invalid_link')}</CenteredMessage>
53
- if (isLoading) return <CenteredMessage>{t('common.loading')}</CenteredMessage>
54
- if (lookupError || !invitation) {
55
- return <CenteredMessage>{t('accept_invitation.not_found')}</CenteredMessage>
56
- }
57
-
58
- async function onSubmit(values: AcceptValues) {
59
- setError(null)
60
- try {
61
- await acceptInvitation(invitationId, token ?? '', values)
62
- // The index route decides where to go from there — it already knows how
63
- // to skip the picker for someone who runs exactly one seller. Without
64
- // this the form simply sits there after a successful accept, and the
65
- // obvious next move is to submit again, against an invitation that has
66
- // now been consumed.
67
- navigate({ to: '/', replace: true })
68
- } catch {
69
- setError(t('accept_invitation.failed'))
70
- }
71
- }
72
-
73
- const { errors, isSubmitting } = form.formState
74
21
 
75
22
  return (
76
23
  <div className="flex min-h-screen items-center justify-center p-6">
77
- <div className="w-full max-w-sm">
78
- <h1 className="font-medium text-xl">{t('accept_invitation.title')}</h1>
79
- <p className="mt-1 mb-6 text-muted-foreground text-sm">
80
- {t('accept_invitation.subtitle', { email: invitation.email })}
81
- </p>
82
-
83
- <form onSubmit={form.handleSubmit(onSubmit)}>
84
- <FieldGroup>
85
- <Field>
86
- <FieldLabel htmlFor="first_name">{t('accept_invitation.first_name')}</FieldLabel>
87
- <Input id="first_name" {...form.register('first_name')} />
88
- </Field>
89
-
90
- <Field>
91
- <FieldLabel htmlFor="last_name">{t('accept_invitation.last_name')}</FieldLabel>
92
- <Input id="last_name" {...form.register('last_name')} />
93
- </Field>
94
-
95
- <Field>
96
- <FieldLabel htmlFor="password">{t('accept_invitation.password')}</FieldLabel>
97
- <Input
98
- id="password"
99
- type="password"
100
- aria-invalid={!!errors.password || undefined}
101
- {...form.register('password', { required: true })}
102
- />
103
- <FieldError errors={[errors.password]} />
104
- </Field>
105
-
106
- <Field>
107
- <FieldLabel htmlFor="password_confirmation">
108
- {t('accept_invitation.password_confirmation')}
109
- </FieldLabel>
110
- <Input
111
- id="password_confirmation"
112
- type="password"
113
- {...form.register('password_confirmation')}
114
- />
115
- </Field>
116
-
117
- {error && <p className="text-destructive text-sm">{error}</p>}
118
-
119
- <Button type="submit" disabled={isSubmitting}>
120
- {isSubmitting ? t('accept_invitation.submitting') : t('accept_invitation.submit')}
121
- </Button>
122
- </FieldGroup>
123
- </form>
24
+ <div className="flex w-full max-w-sm flex-col gap-6">
25
+ <InvitationAcceptance
26
+ invitationId={invitationId}
27
+ token={token}
28
+ lookup={async (id, token) => {
29
+ const invitation = await sellerClient().auth.lookupInvitation(id, token)
30
+ // Carried by the lookup alone; the team listings leave it out.
31
+ return {
32
+ ...invitation,
33
+ invitee_exists: invitation.invitee_exists ?? false,
34
+ target: invitation.seller,
35
+ }
36
+ }}
37
+ onAccepted={(invitation) =>
38
+ navigate({
39
+ to: '/$sellerId',
40
+ params: { sellerId: invitation.target.id },
41
+ replace: true,
42
+ })
43
+ }
44
+ />
124
45
  </div>
125
46
  </div>
126
47
  )
@@ -24,15 +24,13 @@ import {
24
24
  Progress,
25
25
  ScrollArea,
26
26
  StatusBadge as SharedStatusBadge,
27
+ StepIcon,
27
28
  Textarea,
28
29
  toastManager,
29
30
  } from '@spree/dashboard-ui'
30
31
  import {
31
- CheckCircle2Icon,
32
32
  ChevronRightIcon,
33
33
  CircleAlertIcon,
34
- CircleIcon,
35
- ClockIcon,
36
34
  ExternalLinkIcon,
37
35
  XCircleIcon,
38
36
  } from '@spree/dashboard-ui/icons'
@@ -517,17 +515,14 @@ function panelRoute(kind: string): string | undefined {
517
515
  }
518
516
  }
519
517
 
518
+ // The operator's setup checklist icons, plus a submission sent back.
520
519
  function StatusIcon({ status }: { status: string }) {
521
- if (status === 'complete') {
522
- return <CheckCircle2Icon className="size-5 shrink-0 text-success" />
523
- }
524
- if (status === 'pending') {
525
- return <ClockIcon className="size-5 shrink-0 text-warning" />
526
- }
520
+ if (status === 'complete') return <StepIcon status="done" className="size-5" />
521
+ if (status === 'pending') return <StepIcon status="in_progress" className="size-5" />
527
522
  if (status === 'rejected') {
528
523
  return <XCircleIcon className="size-5 shrink-0 text-destructive" />
529
524
  }
530
- return <CircleIcon className="size-5 shrink-0 text-muted-foreground" />
525
+ return <StepIcon status="todo" className="size-5" />
531
526
  }
532
527
 
533
528
  function StatusBadge({ status }: { status: string }) {