@stapel/workspaces-react 0.5.0 → 0.7.0
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 +40 -0
- package/dist/api/generated/schema.d.ts +384 -4
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/types.d.ts +20 -5
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/workspacesApi.d.ts +23 -1
- package/dist/api/workspacesApi.d.ts.map +1 -1
- package/dist/api/workspacesApi.js +4 -0
- package/dist/api/workspacesApi.js.map +1 -1
- package/dist/default/InviteAcceptPage.d.ts +28 -0
- package/dist/default/InviteAcceptPage.d.ts.map +1 -0
- package/dist/default/InviteAcceptPage.js +141 -0
- package/dist/default/InviteAcceptPage.js.map +1 -0
- package/dist/default/MembersManager.d.ts +4 -4
- package/dist/default/MembersManager.d.ts.map +1 -1
- package/dist/default/MembersManager.js +61 -56
- package/dist/default/MembersManager.js.map +1 -1
- package/dist/default/index.d.ts +2 -0
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +1 -0
- package/dist/default/index.js.map +1 -1
- package/dist/flows/inviteAcceptFlow.d.ts +155 -0
- package/dist/flows/inviteAcceptFlow.d.ts.map +1 -0
- package/dist/flows/inviteAcceptFlow.js +126 -0
- package/dist/flows/inviteAcceptFlow.js.map +1 -0
- package/dist/headless/Can.d.ts +40 -0
- package/dist/headless/Can.d.ts.map +1 -0
- package/dist/headless/Can.js +30 -0
- package/dist/headless/Can.js.map +1 -0
- package/dist/headless/InviteAcceptFlow.d.ts +44 -0
- package/dist/headless/InviteAcceptFlow.d.ts.map +1 -0
- package/dist/headless/InviteAcceptFlow.js +67 -0
- package/dist/headless/InviteAcceptFlow.js.map +1 -0
- package/dist/headless/RoleSelect.d.ts +41 -0
- package/dist/headless/RoleSelect.d.ts.map +1 -0
- package/dist/headless/RoleSelect.js +34 -0
- package/dist/headless/RoleSelect.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +48 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.gen.js +24 -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 +8 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
- package/dist/i18n/keys.d.ts +28 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +64 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +30 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/model/capabilities.d.ts +21 -0
- package/dist/model/capabilities.d.ts.map +1 -0
- package/dist/model/capabilities.js +32 -0
- package/dist/model/capabilities.js.map +1 -0
- package/dist/model/emailMask.d.ts +22 -0
- package/dist/model/emailMask.d.ts.map +1 -0
- package/dist/model/emailMask.js +36 -0
- package/dist/model/emailMask.js.map +1 -0
- package/dist/model/mutations.d.ts +23 -3
- package/dist/model/mutations.d.ts.map +1 -1
- package/dist/model/mutations.js +39 -1
- package/dist/model/mutations.js.map +1 -1
- package/dist/model/queries.d.ts +56 -6
- package/dist/model/queries.d.ts.map +1 -1
- package/dist/model/queries.js +75 -7
- package/dist/model/queries.js.map +1 -1
- package/dist/model/queryKeys.d.ts +2 -0
- package/dist/model/queryKeys.d.ts.map +1 -1
- package/dist/model/queryKeys.js +6 -0
- package/dist/model/queryKeys.js.map +1 -1
- package/llms.txt +40 -15
- package/manifest.json +225 -3
- package/package.json +5 -5
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +384 -4
- package/src/api/types.ts +20 -5
- package/src/api/workspacesApi.ts +50 -0
- package/src/default/InviteAcceptPage.tsx +373 -0
- package/src/default/MembersManager.tsx +155 -145
- package/src/default/index.ts +2 -0
- package/src/flows/inviteAcceptFlow.ts +289 -0
- package/src/headless/Can.tsx +49 -0
- package/src/headless/InviteAcceptFlow.tsx +105 -0
- package/src/headless/RoleSelect.tsx +58 -0
- package/src/i18n/generated/errors.gen.ts +24 -0
- package/src/i18n/generated/errors.json +62 -0
- package/src/i18n/generated/errors.ru.gen.ts +8 -0
- package/src/i18n/keys.ts +68 -0
- package/src/i18n/ru.ts +34 -0
- package/src/index.ts +26 -0
- package/src/model/capabilities.ts +37 -0
- package/src/model/emailMask.ts +37 -0
- package/src/model/mutations.ts +56 -4
- package/src/model/queries.ts +99 -7
- package/src/model/queryKeys.ts +10 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<InviteAcceptPage/>` — the default `/invite/{token}` route component
|
|
3
|
+
* (org-program §B4): every state of the `InviteAcceptFlow` headless journey
|
|
4
|
+
* rendered with antd, with the two HOST SLOTS the pair cannot own:
|
|
5
|
+
*
|
|
6
|
+
* - `renderLoginPanel` — the sign-in surface for a registered invitee
|
|
7
|
+
* without a session (drop auth-react's `<AuthPanel/>` in; call
|
|
8
|
+
* `onLoggedIn(email)` — or simply let the `sessionEmail` prop update,
|
|
9
|
+
* the flow re-routes on it automatically).
|
|
10
|
+
* - `renderInitialSetup` — the basic-data step for a freshly created
|
|
11
|
+
* account (drop profiles-react's `InitialSetupPrompt`/`InitialSetupModal`
|
|
12
|
+
* in; call `onDone()`). Omitted → the step auto-skips.
|
|
13
|
+
*
|
|
14
|
+
* THE GRANT SEAM (§B4): for a NEW user the flow mints a login grant and this
|
|
15
|
+
* page hands it to YOUR `onLoginGrant(grantToken)` — wire it to auth-react's
|
|
16
|
+
* `api.exchangeLoginGrant(grantToken)` + `session.adopt(...)`. When your
|
|
17
|
+
* callback's promise resolves the page advances the flow automatically
|
|
18
|
+
* (`grantExchanged`); a rejection shows a retry. The pairs stay decoupled:
|
|
19
|
+
* this package never imports auth-react.
|
|
20
|
+
*
|
|
21
|
+
* Like the pair's other default skins (MembersManager/WorkspaceSettings),
|
|
22
|
+
* theming comes from the host's antd ConfigProvider — this is a route BODY,
|
|
23
|
+
* not a self-themed shell.
|
|
24
|
+
*/
|
|
25
|
+
import { useEffect, useRef, useState } from "react";
|
|
26
|
+
import type { ReactElement, ReactNode } from "react";
|
|
27
|
+
import { Alert, Button, Card, Flex, Result, Spin, Typography } from "antd";
|
|
28
|
+
import { useFormatFlowError, useT } from "@stapel/core";
|
|
29
|
+
import { InviteAcceptFlow } from "../headless/InviteAcceptFlow.js";
|
|
30
|
+
import type { InviteAcceptFlowBag } from "../headless/InviteAcceptFlow.js";
|
|
31
|
+
import type { InvitationPreview, Member } from "../api/types.js";
|
|
32
|
+
import { WORKSPACES_I18N_KEYS } from "../i18n/keys.js";
|
|
33
|
+
|
|
34
|
+
export interface InviteAcceptPageProps {
|
|
35
|
+
/** The invite token from the route — a bearer secret, never log it. */
|
|
36
|
+
readonly token: string;
|
|
37
|
+
/** The signed-in account's email, or null/undefined when no session
|
|
38
|
+
* (e.g. auth-react's `useAuthSessionState().user?.email ?? null`). */
|
|
39
|
+
readonly sessionEmail?: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Exchange the login grant at auth (the §B4 seam — see the module doc).
|
|
42
|
+
* Resolve = signed in (the page advances); reject = retry offered.
|
|
43
|
+
*/
|
|
44
|
+
readonly onLoginGrant?: (grantToken: string) => void | Promise<void>;
|
|
45
|
+
/** Login slot for `loginRequired` (registered invitee, no session). */
|
|
46
|
+
readonly renderLoginPanel?: (ctx: {
|
|
47
|
+
onLoggedIn: (email: string) => void;
|
|
48
|
+
}) => ReactNode;
|
|
49
|
+
/** Basic-data slot for `basicData` (fresh account). Omitted → auto-skip. */
|
|
50
|
+
readonly renderInitialSetup?: (ctx: { onDone: () => void }) => ReactNode;
|
|
51
|
+
/** Wrong-account CTA (sign out / switch). Omitted → CTA hidden. */
|
|
52
|
+
readonly onSwitchAccount?: () => void;
|
|
53
|
+
readonly onAccepted?: (member: Member) => void;
|
|
54
|
+
readonly onDeclined?: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function InviteAcceptPage(props: InviteAcceptPageProps): ReactElement {
|
|
58
|
+
return (
|
|
59
|
+
<InviteAcceptFlow
|
|
60
|
+
token={props.token}
|
|
61
|
+
sessionEmail={props.sessionEmail ?? null}
|
|
62
|
+
{...(props.onAccepted !== undefined ? { onAccepted: props.onAccepted } : {})}
|
|
63
|
+
{...(props.onDeclined !== undefined ? { onDeclined: props.onDeclined } : {})}
|
|
64
|
+
>
|
|
65
|
+
{(bag) => (
|
|
66
|
+
<PageBody
|
|
67
|
+
bag={bag}
|
|
68
|
+
sessionEmail={props.sessionEmail ?? null}
|
|
69
|
+
{...(props.onLoginGrant !== undefined
|
|
70
|
+
? { onLoginGrant: props.onLoginGrant }
|
|
71
|
+
: {})}
|
|
72
|
+
{...(props.renderLoginPanel !== undefined
|
|
73
|
+
? { renderLoginPanel: props.renderLoginPanel }
|
|
74
|
+
: {})}
|
|
75
|
+
{...(props.renderInitialSetup !== undefined
|
|
76
|
+
? { renderInitialSetup: props.renderInitialSetup }
|
|
77
|
+
: {})}
|
|
78
|
+
{...(props.onSwitchAccount !== undefined
|
|
79
|
+
? { onSwitchAccount: props.onSwitchAccount }
|
|
80
|
+
: {})}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
</InviteAcceptFlow>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const UNAVAILABLE_KEY: Record<string, string> = {
|
|
88
|
+
expired: WORKSPACES_I18N_KEYS.inviteUnavailableExpired,
|
|
89
|
+
revoked: WORKSPACES_I18N_KEYS.inviteUnavailableRevoked,
|
|
90
|
+
accepted: WORKSPACES_I18N_KEYS.inviteUnavailableAccepted,
|
|
91
|
+
declined: WORKSPACES_I18N_KEYS.inviteUnavailableDeclined,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function InviteHeader(props: { preview: InvitationPreview }): ReactElement {
|
|
95
|
+
const t = useT();
|
|
96
|
+
return (
|
|
97
|
+
<Flex vertical gap={4}>
|
|
98
|
+
<Typography.Title level={3} style={{ margin: 0 }}>
|
|
99
|
+
{t(WORKSPACES_I18N_KEYS.inviteAcceptTitle, {
|
|
100
|
+
workspace: props.preview.workspace_name,
|
|
101
|
+
})}
|
|
102
|
+
</Typography.Title>
|
|
103
|
+
<Typography.Text type="secondary">
|
|
104
|
+
{t(WORKSPACES_I18N_KEYS.inviteRoleLine, { role: props.preview.role })}
|
|
105
|
+
</Typography.Text>
|
|
106
|
+
<Typography.Text type="secondary">
|
|
107
|
+
{t(WORKSPACES_I18N_KEYS.inviteEmailLine, {
|
|
108
|
+
email: props.preview.email_masked,
|
|
109
|
+
})}
|
|
110
|
+
</Typography.Text>
|
|
111
|
+
</Flex>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A real component (not hooks in the render-prop lambda) — owns the
|
|
116
|
+
* grant-exchange effect below. */
|
|
117
|
+
function PageBody(props: {
|
|
118
|
+
bag: InviteAcceptFlowBag;
|
|
119
|
+
sessionEmail: string | null;
|
|
120
|
+
onLoginGrant?: (grantToken: string) => void | Promise<void>;
|
|
121
|
+
renderLoginPanel?: (ctx: { onLoggedIn: (email: string) => void }) => ReactNode;
|
|
122
|
+
renderInitialSetup?: (ctx: { onDone: () => void }) => ReactNode;
|
|
123
|
+
onSwitchAccount?: () => void;
|
|
124
|
+
}): ReactElement {
|
|
125
|
+
const t = useT();
|
|
126
|
+
const formatError = useFormatFlowError();
|
|
127
|
+
const { bag } = props;
|
|
128
|
+
const s = bag.state;
|
|
129
|
+
|
|
130
|
+
// ── the grant seam: run the host's exchange when the grant is issued ──────
|
|
131
|
+
const { onLoginGrant, renderInitialSetup } = props;
|
|
132
|
+
const grantToken = s.step === "grantIssued" ? s.grantToken : null;
|
|
133
|
+
const [exchangeFailed, setExchangeFailed] = useState(false);
|
|
134
|
+
const exchangedRef = useRef<string | null>(null);
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (grantToken === null || onLoginGrant === undefined) return;
|
|
137
|
+
if (exchangedRef.current === grantToken) return; // once per grant
|
|
138
|
+
exchangedRef.current = grantToken;
|
|
139
|
+
setExchangeFailed(false);
|
|
140
|
+
void (async () => {
|
|
141
|
+
try {
|
|
142
|
+
await onLoginGrant(grantToken);
|
|
143
|
+
bag.grantExchanged();
|
|
144
|
+
} catch {
|
|
145
|
+
// The grant is single-use server-side but un-consumed on a network
|
|
146
|
+
// fault; retry re-runs the host callback with the SAME token.
|
|
147
|
+
exchangedRef.current = null;
|
|
148
|
+
setExchangeFailed(true);
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
151
|
+
}, [grantToken, onLoginGrant, bag]);
|
|
152
|
+
|
|
153
|
+
// ── basic-data auto-skip when the host provides no slot ───────────────────
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (s.step === "basicData" && renderInitialSetup === undefined) {
|
|
156
|
+
bag.completeBasicData();
|
|
157
|
+
}
|
|
158
|
+
}, [s.step, renderInitialSetup, bag]);
|
|
159
|
+
|
|
160
|
+
switch (s.step) {
|
|
161
|
+
case "idle":
|
|
162
|
+
case "loadingPreview":
|
|
163
|
+
return (
|
|
164
|
+
<Card data-testid="invite-accept-page">
|
|
165
|
+
<Flex vertical align="center" gap="middle">
|
|
166
|
+
<Typography.Text type="secondary">
|
|
167
|
+
{t(WORKSPACES_I18N_KEYS.inviteLoading)}
|
|
168
|
+
</Typography.Text>
|
|
169
|
+
<Spin />
|
|
170
|
+
</Flex>
|
|
171
|
+
</Card>
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
case "previewError":
|
|
175
|
+
return (
|
|
176
|
+
<Card data-testid="invite-accept-page">
|
|
177
|
+
<Alert type="error" showIcon message={formatError(s.error)} />
|
|
178
|
+
</Card>
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
case "unavailable":
|
|
182
|
+
return (
|
|
183
|
+
<Card data-testid="invite-accept-page">
|
|
184
|
+
<Result
|
|
185
|
+
status="info"
|
|
186
|
+
title={s.preview.workspace_name}
|
|
187
|
+
subTitle={t(
|
|
188
|
+
UNAVAILABLE_KEY[s.status] ??
|
|
189
|
+
WORKSPACES_I18N_KEYS.inviteUnavailableRevoked
|
|
190
|
+
)}
|
|
191
|
+
/>
|
|
192
|
+
</Card>
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
case "wrongAccount":
|
|
196
|
+
return (
|
|
197
|
+
<Card data-testid="invite-accept-page">
|
|
198
|
+
<Flex vertical gap="middle">
|
|
199
|
+
<InviteHeader preview={s.preview} />
|
|
200
|
+
<Alert
|
|
201
|
+
type="warning"
|
|
202
|
+
showIcon
|
|
203
|
+
message={t(WORKSPACES_I18N_KEYS.inviteWrongAccount)}
|
|
204
|
+
description={t(WORKSPACES_I18N_KEYS.inviteWrongAccountHint, {
|
|
205
|
+
email: props.sessionEmail ?? "",
|
|
206
|
+
invited: s.preview.email_masked,
|
|
207
|
+
})}
|
|
208
|
+
/>
|
|
209
|
+
{props.onSwitchAccount && (
|
|
210
|
+
<Button onClick={props.onSwitchAccount} data-analytics="flow">
|
|
211
|
+
{t(WORKSPACES_I18N_KEYS.inviteSwitchAccountCta)}
|
|
212
|
+
</Button>
|
|
213
|
+
)}
|
|
214
|
+
</Flex>
|
|
215
|
+
</Card>
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
case "loginRequired":
|
|
219
|
+
return (
|
|
220
|
+
<Card data-testid="invite-accept-page">
|
|
221
|
+
<Flex vertical gap="middle">
|
|
222
|
+
<InviteHeader preview={s.preview} />
|
|
223
|
+
<Typography.Text strong>
|
|
224
|
+
{t(WORKSPACES_I18N_KEYS.inviteLoginTitle)}
|
|
225
|
+
</Typography.Text>
|
|
226
|
+
{props.renderLoginPanel?.({
|
|
227
|
+
onLoggedIn: (email) => bag.sessionEstablished(email),
|
|
228
|
+
})}
|
|
229
|
+
</Flex>
|
|
230
|
+
</Card>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
case "newUser":
|
|
234
|
+
case "claiming":
|
|
235
|
+
case "claimError":
|
|
236
|
+
return (
|
|
237
|
+
<Card data-testid="invite-accept-page">
|
|
238
|
+
<Flex vertical gap="middle">
|
|
239
|
+
<InviteHeader preview={s.preview} />
|
|
240
|
+
<Typography.Text type="secondary">
|
|
241
|
+
{t(WORKSPACES_I18N_KEYS.inviteNewUserHint, {
|
|
242
|
+
email: s.preview.email_masked,
|
|
243
|
+
})}
|
|
244
|
+
</Typography.Text>
|
|
245
|
+
{s.step === "claimError" && (
|
|
246
|
+
<Alert type="error" showIcon message={formatError(s.error)} />
|
|
247
|
+
)}
|
|
248
|
+
<Button
|
|
249
|
+
type="primary"
|
|
250
|
+
loading={s.step === "claiming"}
|
|
251
|
+
onClick={() => bag.claim()}
|
|
252
|
+
data-analytics="flow"
|
|
253
|
+
>
|
|
254
|
+
{t(
|
|
255
|
+
s.step === "claiming"
|
|
256
|
+
? WORKSPACES_I18N_KEYS.inviteClaiming
|
|
257
|
+
: WORKSPACES_I18N_KEYS.inviteCreateAccountCta
|
|
258
|
+
)}
|
|
259
|
+
</Button>
|
|
260
|
+
</Flex>
|
|
261
|
+
</Card>
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
case "grantIssued":
|
|
265
|
+
return (
|
|
266
|
+
<Card data-testid="invite-accept-page">
|
|
267
|
+
<Flex vertical align="center" gap="middle">
|
|
268
|
+
{exchangeFailed ? (
|
|
269
|
+
<>
|
|
270
|
+
<Alert
|
|
271
|
+
type="error"
|
|
272
|
+
showIcon
|
|
273
|
+
message={t(WORKSPACES_I18N_KEYS.inviteExchangeFailed)}
|
|
274
|
+
/>
|
|
275
|
+
<Button
|
|
276
|
+
type="primary"
|
|
277
|
+
onClick={() => {
|
|
278
|
+
// Re-arm the effect for the same grant token.
|
|
279
|
+
exchangedRef.current = null;
|
|
280
|
+
setExchangeFailed(false);
|
|
281
|
+
}}
|
|
282
|
+
data-analytics="flow"
|
|
283
|
+
>
|
|
284
|
+
{t(WORKSPACES_I18N_KEYS.inviteRetryCta)}
|
|
285
|
+
</Button>
|
|
286
|
+
</>
|
|
287
|
+
) : (
|
|
288
|
+
<>
|
|
289
|
+
<Typography.Text type="secondary">
|
|
290
|
+
{t(WORKSPACES_I18N_KEYS.inviteExchanging)}
|
|
291
|
+
</Typography.Text>
|
|
292
|
+
<Spin />
|
|
293
|
+
</>
|
|
294
|
+
)}
|
|
295
|
+
</Flex>
|
|
296
|
+
</Card>
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
case "basicData":
|
|
300
|
+
return (
|
|
301
|
+
<Card data-testid="invite-accept-page">
|
|
302
|
+
<Flex vertical gap="middle">
|
|
303
|
+
<Typography.Title level={4} style={{ margin: 0 }}>
|
|
304
|
+
{t(WORKSPACES_I18N_KEYS.inviteBasicDataTitle)}
|
|
305
|
+
</Typography.Title>
|
|
306
|
+
{renderInitialSetup ? (
|
|
307
|
+
renderInitialSetup({ onDone: () => bag.completeBasicData() })
|
|
308
|
+
) : (
|
|
309
|
+
<Spin />
|
|
310
|
+
)}
|
|
311
|
+
</Flex>
|
|
312
|
+
</Card>
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
case "acceptPrompt":
|
|
316
|
+
case "accepting":
|
|
317
|
+
case "declining":
|
|
318
|
+
case "acceptError":
|
|
319
|
+
return (
|
|
320
|
+
<Card data-testid="invite-accept-page">
|
|
321
|
+
<Flex vertical gap="middle">
|
|
322
|
+
<InviteHeader preview={s.preview} />
|
|
323
|
+
{s.step === "acceptError" && (
|
|
324
|
+
<Alert type="error" showIcon message={formatError(s.error)} />
|
|
325
|
+
)}
|
|
326
|
+
<Flex gap="small">
|
|
327
|
+
<Button
|
|
328
|
+
type="primary"
|
|
329
|
+
loading={s.step === "accepting"}
|
|
330
|
+
disabled={s.step === "declining"}
|
|
331
|
+
onClick={() => bag.accept()}
|
|
332
|
+
data-analytics="flow"
|
|
333
|
+
>
|
|
334
|
+
{t(WORKSPACES_I18N_KEYS.inviteJoinCta)}
|
|
335
|
+
</Button>
|
|
336
|
+
<Button
|
|
337
|
+
danger
|
|
338
|
+
loading={s.step === "declining"}
|
|
339
|
+
disabled={s.step === "accepting"}
|
|
340
|
+
onClick={() => bag.decline()}
|
|
341
|
+
data-analytics="flow"
|
|
342
|
+
>
|
|
343
|
+
{t(WORKSPACES_I18N_KEYS.inviteDeclineCta)}
|
|
344
|
+
</Button>
|
|
345
|
+
</Flex>
|
|
346
|
+
</Flex>
|
|
347
|
+
</Card>
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
case "accepted":
|
|
351
|
+
return (
|
|
352
|
+
<Card data-testid="invite-accept-page">
|
|
353
|
+
<Result
|
|
354
|
+
status="success"
|
|
355
|
+
title={t(WORKSPACES_I18N_KEYS.inviteAccepted, {
|
|
356
|
+
workspace: s.preview.workspace_name,
|
|
357
|
+
})}
|
|
358
|
+
/>
|
|
359
|
+
</Card>
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
case "declined":
|
|
363
|
+
return (
|
|
364
|
+
<Card data-testid="invite-accept-page">
|
|
365
|
+
<Result
|
|
366
|
+
status="info"
|
|
367
|
+
title={s.preview.workspace_name}
|
|
368
|
+
subTitle={t(WORKSPACES_I18N_KEYS.inviteDeclined)}
|
|
369
|
+
/>
|
|
370
|
+
</Card>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|