@stapel/workspaces-react 0.8.0 → 0.10.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 +51 -0
- package/dist/api/generated/schema.d.ts +263 -2
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/types.d.ts +21 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/workspacesApi.d.ts +31 -1
- package/dist/api/workspacesApi.d.ts.map +1 -1
- package/dist/api/workspacesApi.js +3 -0
- package/dist/api/workspacesApi.js.map +1 -1
- package/dist/default/MembersManager.js +2 -2
- package/dist/i18n/generated/errors.gen.d.ts +36 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.gen.js +18 -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 +6 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/model/mutations.d.ts +84 -2
- package/dist/model/mutations.d.ts.map +1 -1
- package/dist/model/mutations.js +94 -1
- package/dist/model/mutations.js.map +1 -1
- package/dist/model/queries.d.ts +19 -1
- package/dist/model/queries.d.ts.map +1 -1
- package/dist/model/queries.js +25 -0
- 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 +9 -1
- package/dist/model/queryKeys.js.map +1 -1
- package/llms.txt +10 -3
- package/manifest.json +97 -2
- package/package.json +3 -3
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +263 -2
- package/src/api/types.ts +22 -0
- package/src/api/workspacesApi.ts +59 -0
- package/src/default/MembersManager.tsx +2 -2
- package/src/i18n/generated/errors.gen.ts +18 -0
- package/src/i18n/generated/errors.json +44 -0
- package/src/i18n/generated/errors.ru.gen.ts +6 -0
- package/src/index.ts +7 -0
- package/src/model/mutations.ts +128 -1
- package/src/model/queries.ts +30 -0
- package/src/model/queryKeys.ts +11 -1
package/src/index.ts
CHANGED
|
@@ -60,6 +60,7 @@ export {
|
|
|
60
60
|
useInvitations,
|
|
61
61
|
useInfiniteInvitations,
|
|
62
62
|
useRoles,
|
|
63
|
+
useInstanceShape,
|
|
63
64
|
useInvitationPreview,
|
|
64
65
|
useCapabilities,
|
|
65
66
|
useCapabilityGate,
|
|
@@ -88,6 +89,8 @@ export {
|
|
|
88
89
|
useInviteMembers,
|
|
89
90
|
useUpdateMemberRole,
|
|
90
91
|
useRemoveMember,
|
|
92
|
+
useRenameMember,
|
|
93
|
+
useRenameInvitation,
|
|
91
94
|
useAcceptInvitation,
|
|
92
95
|
useClaimInvitation,
|
|
93
96
|
useDeclineInvitation,
|
|
@@ -98,6 +101,8 @@ export {
|
|
|
98
101
|
} from "./model/mutations.js";
|
|
99
102
|
export type {
|
|
100
103
|
MemberRoleChange,
|
|
104
|
+
MemberRename,
|
|
105
|
+
InvitationRename,
|
|
101
106
|
MemberPasswordResetVars,
|
|
102
107
|
} from "./model/mutations.js";
|
|
103
108
|
|
|
@@ -122,6 +127,8 @@ export type {
|
|
|
122
127
|
InvitationClaim,
|
|
123
128
|
MemberPasswordReset,
|
|
124
129
|
MemberPasswordResetResult,
|
|
130
|
+
DisplayNameUpdate,
|
|
131
|
+
DisplayNameResult,
|
|
125
132
|
ProvisionedUserPolicy,
|
|
126
133
|
WorkspaceSecuritySettings,
|
|
127
134
|
RoleInfo,
|
package/src/model/mutations.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
} from "@tanstack/react-query";
|
|
6
6
|
import type { StapelApiError } from "@stapel/core";
|
|
7
7
|
import type {
|
|
8
|
+
DisplayNameResult,
|
|
8
9
|
Invitation,
|
|
9
10
|
Member,
|
|
10
11
|
MemberInvite,
|
|
@@ -22,7 +23,7 @@ import { useWorkspacesApi } from "./context.js";
|
|
|
22
23
|
import { workspacesQueryKeys } from "./queryKeys.js";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Write hooks (frontend-standard §2 —
|
|
26
|
+
* Write hooks (frontend-standard §2 — mutations invalidate on success). Each mutation
|
|
26
27
|
* invalidates exactly the server state it can move. Membership and ownership are
|
|
27
28
|
* server truth (roles gate access in other services via the membership cache),
|
|
28
29
|
* so NONE of these are optimistic (frontend-core-architecture §2.6: optimistic
|
|
@@ -179,6 +180,132 @@ export function useRemoveMember(
|
|
|
179
180
|
return useMutation(options);
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
/** The variable for {@link useRenameMember}: whose name, and the new one.
|
|
184
|
+
* `displayName` is nullable because clearing IS a supported outcome — blank,
|
|
185
|
+
* whitespace-only and `null` all mean "no name for this person" to the
|
|
186
|
+
* backend, which then answers with an empty string. */
|
|
187
|
+
export interface MemberRename {
|
|
188
|
+
readonly userId: string;
|
|
189
|
+
readonly displayName: string | null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Correct a co-member's display name (PATCH /{id}/members/{userId}/name,
|
|
194
|
+
* stapel-workspaces ≥0.19.0) — the roster-side fix an owner/admin applies
|
|
195
|
+
* without waiting for the person themselves.
|
|
196
|
+
*
|
|
197
|
+
* **Capability `members.role.change`**, not `members.invite`: ask
|
|
198
|
+
* {@link "./queries.js".useCapabilityGate}(workspaceId,
|
|
199
|
+
* "members.role.change") before offering the affordance. It is `standard`, so
|
|
200
|
+
* no step-up is demanded (contrast {@link useResetMemberPassword}). Anonymous
|
|
201
|
+
* callers are denied outright, and only an owner may rename an owner.
|
|
202
|
+
*
|
|
203
|
+
* **What the write moves, and hence what is invalidated.** The backend writes
|
|
204
|
+
* the CANONICAL name — stapel-profiles' `Profile.display_name` through the
|
|
205
|
+
* in-process profiles seam — not the membership's local
|
|
206
|
+
* `display_name_hint`, which goes dark the moment a real profile exists. So
|
|
207
|
+
* the stale set is not "the page this row was on": it is EVERY cached roster,
|
|
208
|
+
* in every workspace, because `MemberResponse.display_name` is a live lookup
|
|
209
|
+
* of that one canonical value. Hence
|
|
210
|
+
* {@link "./queryKeys.js".workspacesQueryKeys.membersAll}, the workspace-less
|
|
211
|
+
* prefix — invalidating `members(workspaceId)` would leave the same person
|
|
212
|
+
* showing their old name on every other roster the session has open. A
|
|
213
|
+
* rename can also move a row out of an active `search` filter, which the
|
|
214
|
+
* prefix covers too (every page, every filter).
|
|
215
|
+
*
|
|
216
|
+
* A host that ALSO renders `@stapel/profiles-react` data for this person owns
|
|
217
|
+
* the other half: this pair never reaches into another pair's query namespace
|
|
218
|
+
* (the same seam discipline as `InviteAcceptFlow`'s basic-data slot), so
|
|
219
|
+
* invalidate `profilesQueryKeys.profile(userId)` yourself there.
|
|
220
|
+
*
|
|
221
|
+
* **Errors** arrive in the single dialect, keyed: the four display-name rules
|
|
222
|
+
* borrowed verbatim from stapel-profiles
|
|
223
|
+
* (`error.400.display_name_too_short` / `_forbidden_chars` /
|
|
224
|
+
* `_invisible_chars` / `_emoji`), over-length as the fleet-standard
|
|
225
|
+
* `error.400.field.max_length` with `{field, max_length}` (no bespoke code),
|
|
226
|
+
* and `error.503.profiles_unavailable` where stapel-profiles does not run in
|
|
227
|
+
* the deployment's process — an honest refusal rather than a 200 over a write
|
|
228
|
+
* that did not happen. Render them with `explainWorkspacesError` / `t(code,
|
|
229
|
+
* params)` like every other key.
|
|
230
|
+
*/
|
|
231
|
+
export function useRenameMember(
|
|
232
|
+
workspaceId: string
|
|
233
|
+
): UseMutationResult<DisplayNameResult, StapelApiError, MemberRename> {
|
|
234
|
+
const api = useWorkspacesApi();
|
|
235
|
+
const queryClient = useQueryClient();
|
|
236
|
+
const options: UseMutationOptions<
|
|
237
|
+
DisplayNameResult,
|
|
238
|
+
StapelApiError,
|
|
239
|
+
MemberRename
|
|
240
|
+
> = {
|
|
241
|
+
mutationFn: ({ userId, displayName }) =>
|
|
242
|
+
api.renameMember(workspaceId, userId, { display_name: displayName }),
|
|
243
|
+
onSuccess: () => {
|
|
244
|
+
void queryClient.invalidateQueries({
|
|
245
|
+
queryKey: workspacesQueryKeys.membersAll(),
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
return useMutation(options);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The variable for {@link useRenameInvitation}: which pending invitation, and
|
|
253
|
+
* the corrected name hint (nullable for the same reason as
|
|
254
|
+
* {@link MemberRename}). */
|
|
255
|
+
export interface InvitationRename {
|
|
256
|
+
readonly invitationId: string;
|
|
257
|
+
readonly displayName: string | null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Fix a pending invitation's name hint (PATCH
|
|
262
|
+
* /{id}/invitations/{invitationId}/name, stapel-workspaces ≥0.19.0) — the
|
|
263
|
+
* same correction as {@link useRenameMember}, one step earlier. The invitee
|
|
264
|
+
* has not accepted, so there is no profile of theirs to write and the name
|
|
265
|
+
* lives on the invitation; `accept_invitation` copies it onto the membership.
|
|
266
|
+
* Before this endpoint the only fix for a typo in an invitee's name was
|
|
267
|
+
* revoke-and-re-invite, which re-mails the person.
|
|
268
|
+
*
|
|
269
|
+
* **The same capability as the member rename** (`members.role.change`, not
|
|
270
|
+
* the invitation surface's `members.invite`) — the hint IS the member's name
|
|
271
|
+
* after acceptance, so splitting them would let a role fix a name that
|
|
272
|
+
* reverts.
|
|
273
|
+
*
|
|
274
|
+
* Only a PENDING invitation is editable; a terminal one refuses with the
|
|
275
|
+
* keyed answers revoke gives (`error.400.invitation_already_used` /
|
|
276
|
+
* `_revoked` / `_declined` / `_expired`), and an accepted invitation's name
|
|
277
|
+
* is the member's name now — use {@link useRenameMember}. Validation is the
|
|
278
|
+
* same stapel-profiles canon, so the same four display-name keys surface
|
|
279
|
+
* here.
|
|
280
|
+
*
|
|
281
|
+
* Invalidation is deliberately NARROWER than the member rename's: the hint is
|
|
282
|
+
* a workspace-local column on one invitation, not a shared canonical name, so
|
|
283
|
+
* only this workspace's invitation lists (every page and the infinite walk —
|
|
284
|
+
* the bare 3-tuple prefix) can be showing it.
|
|
285
|
+
*/
|
|
286
|
+
export function useRenameInvitation(
|
|
287
|
+
workspaceId: string
|
|
288
|
+
): UseMutationResult<DisplayNameResult, StapelApiError, InvitationRename> {
|
|
289
|
+
const api = useWorkspacesApi();
|
|
290
|
+
const queryClient = useQueryClient();
|
|
291
|
+
const options: UseMutationOptions<
|
|
292
|
+
DisplayNameResult,
|
|
293
|
+
StapelApiError,
|
|
294
|
+
InvitationRename
|
|
295
|
+
> = {
|
|
296
|
+
mutationFn: ({ invitationId, displayName }) =>
|
|
297
|
+
api.renameInvitation(workspaceId, invitationId, {
|
|
298
|
+
display_name: displayName,
|
|
299
|
+
}),
|
|
300
|
+
onSuccess: () => {
|
|
301
|
+
void queryClient.invalidateQueries({
|
|
302
|
+
queryKey: workspacesQueryKeys.invitations(workspaceId),
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
return useMutation(options);
|
|
307
|
+
}
|
|
308
|
+
|
|
182
309
|
/**
|
|
183
310
|
* Withdraw a live invitation (POST /{id}/invitations/{invitationId}/revoke,
|
|
184
311
|
* #109). The variable is the invitation id; the reply is the UPDATED row, so
|
package/src/model/queries.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { StapelApiError } from "@stapel/core";
|
|
|
9
9
|
import type {
|
|
10
10
|
InvitationPage,
|
|
11
11
|
InvitationPreview,
|
|
12
|
+
InstanceShape,
|
|
12
13
|
InvitationsParams,
|
|
13
14
|
MemberPage,
|
|
14
15
|
MembersParams,
|
|
@@ -173,6 +174,35 @@ export function useRoles(): UseQueryResult<
|
|
|
173
174
|
});
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Deployment shape (GET /instance, unauthenticated).
|
|
179
|
+
*
|
|
180
|
+
* Answers the question a screen must ask BEFORE deciding what to show
|
|
181
|
+
* someone with no workspace: is this a closed deployment (`landing:
|
|
182
|
+
* "none"` — no workspace exists and none can be created) or a public cloud
|
|
183
|
+
* (`"personal"` — one exists and the screen can lead there)?
|
|
184
|
+
*
|
|
185
|
+
* Why a hook instead of a profile field: the reader is exactly the person
|
|
186
|
+
* who has no access — kicked out of their workspace, or having left it. So
|
|
187
|
+
* the request deliberately does NOT wait for a session, unlike {@link
|
|
188
|
+
* useRoles}: waiting would mean never answering the one person it exists for.
|
|
189
|
+
*
|
|
190
|
+
* The data is static per deployment — it only changes alongside the
|
|
191
|
+
* deployment itself, so the cache lives until page reload and is never
|
|
192
|
+
* refetched.
|
|
193
|
+
*/
|
|
194
|
+
export function useInstanceShape(): UseQueryResult<
|
|
195
|
+
InstanceShape,
|
|
196
|
+
StapelApiError
|
|
197
|
+
> {
|
|
198
|
+
const api = useWorkspacesApi();
|
|
199
|
+
return useQuery({
|
|
200
|
+
queryKey: workspacesQueryKeys.instance(),
|
|
201
|
+
queryFn: () => api.getInstanceShape(),
|
|
202
|
+
staleTime: Infinity,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
176
206
|
/**
|
|
177
207
|
* Public invitation preview (GET /invitations/{token}, AllowAny — org-program
|
|
178
208
|
* §B2): what the `/invite/{token}` page renders BEFORE any auth decision.
|
package/src/model/queryKeys.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { InvitationsParams, MembersParams } from "../api/types.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Namespaced TanStack Query keys (frontend-standard §2 —
|
|
4
|
+
* Namespaced TanStack Query keys (frontend-standard §2 — namespaced keys).
|
|
5
5
|
* Everything under the `"workspaces"` root so a host can invalidate the whole
|
|
6
6
|
* module or match a single resource. Persist scope is per-user via core's query
|
|
7
7
|
* runtime (`setPersistUser`). Explicit tuple return types satisfy
|
|
@@ -14,6 +14,7 @@ export const workspacesQueryKeys: {
|
|
|
14
14
|
list(): readonly ["workspaces", "list"];
|
|
15
15
|
detail(workspaceId: string): readonly ["workspaces", "detail", string];
|
|
16
16
|
members(workspaceId: string): readonly ["workspaces", "members", string];
|
|
17
|
+
membersAll(): readonly ["workspaces", "members"];
|
|
17
18
|
membersPage(
|
|
18
19
|
workspaceId: string,
|
|
19
20
|
params: MembersParams
|
|
@@ -34,6 +35,7 @@ export const workspacesQueryKeys: {
|
|
|
34
35
|
InvitationsParams,
|
|
35
36
|
];
|
|
36
37
|
roles(): readonly ["workspaces", "roles"];
|
|
38
|
+
instance(): readonly ["workspaces", "instance"];
|
|
37
39
|
invitationPreview(
|
|
38
40
|
token: string
|
|
39
41
|
): readonly ["workspaces", "invitation-preview", string];
|
|
@@ -44,6 +46,13 @@ export const workspacesQueryKeys: {
|
|
|
44
46
|
// The bare 3-tuple is a valid prefix of membersPage's 4-tuple, so
|
|
45
47
|
// invalidating `members(workspaceId)` (mutations.ts) drops every page.
|
|
46
48
|
members: (workspaceId) => [ROOT, "members", workspaceId],
|
|
49
|
+
// EVERY workspace's roster, not one. Not a read key — nothing fetches it —
|
|
50
|
+
// but the invalidation scope of a write that moves a name the whole fleet
|
|
51
|
+
// shares: `MemberResponse.display_name` is a live lookup in
|
|
52
|
+
// stapel-profiles, so a rename lands on the person's CANONICAL name and
|
|
53
|
+
// every roster that lists them is stale at once, not just the one the
|
|
54
|
+
// admin happened to be looking at.
|
|
55
|
+
membersAll: () => [ROOT, "members"],
|
|
47
56
|
membersPage: (workspaceId, params) => [ROOT, "members", workspaceId, params],
|
|
48
57
|
// Same prefix trick as `members`: the bare 3-tuple invalidates every page
|
|
49
58
|
// (and the infinite list, which keys on it) after a revoke / resend.
|
|
@@ -65,6 +74,7 @@ export const workspacesQueryKeys: {
|
|
|
65
74
|
filters,
|
|
66
75
|
],
|
|
67
76
|
roles: () => [ROOT, "roles"],
|
|
77
|
+
instance: () => [ROOT, "instance"],
|
|
68
78
|
// NOTE: the key carries the invite TOKEN (a secret). Core's query persist
|
|
69
79
|
// scope is per-user and the preview response is already public-by-design
|
|
70
80
|
// (masked email only) — but hosts logging query keys should treat this one
|