@invect/rbac 0.0.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/dist/backend/index.cjs +1365 -0
  4. package/dist/backend/index.cjs.map +1 -0
  5. package/dist/backend/index.d.ts +3 -0
  6. package/dist/backend/index.d.ts.map +1 -0
  7. package/dist/backend/index.mjs +1363 -0
  8. package/dist/backend/index.mjs.map +1 -0
  9. package/dist/backend/plugin.d.ts +60 -0
  10. package/dist/backend/plugin.d.ts.map +1 -0
  11. package/dist/frontend/components/AccessControlPage.d.ts +2 -0
  12. package/dist/frontend/components/AccessControlPage.d.ts.map +1 -0
  13. package/dist/frontend/components/FlowAccessPanel.d.ts +10 -0
  14. package/dist/frontend/components/FlowAccessPanel.d.ts.map +1 -0
  15. package/dist/frontend/components/ShareButton.d.ts +9 -0
  16. package/dist/frontend/components/ShareButton.d.ts.map +1 -0
  17. package/dist/frontend/components/ShareFlowModal.d.ts +12 -0
  18. package/dist/frontend/components/ShareFlowModal.d.ts.map +1 -0
  19. package/dist/frontend/components/TeamsPage.d.ts +5 -0
  20. package/dist/frontend/components/TeamsPage.d.ts.map +1 -0
  21. package/dist/frontend/components/UserMenuSection.d.ts +14 -0
  22. package/dist/frontend/components/UserMenuSection.d.ts.map +1 -0
  23. package/dist/frontend/components/access-control/AccessControlPage.d.ts +2 -0
  24. package/dist/frontend/components/access-control/AccessControlPage.d.ts.map +1 -0
  25. package/dist/frontend/components/access-control/AccessTable.d.ts +17 -0
  26. package/dist/frontend/components/access-control/AccessTable.d.ts.map +1 -0
  27. package/dist/frontend/components/access-control/FlowDetailPanel.d.ts +11 -0
  28. package/dist/frontend/components/access-control/FlowDetailPanel.d.ts.map +1 -0
  29. package/dist/frontend/components/access-control/FormDialog.d.ts +7 -0
  30. package/dist/frontend/components/access-control/FormDialog.d.ts.map +1 -0
  31. package/dist/frontend/components/access-control/MemberCombobox.d.ts +8 -0
  32. package/dist/frontend/components/access-control/MemberCombobox.d.ts.map +1 -0
  33. package/dist/frontend/components/access-control/MoveConfirmationDialog.d.ts +9 -0
  34. package/dist/frontend/components/access-control/MoveConfirmationDialog.d.ts.map +1 -0
  35. package/dist/frontend/components/access-control/PrincipalCombobox.d.ts +11 -0
  36. package/dist/frontend/components/access-control/PrincipalCombobox.d.ts.map +1 -0
  37. package/dist/frontend/components/access-control/ScopeDetailPanel.d.ts +11 -0
  38. package/dist/frontend/components/access-control/ScopeDetailPanel.d.ts.map +1 -0
  39. package/dist/frontend/components/access-control/index.d.ts +4 -0
  40. package/dist/frontend/components/access-control/index.d.ts.map +1 -0
  41. package/dist/frontend/components/access-control/types.d.ts +36 -0
  42. package/dist/frontend/components/access-control/types.d.ts.map +1 -0
  43. package/dist/frontend/components/access-control/useUsers.d.ts +3 -0
  44. package/dist/frontend/components/access-control/useUsers.d.ts.map +1 -0
  45. package/dist/frontend/hooks/useFlowAccess.d.ts +15 -0
  46. package/dist/frontend/hooks/useFlowAccess.d.ts.map +1 -0
  47. package/dist/frontend/hooks/useScopes.d.ts +15 -0
  48. package/dist/frontend/hooks/useScopes.d.ts.map +1 -0
  49. package/dist/frontend/hooks/useTeams.d.ts +25 -0
  50. package/dist/frontend/hooks/useTeams.d.ts.map +1 -0
  51. package/dist/frontend/index.cjs +2928 -0
  52. package/dist/frontend/index.cjs.map +1 -0
  53. package/dist/frontend/index.d.ts +23 -0
  54. package/dist/frontend/index.d.ts.map +1 -0
  55. package/dist/frontend/index.mjs +2899 -0
  56. package/dist/frontend/index.mjs.map +1 -0
  57. package/dist/frontend/providers/RbacProvider.d.ts +33 -0
  58. package/dist/frontend/providers/RbacProvider.d.ts.map +1 -0
  59. package/dist/frontend/stores/accessControlStore.d.ts +49 -0
  60. package/dist/frontend/stores/accessControlStore.d.ts.map +1 -0
  61. package/dist/frontend/types.d.ts +95 -0
  62. package/dist/frontend/types.d.ts.map +1 -0
  63. package/dist/shared/types.cjs +0 -0
  64. package/dist/shared/types.d.ts +172 -0
  65. package/dist/shared/types.d.ts.map +1 -0
  66. package/dist/shared/types.mjs +1 -0
  67. package/package.json +107 -0
