@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,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<RoleSelectField/>` — the default skin for {@link RoleSelect} (§54: every
|
|
3
|
+
* headless primitive gets an AntD default).
|
|
4
|
+
*
|
|
5
|
+
* The visual pass photographed the role story as five bullet points: no
|
|
6
|
+
* control, no current value, nothing focusable, and a deployment role
|
|
7
|
+
* (`secretary`) printed lowercase beside title-cased builtin ones. This is
|
|
8
|
+
* that primitive drawn as what it is — a real `<Select>` with an accessible
|
|
9
|
+
* name, the effective registry behind it, and the role's RANK as the caption
|
|
10
|
+
* in the menu, because "admin outranks member" is the fact a person picking a
|
|
11
|
+
* role is actually reasoning about.
|
|
12
|
+
*
|
|
13
|
+
* The three non-ready states are not a select at all:
|
|
14
|
+
* - loading — the control is there and busy, so the layout does not jump.
|
|
15
|
+
* - failed — NO picker. An enabled `<Select options={[]}/>` offers a choice
|
|
16
|
+
* that does not exist; the reason is printed instead (the registry read is
|
|
17
|
+
* a separate load from whatever list the caller is drawing).
|
|
18
|
+
* - empty — a registry with no roles is not a picker either.
|
|
19
|
+
*/
|
|
20
|
+
import type { ReactElement } from "react";
|
|
21
|
+
import { Flex, Select, Typography } from "antd";
|
|
22
|
+
import { matchList, useI18n, useT } from "@stapel/core";
|
|
23
|
+
import { spacing, fontSize } from "@stapel/tokens";
|
|
24
|
+
import { RoleSelect } from "../headless/RoleSelect.js";
|
|
25
|
+
import { titleCaseKey } from "../model/format.js";
|
|
26
|
+
import type { RoleInfo } from "../api/types.js";
|
|
27
|
+
import { WORKSPACES_I18N_KEYS } from "../i18n/keys.js";
|
|
28
|
+
import { Muted } from "./parts.js";
|
|
29
|
+
|
|
30
|
+
/** The system-protected role only an owner may grant. */
|
|
31
|
+
const OWNER_ROLE = "owner";
|
|
32
|
+
|
|
33
|
+
export interface RoleSelectFieldProps {
|
|
34
|
+
/** The registry key currently chosen. Kept as the VALUE even when the
|
|
35
|
+
* registry has no label for it — a deployment that removed an overlay role
|
|
36
|
+
* must still show what the member actually holds. */
|
|
37
|
+
readonly value: string;
|
|
38
|
+
readonly onChange: (role: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* The control's accessible name. Required, not optional: a roster renders
|
|
41
|
+
* one of these per row, and "combobox, combobox, combobox" is what a screen
|
|
42
|
+
* reader says when the name is left to the layout.
|
|
43
|
+
*/
|
|
44
|
+
readonly label: string;
|
|
45
|
+
/** Draw the name above the control as well. Off in a table cell, where the
|
|
46
|
+
* column header is the visible label. */
|
|
47
|
+
readonly showLabel?: boolean;
|
|
48
|
+
/** Leave `owner` out of the menu — the invite path, where the backend
|
|
49
|
+
* enforces "only an owner grants owner" anyway. */
|
|
50
|
+
readonly excludeOwner?: boolean;
|
|
51
|
+
readonly size?: "small" | "middle" | "large";
|
|
52
|
+
/** Forwarded straight to the control by a caller that has already stated
|
|
53
|
+
* its own reason beside it (a gate, a read-only roster). */
|
|
54
|
+
readonly disabled?: boolean;
|
|
55
|
+
readonly "aria-describedby"?: string | undefined;
|
|
56
|
+
readonly testId?: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Role key → the word for it, with no registry read behind it.
|
|
61
|
+
*
|
|
62
|
+
* `RoleSelect`'s own `labelFor` is a bundle lookup that never touches the
|
|
63
|
+
* query, but mounting `<RoleSelect>` to reach it also fires GET /roles — which
|
|
64
|
+
* on the PUBLIC invitation page is a call an anonymous reader cannot make.
|
|
65
|
+
* A caption that only needs a word asks for a word.
|
|
66
|
+
*/
|
|
67
|
+
export function useRoleLabel(): (role: string) => string {
|
|
68
|
+
const i18n = useI18n();
|
|
69
|
+
// Raw bundle lookup, NOT `t()`: `t` falls back to the key itself, and
|
|
70
|
+
// `workspaces.role.secretary` is the one thing this must never print.
|
|
71
|
+
const bundle = i18n.getBundle();
|
|
72
|
+
return (role) => bundle[`workspaces.role.${role}`] ?? titleCaseKey(role);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A role as a WORD, where there is nothing to pick — an invitation's granted
|
|
77
|
+
* role, a read-only roster. The same label resolution as the field, so one
|
|
78
|
+
* screen never calls `admin` "Admin" while its neighbour prints `admin`.
|
|
79
|
+
*/
|
|
80
|
+
export function RoleLabel(props: { readonly role: string }): ReactElement {
|
|
81
|
+
const labelFor = useRoleLabel();
|
|
82
|
+
return <>{labelFor(props.role)}</>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function RoleSelectField(props: RoleSelectFieldProps): ReactElement {
|
|
86
|
+
const t = useT();
|
|
87
|
+
return (
|
|
88
|
+
<RoleSelect>
|
|
89
|
+
{({ state, labelFor }) => {
|
|
90
|
+
const control = matchList(state, {
|
|
91
|
+
loading: () => (
|
|
92
|
+
<Select<string>
|
|
93
|
+
value={props.value}
|
|
94
|
+
loading
|
|
95
|
+
open={false}
|
|
96
|
+
size={props.size ?? "middle"}
|
|
97
|
+
style={{ width: "100%" }}
|
|
98
|
+
aria-label={props.label}
|
|
99
|
+
options={[{ value: props.value, label: labelFor(props.value) }]}
|
|
100
|
+
onChange={props.onChange}
|
|
101
|
+
disabled
|
|
102
|
+
data-disabled-reason="the role registry is still loading; the current role is shown meanwhile"
|
|
103
|
+
{...(props.testId !== undefined ? { "data-testid": props.testId } : {})}
|
|
104
|
+
/>
|
|
105
|
+
),
|
|
106
|
+
// A failed registry read is not an empty registry, and neither is a
|
|
107
|
+
// picker. The role still READS; only the choice is withdrawn.
|
|
108
|
+
failed: () => (
|
|
109
|
+
<Flex vertical gap={spacing["1"]} align="flex-start">
|
|
110
|
+
<Typography.Text>{labelFor(props.value)}</Typography.Text>
|
|
111
|
+
<Muted testId={props.testId !== undefined ? `${props.testId}-blocked` : undefined}>
|
|
112
|
+
{t(WORKSPACES_I18N_KEYS.rolesLoadFailed)}
|
|
113
|
+
</Muted>
|
|
114
|
+
</Flex>
|
|
115
|
+
),
|
|
116
|
+
// A registry that came back EMPTY is not a registry that failed —
|
|
117
|
+
// saying "we could not load the role list" about a successful read
|
|
118
|
+
// is the same class of lie as an empty state over a failed one.
|
|
119
|
+
empty: () => (
|
|
120
|
+
<Flex vertical gap={spacing["1"]} align="flex-start">
|
|
121
|
+
<Typography.Text>{labelFor(props.value)}</Typography.Text>
|
|
122
|
+
<Muted>{t(WORKSPACES_I18N_KEYS.rolesEmpty)}</Muted>
|
|
123
|
+
</Flex>
|
|
124
|
+
),
|
|
125
|
+
ready: (roles) => {
|
|
126
|
+
const offered: readonly RoleInfo[] =
|
|
127
|
+
props.excludeOwner === true
|
|
128
|
+
? roles.filter((role) => role.role !== OWNER_ROLE)
|
|
129
|
+
: roles;
|
|
130
|
+
const rankOf = new Map(offered.map((role) => [role.role, role.rank]));
|
|
131
|
+
const options = offered.map((role) => ({
|
|
132
|
+
value: role.role,
|
|
133
|
+
label: labelFor(role.role),
|
|
134
|
+
}));
|
|
135
|
+
// The member's CURRENT role may be missing from the offered set
|
|
136
|
+
// (an overlay role a deployment removed, or `owner` on the invite
|
|
137
|
+
// path). Antd would render the bare key for it; the registry's
|
|
138
|
+
// own label is better, and the row must keep showing what the
|
|
139
|
+
// person actually holds.
|
|
140
|
+
const hasCurrent = options.some((option) => option.value === props.value);
|
|
141
|
+
const allOptions = hasCurrent
|
|
142
|
+
? options
|
|
143
|
+
: [{ value: props.value, label: labelFor(props.value) }, ...options];
|
|
144
|
+
return (
|
|
145
|
+
<Select<string>
|
|
146
|
+
value={props.value}
|
|
147
|
+
onChange={props.onChange}
|
|
148
|
+
size={props.size ?? "middle"}
|
|
149
|
+
style={{ width: "100%" }}
|
|
150
|
+
aria-label={props.label}
|
|
151
|
+
{...(props["aria-describedby"] !== undefined
|
|
152
|
+
? { "aria-describedby": props["aria-describedby"] }
|
|
153
|
+
: {})}
|
|
154
|
+
{...(props.disabled !== undefined ? { disabled: props.disabled } : {})}
|
|
155
|
+
options={allOptions}
|
|
156
|
+
optionRender={(option) => {
|
|
157
|
+
const rank = rankOf.get(String(option.value));
|
|
158
|
+
return (
|
|
159
|
+
<Flex vertical gap={spacing["0"]}>
|
|
160
|
+
<span>{option.label}</span>
|
|
161
|
+
{rank !== undefined && (
|
|
162
|
+
<Typography.Text
|
|
163
|
+
type="secondary"
|
|
164
|
+
style={{ fontSize: fontSize.xs.fontSize }}
|
|
165
|
+
>
|
|
166
|
+
{t(WORKSPACES_I18N_KEYS.roleRankCaption, { rank })}
|
|
167
|
+
</Typography.Text>
|
|
168
|
+
)}
|
|
169
|
+
</Flex>
|
|
170
|
+
);
|
|
171
|
+
}}
|
|
172
|
+
{...(props.testId !== undefined ? { "data-testid": props.testId } : {})}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
if (props.showLabel !== true) return control;
|
|
178
|
+
return (
|
|
179
|
+
<Flex vertical gap={spacing["1"]} style={{ width: "100%" }}>
|
|
180
|
+
<Typography.Text>{props.label}</Typography.Text>
|
|
181
|
+
{control}
|
|
182
|
+
</Flex>
|
|
183
|
+
);
|
|
184
|
+
}}
|
|
185
|
+
</RoleSelect>
|
|
186
|
+
);
|
|
187
|
+
}
|