@stapel/workspaces-react 0.9.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 (41) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/api/generated/schema.d.ts +173 -17
  3. package/dist/api/generated/schema.d.ts.map +1 -1
  4. package/dist/api/types.d.ts +14 -0
  5. package/dist/api/types.d.ts.map +1 -1
  6. package/dist/api/workspacesApi.d.ts +29 -1
  7. package/dist/api/workspacesApi.d.ts.map +1 -1
  8. package/dist/api/workspacesApi.js +2 -0
  9. package/dist/api/workspacesApi.js.map +1 -1
  10. package/dist/i18n/generated/errors.gen.d.ts +30 -0
  11. package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
  12. package/dist/i18n/generated/errors.gen.js +15 -0
  13. package/dist/i18n/generated/errors.gen.js.map +1 -1
  14. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -1
  15. package/dist/i18n/generated/errors.ru.gen.js +5 -0
  16. package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/model/mutations.d.ts +83 -1
  22. package/dist/model/mutations.d.ts.map +1 -1
  23. package/dist/model/mutations.js +93 -0
  24. package/dist/model/mutations.js.map +1 -1
  25. package/dist/model/queryKeys.d.ts +1 -0
  26. package/dist/model/queryKeys.d.ts.map +1 -1
  27. package/dist/model/queryKeys.js +7 -0
  28. package/dist/model/queryKeys.js.map +1 -1
  29. package/llms.txt +7 -3
  30. package/manifest.json +73 -2
  31. package/package.json +3 -3
  32. package/src/analytics/generated/events.json +1 -1
  33. package/src/api/generated/schema.ts +173 -17
  34. package/src/api/types.ts +14 -0
  35. package/src/api/workspacesApi.ts +53 -0
  36. package/src/i18n/generated/errors.gen.ts +15 -0
  37. package/src/i18n/generated/errors.json +35 -0
  38. package/src/i18n/generated/errors.ru.gen.ts +5 -0
  39. package/src/index.ts +6 -0
  40. package/src/model/mutations.ts +127 -0
  41. package/src/model/queryKeys.ts +8 -0
package/src/index.ts CHANGED
@@ -89,6 +89,8 @@ export {
89
89
  useInviteMembers,
90
90
  useUpdateMemberRole,
91
91
  useRemoveMember,
92
+ useRenameMember,
93
+ useRenameInvitation,
92
94
  useAcceptInvitation,
93
95
  useClaimInvitation,
94
96
  useDeclineInvitation,
@@ -99,6 +101,8 @@ export {
99
101
  } from "./model/mutations.js";
100
102
  export type {
101
103
  MemberRoleChange,
104
+ MemberRename,
105
+ InvitationRename,
102
106
  MemberPasswordResetVars,
103
107
  } from "./model/mutations.js";
104
108
 
@@ -123,6 +127,8 @@ export type {
123
127
  InvitationClaim,
124
128
  MemberPasswordReset,
125
129
  MemberPasswordResetResult,
130
+ DisplayNameUpdate,
131
+ DisplayNameResult,
126
132
  ProvisionedUserPolicy,
127
133
  WorkspaceSecuritySettings,
128
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,
@@ -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
@@ -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
@@ -45,6 +46,13 @@ export const workspacesQueryKeys: {
45
46
  // The bare 3-tuple is a valid prefix of membersPage's 4-tuple, so
46
47
  // invalidating `members(workspaceId)` (mutations.ts) drops every page.
47
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"],
48
56
  membersPage: (workspaceId, params) => [ROOT, "members", workspaceId, params],
49
57
  // Same prefix trick as `members`: the bare 3-tuple invalidates every page
50
58
  // (and the infinite list, which keys on it) after a revoke / resend.