@@ -0,0 +1,2928 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let lucide_react = require("lucide-react");
3
+ let react = require("react");
4
+ let _tanstack_react_query = require("@tanstack/react-query");
5
+ let _invect_frontend = require("@invect/frontend");
6
+ let react_jsx_runtime = require("react/jsx-runtime");
7
+ let clsx = require("clsx");
8
+ let tailwind_merge = require("tailwind-merge");
9
+ let zustand = require("zustand");
10
+ //#region src/frontend/providers/RbacProvider.tsx
11
+ /**
12
+ * RBAC Context Provider
13
+ *
14
+ * Wraps the Invect app tree with RBAC state — current user identity,
15
+ * permissions cache, and permission-checking utilities.
16
+ *
17
+ * Fetches GET /plugins/auth/me on mount and caches the result.
18
+ */
19
+ const RbacContext = (0, react.createContext)(null);
20
+ function RbacProvider({ children }) {
21
+ const api = (0, _invect_frontend.useApiClient)();
22
+ const { data: me, isLoading } = (0, _tanstack_react_query.useQuery)({
23
+ queryKey: [
24
+ "rbac",
25
+ "auth",
26
+ "me"
27
+ ],
28
+ queryFn: async () => {
29
+ const response = await fetch(`${api.getBaseURL()}/plugins/auth/me`, { credentials: "include" });
30
+ if (!response.ok) return {
31
+ identity: null,
32
+ permissions: [],
33
+ isAuthenticated: false
34
+ };
35
+ return response.json();
36
+ },
37
+ staleTime: 300 * 1e3,
38
+ retry: 1
39
+ });
40
+ const permissions = (0, react.useMemo)(() => new Set(me?.permissions ?? []), [me?.permissions]);
41
+ const checkPermission = (0, react.useCallback)((permission) => {
42
+ if (!me?.isAuthenticated) return true;
43
+ return permissions.has(permission) || permissions.has("admin:*");
44
+ }, [me?.isAuthenticated, permissions]);
45
+ const value = (0, react.useMemo)(() => ({
46
+ user: me?.identity ?? null,
47
+ isAuthenticated: me?.isAuthenticated ?? false,
48
+ permissions,
49
+ checkPermission,
50
+ isLoading
51
+ }), [
52
+ me,
53
+ permissions,
54
+ checkPermission,
55
+ isLoading
56
+ ]);
57
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RbacContext.Provider, {
58
+ value,
59
+ children
60
+ });
61
+ }
62
+ /**
63
+ * Access RBAC context — current user, permissions, and permission checking.
64
+ *
65
+ * Must be used within an `<RbacProvider>`.
66
+ * Returns a safe fallback (unauthenticated, allow all) if provider is missing.
67
+ */
68
+ function useRbac() {
69
+ const ctx = (0, react.useContext)(RbacContext);
70
+ if (!ctx) return {
71
+ user: null,
72
+ isAuthenticated: false,
73
+ permissions: /* @__PURE__ */ new Set(),
74
+ checkPermission: () => true,
75
+ isLoading: false
76
+ };
77
+ return ctx;
78
+ }
79
+ //#endregion
80
+ //#region src/frontend/hooks/useFlowAccess.ts
81
+ /**
82
+ * useFlowAccess — React Query hooks for flow access records
83
+ */
84
+ /** Fetch access records for a specific flow */
85
+ function useFlowAccess(flowId) {
86
+ const api = (0, _invect_frontend.useApiClient)();
87
+ return (0, _tanstack_react_query.useQuery)({
88
+ queryKey: [
89
+ "rbac",
90
+ "flow-access",
91
+ flowId
92
+ ],
93
+ queryFn: async () => {
94
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/flows/${flowId}/access`, { credentials: "include" });
95
+ if (!response.ok) throw new Error(`Failed to fetch flow access: ${response.status}`);
96
+ return response.json();
97
+ },
98
+ enabled: !!flowId
99
+ });
100
+ }
101
+ /** Fetch all flow IDs accessible to the current user with their effective permission */
102
+ function useAccessibleFlows() {
103
+ const api = (0, _invect_frontend.useApiClient)();
104
+ return (0, _tanstack_react_query.useQuery)({
105
+ queryKey: ["rbac", "accessible-flows"],
106
+ queryFn: async () => {
107
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/flows/accessible`, { credentials: "include" });
108
+ if (!response.ok) {
109
+ const err = await response.json().catch(() => ({}));
110
+ throw new Error(err.error || err.message || `Failed to fetch accessible flows: ${response.status}`);
111
+ }
112
+ return response.json();
113
+ }
114
+ });
115
+ }
116
+ /** Grant access to a flow */
117
+ function useGrantFlowAccess(flowId) {
118
+ const api = (0, _invect_frontend.useApiClient)();
119
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
120
+ return (0, _tanstack_react_query.useMutation)({
121
+ mutationFn: async (input) => {
122
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/flows/${flowId}/access`, {
123
+ method: "POST",
124
+ headers: { "content-type": "application/json" },
125
+ credentials: "include",
126
+ body: JSON.stringify(input)
127
+ });
128
+ if (!response.ok) {
129
+ const err = await response.json().catch(() => ({}));
130
+ throw new Error(err.error || `Failed to grant access: ${response.status}`);
131
+ }
132
+ return response.json();
133
+ },
134
+ onSuccess: () => {
135
+ queryClient.invalidateQueries({ queryKey: [
136
+ "rbac",
137
+ "flow-access",
138
+ flowId
139
+ ] });
140
+ queryClient.invalidateQueries({ queryKey: [
141
+ "rbac",
142
+ "effective-flow-access",
143
+ flowId
144
+ ] });
145
+ }
146
+ });
147
+ }
148
+ /** Revoke a specific access record */
149
+ function useRevokeFlowAccess(flowId) {
150
+ const api = (0, _invect_frontend.useApiClient)();
151
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
152
+ return (0, _tanstack_react_query.useMutation)({
153
+ mutationFn: async (accessId) => {
154
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/flows/${flowId}/access/${accessId}`, {
155
+ method: "DELETE",
156
+ credentials: "include"
157
+ });
158
+ if (!response.ok && response.status !== 204) {
159
+ const err = await response.json().catch(() => ({}));
160
+ throw new Error(err.error || `Failed to revoke access: ${response.status}`);
161
+ }
162
+ },
163
+ onSuccess: () => {
164
+ queryClient.invalidateQueries({ queryKey: [
165
+ "rbac",
166
+ "flow-access",
167
+ flowId
168
+ ] });
169
+ }
170
+ });
171
+ }
172
+ //#endregion
173
+ //#region src/frontend/components/ShareFlowModal.tsx
174
+ /**
175
+ * ShareFlowModal — Modal for managing flow access permissions.
176
+ *
177
+ * Shows current access records and allows granting/revoking access.
178
+ */
179
+ const AVATAR_COLORS = [
180
+ "bg-blue-500/15 text-blue-600 dark:text-blue-400",
181
+ "bg-violet-500/15 text-violet-600 dark:text-violet-400",
182
+ "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400",
183
+ "bg-amber-500/15 text-amber-600 dark:text-amber-400",
184
+ "bg-rose-500/15 text-rose-600 dark:text-rose-400",
185
+ "bg-cyan-500/15 text-cyan-600 dark:text-cyan-400",
186
+ "bg-fuchsia-500/15 text-fuchsia-600 dark:text-fuchsia-400",
187
+ "bg-orange-500/15 text-orange-600 dark:text-orange-400"
188
+ ];
189
+ function getAvatarColor(id) {
190
+ let hash = 0;
191
+ for (let i = 0; i < id.length; i++) hash = (hash << 5) - hash + id.charCodeAt(i) | 0;
192
+ return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
193
+ }
194
+ function getPermissionColor(permission) {
195
+ switch (permission) {
196
+ case "owner": return "border-amber-500/30 bg-amber-500/5 text-amber-600 dark:text-amber-400";
197
+ case "editor": return "border-blue-500/30 bg-blue-500/5 text-blue-600 dark:text-blue-400";
198
+ case "operator": return "border-emerald-500/30 bg-emerald-500/5 text-emerald-600 dark:text-emerald-400";
199
+ default: return "border-imp-border bg-imp-muted/50 text-imp-muted-foreground";
200
+ }
201
+ }
202
+ const PERMISSION_OPTIONS = [
203
+ {
204
+ value: "viewer",
205
+ label: "Viewer"
206
+ },
207
+ {
208
+ value: "operator",
209
+ label: "Operator"
210
+ },
211
+ {
212
+ value: "editor",
213
+ label: "Editor"
214
+ },
215
+ {
216
+ value: "owner",
217
+ label: "Owner"
218
+ }
219
+ ];
220
+ function ShareFlowModal({ flowId, onClose }) {
221
+ const { user } = useRbac();
222
+ const { data, isLoading } = useFlowAccess(flowId);
223
+ const grantAccess = useGrantFlowAccess(flowId);
224
+ const revokeAccess = useRevokeFlowAccess(flowId);
225
+ const [newPrincipal, setNewPrincipal] = (0, react.useState)("");
226
+ const [newPermission, setNewPermission] = (0, react.useState)("viewer");
227
+ const [principalType, setPrincipalType] = (0, react.useState)("user");
228
+ const [error, setError] = (0, react.useState)(null);
229
+ const [openRoleDropdown, setOpenRoleDropdown] = (0, react.useState)(null);
230
+ const accessRecords = data?.access ?? [];
231
+ const handleGrant = async () => {
232
+ if (!newPrincipal.trim()) {
233
+ setError("Please enter a user ID or team ID");
234
+ return;
235
+ }
236
+ setError(null);
237
+ try {
238
+ await grantAccess.mutateAsync({
239
+ ...principalType === "user" ? { userId: newPrincipal } : { teamId: newPrincipal },
240
+ permission: newPermission
241
+ });
242
+ setNewPrincipal("");
243
+ } catch (err) {
244
+ setError(err instanceof Error ? err.message : "Failed to grant access");
245
+ }
246
+ };
247
+ const handleRevoke = async (accessId) => {
248
+ try {
249
+ await revokeAccess.mutateAsync(accessId);
250
+ } catch (err) {
251
+ setError(err instanceof Error ? err.message : "Failed to revoke access");
252
+ }
253
+ };
254
+ const handleChangeRole = async (accessId, record, newRole) => {
255
+ try {
256
+ await grantAccess.mutateAsync({
257
+ ...record.userId ? { userId: record.userId } : record.teamId ? { teamId: record.teamId } : {},
258
+ permission: newRole
259
+ });
260
+ setOpenRoleDropdown(null);
261
+ } catch (err) {
262
+ setError(err instanceof Error ? err.message : "Failed to update role");
263
+ }
264
+ };
265
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
266
+ className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50",
267
+ onClick: onClose,
268
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
269
+ className: "w-full max-w-md rounded-xl border border-imp-border bg-imp-background p-5 shadow-2xl",
270
+ onClick: (e) => e.stopPropagation(),
271
+ children: [
272
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
273
+ className: "mb-4 flex items-center justify-between",
274
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
275
+ className: "text-base font-semibold text-imp-foreground",
276
+ children: "Share Flow"
277
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
278
+ onClick: onClose,
279
+ className: "rounded-lg p-1.5 text-imp-muted-foreground transition-colors hover:bg-imp-muted hover:text-imp-foreground",
280
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "h-4 w-4" })
281
+ })]
282
+ }),
283
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
284
+ className: "mb-3 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600 dark:bg-red-950/20 dark:text-red-400",
285
+ children: error
286
+ }),
287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
288
+ className: "mb-4",
289
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
290
+ className: "mb-2 text-xs font-medium uppercase tracking-wide text-imp-muted-foreground",
291
+ children: "People with access"
292
+ }), isLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
293
+ className: "py-4 text-center text-sm text-imp-muted-foreground",
294
+ children: "Loading..."
295
+ }) : accessRecords.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
296
+ className: "py-4 text-center text-sm text-imp-muted-foreground",
297
+ children: "No flow-specific access records yet."
298
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
299
+ className: "max-h-48 space-y-0.5 overflow-y-auto",
300
+ children: accessRecords.map((record) => {
301
+ const principalId = record.userId ?? record.teamId ?? "?";
302
+ const isCurrentUser = record.userId === user?.id;
303
+ const isRoleDropdownOpen = openRoleDropdown === record.id;
304
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
305
+ className: "flex items-center justify-between rounded-lg px-2.5 py-2 transition-colors hover:bg-imp-muted/30",
306
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
307
+ className: "flex items-center gap-2.5 min-w-0",
308
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
309
+ className: `flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-semibold ${getAvatarColor(principalId)}`,
310
+ children: principalId[0]?.toUpperCase()
311
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
312
+ className: "truncate text-sm font-medium text-imp-foreground",
313
+ children: [principalId, isCurrentUser && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
314
+ className: "ml-1 font-normal text-imp-muted-foreground",
315
+ children: "(you)"
316
+ })]
317
+ })]
318
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
319
+ className: "relative",
320
+ children: isCurrentUser ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
321
+ className: `inline-flex rounded-full border px-2.5 py-0.5 text-xs font-medium capitalize ${getPermissionColor(record.permission)}`,
322
+ children: record.permission
323
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
324
+ type: "button",
325
+ onClick: () => setOpenRoleDropdown(isRoleDropdownOpen ? null : record.id),
326
+ className: `inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium capitalize transition-colors hover:bg-imp-muted/50 ${getPermissionColor(record.permission)}`,
327
+ children: [record.permission, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "h-3 w-3" })]
328
+ }), isRoleDropdownOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
329
+ className: "fixed inset-0 z-10",
330
+ onClick: () => setOpenRoleDropdown(null)
331
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
332
+ className: "absolute right-0 top-full z-20 mt-1 w-36 rounded-lg border border-imp-border bg-imp-background py-1 shadow-lg",
333
+ children: [
334
+ PERMISSION_OPTIONS.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
335
+ type: "button",
336
+ className: `w-full px-3 py-1.5 text-left text-xs ${record.permission === opt.value ? "bg-imp-muted font-medium text-imp-foreground" : "text-imp-foreground hover:bg-imp-muted/50"}`,
337
+ onClick: () => handleChangeRole(record.id, record, opt.value),
338
+ children: opt.label
339
+ }, opt.value)),
340
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "my-1 border-t border-imp-border" }),
341
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
342
+ type: "button",
343
+ className: "w-full px-3 py-1.5 text-left text-xs text-red-600 hover:bg-red-500/10 dark:text-red-400",
344
+ onClick: () => {
345
+ handleRevoke(record.id);
346
+ setOpenRoleDropdown(null);
347
+ },
348
+ children: "Remove access"
349
+ })
350
+ ]
351
+ })] })] })
352
+ })]
353
+ }, record.id);
354
+ })
355
+ })]
356
+ }),
357
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
358
+ className: "border-t border-imp-border pt-4",
359
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
360
+ className: "mb-2.5 text-xs font-medium uppercase tracking-wide text-imp-muted-foreground",
361
+ children: "Add people"
362
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
363
+ className: "flex gap-2",
364
+ children: [
365
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
366
+ type: "button",
367
+ onClick: () => setPrincipalType(principalType === "user" ? "team" : "user"),
368
+ className: "shrink-0 rounded-lg border border-imp-border bg-imp-background px-2.5 py-1.5 text-xs font-medium text-imp-foreground transition-colors hover:bg-imp-muted/50",
369
+ children: principalType === "user" ? "User" : "Team"
370
+ }),
371
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
372
+ type: "text",
373
+ value: newPrincipal,
374
+ onChange: (e) => setNewPrincipal(e.target.value),
375
+ placeholder: principalType === "user" ? "User ID" : "Team ID",
376
+ className: "flex-1 rounded-lg border border-imp-border bg-imp-background px-3 py-1.5 text-sm placeholder:text-imp-muted-foreground focus:border-imp-primary/50 focus:outline-none",
377
+ onKeyDown: (e) => {
378
+ if (e.key === "Enter") handleGrant();
379
+ }
380
+ }),
381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
382
+ type: "button",
383
+ onClick: () => {
384
+ const next = PERMISSION_OPTIONS[(PERMISSION_OPTIONS.findIndex((o) => o.value === newPermission) + 1) % PERMISSION_OPTIONS.length];
385
+ setNewPermission(next.value);
386
+ },
387
+ className: `shrink-0 rounded-lg border px-2.5 py-1.5 text-xs font-medium capitalize transition-colors ${getPermissionColor(newPermission)}`,
388
+ children: newPermission
389
+ }),
390
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
391
+ onClick: handleGrant,
392
+ disabled: grantAccess.isPending,
393
+ className: "shrink-0 rounded-lg bg-imp-primary px-3.5 py-1.5 text-sm font-medium text-imp-primary-foreground transition-colors hover:bg-imp-primary/90 disabled:opacity-50",
394
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Share2, { className: "h-3.5 w-3.5" })
395
+ })
396
+ ]
397
+ })]
398
+ })
399
+ ]
400
+ })
401
+ });
402
+ }
403
+ //#endregion
404
+ //#region src/frontend/components/ShareButton.tsx
405
+ /**
406
+ * ShareButton — Header action component for the flow editor.
407
+ *
408
+ * Renders a "Share" button in the flow header that opens the ShareFlowModal.
409
+ * Registered as a headerAction contribution for the 'flowHeader' context.
410
+ */
411
+ function ShareButton({ flowId }) {
412
+ const [isOpen, setIsOpen] = (0, react.useState)(false);
413
+ const { isAuthenticated, checkPermission } = useRbac();
414
+ const { data } = useAccessibleFlows();
415
+ if (!flowId) return null;
416
+ if ((isAuthenticated && checkPermission("admin:*") ? "owner" : data?.permissions?.[flowId] ?? null) !== "owner") return null;
417
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
418
+ onClick: () => setIsOpen(true),
419
+ className: "inline-flex items-center gap-2 rounded-md border border-imp-border px-3 py-1.5 text-sm font-medium text-imp-foreground transition-colors hover:border-imp-primary/50 hover:bg-imp-muted",
420
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Share2, { className: "h-4 w-4" }), "Share"]
421
+ }), isOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShareFlowModal, {
422
+ flowId,
423
+ onClose: () => setIsOpen(false)
424
+ })] });
425
+ }
426
+ //#endregion
427
+ //#region src/frontend/components/FlowAccessPanel.tsx
428
+ /**
429
+ * FlowAccessPanel — Panel tab for the flow editor.
430
+ *
431
+ * Shows the current access records for the selected flow in a
432
+ * compact list. Registered as a panelTab contribution for the
433
+ * 'flowEditor' context.
434
+ */
435
+ function FlowAccessPanel({ flowId }) {
436
+ const { user } = useRbac();
437
+ const { data, isLoading, error } = useFlowAccess(flowId);
438
+ const accessRecords = data?.access ?? [];
439
+ const myRecord = accessRecords.find((r) => r.userId === user?.id);
440
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
441
+ className: "flex h-full flex-col p-4",
442
+ children: [
443
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
444
+ className: "mb-4 flex items-center justify-between",
445
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
446
+ className: "flex items-center gap-2",
447
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Shield, { className: "h-4 w-4 text-imp-muted-foreground" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
448
+ className: "text-sm font-medium",
449
+ children: "Access Control"
450
+ })]
451
+ }), myRecord && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
452
+ className: "rounded-full border border-imp-border px-2 py-0.5 text-xs font-medium capitalize",
453
+ children: myRecord.permission
454
+ })]
455
+ }),
456
+ isLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
457
+ className: "flex flex-1 items-center justify-center",
458
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
459
+ className: "text-sm text-imp-muted-foreground",
460
+ children: "Loading access records..."
461
+ })
462
+ }) : error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
463
+ className: "flex flex-1 items-center justify-center",
464
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
465
+ className: "text-sm text-red-500",
466
+ children: error instanceof Error ? error.message : "Failed to load access records"
467
+ })
468
+ }) : accessRecords.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
469
+ className: "flex flex-1 flex-col items-center justify-center gap-2",
470
+ children: [
471
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Users, { className: "h-8 w-8 text-imp-muted-foreground/50" }),
472
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
473
+ className: "text-sm text-imp-muted-foreground",
474
+ children: "No access records"
475
+ }),
476
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
477
+ className: "text-xs text-imp-muted-foreground",
478
+ children: "Use the Share button to grant access."
479
+ })
480
+ ]
481
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
482
+ className: "flex-1 space-y-1 overflow-y-auto",
483
+ children: accessRecords.map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
484
+ className: "flex items-center gap-3 rounded-md px-3 py-2 bg-imp-muted/30",
485
+ children: [
486
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
487
+ className: "flex h-7 w-7 items-center justify-center rounded-full bg-imp-primary/10",
488
+ children: record.teamId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Users, { className: "h-3.5 w-3.5 text-imp-primary" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.User, { className: "h-3.5 w-3.5 text-imp-primary" })
489
+ }),
490
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
491
+ className: "flex-1 min-w-0",
492
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
493
+ className: "truncate text-sm",
494
+ children: [record.userId ?? record.teamId, record.userId === user?.id && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
495
+ className: "ml-1 text-imp-muted-foreground",
496
+ children: "(you)"
497
+ })]
498
+ }), record.expiresAt && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
499
+ className: "text-xs text-imp-muted-foreground",
500
+ children: ["Expires ", new Date(record.expiresAt).toLocaleDateString()]
501
+ })]
502
+ }),
503
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
504
+ className: "rounded-full bg-imp-muted px-2 py-0.5 text-xs font-medium capitalize",
505
+ children: record.permission
506
+ })
507
+ ]
508
+ }, record.id))
509
+ }),
510
+ accessRecords.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
511
+ className: "mt-3 border-t border-imp-border pt-3",
512
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
513
+ className: "text-xs text-imp-muted-foreground",
514
+ children: [
515
+ accessRecords.length,
516
+ " ",
517
+ accessRecords.length === 1 ? "person" : "people",
518
+ " with access"
519
+ ]
520
+ })
521
+ })
522
+ ]
523
+ });
524
+ }
525
+ //#endregion
526
+ //#region src/frontend/hooks/useTeams.ts
527
+ /**
528
+ * useTeams — React Query hooks for team management
529
+ */
530
+ /** List all teams */
531
+ function useTeams() {
532
+ const api = (0, _invect_frontend.useApiClient)();
533
+ return (0, _tanstack_react_query.useQuery)({
534
+ queryKey: ["rbac", "teams"],
535
+ queryFn: async () => {
536
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/teams`, { credentials: "include" });
537
+ if (!response.ok) throw new Error(`Failed to fetch teams: ${response.status}`);
538
+ return response.json();
539
+ }
540
+ });
541
+ }
542
+ /** Get a single team with members */
543
+ function useTeam(teamId) {
544
+ const api = (0, _invect_frontend.useApiClient)();
545
+ return (0, _tanstack_react_query.useQuery)({
546
+ queryKey: [
547
+ "rbac",
548
+ "team",
549
+ teamId
550
+ ],
551
+ queryFn: async () => {
552
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/teams/${teamId}`, { credentials: "include" });
553
+ if (!response.ok) throw new Error(`Failed to fetch team: ${response.status}`);
554
+ return response.json();
555
+ },
556
+ enabled: !!teamId
557
+ });
558
+ }
559
+ /** Get current user's teams */
560
+ function useMyTeams() {
561
+ const api = (0, _invect_frontend.useApiClient)();
562
+ return (0, _tanstack_react_query.useQuery)({
563
+ queryKey: ["rbac", "my-teams"],
564
+ queryFn: async () => {
565
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/my-teams`, { credentials: "include" });
566
+ if (!response.ok) throw new Error(`Failed to fetch my teams: ${response.status}`);
567
+ return response.json();
568
+ }
569
+ });
570
+ }
571
+ /** Create a team */
572
+ function useCreateTeam() {
573
+ const api = (0, _invect_frontend.useApiClient)();
574
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
575
+ return (0, _tanstack_react_query.useMutation)({
576
+ mutationFn: async (input) => {
577
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/teams`, {
578
+ method: "POST",
579
+ headers: { "content-type": "application/json" },
580
+ credentials: "include",
581
+ body: JSON.stringify(input)
582
+ });
583
+ if (!response.ok) {
584
+ const err = await response.json().catch(() => ({}));
585
+ throw new Error(err.error || `Failed to create team: ${response.status}`);
586
+ }
587
+ return response.json();
588
+ },
589
+ onSuccess: () => {
590
+ queryClient.invalidateQueries({ queryKey: ["rbac", "teams"] });
591
+ queryClient.invalidateQueries({ queryKey: ["rbac", "scope-tree"] });
592
+ }
593
+ });
594
+ }
595
+ /** Update a team */
596
+ function useUpdateTeam(teamId) {
597
+ const api = (0, _invect_frontend.useApiClient)();
598
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
599
+ return (0, _tanstack_react_query.useMutation)({
600
+ mutationFn: async (input) => {
601
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/teams/${teamId}`, {
602
+ method: "PUT",
603
+ headers: { "content-type": "application/json" },
604
+ credentials: "include",
605
+ body: JSON.stringify(input)
606
+ });
607
+ if (!response.ok) {
608
+ const err = await response.json().catch(() => ({}));
609
+ throw new Error(err.error || `Failed to update team: ${response.status}`);
610
+ }
611
+ return response.json();
612
+ },
613
+ onSuccess: () => {
614
+ queryClient.invalidateQueries({ queryKey: ["rbac", "teams"] });
615
+ queryClient.invalidateQueries({ queryKey: [
616
+ "rbac",
617
+ "team",
618
+ teamId
619
+ ] });
620
+ queryClient.invalidateQueries({ queryKey: ["rbac", "scope-tree"] });
621
+ }
622
+ });
623
+ }
624
+ /** Delete a team */
625
+ function useDeleteTeam() {
626
+ const api = (0, _invect_frontend.useApiClient)();
627
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
628
+ return (0, _tanstack_react_query.useMutation)({
629
+ mutationFn: async (teamId) => {
630
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/teams/${teamId}`, {
631
+ method: "DELETE",
632
+ credentials: "include"
633
+ });
634
+ if (!response.ok && response.status !== 204) {
635
+ const err = await response.json().catch(() => ({}));
636
+ throw new Error(err.error || `Failed to delete team: ${response.status}`);
637
+ }
638
+ },
639
+ onSuccess: () => {
640
+ queryClient.invalidateQueries({ queryKey: ["rbac", "teams"] });
641
+ queryClient.invalidateQueries({ queryKey: ["rbac", "scope-tree"] });
642
+ }
643
+ });
644
+ }
645
+ /** Add a member to a team */
646
+ function useAddTeamMember(teamId) {
647
+ const api = (0, _invect_frontend.useApiClient)();
648
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
649
+ return (0, _tanstack_react_query.useMutation)({
650
+ mutationFn: async (input) => {
651
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/teams/${teamId}/members`, {
652
+ method: "POST",
653
+ headers: { "content-type": "application/json" },
654
+ credentials: "include",
655
+ body: JSON.stringify(input)
656
+ });
657
+ if (!response.ok) {
658
+ const err = await response.json().catch(() => ({}));
659
+ throw new Error(err.error || `Failed to add member: ${response.status}`);
660
+ }
661
+ return response.json();
662
+ },
663
+ onSuccess: () => {
664
+ queryClient.invalidateQueries({ queryKey: [
665
+ "rbac",
666
+ "team",
667
+ teamId
668
+ ] });
669
+ queryClient.invalidateQueries({ queryKey: ["rbac", "teams"] });
670
+ queryClient.invalidateQueries({ queryKey: ["rbac", "scope-tree"] });
671
+ }
672
+ });
673
+ }
674
+ /** Remove a member from a team */
675
+ function useRemoveTeamMember(teamId) {
676
+ const api = (0, _invect_frontend.useApiClient)();
677
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
678
+ return (0, _tanstack_react_query.useMutation)({
679
+ mutationFn: async (userId) => {
680
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/teams/${teamId}/members/${userId}`, {
681
+ method: "DELETE",
682
+ credentials: "include"
683
+ });
684
+ if (!response.ok && response.status !== 204) {
685
+ const err = await response.json().catch(() => ({}));
686
+ throw new Error(err.error || `Failed to remove member: ${response.status}`);
687
+ }
688
+ },
689
+ onSuccess: () => {
690
+ queryClient.invalidateQueries({ queryKey: [
691
+ "rbac",
692
+ "team",
693
+ teamId
694
+ ] });
695
+ queryClient.invalidateQueries({ queryKey: ["rbac", "teams"] });
696
+ queryClient.invalidateQueries({ queryKey: ["rbac", "scope-tree"] });
697
+ }
698
+ });
699
+ }
700
+ //#endregion
701
+ //#region src/frontend/hooks/useScopes.ts
702
+ function useScopeTree() {
703
+ const api = (0, _invect_frontend.useApiClient)();
704
+ return (0, _tanstack_react_query.useQuery)({
705
+ queryKey: ["rbac", "scope-tree"],
706
+ queryFn: async () => {
707
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/scopes/tree`, { credentials: "include" });
708
+ if (!response.ok) {
709
+ const err = await response.json().catch(() => ({}));
710
+ throw new Error(err.error || `Failed to fetch scope tree: ${response.status}`);
711
+ }
712
+ return response.json();
713
+ }
714
+ });
715
+ }
716
+ function useScopeAccess(scopeId) {
717
+ const api = (0, _invect_frontend.useApiClient)();
718
+ return (0, _tanstack_react_query.useQuery)({
719
+ queryKey: [
720
+ "rbac",
721
+ "scope-access",
722
+ scopeId
723
+ ],
724
+ queryFn: async () => {
725
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/scopes/${scopeId}/access`, { credentials: "include" });
726
+ if (!response.ok) {
727
+ const err = await response.json().catch(() => ({}));
728
+ throw new Error(err.error || `Failed to fetch scope access: ${response.status}`);
729
+ }
730
+ return response.json();
731
+ },
732
+ enabled: !!scopeId
733
+ });
734
+ }
735
+ function useGrantScopeAccess(scopeId) {
736
+ const api = (0, _invect_frontend.useApiClient)();
737
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
738
+ return (0, _tanstack_react_query.useMutation)({
739
+ mutationFn: async (input) => {
740
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/scopes/${scopeId}/access`, {
741
+ method: "POST",
742
+ headers: { "content-type": "application/json" },
743
+ credentials: "include",
744
+ body: JSON.stringify(input)
745
+ });
746
+ if (!response.ok) {
747
+ const err = await response.json().catch(() => ({}));
748
+ throw new Error(err.error || `Failed to grant scope access: ${response.status}`);
749
+ }
750
+ return response.json();
751
+ },
752
+ onSuccess: () => {
753
+ queryClient.invalidateQueries({ queryKey: [
754
+ "rbac",
755
+ "scope-access",
756
+ scopeId
757
+ ] });
758
+ queryClient.invalidateQueries({ queryKey: ["rbac", "scope-tree"] });
759
+ }
760
+ });
761
+ }
762
+ function useRevokeScopeAccess(scopeId) {
763
+ const api = (0, _invect_frontend.useApiClient)();
764
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
765
+ return (0, _tanstack_react_query.useMutation)({
766
+ mutationFn: async (accessId) => {
767
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/scopes/${scopeId}/access/${accessId}`, {
768
+ method: "DELETE",
769
+ credentials: "include"
770
+ });
771
+ if (!response.ok && response.status !== 204) {
772
+ const err = await response.json().catch(() => ({}));
773
+ throw new Error(err.error || `Failed to revoke scope access: ${response.status}`);
774
+ }
775
+ },
776
+ onSuccess: () => {
777
+ queryClient.invalidateQueries({ queryKey: [
778
+ "rbac",
779
+ "scope-access",
780
+ scopeId
781
+ ] });
782
+ queryClient.invalidateQueries({ queryKey: ["rbac", "scope-tree"] });
783
+ }
784
+ });
785
+ }
786
+ function useEffectiveFlowAccess(flowId) {
787
+ const api = (0, _invect_frontend.useApiClient)();
788
+ return (0, _tanstack_react_query.useQuery)({
789
+ queryKey: [
790
+ "rbac",
791
+ "effective-flow-access",
792
+ flowId
793
+ ],
794
+ queryFn: async () => {
795
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/flows/${flowId}/effective-access`, { credentials: "include" });
796
+ if (!response.ok) {
797
+ const err = await response.json().catch(() => ({}));
798
+ throw new Error(err.error || `Failed to fetch effective flow access: ${response.status}`);
799
+ }
800
+ return response.json();
801
+ },
802
+ enabled: !!flowId
803
+ });
804
+ }
805
+ function useMoveFlow(flowId) {
806
+ const api = (0, _invect_frontend.useApiClient)();
807
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
808
+ return (0, _tanstack_react_query.useMutation)({
809
+ mutationFn: async (scopeId) => {
810
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/flows/${flowId}/scope`, {
811
+ method: "PUT",
812
+ headers: { "content-type": "application/json" },
813
+ credentials: "include",
814
+ body: JSON.stringify({ scopeId })
815
+ });
816
+ if (!response.ok) {
817
+ const err = await response.json().catch(() => ({}));
818
+ throw new Error(err.error || `Failed to move flow: ${response.status}`);
819
+ }
820
+ return response.json();
821
+ },
822
+ onSuccess: () => {
823
+ queryClient.invalidateQueries({ queryKey: ["rbac", "scope-tree"] });
824
+ queryClient.invalidateQueries({ queryKey: ["rbac", "accessible-flows"] });
825
+ queryClient.invalidateQueries({ queryKey: [
826
+ "rbac",
827
+ "effective-flow-access",
828
+ flowId
829
+ ] });
830
+ }
831
+ });
832
+ }
833
+ function usePreviewMove() {
834
+ const api = (0, _invect_frontend.useApiClient)();
835
+ return (0, _tanstack_react_query.useMutation)({ mutationFn: async (input) => {
836
+ const response = await fetch(`${api.getBaseURL()}/plugins/rbac/preview-move`, {
837
+ method: "POST",
838
+ headers: { "content-type": "application/json" },
839
+ credentials: "include",
840
+ body: JSON.stringify(input)
841
+ });
842
+ if (!response.ok) {
843
+ const err = await response.json().catch(() => ({}));
844
+ throw new Error(err.error || `Failed to preview move: ${response.status}`);
845
+ }
846
+ return response.json();
847
+ } });
848
+ }
849
+ //#endregion
850
+ //#region src/frontend/components/access-control/types.ts
851
+ function formatPermissionLabel(permission) {
852
+ switch (permission) {
853
+ case "owner": return "Owner";
854
+ case "editor": return "Editor";
855
+ case "operator": return "Operator";
856
+ default: return "Viewer";
857
+ }
858
+ }
859
+ function getPermissionBadgeClasses(_permission) {
860
+ return "border-imp-border text-imp-muted-foreground";
861
+ }
862
+ //#endregion
863
+ //#region src/frontend/components/access-control/useUsers.ts
864
+ function useUsers$1() {
865
+ const api = (0, _invect_frontend.useApiClient)();
866
+ const { data } = (0, _tanstack_react_query.useQuery)({
867
+ queryKey: ["rbac", "auth-users"],
868
+ queryFn: async () => {
869
+ const response = await fetch(`${api.getBaseURL()}/plugins/auth/users?limit=200`, { credentials: "include" });
870
+ if (!response.ok) throw new Error(`Failed to fetch users: ${response.status}`);
871
+ return response.json();
872
+ },
873
+ staleTime: 1e3 * 60 * 5,
874
+ gcTime: 1e3 * 60 * 10
875
+ });
876
+ return data?.users ?? [];
877
+ }
878
+ //#endregion
879
+ //#region src/frontend/components/access-control/AccessTable.tsx
880
+ function cn(...inputs) {
881
+ return (0, tailwind_merge.twMerge)((0, clsx.clsx)(inputs));
882
+ }
883
+ const ROLE_OPTIONS = [
884
+ {
885
+ value: "viewer",
886
+ label: "Viewer",
887
+ description: "Can inspect the flow."
888
+ },
889
+ {
890
+ value: "operator",
891
+ label: "Operator",
892
+ description: "Can inspect and run the flow."
893
+ },
894
+ {
895
+ value: "editor",
896
+ label: "Editor",
897
+ description: "Can inspect, run, and edit the flow."
898
+ },
899
+ {
900
+ value: "owner",
901
+ label: "Owner",
902
+ description: "Can edit and manage sharing."
903
+ }
904
+ ];
905
+ function RoleSelector({ value, onChange }) {
906
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuTrigger, {
907
+ asChild: true,
908
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
909
+ type: "button",
910
+ className: cn("inline-flex w-full items-center justify-between gap-1 rounded-md border px-2.5 py-1.5 text-sm font-medium capitalize", getPermissionBadgeClasses(value)),
911
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
912
+ className: "truncate",
913
+ children: value
914
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "h-3.5 w-3.5 shrink-0" })]
915
+ })
916
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.DropdownMenuContent, {
917
+ align: "end",
918
+ className: "w-56",
919
+ children: [
920
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuLabel, {
921
+ className: "text-xs",
922
+ children: "Select role"
923
+ }),
924
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuSeparator, {}),
925
+ ROLE_OPTIONS.map((role) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuItem, {
926
+ onSelect: () => onChange(role.value),
927
+ className: cn("items-start gap-0 px-2 py-2", value === role.value && "bg-accent text-accent-foreground"),
928
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
929
+ className: "min-w-0 text-left",
930
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
931
+ className: "text-sm font-medium",
932
+ children: role.label
933
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
934
+ className: "text-xs text-muted-foreground",
935
+ children: role.description
936
+ })]
937
+ })
938
+ }, role.value))
939
+ ]
940
+ })] });
941
+ }
942
+ function OptionalRoleSelector({ value, onChange, emptyLabel = "No direct access" }) {
943
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuTrigger, {
944
+ asChild: true,
945
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
946
+ type: "button",
947
+ className: cn("inline-flex w-40 items-center justify-between gap-1 rounded-md border px-2.5 py-1.5 text-sm font-medium capitalize", value ? getPermissionBadgeClasses(value) : "border-imp-border text-imp-muted-foreground hover:bg-imp-muted/50"),
948
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
949
+ className: "truncate",
950
+ children: value ?? emptyLabel
951
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "h-3.5 w-3.5 shrink-0" })]
952
+ })
953
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.DropdownMenuContent, {
954
+ align: "end",
955
+ className: "w-56",
956
+ children: [
957
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuLabel, {
958
+ className: "text-xs",
959
+ children: "Set access level"
960
+ }),
961
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuSeparator, {}),
962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuItem, {
963
+ onSelect: () => onChange(null),
964
+ className: cn("px-2 py-2 text-sm", value === null && "bg-accent text-accent-foreground"),
965
+ children: emptyLabel
966
+ }),
967
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuSeparator, {}),
968
+ ROLE_OPTIONS.map((role) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuItem, {
969
+ onSelect: () => onChange(role.value),
970
+ className: cn("items-start gap-0 px-2 py-2", value === role.value && "bg-accent text-accent-foreground"),
971
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
972
+ className: "min-w-0 text-left",
973
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
974
+ className: "text-sm font-medium",
975
+ children: role.label
976
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
977
+ className: "text-xs text-muted-foreground",
978
+ children: role.description
979
+ })]
980
+ })
981
+ }, role.value))
982
+ ]
983
+ })] });
984
+ }
985
+ function AccessTable({ rows, isLoading, emptyLabel }) {
986
+ const [pendingRemovalRow, setPendingRemovalRow] = (0, react.useState)(null);
987
+ const removalDialogCopy = pendingRemovalRow ? pendingRemovalRow.group === "Members" ? {
988
+ title: "Remove team member",
989
+ body: `Remove "${pendingRemovalRow.label}" from team?`,
990
+ confirmLabel: "Remove member"
991
+ } : {
992
+ title: pendingRemovalRow.kind === "team" ? "Revoke team access" : "Revoke user access",
993
+ body: pendingRemovalRow.kind === "team" ? `Remove direct access for ${pendingRemovalRow.label}?` : `Remove direct access for ${pendingRemovalRow.label}?`,
994
+ confirmLabel: "Remove access"
995
+ } : null;
996
+ if (isLoading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
997
+ className: "px-2 py-4 text-sm text-center text-imp-muted-foreground",
998
+ children: "Loading…"
999
+ });
1000
+ if (rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1001
+ className: "px-2 py-4 text-sm text-center text-imp-muted-foreground",
1002
+ children: emptyLabel
1003
+ });
1004
+ const groups = [];
1005
+ for (const row of rows) {
1006
+ const groupLabel = row.group ?? "";
1007
+ const existing = groups.find((g) => g.label === groupLabel);
1008
+ if (existing) existing.rows.push(row);
1009
+ else groups.push({
1010
+ label: groupLabel,
1011
+ rows: [row]
1012
+ });
1013
+ }
1014
+ const hasGroups = groups.length > 1;
1015
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1016
+ className: "w-full text-sm table-fixed",
1017
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("colgroup", { children: [
1018
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", {}),
1019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { className: "w-36" }),
1020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { className: "w-10" })
1021
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [hasGroups && group.label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1022
+ colSpan: 3,
1023
+ className: "px-3 pt-4 pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-imp-muted-foreground",
1024
+ children: group.label
1025
+ }) }), group.rows.map((row, rowIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
1026
+ className: cn("hover:bg-imp-muted/20", rowIndex > 0 && "border-t border-imp-border"),
1027
+ children: [
1028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1029
+ className: "px-3 py-2.5",
1030
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1031
+ className: "flex items-center gap-3",
1032
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1033
+ className: cn("flex h-7 w-7 shrink-0 items-center justify-center rounded-full", row.kind === "user" ? "bg-imp-primary/10" : "bg-imp-muted"),
1034
+ children: row.kind === "user" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.User, { className: "h-3.5 w-3.5 text-imp-primary" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Users, { className: "h-3.5 w-3.5 text-imp-muted-foreground" })
1035
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1036
+ className: "min-w-0",
1037
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1038
+ className: "flex items-center gap-2",
1039
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1040
+ className: "font-medium truncate",
1041
+ children: row.label
1042
+ }), row.kind === "team" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1043
+ className: "shrink-0 rounded border border-imp-border px-1.5 py-0.5 text-[11px] font-medium text-imp-muted-foreground",
1044
+ children: "Team"
1045
+ })]
1046
+ }), row.source && row.source !== "Direct grant" && row.source !== "Member" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1047
+ className: "text-[11px] text-imp-muted-foreground",
1048
+ children: row.source
1049
+ })]
1050
+ })]
1051
+ })
1052
+ }),
1053
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1054
+ className: "px-3 py-2.5 text-center",
1055
+ children: row.permission ? row.onPermissionChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuTrigger, {
1056
+ asChild: true,
1057
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1058
+ type: "button",
1059
+ className: cn("inline-flex w-32 items-center justify-between gap-1 rounded-full border px-2.5 py-1 text-xs font-medium capitalize hover:bg-imp-muted/50", getPermissionBadgeClasses(row.permission)),
1060
+ title: row.source,
1061
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1062
+ className: "truncate",
1063
+ children: row.permission
1064
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "h-3.5 w-3.5 shrink-0" })]
1065
+ })
1066
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.DropdownMenuContent, {
1067
+ align: "end",
1068
+ className: "w-64",
1069
+ children: [
1070
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuLabel, {
1071
+ className: "text-xs",
1072
+ children: "Change access role"
1073
+ }),
1074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuSeparator, {}),
1075
+ ROLE_OPTIONS.map((role) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DropdownMenuItem, {
1076
+ onSelect: () => row.onPermissionChange?.(role.value),
1077
+ className: cn("items-start gap-0 px-2 py-2", row.permission === role.value && "bg-accent text-accent-foreground"),
1078
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1079
+ className: "min-w-0 text-left",
1080
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1081
+ className: "text-sm font-medium",
1082
+ children: role.label
1083
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1084
+ className: "text-xs text-muted-foreground",
1085
+ children: role.description
1086
+ })]
1087
+ })
1088
+ }, role.value))
1089
+ ]
1090
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1091
+ className: cn("inline-flex w-32 justify-center rounded-full border px-2.5 py-1 text-xs font-medium capitalize", getPermissionBadgeClasses(row.permission)),
1092
+ title: row.source,
1093
+ children: row.permission
1094
+ }) : null
1095
+ }),
1096
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1097
+ className: "px-3 py-2.5 text-center",
1098
+ children: row.canRemove && row.onRemove ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1099
+ type: "button",
1100
+ onClick: () => setPendingRemovalRow(row),
1101
+ className: "p-1 rounded text-imp-muted-foreground hover:bg-red-500/10 hover:text-red-500",
1102
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, { className: "h-3.5 w-3.5" })
1103
+ }) : null
1104
+ })
1105
+ ]
1106
+ }, row.id))] }, group.label || "_default")) })]
1107
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.Dialog, {
1108
+ open: !!pendingRemovalRow,
1109
+ onOpenChange: (open) => !open && setPendingRemovalRow(null),
1110
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.DialogContent, {
1111
+ className: "max-w-sm border-imp-border bg-imp-background text-imp-foreground sm:max-w-sm",
1112
+ children: [
1113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DialogHeader, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DialogTitle, {
1114
+ className: "text-sm font-semibold",
1115
+ children: removalDialogCopy?.title
1116
+ }) }),
1117
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1118
+ className: "text-sm text-imp-muted-foreground",
1119
+ children: removalDialogCopy?.body
1120
+ }),
1121
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.DialogFooter, {
1122
+ className: "justify-between w-full sm:gap-2",
1123
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1124
+ type: "button",
1125
+ onClick: () => setPendingRemovalRow(null),
1126
+ className: "rounded-md mr-auto border border-imp-border px-3 py-1.5 text-xs font-medium text-imp-muted-foreground hover:text-imp-foreground",
1127
+ children: "Cancel"
1128
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1129
+ type: "button",
1130
+ onClick: () => {
1131
+ pendingRemovalRow?.onRemove?.();
1132
+ setPendingRemovalRow(null);
1133
+ },
1134
+ className: "rounded-md bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-700",
1135
+ children: removalDialogCopy?.confirmLabel
1136
+ })]
1137
+ })
1138
+ ]
1139
+ })
1140
+ })] });
1141
+ }
1142
+ //#endregion
1143
+ //#region src/frontend/components/access-control/FormDialog.tsx
1144
+ function FormDialog({ open, onClose, title, children }) {
1145
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.Dialog, {
1146
+ open,
1147
+ onOpenChange: (nextOpen) => !nextOpen && onClose(),
1148
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.DialogContent, {
1149
+ className: "max-w-md p-4 border-imp-border bg-imp-background text-imp-foreground sm:max-w-md",
1150
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DialogHeader, {
1151
+ className: "mb-1",
1152
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DialogTitle, {
1153
+ className: "text-sm font-semibold",
1154
+ children: title
1155
+ })
1156
+ }), children]
1157
+ })
1158
+ });
1159
+ }
1160
+ //#endregion
1161
+ //#region src/frontend/components/access-control/MemberCombobox.tsx
1162
+ function MemberCombobox({ users, excludeIds, selectedUserIds, onSelect }) {
1163
+ const [query, setQuery] = (0, react.useState)("");
1164
+ const [open, setOpen] = (0, react.useState)(false);
1165
+ const containerRef = (0, react.useRef)(null);
1166
+ const selectedSet = new Set(selectedUserIds);
1167
+ const filteredUsers = users.filter((user) => {
1168
+ if (excludeIds.has(user.id)) return false;
1169
+ if (selectedSet.has(user.id)) return false;
1170
+ return `${user.name || ""} ${user.email || ""} ${user.id}`.toLowerCase().includes(query.toLowerCase());
1171
+ });
1172
+ const toggle = (userId) => {
1173
+ onSelect(selectedSet.has(userId) ? selectedUserIds.filter((id) => id !== userId) : [...selectedUserIds, userId]);
1174
+ };
1175
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1176
+ ref: containerRef,
1177
+ className: "relative",
1178
+ onBlur: (e) => {
1179
+ if (!containerRef.current?.contains(e.relatedTarget)) setOpen(false);
1180
+ },
1181
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1182
+ className: "flex min-h-[2.5rem] w-full cursor-text flex-wrap gap-1.5 rounded-md border border-imp-border bg-imp-background px-2 py-1.5 focus-within:border-imp-primary/50",
1183
+ onClick: () => setOpen(true),
1184
+ children: [selectedUserIds.map((userId) => {
1185
+ const user = users.find((u) => u.id === userId);
1186
+ const label = user?.name || user?.email || userId;
1187
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1188
+ className: "inline-flex items-center gap-1 rounded-md border border-imp-border bg-imp-muted px-2 py-0.5 text-xs font-medium text-imp-foreground",
1189
+ children: [
1190
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.User, { className: "h-3 w-3 shrink-0 text-imp-muted-foreground" }),
1191
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1192
+ className: "max-w-[140px] truncate",
1193
+ children: label
1194
+ }),
1195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1196
+ type: "button",
1197
+ onMouseDown: (e) => e.preventDefault(),
1198
+ onClick: (e) => {
1199
+ e.stopPropagation();
1200
+ toggle(userId);
1201
+ },
1202
+ className: "ml-0.5 text-imp-muted-foreground hover:text-imp-foreground",
1203
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "h-3 w-3" })
1204
+ })
1205
+ ]
1206
+ }, userId);
1207
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1208
+ value: query,
1209
+ onChange: (e) => {
1210
+ setQuery(e.target.value);
1211
+ setOpen(true);
1212
+ },
1213
+ onFocus: () => setOpen(true),
1214
+ placeholder: selectedUserIds.length === 0 ? "Search users…" : "Add more…",
1215
+ className: "min-w-[120px] flex-1 bg-transparent text-sm outline-none placeholder:text-imp-muted-foreground"
1216
+ })]
1217
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1218
+ className: "absolute left-0 z-20 mt-1 w-full overflow-y-auto rounded-md border border-imp-border bg-imp-background shadow-lg top-full max-h-52",
1219
+ children: filteredUsers.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1220
+ className: "px-3 py-2 text-xs text-imp-muted-foreground",
1221
+ children: query ? "No matches." : "No more users to add."
1222
+ }) : filteredUsers.map((user) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1223
+ type: "button",
1224
+ onMouseDown: (e) => e.preventDefault(),
1225
+ onClick: () => {
1226
+ toggle(user.id);
1227
+ setQuery("");
1228
+ },
1229
+ className: "flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-imp-muted/50",
1230
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.User, { className: "h-4 w-4 shrink-0 text-imp-muted-foreground" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1231
+ className: "min-w-0 flex-1",
1232
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1233
+ className: "truncate font-medium",
1234
+ children: user.name || user.email || user.id
1235
+ }), user.name && user.email && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1236
+ className: "truncate text-xs text-imp-muted-foreground",
1237
+ children: user.email
1238
+ })]
1239
+ })]
1240
+ }, user.id))
1241
+ })]
1242
+ });
1243
+ }
1244
+ //#endregion
1245
+ //#region src/frontend/components/access-control/ScopeDetailPanel.tsx
1246
+ function ScopeDetailPanel({ scopeId, scopeName, users, userMap, teams, isAdmin }) {
1247
+ const scopeQuery = useTeam(scopeId);
1248
+ const scopeAccessQuery = useScopeAccess(scopeId);
1249
+ const grantScopeAccess = useGrantScopeAccess(scopeId);
1250
+ const revokeScopeAccess = useRevokeScopeAccess(scopeId);
1251
+ const addTeamMember = useAddTeamMember(scopeId);
1252
+ const removeTeamMember = useRemoveTeamMember(scopeId);
1253
+ const deleteScope = useDeleteTeam();
1254
+ const [showGrantDialog, setShowGrantDialog] = (0, react.useState)(false);
1255
+ const [showMemberDialog, setShowMemberDialog] = (0, react.useState)(false);
1256
+ const [showDeleteConfirm, setShowDeleteConfirm] = (0, react.useState)(false);
1257
+ const [permission, setPermission] = (0, react.useState)("viewer");
1258
+ const [grantUserIds, setGrantUserIds] = (0, react.useState)([]);
1259
+ const [memberUserIds, setMemberUserIds] = (0, react.useState)([]);
1260
+ const members = scopeQuery.data?.members ?? [];
1261
+ const scopeAccess = scopeAccessQuery.data?.access ?? [];
1262
+ const existingMemberIds = new Set(members.map((member) => member.userId));
1263
+ const existingScopeUserIds = new Set(scopeAccess.map((record) => record.userId).filter(Boolean));
1264
+ const teamRoleRecord = scopeAccess.find((record) => record.teamId === scopeId) ?? null;
1265
+ const childTeams = teams.filter((team) => team.parentId === scopeId);
1266
+ const isLoading = scopeQuery.isLoading || scopeAccessQuery.isLoading;
1267
+ const accessRows = (0, react.useMemo)(() => {
1268
+ const rows = [];
1269
+ for (const member of members) {
1270
+ const user = userMap.get(member.userId);
1271
+ rows.push({
1272
+ id: `member:${member.id}`,
1273
+ label: user?.name || user?.email || member.userId,
1274
+ kind: "user",
1275
+ permission: null,
1276
+ source: "Member",
1277
+ group: "Members",
1278
+ canRemove: isAdmin,
1279
+ onRemove: () => removeTeamMember.mutate(member.userId)
1280
+ });
1281
+ }
1282
+ for (const record of scopeAccess) {
1283
+ if (record.teamId === scopeId) continue;
1284
+ const isUser = !!record.userId;
1285
+ const userId = record.userId ?? null;
1286
+ rows.push({
1287
+ id: `access:${record.id}`,
1288
+ label: isUser ? userMap.get(userId ?? "")?.name || userMap.get(userId ?? "")?.email || userId || "Unknown" : teams.find((team) => team.id === record.teamId)?.name || record.teamId || "Unknown",
1289
+ kind: isUser ? "user" : "team",
1290
+ permission: record.permission,
1291
+ source: "Direct grant",
1292
+ group: "Access Grants",
1293
+ canRemove: isAdmin,
1294
+ onPermissionChange: userId ? (permission) => grantScopeAccess.mutate({
1295
+ userId,
1296
+ permission
1297
+ }) : void 0,
1298
+ onRemove: () => revokeScopeAccess.mutate(record.id)
1299
+ });
1300
+ }
1301
+ return rows;
1302
+ }, [
1303
+ members,
1304
+ scopeAccess,
1305
+ userMap,
1306
+ teams,
1307
+ isAdmin,
1308
+ removeTeamMember,
1309
+ grantScopeAccess,
1310
+ revokeScopeAccess
1311
+ ]);
1312
+ const memberRows = accessRows.filter((row) => row.group === "Members");
1313
+ accessRows.filter((row) => row.group === "Access Grants");
1314
+ const scopePath = (0, react.useMemo)(() => {
1315
+ const path = [];
1316
+ let current = teams.find((team) => team.id === scopeId) ?? null;
1317
+ while (current) {
1318
+ path.unshift(current);
1319
+ current = current.parentId ? teams.find((team) => team.id === current?.parentId) ?? null : null;
1320
+ }
1321
+ return path;
1322
+ }, [teams, scopeId]);
1323
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1324
+ className: "flex flex-col h-full overflow-hidden",
1325
+ children: [
1326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1327
+ className: "px-5 py-4 overflow-hidden border-b shrink-0 border-imp-border",
1328
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1329
+ className: "flex items-start gap-3",
1330
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1331
+ className: "flex items-center justify-center flex-none w-10 h-10 rounded-xl bg-imp-primary/10 text-imp-primary",
1332
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Users, { className: "w-5 h-5" })
1333
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1334
+ className: "flex-1 min-w-0",
1335
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1336
+ className: "flex items-center gap-2",
1337
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1338
+ className: "flex-1 min-w-0 text-base font-semibold truncate",
1339
+ children: scopeName
1340
+ }), isAdmin ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1341
+ className: "flex items-center gap-1.5",
1342
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1343
+ type: "button",
1344
+ onClick: () => setShowDeleteConfirm(true),
1345
+ className: "flex items-center gap-1.5 rounded-md border border-red-200 px-3 py-1.5 text-xs font-medium text-red-600 transition-colors hover:bg-red-500/10 dark:border-red-500/20 dark:text-red-400",
1346
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, { className: "w-4 h-4" }), " Delete team"]
1347
+ })
1348
+ }) : null]
1349
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1350
+ className: "flex flex-wrap items-center gap-1 mt-1 text-[11px] text-imp-muted-foreground",
1351
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Root" }), scopePath.map((team) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1352
+ className: "flex items-center gap-1",
1353
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, { className: "w-3 h-3" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1354
+ className: team.id === scopeId ? "font-medium text-imp-foreground" : void 0,
1355
+ children: team.name
1356
+ })]
1357
+ }, team.id))]
1358
+ })]
1359
+ })]
1360
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1361
+ className: "flex items-center justify-between",
1362
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1363
+ className: "mt-3 text-xs text-imp-muted-foreground",
1364
+ children: "Access applies to all flows inside this team and its child teams."
1365
+ })
1366
+ })]
1367
+ }),
1368
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1369
+ className: "flex-1 px-5 py-4 overflow-x-hidden overflow-y-auto",
1370
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1371
+ className: "space-y-6",
1372
+ children: [
1373
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1374
+ className: "flex items-center justify-between gap-4",
1375
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1376
+ className: "min-w-0",
1377
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1378
+ className: "text-sm font-semibold text-imp-foreground",
1379
+ children: "Team Role on Child Flows"
1380
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1381
+ className: "mt-0.5 text-xs text-imp-muted-foreground",
1382
+ children: "Base role applied to every flow inside this team."
1383
+ })]
1384
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1385
+ className: "shrink-0",
1386
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionalRoleSelector, {
1387
+ value: teamRoleRecord?.permission ?? null,
1388
+ emptyLabel: "No team role",
1389
+ onChange: (nextPermission) => {
1390
+ if (nextPermission === null) {
1391
+ if (teamRoleRecord) revokeScopeAccess.mutate(teamRoleRecord.id);
1392
+ return;
1393
+ }
1394
+ grantScopeAccess.mutate({
1395
+ teamId: scopeId,
1396
+ permission: nextPermission
1397
+ });
1398
+ }
1399
+ })
1400
+ })]
1401
+ }) }),
1402
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1403
+ className: "space-y-2",
1404
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1405
+ className: "flex items-center justify-between",
1406
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1407
+ className: "text-sm font-semibold text-imp-foreground",
1408
+ children: "Members"
1409
+ }), isAdmin ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1410
+ type: "button",
1411
+ onClick: () => setShowMemberDialog(true),
1412
+ className: "flex items-center gap-1.5 rounded-md border border-imp-border px-3 py-1.5 text-xs font-medium text-imp-muted-foreground transition-colors hover:border-imp-primary/50 hover:text-imp-foreground",
1413
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "w-4 h-4" }), " Add Member"]
1414
+ }) : null]
1415
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1416
+ className: "overflow-hidden border rounded-xl border-imp-border bg-imp-background/40",
1417
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessTable, {
1418
+ rows: memberRows,
1419
+ isLoading,
1420
+ emptyLabel: "No members"
1421
+ })
1422
+ })]
1423
+ }),
1424
+ childTeams.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1425
+ className: "space-y-2",
1426
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1427
+ className: "text-sm font-semibold text-imp-foreground",
1428
+ children: "Sub-teams"
1429
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1430
+ className: "space-y-2",
1431
+ children: childTeams.map((team) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1432
+ className: "flex items-center gap-2 px-3 py-2 border rounded-xl border-imp-border bg-imp-background/40",
1433
+ children: [
1434
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Users, { className: "w-3.5 h-3.5 text-imp-muted-foreground" }),
1435
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1436
+ className: "flex-1 min-w-0 text-sm font-medium truncate text-imp-foreground",
1437
+ children: team.name
1438
+ }),
1439
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1440
+ className: "text-[11px] text-imp-muted-foreground",
1441
+ children: "Child team"
1442
+ })
1443
+ ]
1444
+ }, team.id))
1445
+ })]
1446
+ }) : null
1447
+ ]
1448
+ })
1449
+ }),
1450
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormDialog, {
1451
+ open: showGrantDialog,
1452
+ onClose: () => setShowGrantDialog(false),
1453
+ title: "Grant Scope Access",
1454
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1455
+ className: "flex flex-col gap-3",
1456
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemberCombobox, {
1457
+ users,
1458
+ excludeIds: existingScopeUserIds,
1459
+ selectedUserIds: grantUserIds,
1460
+ onSelect: setGrantUserIds
1461
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1462
+ className: "flex items-center gap-2",
1463
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoleSelector, {
1464
+ value: permission,
1465
+ onChange: setPermission
1466
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1467
+ type: "button",
1468
+ onClick: () => {
1469
+ if (grantUserIds.length === 0) return;
1470
+ for (const userId of grantUserIds) grantScopeAccess.mutate({
1471
+ userId,
1472
+ permission
1473
+ });
1474
+ setGrantUserIds([]);
1475
+ setShowGrantDialog(false);
1476
+ },
1477
+ disabled: grantUserIds.length === 0 || grantScopeAccess.isPending,
1478
+ className: "ml-auto rounded-md bg-imp-primary px-4 py-2 text-sm font-semibold text-imp-primary-foreground hover:bg-imp-primary/90 disabled:opacity-50",
1479
+ children: "Grant"
1480
+ })]
1481
+ })]
1482
+ })
1483
+ }),
1484
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormDialog, {
1485
+ open: showMemberDialog,
1486
+ onClose: () => setShowMemberDialog(false),
1487
+ title: "Add Team Member",
1488
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1489
+ className: "flex flex-col gap-3",
1490
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemberCombobox, {
1491
+ users,
1492
+ excludeIds: existingMemberIds,
1493
+ selectedUserIds: memberUserIds,
1494
+ onSelect: setMemberUserIds
1495
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1496
+ type: "button",
1497
+ onClick: () => {
1498
+ if (memberUserIds.length === 0) return;
1499
+ for (const userId of memberUserIds) addTeamMember.mutate({ userId });
1500
+ setMemberUserIds([]);
1501
+ setShowMemberDialog(false);
1502
+ },
1503
+ disabled: memberUserIds.length === 0 || addTeamMember.isPending,
1504
+ className: "rounded-md bg-imp-primary px-4 py-2 text-sm font-semibold text-imp-primary-foreground hover:bg-imp-primary/90 disabled:opacity-50",
1505
+ children: "Add"
1506
+ })]
1507
+ })
1508
+ }),
1509
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.Dialog, {
1510
+ open: showDeleteConfirm,
1511
+ onOpenChange: (open) => !open && setShowDeleteConfirm(false),
1512
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.DialogContent, {
1513
+ className: "max-w-sm border-imp-border bg-imp-background text-imp-foreground sm:max-w-sm",
1514
+ children: [
1515
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DialogHeader, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.DialogTitle, {
1516
+ className: "text-sm font-semibold",
1517
+ children: "Delete team"
1518
+ }) }),
1519
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1520
+ className: "text-sm text-imp-muted-foreground",
1521
+ children: [
1522
+ "Are you sure you want to delete",
1523
+ " ",
1524
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1525
+ className: "text-imp-foreground",
1526
+ children: scopeName
1527
+ }),
1528
+ "? Flows directly inside this team will move to the parent team when one exists, and this team's access grants will be removed."
1529
+ ]
1530
+ }),
1531
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.DialogFooter, {
1532
+ className: "gap-2 sm:gap-0",
1533
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1534
+ type: "button",
1535
+ onClick: () => setShowDeleteConfirm(false),
1536
+ className: "rounded-md border border-imp-border px-3 py-1.5 text-xs font-medium text-imp-muted-foreground hover:text-imp-foreground",
1537
+ children: "Cancel"
1538
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1539
+ type: "button",
1540
+ onClick: () => {
1541
+ deleteScope.mutate(scopeId);
1542
+ setShowDeleteConfirm(false);
1543
+ },
1544
+ className: "rounded-md bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-700",
1545
+ children: "Delete"
1546
+ })]
1547
+ })
1548
+ ]
1549
+ })
1550
+ })
1551
+ ]
1552
+ });
1553
+ }
1554
+ //#endregion
1555
+ //#region src/frontend/components/access-control/PrincipalCombobox.tsx
1556
+ function PrincipalCombobox({ users, teams, excludeUserIds, excludeTeamIds, selections, onSelect }) {
1557
+ const [query, setQuery] = (0, react.useState)("");
1558
+ const [open, setOpen] = (0, react.useState)(false);
1559
+ const containerRef = (0, react.useRef)(null);
1560
+ const selectedUserIds = new Set(selections.filter((s) => s.type === "user").map((s) => s.id));
1561
+ const selectedTeamIds = new Set(selections.filter((s) => s.type === "team").map((s) => s.id));
1562
+ const filteredUsers = users.filter((user) => {
1563
+ if (excludeUserIds.has(user.id)) return false;
1564
+ if (selectedUserIds.has(user.id)) return false;
1565
+ return `${user.name || ""} ${user.email || ""} ${user.id}`.toLowerCase().includes(query.toLowerCase());
1566
+ });
1567
+ const filteredTeams = teams.filter((team) => {
1568
+ if (excludeTeamIds.has(team.id)) return false;
1569
+ if (selectedTeamIds.has(team.id)) return false;
1570
+ return `${team.name} ${team.description || ""}`.toLowerCase().includes(query.toLowerCase());
1571
+ });
1572
+ const toggle = (sel) => {
1573
+ onSelect(selections.some((s) => s.type === sel.type && s.id === sel.id) ? selections.filter((s) => !(s.type === sel.type && s.id === sel.id)) : [...selections, sel]);
1574
+ };
1575
+ const getLabel = (sel) => {
1576
+ if (sel.type === "user") {
1577
+ const user = users.find((u) => u.id === sel.id);
1578
+ return user?.name || user?.email || sel.id;
1579
+ }
1580
+ return teams.find((t) => t.id === sel.id)?.name || sel.id;
1581
+ };
1582
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1583
+ ref: containerRef,
1584
+ className: "relative",
1585
+ onBlur: (e) => {
1586
+ if (!containerRef.current?.contains(e.relatedTarget)) setOpen(false);
1587
+ },
1588
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1589
+ className: "flex min-h-[2.5rem] w-full cursor-text flex-wrap gap-1.5 rounded-md border border-imp-border bg-imp-background px-2 py-1.5 focus-within:border-imp-primary/50",
1590
+ onClick: () => setOpen(true),
1591
+ children: [selections.map((sel) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1592
+ className: "inline-flex items-center gap-1 rounded-md border border-imp-border bg-imp-muted px-2 py-0.5 text-xs font-medium text-imp-foreground",
1593
+ children: [
1594
+ sel.type === "user" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.User, { className: "h-3 w-3 shrink-0 text-imp-muted-foreground" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Users, { className: "h-3 w-3 shrink-0 text-imp-muted-foreground" }),
1595
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1596
+ className: "max-w-[140px] truncate",
1597
+ children: getLabel(sel)
1598
+ }),
1599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1600
+ type: "button",
1601
+ onMouseDown: (e) => e.preventDefault(),
1602
+ onClick: (e) => {
1603
+ e.stopPropagation();
1604
+ toggle(sel);
1605
+ },
1606
+ className: "ml-0.5 text-imp-muted-foreground hover:text-imp-foreground",
1607
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "h-3 w-3" })
1608
+ })
1609
+ ]
1610
+ }, `${sel.type}:${sel.id}`)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1611
+ value: query,
1612
+ onChange: (e) => {
1613
+ setQuery(e.target.value);
1614
+ setOpen(true);
1615
+ },
1616
+ onFocus: () => setOpen(true),
1617
+ placeholder: selections.length === 0 ? "Search users or teams…" : "Add more…",
1618
+ className: "min-w-[120px] flex-1 bg-transparent text-sm outline-none placeholder:text-imp-muted-foreground"
1619
+ })]
1620
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1621
+ className: "absolute left-0 z-20 mt-1 w-full overflow-y-auto rounded-md border border-imp-border bg-imp-background shadow-lg top-full max-h-52",
1622
+ children: filteredTeams.length === 0 && filteredUsers.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1623
+ className: "px-3 py-2 text-xs text-imp-muted-foreground",
1624
+ children: query ? "No matches." : "No more users or teams to add."
1625
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [filteredTeams.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1626
+ className: "px-3 py-1 text-[10px] font-medium uppercase tracking-wider text-imp-muted-foreground",
1627
+ children: "Teams"
1628
+ }), filteredTeams.map((team) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1629
+ type: "button",
1630
+ onMouseDown: (e) => e.preventDefault(),
1631
+ onClick: () => {
1632
+ toggle({
1633
+ type: "team",
1634
+ id: team.id
1635
+ });
1636
+ setQuery("");
1637
+ },
1638
+ className: "flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-imp-muted/50",
1639
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Users, { className: "h-4 w-4 shrink-0 text-imp-muted-foreground" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1640
+ className: "truncate font-medium",
1641
+ children: team.name
1642
+ })]
1643
+ }, team.id))] }), filteredUsers.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1644
+ className: "px-3 py-1 text-[10px] font-medium uppercase tracking-wider text-imp-muted-foreground",
1645
+ children: "Users"
1646
+ }), filteredUsers.map((user) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1647
+ type: "button",
1648
+ onMouseDown: (e) => e.preventDefault(),
1649
+ onClick: () => {
1650
+ toggle({
1651
+ type: "user",
1652
+ id: user.id
1653
+ });
1654
+ setQuery("");
1655
+ },
1656
+ className: "flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-imp-muted/50",
1657
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.User, { className: "h-4 w-4 shrink-0 text-imp-muted-foreground" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1658
+ className: "min-w-0 flex-1",
1659
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1660
+ className: "truncate font-medium",
1661
+ children: user.name || user.email || user.id
1662
+ }), user.name && user.email && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1663
+ className: "truncate text-xs text-imp-muted-foreground",
1664
+ children: user.email
1665
+ })]
1666
+ })]
1667
+ }, user.id))] })] })
1668
+ })]
1669
+ });
1670
+ }
1671
+ //#endregion
1672
+ //#region src/frontend/components/access-control/FlowDetailPanel.tsx
1673
+ function FlowDetailPanel({ flowId, flowName, users, userMap, teams, isAdmin }) {
1674
+ const effectiveFlowAccessQuery = useEffectiveFlowAccess(flowId);
1675
+ const grantFlowAccess = useGrantFlowAccess(flowId);
1676
+ const revokeFlowAccess = useRevokeFlowAccess(flowId);
1677
+ const [showGrantDialog, setShowGrantDialog] = (0, react.useState)(false);
1678
+ const [principalSelections, setPrincipalSelections] = (0, react.useState)([]);
1679
+ const [permission, setPermission] = (0, react.useState)("viewer");
1680
+ const effectiveRecords = effectiveFlowAccessQuery.data?.records ?? [];
1681
+ const existingDirectUserIds = new Set(effectiveRecords.filter((record) => record.source === "direct" && record.userId).map((record) => record.userId));
1682
+ const existingDirectTeamIds = new Set(effectiveRecords.filter((record) => record.source === "direct" && record.teamId).map((record) => record.teamId));
1683
+ const owningTeam = teams.find((team) => team.id === effectiveFlowAccessQuery.data?.scopeId) ?? null;
1684
+ const scopePath = (0, react.useMemo)(() => {
1685
+ const path = [];
1686
+ let current = owningTeam;
1687
+ while (current) {
1688
+ path.unshift(current);
1689
+ current = current.parentId ? teams.find((team) => team.id === current?.parentId) ?? null : null;
1690
+ }
1691
+ return path;
1692
+ }, [owningTeam, teams]);
1693
+ const accessRows = (0, react.useMemo)(() => {
1694
+ const rows = effectiveRecords.map((record) => {
1695
+ const isUser = !!record.userId;
1696
+ const userId = record.userId ?? null;
1697
+ const teamId = record.teamId ?? null;
1698
+ return {
1699
+ id: `${record.source}:${record.id}`,
1700
+ label: isUser ? userMap.get(userId ?? "")?.name || userMap.get(userId ?? "")?.email || userId || "Unknown" : teams.find((team) => team.id === teamId)?.name || teamId || "Unknown",
1701
+ kind: isUser ? "user" : "team",
1702
+ permission: record.permission,
1703
+ source: record.source === "direct" ? "Direct grant" : `via ${record.scopeName || "team"}`,
1704
+ group: record.source === "direct" ? "Direct" : "Inherited",
1705
+ canRemove: record.source === "direct" && isAdmin,
1706
+ onPermissionChange: record.source === "direct" ? (permission) => grantFlowAccess.mutate({
1707
+ ...userId ? { userId } : teamId ? { teamId } : {},
1708
+ permission
1709
+ }) : void 0,
1710
+ onRemove: () => revokeFlowAccess.mutate(record.id)
1711
+ };
1712
+ });
1713
+ rows.sort((a, b) => {
1714
+ const aIsDirect = a.source === "Direct grant" ? 1 : 0;
1715
+ return (b.source === "Direct grant" ? 1 : 0) - aIsDirect;
1716
+ });
1717
+ return rows;
1718
+ }, [
1719
+ effectiveRecords,
1720
+ userMap,
1721
+ teams,
1722
+ isAdmin,
1723
+ grantFlowAccess,
1724
+ revokeFlowAccess
1725
+ ]);
1726
+ const directRows = accessRows.filter((row) => row.group === "Direct");
1727
+ const inheritedRows = accessRows.filter((row) => row.group === "Inherited");
1728
+ const openFlow = () => {
1729
+ const path = window.location.pathname;
1730
+ const accessIdx = path.lastIndexOf("/access");
1731
+ const basePath = accessIdx >= 0 ? path.slice(0, accessIdx) : path;
1732
+ window.open(`${basePath}/flow/${flowId}`, "_blank");
1733
+ };
1734
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1735
+ className: "flex flex-col h-full overflow-hidden",
1736
+ children: [
1737
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1738
+ className: "px-5 py-4 overflow-hidden border-b shrink-0 border-imp-border",
1739
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1740
+ className: "flex items-start gap-3",
1741
+ children: [
1742
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1743
+ className: "flex items-center justify-center flex-none w-10 h-10 rounded-xl bg-imp-primary/10 text-imp-primary",
1744
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Workflow, { className: "w-5 h-5" })
1745
+ }),
1746
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1747
+ className: "flex-1 min-w-0",
1748
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1749
+ className: "flex items-center gap-2",
1750
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1751
+ className: "flex-1 min-w-0 text-base font-semibold truncate",
1752
+ children: flowName
1753
+ })
1754
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1755
+ className: "mt-1 flex flex-wrap items-center gap-1 text-[11px] text-imp-muted-foreground",
1756
+ children: [
1757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Root" }),
1758
+ scopePath.map((team) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1759
+ className: "flex items-center gap-1",
1760
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, { className: "w-3 h-3" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: team.name })]
1761
+ }, team.id)),
1762
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1763
+ className: "flex items-center gap-1",
1764
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, { className: "w-3 h-3" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1765
+ className: "font-medium text-imp-foreground",
1766
+ children: flowName
1767
+ })]
1768
+ })
1769
+ ]
1770
+ })]
1771
+ }),
1772
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1773
+ className: "flex items-center gap-1.5",
1774
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1775
+ type: "button",
1776
+ onClick: openFlow,
1777
+ className: "flex items-center gap-1.5 rounded-md border border-imp-border px-3 py-1.5 text-xs font-medium text-imp-muted-foreground transition-colors hover:border-imp-primary/50 hover:text-imp-foreground",
1778
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ExternalLink, { className: "w-4 h-4" }), " Open in Editor"]
1779
+ })
1780
+ })
1781
+ ]
1782
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1783
+ className: "mt-3 text-xs text-imp-muted-foreground",
1784
+ children: "All principals with access and how they got it."
1785
+ })]
1786
+ }),
1787
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1788
+ className: "flex-1 px-5 py-4 overflow-x-hidden overflow-y-auto",
1789
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1790
+ className: "space-y-6",
1791
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1792
+ className: "space-y-2",
1793
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1794
+ className: "flex items-center justify-between",
1795
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1796
+ className: "text-sm font-semibold text-imp-foreground",
1797
+ children: "Direct Access"
1798
+ }), isAdmin ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1799
+ type: "button",
1800
+ onClick: () => setShowGrantDialog(true),
1801
+ className: "flex items-center gap-1.5 rounded-md border border-imp-border px-3 py-1.5 text-xs font-medium text-imp-muted-foreground transition-colors hover:border-imp-primary/50 hover:text-imp-foreground",
1802
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "w-4 h-4" }), " Grant Access"]
1803
+ }) : null]
1804
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1805
+ className: "overflow-hidden border rounded-xl border-imp-border bg-imp-background/40",
1806
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessTable, {
1807
+ rows: directRows,
1808
+ isLoading: effectiveFlowAccessQuery.isLoading,
1809
+ emptyLabel: "No direct access assigned"
1810
+ })
1811
+ })]
1812
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1813
+ className: "space-y-2",
1814
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1815
+ className: "text-sm font-semibold text-imp-foreground",
1816
+ children: "Inherited Access"
1817
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1818
+ className: "overflow-hidden border rounded-xl border-imp-border bg-imp-background/40",
1819
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessTable, {
1820
+ rows: inheritedRows,
1821
+ isLoading: effectiveFlowAccessQuery.isLoading,
1822
+ emptyLabel: "No inherited access"
1823
+ })
1824
+ })]
1825
+ })]
1826
+ })
1827
+ }),
1828
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormDialog, {
1829
+ open: showGrantDialog,
1830
+ onClose: () => setShowGrantDialog(false),
1831
+ title: "Grant Flow Access",
1832
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1833
+ className: "flex flex-col gap-3",
1834
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PrincipalCombobox, {
1835
+ users,
1836
+ teams,
1837
+ excludeUserIds: existingDirectUserIds,
1838
+ excludeTeamIds: existingDirectTeamIds,
1839
+ selections: principalSelections,
1840
+ onSelect: setPrincipalSelections
1841
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1842
+ className: "flex items-center gap-2",
1843
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoleSelector, {
1844
+ value: permission,
1845
+ onChange: setPermission
1846
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1847
+ type: "button",
1848
+ onClick: () => {
1849
+ if (principalSelections.length === 0) return;
1850
+ for (const sel of principalSelections) grantFlowAccess.mutate({
1851
+ ...sel.type === "user" ? { userId: sel.id } : { teamId: sel.id },
1852
+ permission
1853
+ });
1854
+ setPrincipalSelections([]);
1855
+ setShowGrantDialog(false);
1856
+ },
1857
+ disabled: principalSelections.length === 0 || grantFlowAccess.isPending,
1858
+ className: "ml-auto rounded-md bg-imp-primary px-4 py-2 text-sm font-semibold text-imp-primary-foreground hover:bg-imp-primary/90 disabled:opacity-50",
1859
+ children: "Grant"
1860
+ })]
1861
+ })]
1862
+ })
1863
+ })
1864
+ ]
1865
+ });
1866
+ }
1867
+ //#endregion
1868
+ //#region src/frontend/components/access-control/MoveConfirmationDialog.tsx
1869
+ function MoveConfirmationDialog({ pendingMove, preview, error, onClose }) {
1870
+ const moveFlow = useMoveFlow(pendingMove?.type === "flow" ? pendingMove.id : "");
1871
+ const reparentScope = useUpdateTeam(pendingMove?.type === "scope" ? pendingMove.id : "");
1872
+ const isSubmitting = moveFlow.isPending || reparentScope.isPending;
1873
+ const handleConfirm = async () => {
1874
+ if (!pendingMove) return;
1875
+ if (pendingMove.type === "flow") await moveFlow.mutateAsync(pendingMove.targetScopeId);
1876
+ else await reparentScope.mutateAsync({ parentId: pendingMove.targetScopeId });
1877
+ onClose();
1878
+ };
1879
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1880
+ className: "fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/35",
1881
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1882
+ className: "w-full max-w-lg rounded-xl border border-imp-border bg-imp-background shadow-[var(--imp-shadow-floating)]",
1883
+ children: [
1884
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1885
+ className: "flex items-center justify-between px-5 py-4 border-b border-imp-border",
1886
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1887
+ className: "flex items-center gap-2 text-sm font-semibold text-imp-foreground",
1888
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Move, { className: "w-4 h-4 text-imp-primary" }), "Confirm Move"]
1889
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1890
+ className: "mt-1 text-xs text-imp-muted-foreground",
1891
+ children: "Review the access impact before applying this hierarchy change."
1892
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1893
+ type: "button",
1894
+ onClick: onClose,
1895
+ className: "p-1 rounded text-imp-muted-foreground hover:text-imp-foreground",
1896
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "w-4 h-4" })
1897
+ })]
1898
+ }),
1899
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1900
+ className: "px-5 py-4 space-y-4 text-sm",
1901
+ children: error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1902
+ className: "px-3 py-2 text-red-700 border border-red-300 rounded-lg bg-red-50 dark:border-red-900 dark:bg-red-950/20 dark:text-red-300",
1903
+ children: error
1904
+ }) : !preview ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1905
+ className: "px-3 py-4 text-center border rounded-lg border-imp-border bg-imp-card text-imp-muted-foreground",
1906
+ children: "Calculating impact…"
1907
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1908
+ className: "px-3 py-3 border rounded-lg border-imp-border bg-imp-card",
1909
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1910
+ className: "font-medium text-imp-foreground",
1911
+ children: [
1912
+ "Move \"",
1913
+ preview.item.name,
1914
+ "\" to",
1915
+ " ",
1916
+ preview.target.path.length > 0 ? preview.target.path.join(" / ") : "Top level"
1917
+ ]
1918
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1919
+ className: "mt-1 text-xs text-imp-muted-foreground",
1920
+ children: [
1921
+ preview.affectedFlows,
1922
+ " flow",
1923
+ preview.affectedFlows === 1 ? "" : "s",
1924
+ " affected"
1925
+ ]
1926
+ })]
1927
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1928
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1929
+ className: "mb-2 text-xs font-medium tracking-wider uppercase text-imp-muted-foreground",
1930
+ children: "Access Changes"
1931
+ }),
1932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1933
+ className: "p-2 space-y-1 overflow-y-auto border rounded-lg max-h-56 border-imp-border bg-imp-card",
1934
+ children: preview.accessChanges.gained.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1935
+ className: "px-2 py-2 text-xs text-imp-muted-foreground",
1936
+ children: "No new principals gain access from this move."
1937
+ }) : preview.accessChanges.gained.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1938
+ className: "flex items-center gap-2 rounded-md px-2 py-1.5 text-xs",
1939
+ children: [
1940
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1941
+ className: "flex items-center justify-center w-6 h-6 rounded-full shrink-0 bg-imp-primary/10",
1942
+ children: entry.userId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.User, { className: "w-3 h-3 text-imp-primary" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Users, { className: "w-3 h-3 text-imp-primary" })
1943
+ }),
1944
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1945
+ className: "flex-1 min-w-0",
1946
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1947
+ className: "font-medium truncate",
1948
+ children: entry.name
1949
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1950
+ className: "truncate text-[10px] text-imp-muted-foreground",
1951
+ children: entry.source
1952
+ })]
1953
+ }),
1954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1955
+ className: `rounded px-1.5 py-0.5 text-[10px] font-medium ${getPermissionBadgeClasses(entry.permission)}`,
1956
+ children: entry.permission
1957
+ })
1958
+ ]
1959
+ }, `${entry.source}-${entry.name}`))
1960
+ }),
1961
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1962
+ className: "mt-2 text-xs text-imp-muted-foreground",
1963
+ children: [
1964
+ preview.accessChanges.unchanged,
1965
+ " access grant",
1966
+ preview.accessChanges.unchanged === 1 ? "" : "s",
1967
+ " remain unchanged."
1968
+ ]
1969
+ })
1970
+ ] })] })
1971
+ }),
1972
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1973
+ className: "flex items-center justify-end gap-2 px-5 py-4 border-t border-imp-border",
1974
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1975
+ type: "button",
1976
+ onClick: onClose,
1977
+ className: "rounded border border-imp-border px-3 py-1.5 text-sm text-imp-muted-foreground hover:text-imp-foreground",
1978
+ children: "Cancel"
1979
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1980
+ type: "button",
1981
+ onClick: handleConfirm,
1982
+ disabled: !preview || isSubmitting || !!error,
1983
+ className: "rounded bg-imp-primary px-3 py-1.5 text-sm font-medium text-imp-primary-foreground hover:bg-imp-primary/90 disabled:opacity-50",
1984
+ children: isSubmitting ? "Applying…" : `Move ${pendingMove?.type === "scope" ? "Team" : "Flow"}`
1985
+ })]
1986
+ })
1987
+ ]
1988
+ })
1989
+ });
1990
+ }
1991
+ //#endregion
1992
+ //#region src/frontend/stores/accessControlStore.ts
1993
+ const useAccessControlStore = (0, zustand.create)()((set) => ({
1994
+ selected: null,
1995
+ setSelected: (item) => set({ selected: item }),
1996
+ expandedNodes: /* @__PURE__ */ new Set(),
1997
+ setExpandedNodes: (nodes) => set({ expandedNodes: nodes }),
1998
+ toggleNode: (scopeId) => set((state) => {
1999
+ const next = new Set(state.expandedNodes);
2000
+ if (next.has(scopeId)) next.delete(scopeId);
2001
+ else next.add(scopeId);
2002
+ return { expandedNodes: next };
2003
+ }),
2004
+ expandAll: (ids) => set({ expandedNodes: new Set(ids) }),
2005
+ search: "",
2006
+ setSearch: (q) => set({ search: q }),
2007
+ showNewTeam: false,
2008
+ setShowNewTeam: (show) => set({ showNewTeam: show }),
2009
+ newTeamName: "",
2010
+ setNewTeamName: (name) => set({ newTeamName: name }),
2011
+ draggedItem: null,
2012
+ dropTarget: null,
2013
+ startDrag: (item) => set({
2014
+ draggedItem: item,
2015
+ dropTarget: null
2016
+ }),
2017
+ endDrag: () => set({
2018
+ draggedItem: null,
2019
+ dropTarget: null
2020
+ }),
2021
+ setDropTarget: (targetId) => set({ dropTarget: targetId }),
2022
+ pendingMove: null,
2023
+ movePreview: null,
2024
+ moveError: null,
2025
+ setPendingMove: (move) => set({ pendingMove: move }),
2026
+ setMovePreview: (preview) => set({ movePreview: preview }),
2027
+ setMoveError: (error) => set({ moveError: error }),
2028
+ clearMove: () => set({
2029
+ pendingMove: null,
2030
+ movePreview: null,
2031
+ moveError: null
2032
+ })
2033
+ }));
2034
+ function collectScopeIds(scopes) {
2035
+ return scopes.flatMap((scope) => [scope.id, ...collectScopeIds(scope.children)]);
2036
+ }
2037
+ function findScopeNode$1(scopes, scopeId) {
2038
+ for (const scope of scopes) {
2039
+ if (scope.id === scopeId) return scope;
2040
+ const child = findScopeNode$1(scope.children, scopeId);
2041
+ if (child) return child;
2042
+ }
2043
+ return null;
2044
+ }
2045
+ function scopeContains(scope, potentialChildId) {
2046
+ for (const child of scope.children) if (child.id === potentialChildId || scopeContains(child, potentialChildId)) return true;
2047
+ return false;
2048
+ }
2049
+ function isDescendantScope(scopes, draggedScopeId, potentialTargetId) {
2050
+ const draggedScope = findScopeNode$1(scopes, draggedScopeId);
2051
+ if (!draggedScope) return false;
2052
+ return scopeContains(draggedScope, potentialTargetId);
2053
+ }
2054
+ /**
2055
+ * Returns true if dropping `dragged` onto `targetId` is a valid move.
2056
+ * targetId of null means "move to root".
2057
+ */
2058
+ function canDropOnTarget(dragged, targetId, allScopes) {
2059
+ if (dragged.type === "scope" && dragged.id === targetId) return false;
2060
+ if (dragged.type === "scope" && targetId !== null && isDescendantScope(allScopes, dragged.id, targetId)) return false;
2061
+ if (dragged.parentId === targetId) return false;
2062
+ return true;
2063
+ }
2064
+ //#endregion
2065
+ //#region src/frontend/components/access-control/AccessControlPage.tsx
2066
+ function findScopeNode(scopes, scopeId) {
2067
+ for (const scope of scopes) {
2068
+ if (scope.id === scopeId) return scope;
2069
+ const child = findScopeNode(scope.children, scopeId);
2070
+ if (child) return child;
2071
+ }
2072
+ return null;
2073
+ }
2074
+ function filterScopes(scopes, query) {
2075
+ return scopes.reduce((acc, scope) => {
2076
+ const matchesScope = scope.name.toLowerCase().includes(query) || (scope.description?.toLowerCase().includes(query) ?? false);
2077
+ const matchingChildren = filterScopes(scope.children, query);
2078
+ const matchingFlows = scope.flows.filter((flow) => flow.name.toLowerCase().includes(query));
2079
+ if (matchesScope || matchingChildren.length > 0 || matchingFlows.length > 0) acc.push({
2080
+ ...scope,
2081
+ children: matchingChildren,
2082
+ flows: matchingFlows
2083
+ });
2084
+ return acc;
2085
+ }, []);
2086
+ }
2087
+ function findSelectedName(scopes, flows, selected) {
2088
+ if (!selected) return null;
2089
+ if (selected.kind === "team") return findScopeNode(scopes, selected.id)?.name ?? selected.name;
2090
+ const stack = [...flows];
2091
+ const scopeStack = [...scopes];
2092
+ while (scopeStack.length > 0) {
2093
+ const current = scopeStack.pop();
2094
+ if (!current) break;
2095
+ stack.push(...current.flows);
2096
+ scopeStack.push(...current.children);
2097
+ }
2098
+ return stack.find((flow) => flow.id === selected.id)?.name ?? selected.name;
2099
+ }
2100
+ function FlowTreeRow({ flow, isSelected, onSelect }) {
2101
+ const { draggedItem, startDrag, endDrag } = useAccessControlStore();
2102
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2103
+ draggable: true,
2104
+ onDragStart: (event) => {
2105
+ event.dataTransfer.effectAllowed = "move";
2106
+ startDrag({
2107
+ type: "flow",
2108
+ id: flow.id,
2109
+ name: flow.name,
2110
+ parentId: flow.scopeId ?? null
2111
+ });
2112
+ },
2113
+ onDragEnd: endDrag,
2114
+ onClick: () => onSelect(flow),
2115
+ className: (0, clsx.clsx)("group flex cursor-pointer items-center gap-2 rounded-md px-4 py-1.5 transition-colors", "hover:bg-imp-muted/50", draggedItem?.type === "flow" && draggedItem.id === flow.id && "opacity-40", isSelected && "bg-imp-primary/10 text-imp-primary ring-1 ring-imp-primary/30"),
2116
+ children: [
2117
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Workflow, { className: (0, clsx.clsx)("h-4 w-4 shrink-0", isSelected ? "text-imp-primary" : "text-imp-muted-foreground") }),
2118
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2119
+ className: "flex-1 min-w-0 text-sm font-medium truncate",
2120
+ children: flow.name
2121
+ }),
2122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.GripVertical, { className: "h-3.5 w-3.5 shrink-0 text-imp-muted-foreground/60 opacity-0 transition-opacity group-hover:opacity-100" })
2123
+ ]
2124
+ });
2125
+ }
2126
+ function ScopeTreeRow({ scope, allScopes, depth, onSelectScope, onSelectFlow, onTriggerMove }) {
2127
+ const { draggedItem, dropTarget, selected, expandedNodes, startDrag, endDrag, setDropTarget, toggleNode } = useAccessControlStore();
2128
+ const isExpanded = expandedNodes.has(scope.id);
2129
+ const isSelected = selected?.kind === "team" && selected.id === scope.id;
2130
+ const isDragging = draggedItem?.type === "scope" && draggedItem.id === scope.id;
2131
+ const hasChildren = scope.children.length > 0 || scope.flows.length > 0;
2132
+ const canDrop = draggedItem !== null && canDropOnTarget(draggedItem, scope.id, allScopes);
2133
+ const isDropTarget = dropTarget === scope.id && canDrop;
2134
+ const handleDragOver = (event) => {
2135
+ if (!draggedItem || !canDrop) return;
2136
+ event.preventDefault();
2137
+ event.stopPropagation();
2138
+ if (dropTarget !== scope.id) setDropTarget(scope.id);
2139
+ };
2140
+ const handleDragLeave = (event) => {
2141
+ const related = event.relatedTarget;
2142
+ if (related && event.currentTarget.contains(related)) return;
2143
+ if (dropTarget === scope.id) setDropTarget(null);
2144
+ };
2145
+ const handleDrop = (event) => {
2146
+ event.preventDefault();
2147
+ event.stopPropagation();
2148
+ if (!draggedItem || !canDrop) return;
2149
+ onTriggerMove(draggedItem, scope.id);
2150
+ };
2151
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2152
+ onDragOver: handleDragOver,
2153
+ onDragLeave: handleDragLeave,
2154
+ onDrop: handleDrop,
2155
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2156
+ draggable: true,
2157
+ onDragStart: (event) => {
2158
+ event.dataTransfer.effectAllowed = "move";
2159
+ startDrag({
2160
+ type: "scope",
2161
+ id: scope.id,
2162
+ name: scope.name,
2163
+ parentId: scope.parentId ?? null
2164
+ });
2165
+ },
2166
+ onDragEnd: endDrag,
2167
+ onClick: () => onSelectScope(scope),
2168
+ className: (0, clsx.clsx)("group flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2 transition-colors", isDragging && "opacity-40", isDropTarget && "ring-2 ring-imp-primary/40 bg-imp-primary/5", isSelected ? "text-imp-primary ring-1 ring-imp-primary/30" : "hover:bg-accent"),
2169
+ children: [
2170
+ hasChildren ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2171
+ type: "button",
2172
+ onClick: (event) => {
2173
+ event.stopPropagation();
2174
+ toggleNode(scope.id);
2175
+ },
2176
+ className: "rounded p-0.5 text-imp-muted-foreground hover:bg-imp-muted",
2177
+ children: isExpanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "w-4 h-4" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, { className: "w-4 h-4" })
2178
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-5 shrink-0" }),
2179
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Users, { className: (0, clsx.clsx)("h-4 w-4 shrink-0", isSelected ? "text-imp-primary" : "text-imp-muted-foreground") }),
2180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2181
+ className: "flex-1 min-w-0 text-sm font-medium truncate",
2182
+ children: scope.name
2183
+ }),
2184
+ scope.teamPermission ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2185
+ className: (0, clsx.clsx)("inline-flex shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium", getPermissionBadgeClasses(scope.teamPermission)),
2186
+ children: formatPermissionLabel(scope.teamPermission)
2187
+ }) : null,
2188
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.GripVertical, { className: "h-3.5 w-3.5 shrink-0 text-imp-muted-foreground/60 opacity-0 transition-opacity group-hover:opacity-100" })
2189
+ ]
2190
+ }), isExpanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2191
+ className: "space-y-1 border-l border-imp-border",
2192
+ style: { marginLeft: "22px" },
2193
+ children: [scope.flows.map((flow) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FlowTreeRow, {
2194
+ flow,
2195
+ isSelected: selected?.kind === "flow" && selected.id === flow.id,
2196
+ onSelect: onSelectFlow
2197
+ }, flow.id)), scope.children.map((child) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScopeTreeRow, {
2198
+ scope: child,
2199
+ allScopes,
2200
+ depth: depth + 1,
2201
+ onSelectScope,
2202
+ onSelectFlow,
2203
+ onTriggerMove
2204
+ }, child.id))]
2205
+ }) : null]
2206
+ });
2207
+ }
2208
+ const EMPTY_SCOPES = [];
2209
+ const EMPTY_FLOWS = [];
2210
+ function AccessControlPage() {
2211
+ const { isAuthenticated, checkPermission } = useRbac();
2212
+ const isAdmin = isAuthenticated && checkPermission("admin:*");
2213
+ const users = useUsers$1();
2214
+ const userMap = (0, react.useMemo)(() => {
2215
+ const map = /* @__PURE__ */ new Map();
2216
+ for (const user of users) map.set(user.id, user);
2217
+ return map;
2218
+ }, [users]);
2219
+ const teams = useTeams().data?.teams ?? [];
2220
+ const scopeTreeQuery = useScopeTree();
2221
+ const createTeam = useCreateTeam();
2222
+ const previewMove = usePreviewMove();
2223
+ const { selected, setSelected, search, setSearch, expandedNodes, expandAll, showNewTeam, setShowNewTeam, newTeamName, setNewTeamName, draggedItem, dropTarget, setDropTarget, endDrag, pendingMove, setPendingMove, movePreview, setMovePreview, moveError, setMoveError, clearMove } = useAccessControlStore();
2224
+ const scopes = scopeTreeQuery.data?.scopes ?? EMPTY_SCOPES;
2225
+ const unscopedFlows = scopeTreeQuery.data?.unscopedFlows ?? EMPTY_FLOWS;
2226
+ (0, react.useEffect)(() => {
2227
+ if (scopes.length > 0 && expandedNodes.size === 0) expandAll(collectScopeIds(scopes));
2228
+ }, [scopes]);
2229
+ (0, react.useEffect)(() => {
2230
+ scopeTreeQuery.refetch();
2231
+ }, []);
2232
+ const normalizedSearch = search.trim().toLowerCase();
2233
+ const filteredScopes = (0, react.useMemo)(() => normalizedSearch ? filterScopes(scopes, normalizedSearch) : scopes, [scopes, normalizedSearch]);
2234
+ const filteredUnscopedFlows = (0, react.useMemo)(() => normalizedSearch ? unscopedFlows.filter((f) => f.name.toLowerCase().includes(normalizedSearch)) : unscopedFlows, [unscopedFlows, normalizedSearch]);
2235
+ const selectedName = (0, react.useMemo)(() => findSelectedName(scopes, unscopedFlows, selected), [
2236
+ scopes,
2237
+ unscopedFlows,
2238
+ selected
2239
+ ]);
2240
+ const hasTreeItems = filteredScopes.length > 0 || filteredUnscopedFlows.length > 0;
2241
+ const handleTriggerMove = (0, react.useCallback)(async (item, targetScopeId) => {
2242
+ endDrag();
2243
+ if (!canDropOnTarget(item, targetScopeId, scopes)) return;
2244
+ setPendingMove({
2245
+ type: item.type,
2246
+ id: item.id,
2247
+ name: item.name,
2248
+ targetScopeId
2249
+ });
2250
+ setMovePreview(null);
2251
+ setMoveError(null);
2252
+ try {
2253
+ setMovePreview(await previewMove.mutateAsync({
2254
+ type: item.type,
2255
+ id: item.id,
2256
+ targetScopeId
2257
+ }));
2258
+ } catch (err) {
2259
+ setMoveError(err instanceof Error ? err.message : "Failed to preview move");
2260
+ }
2261
+ }, [
2262
+ scopes,
2263
+ endDrag,
2264
+ previewMove,
2265
+ setPendingMove,
2266
+ setMovePreview,
2267
+ setMoveError
2268
+ ]);
2269
+ if (!isAuthenticated) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_invect_frontend.PageLayout, {
2270
+ title: "Access Control",
2271
+ subtitle: "Manage team hierarchy and flow-level access grants.",
2272
+ icon: lucide_react.Shield,
2273
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2274
+ className: "text-sm text-imp-muted-foreground",
2275
+ children: "Please sign in to access this page."
2276
+ })
2277
+ });
2278
+ const isLoading = scopeTreeQuery.isLoading;
2279
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.PageLayout, {
2280
+ title: "Access Control",
2281
+ subtitle: "Manage team hierarchy and flow-level access grants.",
2282
+ icon: lucide_react.Shield,
2283
+ children: [
2284
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2285
+ className: "flex items-center gap-2",
2286
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2287
+ className: "relative flex-1 max-w-sm",
2288
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Search, { className: "absolute w-3.5 h-3.5 pointer-events-none left-3 top-1/2 -translate-y-1/2 text-imp-muted-foreground" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2289
+ value: search,
2290
+ onChange: (event) => setSearch(event.target.value),
2291
+ placeholder: "Search teams and flows…",
2292
+ className: "w-full py-2 pr-3 text-sm border rounded-lg outline-none pl-9 border-imp-border bg-imp-background placeholder:text-imp-muted-foreground focus:border-imp-primary/50"
2293
+ })]
2294
+ }), isAdmin ? showNewTeam ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2295
+ className: "flex items-center gap-1.5",
2296
+ children: [
2297
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2298
+ value: newTeamName,
2299
+ onChange: (event) => setNewTeamName(event.target.value),
2300
+ placeholder: "Team name",
2301
+ className: "px-2.5 py-2 text-sm border rounded-lg border-imp-border bg-imp-background",
2302
+ autoFocus: true,
2303
+ onKeyDown: (event) => {
2304
+ if (event.key === "Enter" && newTeamName.trim()) {
2305
+ createTeam.mutate({ name: newTeamName.trim() });
2306
+ setNewTeamName("");
2307
+ setShowNewTeam(false);
2308
+ }
2309
+ if (event.key === "Escape") {
2310
+ setShowNewTeam(false);
2311
+ setNewTeamName("");
2312
+ }
2313
+ }
2314
+ }),
2315
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2316
+ type: "button",
2317
+ onClick: () => {
2318
+ if (!newTeamName.trim()) return;
2319
+ createTeam.mutate({ name: newTeamName.trim() });
2320
+ setNewTeamName("");
2321
+ setShowNewTeam(false);
2322
+ },
2323
+ disabled: !newTeamName.trim(),
2324
+ className: "px-3 py-2 text-sm font-medium rounded-lg bg-imp-primary text-imp-primary-foreground hover:bg-imp-primary/90 disabled:opacity-50",
2325
+ children: "Create"
2326
+ }),
2327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2328
+ type: "button",
2329
+ onClick: () => {
2330
+ setShowNewTeam(false);
2331
+ setNewTeamName("");
2332
+ },
2333
+ className: "px-3 py-2 text-sm rounded-lg text-imp-muted-foreground hover:text-imp-foreground",
2334
+ children: "Cancel"
2335
+ })
2336
+ ]
2337
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2338
+ type: "button",
2339
+ onClick: () => setShowNewTeam(true),
2340
+ className: "flex items-center gap-1.5 rounded-lg border border-imp-border px-3 py-2 text-sm font-medium text-imp-muted-foreground hover:border-imp-primary/50 hover:text-imp-foreground",
2341
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "w-4 h-4" }), " New Team"]
2342
+ }) : null]
2343
+ }),
2344
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2345
+ className: "grid h-[calc(100vh-220px)] min-h-130 border rounded-lg overflow-hidden",
2346
+ style: { gridTemplateColumns: "320px 1fr" },
2347
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2348
+ className: "flex flex-col overflow-hidden bg-imp-muted/20",
2349
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2350
+ className: "flex-1 px-1 py-2 overflow-y-auto",
2351
+ onDragOver: (event) => {
2352
+ if (!draggedItem) return;
2353
+ if (dropTarget === null) {
2354
+ event.preventDefault();
2355
+ setDropTarget("root");
2356
+ }
2357
+ },
2358
+ onDragLeave: (event) => {
2359
+ const related = event.relatedTarget;
2360
+ if (related && event.currentTarget.contains(related)) return;
2361
+ if (dropTarget === "root") setDropTarget(null);
2362
+ },
2363
+ onDrop: (event) => {
2364
+ event.preventDefault();
2365
+ if (!draggedItem) return;
2366
+ handleTriggerMove(draggedItem, null);
2367
+ },
2368
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2369
+ className: (0, clsx.clsx)("min-h-full rounded-lg py-1 transition-colors", dropTarget === "root" ? "bg-imp-primary/5 ring-1 ring-imp-primary/20 ring-inset" : ""),
2370
+ children: isLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2371
+ className: "flex items-center justify-center px-4 py-12 text-sm text-center text-imp-muted-foreground",
2372
+ children: "Loading hierarchy…"
2373
+ }) : !hasTreeItems ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2374
+ className: "flex flex-col items-center justify-center px-6 py-16 text-center",
2375
+ children: [
2376
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FolderInput, { className: "w-10 h-10 mb-3 opacity-30 text-imp-muted-foreground" }),
2377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2378
+ className: "text-base font-medium text-imp-foreground",
2379
+ children: normalizedSearch ? "No matching teams or flows" : "No teams or flows yet"
2380
+ }),
2381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2382
+ className: "max-w-sm mt-2 text-sm text-imp-muted-foreground",
2383
+ children: normalizedSearch ? "Adjust the search to find a team or flow in the hierarchy." : "Create a team to start organizing flows and access scopes."
2384
+ })
2385
+ ]
2386
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2387
+ className: "space-y-1",
2388
+ children: [filteredUnscopedFlows.map((flow) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FlowTreeRow, {
2389
+ flow,
2390
+ isSelected: selected?.kind === "flow" && selected.id === flow.id,
2391
+ onSelect: (nextFlow) => setSelected({
2392
+ kind: "flow",
2393
+ id: nextFlow.id,
2394
+ name: nextFlow.name
2395
+ })
2396
+ }, flow.id)), filteredScopes.map((scope) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScopeTreeRow, {
2397
+ scope,
2398
+ allScopes: scopes,
2399
+ depth: 0,
2400
+ onSelectScope: (nextScope) => setSelected({
2401
+ kind: "team",
2402
+ id: nextScope.id,
2403
+ name: nextScope.name
2404
+ }),
2405
+ onSelectFlow: (nextFlow) => setSelected({
2406
+ kind: "flow",
2407
+ id: nextFlow.id,
2408
+ name: nextFlow.name
2409
+ }),
2410
+ onTriggerMove: handleTriggerMove
2411
+ }, scope.id))]
2412
+ })
2413
+ })
2414
+ })
2415
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
2416
+ className: "flex flex-col overflow-hidden border-l border-imp-border bg-imp-muted/30",
2417
+ children: !selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2418
+ className: "flex flex-col items-center justify-center h-full gap-4 px-8 text-center text-imp-muted-foreground",
2419
+ children: [
2420
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2421
+ className: "flex items-center justify-center w-12 h-12 rounded-xl bg-imp-primary/5",
2422
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Shield, { className: "w-6 h-6 text-imp-primary/40" })
2423
+ }),
2424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2425
+ className: "text-sm font-medium text-imp-foreground",
2426
+ children: "Select a team or flow"
2427
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2428
+ className: "mt-1.5 text-xs text-imp-muted-foreground max-w-[280px]",
2429
+ children: "Choose an item from the tree to manage its members and permissions."
2430
+ })] }),
2431
+ hasTreeItems ? null : isAdmin ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2432
+ type: "button",
2433
+ onClick: () => setShowNewTeam(true),
2434
+ className: "flex items-center gap-1.5 rounded-lg border border-imp-border px-3 py-1.5 text-xs font-medium text-imp-foreground transition-colors hover:border-imp-primary/50 hover:bg-imp-muted/50",
2435
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "h-3.5 w-3.5" }), " Create your first team"]
2436
+ }) : null
2437
+ ]
2438
+ }) : selected.kind === "team" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScopeDetailPanel, {
2439
+ scopeId: selected.id,
2440
+ scopeName: selectedName ?? selected.name,
2441
+ users,
2442
+ userMap,
2443
+ teams,
2444
+ isAdmin: !!isAdmin
2445
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FlowDetailPanel, {
2446
+ flowId: selected.id,
2447
+ flowName: selectedName ?? selected.name,
2448
+ users,
2449
+ userMap,
2450
+ teams,
2451
+ isAdmin: !!isAdmin
2452
+ })
2453
+ })]
2454
+ }),
2455
+ pendingMove ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MoveConfirmationDialog, {
2456
+ pendingMove,
2457
+ preview: movePreview,
2458
+ error: moveError,
2459
+ onClose: clearMove
2460
+ }) : null
2461
+ ]
2462
+ });
2463
+ }
2464
+ //#endregion
2465
+ //#region src/frontend/components/TeamsPage.tsx
2466
+ /**
2467
+ * TeamsPage — Team management page with create/edit/delete teams and member management.
2468
+ */
2469
+ function useUsers() {
2470
+ const api = (0, _invect_frontend.useApiClient)();
2471
+ const [users, setUsers] = (0, react.useState)([]);
2472
+ (0, react.useEffect)(() => {
2473
+ let cancelled = false;
2474
+ fetch(`${api.getBaseURL()}/plugins/auth/users?limit=200`, { credentials: "include" }).then((r) => r.ok ? r.json() : null).then((data) => {
2475
+ if (!cancelled && data?.users) setUsers(data.users);
2476
+ }).catch(() => {});
2477
+ return () => {
2478
+ cancelled = true;
2479
+ };
2480
+ }, [api]);
2481
+ return { users };
2482
+ }
2483
+ function TeamsPage() {
2484
+ const { isAuthenticated, checkPermission } = useRbac();
2485
+ const teamsQuery = useTeams();
2486
+ const createTeam = useCreateTeam();
2487
+ const deleteTeam = useDeleteTeam();
2488
+ const { users } = useUsers();
2489
+ const [expandedTeamId, setExpandedTeamId] = (0, react.useState)(null);
2490
+ const [teamSearch, setTeamSearch] = (0, react.useState)("");
2491
+ const [showCreateForm, setShowCreateForm] = (0, react.useState)(false);
2492
+ const [newTeamName, setNewTeamName] = (0, react.useState)("");
2493
+ const [newTeamDescription, setNewTeamDescription] = (0, react.useState)("");
2494
+ const teams = teamsQuery.data?.teams ?? [];
2495
+ const isAdmin = isAuthenticated && checkPermission("admin:*");
2496
+ const userMap = (0, react.useMemo)(() => {
2497
+ const map = /* @__PURE__ */ new Map();
2498
+ for (const u of users) map.set(u.id, u);
2499
+ return map;
2500
+ }, [users]);
2501
+ const filteredTeams = (0, react.useMemo)(() => {
2502
+ const q = teamSearch.trim().toLowerCase();
2503
+ if (!q) return teams;
2504
+ return teams.filter((t) => t.name.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q));
2505
+ }, [teams, teamSearch]);
2506
+ const handleCreate = async () => {
2507
+ if (!newTeamName.trim()) return;
2508
+ await createTeam.mutateAsync({
2509
+ name: newTeamName.trim(),
2510
+ description: newTeamDescription.trim() || void 0
2511
+ });
2512
+ setNewTeamName("");
2513
+ setNewTeamDescription("");
2514
+ setShowCreateForm(false);
2515
+ };
2516
+ const handleDelete = async (teamId) => {
2517
+ await deleteTeam.mutateAsync(teamId);
2518
+ if (expandedTeamId === teamId) setExpandedTeamId(null);
2519
+ };
2520
+ if (!isAuthenticated) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2521
+ className: "flex items-center justify-center w-full h-full min-h-0 overflow-y-auto imp-page bg-imp-background text-imp-foreground",
2522
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2523
+ className: "text-sm text-imp-muted-foreground",
2524
+ children: "Please sign in to access this page."
2525
+ })
2526
+ });
2527
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_invect_frontend.PageLayout, {
2528
+ title: "Teams",
2529
+ icon: lucide_react.Users,
2530
+ actions: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2531
+ className: "flex items-center gap-2",
2532
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2533
+ className: "text-xs text-imp-muted-foreground",
2534
+ children: [
2535
+ filteredTeams.length,
2536
+ " team",
2537
+ filteredTeams.length !== 1 ? "s" : ""
2538
+ ]
2539
+ }), isAdmin && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2540
+ type: "button",
2541
+ onClick: () => setShowCreateForm(!showCreateForm),
2542
+ className: "flex items-center gap-1 px-2 py-1 text-xs font-medium rounded bg-imp-primary text-imp-primary-foreground hover:bg-imp-primary/90",
2543
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "w-3 h-3" }), "New Team"]
2544
+ })]
2545
+ }),
2546
+ children: [
2547
+ showCreateForm && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2548
+ className: "p-3 border rounded-lg border-imp-border bg-imp-card",
2549
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2550
+ className: "mb-2 text-xs font-medium text-imp-foreground",
2551
+ children: "Create Team"
2552
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2553
+ className: "space-y-2",
2554
+ children: [
2555
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2556
+ type: "text",
2557
+ value: newTeamName,
2558
+ onChange: (e) => setNewTeamName(e.target.value),
2559
+ placeholder: "Team name",
2560
+ className: "w-full rounded border border-imp-border bg-imp-background px-2 py-1.5 text-sm placeholder:text-imp-muted-foreground",
2561
+ onKeyDown: (e) => {
2562
+ if (e.key === "Enter") handleCreate();
2563
+ }
2564
+ }),
2565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2566
+ type: "text",
2567
+ value: newTeamDescription,
2568
+ onChange: (e) => setNewTeamDescription(e.target.value),
2569
+ placeholder: "Description (optional)",
2570
+ className: "w-full rounded border border-imp-border bg-imp-background px-2 py-1.5 text-sm placeholder:text-imp-muted-foreground",
2571
+ onKeyDown: (e) => {
2572
+ if (e.key === "Enter") handleCreate();
2573
+ }
2574
+ }),
2575
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2576
+ className: "flex items-center gap-2",
2577
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2578
+ type: "button",
2579
+ onClick: handleCreate,
2580
+ disabled: createTeam.isPending || !newTeamName.trim(),
2581
+ className: "px-3 py-1 text-xs font-medium rounded bg-imp-primary text-imp-primary-foreground hover:bg-imp-primary/90 disabled:opacity-50",
2582
+ children: createTeam.isPending ? "Creating…" : "Create"
2583
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2584
+ type: "button",
2585
+ onClick: () => setShowCreateForm(false),
2586
+ className: "px-3 py-1 text-xs rounded text-imp-muted-foreground hover:text-imp-foreground",
2587
+ children: "Cancel"
2588
+ })]
2589
+ })
2590
+ ]
2591
+ })]
2592
+ }),
2593
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2594
+ className: "relative",
2595
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Search, { className: "pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-imp-muted-foreground" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2596
+ value: teamSearch,
2597
+ onChange: (e) => setTeamSearch(e.target.value),
2598
+ placeholder: "Search teams…",
2599
+ className: "w-full rounded-lg border border-imp-border bg-transparent py-2 pl-9 pr-3 text-sm outline-none placeholder:text-imp-muted-foreground focus:border-imp-primary/50"
2600
+ })]
2601
+ }),
2602
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2603
+ className: "border rounded-lg border-imp-border bg-imp-card",
2604
+ children: teamsQuery.isLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2605
+ className: "px-4 py-8 text-sm text-center text-imp-muted-foreground",
2606
+ children: "Loading…"
2607
+ }) : teamsQuery.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2608
+ className: "px-4 py-8 text-sm text-center text-red-500",
2609
+ children: teamsQuery.error instanceof Error ? teamsQuery.error.message : "Failed to load teams"
2610
+ }) : filteredTeams.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2611
+ className: "px-4 py-8 text-sm text-center text-imp-muted-foreground",
2612
+ children: teamSearch ? "No teams match this search." : "No teams yet. Create one to get started."
2613
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2614
+ className: "divide-y divide-imp-border",
2615
+ children: filteredTeams.map((team) => {
2616
+ const isExpanded = expandedTeamId === team.id;
2617
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2618
+ className: "flex items-center gap-3 px-3 py-2",
2619
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2620
+ type: "button",
2621
+ className: "flex items-center flex-1 gap-3 text-left transition-opacity hover:opacity-80",
2622
+ onClick: () => setExpandedTeamId(isExpanded ? null : team.id),
2623
+ children: [
2624
+ isExpanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-imp-muted-foreground" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, { className: "h-3.5 w-3.5 shrink-0 text-imp-muted-foreground" }),
2625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Users, { className: "h-3.5 w-3.5 shrink-0 text-imp-primary" }),
2626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2627
+ className: "flex-1 min-w-0",
2628
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2629
+ className: "block text-sm font-medium truncate",
2630
+ children: team.name
2631
+ }), team.description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2632
+ className: "block text-xs truncate text-imp-muted-foreground",
2633
+ children: team.description
2634
+ })]
2635
+ })
2636
+ ]
2637
+ }), isAdmin && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2638
+ type: "button",
2639
+ onClick: () => handleDelete(team.id),
2640
+ className: "p-1 rounded shrink-0 text-imp-muted-foreground hover:text-red-500",
2641
+ title: "Delete team",
2642
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, { className: "h-3.5 w-3.5" })
2643
+ })]
2644
+ }), isExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TeamMembersPanel, {
2645
+ teamId: team.id,
2646
+ users,
2647
+ userMap,
2648
+ isAdmin: !!isAdmin
2649
+ })] }, team.id);
2650
+ })
2651
+ })
2652
+ })
2653
+ ]
2654
+ });
2655
+ }
2656
+ function TeamMembersPanel({ teamId, users, userMap, isAdmin }) {
2657
+ const { data, isLoading } = useTeam(teamId);
2658
+ const addMember = useAddTeamMember(teamId);
2659
+ const removeMember = useRemoveTeamMember(teamId);
2660
+ const members = data?.members ?? [];
2661
+ const existingUserIds = new Set(members.map((m) => m.userId));
2662
+ const getUserLabel = (userId) => {
2663
+ const u = userMap.get(userId);
2664
+ return u?.name || u?.email || userId;
2665
+ };
2666
+ const [selectedUserId, setSelectedUserId] = (0, react.useState)(null);
2667
+ const handleAdd = async () => {
2668
+ if (!selectedUserId) return;
2669
+ await addMember.mutateAsync({ userId: selectedUserId });
2670
+ setSelectedUserId(null);
2671
+ };
2672
+ const handleRemove = async (userId) => {
2673
+ await removeMember.mutateAsync(userId);
2674
+ };
2675
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2676
+ className: "px-3 pt-2 pb-3 border-t border-imp-border bg-imp-muted/20",
2677
+ children: [isLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2678
+ className: "py-3 text-xs text-center text-imp-muted-foreground",
2679
+ children: "Loading…"
2680
+ }) : members.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2681
+ className: "py-2 text-xs text-imp-muted-foreground",
2682
+ children: "No members yet."
2683
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2684
+ className: "mb-2 space-y-1",
2685
+ children: members.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2686
+ className: "flex items-center gap-2 px-2 py-1 text-xs rounded",
2687
+ children: [
2688
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2689
+ className: "flex items-center justify-center w-5 h-5 rounded-full shrink-0 bg-imp-primary/10",
2690
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.User, { className: "w-3 h-3 text-imp-primary" })
2691
+ }),
2692
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2693
+ className: "flex-1 min-w-0 truncate",
2694
+ children: getUserLabel(member.userId)
2695
+ }),
2696
+ isAdmin && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2697
+ type: "button",
2698
+ onClick: () => handleRemove(member.userId),
2699
+ className: "shrink-0 rounded p-0.5 text-imp-muted-foreground hover:text-red-500",
2700
+ title: "Remove member",
2701
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, { className: "w-3 h-3" })
2702
+ })
2703
+ ]
2704
+ }, member.id))
2705
+ }), isAdmin && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2706
+ className: "flex items-center gap-1.5",
2707
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemberSearchCombobox, {
2708
+ users,
2709
+ excludeIds: existingUserIds,
2710
+ selectedUserId,
2711
+ onSelect: setSelectedUserId
2712
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2713
+ type: "button",
2714
+ onClick: handleAdd,
2715
+ disabled: addMember.isPending || !selectedUserId,
2716
+ className: "shrink-0 rounded bg-imp-primary px-2.5 py-1 text-xs font-medium text-imp-primary-foreground hover:bg-imp-primary/90 disabled:opacity-50",
2717
+ children: addMember.isPending ? "…" : "Add"
2718
+ })]
2719
+ })]
2720
+ });
2721
+ }
2722
+ function MemberSearchCombobox({ users, excludeIds, selectedUserId, onSelect }) {
2723
+ const [open, setOpen] = (0, react.useState)(false);
2724
+ const [query, setQuery] = (0, react.useState)("");
2725
+ const containerRef = (0, react.useRef)(null);
2726
+ const inputRef = (0, react.useRef)(null);
2727
+ const selectedUser = selectedUserId ? users.find((u) => u.id === selectedUserId) : null;
2728
+ const filtered = (0, react.useMemo)(() => {
2729
+ const q = query.toLowerCase();
2730
+ return users.filter((u) => {
2731
+ if (excludeIds.has(u.id)) return false;
2732
+ if (!q) return true;
2733
+ return u.name?.toLowerCase().includes(q) || u.email?.toLowerCase().includes(q) || u.id.toLowerCase().includes(q);
2734
+ });
2735
+ }, [
2736
+ users,
2737
+ excludeIds,
2738
+ query
2739
+ ]);
2740
+ (0, react.useEffect)(() => {
2741
+ const handleClick = (e) => {
2742
+ if (containerRef.current && !containerRef.current.contains(e.target)) setOpen(false);
2743
+ };
2744
+ if (open) {
2745
+ document.addEventListener("mousedown", handleClick);
2746
+ return () => document.removeEventListener("mousedown", handleClick);
2747
+ }
2748
+ }, [open]);
2749
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2750
+ ref: containerRef,
2751
+ className: "relative flex-1 min-w-0",
2752
+ children: [selectedUser ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2753
+ type: "button",
2754
+ onClick: () => {
2755
+ onSelect(null);
2756
+ setQuery("");
2757
+ },
2758
+ className: "flex w-full items-center gap-1.5 rounded border border-imp-border bg-imp-background px-2 py-1 text-xs text-left",
2759
+ children: [
2760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.User, { className: "w-3 h-3 shrink-0 text-imp-muted-foreground" }),
2761
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2762
+ className: "flex-1 min-w-0 truncate",
2763
+ children: selectedUser.name || selectedUser.email || selectedUser.id
2764
+ }),
2765
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "w-3 h-3 shrink-0 text-imp-muted-foreground" })
2766
+ ]
2767
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2768
+ className: "relative",
2769
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Search, { className: "absolute w-3 h-3 -translate-y-1/2 pointer-events-none left-2 top-1/2 text-imp-muted-foreground" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2770
+ ref: inputRef,
2771
+ type: "text",
2772
+ value: query,
2773
+ onChange: (e) => {
2774
+ setQuery(e.target.value);
2775
+ if (!open) setOpen(true);
2776
+ },
2777
+ onFocus: () => setOpen(true),
2778
+ placeholder: "Search users…",
2779
+ className: "w-full py-1 pl-6 pr-2 text-xs border rounded border-imp-border bg-imp-background placeholder:text-imp-muted-foreground"
2780
+ })]
2781
+ }), open && !selectedUser && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2782
+ className: "absolute left-0 z-50 w-full mt-1 border rounded-md shadow-lg top-full border-imp-border bg-imp-background",
2783
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2784
+ className: "py-1 overflow-y-auto max-h-40",
2785
+ children: filtered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2786
+ className: "px-3 py-2 text-xs text-imp-muted-foreground",
2787
+ children: "No users found."
2788
+ }) : filtered.map((u) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2789
+ type: "button",
2790
+ onClick: () => {
2791
+ onSelect(u.id);
2792
+ setQuery("");
2793
+ setOpen(false);
2794
+ },
2795
+ className: "flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs hover:bg-imp-muted/50 transition-colors",
2796
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.User, { className: "h-3.5 w-3.5 shrink-0 text-imp-muted-foreground" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2797
+ className: "flex-1 min-w-0 truncate",
2798
+ children: u.name || u.email || u.id
2799
+ })]
2800
+ }, u.id))
2801
+ })
2802
+ })]
2803
+ });
2804
+ }
2805
+ //#endregion
2806
+ //#region src/frontend/components/UserMenuSection.tsx
2807
+ /**
2808
+ * UserMenuSection — Sidebar footer component showing current user info.
2809
+ *
2810
+ * Displays the authenticated user's name and role in the sidebar footer area.
2811
+ * This is a standalone component — not directly injected via plugin system
2812
+ * but available for host apps to use in custom sidebar layouts.
2813
+ */
2814
+ function UserMenuSection({ collapsed = false }) {
2815
+ const { user, isAuthenticated, isLoading } = useRbac();
2816
+ if (isLoading || !isAuthenticated || !user) return null;
2817
+ const initials = (user.name ?? user.id)[0]?.toUpperCase() ?? "?";
2818
+ if (collapsed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2819
+ className: "flex justify-center px-2 py-2",
2820
+ title: `${user.name ?? user.id} (${user.role ?? "user"})`,
2821
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2822
+ className: "flex h-8 w-8 items-center justify-center rounded-full bg-imp-primary/10 text-xs font-medium text-imp-primary",
2823
+ children: initials
2824
+ })
2825
+ });
2826
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2827
+ className: "flex items-center gap-3 px-3 py-2",
2828
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2829
+ className: "flex h-8 w-8 items-center justify-center rounded-full bg-imp-primary/10 text-xs font-medium text-imp-primary",
2830
+ children: initials
2831
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2832
+ className: "min-w-0 flex-1",
2833
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2834
+ className: "truncate text-sm font-medium",
2835
+ children: user.name ?? user.id
2836
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2837
+ className: "truncate text-xs text-imp-muted-foreground capitalize",
2838
+ children: user.role ?? "user"
2839
+ })]
2840
+ })]
2841
+ });
2842
+ }
2843
+ function UserAvatar({ className }) {
2844
+ const { user, isAuthenticated } = useRbac();
2845
+ if (!isAuthenticated || !user) return null;
2846
+ const initials = (user.name ?? user.id)[0]?.toUpperCase() ?? "?";
2847
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2848
+ className: `flex items-center justify-center rounded-full bg-imp-primary/10 text-xs font-medium text-imp-primary ${className ?? "h-6 w-6"}`,
2849
+ title: user.name ?? user.id,
2850
+ children: initials
2851
+ });
2852
+ }
2853
+ //#endregion
2854
+ //#region src/frontend/index.ts
2855
+ /**
2856
+ * @invect/rbac/ui — Frontend Plugin Entry Point
2857
+ *
2858
+ * This is the browser-safe entry point that exports the RBAC frontend plugin.
2859
+ * Import via: `import { rbacFrontendPlugin } from '@invect/rbac/ui'`
2860
+ *
2861
+ * No Node.js dependencies. No @invect/core runtime imports.
2862
+ */
2863
+ const rbacFrontendPlugin = {
2864
+ id: "rbac",
2865
+ name: "Role-Based Access Control",
2866
+ providers: [RbacProvider],
2867
+ sidebar: [{
2868
+ label: "Access Control",
2869
+ icon: lucide_react.Shield,
2870
+ path: "/access",
2871
+ position: "top",
2872
+ permission: "flow:read"
2873
+ }],
2874
+ routes: [{
2875
+ path: "/access",
2876
+ component: AccessControlPage
2877
+ }],
2878
+ panelTabs: [{
2879
+ context: "flowEditor",
2880
+ label: "Access",
2881
+ icon: lucide_react.Shield,
2882
+ component: FlowAccessPanel,
2883
+ permission: "flow:read"
2884
+ }],
2885
+ headerActions: [{
2886
+ context: "flowHeader",
2887
+ component: ShareButton,
2888
+ permission: "flow:read"
2889
+ }],
2890
+ components: {
2891
+ "rbac.AccessControlPage": AccessControlPage,
2892
+ "rbac.FlowAccessPanel": FlowAccessPanel,
2893
+ "rbac.ShareButton": ShareButton
2894
+ },
2895
+ checkPermission: (_permission, _context) => {}
2896
+ };
2897
+ //#endregion
2898
+ exports.AccessControlPage = AccessControlPage;
2899
+ exports.FlowAccessPanel = FlowAccessPanel;
2900
+ exports.RbacProvider = RbacProvider;
2901
+ exports.ShareButton = ShareButton;
2902
+ exports.ShareFlowModal = ShareFlowModal;
2903
+ exports.TeamsPage = TeamsPage;
2904
+ exports.UserAvatar = UserAvatar;
2905
+ exports.UserMenuSection = UserMenuSection;
2906
+ exports.rbacFrontendPlugin = rbacFrontendPlugin;
2907
+ exports.useAccessibleFlows = useAccessibleFlows;
2908
+ exports.useAddTeamMember = useAddTeamMember;
2909
+ exports.useCreateTeam = useCreateTeam;
2910
+ exports.useDeleteTeam = useDeleteTeam;
2911
+ exports.useEffectiveFlowAccess = useEffectiveFlowAccess;
2912
+ exports.useFlowAccess = useFlowAccess;
2913
+ exports.useGrantFlowAccess = useGrantFlowAccess;
2914
+ exports.useGrantScopeAccess = useGrantScopeAccess;
2915
+ exports.useMoveFlow = useMoveFlow;
2916
+ exports.useMyTeams = useMyTeams;
2917
+ exports.usePreviewMove = usePreviewMove;
2918
+ exports.useRbac = useRbac;
2919
+ exports.useRemoveTeamMember = useRemoveTeamMember;
2920
+ exports.useRevokeFlowAccess = useRevokeFlowAccess;
2921
+ exports.useRevokeScopeAccess = useRevokeScopeAccess;
2922
+ exports.useScopeAccess = useScopeAccess;
2923
+ exports.useScopeTree = useScopeTree;
2924
+ exports.useTeam = useTeam;
2925
+ exports.useTeams = useTeams;
2926
+ exports.useUpdateTeam = useUpdateTeam;
2927
+
2928
+ //# sourceMappingURL=index.cjs.map