@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/api/generated/schema.d.ts +263 -2
  3. package/dist/api/generated/schema.d.ts.map +1 -1
  4. package/dist/api/types.d.ts +21 -0
  5. package/dist/api/types.d.ts.map +1 -1
  6. package/dist/api/workspacesApi.d.ts +31 -1
  7. package/dist/api/workspacesApi.d.ts.map +1 -1
  8. package/dist/api/workspacesApi.js +3 -0
  9. package/dist/api/workspacesApi.js.map +1 -1
  10. package/dist/default/MembersManager.js +2 -2
  11. package/dist/i18n/generated/errors.gen.d.ts +36 -0
  12. package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
  13. package/dist/i18n/generated/errors.gen.js +18 -0
  14. package/dist/i18n/generated/errors.gen.js.map +1 -1
  15. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -1
  16. package/dist/i18n/generated/errors.ru.gen.js +6 -0
  17. package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
  18. package/dist/index.d.ts +4 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/model/mutations.d.ts +84 -2
  23. package/dist/model/mutations.d.ts.map +1 -1
  24. package/dist/model/mutations.js +94 -1
  25. package/dist/model/mutations.js.map +1 -1
  26. package/dist/model/queries.d.ts +19 -1
  27. package/dist/model/queries.d.ts.map +1 -1
  28. package/dist/model/queries.js +25 -0
  29. package/dist/model/queries.js.map +1 -1
  30. package/dist/model/queryKeys.d.ts +2 -0
  31. package/dist/model/queryKeys.d.ts.map +1 -1
  32. package/dist/model/queryKeys.js +9 -1
  33. package/dist/model/queryKeys.js.map +1 -1
  34. package/llms.txt +10 -3
  35. package/manifest.json +97 -2
  36. package/package.json +3 -3
  37. package/src/analytics/generated/events.json +1 -1
  38. package/src/api/generated/schema.ts +263 -2
  39. package/src/api/types.ts +22 -0
  40. package/src/api/workspacesApi.ts +59 -0
  41. package/src/default/MembersManager.tsx +2 -2
  42. package/src/i18n/generated/errors.gen.ts +18 -0
  43. package/src/i18n/generated/errors.json +44 -0
  44. package/src/i18n/generated/errors.ru.gen.ts +6 -0
  45. package/src/index.ts +7 -0
  46. package/src/model/mutations.ts +128 -1
  47. package/src/model/queries.ts +30 -0
  48. 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,
@@ -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 — "мутации с инвалидацией"). Each mutation
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
@@ -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.
@@ -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