@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
|
@@ -10,26 +10,26 @@ export interface paths {
|
|
|
10
10
|
cookie?: never;
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
|
-
* @description
|
|
13
|
+
* @description Turns an unaskable plan ceiling into 503 for the whole view.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* Mixed in rather than written as an ``except`` at each call site because
|
|
16
|
+
* the seam is consulted from views AND from inside ``services`` under the
|
|
17
|
+
* workspace lock, and the set of call sites grows: a view that gains a
|
|
18
|
+
* method later inherits the mapping instead of re-deriving it. The 402
|
|
19
|
+
* denials stay per-call-site — those carry a limit the screen renders.
|
|
20
20
|
*
|
|
21
21
|
* **Permissions:** `IsAuthenticated`
|
|
22
22
|
*/
|
|
23
23
|
get: operations["workspaces_api_v1_retrieve"];
|
|
24
24
|
put?: never;
|
|
25
25
|
/**
|
|
26
|
-
* @description
|
|
27
|
-
*
|
|
28
|
-
* Subclasses (or downstream projects) can swap the request/response
|
|
29
|
-
* serializers without copying method bodies:
|
|
26
|
+
* @description Turns an unaskable plan ceiling into 503 for the whole view.
|
|
30
27
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* Mixed in rather than written as an ``except`` at each call site because
|
|
29
|
+
* the seam is consulted from views AND from inside ``services`` under the
|
|
30
|
+
* workspace lock, and the set of call sites grows: a view that gains a
|
|
31
|
+
* method later inherits the mapping instead of re-deriving it. The 402
|
|
32
|
+
* denials stay per-call-site — those carry a limit the screen renders.
|
|
33
33
|
*
|
|
34
34
|
* **Permissions:** `IsAuthenticated`
|
|
35
35
|
*/
|
|
@@ -48,13 +48,21 @@ export interface paths {
|
|
|
48
48
|
cookie?: never;
|
|
49
49
|
};
|
|
50
50
|
/**
|
|
51
|
-
* @description Overridable serializer
|
|
51
|
+
* @description Overridable request/response serializer seam for an API view.
|
|
52
|
+
*
|
|
53
|
+
* Declare the classes as attributes, read them through the getters::
|
|
52
54
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
+
* class WalletView(SerializerSeamMixin, APIView):
|
|
56
|
+
* response_serializer_class = WalletResponseSerializer
|
|
55
57
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
+
* def get(self, request):
|
|
59
|
+
* response_cls = self.get_response_serializer_class()
|
|
60
|
+
* return StapelResponse(response_cls(wallet_to_dto(...)))
|
|
61
|
+
*
|
|
62
|
+
* A host project swaps either direction by subclassing and setting the
|
|
63
|
+
* attribute, or overrides the getter for a per-request decision — no HTTP
|
|
64
|
+
* method body is ever copied. ``None`` means the direction carries no
|
|
65
|
+
* serializer (raw ``request.FILES``, an empty 204 body).
|
|
58
66
|
*
|
|
59
67
|
* **Permissions:** `IsAuthenticated`
|
|
60
68
|
*/
|
|
@@ -62,13 +70,13 @@ export interface paths {
|
|
|
62
70
|
put?: never;
|
|
63
71
|
post?: never;
|
|
64
72
|
/**
|
|
65
|
-
* @description
|
|
66
|
-
*
|
|
67
|
-
* Subclasses (or downstream projects) can swap the request/response
|
|
68
|
-
* serializers without copying method bodies:
|
|
73
|
+
* @description End a workspace — the transition itself lives in the service.
|
|
69
74
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
75
|
+
* The view's whole job here is authority (owner, and only owner) and
|
|
76
|
+
* turning the service's typed refusal back into the keyed 409 the
|
|
77
|
+
* frontend routes on. The write, the event and the audit line are one
|
|
78
|
+
* indivisible act in ``services.delete_workspace``, because a second
|
|
79
|
+
* door into this transition is a second chance to forget the record.
|
|
72
80
|
*
|
|
73
81
|
* **Permissions:** `IsAuthenticated`
|
|
74
82
|
*/
|
|
@@ -76,13 +84,21 @@ export interface paths {
|
|
|
76
84
|
options?: never;
|
|
77
85
|
head?: never;
|
|
78
86
|
/**
|
|
79
|
-
* @description Overridable serializer
|
|
87
|
+
* @description Overridable request/response serializer seam for an API view.
|
|
80
88
|
*
|
|
81
|
-
*
|
|
82
|
-
* serializers without copying method bodies:
|
|
89
|
+
* Declare the classes as attributes, read them through the getters::
|
|
83
90
|
*
|
|
84
|
-
* class
|
|
85
|
-
* response_serializer_class =
|
|
91
|
+
* class WalletView(SerializerSeamMixin, APIView):
|
|
92
|
+
* response_serializer_class = WalletResponseSerializer
|
|
93
|
+
*
|
|
94
|
+
* def get(self, request):
|
|
95
|
+
* response_cls = self.get_response_serializer_class()
|
|
96
|
+
* return StapelResponse(response_cls(wallet_to_dto(...)))
|
|
97
|
+
*
|
|
98
|
+
* A host project swaps either direction by subclassing and setting the
|
|
99
|
+
* attribute, or overrides the getter for a per-request decision — no HTTP
|
|
100
|
+
* method body is ever copied. ``None`` means the direction carries no
|
|
101
|
+
* serializer (raw ``request.FILES``, an empty 204 body).
|
|
86
102
|
*
|
|
87
103
|
* **Permissions:** `IsAuthenticated`
|
|
88
104
|
*/
|
|
@@ -292,13 +308,21 @@ export interface paths {
|
|
|
292
308
|
cookie?: never;
|
|
293
309
|
};
|
|
294
310
|
/**
|
|
295
|
-
* @description Overridable serializer
|
|
311
|
+
* @description Overridable request/response serializer seam for an API view.
|
|
312
|
+
*
|
|
313
|
+
* Declare the classes as attributes, read them through the getters::
|
|
296
314
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
315
|
+
* class WalletView(SerializerSeamMixin, APIView):
|
|
316
|
+
* response_serializer_class = WalletResponseSerializer
|
|
299
317
|
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
318
|
+
* def get(self, request):
|
|
319
|
+
* response_cls = self.get_response_serializer_class()
|
|
320
|
+
* return StapelResponse(response_cls(wallet_to_dto(...)))
|
|
321
|
+
*
|
|
322
|
+
* A host project swaps either direction by subclassing and setting the
|
|
323
|
+
* attribute, or overrides the getter for a per-request decision — no HTTP
|
|
324
|
+
* method body is ever copied. ``None`` means the direction carries no
|
|
325
|
+
* serializer (raw ``request.FILES``, an empty 204 body).
|
|
302
326
|
*
|
|
303
327
|
* **Permissions:** `IsAuthenticated`
|
|
304
328
|
*/
|
|
@@ -322,13 +346,21 @@ export interface paths {
|
|
|
322
346
|
put?: never;
|
|
323
347
|
post?: never;
|
|
324
348
|
/**
|
|
325
|
-
* @description Overridable serializer
|
|
349
|
+
* @description Overridable request/response serializer seam for an API view.
|
|
350
|
+
*
|
|
351
|
+
* Declare the classes as attributes, read them through the getters::
|
|
326
352
|
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
353
|
+
* class WalletView(SerializerSeamMixin, APIView):
|
|
354
|
+
* response_serializer_class = WalletResponseSerializer
|
|
329
355
|
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
356
|
+
* def get(self, request):
|
|
357
|
+
* response_cls = self.get_response_serializer_class()
|
|
358
|
+
* return StapelResponse(response_cls(wallet_to_dto(...)))
|
|
359
|
+
*
|
|
360
|
+
* A host project swaps either direction by subclassing and setting the
|
|
361
|
+
* attribute, or overrides the getter for a per-request decision — no HTTP
|
|
362
|
+
* method body is ever copied. ``None`` means the direction carries no
|
|
363
|
+
* serializer (raw ``request.FILES``, an empty 204 body).
|
|
332
364
|
*
|
|
333
365
|
* **Permissions:** `IsAuthenticated`
|
|
334
366
|
*/
|
|
@@ -336,13 +368,21 @@ export interface paths {
|
|
|
336
368
|
options?: never;
|
|
337
369
|
head?: never;
|
|
338
370
|
/**
|
|
339
|
-
* @description Overridable serializer
|
|
371
|
+
* @description Overridable request/response serializer seam for an API view.
|
|
372
|
+
*
|
|
373
|
+
* Declare the classes as attributes, read them through the getters::
|
|
374
|
+
*
|
|
375
|
+
* class WalletView(SerializerSeamMixin, APIView):
|
|
376
|
+
* response_serializer_class = WalletResponseSerializer
|
|
340
377
|
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
378
|
+
* def get(self, request):
|
|
379
|
+
* response_cls = self.get_response_serializer_class()
|
|
380
|
+
* return StapelResponse(response_cls(wallet_to_dto(...)))
|
|
343
381
|
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
382
|
+
* A host project swaps either direction by subclassing and setting the
|
|
383
|
+
* attribute, or overrides the getter for a per-request decision — no HTTP
|
|
384
|
+
* method body is ever copied. ``None`` means the direction carries no
|
|
385
|
+
* serializer (raw ``request.FILES``, an empty 204 body).
|
|
346
386
|
*
|
|
347
387
|
* **Permissions:** `IsAuthenticated`
|
|
348
388
|
*/
|
|
@@ -480,13 +520,13 @@ export interface paths {
|
|
|
480
520
|
get?: never;
|
|
481
521
|
put?: never;
|
|
482
522
|
/**
|
|
483
|
-
* @description
|
|
523
|
+
* @description Turns an unaskable plan ceiling into 503 for the whole view.
|
|
484
524
|
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
525
|
+
* Mixed in rather than written as an ``except`` at each call site because
|
|
526
|
+
* the seam is consulted from views AND from inside ``services`` under the
|
|
527
|
+
* workspace lock, and the set of call sites grows: a view that gains a
|
|
528
|
+
* method later inherits the mapping instead of re-deriving it. The 402
|
|
529
|
+
* denials stay per-call-site — those carry a limit the screen renders.
|
|
490
530
|
*
|
|
491
531
|
* **Permissions:** `IsAuthenticated`
|
|
492
532
|
*/
|
|
@@ -520,7 +560,8 @@ export interface paths {
|
|
|
520
560
|
* Gate stack, in order: HIGH step-up (``@requires_verification``, scope
|
|
521
561
|
* ``sensitive`` — the same store as admin step-up) → capability
|
|
522
562
|
* ``members.provision`` (403) → entitlement ``workspaces.provision_user``
|
|
523
|
-
* (402;
|
|
563
|
+
* (402; an unreachable billing answers 503, not allow) → optional
|
|
564
|
+
* per-user debit
|
|
524
565
|
* (``STAPEL_WORKSPACES["PROVISION_USER_CREDITS"]`` > 0).
|
|
525
566
|
*
|
|
526
567
|
* Credentials: a synthetic account normally has no email — when the
|
|
@@ -653,6 +694,11 @@ export interface paths {
|
|
|
653
694
|
* NOT consumed here: accept stays a separate, deliberate step after
|
|
654
695
|
* setup. Neither the invite token nor the grant token is ever logged.
|
|
655
696
|
*
|
|
697
|
+
* ONE live grant at a time (WORK-03): while the previous grant is inside
|
|
698
|
+
* its TTL this answers 429 ``error.429.invitation_grant_pending`` with a
|
|
699
|
+
* ``Retry-After``, so a leaked invite link cannot be replayed into an
|
|
700
|
+
* endless supply of sign-in credentials for the invited mailbox.
|
|
701
|
+
*
|
|
656
702
|
* **Permissions:** `AllowAny`
|
|
657
703
|
*/
|
|
658
704
|
post: operations["workspaces_api_v1_invitations_claim_create"];
|
|
@@ -674,11 +720,36 @@ export interface paths {
|
|
|
674
720
|
/**
|
|
675
721
|
* @description Decline an invitation — the invitee's terminal "no" (spec §B2).
|
|
676
722
|
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
723
|
+
* AllowAny, on the token, exactly like preview and claim — and this is a
|
|
724
|
+
* CHANGE from the original authenticated-only rule, made because that rule
|
|
725
|
+
* made the common "no" impossible to say.
|
|
726
|
+
*
|
|
727
|
+
* Saying no used to require an account. The invitee who has none is the
|
|
728
|
+
* majority case on this path (that is why ``claim`` exists at all), so
|
|
729
|
+
* declining meant first creating the very account the person was declining
|
|
730
|
+
* to create: the flow asked for the commitment before the decision, and
|
|
731
|
+
* the refusal left a live, empty account behind it. Verified on the stand.
|
|
732
|
+
*
|
|
733
|
+
* The token is the proof, and it is the same proof the module already
|
|
734
|
+
* accepts elsewhere for strictly more: ``claim`` takes this token from an
|
|
735
|
+
* anonymous caller and mints a LOGIN GRANT for the invited mailbox.
|
|
736
|
+
* Anything a token holder could do by declining, they could already do —
|
|
737
|
+
* and more — by claiming. What the token cannot do is act as somebody
|
|
738
|
+
* else: a request that arrives WITH a session naming an address must still
|
|
739
|
+
* be the invited address, so a signed-in person cannot resolve an
|
|
740
|
+
* invitation belonging to someone else. That check is unchanged.
|
|
741
|
+
*
|
|
742
|
+
* A session with no address at all (a guest session — this product mints
|
|
743
|
+
* them at meeting doors) is treated as the anonymous case rather than as a
|
|
744
|
+
* mismatched account: it names nobody, so it can neither prove nor
|
|
745
|
+
* disprove ownership, and the token stands on its own. Before this it was
|
|
746
|
+
* refused outright by ``ANONYMOUS_DENIED``, which is how a guest who was
|
|
747
|
+
* also an invitee could not decline at all.
|
|
748
|
+
*
|
|
749
|
+
* Decline ≠ revoke — both states stay distinguishable in the preview
|
|
750
|
+
* ``status``. Throttled and token-never-logged like its AllowAny siblings.
|
|
680
751
|
*
|
|
681
|
-
* **Permissions:** `
|
|
752
|
+
* **Permissions:** `AllowAny`
|
|
682
753
|
*/
|
|
683
754
|
post: operations["workspaces_api_v1_invitations_decline_create"];
|
|
684
755
|
delete?: never;
|
|
@@ -697,13 +768,13 @@ export interface paths {
|
|
|
697
768
|
get?: never;
|
|
698
769
|
put?: never;
|
|
699
770
|
/**
|
|
700
|
-
* @description
|
|
701
|
-
*
|
|
702
|
-
* Subclasses (or downstream projects) can swap the request/response
|
|
703
|
-
* serializers without copying method bodies:
|
|
771
|
+
* @description Turns an unaskable plan ceiling into 503 for the whole view.
|
|
704
772
|
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
773
|
+
* Mixed in rather than written as an ``except`` at each call site because
|
|
774
|
+
* the seam is consulted from views AND from inside ``services`` under the
|
|
775
|
+
* workspace lock, and the set of call sites grows: a view that gains a
|
|
776
|
+
* method later inherits the mapping instead of re-deriving it. The 402
|
|
777
|
+
* denials stay per-call-site — those carry a limit the screen renders.
|
|
707
778
|
*
|
|
708
779
|
* **Permissions:** `IsAuthenticated`
|
|
709
780
|
*/
|
|
@@ -999,6 +1070,46 @@ export interface components {
|
|
|
999
1070
|
*/
|
|
1000
1071
|
last_sent_at?: string | null;
|
|
1001
1072
|
};
|
|
1073
|
+
/** @description How far the workspace's require_mfa policy has actually got. */
|
|
1074
|
+
MFAEnforcementStatus: {
|
|
1075
|
+
/**
|
|
1076
|
+
* @description pending / enforcing / enforced / failed. Only "enforced" means every active member's second factor has been confirmed — a settings flag saying require_mfa=true says somebody asked for it, not that it holds
|
|
1077
|
+
* @example enforcing
|
|
1078
|
+
*/
|
|
1079
|
+
state: string;
|
|
1080
|
+
/**
|
|
1081
|
+
* @description How many sweeps have run since the policy was switched on
|
|
1082
|
+
* @example 3
|
|
1083
|
+
*/
|
|
1084
|
+
attempts?: number;
|
|
1085
|
+
/**
|
|
1086
|
+
* @description Members auth answered about in the last sweep
|
|
1087
|
+
* @example 12
|
|
1088
|
+
*/
|
|
1089
|
+
checked_members?: number;
|
|
1090
|
+
/**
|
|
1091
|
+
* @description Of those, members with no strong factor — suspended with reason no_mfa
|
|
1092
|
+
* @example 2
|
|
1093
|
+
*/
|
|
1094
|
+
noncompliant_members?: number;
|
|
1095
|
+
/**
|
|
1096
|
+
* @description Active members whose factor nobody has confirmed yet. They are NOT admitted while the policy is on; this is the number an administrator has to get to zero
|
|
1097
|
+
* @example 1
|
|
1098
|
+
*/
|
|
1099
|
+
unverified_members?: number;
|
|
1100
|
+
/**
|
|
1101
|
+
* @description ISO 8601 time of the last sweep; null before the first
|
|
1102
|
+
* @example 2026-08-11T10:00:00Z
|
|
1103
|
+
*/
|
|
1104
|
+
last_attempt_at?: string | null;
|
|
1105
|
+
/** @description ISO 8601 time coverage last became complete; null while it is not */
|
|
1106
|
+
completed_at?: string | null;
|
|
1107
|
+
/**
|
|
1108
|
+
* @description What auth answered when the last sweep failed, verbatim; empty otherwise. Never credential material
|
|
1109
|
+
* @example FunctionCallError: mfa_status timed out
|
|
1110
|
+
*/
|
|
1111
|
+
last_error?: string;
|
|
1112
|
+
};
|
|
1002
1113
|
/** @description Invite payload. */
|
|
1003
1114
|
MemberInviteRequest: {
|
|
1004
1115
|
/**
|
|
@@ -1109,6 +1220,13 @@ export interface components {
|
|
|
1109
1220
|
* @example Ada Lovelace
|
|
1110
1221
|
*/
|
|
1111
1222
|
display_name?: string | null;
|
|
1223
|
+
/**
|
|
1224
|
+
* @description Whether this member was PROVEN to hold a strong second factor — true, false, or null for "nobody has asked yet". Under a require_mfa policy the null is what an administrator acts on: those members are not admitted until the answer arrives
|
|
1225
|
+
* @example true
|
|
1226
|
+
*/
|
|
1227
|
+
mfa_compliant?: boolean | null;
|
|
1228
|
+
/** @description Whether this row IS the caller. Server-derived — the client is never asked to compare ids it may not have. Always false on the service-to-service membership read, where the caller is a service and not a member of anything */
|
|
1229
|
+
is_self?: boolean;
|
|
1112
1230
|
};
|
|
1113
1231
|
PaginatedAuditEventResponseList: {
|
|
1114
1232
|
items: components["schemas"]["AuditEventResponse"][];
|
|
@@ -1396,6 +1514,15 @@ export interface components {
|
|
|
1396
1514
|
* @example Victor P.
|
|
1397
1515
|
*/
|
|
1398
1516
|
owner_display_name?: string;
|
|
1517
|
+
/** @description State of the security.require_mfa policy — present on the single-workspace responses (GET/PATCH detail) when the policy is on, null otherwise. The settings block says what was asked for; this says what actually holds, so an administrator who switched MFA on is not told "done" while half the organization was never checked */
|
|
1518
|
+
mfa_enforcement?: components["schemas"]["MFAEnforcementStatus"] | null;
|
|
1519
|
+
/** @description Whether THIS caller may delete THIS workspace right now. The ANSWER, not the inputs: a client deriving it from my_role alone draws a delete button on the instance's default workspace and on a personal workspace the next sign-in re-mints — both of which the endpoint refuses, which is the "button that leads to a refusal" defect. Same shape and same reason as can_create_workspace on the list response */
|
|
1520
|
+
can_delete?: boolean;
|
|
1521
|
+
/**
|
|
1522
|
+
* @description The error code the DELETE would return, or "" when can_delete is true. The CODE rather than a sentence, so the client renders the refusal in the caller's own language through the error map it already ships. Server verdict and screen explanation are ONE evaluation (services.deletion_block_reason), so a screen cannot promise what the endpoint denies
|
|
1523
|
+
* @example error.409.workspace_is_instance_default
|
|
1524
|
+
*/
|
|
1525
|
+
delete_blocked_reason?: string;
|
|
1399
1526
|
};
|
|
1400
1527
|
};
|
|
1401
1528
|
responses: never;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Which workspace is this screen about" — the seam between the NAV contract
|
|
3
|
+
* and the four workspace-scoped admin screens.
|
|
4
|
+
*
|
|
5
|
+
* The nav contract routes a screen; it does not hand it an ambient scope, and
|
|
6
|
+
* the active workspace is not a path segment of a settings URL — it is
|
|
7
|
+
* RUNTIME state, the same state the container writes when a person switches
|
|
8
|
+
* (`WorkspaceSelection.switchTo`). So a screen declared in nav takes NO
|
|
9
|
+
* `workspaceId` from the route and reads it from
|
|
10
|
+
* {@link useOptionalWorkspaceSelection} instead; a host that mounts the same
|
|
11
|
+
* component itself keeps passing `workspaceId` explicitly and never touches
|
|
12
|
+
* the selection at all.
|
|
13
|
+
*
|
|
14
|
+
* The `Optional` accessor is load-bearing. `useWorkspaceSelection()` throws
|
|
15
|
+
* outside its provider, which is right for application code and wrong for a
|
|
16
|
+
* screen the shell mounts from a manifest: a shell that has not wired the
|
|
17
|
+
* provider would render a white page with an error in the console. Here the
|
|
18
|
+
* three answers a person can actually get are drawn instead:
|
|
19
|
+
*
|
|
20
|
+
* - the list is still loading / failed → the substrate's own arms (a
|
|
21
|
+
* skeleton, and the failure WITH a retry — never "you have no workspaces"),
|
|
22
|
+
* - the person belongs to none → "you are not in a workspace yet",
|
|
23
|
+
* - there is no selection at all (no provider, or nothing resolved) →
|
|
24
|
+
* "choose a workspace".
|
|
25
|
+
*
|
|
26
|
+
* None of them is blank, and none of them claims something the client does
|
|
27
|
+
* not know.
|
|
28
|
+
*/
|
|
29
|
+
import type { ReactElement, ReactNode } from "react";
|
|
30
|
+
import { useT } from "@stapel/core";
|
|
31
|
+
import { EmptyState, LoadBoundary } from "@stapel/tokens-antd/skin";
|
|
32
|
+
import { useOptionalWorkspaceSelection } from "../model/selection.js";
|
|
33
|
+
import { WORKSPACES_I18N_KEYS } from "../i18n/keys.js";
|
|
34
|
+
|
|
35
|
+
export interface ActiveWorkspaceBoundaryProps {
|
|
36
|
+
/** The workspace the HOST named. Given, the selection is never consulted. */
|
|
37
|
+
readonly workspaceId?: string | undefined;
|
|
38
|
+
/** Rendered once a workspace id is known. */
|
|
39
|
+
readonly children: (workspaceId: string) => ReactNode;
|
|
40
|
+
/** Test id of the chooser / empty arms (the ready arm has no wrapper). */
|
|
41
|
+
readonly testId: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the workspace this screen is about, or draw the reason it cannot be
|
|
46
|
+
* resolved. Renders `children(workspaceId)` unchanged once there is one.
|
|
47
|
+
*/
|
|
48
|
+
export function ActiveWorkspaceBoundary(
|
|
49
|
+
props: ActiveWorkspaceBoundaryProps
|
|
50
|
+
): ReactElement {
|
|
51
|
+
const t = useT();
|
|
52
|
+
// Unconditional, as a hook must be: reading the context is cheap and it
|
|
53
|
+
// returns null outside a provider rather than throwing.
|
|
54
|
+
const selection = useOptionalWorkspaceSelection();
|
|
55
|
+
|
|
56
|
+
if (props.workspaceId !== undefined) {
|
|
57
|
+
return <>{props.children(props.workspaceId)}</>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const chooser = (
|
|
61
|
+
<EmptyState
|
|
62
|
+
title={t(WORKSPACES_I18N_KEYS.activeChooseTitle)}
|
|
63
|
+
hint={t(WORKSPACES_I18N_KEYS.activeChooseHint)}
|
|
64
|
+
testId={`${props.testId}-choose`}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (selection === null) return chooser;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<LoadBoundary
|
|
72
|
+
state={selection.state}
|
|
73
|
+
testId={props.testId}
|
|
74
|
+
onRetry={() => {
|
|
75
|
+
void selection.refetch();
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{(workspaces) => {
|
|
79
|
+
if (selection.current !== null) {
|
|
80
|
+
return <>{props.children(selection.current.id)}</>;
|
|
81
|
+
}
|
|
82
|
+
// The list arrived and the chain still answered nobody: either the
|
|
83
|
+
// person belongs to no workspace (a different sentence from "pick
|
|
84
|
+
// one"), or a host is driving the URL layer at something unresolved.
|
|
85
|
+
return workspaces.length === 0 ? (
|
|
86
|
+
<EmptyState
|
|
87
|
+
title={t(WORKSPACES_I18N_KEYS.activeNoneTitle)}
|
|
88
|
+
hint={t(WORKSPACES_I18N_KEYS.activeNoneHint)}
|
|
89
|
+
testId={`${props.testId}-none`}
|
|
90
|
+
/>
|
|
91
|
+
) : (
|
|
92
|
+
chooser
|
|
93
|
+
);
|
|
94
|
+
}}
|
|
95
|
+
</LoadBoundary>
|
|
96
|
+
);
|
|
97
|
+
}
|