@stapel/workspaces-react 0.6.0 → 0.7.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 +14 -0
- package/dist/api/generated/schema.d.ts +384 -4
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/types.d.ts +20 -5
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/workspacesApi.d.ts +23 -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/default/InviteAcceptPage.d.ts +28 -0
- package/dist/default/InviteAcceptPage.d.ts.map +1 -0
- package/dist/default/InviteAcceptPage.js +141 -0
- package/dist/default/InviteAcceptPage.js.map +1 -0
- package/dist/default/MembersManager.d.ts +4 -4
- package/dist/default/MembersManager.d.ts.map +1 -1
- package/dist/default/MembersManager.js +61 -56
- package/dist/default/MembersManager.js.map +1 -1
- package/dist/default/index.d.ts +2 -0
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +1 -0
- package/dist/default/index.js.map +1 -1
- package/dist/flows/inviteAcceptFlow.d.ts +155 -0
- package/dist/flows/inviteAcceptFlow.d.ts.map +1 -0
- package/dist/flows/inviteAcceptFlow.js +126 -0
- package/dist/flows/inviteAcceptFlow.js.map +1 -0
- package/dist/headless/Can.d.ts +40 -0
- package/dist/headless/Can.d.ts.map +1 -0
- package/dist/headless/Can.js +30 -0
- package/dist/headless/Can.js.map +1 -0
- package/dist/headless/InviteAcceptFlow.d.ts +44 -0
- package/dist/headless/InviteAcceptFlow.d.ts.map +1 -0
- package/dist/headless/InviteAcceptFlow.js +67 -0
- package/dist/headless/InviteAcceptFlow.js.map +1 -0
- package/dist/headless/RoleSelect.d.ts +41 -0
- package/dist/headless/RoleSelect.d.ts.map +1 -0
- package/dist/headless/RoleSelect.js +34 -0
- package/dist/headless/RoleSelect.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +48 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.gen.js +24 -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 +8 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
- package/dist/i18n/keys.d.ts +28 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +64 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +30 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/model/capabilities.d.ts +21 -0
- package/dist/model/capabilities.d.ts.map +1 -0
- package/dist/model/capabilities.js +32 -0
- package/dist/model/capabilities.js.map +1 -0
- package/dist/model/emailMask.d.ts +22 -0
- package/dist/model/emailMask.d.ts.map +1 -0
- package/dist/model/emailMask.js +36 -0
- package/dist/model/emailMask.js.map +1 -0
- package/dist/model/mutations.d.ts +23 -3
- package/dist/model/mutations.d.ts.map +1 -1
- package/dist/model/mutations.js +39 -1
- package/dist/model/mutations.js.map +1 -1
- package/dist/model/queries.d.ts +37 -1
- package/dist/model/queries.d.ts.map +1 -1
- package/dist/model/queries.js +49 -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 +6 -0
- package/dist/model/queryKeys.js.map +1 -1
- package/llms.txt +40 -15
- package/manifest.json +225 -3
- package/package.json +4 -4
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +384 -4
- package/src/api/types.ts +20 -5
- package/src/api/workspacesApi.ts +50 -0
- package/src/default/InviteAcceptPage.tsx +373 -0
- package/src/default/MembersManager.tsx +155 -145
- package/src/default/index.ts +2 -0
- package/src/flows/inviteAcceptFlow.ts +289 -0
- package/src/headless/Can.tsx +49 -0
- package/src/headless/InviteAcceptFlow.tsx +105 -0
- package/src/headless/RoleSelect.tsx +58 -0
- package/src/i18n/generated/errors.gen.ts +24 -0
- package/src/i18n/generated/errors.json +62 -0
- package/src/i18n/generated/errors.ru.gen.ts +8 -0
- package/src/i18n/keys.ts +68 -0
- package/src/i18n/ru.ts +34 -0
- package/src/index.ts +26 -0
- package/src/model/capabilities.ts +37 -0
- package/src/model/emailMask.ts +37 -0
- package/src/model/mutations.ts +56 -4
- package/src/model/queries.ts +73 -0
- package/src/model/queryKeys.ts +10 -0
package/src/index.ts
CHANGED
|
@@ -25,6 +25,12 @@ export type {
|
|
|
25
25
|
FlowError,
|
|
26
26
|
} from "@stapel/core";
|
|
27
27
|
export { toFlowError } from "./flows/errors.js";
|
|
28
|
+
export { createInviteAcceptFlow } from "./flows/inviteAcceptFlow.js";
|
|
29
|
+
export type {
|
|
30
|
+
InviteAcceptFlow as InviteAcceptFlowController,
|
|
31
|
+
InviteAcceptFlowDeps,
|
|
32
|
+
InviteAcceptState,
|
|
33
|
+
} from "./flows/inviteAcceptFlow.js";
|
|
28
34
|
export { WORKSPACES_FLOWS, flowEndpoints } from "./flows/registry.js";
|
|
29
35
|
export type {
|
|
30
36
|
WorkspacesFlowId,
|
|
@@ -51,7 +57,15 @@ export {
|
|
|
51
57
|
useWorkspaces,
|
|
52
58
|
useWorkspace,
|
|
53
59
|
useMembers,
|
|
60
|
+
useRoles,
|
|
61
|
+
useInvitationPreview,
|
|
62
|
+
useCapabilities,
|
|
54
63
|
} from "./model/queries.js";
|
|
64
|
+
export type { CapabilitiesResult } from "./model/queries.js";
|
|
65
|
+
|
|
66
|
+
// ── model (capability matcher + email-mask ports — backend-synced utils) ─────
|
|
67
|
+
export { capabilityMatches, hasCapability } from "./model/capabilities.js";
|
|
68
|
+
export { maskEmail, emailMatchesMask } from "./model/emailMask.js";
|
|
55
69
|
|
|
56
70
|
// ── model (write hooks) ──────────────────────────────────────────────────────
|
|
57
71
|
export {
|
|
@@ -62,6 +76,8 @@ export {
|
|
|
62
76
|
useUpdateMemberRole,
|
|
63
77
|
useRemoveMember,
|
|
64
78
|
useAcceptInvitation,
|
|
79
|
+
useClaimInvitation,
|
|
80
|
+
useDeclineInvitation,
|
|
65
81
|
} from "./model/mutations.js";
|
|
66
82
|
export type { MemberRoleChange } from "./model/mutations.js";
|
|
67
83
|
|
|
@@ -79,6 +95,10 @@ export type {
|
|
|
79
95
|
MemberRoleUpdate,
|
|
80
96
|
Invitation,
|
|
81
97
|
InvitationAccept,
|
|
98
|
+
InvitationPreview,
|
|
99
|
+
InvitationClaim,
|
|
100
|
+
RoleInfo,
|
|
101
|
+
RoleList,
|
|
82
102
|
WorkspaceRole,
|
|
83
103
|
WorkspaceKind,
|
|
84
104
|
} from "./api/types.js";
|
|
@@ -91,6 +111,12 @@ export { Members } from "./headless/Members.js";
|
|
|
91
111
|
export type { MembersBag } from "./headless/Members.js";
|
|
92
112
|
export { AcceptInvitation } from "./headless/AcceptInvitation.js";
|
|
93
113
|
export type { AcceptInvitationBag } from "./headless/AcceptInvitation.js";
|
|
114
|
+
export { Can } from "./headless/Can.js";
|
|
115
|
+
export type { CanBag } from "./headless/Can.js";
|
|
116
|
+
export { RoleSelect } from "./headless/RoleSelect.js";
|
|
117
|
+
export type { RoleSelectBag } from "./headless/RoleSelect.js";
|
|
118
|
+
export { InviteAcceptFlow } from "./headless/InviteAcceptFlow.js";
|
|
119
|
+
export type { InviteAcceptFlowBag } from "./headless/InviteAcceptFlow.js";
|
|
94
120
|
|
|
95
121
|
// ── i18n ─────────────────────────────────────────────────────────────────────
|
|
96
122
|
export {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side capability matcher (org-program §A1/§A2) — a PORT of
|
|
3
|
+
* stapel-workspaces' `capabilities.capability_matches` (which is itself
|
|
4
|
+
* mirrored by the consumer helper in `stapel_core.django.workspaces`).
|
|
5
|
+
* KEEP SEMANTICS IN SYNC with the backend: capability strings are namespaced
|
|
6
|
+
* `"<domain>.<action>"`, free-form for the client (`meetings.spotlight`,
|
|
7
|
+
* `records.view`, …); a granted string may be the exact capability, the
|
|
8
|
+
* global wildcard `"*"`, or a prefix wildcard like `"members.*"` (which
|
|
9
|
+
* matches `members.remove` AND deeper `members.role.change`).
|
|
10
|
+
*
|
|
11
|
+
* The client-side check is a UI convenience (hide/disable affordances) —
|
|
12
|
+
* NEVER an access decision: the backend re-checks every capability on every
|
|
13
|
+
* operation (`has_capability` in-service, `workspaces.check_capability`
|
|
14
|
+
* cross-service).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Match one granted capability string against the requested one. */
|
|
18
|
+
export function capabilityMatches(
|
|
19
|
+
capability: string,
|
|
20
|
+
granted: string
|
|
21
|
+
): boolean {
|
|
22
|
+
if (granted === "*" || granted === capability) return true;
|
|
23
|
+
if (granted.endsWith(".*")) {
|
|
24
|
+
return capability.startsWith(granted.slice(0, -1));
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True if any of `granted` (verbatim registry strings, wildcards included)
|
|
30
|
+
* matches `capability`. Empty/absent grants deny — deny-by-default. */
|
|
31
|
+
export function hasCapability(
|
|
32
|
+
granted: readonly string[] | null | undefined,
|
|
33
|
+
capability: string
|
|
34
|
+
): boolean {
|
|
35
|
+
if (!granted) return false;
|
|
36
|
+
return granted.some((g) => capabilityMatches(capability, g));
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email masking — a PORT of stapel-workspaces' `views._mask_email` (the
|
|
3
|
+
* function that produces `InvitationPreviewResponse.email_masked`). KEEP IN
|
|
4
|
+
* SYNC with the backend: first character of the local part and of the domain
|
|
5
|
+
* name survive, the TLD stays readable — `m***@e***.com`.
|
|
6
|
+
*
|
|
7
|
+
* Why the client re-implements it: the public invitation preview deliberately
|
|
8
|
+
* carries only the MASKED email (harvest-proof, org-program §B2), yet the
|
|
9
|
+
* §B4 flow must route "session & email match" vs "wrong account" BEFORE
|
|
10
|
+
* attempting the accept. Masking the signed-in account's own (fully known)
|
|
11
|
+
* email with the same algorithm and comparing the two masks is the exact
|
|
12
|
+
* decision the backend would make — without ever exposing the invitee's
|
|
13
|
+
* address. A mask collision (two addresses masking identically) merely shows
|
|
14
|
+
* the accept prompt; the backend's real email-match on accept remains the
|
|
15
|
+
* enforcement point.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Mask an email exactly like the backend's invitation preview does. */
|
|
19
|
+
export function maskEmail(email: string): string {
|
|
20
|
+
const at = email.indexOf("@");
|
|
21
|
+
const local = at === -1 ? email : email.slice(0, at);
|
|
22
|
+
const domain = at === -1 ? "" : email.slice(at + 1);
|
|
23
|
+
const dot = domain.lastIndexOf(".");
|
|
24
|
+
const maskedLocal = `${local.slice(0, 1) || "*"}***`;
|
|
25
|
+
if (dot !== -1) {
|
|
26
|
+
const domainName = domain.slice(0, dot);
|
|
27
|
+
const tld = domain.slice(dot + 1);
|
|
28
|
+
return `${maskedLocal}@${domainName.slice(0, 1) || "*"}***.${tld}`;
|
|
29
|
+
}
|
|
30
|
+
return `${maskedLocal}@${domain.slice(0, 1) || "*"}***`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether `email`, masked by the backend's algorithm, equals `emailMasked`
|
|
34
|
+
* (case-insensitive on the email, as the backend matches `iexact`). */
|
|
35
|
+
export function emailMatchesMask(email: string, emailMasked: string): boolean {
|
|
36
|
+
return maskEmail(email.trim().toLowerCase()) === emailMasked.toLowerCase();
|
|
37
|
+
}
|
package/src/model/mutations.ts
CHANGED
|
@@ -9,9 +9,9 @@ import type {
|
|
|
9
9
|
MemberInvite,
|
|
10
10
|
MemberInviteResult,
|
|
11
11
|
InvitationAccept,
|
|
12
|
+
InvitationClaim,
|
|
12
13
|
Workspace,
|
|
13
14
|
WorkspaceCreate,
|
|
14
|
-
WorkspaceRole,
|
|
15
15
|
WorkspaceUpdate,
|
|
16
16
|
} from "../api/types.js";
|
|
17
17
|
import { useWorkspacesApi } from "./context.js";
|
|
@@ -123,10 +123,14 @@ export function useInviteMembers(
|
|
|
123
123
|
return useMutation(options);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/** The variable for {@link useUpdateMemberRole}: which member, which new role.
|
|
126
|
+
/** The variable for {@link useUpdateMemberRole}: which member, which new role.
|
|
127
|
+
* `role` is an open string since stapel-workspaces 0.6.0 (org-program §A1):
|
|
128
|
+
* the effective registry is settings-extensible, so any key of GET /roles is
|
|
129
|
+
* valid — the backend validates against the registry. {@link WorkspaceRole}
|
|
130
|
+
* still names the builtin four for literals. */
|
|
127
131
|
export interface MemberRoleChange {
|
|
128
132
|
readonly userId: string;
|
|
129
|
-
readonly role:
|
|
133
|
+
readonly role: string;
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
/**
|
|
@@ -186,10 +190,58 @@ export function useAcceptInvitation(): UseMutationResult<
|
|
|
186
190
|
const queryClient = useQueryClient();
|
|
187
191
|
const options: UseMutationOptions<Member, StapelApiError, InvitationAccept> = {
|
|
188
192
|
mutationFn: (body) => api.acceptInvitation(body),
|
|
189
|
-
onSuccess: () => {
|
|
193
|
+
onSuccess: (_member, body) => {
|
|
190
194
|
void queryClient.invalidateQueries({
|
|
191
195
|
queryKey: workspacesQueryKeys.list(),
|
|
192
196
|
});
|
|
197
|
+
// The preview's `status` moved pending → accepted.
|
|
198
|
+
void queryClient.invalidateQueries({
|
|
199
|
+
queryKey: workspacesQueryKeys.invitationPreview(body.token),
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
return useMutation(options);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Mint a login grant for a NOT-yet-registered invitee (POST
|
|
208
|
+
* /invitations/{token}/claim, AllowAny — org-program §B2/§B3). The variable
|
|
209
|
+
* is the invite token; the result carries the single-use `grant_token` to
|
|
210
|
+
* exchange at AUTH's `POST /grant/exchange/` (the auth-react api — this pair
|
|
211
|
+
* deliberately does not call it; see `InviteAcceptFlow`'s `onLoginGrant`
|
|
212
|
+
* seam). 409 `email_already_registered` → switch to login. The invitation is
|
|
213
|
+
* NOT consumed here. The grant token is a credential: never log it.
|
|
214
|
+
*/
|
|
215
|
+
export function useClaimInvitation(): UseMutationResult<
|
|
216
|
+
InvitationClaim,
|
|
217
|
+
StapelApiError,
|
|
218
|
+
string
|
|
219
|
+
> {
|
|
220
|
+
const api = useWorkspacesApi();
|
|
221
|
+
const options: UseMutationOptions<InvitationClaim, StapelApiError, string> = {
|
|
222
|
+
mutationFn: (token) => api.claimInvitation(token),
|
|
223
|
+
};
|
|
224
|
+
return useMutation(options);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Decline an invitation (POST /invitations/{token}/decline, authenticated +
|
|
229
|
+
* email-match — org-program §B2). The variable is the invite token. Decline ≠
|
|
230
|
+
* revoke — the preview's `status` becomes `declined`.
|
|
231
|
+
*/
|
|
232
|
+
export function useDeclineInvitation(): UseMutationResult<
|
|
233
|
+
void,
|
|
234
|
+
StapelApiError,
|
|
235
|
+
string
|
|
236
|
+
> {
|
|
237
|
+
const api = useWorkspacesApi();
|
|
238
|
+
const queryClient = useQueryClient();
|
|
239
|
+
const options: UseMutationOptions<void, StapelApiError, string> = {
|
|
240
|
+
mutationFn: (token) => api.declineInvitation(token),
|
|
241
|
+
onSuccess: (_result, token) => {
|
|
242
|
+
void queryClient.invalidateQueries({
|
|
243
|
+
queryKey: workspacesQueryKeys.invitationPreview(token),
|
|
244
|
+
});
|
|
193
245
|
},
|
|
194
246
|
};
|
|
195
247
|
return useMutation(options);
|
package/src/model/queries.ts
CHANGED
|
@@ -3,13 +3,16 @@ import type { UseQueryResult } from "@tanstack/react-query";
|
|
|
3
3
|
import { useActiveSessionReady } from "@stapel/core";
|
|
4
4
|
import type { StapelApiError } from "@stapel/core";
|
|
5
5
|
import type {
|
|
6
|
+
InvitationPreview,
|
|
6
7
|
MemberPage,
|
|
7
8
|
MembersParams,
|
|
9
|
+
RoleInfo,
|
|
8
10
|
Workspace,
|
|
9
11
|
WorkspaceList,
|
|
10
12
|
} from "../api/types.js";
|
|
11
13
|
import { useWorkspacesApi } from "./context.js";
|
|
12
14
|
import { workspacesQueryKeys } from "./queryKeys.js";
|
|
15
|
+
import { hasCapability } from "./capabilities.js";
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Read hooks over the workspaces API. Staleness follows core's query defaults;
|
|
@@ -77,3 +80,73 @@ export function useMembers(
|
|
|
77
80
|
enabled: sessionReady && workspaceId !== null && workspaceId !== "",
|
|
78
81
|
});
|
|
79
82
|
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The effective role registry (GET /roles, org-program §A2): builtin four +
|
|
86
|
+
* the deployment's `STAPEL_WORKSPACES["ROLES"]` overlay, capability strings
|
|
87
|
+
* verbatim, rank-descending. Deployment-static data — role UI (RoleSelect)
|
|
88
|
+
* reads this instead of hardcoding the builtin four. Session-ready-gated like
|
|
89
|
+
* {@link useWorkspaces} (IsAuthenticated endpoint, mounts at screen top).
|
|
90
|
+
*/
|
|
91
|
+
export function useRoles(): UseQueryResult<
|
|
92
|
+
readonly RoleInfo[],
|
|
93
|
+
StapelApiError
|
|
94
|
+
> {
|
|
95
|
+
const api = useWorkspacesApi();
|
|
96
|
+
const sessionReady = useActiveSessionReady();
|
|
97
|
+
return useQuery({
|
|
98
|
+
queryKey: workspacesQueryKeys.roles(),
|
|
99
|
+
queryFn: async () => (await api.listRoles()).roles ?? [],
|
|
100
|
+
enabled: sessionReady,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Public invitation preview (GET /invitations/{token}, AllowAny — org-program
|
|
106
|
+
* §B2): what the `/invite/{token}` page renders BEFORE any auth decision.
|
|
107
|
+
* Deliberately NOT session-gated — the whole point is that the invitee may
|
|
108
|
+
* have no session at all; the token in the URL is the bearer secret.
|
|
109
|
+
*/
|
|
110
|
+
export function useInvitationPreview(
|
|
111
|
+
token: string | null
|
|
112
|
+
): UseQueryResult<InvitationPreview, StapelApiError> {
|
|
113
|
+
const api = useWorkspacesApi();
|
|
114
|
+
return useQuery({
|
|
115
|
+
queryKey: workspacesQueryKeys.invitationPreview(token ?? ""),
|
|
116
|
+
queryFn: () => api.getInvitationPreview(token as string),
|
|
117
|
+
enabled: token !== null && token !== "",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** What {@link useCapabilities} returns: the caller's granted capability
|
|
122
|
+
* strings in one workspace plus the wildcard-aware `can()` check. */
|
|
123
|
+
export interface CapabilitiesResult {
|
|
124
|
+
/** Verbatim registry strings of the caller's role (wildcards included);
|
|
125
|
+
* empty while loading or when the caller is not a member. */
|
|
126
|
+
readonly capabilities: readonly string[];
|
|
127
|
+
/** Wildcard-aware check (`*` / `prefix.*` — the backend matcher, ported).
|
|
128
|
+
* UI convenience only: the backend re-checks on every operation. */
|
|
129
|
+
can(capability: string): boolean;
|
|
130
|
+
readonly isLoading: boolean;
|
|
131
|
+
readonly isError: boolean;
|
|
132
|
+
readonly error: StapelApiError | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The caller's capabilities in one workspace (org-program §A2): reads
|
|
137
|
+
* `my_capabilities` off the workspace detail (`WorkspaceResponse`, additive
|
|
138
|
+
* field since stapel-workspaces 0.6.0) and exposes the ported wildcard
|
|
139
|
+
* matcher. Deny-by-default: `can()` is false while loading, on error, or when
|
|
140
|
+
* the backend predates the field.
|
|
141
|
+
*/
|
|
142
|
+
export function useCapabilities(workspaceId: string | null): CapabilitiesResult {
|
|
143
|
+
const query = useWorkspace(workspaceId);
|
|
144
|
+
const capabilities = query.data?.my_capabilities ?? [];
|
|
145
|
+
return {
|
|
146
|
+
capabilities,
|
|
147
|
+
can: (capability) => hasCapability(capabilities, capability),
|
|
148
|
+
isLoading: query.isLoading,
|
|
149
|
+
isError: query.isError,
|
|
150
|
+
error: query.error ?? null,
|
|
151
|
+
};
|
|
152
|
+
}
|
package/src/model/queryKeys.ts
CHANGED
|
@@ -18,6 +18,10 @@ export const workspacesQueryKeys: {
|
|
|
18
18
|
workspaceId: string,
|
|
19
19
|
params: MembersParams
|
|
20
20
|
): readonly ["workspaces", "members", string, MembersParams];
|
|
21
|
+
roles(): readonly ["workspaces", "roles"];
|
|
22
|
+
invitationPreview(
|
|
23
|
+
token: string
|
|
24
|
+
): readonly ["workspaces", "invitation-preview", string];
|
|
21
25
|
} = {
|
|
22
26
|
all: [ROOT],
|
|
23
27
|
list: () => [ROOT, "list"],
|
|
@@ -26,4 +30,10 @@ export const workspacesQueryKeys: {
|
|
|
26
30
|
// invalidating `members(workspaceId)` (mutations.ts) drops every page.
|
|
27
31
|
members: (workspaceId) => [ROOT, "members", workspaceId],
|
|
28
32
|
membersPage: (workspaceId, params) => [ROOT, "members", workspaceId, params],
|
|
33
|
+
roles: () => [ROOT, "roles"],
|
|
34
|
+
// NOTE: the key carries the invite TOKEN (a secret). Core's query persist
|
|
35
|
+
// scope is per-user and the preview response is already public-by-design
|
|
36
|
+
// (masked email only) — but hosts logging query keys should treat this one
|
|
37
|
+
// as sensitive.
|
|
38
|
+
invitationPreview: (token) => [ROOT, "invitation-preview", token],
|
|
29
39
|
};
|