@stapel/workspaces-react 0.17.0 → 0.19.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/CHANGELOG.md +218 -0
- package/README.md +39 -8
- package/dist/api/generated/schema.d.ts +187 -60
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/default/ActiveWorkspace.d.ts +43 -0
- package/dist/default/ActiveWorkspace.d.ts.map +1 -0
- package/dist/default/ActiveWorkspace.js +33 -0
- package/dist/default/ActiveWorkspace.js.map +1 -0
- package/dist/default/AuditTrailPane.d.ts +12 -0
- package/dist/default/AuditTrailPane.d.ts.map +1 -0
- package/dist/default/AuditTrailPane.js +154 -0
- package/dist/default/AuditTrailPane.js.map +1 -0
- package/dist/default/InvitationsPane.d.ts +19 -0
- package/dist/default/InvitationsPane.d.ts.map +1 -0
- package/dist/default/InvitationsPane.js +217 -0
- package/dist/default/InvitationsPane.js.map +1 -0
- package/dist/default/InviteAcceptPage.d.ts +7 -0
- package/dist/default/InviteAcceptPage.d.ts.map +1 -1
- package/dist/default/InviteAcceptPage.js +74 -38
- package/dist/default/InviteAcceptPage.js.map +1 -1
- package/dist/default/MembersManager.d.ts +12 -4
- package/dist/default/MembersManager.d.ts.map +1 -1
- package/dist/default/MembersManager.js +320 -84
- package/dist/default/MembersManager.js.map +1 -1
- package/dist/default/RoleSelectField.d.ts +64 -0
- package/dist/default/RoleSelectField.d.ts.map +1 -0
- package/dist/default/RoleSelectField.js +78 -0
- package/dist/default/RoleSelectField.js.map +1 -0
- package/dist/default/WorkspaceSettings.d.ts +7 -1
- package/dist/default/WorkspaceSettings.d.ts.map +1 -1
- package/dist/default/WorkspaceSettings.js +203 -45
- package/dist/default/WorkspaceSettings.js.map +1 -1
- package/dist/default/WorkspacesPage.d.ts +13 -0
- package/dist/default/WorkspacesPage.d.ts.map +1 -0
- package/dist/default/WorkspacesPage.js +109 -0
- package/dist/default/WorkspacesPage.js.map +1 -0
- package/dist/default/index.d.ts +28 -7
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +24 -7
- package/dist/default/index.js.map +1 -1
- package/dist/default/parts.d.ts +104 -0
- package/dist/default/parts.d.ts.map +1 -0
- package/dist/default/parts.js +143 -0
- package/dist/default/parts.js.map +1 -0
- package/dist/headless/Members.d.ts +59 -14
- package/dist/headless/Members.d.ts.map +1 -1
- package/dist/headless/Members.js +35 -10
- package/dist/headless/Members.js.map +1 -1
- package/dist/headless/RoleSelect.d.ts +9 -3
- package/dist/headless/RoleSelect.d.ts.map +1 -1
- package/dist/headless/RoleSelect.js +3 -2
- package/dist/headless/RoleSelect.js.map +1 -1
- package/dist/i18n/es.d.ts +11 -12
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +267 -14
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.es.gen.js +5 -0
- package/dist/i18n/generated/errors.es.gen.js.map +1 -1
- package/dist/i18n/generated/errors.gen.d.ts +30 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.gen.js +15 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -1
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.ru.gen.js +5 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
- package/dist/i18n/keys.d.ts +209 -1
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +410 -4
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts +7 -2
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +209 -4
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model/format.d.ts +56 -0
- package/dist/model/format.d.ts.map +1 -0
- package/dist/model/format.js +125 -0
- package/dist/model/format.js.map +1 -0
- package/dist/model/selection.d.ts +15 -0
- package/dist/model/selection.d.ts.map +1 -1
- package/dist/model/selection.js +18 -1
- package/dist/model/selection.js.map +1 -1
- package/dist/nav/manifest.d.ts +45 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +77 -0
- package/dist/nav/manifest.js.map +1 -0
- package/llms.txt +11 -10
- package/manifest.json +281 -69
- package/package.json +18 -12
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +187 -60
- package/src/default/ActiveWorkspace.tsx +97 -0
- package/src/default/AuditTrailPane.tsx +309 -0
- package/src/default/InvitationsPane.tsx +501 -0
- package/src/default/InviteAcceptPage.tsx +187 -90
- package/src/default/MembersManager.tsx +832 -222
- package/src/default/RoleSelectField.tsx +187 -0
- package/src/default/WorkspaceSettings.tsx +540 -125
- package/src/default/WorkspacesPage.tsx +394 -0
- package/src/default/index.ts +28 -7
- package/src/default/parts.tsx +315 -0
- package/src/headless/Members.tsx +84 -16
- package/src/headless/RoleSelect.tsx +12 -5
- package/src/i18n/es.ts +326 -14
- package/src/i18n/generated/errors.es.gen.ts +5 -0
- package/src/i18n/generated/errors.gen.ts +15 -0
- package/src/i18n/generated/errors.json +37 -0
- package/src/i18n/generated/errors.ru.gen.ts +5 -0
- package/src/i18n/keys.ts +462 -5
- package/src/i18n/ru.ts +259 -5
- package/src/index.ts +1 -0
- package/src/model/format.ts +158 -0
- package/src/model/selection.tsx +19 -1
- package/src/nav/manifest.ts +120 -0
- package/dist/default/ErrorAlert.d.ts +0 -8
- package/dist/default/ErrorAlert.d.ts.map +0 -1
- package/dist/default/ErrorAlert.js +0 -30
- package/dist/default/ErrorAlert.js.map +0 -1
- package/src/default/ErrorAlert.tsx +0 -47
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* - `renderLoginPanel` — the sign-in surface for a registered invitee
|
|
7
7
|
* without a session (drop auth-react's `<AuthPanel/>` in; call
|
|
8
8
|
* `onLoggedIn(email)` — or simply let the `sessionEmail` prop update,
|
|
9
|
-
* the flow re-routes on it automatically).
|
|
9
|
+
* the flow re-routes on it automatically). Unfilled, it renders a
|
|
10
|
+
* `SlotPlaceholder` in dev rather than a hole: an accept page with no way
|
|
11
|
+
* to sign in looks finished, which is the one defect nobody reports.
|
|
10
12
|
* - `renderInitialSetup` — the basic-data step for a freshly created
|
|
11
13
|
* account (drop profiles-react's `InitialSetupPrompt`/`InitialSetupModal`
|
|
12
|
-
* in; call `onDone()`). Omitted → the step auto-skips
|
|
14
|
+
* in; call `onDone()`). Omitted → the step auto-skips, which is a
|
|
15
|
+
* deliberate journey decision, not an unfilled hole.
|
|
13
16
|
*
|
|
14
17
|
* THE GRANT SEAM (§B4): for a NEW user the flow mints a login grant and this
|
|
15
18
|
* page hands it to YOUR `onLoginGrant(grantToken)` — wire it to auth-react's
|
|
@@ -18,18 +21,39 @@
|
|
|
18
21
|
* (`grantExchanged`); a rejection shows a retry. The pairs stay decoupled:
|
|
19
22
|
* this package never imports auth-react.
|
|
20
23
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
+
* ## One primary per screen
|
|
25
|
+
*
|
|
26
|
+
* The visual pass photographed five solid blue buttons on this page, with
|
|
27
|
+
* "Decline" — irreversible, and the one the workspace is told about —
|
|
28
|
+
* indistinguishable from "Accept". Every state here now has at most ONE
|
|
29
|
+
* primary: joining. Declining is a quiet danger link behind a confirm that
|
|
30
|
+
* says what it costs. Failures come from the substrate's `ErrorAlert`, so a
|
|
31
|
+
* 429 on the claim reads "you can request another one in 42 seconds" (the
|
|
32
|
+
* backend's keyed `error.429.invitation_grant_pending` with its `retry_after`)
|
|
33
|
+
* instead of a generic shrug.
|
|
24
34
|
*/
|
|
25
35
|
import { useEffect, useRef, useState } from "react";
|
|
26
36
|
import type { ReactElement, ReactNode } from "react";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
37
|
+
import { Button, Card, Flex, Result, Skeleton, Typography } from "antd";
|
|
38
|
+
import {
|
|
39
|
+
SlotPlaceholder,
|
|
40
|
+
actionAvailable,
|
|
41
|
+
actionBlocked,
|
|
42
|
+
useDescribeFlowError,
|
|
43
|
+
useT,
|
|
44
|
+
} from "@stapel/core";
|
|
45
|
+
import {
|
|
46
|
+
ErrorAlert,
|
|
47
|
+
GatedButton,
|
|
48
|
+
SkinConfirm,
|
|
49
|
+
SkinTheme,
|
|
50
|
+
} from "@stapel/tokens-antd/skin";
|
|
51
|
+
import { spacing } from "@stapel/tokens";
|
|
29
52
|
import { InviteAcceptFlow } from "../headless/InviteAcceptFlow.js";
|
|
30
53
|
import type { InviteAcceptFlowBag } from "../headless/InviteAcceptFlow.js";
|
|
31
54
|
import type { InvitationPreview, Member } from "../api/types.js";
|
|
32
55
|
import { WORKSPACES_I18N_KEYS } from "../i18n/keys.js";
|
|
56
|
+
import { useRoleLabel } from "./RoleSelectField.js";
|
|
33
57
|
|
|
34
58
|
export interface InviteAcceptPageProps {
|
|
35
59
|
/** The invite token from the route — a bearer secret, never log it. */
|
|
@@ -50,37 +74,47 @@ export interface InviteAcceptPageProps {
|
|
|
50
74
|
readonly renderInitialSetup?: (ctx: { onDone: () => void }) => ReactNode;
|
|
51
75
|
/** Wrong-account CTA (sign out / switch). Omitted → CTA hidden. */
|
|
52
76
|
readonly onSwitchAccount?: () => void;
|
|
77
|
+
/**
|
|
78
|
+
* Leave the invitation page — the door out of a terminal state (expired,
|
|
79
|
+
* revoked, already used), where there is nothing left to do here. The pair
|
|
80
|
+
* owns no router, so omitting it draws no button and the page states the
|
|
81
|
+
* next step in words instead of offering one that goes nowhere.
|
|
82
|
+
*/
|
|
83
|
+
readonly onExit?: () => void;
|
|
53
84
|
readonly onAccepted?: (member: Member) => void;
|
|
54
85
|
readonly onDeclined?: () => void;
|
|
55
86
|
}
|
|
56
87
|
|
|
57
88
|
export function InviteAcceptPage(props: InviteAcceptPageProps): ReactElement {
|
|
58
89
|
return (
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
<SkinTheme surface="base" data-testid="invite-accept-root">
|
|
91
|
+
<InviteAcceptFlow
|
|
92
|
+
token={props.token}
|
|
93
|
+
sessionEmail={props.sessionEmail ?? null}
|
|
94
|
+
{...(props.onAccepted !== undefined ? { onAccepted: props.onAccepted } : {})}
|
|
95
|
+
{...(props.onDeclined !== undefined ? { onDeclined: props.onDeclined } : {})}
|
|
96
|
+
>
|
|
97
|
+
{(bag) => (
|
|
98
|
+
<PageBody
|
|
99
|
+
bag={bag}
|
|
100
|
+
sessionEmail={props.sessionEmail ?? null}
|
|
101
|
+
{...(props.onLoginGrant !== undefined
|
|
102
|
+
? { onLoginGrant: props.onLoginGrant }
|
|
103
|
+
: {})}
|
|
104
|
+
{...(props.renderLoginPanel !== undefined
|
|
105
|
+
? { renderLoginPanel: props.renderLoginPanel }
|
|
106
|
+
: {})}
|
|
107
|
+
{...(props.renderInitialSetup !== undefined
|
|
108
|
+
? { renderInitialSetup: props.renderInitialSetup }
|
|
109
|
+
: {})}
|
|
110
|
+
{...(props.onSwitchAccount !== undefined
|
|
111
|
+
? { onSwitchAccount: props.onSwitchAccount }
|
|
112
|
+
: {})}
|
|
113
|
+
{...(props.onExit !== undefined ? { onExit: props.onExit } : {})}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
</InviteAcceptFlow>
|
|
117
|
+
</SkinTheme>
|
|
84
118
|
);
|
|
85
119
|
}
|
|
86
120
|
|
|
@@ -93,15 +127,21 @@ const UNAVAILABLE_KEY: Record<string, string> = {
|
|
|
93
127
|
|
|
94
128
|
function InviteHeader(props: { preview: InvitationPreview }): ReactElement {
|
|
95
129
|
const t = useT();
|
|
130
|
+
const labelFor = useRoleLabel();
|
|
96
131
|
return (
|
|
97
|
-
<Flex vertical gap={
|
|
132
|
+
<Flex vertical gap={spacing["1"]}>
|
|
98
133
|
<Typography.Title level={3} style={{ margin: 0 }}>
|
|
99
134
|
{t(WORKSPACES_I18N_KEYS.inviteAcceptTitle, {
|
|
100
135
|
workspace: props.preview.workspace_name,
|
|
101
136
|
})}
|
|
102
137
|
</Typography.Title>
|
|
138
|
+
{/* The registry's word for the role, never the wire's key: `secretary`
|
|
139
|
+
lowercase beside title-cased builtins is the same defect the roster
|
|
140
|
+
fixed, and this is the screen a person meets FIRST. */}
|
|
103
141
|
<Typography.Text type="secondary">
|
|
104
|
-
{t(WORKSPACES_I18N_KEYS.inviteRoleLine, {
|
|
142
|
+
{t(WORKSPACES_I18N_KEYS.inviteRoleLine, {
|
|
143
|
+
role: labelFor(props.preview.role),
|
|
144
|
+
})}
|
|
105
145
|
</Typography.Text>
|
|
106
146
|
<Typography.Text type="secondary">
|
|
107
147
|
{t(WORKSPACES_I18N_KEYS.inviteEmailLine, {
|
|
@@ -121,11 +161,13 @@ function PageBody(props: {
|
|
|
121
161
|
renderLoginPanel?: (ctx: { onLoggedIn: (email: string) => void }) => ReactNode;
|
|
122
162
|
renderInitialSetup?: (ctx: { onDone: () => void }) => ReactNode;
|
|
123
163
|
onSwitchAccount?: () => void;
|
|
164
|
+
onExit?: () => void;
|
|
124
165
|
}): ReactElement {
|
|
125
166
|
const t = useT();
|
|
126
|
-
const
|
|
167
|
+
const describeError = useDescribeFlowError();
|
|
127
168
|
const { bag } = props;
|
|
128
169
|
const s = bag.state;
|
|
170
|
+
const [confirmingDecline, setConfirmingDecline] = useState(false);
|
|
129
171
|
|
|
130
172
|
// ── the grant seam: run the host's exchange when the grant is issued ──────
|
|
131
173
|
const { onLoginGrant, renderInitialSetup } = props;
|
|
@@ -162,11 +204,11 @@ function PageBody(props: {
|
|
|
162
204
|
case "loadingPreview":
|
|
163
205
|
return (
|
|
164
206
|
<Card data-testid="invite-accept-page">
|
|
165
|
-
<Flex vertical
|
|
207
|
+
<Flex vertical gap={spacing["3"]} role="status" aria-busy="true">
|
|
166
208
|
<Typography.Text type="secondary">
|
|
167
209
|
{t(WORKSPACES_I18N_KEYS.inviteLoading)}
|
|
168
210
|
</Typography.Text>
|
|
169
|
-
<
|
|
211
|
+
<Skeleton active paragraph={{ rows: 2 }} />
|
|
170
212
|
</Flex>
|
|
171
213
|
</Card>
|
|
172
214
|
);
|
|
@@ -174,20 +216,45 @@ function PageBody(props: {
|
|
|
174
216
|
case "previewError":
|
|
175
217
|
return (
|
|
176
218
|
<Card data-testid="invite-accept-page">
|
|
177
|
-
<
|
|
219
|
+
<ErrorAlert error={describeError(s.error)} testId="invite-preview-error" />
|
|
178
220
|
</Card>
|
|
179
221
|
);
|
|
180
222
|
|
|
223
|
+
// A dead end, and drawn as one. It used to be antd's INFO result — a
|
|
224
|
+
// blue 88px "!" disc, the same pictogram a tip uses — with nothing to do
|
|
225
|
+
// next. The link cannot be revived from here, so the screen says what
|
|
226
|
+
// became of it, in the warning tone that means "this did not work", and
|
|
227
|
+
// names the one thing that does help: asking for another one.
|
|
181
228
|
case "unavailable":
|
|
182
229
|
return (
|
|
183
230
|
<Card data-testid="invite-accept-page">
|
|
184
231
|
<Result
|
|
185
|
-
status="
|
|
232
|
+
status="warning"
|
|
186
233
|
title={s.preview.workspace_name}
|
|
187
234
|
subTitle={t(
|
|
188
235
|
UNAVAILABLE_KEY[s.status] ??
|
|
189
236
|
WORKSPACES_I18N_KEYS.inviteUnavailableRevoked
|
|
190
237
|
)}
|
|
238
|
+
extra={
|
|
239
|
+
<Flex vertical gap={spacing["3"]} align="center">
|
|
240
|
+
<Typography.Text type="secondary">
|
|
241
|
+
{t(WORKSPACES_I18N_KEYS.inviteUnavailableNextStep, {
|
|
242
|
+
workspace: s.preview.workspace_name,
|
|
243
|
+
})}
|
|
244
|
+
</Typography.Text>
|
|
245
|
+
{props.onExit !== undefined && (
|
|
246
|
+
<Button
|
|
247
|
+
type="primary"
|
|
248
|
+
onClick={props.onExit}
|
|
249
|
+
data-analytics="none"
|
|
250
|
+
data-analytics-reason="navigation — the host owns the router"
|
|
251
|
+
data-testid="invite-exit"
|
|
252
|
+
>
|
|
253
|
+
{t(WORKSPACES_I18N_KEYS.inviteExitCta)}
|
|
254
|
+
</Button>
|
|
255
|
+
)}
|
|
256
|
+
</Flex>
|
|
257
|
+
}
|
|
191
258
|
/>
|
|
192
259
|
</Card>
|
|
193
260
|
);
|
|
@@ -195,22 +262,29 @@ function PageBody(props: {
|
|
|
195
262
|
case "wrongAccount":
|
|
196
263
|
return (
|
|
197
264
|
<Card data-testid="invite-accept-page">
|
|
198
|
-
<Flex vertical gap="
|
|
265
|
+
<Flex vertical gap={spacing["4"]}>
|
|
199
266
|
<InviteHeader preview={s.preview} />
|
|
200
|
-
<
|
|
201
|
-
type="warning"
|
|
202
|
-
showIcon
|
|
267
|
+
<ErrorAlert
|
|
203
268
|
message={t(WORKSPACES_I18N_KEYS.inviteWrongAccount)}
|
|
204
|
-
|
|
269
|
+
detail={t(WORKSPACES_I18N_KEYS.inviteWrongAccountHint, {
|
|
205
270
|
email: props.sessionEmail ?? "",
|
|
206
271
|
invited: s.preview.email_masked,
|
|
207
272
|
})}
|
|
273
|
+
testId="invite-wrong-account"
|
|
274
|
+
{...(props.onSwitchAccount !== undefined
|
|
275
|
+
? {
|
|
276
|
+
action: (
|
|
277
|
+
<Button
|
|
278
|
+
onClick={props.onSwitchAccount}
|
|
279
|
+
data-analytics="flow"
|
|
280
|
+
data-testid="invite-switch-account"
|
|
281
|
+
>
|
|
282
|
+
{t(WORKSPACES_I18N_KEYS.inviteSwitchAccountCta)}
|
|
283
|
+
</Button>
|
|
284
|
+
),
|
|
285
|
+
}
|
|
286
|
+
: {})}
|
|
208
287
|
/>
|
|
209
|
-
{props.onSwitchAccount && (
|
|
210
|
-
<Button onClick={props.onSwitchAccount} data-analytics="flow">
|
|
211
|
-
{t(WORKSPACES_I18N_KEYS.inviteSwitchAccountCta)}
|
|
212
|
-
</Button>
|
|
213
|
-
)}
|
|
214
288
|
</Flex>
|
|
215
289
|
</Card>
|
|
216
290
|
);
|
|
@@ -218,14 +292,18 @@ function PageBody(props: {
|
|
|
218
292
|
case "loginRequired":
|
|
219
293
|
return (
|
|
220
294
|
<Card data-testid="invite-accept-page">
|
|
221
|
-
<Flex vertical gap="
|
|
295
|
+
<Flex vertical gap={spacing["4"]}>
|
|
222
296
|
<InviteHeader preview={s.preview} />
|
|
223
297
|
<Typography.Text strong>
|
|
224
298
|
{t(WORKSPACES_I18N_KEYS.inviteLoginTitle)}
|
|
225
299
|
</Typography.Text>
|
|
226
|
-
{props.renderLoginPanel
|
|
227
|
-
|
|
228
|
-
|
|
300
|
+
{props.renderLoginPanel !== undefined ? (
|
|
301
|
+
props.renderLoginPanel({
|
|
302
|
+
onLoggedIn: (email) => bag.sessionEstablished(email),
|
|
303
|
+
})
|
|
304
|
+
) : (
|
|
305
|
+
<SlotPlaceholder name="renderLoginPanel" data-testid="invite-login-slot" />
|
|
306
|
+
)}
|
|
229
307
|
</Flex>
|
|
230
308
|
</Card>
|
|
231
309
|
);
|
|
@@ -235,7 +313,7 @@ function PageBody(props: {
|
|
|
235
313
|
case "claimError":
|
|
236
314
|
return (
|
|
237
315
|
<Card data-testid="invite-accept-page">
|
|
238
|
-
<Flex vertical gap="
|
|
316
|
+
<Flex vertical gap={spacing["4"]}>
|
|
239
317
|
<InviteHeader preview={s.preview} />
|
|
240
318
|
<Typography.Text type="secondary">
|
|
241
319
|
{t(WORKSPACES_I18N_KEYS.inviteNewUserHint, {
|
|
@@ -243,13 +321,14 @@ function PageBody(props: {
|
|
|
243
321
|
})}
|
|
244
322
|
</Typography.Text>
|
|
245
323
|
{s.step === "claimError" && (
|
|
246
|
-
<
|
|
324
|
+
<ErrorAlert error={describeError(s.error)} testId="invite-claim-error" />
|
|
247
325
|
)}
|
|
248
326
|
<Button
|
|
249
327
|
type="primary"
|
|
250
328
|
loading={s.step === "claiming"}
|
|
251
329
|
onClick={() => bag.claim()}
|
|
252
330
|
data-analytics="flow"
|
|
331
|
+
data-testid="invite-create-account"
|
|
253
332
|
>
|
|
254
333
|
{t(
|
|
255
334
|
s.step === "claiming"
|
|
@@ -264,33 +343,25 @@ function PageBody(props: {
|
|
|
264
343
|
case "grantIssued":
|
|
265
344
|
return (
|
|
266
345
|
<Card data-testid="invite-accept-page">
|
|
267
|
-
<Flex vertical
|
|
346
|
+
<Flex vertical gap={spacing["3"]}>
|
|
268
347
|
{exchangeFailed ? (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
exchangedRef.current = null;
|
|
280
|
-
setExchangeFailed(false);
|
|
281
|
-
}}
|
|
282
|
-
data-analytics="flow"
|
|
283
|
-
>
|
|
284
|
-
{t(WORKSPACES_I18N_KEYS.inviteRetryCta)}
|
|
285
|
-
</Button>
|
|
286
|
-
</>
|
|
348
|
+
<ErrorAlert
|
|
349
|
+
message={t(WORKSPACES_I18N_KEYS.inviteExchangeFailed)}
|
|
350
|
+
retryLabel={t(WORKSPACES_I18N_KEYS.inviteRetryCta)}
|
|
351
|
+
testId="invite-exchange-error"
|
|
352
|
+
onRetry={() => {
|
|
353
|
+
// Re-arm the effect for the same grant token.
|
|
354
|
+
exchangedRef.current = null;
|
|
355
|
+
setExchangeFailed(false);
|
|
356
|
+
}}
|
|
357
|
+
/>
|
|
287
358
|
) : (
|
|
288
|
-
|
|
359
|
+
<Flex vertical gap={spacing["3"]} role="status" aria-busy="true">
|
|
289
360
|
<Typography.Text type="secondary">
|
|
290
361
|
{t(WORKSPACES_I18N_KEYS.inviteExchanging)}
|
|
291
362
|
</Typography.Text>
|
|
292
|
-
<
|
|
293
|
-
|
|
363
|
+
<Skeleton active paragraph={{ rows: 1 }} />
|
|
364
|
+
</Flex>
|
|
294
365
|
)}
|
|
295
366
|
</Flex>
|
|
296
367
|
</Card>
|
|
@@ -299,14 +370,14 @@ function PageBody(props: {
|
|
|
299
370
|
case "basicData":
|
|
300
371
|
return (
|
|
301
372
|
<Card data-testid="invite-accept-page">
|
|
302
|
-
<Flex vertical gap="
|
|
373
|
+
<Flex vertical gap={spacing["4"]}>
|
|
303
374
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
|
304
375
|
{t(WORKSPACES_I18N_KEYS.inviteBasicDataTitle)}
|
|
305
376
|
</Typography.Title>
|
|
306
|
-
{renderInitialSetup ? (
|
|
377
|
+
{renderInitialSetup !== undefined ? (
|
|
307
378
|
renderInitialSetup({ onDone: () => bag.completeBasicData() })
|
|
308
379
|
) : (
|
|
309
|
-
<
|
|
380
|
+
<Skeleton active paragraph={{ rows: 1 }} />
|
|
310
381
|
)}
|
|
311
382
|
</Flex>
|
|
312
383
|
</Card>
|
|
@@ -315,37 +386,63 @@ function PageBody(props: {
|
|
|
315
386
|
case "acceptPrompt":
|
|
316
387
|
case "accepting":
|
|
317
388
|
case "declining":
|
|
318
|
-
case "acceptError":
|
|
389
|
+
case "acceptError": {
|
|
390
|
+
const busy = s.step === "accepting" || s.step === "declining";
|
|
391
|
+
const busyGate = busy
|
|
392
|
+
? actionBlocked(WORKSPACES_I18N_KEYS.inviteBlockedBusy)
|
|
393
|
+
: actionAvailable();
|
|
319
394
|
return (
|
|
320
395
|
<Card data-testid="invite-accept-page">
|
|
321
|
-
<Flex vertical gap="
|
|
396
|
+
<Flex vertical gap={spacing["4"]}>
|
|
322
397
|
<InviteHeader preview={s.preview} />
|
|
323
398
|
{s.step === "acceptError" && (
|
|
324
|
-
<
|
|
399
|
+
<ErrorAlert error={describeError(s.error)} testId="invite-accept-error" />
|
|
325
400
|
)}
|
|
326
|
-
|
|
401
|
+
{/* One primary — joining. Declining is the quiet, irreversible
|
|
402
|
+
path and sits under it behind a confirm. */}
|
|
403
|
+
<Flex vertical gap={spacing["2"]} align="flex-start">
|
|
327
404
|
<Button
|
|
328
405
|
type="primary"
|
|
406
|
+
block
|
|
329
407
|
loading={s.step === "accepting"}
|
|
330
|
-
disabled={s.step === "declining"}
|
|
331
408
|
onClick={() => bag.accept()}
|
|
332
409
|
data-analytics="flow"
|
|
410
|
+
data-testid="invite-join"
|
|
333
411
|
>
|
|
334
412
|
{t(WORKSPACES_I18N_KEYS.inviteJoinCta)}
|
|
335
413
|
</Button>
|
|
336
|
-
<
|
|
414
|
+
<GatedButton
|
|
415
|
+
gate={busyGate}
|
|
337
416
|
danger
|
|
417
|
+
type="link"
|
|
338
418
|
loading={s.step === "declining"}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
data-analytics="
|
|
419
|
+
onClick={() => setConfirmingDecline(true)}
|
|
420
|
+
testId="invite-decline"
|
|
421
|
+
data-analytics="none"
|
|
422
|
+
data-analytics-reason="opens the decline confirm"
|
|
342
423
|
>
|
|
343
424
|
{t(WORKSPACES_I18N_KEYS.inviteDeclineCta)}
|
|
344
|
-
</
|
|
425
|
+
</GatedButton>
|
|
345
426
|
</Flex>
|
|
427
|
+
<SkinConfirm
|
|
428
|
+
open={confirmingDecline}
|
|
429
|
+
danger
|
|
430
|
+
title={t(WORKSPACES_I18N_KEYS.inviteDeclineConfirm)}
|
|
431
|
+
body={t(WORKSPACES_I18N_KEYS.inviteDeclineConfirmBody)}
|
|
432
|
+
confirmLabel={t(WORKSPACES_I18N_KEYS.inviteDeclineCta)}
|
|
433
|
+
cancelLabel={t(WORKSPACES_I18N_KEYS.cancel)}
|
|
434
|
+
confirming={s.step === "declining"}
|
|
435
|
+
onConfirm={() => {
|
|
436
|
+
bag.decline();
|
|
437
|
+
setConfirmingDecline(false);
|
|
438
|
+
}}
|
|
439
|
+
onCancel={() => setConfirmingDecline(false)}
|
|
440
|
+
data-testid="invite-decline-confirm"
|
|
441
|
+
/>
|
|
346
442
|
</Flex>
|
|
347
443
|
</Card>
|
|
348
444
|
);
|
|
445
|
+
}
|
|
349
446
|
|
|
350
447
|
case "accepted":
|
|
351
448
|
return (
|