@stapel/workspaces-react 0.9.0 → 0.11.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 +102 -0
- package/dist/api/generated/schema.d.ts +283 -17
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/types.d.ts +23 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/workspacesApi.d.ts +37 -1
- package/dist/api/workspacesApi.d.ts.map +1 -1
- package/dist/api/workspacesApi.js +4 -0
- package/dist/api/workspacesApi.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/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/model/mutations.d.ts +98 -1
- package/dist/model/mutations.d.ts.map +1 -1
- package/dist/model/mutations.js +132 -0
- package/dist/model/mutations.js.map +1 -1
- package/dist/model/queryKeys.d.ts +1 -0
- package/dist/model/queryKeys.d.ts.map +1 -1
- package/dist/model/queryKeys.js +7 -0
- package/dist/model/queryKeys.js.map +1 -1
- package/dist/model/selection.d.ts +146 -0
- package/dist/model/selection.d.ts.map +1 -0
- package/dist/model/selection.js +217 -0
- package/dist/model/selection.js.map +1 -0
- package/llms.txt +11 -3
- package/manifest.json +117 -2
- package/package.json +2 -2
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +283 -17
- package/src/api/types.ts +23 -0
- package/src/api/workspacesApi.ts +74 -0
- package/src/i18n/generated/errors.gen.ts +15 -0
- package/src/i18n/generated/errors.json +35 -0
- package/src/i18n/generated/errors.ru.gen.ts +5 -0
- package/src/index.ts +22 -0
- package/src/model/mutations.ts +186 -0
- package/src/model/queryKeys.ts +8 -0
- package/src/model/selection.tsx +382 -0
|
@@ -126,6 +126,46 @@ export interface paths {
|
|
|
126
126
|
patch?: never;
|
|
127
127
|
trace?: never;
|
|
128
128
|
};
|
|
129
|
+
"/workspaces/api/v1/{workspace_id}/invitations/{invitation_id}/name": {
|
|
130
|
+
parameters: {
|
|
131
|
+
query?: never;
|
|
132
|
+
header?: never;
|
|
133
|
+
path?: never;
|
|
134
|
+
cookie?: never;
|
|
135
|
+
};
|
|
136
|
+
get?: never;
|
|
137
|
+
put?: never;
|
|
138
|
+
post?: never;
|
|
139
|
+
delete?: never;
|
|
140
|
+
options?: never;
|
|
141
|
+
head?: never;
|
|
142
|
+
/**
|
|
143
|
+
* @description ``PATCH <ws>/invitations/<id>/name`` — fix a pending invite's name hint.
|
|
144
|
+
*
|
|
145
|
+
* The same correction as :class:`MemberNameView`, one step earlier: the
|
|
146
|
+
* invitee has not accepted, so there is no profile of theirs to write and
|
|
147
|
+
* the name lives on the invitation as ``display_name_hint`` — the invite
|
|
148
|
+
* modal's "Name" field, which ``accept_invitation`` copies onto the
|
|
149
|
+
* membership at acceptance. Editing it after the fact is why this endpoint
|
|
150
|
+
* exists: before #109's invitation surface the only fix for a typo in an
|
|
151
|
+
* invitee's name was to revoke and re-invite, which re-mails the person.
|
|
152
|
+
*
|
|
153
|
+
* Only a **pending** invitation is editable, with the same keyed refusals
|
|
154
|
+
* revoke gives for each terminal state (``invitation_revoked`` /
|
|
155
|
+
* ``already_used`` / ``declined`` / ``expired``): an accepted invitation's
|
|
156
|
+
* name is the member's name now — use the member endpoint — and a dead
|
|
157
|
+
* invitation is not a thing to relabel. Unknown and cross-workspace ids
|
|
158
|
+
* collapse into one identical 404 (inherited resolution).
|
|
159
|
+
*
|
|
160
|
+
* The value is still held to stapel-profiles' name canon even though the
|
|
161
|
+
* column being written is local: this hint becomes a displayed name, and
|
|
162
|
+
* the two endpoints must not disagree about what a name may contain.
|
|
163
|
+
*
|
|
164
|
+
* **Permissions:** `IsAuthenticated`
|
|
165
|
+
*/
|
|
166
|
+
patch: operations["workspaces_api_v1_invitations_name_partial_update"];
|
|
167
|
+
trace?: never;
|
|
168
|
+
};
|
|
129
169
|
"/workspaces/api/v1/{workspace_id}/invitations/{invitation_id}/resend": {
|
|
130
170
|
parameters: {
|
|
131
171
|
query?: never;
|
|
@@ -261,6 +301,50 @@ export interface paths {
|
|
|
261
301
|
patch: operations["workspaces_api_v1_members_partial_update"];
|
|
262
302
|
trace?: never;
|
|
263
303
|
};
|
|
304
|
+
"/workspaces/api/v1/{workspace_id}/members/{user_id}/name": {
|
|
305
|
+
parameters: {
|
|
306
|
+
query?: never;
|
|
307
|
+
header?: never;
|
|
308
|
+
path?: never;
|
|
309
|
+
cookie?: never;
|
|
310
|
+
};
|
|
311
|
+
get?: never;
|
|
312
|
+
put?: never;
|
|
313
|
+
post?: never;
|
|
314
|
+
delete?: never;
|
|
315
|
+
options?: never;
|
|
316
|
+
head?: never;
|
|
317
|
+
/**
|
|
318
|
+
* @description ``PATCH <ws>/members/<user_id>/name`` — correct a member's display name.
|
|
319
|
+
*
|
|
320
|
+
* Writes the CANONICAL name: stapel-profiles' ``Profile.display_name``,
|
|
321
|
+
* reached through this module's existing in-process profiles seam
|
|
322
|
+
* (``services.set_profile_display_name``), which also publishes
|
|
323
|
+
* ``profile.changed`` so every consumer of that name follows.
|
|
324
|
+
*
|
|
325
|
+
* NOT ``WorkspaceMember.display_name_hint``: the hint is a pre-profile
|
|
326
|
+
* placeholder, copied once at creation and dark from the moment a real
|
|
327
|
+
* profile exists (see its docstring in ``models.py``). Writing it would
|
|
328
|
+
* produce a correction the roster shows and nothing else in the product
|
|
329
|
+
* ever does — including, eventually, the roster.
|
|
330
|
+
*
|
|
331
|
+
* Where stapel-profiles does not run in this process there is nothing to
|
|
332
|
+
* write and no remote operation to call for it (that module publishes no
|
|
333
|
+
* write-somebody-else's-name endpoint and no comm Function), so the answer
|
|
334
|
+
* is an honest ``error.503.profiles_unavailable`` — never a 200 over a
|
|
335
|
+
* write that did not happen.
|
|
336
|
+
*
|
|
337
|
+
* Only an owner may rename an owner — the same hardcoded owner protection
|
|
338
|
+
* that role changes, removals and password resets carry. Renaming is not
|
|
339
|
+
* escalation, but an admin relabelling the owner of the organization on
|
|
340
|
+
* every screen in the product is close enough to the same act to answer
|
|
341
|
+
* the same way.
|
|
342
|
+
*
|
|
343
|
+
* **Permissions:** `IsAuthenticated`
|
|
344
|
+
*/
|
|
345
|
+
patch: operations["workspaces_api_v1_members_name_partial_update"];
|
|
346
|
+
trace?: never;
|
|
347
|
+
};
|
|
264
348
|
"/workspaces/api/v1/{workspace_id}/members/{user_id}/password/reset": {
|
|
265
349
|
parameters: {
|
|
266
350
|
query?: never;
|
|
@@ -577,6 +661,50 @@ export interface paths {
|
|
|
577
661
|
patch?: never;
|
|
578
662
|
trace?: never;
|
|
579
663
|
};
|
|
664
|
+
"/workspaces/api/v1/me/preferred-workspace": {
|
|
665
|
+
parameters: {
|
|
666
|
+
query?: never;
|
|
667
|
+
header?: never;
|
|
668
|
+
path?: never;
|
|
669
|
+
cookie?: never;
|
|
670
|
+
};
|
|
671
|
+
get?: never;
|
|
672
|
+
/**
|
|
673
|
+
* @description ``PUT/DELETE me/preferred-workspace`` — the person states where home is.
|
|
674
|
+
*
|
|
675
|
+
* ``STAPEL_WORKSPACES["DEFAULT_WORKSPACE_ID"]`` already describes itself as
|
|
676
|
+
* "a DEFAULT, not a cage: a person still switches spaces, and their
|
|
677
|
+
* explicit choice wins over it" — and until this endpoint there was nowhere
|
|
678
|
+
* for that choice to be written down. Clients filled the hole by guessing,
|
|
679
|
+
* and the guess (``workspaces[0]`` off a recency-ordered list) is #239.
|
|
680
|
+
*
|
|
681
|
+
* The choice is stated, never inferred. ``last_accessed_at`` remains what
|
|
682
|
+
* it has always been: telemetry written as a side effect of a GET, used to
|
|
683
|
+
* sort the list and for nothing else.
|
|
684
|
+
*
|
|
685
|
+
* Deliberately user-scoped rather than workspace-scoped
|
|
686
|
+
* (``.../<workspace_id>/prefer``): there is exactly one answer per person,
|
|
687
|
+
* and a route shaped per workspace would invite a second one.
|
|
688
|
+
*
|
|
689
|
+
* **Permissions:** `IsAuthenticated`
|
|
690
|
+
*/
|
|
691
|
+
put: operations["workspaces_api_v1_me_preferred_workspace_update"];
|
|
692
|
+
post?: never;
|
|
693
|
+
/**
|
|
694
|
+
* @description Clear the choice — back to the instance default / the client's chain.
|
|
695
|
+
*
|
|
696
|
+
* Answers 200 with an empty id rather than 204: the client's whole job
|
|
697
|
+
* here is to re-resolve, and handing it the new state saves it from
|
|
698
|
+
* guessing what "no content" left behind. Idempotent.
|
|
699
|
+
*
|
|
700
|
+
* **Permissions:** `IsAuthenticated`
|
|
701
|
+
*/
|
|
702
|
+
delete: operations["workspaces_api_v1_me_preferred_workspace_destroy"];
|
|
703
|
+
options?: never;
|
|
704
|
+
head?: never;
|
|
705
|
+
patch?: never;
|
|
706
|
+
trace?: never;
|
|
707
|
+
};
|
|
580
708
|
"/workspaces/api/v1/roles": {
|
|
581
709
|
parameters: {
|
|
582
710
|
query?: never;
|
|
@@ -606,34 +734,42 @@ export interface paths {
|
|
|
606
734
|
export type webhooks = Record<string, never>;
|
|
607
735
|
export interface components {
|
|
608
736
|
schemas: {
|
|
737
|
+
/** @description What the name is after the write — the stored value, not the request. */
|
|
738
|
+
DisplayNameResponse: {
|
|
739
|
+
/**
|
|
740
|
+
* @description The stored name, trimmed and canon-checked. Empty string when the name was cleared
|
|
741
|
+
* @example Ada Lovelace
|
|
742
|
+
*/
|
|
743
|
+
display_name: string;
|
|
744
|
+
};
|
|
609
745
|
/**
|
|
610
|
-
* @description
|
|
611
|
-
*
|
|
746
|
+
* @description How THIS instance is deployed — what the client needs to know before
|
|
747
|
+
* deciding what to show a person with no workspace.
|
|
612
748
|
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
749
|
+
* The ``landing`` axis (``STREET_LANDING_MODE``) has existed since
|
|
750
|
+
* 2026-08-03 but lived only in backend config, with nothing exposing it —
|
|
751
|
+
* a client had no way to tell a closed deployment from a public cloud one.
|
|
616
752
|
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
753
|
+
* This matters because the screen shown to someone who is no longer (or
|
|
754
|
+
* never was) a workspace member differs by deployment:
|
|
619
755
|
*
|
|
620
|
-
* * ``none`` —
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
* * ``personal`` —
|
|
624
|
-
*
|
|
756
|
+
* * ``none`` — closed deployment: the person has no workspace and none is
|
|
757
|
+
* obtainable. An honest screen says "you're not a member, ask for an
|
|
758
|
+
* invite" and offers no path forward;
|
|
759
|
+
* * ``personal`` — public cloud: they have their own workspace, and the
|
|
760
|
+
* client can route them there.
|
|
625
761
|
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
762
|
+
* Offering "create a workspace" to someone the instance will never let
|
|
763
|
+
* create one is a dead end drawn as a button.
|
|
628
764
|
*/
|
|
629
765
|
InstanceShapeResponse: {
|
|
630
766
|
/**
|
|
631
|
-
* @description
|
|
767
|
+
* @description What a "street" (no-invite) arrival lands on
|
|
632
768
|
* @example personal
|
|
633
769
|
*/
|
|
634
770
|
landing: string;
|
|
635
771
|
/**
|
|
636
|
-
* @description
|
|
772
|
+
* @description Whether self-registration is available
|
|
637
773
|
* @example True
|
|
638
774
|
*/
|
|
639
775
|
registration_open: boolean;
|
|
@@ -754,7 +890,7 @@ export interface components {
|
|
|
754
890
|
/** @description Invite payload. */
|
|
755
891
|
MemberInviteRequest: {
|
|
756
892
|
/**
|
|
757
|
-
* @description Name hint for the invitee (this invite's "
|
|
893
|
+
* @description Name hint for the invitee (this invite's "Name" field), applied to every email in this request. Optional — an invite without one behaves exactly as before. NOT written into stapel-profiles directly (that store is the invitee's own): held on the invitation, copied onto the member at accept time, and shown only until stapel-profiles has a real name for the person
|
|
758
894
|
* @example Ada Lovelace
|
|
759
895
|
*/
|
|
760
896
|
display_name?: string | null;
|
|
@@ -888,6 +1024,14 @@ export interface components {
|
|
|
888
1024
|
/** @description Number of items in current page */
|
|
889
1025
|
count: number;
|
|
890
1026
|
};
|
|
1027
|
+
/** @description Roster-side name correction (one field, both name-edit endpoints). */
|
|
1028
|
+
PatchedDisplayNameUpdateRequest: {
|
|
1029
|
+
/**
|
|
1030
|
+
* @description The name to show for this person. Held to stapel-profiles' display-name canon (validate_display_name) — this module never adds a second, differently-strict rule. Blank, whitespace-only, null and a missing key all mean the same thing: clear the name
|
|
1031
|
+
* @example Ada Lovelace
|
|
1032
|
+
*/
|
|
1033
|
+
display_name?: string | null;
|
|
1034
|
+
};
|
|
891
1035
|
/** @description MemberUpdateRequest(role: str) */
|
|
892
1036
|
PatchedMemberUpdateRequest: {
|
|
893
1037
|
role?: string;
|
|
@@ -900,6 +1044,23 @@ export interface components {
|
|
|
900
1044
|
[key: string]: unknown;
|
|
901
1045
|
} | null;
|
|
902
1046
|
};
|
|
1047
|
+
/** @description Which workspace the person is choosing as home. */
|
|
1048
|
+
PreferredWorkspaceRequest: {
|
|
1049
|
+
/**
|
|
1050
|
+
* Format: uuid
|
|
1051
|
+
* @description UUID of a workspace the caller actively belongs to
|
|
1052
|
+
* @example a8bba7ae-5d2e-43e5-9911-0553e7df50b3
|
|
1053
|
+
*/
|
|
1054
|
+
workspace_id: string;
|
|
1055
|
+
};
|
|
1056
|
+
/** @description The caller's stated home workspace after the write. */
|
|
1057
|
+
PreferredWorkspaceResponse: {
|
|
1058
|
+
/**
|
|
1059
|
+
* @description The workspace now recorded as the caller's choice, or "" after a DELETE cleared it
|
|
1060
|
+
* @example a8bba7ae-5d2e-43e5-9911-0553e7df50b3
|
|
1061
|
+
*/
|
|
1062
|
+
preferred_workspace_id?: string;
|
|
1063
|
+
};
|
|
903
1064
|
/** @description Provision an org-created (synthetic) member (org-program spec §C1). */
|
|
904
1065
|
ProvisionMemberRequest: {
|
|
905
1066
|
/**
|
|
@@ -1032,6 +1193,11 @@ export interface components {
|
|
|
1032
1193
|
* @example a8bba7ae-5d2e-43e5-9911-0553e7df50b3
|
|
1033
1194
|
*/
|
|
1034
1195
|
default_workspace_id?: string;
|
|
1196
|
+
/**
|
|
1197
|
+
* @description The person's OWN stated choice of home workspace (PUT me/preferred-workspace), echoed back ONLY while the membership carrying it is active, else "". This is the "explicit choice" default_workspace_id documents itself as yielding to, so a client resolves preferred first and the instance default only after it. Never inferred from behaviour — where somebody last clicked is last_accessed_at, which is telemetry, and reading it as a choice is what produced #239
|
|
1198
|
+
* @example a8bba7ae-5d2e-43e5-9911-0553e7df50b3
|
|
1199
|
+
*/
|
|
1200
|
+
preferred_workspace_id?: string;
|
|
1035
1201
|
};
|
|
1036
1202
|
/** @description Workspace details. */
|
|
1037
1203
|
WorkspaceResponse: {
|
|
@@ -1249,6 +1415,34 @@ export interface operations {
|
|
|
1249
1415
|
};
|
|
1250
1416
|
};
|
|
1251
1417
|
};
|
|
1418
|
+
workspaces_api_v1_invitations_name_partial_update: {
|
|
1419
|
+
parameters: {
|
|
1420
|
+
query?: never;
|
|
1421
|
+
header?: never;
|
|
1422
|
+
path: {
|
|
1423
|
+
invitation_id: string;
|
|
1424
|
+
workspace_id: string;
|
|
1425
|
+
};
|
|
1426
|
+
cookie?: never;
|
|
1427
|
+
};
|
|
1428
|
+
requestBody?: {
|
|
1429
|
+
content: {
|
|
1430
|
+
"application/json": components["schemas"]["PatchedDisplayNameUpdateRequest"];
|
|
1431
|
+
"application/x-www-form-urlencoded": components["schemas"]["PatchedDisplayNameUpdateRequest"];
|
|
1432
|
+
"multipart/form-data": components["schemas"]["PatchedDisplayNameUpdateRequest"];
|
|
1433
|
+
};
|
|
1434
|
+
};
|
|
1435
|
+
responses: {
|
|
1436
|
+
200: {
|
|
1437
|
+
headers: {
|
|
1438
|
+
[name: string]: unknown;
|
|
1439
|
+
};
|
|
1440
|
+
content: {
|
|
1441
|
+
"application/json": components["schemas"]["DisplayNameResponse"];
|
|
1442
|
+
};
|
|
1443
|
+
};
|
|
1444
|
+
};
|
|
1445
|
+
};
|
|
1252
1446
|
workspaces_api_v1_invitations_resend_create: {
|
|
1253
1447
|
parameters: {
|
|
1254
1448
|
query?: never;
|
|
@@ -1372,6 +1566,34 @@ export interface operations {
|
|
|
1372
1566
|
};
|
|
1373
1567
|
};
|
|
1374
1568
|
};
|
|
1569
|
+
workspaces_api_v1_members_name_partial_update: {
|
|
1570
|
+
parameters: {
|
|
1571
|
+
query?: never;
|
|
1572
|
+
header?: never;
|
|
1573
|
+
path: {
|
|
1574
|
+
user_id: string;
|
|
1575
|
+
workspace_id: string;
|
|
1576
|
+
};
|
|
1577
|
+
cookie?: never;
|
|
1578
|
+
};
|
|
1579
|
+
requestBody?: {
|
|
1580
|
+
content: {
|
|
1581
|
+
"application/json": components["schemas"]["PatchedDisplayNameUpdateRequest"];
|
|
1582
|
+
"application/x-www-form-urlencoded": components["schemas"]["PatchedDisplayNameUpdateRequest"];
|
|
1583
|
+
"multipart/form-data": components["schemas"]["PatchedDisplayNameUpdateRequest"];
|
|
1584
|
+
};
|
|
1585
|
+
};
|
|
1586
|
+
responses: {
|
|
1587
|
+
200: {
|
|
1588
|
+
headers: {
|
|
1589
|
+
[name: string]: unknown;
|
|
1590
|
+
};
|
|
1591
|
+
content: {
|
|
1592
|
+
"application/json": components["schemas"]["DisplayNameResponse"];
|
|
1593
|
+
};
|
|
1594
|
+
};
|
|
1595
|
+
};
|
|
1596
|
+
};
|
|
1375
1597
|
workspaces_api_v1_members_password_reset_create: {
|
|
1376
1598
|
parameters: {
|
|
1377
1599
|
query?: never;
|
|
@@ -1611,6 +1833,50 @@ export interface operations {
|
|
|
1611
1833
|
};
|
|
1612
1834
|
};
|
|
1613
1835
|
};
|
|
1836
|
+
workspaces_api_v1_me_preferred_workspace_update: {
|
|
1837
|
+
parameters: {
|
|
1838
|
+
query?: never;
|
|
1839
|
+
header?: never;
|
|
1840
|
+
path?: never;
|
|
1841
|
+
cookie?: never;
|
|
1842
|
+
};
|
|
1843
|
+
requestBody: {
|
|
1844
|
+
content: {
|
|
1845
|
+
"application/json": components["schemas"]["PreferredWorkspaceRequest"];
|
|
1846
|
+
"application/x-www-form-urlencoded": components["schemas"]["PreferredWorkspaceRequest"];
|
|
1847
|
+
"multipart/form-data": components["schemas"]["PreferredWorkspaceRequest"];
|
|
1848
|
+
};
|
|
1849
|
+
};
|
|
1850
|
+
responses: {
|
|
1851
|
+
200: {
|
|
1852
|
+
headers: {
|
|
1853
|
+
[name: string]: unknown;
|
|
1854
|
+
};
|
|
1855
|
+
content: {
|
|
1856
|
+
"application/json": components["schemas"]["PreferredWorkspaceResponse"];
|
|
1857
|
+
};
|
|
1858
|
+
};
|
|
1859
|
+
};
|
|
1860
|
+
};
|
|
1861
|
+
workspaces_api_v1_me_preferred_workspace_destroy: {
|
|
1862
|
+
parameters: {
|
|
1863
|
+
query?: never;
|
|
1864
|
+
header?: never;
|
|
1865
|
+
path?: never;
|
|
1866
|
+
cookie?: never;
|
|
1867
|
+
};
|
|
1868
|
+
requestBody?: never;
|
|
1869
|
+
responses: {
|
|
1870
|
+
200: {
|
|
1871
|
+
headers: {
|
|
1872
|
+
[name: string]: unknown;
|
|
1873
|
+
};
|
|
1874
|
+
content: {
|
|
1875
|
+
"application/json": components["schemas"]["PreferredWorkspaceResponse"];
|
|
1876
|
+
};
|
|
1877
|
+
};
|
|
1878
|
+
};
|
|
1879
|
+
};
|
|
1614
1880
|
workspaces_api_v1_roles_retrieve: {
|
|
1615
1881
|
parameters: {
|
|
1616
1882
|
query?: never;
|
package/src/api/types.ts
CHANGED
|
@@ -24,6 +24,15 @@ export type Operations = operations;
|
|
|
24
24
|
export type Workspace = Schemas["WorkspaceResponse"];
|
|
25
25
|
/** GET / 200 body — the caller's workspaces (memberships). */
|
|
26
26
|
export type WorkspaceList = Schemas["WorkspaceListResponse"];
|
|
27
|
+
/** PUT /me/preferred-workspace request body — the workspace the person is
|
|
28
|
+
* choosing as home. Must be one they actively belong to; anything else
|
|
29
|
+
* (unknown, not a member, invitation still pending, membership suspended)
|
|
30
|
+
* answers ONE identical `error.404.workspace_not_found`, so the endpoint
|
|
31
|
+
* cannot be used to probe which workspace ids are real. */
|
|
32
|
+
export type PreferredWorkspace = Schemas["PreferredWorkspaceRequest"];
|
|
33
|
+
/** PUT/DELETE /me/preferred-workspace 200 body — the choice after the write
|
|
34
|
+
* (`""` after a DELETE cleared it). */
|
|
35
|
+
export type PreferredWorkspaceResult = Schemas["PreferredWorkspaceResponse"];
|
|
27
36
|
/** POST / request body — create a workspace (slug auto-generated when omitted). */
|
|
28
37
|
export type WorkspaceCreate = Schemas["WorkspaceCreateRequest"];
|
|
29
38
|
/** PATCH /{id} request body — a partial name / slug / settings update. */
|
|
@@ -53,6 +62,20 @@ export type MemberPasswordReset = Schemas["MemberPasswordResetRequest"];
|
|
|
53
62
|
* — see {@link "../model/mutations.js".useResetMemberPassword}, which keeps it
|
|
54
63
|
* out of the query cache. */
|
|
55
64
|
export type MemberPasswordResetResult = Schemas["MemberPasswordResetResponse"];
|
|
65
|
+
/**
|
|
66
|
+
* PATCH /{id}/members/{userId}/name and PATCH
|
|
67
|
+
* /{id}/invitations/{invitationId}/name request body — the ONE field both
|
|
68
|
+
* name-edit endpoints take (stapel-workspaces ≥0.19.0). Blank,
|
|
69
|
+
* whitespace-only, `null` and a missing key all mean the same thing to the
|
|
70
|
+
* backend: clear the name. The value is held to stapel-profiles'
|
|
71
|
+
* `validate_display_name` canon (35-char ceiling as the serializer's
|
|
72
|
+
* `max_length`) — this module declares no second, differently-strict rule.
|
|
73
|
+
*/
|
|
74
|
+
export type DisplayNameUpdate = Schemas["PatchedDisplayNameUpdateRequest"];
|
|
75
|
+
/** PATCH …/name 200 body — what the name IS after the write (the stored,
|
|
76
|
+
* trimmed, canon-checked value; empty string when the name was cleared), not
|
|
77
|
+
* an echo of the request. */
|
|
78
|
+
export type DisplayNameResult = Schemas["DisplayNameResponse"];
|
|
56
79
|
/** POST /invitations/accept request body — the token from the email link. */
|
|
57
80
|
export type InvitationAccept = Schemas["InvitationAcceptRequest"];
|
|
58
81
|
/** GET /invitations/{token} 200 body — the public (AllowAny) preview the
|
package/src/api/workspacesApi.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { StapelClient, StapelRequestOptions } from "@stapel/core";
|
|
2
2
|
import type {
|
|
3
|
+
DisplayNameResult,
|
|
4
|
+
DisplayNameUpdate,
|
|
3
5
|
Invitation,
|
|
4
6
|
InvitationPage,
|
|
5
7
|
InvitationsParams,
|
|
@@ -11,6 +13,8 @@ import type {
|
|
|
11
13
|
MemberPasswordResetResult,
|
|
12
14
|
MemberRoleUpdate,
|
|
13
15
|
MembersParams,
|
|
16
|
+
PreferredWorkspace,
|
|
17
|
+
PreferredWorkspaceResult,
|
|
14
18
|
InvitationAccept,
|
|
15
19
|
InvitationClaim,
|
|
16
20
|
InvitationPreview,
|
|
@@ -74,6 +78,15 @@ export interface WorkspacesApi {
|
|
|
74
78
|
/** Soft-delete a workspace (owner only). */
|
|
75
79
|
deleteWorkspace(workspaceId: string): Promise<void>;
|
|
76
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Record the caller's home workspace — their EXPLICIT choice, which the
|
|
83
|
+
* instance's `default_workspace_id` documents itself as yielding to.
|
|
84
|
+
* Answers 404 for anything they cannot actively open.
|
|
85
|
+
*/
|
|
86
|
+
setPreferredWorkspace(body: PreferredWorkspace): Promise<PreferredWorkspaceResult>;
|
|
87
|
+
/** Clear the caller's home workspace — back to the resolution chain. Idempotent. */
|
|
88
|
+
clearPreferredWorkspace(): Promise<PreferredWorkspaceResult>;
|
|
89
|
+
|
|
77
90
|
/** A workspace's members (viewer+), an anchor-paginated page. */
|
|
78
91
|
listMembers(
|
|
79
92
|
workspaceId: string,
|
|
@@ -93,6 +106,24 @@ export interface WorkspacesApi {
|
|
|
93
106
|
/** Remove a member from a workspace (admin+; the last owner is protected). */
|
|
94
107
|
removeMember(workspaceId: string, userId: string): Promise<void>;
|
|
95
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Correct a member's display name (stapel-workspaces ≥0.19.0, capability
|
|
111
|
+
* `members.role.change`). Writes the CANONICAL name — stapel-profiles'
|
|
112
|
+
* `Profile.display_name` through this module's in-process profiles seam,
|
|
113
|
+
* which also publishes `profile.changed` — NOT the membership's
|
|
114
|
+
* `display_name_hint`, which is a pre-profile placeholder that goes dark the
|
|
115
|
+
* moment a real profile exists.
|
|
116
|
+
*
|
|
117
|
+
* Only an owner may rename an owner. Where stapel-profiles does not run in
|
|
118
|
+
* the deployment's process the answer is `error.503.profiles_unavailable`,
|
|
119
|
+
* never a 200 over a write that did not happen.
|
|
120
|
+
*/
|
|
121
|
+
renameMember(
|
|
122
|
+
workspaceId: string,
|
|
123
|
+
userId: string,
|
|
124
|
+
body: DisplayNameUpdate
|
|
125
|
+
): Promise<DisplayNameResult>;
|
|
126
|
+
|
|
96
127
|
/**
|
|
97
128
|
* Reset a member's password on the organization's order (stapel-workspaces
|
|
98
129
|
* ≥0.14.0, #110). Capability `members.password.reset`, declared **high** —
|
|
@@ -141,6 +172,25 @@ export interface WorkspacesApi {
|
|
|
141
172
|
workspaceId: string,
|
|
142
173
|
invitationId: string
|
|
143
174
|
): Promise<Invitation>;
|
|
175
|
+
/**
|
|
176
|
+
* Fix a still-PENDING invitation's name hint (stapel-workspaces ≥0.19.0,
|
|
177
|
+
* capability `members.role.change` — the same one as {@link renameMember},
|
|
178
|
+
* because this hint IS the member's name after acceptance and splitting the
|
|
179
|
+
* two would let a role fix a name that reverts).
|
|
180
|
+
*
|
|
181
|
+
* The same correction as {@link renameMember}, one step earlier: the invitee
|
|
182
|
+
* has not accepted, so there is no profile of theirs to write and the name
|
|
183
|
+
* lives on the invitation. Before it, the only fix for a typo in an
|
|
184
|
+
* invitee's name was revoke-and-re-invite, which re-mails the person. A
|
|
185
|
+
* terminal invitation refuses with the keyed answers revoke gives
|
|
186
|
+
* (`error.400.invitation_already_used` / `_revoked` / `_declined` /
|
|
187
|
+
* `_expired`).
|
|
188
|
+
*/
|
|
189
|
+
renameInvitation(
|
|
190
|
+
workspaceId: string,
|
|
191
|
+
invitationId: string,
|
|
192
|
+
body: DisplayNameUpdate
|
|
193
|
+
): Promise<DisplayNameResult>;
|
|
144
194
|
|
|
145
195
|
/** Accept an invitation by its token — returns the caller's new membership. */
|
|
146
196
|
acceptInvitation(body: InvitationAccept): Promise<Member>;
|
|
@@ -199,6 +249,16 @@ export function createWorkspacesApi(client: StapelClient): WorkspacesApi {
|
|
|
199
249
|
deleteWorkspace: (workspaceId) =>
|
|
200
250
|
client.delete(`/${encodeURIComponent(workspaceId)}`, mutating()),
|
|
201
251
|
|
|
252
|
+
setPreferredWorkspace: (body) =>
|
|
253
|
+
client.put(
|
|
254
|
+
"/me/preferred-workspace",
|
|
255
|
+
body satisfies PreferredWorkspace,
|
|
256
|
+
mutating()
|
|
257
|
+
),
|
|
258
|
+
|
|
259
|
+
clearPreferredWorkspace: () =>
|
|
260
|
+
client.delete("/me/preferred-workspace", mutating()),
|
|
261
|
+
|
|
202
262
|
listMembers: (workspaceId, params) => {
|
|
203
263
|
const query: Record<string, string | number> = {};
|
|
204
264
|
if (params?.anchor !== undefined) query.anchor = params.anchor;
|
|
@@ -230,6 +290,20 @@ export function createWorkspacesApi(client: StapelClient): WorkspacesApi {
|
|
|
230
290
|
mutating()
|
|
231
291
|
),
|
|
232
292
|
|
|
293
|
+
renameMember: (workspaceId, userId, body) =>
|
|
294
|
+
client.patch(
|
|
295
|
+
`/${encodeURIComponent(workspaceId)}/members/${encodeURIComponent(userId)}/name`,
|
|
296
|
+
body satisfies DisplayNameUpdate,
|
|
297
|
+
mutating()
|
|
298
|
+
),
|
|
299
|
+
|
|
300
|
+
renameInvitation: (workspaceId, invitationId, body) =>
|
|
301
|
+
client.patch(
|
|
302
|
+
`/${encodeURIComponent(workspaceId)}/invitations/${encodeURIComponent(invitationId)}/name`,
|
|
303
|
+
body satisfies DisplayNameUpdate,
|
|
304
|
+
mutating()
|
|
305
|
+
),
|
|
306
|
+
|
|
233
307
|
resetMemberPassword: (workspaceId, userId, body) =>
|
|
234
308
|
client.post(
|
|
235
309
|
`/${encodeURIComponent(workspaceId)}/members/${encodeURIComponent(userId)}/password/reset`,
|
|
@@ -39,6 +39,10 @@ export const WORKSPACES_ERRORS = {
|
|
|
39
39
|
"error.400.bad_request": { status: 400, params: [], remediation: "fix_input", en: "Bad request" },
|
|
40
40
|
"error.400.captcha_invalid": { status: 400, params: [], remediation: "retry", en: "Captcha verification failed. Please try again." },
|
|
41
41
|
"error.400.captcha_required": { status: 400, params: [], remediation: "retry", en: "Captcha token is required." },
|
|
42
|
+
"error.400.display_name_emoji": { status: 400, params: [], remediation: "fix_input", en: "Display name cannot contain emoji" },
|
|
43
|
+
"error.400.display_name_forbidden_chars": { status: 400, params: [], remediation: "fix_input", en: "Display name contains forbidden characters" },
|
|
44
|
+
"error.400.display_name_invisible_chars": { status: 400, params: [], remediation: "fix_input", en: "Display name contains invisible characters" },
|
|
45
|
+
"error.400.display_name_too_short": { status: 400, params: [], remediation: "fix_input", en: "Display name must be at least 2 characters" },
|
|
42
46
|
"error.400.expected_list": { status: 400, params: [], remediation: "fix_input", en: "Expected a list of items" },
|
|
43
47
|
"error.400.field.blank": { status: 400, params: ["field"], remediation: "fix_input", en: "{field} may not be blank" },
|
|
44
48
|
"error.400.field.does_not_exist": { status: 400, params: ["field"], remediation: "fix_input", en: "{field} does not exist" },
|
|
@@ -96,6 +100,7 @@ export const WORKSPACES_ERRORS = {
|
|
|
96
100
|
"error.429.too_many_requests": { status: 429, params: [], remediation: "wait_and_retry", en: "Too many requests. Please try again later." },
|
|
97
101
|
"error.500.internal": { status: 500, params: [], remediation: "contact_support", en: "Something went wrong" },
|
|
98
102
|
"error.503.auth_unavailable": { status: 503, params: [], remediation: "wait_and_retry", en: "The authentication service is unavailable; try again later" },
|
|
103
|
+
"error.503.profiles_unavailable": { status: 503, params: [], remediation: "wait_and_retry", en: "The profiles service is unavailable; try again later" },
|
|
99
104
|
} as const;
|
|
100
105
|
|
|
101
106
|
export type WorkspacesErrorCode = keyof typeof WORKSPACES_ERRORS;
|
|
@@ -106,6 +111,10 @@ export const WORKSPACES_ERROR_CODES: readonly WorkspacesErrorCode[] = [
|
|
|
106
111
|
"error.400.bad_request",
|
|
107
112
|
"error.400.captcha_invalid",
|
|
108
113
|
"error.400.captcha_required",
|
|
114
|
+
"error.400.display_name_emoji",
|
|
115
|
+
"error.400.display_name_forbidden_chars",
|
|
116
|
+
"error.400.display_name_invisible_chars",
|
|
117
|
+
"error.400.display_name_too_short",
|
|
109
118
|
"error.400.expected_list",
|
|
110
119
|
"error.400.field.blank",
|
|
111
120
|
"error.400.field.does_not_exist",
|
|
@@ -163,6 +172,7 @@ export const WORKSPACES_ERROR_CODES: readonly WorkspacesErrorCode[] = [
|
|
|
163
172
|
"error.429.too_many_requests",
|
|
164
173
|
"error.500.internal",
|
|
165
174
|
"error.503.auth_unavailable",
|
|
175
|
+
"error.503.profiles_unavailable",
|
|
166
176
|
];
|
|
167
177
|
|
|
168
178
|
/**
|
|
@@ -175,6 +185,10 @@ export const workspacesErrorBundleEn: Record<WorkspacesErrorCode, string> = {
|
|
|
175
185
|
"error.400.bad_request": "Bad request",
|
|
176
186
|
"error.400.captcha_invalid": "Captcha verification failed. Please try again.",
|
|
177
187
|
"error.400.captcha_required": "Captcha token is required.",
|
|
188
|
+
"error.400.display_name_emoji": "Display name cannot contain emoji",
|
|
189
|
+
"error.400.display_name_forbidden_chars": "Display name contains forbidden characters",
|
|
190
|
+
"error.400.display_name_invisible_chars": "Display name contains invisible characters",
|
|
191
|
+
"error.400.display_name_too_short": "Display name must be at least 2 characters",
|
|
178
192
|
"error.400.expected_list": "Expected a list of items",
|
|
179
193
|
"error.400.field.blank": "{field} may not be blank",
|
|
180
194
|
"error.400.field.does_not_exist": "{field} does not exist",
|
|
@@ -232,4 +246,5 @@ export const workspacesErrorBundleEn: Record<WorkspacesErrorCode, string> = {
|
|
|
232
246
|
"error.429.too_many_requests": "Too many requests. Please try again later.",
|
|
233
247
|
"error.500.internal": "Something went wrong",
|
|
234
248
|
"error.503.auth_unavailable": "The authentication service is unavailable; try again later",
|
|
249
|
+
"error.503.profiles_unavailable": "The profiles service is unavailable; try again later",
|
|
235
250
|
};
|
|
@@ -27,6 +27,34 @@
|
|
|
27
27
|
"remediation": "retry",
|
|
28
28
|
"en": "Captcha token is required."
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
"code": "error.400.display_name_emoji",
|
|
32
|
+
"status": 400,
|
|
33
|
+
"params": [],
|
|
34
|
+
"remediation": "fix_input",
|
|
35
|
+
"en": "Display name cannot contain emoji"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"code": "error.400.display_name_forbidden_chars",
|
|
39
|
+
"status": 400,
|
|
40
|
+
"params": [],
|
|
41
|
+
"remediation": "fix_input",
|
|
42
|
+
"en": "Display name contains forbidden characters"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"code": "error.400.display_name_invisible_chars",
|
|
46
|
+
"status": 400,
|
|
47
|
+
"params": [],
|
|
48
|
+
"remediation": "fix_input",
|
|
49
|
+
"en": "Display name contains invisible characters"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"code": "error.400.display_name_too_short",
|
|
53
|
+
"status": 400,
|
|
54
|
+
"params": [],
|
|
55
|
+
"remediation": "fix_input",
|
|
56
|
+
"en": "Display name must be at least 2 characters"
|
|
57
|
+
},
|
|
30
58
|
{
|
|
31
59
|
"code": "error.400.expected_list",
|
|
32
60
|
"status": 400,
|
|
@@ -461,5 +489,12 @@
|
|
|
461
489
|
"params": [],
|
|
462
490
|
"remediation": "wait_and_retry",
|
|
463
491
|
"en": "The authentication service is unavailable; try again later"
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
"code": "error.503.profiles_unavailable",
|
|
495
|
+
"status": 503,
|
|
496
|
+
"params": [],
|
|
497
|
+
"remediation": "wait_and_retry",
|
|
498
|
+
"en": "The profiles service is unavailable; try again later"
|
|
464
499
|
}
|
|
465
500
|
]
|
|
@@ -16,6 +16,10 @@ export const workspacesErrorBundleRu: Record<WorkspacesErrorCode, string> = {
|
|
|
16
16
|
"error.400.bad_request": "Некорректный запрос",
|
|
17
17
|
"error.400.captcha_invalid": "Проверка капчи не пройдена. Пожалуйста, попробуйте ещё раз.",
|
|
18
18
|
"error.400.captcha_required": "Требуется токен капчи.",
|
|
19
|
+
"error.400.display_name_emoji": "Отображаемое имя не может содержать эмодзи",
|
|
20
|
+
"error.400.display_name_forbidden_chars": "Отображаемое имя содержит запрещённые символы",
|
|
21
|
+
"error.400.display_name_invisible_chars": "Отображаемое имя содержит невидимые символы",
|
|
22
|
+
"error.400.display_name_too_short": "Отображаемое имя должно содержать не менее 2 символов",
|
|
19
23
|
"error.400.expected_list": "Ожидался список элементов",
|
|
20
24
|
"error.400.field.blank": "Поле «{field}» не может быть пустым",
|
|
21
25
|
"error.400.field.does_not_exist": "«{field}» не существует",
|
|
@@ -73,4 +77,5 @@ export const workspacesErrorBundleRu: Record<WorkspacesErrorCode, string> = {
|
|
|
73
77
|
"error.429.too_many_requests": "Слишком много запросов. Пожалуйста, повторите попытку позже.",
|
|
74
78
|
"error.500.internal": "Что-то пошло не так",
|
|
75
79
|
"error.503.auth_unavailable": "Сервис аутентификации недоступен; повторите попытку позже",
|
|
80
|
+
"error.503.profiles_unavailable": "Сервис профилей недоступен; повторите попытку позже",
|
|
76
81
|
};
|