@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
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<WorkspacesPage/>` — the default skin for {@link WorkspaceList}: the
|
|
3
|
+
* roster of workspaces a person belongs to, the door into each one, the
|
|
4
|
+
* "open this one by default" choice, and the create control.
|
|
5
|
+
*
|
|
6
|
+
* §54 said every headless primitive gets an AntD default; this was the one
|
|
7
|
+
* missing, and it is the one every app needs on day one — the audit found
|
|
8
|
+
* `useCanCreateWorkspace` existing only to feed a create button nobody had
|
|
9
|
+
* drawn.
|
|
10
|
+
*
|
|
11
|
+
* Three answers come from the SERVER and are never re-derived here:
|
|
12
|
+
*
|
|
13
|
+
* - `can_create_workspace` — the instance's `WORKSPACE_CREATE_POLICY`, over
|
|
14
|
+
* the same helper the POST gate uses, so a drawn button always opens.
|
|
15
|
+
* - `preferred_workspace_id` — the person's explicit home, which the
|
|
16
|
+
* instance default documents itself as yielding to. Never `workspaces[0]`
|
|
17
|
+
* (that guess was tracker #239) and never `last_accessed_at`, which is
|
|
18
|
+
* telemetry.
|
|
19
|
+
* - `is_guest` — someone here on a link rather than a membership. An empty
|
|
20
|
+
* list means something different to them, and the screen says which.
|
|
21
|
+
*/
|
|
22
|
+
import { useId, useState } from "react";
|
|
23
|
+
import type { ReactElement } from "react";
|
|
24
|
+
import { Button, Card, Flex, Input, Typography, theme as antdTheme } from "antd";
|
|
25
|
+
import {
|
|
26
|
+
actionAvailable,
|
|
27
|
+
actionBlocked,
|
|
28
|
+
loadStateFromQuery,
|
|
29
|
+
requireLoaded,
|
|
30
|
+
useT,
|
|
31
|
+
useTPlural,
|
|
32
|
+
} from "@stapel/core";
|
|
33
|
+
import {
|
|
34
|
+
EmptyState,
|
|
35
|
+
ErrorAlert,
|
|
36
|
+
GatedButton,
|
|
37
|
+
LoadList,
|
|
38
|
+
SkinDialog,
|
|
39
|
+
SkinTheme,
|
|
40
|
+
useBlockedButtonClassName,
|
|
41
|
+
} from "@stapel/tokens-antd/skin";
|
|
42
|
+
import { spacing } from "@stapel/tokens";
|
|
43
|
+
import { WorkspaceList } from "../headless/WorkspaceList.js";
|
|
44
|
+
import type { WorkspaceListBag } from "../headless/WorkspaceList.js";
|
|
45
|
+
import { useWorkspaces } from "../model/queries.js";
|
|
46
|
+
import {
|
|
47
|
+
useClearPreferredWorkspace,
|
|
48
|
+
useSetPreferredWorkspace,
|
|
49
|
+
} from "../model/mutations.js";
|
|
50
|
+
import type { Workspace } from "../api/types.js";
|
|
51
|
+
import { WORKSPACES_I18N_KEYS } from "../i18n/keys.js";
|
|
52
|
+
import { LoadFailure, Muted, StatusTag, SCREEN_STACK } from "./parts.js";
|
|
53
|
+
import { RoleLabel } from "./RoleSelectField.js";
|
|
54
|
+
|
|
55
|
+
export interface WorkspacesPageProps {
|
|
56
|
+
/**
|
|
57
|
+
* Open a workspace. The pair owns no router (there are several, and a
|
|
58
|
+
* library that picks one picks it for every host), so the door is the
|
|
59
|
+
* host's: omit it and no "Open" control is drawn, because a button that
|
|
60
|
+
* goes nowhere is worse than no button.
|
|
61
|
+
*/
|
|
62
|
+
onOpen?(workspace: Workspace): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function WorkspacesPage(props: WorkspacesPageProps): ReactElement {
|
|
66
|
+
return (
|
|
67
|
+
<SkinTheme surface="base" data-testid="workspaces-page">
|
|
68
|
+
<WorkspaceList>
|
|
69
|
+
{(bag) => (
|
|
70
|
+
<PageBody
|
|
71
|
+
bag={bag}
|
|
72
|
+
{...(props.onOpen !== undefined ? { onOpen: props.onOpen } : {})}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
</WorkspaceList>
|
|
76
|
+
</SkinTheme>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function PageBody(props: {
|
|
81
|
+
readonly bag: WorkspaceListBag;
|
|
82
|
+
readonly onOpen?: (workspace: Workspace) => void;
|
|
83
|
+
}): ReactElement {
|
|
84
|
+
const t = useT();
|
|
85
|
+
const tPlural = useTPlural();
|
|
86
|
+
const { bag } = props;
|
|
87
|
+
const listQuery = useWorkspaces();
|
|
88
|
+
const [creating, setCreating] = useState(false);
|
|
89
|
+
const list = listQuery.data ?? null;
|
|
90
|
+
const failureId = useId();
|
|
91
|
+
|
|
92
|
+
// The create verdict is a property of the LIST response, so it is blocked
|
|
93
|
+
// while the list is loading — with core's own sentence — and by the policy
|
|
94
|
+
// when the list arrived and says no.
|
|
95
|
+
//
|
|
96
|
+
// A FAILED read is deliberately not routed through the gate. `requireLoaded`
|
|
97
|
+
// would hand the button its own copy of the outage ("We could not load what
|
|
98
|
+
// this needs…" plus `HTTP 503`) directly above the alert that states the
|
|
99
|
+
// same outage with the retry — the same bad news twice, in two wordings,
|
|
100
|
+
// with two different recoveries. The read has exactly one failure surface on
|
|
101
|
+
// this screen, and the button points at it.
|
|
102
|
+
const listState = loadStateFromQuery(listQuery);
|
|
103
|
+
const listFailed = listState.status === "failed";
|
|
104
|
+
const blockedLook = useBlockedButtonClassName();
|
|
105
|
+
const createGate = requireLoaded(listState, (loaded) =>
|
|
106
|
+
loaded.can_create_workspace === true
|
|
107
|
+
? actionAvailable()
|
|
108
|
+
: actionBlocked(WORKSPACES_I18N_KEYS.listCreateBlockedPolicy)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const rows = bag.state.status === "ready" ? bag.state.data : [];
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div style={SCREEN_STACK}>
|
|
115
|
+
<Card>
|
|
116
|
+
<Flex justify="space-between" align="flex-start" gap={spacing["4"]} wrap>
|
|
117
|
+
<Flex vertical gap={spacing["1"]}>
|
|
118
|
+
<Typography.Title level={4} style={{ marginTop: 0, marginBottom: 0 }}>
|
|
119
|
+
{t(WORKSPACES_I18N_KEYS.pageTitle)}
|
|
120
|
+
</Typography.Title>
|
|
121
|
+
<Typography.Text type="secondary">
|
|
122
|
+
{t(WORKSPACES_I18N_KEYS.pageSubtitle)}
|
|
123
|
+
</Typography.Text>
|
|
124
|
+
{bag.state.status === "ready" && (
|
|
125
|
+
<Muted testId="workspaces-count">
|
|
126
|
+
{tPlural(WORKSPACES_I18N_KEYS.listCount, { count: rows.length })}
|
|
127
|
+
</Muted>
|
|
128
|
+
)}
|
|
129
|
+
</Flex>
|
|
130
|
+
{/* The one primary on the screen. */}
|
|
131
|
+
{listFailed ? (
|
|
132
|
+
<Button
|
|
133
|
+
type="primary"
|
|
134
|
+
// `aria-disabled` + antd's own unavailable paint, never html
|
|
135
|
+
// `disabled`: the button has to stay focusable for the
|
|
136
|
+
// `aria-describedby` below to be announced at all, and it
|
|
137
|
+
// carries no `onClick`, so there is nothing to suppress.
|
|
138
|
+
aria-disabled
|
|
139
|
+
className={blockedLook}
|
|
140
|
+
aria-describedby={failureId}
|
|
141
|
+
data-disabled-reason="the roster read failed; the alert below says so once and carries the retry"
|
|
142
|
+
data-analytics="none"
|
|
143
|
+
data-analytics-reason="local-ui-open-create-dialog"
|
|
144
|
+
data-testid="workspaces-create-open"
|
|
145
|
+
>
|
|
146
|
+
{t(WORKSPACES_I18N_KEYS.listCreate)}
|
|
147
|
+
</Button>
|
|
148
|
+
) : (
|
|
149
|
+
<GatedButton
|
|
150
|
+
gate={createGate}
|
|
151
|
+
type="primary"
|
|
152
|
+
onClick={() => setCreating(true)}
|
|
153
|
+
testId="workspaces-create-open"
|
|
154
|
+
data-analytics="none"
|
|
155
|
+
data-analytics-reason="local-ui-open-create-dialog"
|
|
156
|
+
>
|
|
157
|
+
{t(WORKSPACES_I18N_KEYS.listCreate)}
|
|
158
|
+
</GatedButton>
|
|
159
|
+
)}
|
|
160
|
+
</Flex>
|
|
161
|
+
|
|
162
|
+
{list?.is_guest === true && (
|
|
163
|
+
<div style={{ marginTop: spacing["3"] }}>
|
|
164
|
+
<Muted testId="workspaces-guest">
|
|
165
|
+
{t(WORKSPACES_I18N_KEYS.listGuestNotice)}
|
|
166
|
+
</Muted>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
<ErrorAlert
|
|
171
|
+
thrown={bag.createError}
|
|
172
|
+
style={{ marginTop: spacing["3"] }}
|
|
173
|
+
testId="workspaces-create-error"
|
|
174
|
+
/>
|
|
175
|
+
|
|
176
|
+
<div style={{ marginTop: spacing["4"] }}>
|
|
177
|
+
<LoadList
|
|
178
|
+
state={bag.state}
|
|
179
|
+
testId="workspaces-list"
|
|
180
|
+
onRetry={bag.refetch}
|
|
181
|
+
failed={(error) => (
|
|
182
|
+
<div id={failureId}>
|
|
183
|
+
<LoadFailure
|
|
184
|
+
error={error}
|
|
185
|
+
onRetry={bag.refetch}
|
|
186
|
+
testId="workspaces-list-failed"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
empty={
|
|
191
|
+
<EmptyState
|
|
192
|
+
title={t(WORKSPACES_I18N_KEYS.listEmpty)}
|
|
193
|
+
testId="workspaces-list-empty"
|
|
194
|
+
// The RESTRICTION is stated once, beside the switched-off
|
|
195
|
+
// control above; this line is the other half — what the person
|
|
196
|
+
// can actually do about it. Saying "this installation does not
|
|
197
|
+
// hand out workspaces" here as well was the same fact twice in
|
|
198
|
+
// two wordings on one 390px screen.
|
|
199
|
+
hint={
|
|
200
|
+
createGate.available === true
|
|
201
|
+
? t(WORKSPACES_I18N_KEYS.listEmptyHint)
|
|
202
|
+
: t(WORKSPACES_I18N_KEYS.listInstanceClosed)
|
|
203
|
+
}
|
|
204
|
+
{...(createGate.available === true
|
|
205
|
+
? {
|
|
206
|
+
action: (
|
|
207
|
+
<Button
|
|
208
|
+
type="primary"
|
|
209
|
+
onClick={() => setCreating(true)}
|
|
210
|
+
data-analytics="none"
|
|
211
|
+
data-analytics-reason="local-ui-open-create-dialog"
|
|
212
|
+
>
|
|
213
|
+
{t(WORKSPACES_I18N_KEYS.listCreate)}
|
|
214
|
+
</Button>
|
|
215
|
+
),
|
|
216
|
+
}
|
|
217
|
+
: {})}
|
|
218
|
+
/>
|
|
219
|
+
}
|
|
220
|
+
>
|
|
221
|
+
{(workspaces) => (
|
|
222
|
+
<div role="list" data-testid="workspaces-rows">
|
|
223
|
+
{workspaces.map((workspace) => (
|
|
224
|
+
<WorkspaceRow
|
|
225
|
+
key={workspace.id}
|
|
226
|
+
workspace={workspace}
|
|
227
|
+
isPreferred={list?.preferred_workspace_id === workspace.id}
|
|
228
|
+
{...(props.onOpen !== undefined ? { onOpen: props.onOpen } : {})}
|
|
229
|
+
/>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</LoadList>
|
|
234
|
+
</div>
|
|
235
|
+
</Card>
|
|
236
|
+
|
|
237
|
+
<CreateDialog
|
|
238
|
+
open={creating}
|
|
239
|
+
onClose={() => setCreating(false)}
|
|
240
|
+
isCreating={bag.isCreating}
|
|
241
|
+
onCreate={(name) => {
|
|
242
|
+
bag.create({ name });
|
|
243
|
+
setCreating(false);
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function WorkspaceRow(props: {
|
|
251
|
+
readonly workspace: Workspace;
|
|
252
|
+
readonly isPreferred: boolean;
|
|
253
|
+
readonly onOpen?: (workspace: Workspace) => void;
|
|
254
|
+
}): ReactElement {
|
|
255
|
+
const t = useT();
|
|
256
|
+
const tPlural = useTPlural();
|
|
257
|
+
const { token } = antdTheme.useToken();
|
|
258
|
+
const setPreferred = useSetPreferredWorkspace();
|
|
259
|
+
const clearPreferred = useClearPreferredWorkspace();
|
|
260
|
+
const { workspace } = props;
|
|
261
|
+
const owner = workspace.owner_display_name?.trim();
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<div
|
|
265
|
+
role="listitem"
|
|
266
|
+
data-testid={`workspace-row-${workspace.id}`}
|
|
267
|
+
style={{
|
|
268
|
+
display: "flex",
|
|
269
|
+
flexWrap: "wrap",
|
|
270
|
+
alignItems: "center",
|
|
271
|
+
justifyContent: "space-between",
|
|
272
|
+
gap: spacing["3"],
|
|
273
|
+
paddingTop: spacing["3"],
|
|
274
|
+
paddingBottom: spacing["3"],
|
|
275
|
+
borderBottom: `1px solid ${token.colorSplit}`,
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
<Flex vertical gap={spacing["1"]} style={{ minWidth: 0 }}>
|
|
279
|
+
<Flex gap={spacing["2"]} align="center" wrap>
|
|
280
|
+
<Typography.Text strong>{workspace.name}</Typography.Text>
|
|
281
|
+
{props.isPreferred && (
|
|
282
|
+
<StatusTag tone="info" testId={`workspace-preferred-${workspace.id}`}>
|
|
283
|
+
{t(WORKSPACES_I18N_KEYS.listPreferredTag)}
|
|
284
|
+
</StatusTag>
|
|
285
|
+
)}
|
|
286
|
+
</Flex>
|
|
287
|
+
{/* A workspace NAME stopped identifying one — a person can belong to
|
|
288
|
+
several spaces all called "Personal", one per company. The owner is
|
|
289
|
+
the second line for exactly that reason. */}
|
|
290
|
+
{owner !== undefined && owner !== "" && (
|
|
291
|
+
<Muted>{t(WORKSPACES_I18N_KEYS.listOwnerLine, { owner })}</Muted>
|
|
292
|
+
)}
|
|
293
|
+
<Muted>
|
|
294
|
+
{tPlural(WORKSPACES_I18N_KEYS.listMemberCount, { count: workspace.member_count })}
|
|
295
|
+
{workspace.my_role !== null && (
|
|
296
|
+
<>
|
|
297
|
+
{" · "}
|
|
298
|
+
{t(WORKSPACES_I18N_KEYS.listYourRole)}:{" "}
|
|
299
|
+
<RoleLabel role={workspace.my_role} />
|
|
300
|
+
</>
|
|
301
|
+
)}
|
|
302
|
+
</Muted>
|
|
303
|
+
</Flex>
|
|
304
|
+
<Flex gap={spacing["2"]} align="center" wrap>
|
|
305
|
+
{props.isPreferred ? (
|
|
306
|
+
<Button
|
|
307
|
+
type="link"
|
|
308
|
+
size="small"
|
|
309
|
+
loading={clearPreferred.isPending}
|
|
310
|
+
onClick={() => clearPreferred.mutate()}
|
|
311
|
+
data-analytics="none"
|
|
312
|
+
data-analytics-reason="business action — host app wraps with its own tracked()"
|
|
313
|
+
data-testid={`workspace-clear-home-${workspace.id}`}
|
|
314
|
+
>
|
|
315
|
+
{t(WORKSPACES_I18N_KEYS.listClearPreferred)}
|
|
316
|
+
</Button>
|
|
317
|
+
) : (
|
|
318
|
+
<Button
|
|
319
|
+
type="link"
|
|
320
|
+
size="small"
|
|
321
|
+
loading={setPreferred.isPending}
|
|
322
|
+
onClick={() => setPreferred.mutate({ workspace_id: workspace.id })}
|
|
323
|
+
data-analytics="none"
|
|
324
|
+
data-analytics-reason="business action — host app wraps with its own tracked()"
|
|
325
|
+
data-testid={`workspace-set-home-${workspace.id}`}
|
|
326
|
+
>
|
|
327
|
+
{t(WORKSPACES_I18N_KEYS.listSetPreferred)}
|
|
328
|
+
</Button>
|
|
329
|
+
)}
|
|
330
|
+
{props.onOpen !== undefined && (
|
|
331
|
+
<Button
|
|
332
|
+
onClick={() => props.onOpen?.(workspace)}
|
|
333
|
+
data-analytics="none"
|
|
334
|
+
data-analytics-reason="navigation — the host owns the router"
|
|
335
|
+
data-testid={`workspace-open-${workspace.id}`}
|
|
336
|
+
>
|
|
337
|
+
{t(WORKSPACES_I18N_KEYS.listOpen)}
|
|
338
|
+
</Button>
|
|
339
|
+
)}
|
|
340
|
+
</Flex>
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function CreateDialog(props: {
|
|
346
|
+
readonly open: boolean;
|
|
347
|
+
readonly onClose: () => void;
|
|
348
|
+
readonly isCreating: boolean;
|
|
349
|
+
readonly onCreate: (name: string) => void;
|
|
350
|
+
}): ReactElement {
|
|
351
|
+
const t = useT();
|
|
352
|
+
const [name, setName] = useState("");
|
|
353
|
+
const gate =
|
|
354
|
+
name.trim() === ""
|
|
355
|
+
? actionBlocked(WORKSPACES_I18N_KEYS.listCreateBlockedNoName)
|
|
356
|
+
: actionAvailable();
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<SkinDialog
|
|
360
|
+
open={props.open}
|
|
361
|
+
onClose={props.onClose}
|
|
362
|
+
title={t(WORKSPACES_I18N_KEYS.listCreateDialogTitle)}
|
|
363
|
+
dismissLabel={t(WORKSPACES_I18N_KEYS.dialogClose)}
|
|
364
|
+
data-testid="workspaces-create-dialog"
|
|
365
|
+
footer={
|
|
366
|
+
<GatedButton
|
|
367
|
+
gate={gate}
|
|
368
|
+
type="primary"
|
|
369
|
+
loading={props.isCreating}
|
|
370
|
+
onClick={() => {
|
|
371
|
+
props.onCreate(name.trim());
|
|
372
|
+
setName("");
|
|
373
|
+
}}
|
|
374
|
+
testId="workspaces-create-submit"
|
|
375
|
+
data-analytics="none"
|
|
376
|
+
data-analytics-reason="business action — host app wraps with its own tracked()"
|
|
377
|
+
>
|
|
378
|
+
{t(WORKSPACES_I18N_KEYS.listCreateSubmit)}
|
|
379
|
+
</GatedButton>
|
|
380
|
+
}
|
|
381
|
+
>
|
|
382
|
+
<Flex vertical gap={spacing["2"]}>
|
|
383
|
+
<Typography.Text>{t(WORKSPACES_I18N_KEYS.listCreateNameLabel)}</Typography.Text>
|
|
384
|
+
<Input
|
|
385
|
+
value={name}
|
|
386
|
+
onChange={(event) => setName(event.target.value)}
|
|
387
|
+
placeholder={t(WORKSPACES_I18N_KEYS.listCreateNamePlaceholder)}
|
|
388
|
+
aria-label={t(WORKSPACES_I18N_KEYS.listCreateNameLabel)}
|
|
389
|
+
data-testid="workspaces-create-name"
|
|
390
|
+
/>
|
|
391
|
+
</Flex>
|
|
392
|
+
</SkinDialog>
|
|
393
|
+
);
|
|
394
|
+
}
|
package/src/default/index.ts
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@stapel/workspaces-react/default` —
|
|
3
|
-
*
|
|
4
|
-
* separate entry point so consumers who bring their
|
|
5
|
-
* `antd` into their bundle; importing this subpath is
|
|
2
|
+
* `@stapel/workspaces-react/default` — this pair's DEFAULT SKIN: the antd
|
|
3
|
+
* screens a host mounts to have a working workspace product, not a set of
|
|
4
|
+
* examples (§83, §54). A separate entry point so consumers who bring their
|
|
5
|
+
* own visuals never pull `antd` into their bundle; importing this subpath is
|
|
6
|
+
* the opt-in.
|
|
7
|
+
*
|
|
8
|
+
* Every screen self-themes through `<SkinTheme>` from
|
|
9
|
+
* `@stapel/tokens-antd/skin`, so it is correct inside a dark document with no
|
|
10
|
+
* host wiring, and every one of them needs this pair's `<WorkspacesProvider>`
|
|
11
|
+
* plus core's `<I18nProvider>` above it.
|
|
6
12
|
*
|
|
7
13
|
* ```tsx
|
|
8
|
-
* import {
|
|
9
|
-
*
|
|
10
|
-
*
|
|
14
|
+
* import {
|
|
15
|
+
* WorkspacesPage, WorkspaceSettings, MembersManager,
|
|
16
|
+
* InvitationsPane, AuditTrailPane, InviteAcceptPage,
|
|
17
|
+
* } from "@stapel/workspaces-react/default";
|
|
18
|
+
*
|
|
19
|
+
* <WorkspacesPage onOpen={(ws) => navigate(`/w/${ws.slug}`)} />
|
|
20
|
+
* <WorkspaceSettings workspaceId={id} onDeleted={() => navigate("/")} />
|
|
11
21
|
* <MembersManager workspaceId={id} />
|
|
22
|
+
* <InvitationsPane workspaceId={id} />
|
|
23
|
+
* <AuditTrailPane workspaceId={id} />
|
|
24
|
+
* <InviteAcceptPage token={token} sessionEmail={email} />
|
|
12
25
|
* ```
|
|
13
26
|
*/
|
|
27
|
+
export { WorkspacesPage } from "./WorkspacesPage.js";
|
|
28
|
+
export type { WorkspacesPageProps } from "./WorkspacesPage.js";
|
|
14
29
|
export { WorkspaceSettings } from "./WorkspaceSettings.js";
|
|
15
30
|
export type { WorkspaceSettingsProps } from "./WorkspaceSettings.js";
|
|
16
31
|
export { MembersManager } from "./MembersManager.js";
|
|
17
32
|
export type { MembersManagerProps } from "./MembersManager.js";
|
|
33
|
+
export { InvitationsPane } from "./InvitationsPane.js";
|
|
34
|
+
export type { InvitationsPaneProps } from "./InvitationsPane.js";
|
|
35
|
+
export { AuditTrailPane } from "./AuditTrailPane.js";
|
|
36
|
+
export type { AuditTrailPaneProps } from "./AuditTrailPane.js";
|
|
37
|
+
export { RoleSelectField } from "./RoleSelectField.js";
|
|
38
|
+
export type { RoleSelectFieldProps } from "./RoleSelectField.js";
|
|
18
39
|
export { InviteAcceptPage } from "./InviteAcceptPage.js";
|
|
19
40
|
export type { InviteAcceptPageProps } from "./InviteAcceptPage.js";
|