@carlonicora/nextjs-jsonapi 1.77.3 → 1.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AssistantInterface-BYgI5z1-.d.mts +12 -0
- package/dist/AssistantInterface-DfDcz0gJ.d.ts +12 -0
- package/dist/AssistantMessageInterface-DWnbd6J7.d.ts +36 -0
- package/dist/AssistantMessageInterface-Mla6kgPe.d.mts +36 -0
- package/dist/{AuthComponent-Blbs06ud.d.ts → AuthComponent-B6DIk8Vf.d.ts} +1 -1
- package/dist/{AuthComponent-huIaK5rm.d.mts → AuthComponent-BKI0ZbtD.d.mts} +1 -1
- package/dist/{BlockNoteEditor-7HAAXN3H.mjs → BlockNoteEditor-6CBDTVKV.mjs} +4 -4
- package/dist/{BlockNoteEditor-UB7T7V67.js → BlockNoteEditor-EH4HWI7H.js} +14 -14
- package/dist/{BlockNoteEditor-UB7T7V67.js.map → BlockNoteEditor-EH4HWI7H.js.map} +1 -1
- package/dist/RbacTypes-BTbr27Ew.d.mts +43 -0
- package/dist/RbacTypes-BTbr27Ew.d.ts +43 -0
- package/dist/{auth.interface-CQJ6A2Cj.d.ts → auth.interface-BBUgMZzs.d.ts} +1 -1
- package/dist/{auth.interface-Bdq7-8iV.d.mts → auth.interface-XYEREOD6.d.mts} +1 -1
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-FKLP4NED.js → chunk-5IEWLLLD.js} +379 -18
- package/dist/chunk-5IEWLLLD.js.map +1 -0
- package/dist/{chunk-XI35ALWY.mjs → chunk-BKM5U3DE.mjs} +362 -1
- package/dist/chunk-BKM5U3DE.mjs.map +1 -0
- package/dist/{chunk-F44ET4AC.mjs → chunk-ENRSFVOS.mjs} +2657 -2264
- package/dist/chunk-ENRSFVOS.mjs.map +1 -0
- package/dist/{chunk-JOJZRGZL.mjs → chunk-MEWXQEVE.mjs} +38 -29
- package/dist/{chunk-JOJZRGZL.mjs.map → chunk-MEWXQEVE.mjs.map} +1 -1
- package/dist/{chunk-OTZEXASK.js → chunk-TWDSDTHU.js} +39 -30
- package/dist/chunk-TWDSDTHU.js.map +1 -0
- package/dist/{chunk-CV7UOUKQ.js → chunk-ZDP3MBUI.js} +1813 -1420
- package/dist/chunk-ZDP3MBUI.js.map +1 -0
- package/dist/client/index.d.mts +6 -24
- package/dist/client/index.d.ts +6 -24
- package/dist/client/index.js +4 -10
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -9
- package/dist/components/index.d.mts +51 -34
- package/dist/components/index.d.ts +51 -34
- package/dist/components/index.js +4 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +9 -9
- package/dist/{config-B3jKt9P7.d.ts → config-B5oBQVEA.d.ts} +1 -1
- package/dist/{config-DkHF61xA.d.mts → config-Bx_uh22h.d.mts} +1 -1
- package/dist/contexts/index.d.mts +65 -4
- package/dist/contexts/index.d.ts +65 -4
- package/dist/contexts/index.js +12 -4
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +11 -3
- package/dist/core/index.d.mts +126 -11
- package/dist/core/index.d.ts +126 -11
- package/dist/core/index.js +16 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +15 -1
- package/dist/index.d.mts +118 -20
- package/dist/index.d.ts +118 -20
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -2
- package/dist/{notification.interface-DG6obXUH.d.mts → notification.interface-DLZGtV7Z.d.mts} +1 -1
- package/dist/{notification.interface-DcSuc9CL.d.ts → notification.interface-aLEJbA_g.d.ts} +1 -1
- package/dist/{s3.service-DGilbikH.d.mts → s3.service-CVgLWaDc.d.mts} +2 -2
- package/dist/{s3.service-DjwEQJPe.d.ts → s3.service-SLlX0Zbz.d.ts} +2 -2
- package/dist/server/index.d.mts +3 -3
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/useDataListRetriever-BqJSFBck.d.mts +33 -0
- package/dist/useDataListRetriever-BqJSFBck.d.ts +33 -0
- package/dist/{useSocket-CmzVtg32.d.mts → useSocket-BkxHHujj.d.mts} +1 -1
- package/dist/{useSocket-8eUtnL7J.d.ts → useSocket-CMDjWFYm.d.ts} +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +0 -4
- package/src/components/index.ts +2 -3
- package/src/contexts/index.ts +2 -0
- package/src/core/index.ts +4 -0
- package/src/core/registry/ModuleRegistry.ts +10 -0
- package/src/features/assistant/AssistantModule.ts +19 -0
- package/src/features/assistant/components/containers/AssistantContainer.tsx +56 -0
- package/src/features/assistant/components/containers/__tests__/AssistantContainer.spec.tsx +101 -0
- package/src/features/assistant/components/index.ts +1 -0
- package/src/features/assistant/components/parts/AssistantComposer.tsx +56 -0
- package/src/features/assistant/components/parts/AssistantEmptyState.tsx +47 -0
- package/src/features/assistant/components/parts/AssistantSidebar.tsx +64 -0
- package/src/features/assistant/components/parts/AssistantStatusLine.tsx +19 -0
- package/src/features/assistant/components/parts/AssistantThread.tsx +36 -0
- package/src/features/assistant/components/parts/AssistantThreadHeader.tsx +91 -0
- package/src/features/assistant/components/parts/__tests__/AssistantComposer.spec.tsx +32 -0
- package/src/features/assistant/components/parts/__tests__/AssistantEmptyState.spec.tsx +27 -0
- package/src/features/assistant/components/parts/__tests__/AssistantSidebar.spec.tsx +58 -0
- package/src/features/assistant/components/parts/__tests__/AssistantStatusLine.spec.tsx +19 -0
- package/src/features/assistant/components/parts/__tests__/AssistantThread.spec.tsx +39 -0
- package/src/features/assistant/components/parts/__tests__/AssistantThreadHeader.spec.tsx +67 -0
- package/src/features/assistant/contexts/AssistantContext.tsx +255 -0
- package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +375 -0
- package/src/features/assistant/data/Assistant.ts +37 -0
- package/src/features/assistant/data/AssistantInterface.ts +11 -0
- package/src/features/assistant/data/AssistantService.ts +79 -0
- package/src/features/assistant/data/index.ts +3 -0
- package/src/features/assistant/index.ts +2 -0
- package/src/features/assistant/utils/__tests__/groupThreadsByBucket.spec.ts +24 -0
- package/src/features/assistant/utils/__tests__/resolveReferenceableModules.spec.ts +92 -0
- package/src/features/assistant/utils/groupThreadsByBucket.ts +26 -0
- package/src/features/assistant/utils/resolveReferenceableModules.ts +14 -0
- package/src/features/assistant-message/AssistantMessageModule.ts +28 -0
- package/src/features/assistant-message/components/MessageItem.tsx +60 -0
- package/src/features/assistant-message/components/MessageList.tsx +38 -0
- package/src/features/assistant-message/components/__tests__/MessageItem.spec.tsx +108 -0
- package/src/features/assistant-message/components/index.ts +2 -0
- package/src/features/assistant-message/components/parts/ReferenceBadges.tsx +46 -0
- package/src/features/assistant-message/components/parts/SuggestedFollowUps.tsx +52 -0
- package/src/features/assistant-message/components/parts/__tests__/ReferenceBadges.spec.tsx +59 -0
- package/src/features/assistant-message/components/parts/__tests__/SuggestedFollowUps.spec.tsx +29 -0
- package/src/features/assistant-message/data/AssistantMessage.ts +95 -0
- package/src/features/assistant-message/data/AssistantMessageInterface.ts +21 -0
- package/src/features/assistant-message/data/AssistantMessageService.ts +40 -0
- package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +158 -0
- package/src/features/assistant-message/data/index.ts +3 -0
- package/src/features/assistant-message/index.ts +2 -0
- package/src/features/rbac/components/RbacContainer.tsx +318 -49
- package/src/features/rbac/components/RbacPermissionPicker.tsx +144 -121
- package/src/features/rbac/contexts/RbacContext.tsx +209 -0
- package/src/features/rbac/contexts/index.ts +1 -0
- package/src/features/rbac/data/RbacMatrixModel.ts +84 -0
- package/src/features/rbac/data/RbacService.ts +61 -33
- package/src/features/rbac/data/RbacTypes.ts +28 -0
- package/src/features/rbac/data/index.ts +1 -0
- package/src/features/rbac/index.ts +1 -10
- package/src/features/rbac/rbac.module.ts +13 -0
- package/src/features/user/contexts/CurrentUserContext.tsx +5 -13
- package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +141 -0
- package/src/index.ts +4 -0
- package/dist/HowToInterface-BKhnkzBp.d.ts +0 -17
- package/dist/HowToInterface-Cj8OuQFf.d.mts +0 -17
- package/dist/ModulePathsInterface-BrdqgteS.d.mts +0 -31
- package/dist/ModulePathsInterface-DJKs7s_s.d.ts +0 -31
- package/dist/chunk-CV7UOUKQ.js.map +0 -1
- package/dist/chunk-F44ET4AC.mjs.map +0 -1
- package/dist/chunk-FKLP4NED.js.map +0 -1
- package/dist/chunk-OTZEXASK.js.map +0 -1
- package/dist/chunk-XI35ALWY.mjs.map +0 -1
- package/dist/useRbacState-C88O-5L8.d.ts +0 -77
- package/dist/useRbacState-mqYiRp3J.d.mts +0 -77
- package/src/features/rbac/components/RbacFeatureSection.tsx +0 -66
- package/src/features/rbac/components/RbacModuleTable.tsx +0 -121
- package/src/features/rbac/components/RbacToolbar.tsx +0 -40
- package/src/features/rbac/hooks/useRbacState.test.ts +0 -180
- package/src/features/rbac/hooks/useRbacState.ts +0 -319
- package/src/features/rbac/utils/RbacMigrationGenerator.test.ts +0 -124
- package/src/features/rbac/utils/RbacMigrationGenerator.ts +0 -184
- /package/dist/{BlockNoteEditor-7HAAXN3H.mjs.map → BlockNoteEditor-6CBDTVKV.mjs.map} +0 -0
|
@@ -1,130 +1,157 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
|
|
4
4
|
import { CheckIcon, MinusIcon, XIcon } from "lucide-react";
|
|
5
5
|
import { useTranslations } from "next-intl";
|
|
6
|
-
import { useCallback, useState } from "react";
|
|
7
|
-
import {
|
|
6
|
+
import { useCallback, useEffect, useState } from "react";
|
|
7
|
+
import { cn } from "../../../lib/utils";
|
|
8
|
+
import { Button, Checkbox, Input, Separator } from "../../../shadcnui";
|
|
8
9
|
import { PermissionValue } from "../data/RbacTypes";
|
|
9
|
-
import RbacPermissionCell from "./RbacPermissionCell";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Controlled "global" picker. Exactly ONE instance is rendered by
|
|
13
|
+
* `RbacContainer`; the container holds `activePicker` state and drives this
|
|
14
|
+
* component's `open` + `anchor` props. Cells themselves are plain clickable
|
|
15
|
+
* elements — they just open this picker at their own DOM position.
|
|
16
|
+
*
|
|
17
|
+
* This replaces the previous design that mounted one `<Popover>` per cell
|
|
18
|
+
* (~2,600 total), which was both slow AND caused unreliable click handling
|
|
19
|
+
* across overlapping outside-click listeners.
|
|
20
|
+
*/
|
|
11
21
|
interface RbacPermissionPickerProps {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
/** Whether the picker is open. Driven by `activePicker !== null` in the container. */
|
|
23
|
+
open: boolean;
|
|
24
|
+
/** DOM element the popup should anchor to. Required when `open`. */
|
|
25
|
+
anchor: HTMLElement | null;
|
|
26
|
+
/** Current effective value of the active cell (true | string | false | undefined). */
|
|
27
|
+
value: PermissionValue | undefined;
|
|
28
|
+
/** True if the active cell is in a role row (enables the "inherit from defaults" button). */
|
|
29
|
+
isRoleColumn: boolean;
|
|
30
|
+
/** Relationship paths the backend reports for the active cell's module. */
|
|
15
31
|
knownSegments: string[];
|
|
16
32
|
onSetValue: (value: PermissionValue) => void;
|
|
33
|
+
/** Only meaningful for role rows — clears the token so the row inherits defaults. */
|
|
17
34
|
onClear?: () => void;
|
|
35
|
+
/** Called when the picker should close (outside-click, ESC, etc.). */
|
|
36
|
+
onClose: () => void;
|
|
18
37
|
}
|
|
19
38
|
|
|
20
|
-
export
|
|
39
|
+
export function RbacPermissionPicker({
|
|
40
|
+
open,
|
|
41
|
+
anchor,
|
|
21
42
|
value,
|
|
22
|
-
|
|
23
|
-
isRoleColumn = false,
|
|
43
|
+
isRoleColumn,
|
|
24
44
|
knownSegments,
|
|
25
45
|
onSetValue,
|
|
26
46
|
onClear,
|
|
47
|
+
onClose,
|
|
27
48
|
}: RbacPermissionPickerProps) {
|
|
28
49
|
const t = useTranslations();
|
|
29
|
-
const [open, setOpen] = useState(false);
|
|
30
50
|
const [customSegment, setCustomSegment] = useState("");
|
|
31
51
|
|
|
32
|
-
//
|
|
52
|
+
// Reset the custom-segment input when the picker is reopened for a different cell.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!open) setCustomSegment("");
|
|
55
|
+
}, [open]);
|
|
56
|
+
|
|
33
57
|
const currentSegments: string[] = typeof value === "string" ? value.split("|").filter(Boolean) : [];
|
|
34
58
|
|
|
35
59
|
const toggleSegment = useCallback(
|
|
36
60
|
(segment: string) => {
|
|
37
|
-
const
|
|
61
|
+
const next = currentSegments.includes(segment)
|
|
38
62
|
? currentSegments.filter((s) => s !== segment)
|
|
39
63
|
: [...currentSegments, segment];
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
onSetValue(false);
|
|
43
|
-
} else {
|
|
44
|
-
onSetValue(newSegments.join("|"));
|
|
45
|
-
}
|
|
64
|
+
if (next.length === 0) onSetValue(false);
|
|
65
|
+
else onSetValue(next.join("|"));
|
|
46
66
|
},
|
|
47
67
|
[currentSegments, onSetValue],
|
|
48
68
|
);
|
|
49
69
|
|
|
50
70
|
const addCustomSegment = useCallback(() => {
|
|
51
|
-
if (!customSegment.trim()) return;
|
|
52
71
|
const segment = customSegment.trim();
|
|
72
|
+
if (!segment) return;
|
|
53
73
|
if (!currentSegments.includes(segment)) {
|
|
54
|
-
|
|
55
|
-
onSetValue(newSegments.join("|"));
|
|
74
|
+
onSetValue([...currentSegments, segment].join("|"));
|
|
56
75
|
}
|
|
57
76
|
setCustomSegment("");
|
|
58
77
|
}, [customSegment, currentSegments, onSetValue]);
|
|
59
78
|
|
|
60
79
|
return (
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
>
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
80
|
+
<PopoverPrimitive.Root
|
|
81
|
+
open={open}
|
|
82
|
+
onOpenChange={(next) => {
|
|
83
|
+
if (!next) onClose();
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<PopoverPrimitive.Portal>
|
|
87
|
+
<PopoverPrimitive.Positioner
|
|
88
|
+
anchor={anchor ?? undefined}
|
|
89
|
+
side="bottom"
|
|
90
|
+
align="center"
|
|
91
|
+
sideOffset={4}
|
|
92
|
+
className="isolate z-50"
|
|
93
|
+
>
|
|
94
|
+
<PopoverPrimitive.Popup className="bg-popover text-popover-foreground ring-foreground/10 z-50 flex w-96 flex-col gap-3 rounded-lg p-3 text-xs shadow-md outline-hidden ring-1 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 duration-100">
|
|
95
|
+
{/* Quick value */}
|
|
96
|
+
<div>
|
|
97
|
+
<p className="text-muted-foreground mb-2 text-xs font-medium">{t("rbac.quick_value")}</p>
|
|
98
|
+
<div className="flex gap-2">
|
|
99
|
+
<Button
|
|
100
|
+
size="sm"
|
|
101
|
+
variant={value === true ? "default" : "outline"}
|
|
102
|
+
className={cn("flex-1 gap-1", value === true && "bg-emerald-600 hover:bg-emerald-700")}
|
|
103
|
+
onClick={() => {
|
|
104
|
+
onSetValue(true);
|
|
105
|
+
onClose();
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<CheckIcon className="h-3 w-3" />
|
|
109
|
+
<span>true</span>
|
|
110
|
+
</Button>
|
|
111
|
+
<Button
|
|
112
|
+
size="sm"
|
|
113
|
+
variant={value === false ? "destructive" : "outline"}
|
|
114
|
+
className="flex-1 gap-1"
|
|
115
|
+
onClick={() => {
|
|
116
|
+
onSetValue(false);
|
|
117
|
+
onClose();
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<XIcon className="h-3 w-3" />
|
|
121
|
+
<span>false</span>
|
|
122
|
+
</Button>
|
|
123
|
+
</div>
|
|
95
124
|
</div>
|
|
96
|
-
</div>
|
|
97
125
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
{/* Inherit from defaults — role rows only */}
|
|
127
|
+
{isRoleColumn && onClear && (
|
|
128
|
+
<>
|
|
129
|
+
<Separator />
|
|
130
|
+
<Button
|
|
131
|
+
size="sm"
|
|
132
|
+
variant="ghost"
|
|
133
|
+
className="text-muted-foreground w-full gap-1"
|
|
134
|
+
onClick={() => {
|
|
135
|
+
onClear();
|
|
136
|
+
onClose();
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<MinusIcon className="h-3 w-3" />
|
|
140
|
+
{t("rbac.inherit_from_defaults")}
|
|
141
|
+
</Button>
|
|
142
|
+
</>
|
|
143
|
+
)}
|
|
116
144
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
<p className="text-xs font-medium text-muted-foreground mb-2">{t("rbac.relationships")}</p>
|
|
145
|
+
{/* Relationship-path builder — always visible so users can type custom paths */}
|
|
146
|
+
<Separator />
|
|
147
|
+
<div>
|
|
148
|
+
<p className="text-muted-foreground mb-2 text-xs font-medium">{t("rbac.relationships")}</p>
|
|
149
|
+
{knownSegments.length > 0 && (
|
|
123
150
|
<div className="max-h-40 space-y-1 overflow-y-auto">
|
|
124
151
|
{knownSegments.map((segment) => (
|
|
125
152
|
<label
|
|
126
153
|
key={segment}
|
|
127
|
-
className="flex items-center gap-2 rounded px-1 py-0.5 text-sm
|
|
154
|
+
className="hover:bg-muted flex cursor-pointer items-center gap-2 rounded px-1 py-0.5 text-sm"
|
|
128
155
|
>
|
|
129
156
|
<Checkbox
|
|
130
157
|
checked={currentSegments.includes(segment)}
|
|
@@ -134,46 +161,42 @@ export default function RbacPermissionPicker({
|
|
|
134
161
|
</label>
|
|
135
162
|
))}
|
|
136
163
|
</div>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
</Button>
|
|
161
|
-
</div>
|
|
162
|
-
|
|
163
|
-
{/* Preview */}
|
|
164
|
-
{currentSegments.length > 0 && (
|
|
165
|
-
<div className="mt-2 rounded bg-muted p-2">
|
|
166
|
-
<p className="text-xs text-muted-foreground mb-1">{t("rbac.preview")}</p>
|
|
167
|
-
<p className="text-xs font-mono break-all">{currentSegments.join("|")}</p>
|
|
168
|
-
</div>
|
|
169
|
-
)}
|
|
164
|
+
)}
|
|
165
|
+
<div className="mt-2 flex gap-1">
|
|
166
|
+
<Input
|
|
167
|
+
value={customSegment}
|
|
168
|
+
onChange={(e) => setCustomSegment(e.target.value)}
|
|
169
|
+
placeholder={t("rbac.custom_segment_placeholder")}
|
|
170
|
+
className="h-7 font-mono text-xs"
|
|
171
|
+
onKeyDown={(e) => {
|
|
172
|
+
if (e.key === "Enter") {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
addCustomSegment();
|
|
175
|
+
}
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
<Button
|
|
179
|
+
size="sm"
|
|
180
|
+
variant="outline"
|
|
181
|
+
className="h-7 px-2 text-xs"
|
|
182
|
+
onClick={addCustomSegment}
|
|
183
|
+
disabled={!customSegment.trim()}
|
|
184
|
+
>
|
|
185
|
+
+
|
|
186
|
+
</Button>
|
|
170
187
|
</div>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
{currentSegments.length > 0 && (
|
|
189
|
+
<div className="bg-muted mt-2 rounded p-2">
|
|
190
|
+
<p className="text-muted-foreground mb-1 text-xs">{t("rbac.preview")}</p>
|
|
191
|
+
<p className="font-mono text-xs break-all">{currentSegments.join("|")}</p>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
</PopoverPrimitive.Popup>
|
|
196
|
+
</PopoverPrimitive.Positioner>
|
|
197
|
+
</PopoverPrimitive.Portal>
|
|
198
|
+
</PopoverPrimitive.Root>
|
|
176
199
|
);
|
|
177
200
|
}
|
|
178
201
|
|
|
179
|
-
export
|
|
202
|
+
export default RbacPermissionPicker;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { DownloadIcon, Loader2Icon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { SharedProvider } from "../../../contexts";
|
|
7
|
+
import { Modules } from "../../../core";
|
|
8
|
+
import { usePageUrlGenerator } from "../../../hooks";
|
|
9
|
+
import { BreadcrumbItemData } from "../../../interfaces";
|
|
10
|
+
import { Button } from "../../../shadcnui";
|
|
11
|
+
import { showError, showToast } from "../../../utils/toast";
|
|
12
|
+
import { RbacService } from "../data/RbacService";
|
|
13
|
+
import type { ActionType, PermissionValue, PermToken, RbacMatrix, RbacModuleBlock } from "../data/RbacTypes";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_OUTPUT_PATH = "apps/api/src/rbac/permissions.ts";
|
|
16
|
+
|
|
17
|
+
function upsertToken(tokens: PermToken[] | undefined, action: ActionType, scope: PermissionValue): PermToken[] {
|
|
18
|
+
const next = (tokens ?? []).filter((t) => t.action !== action);
|
|
19
|
+
// `scope === false` has no representation in the matrix — absence of a
|
|
20
|
+
// token for an action IS the "deny" semantics. See RbacContainer helpers.
|
|
21
|
+
if (scope === false) return next;
|
|
22
|
+
next.push({ action, scope });
|
|
23
|
+
return next;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function removeToken(tokens: PermToken[] | undefined, action: ActionType): PermToken[] {
|
|
27
|
+
return (tokens ?? []).filter((t) => t.action !== action);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface RbacContextType {
|
|
31
|
+
matrix: RbacMatrix | null;
|
|
32
|
+
modulePaths: Record<string, readonly string[]>;
|
|
33
|
+
loading: boolean;
|
|
34
|
+
error: string | null;
|
|
35
|
+
saving: boolean;
|
|
36
|
+
roleNames?: Record<string, string>;
|
|
37
|
+
moduleNames?: Record<string, string>;
|
|
38
|
+
updateCell: (moduleId: string, rowKey: "default" | string, action: ActionType, value: PermissionValue) => void;
|
|
39
|
+
clearCell: (moduleId: string, roleId: string, action: ActionType) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const RbacContext = createContext<RbacContextType | undefined>(undefined);
|
|
43
|
+
|
|
44
|
+
type RbacProviderProps = {
|
|
45
|
+
children: ReactNode;
|
|
46
|
+
/** UUID → PascalCase map for roles (e.g. from `@neural-erp/shared`'s `RoleId`). */
|
|
47
|
+
roleNames?: Record<string, string>;
|
|
48
|
+
/** UUID → PascalCase map for modules (e.g. from `@neural-erp/shared`'s `ModuleId`). */
|
|
49
|
+
moduleNames?: Record<string, string>;
|
|
50
|
+
/**
|
|
51
|
+
* Output path for the serialized `permissions.ts` file. Absolute, or
|
|
52
|
+
* relative to the API's repo root. Defaults to
|
|
53
|
+
* `"apps/api/src/rbac/permissions.ts"`.
|
|
54
|
+
*/
|
|
55
|
+
outputPath?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* RbacProvider — owns the matrix state, the save handler, and the breadcrumb +
|
|
60
|
+
* title wiring. Renders the "Save to permissions.ts" button into
|
|
61
|
+
* `title.functions` so it appears in the page header, following the same
|
|
62
|
+
* convention as CompanyContext / other context providers in this codebase.
|
|
63
|
+
*
|
|
64
|
+
* The child `RbacContainer` is stateless — it consumes state via
|
|
65
|
+
* `useRbacContext()`.
|
|
66
|
+
*/
|
|
67
|
+
export const RbacProvider = ({
|
|
68
|
+
children,
|
|
69
|
+
roleNames,
|
|
70
|
+
moduleNames,
|
|
71
|
+
outputPath = DEFAULT_OUTPUT_PATH,
|
|
72
|
+
}: RbacProviderProps) => {
|
|
73
|
+
const generateUrl = usePageUrlGenerator();
|
|
74
|
+
const t = useTranslations();
|
|
75
|
+
|
|
76
|
+
const [matrix, setMatrix] = useState<RbacMatrix | null>(null);
|
|
77
|
+
const [modulePaths, setModulePaths] = useState<Record<string, readonly string[]>>({});
|
|
78
|
+
const [loading, setLoading] = useState(true);
|
|
79
|
+
const [error, setError] = useState<string | null>(null);
|
|
80
|
+
const [saving, setSaving] = useState(false);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
let cancelled = false;
|
|
84
|
+
setLoading(true);
|
|
85
|
+
RbacService.fetchMatrix()
|
|
86
|
+
.then((result) => {
|
|
87
|
+
if (cancelled) return;
|
|
88
|
+
setMatrix(result.matrix);
|
|
89
|
+
setModulePaths(result.modulePaths);
|
|
90
|
+
})
|
|
91
|
+
.catch((err) => {
|
|
92
|
+
if (cancelled) return;
|
|
93
|
+
console.error("Failed to load RBAC matrix:", err);
|
|
94
|
+
setError(t("rbac.loading_error"));
|
|
95
|
+
})
|
|
96
|
+
.finally(() => {
|
|
97
|
+
if (cancelled) return;
|
|
98
|
+
setLoading(false);
|
|
99
|
+
});
|
|
100
|
+
return () => {
|
|
101
|
+
cancelled = true;
|
|
102
|
+
};
|
|
103
|
+
}, [t]);
|
|
104
|
+
|
|
105
|
+
const updateCell = useCallback<RbacContextType["updateCell"]>((moduleId, rowKey, action, value) => {
|
|
106
|
+
setMatrix((prev) => {
|
|
107
|
+
if (!prev) return prev;
|
|
108
|
+
const prevBlock: RbacModuleBlock = prev[moduleId] ?? { default: [] };
|
|
109
|
+
const prevTokens: PermToken[] | undefined =
|
|
110
|
+
rowKey === "default" ? prevBlock.default : (prevBlock as Record<string, PermToken[]>)[rowKey];
|
|
111
|
+
const nextTokens = upsertToken(prevTokens, action, value);
|
|
112
|
+
const nextBlock: RbacModuleBlock = { ...prevBlock, [rowKey]: nextTokens } as RbacModuleBlock;
|
|
113
|
+
return { ...prev, [moduleId]: nextBlock };
|
|
114
|
+
});
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
const clearCell = useCallback<RbacContextType["clearCell"]>((moduleId, roleId, action) => {
|
|
118
|
+
setMatrix((prev) => {
|
|
119
|
+
if (!prev) return prev;
|
|
120
|
+
const prevBlock: RbacModuleBlock = prev[moduleId] ?? { default: [] };
|
|
121
|
+
const prevTokens: PermToken[] | undefined = (prevBlock as Record<string, PermToken[]>)[roleId];
|
|
122
|
+
if (!prevTokens || prevTokens.length === 0) return prev;
|
|
123
|
+
const nextTokens = removeToken(prevTokens, action);
|
|
124
|
+
const nextBlock: RbacModuleBlock = { ...prevBlock } as RbacModuleBlock;
|
|
125
|
+
if (nextTokens.length === 0) {
|
|
126
|
+
delete (nextBlock as Record<string, PermToken[]>)[roleId];
|
|
127
|
+
} else {
|
|
128
|
+
(nextBlock as Record<string, PermToken[]>)[roleId] = nextTokens;
|
|
129
|
+
}
|
|
130
|
+
return { ...prev, [moduleId]: nextBlock };
|
|
131
|
+
});
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
const canSave = Boolean(roleNames && moduleNames && matrix && !saving);
|
|
135
|
+
|
|
136
|
+
const handleSave = useCallback(async () => {
|
|
137
|
+
if (!matrix || !roleNames || !moduleNames) return;
|
|
138
|
+
setSaving(true);
|
|
139
|
+
try {
|
|
140
|
+
await RbacService.saveMatrix({ matrix, roleNames, moduleNames, outputPath });
|
|
141
|
+
showToast(`Saved to ${outputPath}`);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error("Failed to save RBAC matrix:", err);
|
|
144
|
+
showError(err instanceof Error ? err.message : String(err));
|
|
145
|
+
} finally {
|
|
146
|
+
setSaving(false);
|
|
147
|
+
}
|
|
148
|
+
}, [matrix, roleNames, moduleNames, outputPath]);
|
|
149
|
+
|
|
150
|
+
const breadcrumb = (): BreadcrumbItemData[] => {
|
|
151
|
+
const response: BreadcrumbItemData[] = [];
|
|
152
|
+
response.push({
|
|
153
|
+
name: t(`entities.rbac`, { count: 2 }),
|
|
154
|
+
href: generateUrl({ page: Modules.RbacMatrix }),
|
|
155
|
+
});
|
|
156
|
+
return response;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const title = () => {
|
|
160
|
+
const response: {
|
|
161
|
+
type: string;
|
|
162
|
+
functions?: ReactNode;
|
|
163
|
+
} = {
|
|
164
|
+
type: t(`entities.rbac`, { count: 2 }),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const functions: ReactNode[] = [];
|
|
168
|
+
functions.push(
|
|
169
|
+
<Button key="rbacSave" size="sm" onClick={handleSave} disabled={!canSave} className="gap-1">
|
|
170
|
+
{saving ? <Loader2Icon className="h-3.5 w-3.5 animate-spin" /> : <DownloadIcon className="h-3.5 w-3.5" />}
|
|
171
|
+
Save to permissions.ts
|
|
172
|
+
</Button>,
|
|
173
|
+
);
|
|
174
|
+
if (functions.length > 0) response.functions = functions;
|
|
175
|
+
|
|
176
|
+
return response;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Memoize the context value so stable-identity child consumers don't
|
|
180
|
+
// re-render when only title/breadcrumbs change.
|
|
181
|
+
const contextValue = useMemo<RbacContextType>(
|
|
182
|
+
() => ({
|
|
183
|
+
matrix,
|
|
184
|
+
modulePaths,
|
|
185
|
+
loading,
|
|
186
|
+
error,
|
|
187
|
+
saving,
|
|
188
|
+
roleNames,
|
|
189
|
+
moduleNames,
|
|
190
|
+
updateCell,
|
|
191
|
+
clearCell,
|
|
192
|
+
}),
|
|
193
|
+
[matrix, modulePaths, loading, error, saving, roleNames, moduleNames, updateCell, clearCell],
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>
|
|
198
|
+
<RbacContext.Provider value={contextValue}>{children}</RbacContext.Provider>
|
|
199
|
+
</SharedProvider>
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const useRbacContext = (): RbacContextType => {
|
|
204
|
+
const ctx = useContext(RbacContext);
|
|
205
|
+
if (!ctx) {
|
|
206
|
+
throw new Error("useRbacContext must be used within an RbacProvider");
|
|
207
|
+
}
|
|
208
|
+
return ctx;
|
|
209
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./RbacContext";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface } from "../../../core";
|
|
2
|
+
import type { RbacMatrix } from "./RbacTypes";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Input shape accepted by `RbacMatrixModel.createJsonApi()` for a PUT request
|
|
6
|
+
* to the dev matrix endpoint.
|
|
7
|
+
*/
|
|
8
|
+
export interface RbacMatrixInput {
|
|
9
|
+
matrix: RbacMatrix;
|
|
10
|
+
roleNames: Record<string, string>;
|
|
11
|
+
moduleNames: Record<string, string>;
|
|
12
|
+
outputPath: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Frontend model for the dev-only `rbac-matrix` JSON:API resource.
|
|
17
|
+
*
|
|
18
|
+
* Backend contract (see `rbac-dev.controller.ts`):
|
|
19
|
+
* - `GET /_dev/rbac/matrix` → `{ data: { type: "rbac-matrix", id: "singleton",
|
|
20
|
+
* attributes: { matrix } } }`
|
|
21
|
+
* - `PUT /_dev/rbac/matrix` body: `{ data: { type: "rbac-matrix", attributes:
|
|
22
|
+
* { matrix, roleNames, moduleNames, outputPath } } }`
|
|
23
|
+
* → `{ data: { type: "rbac-matrix", id: "singleton", attributes: { bytesWritten, path } } }`
|
|
24
|
+
*
|
|
25
|
+
* The resource is a singleton (`id: "singleton"`) so there is no collection
|
|
26
|
+
* listing; the "read" and "write" shapes share a single model with optional
|
|
27
|
+
* fields populated depending on which endpoint produced the response.
|
|
28
|
+
*/
|
|
29
|
+
export class RbacMatrixModel extends AbstractApiData {
|
|
30
|
+
private _matrix?: RbacMatrix;
|
|
31
|
+
private _modulePaths?: Record<string, readonly string[]>;
|
|
32
|
+
private _bytesWritten?: number;
|
|
33
|
+
private _path?: string;
|
|
34
|
+
|
|
35
|
+
/** The RBAC matrix object (populated after a GET). */
|
|
36
|
+
get matrix(): RbacMatrix | undefined {
|
|
37
|
+
return this._matrix;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* UUID-keyed map of each module's known BFS relationship paths (populated
|
|
42
|
+
* after a GET). Fed to the permission picker as scope suggestions.
|
|
43
|
+
*/
|
|
44
|
+
get modulePaths(): Record<string, readonly string[]> | undefined {
|
|
45
|
+
return this._modulePaths;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Bytes written to the permissions.ts file (populated after a PUT). */
|
|
49
|
+
get bytesWritten(): number | undefined {
|
|
50
|
+
return this._bytesWritten;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Resolved absolute output path (populated after a PUT). */
|
|
54
|
+
get path(): string | undefined {
|
|
55
|
+
return this._path;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
59
|
+
super.rehydrate(data);
|
|
60
|
+
|
|
61
|
+
const attrs = data.jsonApi.attributes ?? {};
|
|
62
|
+
this._matrix = attrs.matrix;
|
|
63
|
+
this._modulePaths = attrs.modulePaths;
|
|
64
|
+
this._bytesWritten = attrs.bytesWritten;
|
|
65
|
+
this._path = attrs.path;
|
|
66
|
+
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
createJsonApi(data: RbacMatrixInput): any {
|
|
71
|
+
return {
|
|
72
|
+
data: {
|
|
73
|
+
type: "rbac-matrix",
|
|
74
|
+
id: "singleton",
|
|
75
|
+
attributes: {
|
|
76
|
+
matrix: data.matrix,
|
|
77
|
+
roleNames: data.roleNames,
|
|
78
|
+
moduleNames: data.moduleNames,
|
|
79
|
+
outputPath: data.outputPath,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|