@carlonicora/nextjs-jsonapi 1.78.0 → 1.80.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.
Files changed (128) hide show
  1. package/dist/{AssistantMessageInterface-DS_tyJTV.d.ts → AssistantMessageInterface-BpEhx2pC.d.ts} +19 -2
  2. package/dist/{AssistantMessageInterface-D0Kwf8CR.d.mts → AssistantMessageInterface-DJ3Me16Y.d.mts} +19 -2
  3. package/dist/{AuthComponent-Blbs06ud.d.ts → AuthComponent-B6DIk8Vf.d.ts} +1 -1
  4. package/dist/{AuthComponent-huIaK5rm.d.mts → AuthComponent-BKI0ZbtD.d.mts} +1 -1
  5. package/dist/{BlockNoteEditor-JXK3JGKJ.mjs → BlockNoteEditor-3M5PD3BZ.mjs} +4 -4
  6. package/dist/{BlockNoteEditor-2G5UYALC.js → BlockNoteEditor-YLTPJPTV.js} +14 -14
  7. package/dist/{BlockNoteEditor-2G5UYALC.js.map → BlockNoteEditor-YLTPJPTV.js.map} +1 -1
  8. package/dist/RbacTypes-BTbr27Ew.d.mts +43 -0
  9. package/dist/RbacTypes-BTbr27Ew.d.ts +43 -0
  10. package/dist/{auth.interface-CQJ6A2Cj.d.ts → auth.interface-BBUgMZzs.d.ts} +1 -1
  11. package/dist/{auth.interface-Bdq7-8iV.d.mts → auth.interface-XYEREOD6.d.mts} +1 -1
  12. package/dist/billing/index.js +346 -346
  13. package/dist/billing/index.mjs +3 -3
  14. package/dist/{chunk-ZEDB6JVB.js → chunk-4NOQNTFI.js} +1585 -1405
  15. package/dist/chunk-4NOQNTFI.js.map +1 -0
  16. package/dist/{chunk-I65SSQ5Z.mjs → chunk-6UMB5LTQ.mjs} +157 -7
  17. package/dist/chunk-6UMB5LTQ.mjs.map +1 -0
  18. package/dist/{chunk-FDJQRIMY.js → chunk-N4YZ45SK.js} +174 -24
  19. package/dist/chunk-N4YZ45SK.js.map +1 -0
  20. package/dist/{chunk-NB6TIKHK.mjs → chunk-NQV5RDCK.mjs} +2524 -2344
  21. package/dist/chunk-NQV5RDCK.mjs.map +1 -0
  22. package/dist/{chunk-NZOUEN67.mjs → chunk-PV5V6CVW.mjs} +38 -29
  23. package/dist/{chunk-NZOUEN67.mjs.map → chunk-PV5V6CVW.mjs.map} +1 -1
  24. package/dist/{chunk-X4YDETTD.js → chunk-ZEJSPTHS.js} +39 -30
  25. package/dist/chunk-ZEJSPTHS.js.map +1 -0
  26. package/dist/client/index.d.mts +6 -24
  27. package/dist/client/index.d.ts +6 -24
  28. package/dist/client/index.js +4 -10
  29. package/dist/client/index.js.map +1 -1
  30. package/dist/client/index.mjs +3 -9
  31. package/dist/components/index.d.mts +55 -39
  32. package/dist/components/index.d.ts +55 -39
  33. package/dist/components/index.js +4 -8
  34. package/dist/components/index.js.map +1 -1
  35. package/dist/components/index.mjs +5 -9
  36. package/dist/{config-B3jKt9P7.d.ts → config-B5oBQVEA.d.ts} +1 -1
  37. package/dist/{config-DkHF61xA.d.mts → config-Bx_uh22h.d.mts} +1 -1
  38. package/dist/contexts/index.d.mts +41 -4
  39. package/dist/contexts/index.d.ts +41 -4
  40. package/dist/contexts/index.js +8 -4
  41. package/dist/contexts/index.js.map +1 -1
  42. package/dist/contexts/index.mjs +7 -3
  43. package/dist/core/index.d.mts +51 -11
  44. package/dist/core/index.d.ts +51 -11
  45. package/dist/core/index.js +8 -2
  46. package/dist/core/index.js.map +1 -1
  47. package/dist/core/index.mjs +7 -1
  48. package/dist/index.d.mts +117 -20
  49. package/dist/index.d.ts +117 -20
  50. package/dist/index.js +11 -3
  51. package/dist/index.js.map +1 -1
  52. package/dist/index.mjs +10 -2
  53. package/dist/{notification.interface-DG6obXUH.d.mts → notification.interface-DLZGtV7Z.d.mts} +1 -1
  54. package/dist/{notification.interface-DcSuc9CL.d.ts → notification.interface-aLEJbA_g.d.ts} +1 -1
  55. package/dist/{s3.service-DGilbikH.d.mts → s3.service-CVgLWaDc.d.mts} +2 -2
  56. package/dist/{s3.service-DjwEQJPe.d.ts → s3.service-SLlX0Zbz.d.ts} +2 -2
  57. package/dist/server/index.d.mts +3 -3
  58. package/dist/server/index.d.ts +3 -3
  59. package/dist/server/index.js +3 -3
  60. package/dist/server/index.mjs +1 -1
  61. package/dist/useDataListRetriever-BqJSFBck.d.mts +33 -0
  62. package/dist/useDataListRetriever-BqJSFBck.d.ts +33 -0
  63. package/dist/{useSocket-CmzVtg32.d.mts → useSocket-BkxHHujj.d.mts} +1 -1
  64. package/dist/{useSocket-8eUtnL7J.d.ts → useSocket-CMDjWFYm.d.ts} +1 -1
  65. package/package.json +1 -1
  66. package/src/client/index.ts +0 -4
  67. package/src/components/index.ts +0 -3
  68. package/src/contexts/index.ts +1 -0
  69. package/src/core/index.ts +2 -0
  70. package/src/core/registry/ModuleRegistry.ts +2 -0
  71. package/src/features/assistant/components/parts/AssistantThread.tsx +1 -1
  72. package/src/features/assistant-message/AssistantMessageModule.ts +4 -0
  73. package/src/features/assistant-message/components/MessageItem.tsx +7 -7
  74. package/src/features/assistant-message/components/MessageList.tsx +1 -1
  75. package/src/features/assistant-message/components/__tests__/MessageItem.spec.tsx +11 -7
  76. package/src/features/assistant-message/components/index.ts +1 -0
  77. package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +135 -0
  78. package/src/features/assistant-message/components/parts/MessageSourcesPanel.tsx +151 -0
  79. package/src/features/assistant-message/components/parts/RelevanceMeter.tsx +29 -0
  80. package/src/features/assistant-message/components/parts/__tests__/MessageSourcesPanel.spec.tsx +70 -0
  81. package/src/features/assistant-message/components/parts/tabs/CitationsTab.tsx +105 -0
  82. package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +88 -0
  83. package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +51 -0
  84. package/src/features/assistant-message/components/parts/tabs/SuggestedQuestionsTab.tsx +24 -0
  85. package/src/features/assistant-message/components/parts/tabs/UsersTab.tsx +142 -0
  86. package/src/features/assistant-message/data/AssistantMessage.ts +20 -0
  87. package/src/features/assistant-message/data/AssistantMessageInterface.ts +2 -0
  88. package/src/features/assistant-message/data/AssistantMessageService.ts +13 -4
  89. package/src/features/assistant-message/data/__tests__/AssistantMessage.citations.spec.ts +65 -0
  90. package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +8 -0
  91. package/src/features/chunk/ChunkModule.ts +18 -0
  92. package/src/features/chunk/data/Chunk.ts +49 -0
  93. package/src/features/chunk/data/ChunkInput.ts +3 -0
  94. package/src/features/chunk/data/ChunkInterface.ts +18 -0
  95. package/src/features/chunk/data/__tests__/Chunk.spec.ts +83 -0
  96. package/src/features/chunk/data/index.ts +3 -0
  97. package/src/features/chunk/index.ts +2 -0
  98. package/src/features/rbac/components/RbacContainer.tsx +318 -49
  99. package/src/features/rbac/components/RbacPermissionPicker.tsx +144 -121
  100. package/src/features/rbac/contexts/RbacContext.tsx +209 -0
  101. package/src/features/rbac/contexts/index.ts +1 -0
  102. package/src/features/rbac/data/RbacMatrixModel.ts +84 -0
  103. package/src/features/rbac/data/RbacService.ts +61 -33
  104. package/src/features/rbac/data/RbacTypes.ts +28 -0
  105. package/src/features/rbac/data/index.ts +1 -0
  106. package/src/features/rbac/index.ts +1 -10
  107. package/src/features/rbac/rbac.module.ts +13 -0
  108. package/dist/ModulePathsInterface-BrdqgteS.d.mts +0 -31
  109. package/dist/ModulePathsInterface-DJKs7s_s.d.ts +0 -31
  110. package/dist/chunk-FDJQRIMY.js.map +0 -1
  111. package/dist/chunk-I65SSQ5Z.mjs.map +0 -1
  112. package/dist/chunk-NB6TIKHK.mjs.map +0 -1
  113. package/dist/chunk-X4YDETTD.js.map +0 -1
  114. package/dist/chunk-ZEDB6JVB.js.map +0 -1
  115. package/dist/useRbacState-C88O-5L8.d.ts +0 -77
  116. package/dist/useRbacState-mqYiRp3J.d.mts +0 -77
  117. package/src/features/assistant-message/components/parts/ReferenceBadges.tsx +0 -46
  118. package/src/features/assistant-message/components/parts/SuggestedFollowUps.tsx +0 -52
  119. package/src/features/assistant-message/components/parts/__tests__/ReferenceBadges.spec.tsx +0 -59
  120. package/src/features/assistant-message/components/parts/__tests__/SuggestedFollowUps.spec.tsx +0 -29
  121. package/src/features/rbac/components/RbacFeatureSection.tsx +0 -66
  122. package/src/features/rbac/components/RbacModuleTable.tsx +0 -121
  123. package/src/features/rbac/components/RbacToolbar.tsx +0 -40
  124. package/src/features/rbac/hooks/useRbacState.test.ts +0 -180
  125. package/src/features/rbac/hooks/useRbacState.ts +0 -319
  126. package/src/features/rbac/utils/RbacMigrationGenerator.test.ts +0 -124
  127. package/src/features/rbac/utils/RbacMigrationGenerator.ts +0 -184
  128. /package/dist/{BlockNoteEditor-JXK3JGKJ.mjs.map → BlockNoteEditor-3M5PD3BZ.mjs.map} +0 -0
@@ -1,77 +0,0 @@
1
- import { F as FeatureInterface } from './feature.interface-BO25VLlx.js';
2
- import { R as RoleInterface } from './notification.interface-DcSuc9CL.js';
3
- import { P as PermissionMappingInterface, M as ModulePathsInterface, A as ActionType, a as PermissionValue, b as PermissionsMap } from './ModulePathsInterface-DJKs7s_s.js';
4
-
5
- type PageInfo = {
6
- startItem: number;
7
- endItem: number;
8
- pageSize: number;
9
- };
10
- type DataListRetriever<T> = {
11
- ready?: boolean;
12
- setReady: (state: boolean) => void;
13
- isLoaded: boolean;
14
- data: T[] | undefined;
15
- total?: number;
16
- next?: (onlyNewRecords?: boolean) => Promise<void>;
17
- previous?: (onlyNewRecords?: boolean) => Promise<void>;
18
- search: (search: string) => Promise<void>;
19
- refresh: () => Promise<void>;
20
- addAdditionalParameter: (key: string, value: any | null) => void;
21
- removeAdditionalParameter: (key: string) => void;
22
- setRefreshedElement: (element: T) => void;
23
- removeElement: (element: T) => void;
24
- isSearch: boolean;
25
- pageInfo?: PageInfo;
26
- };
27
- declare function useDataListRetriever<T>(params: {
28
- ready?: boolean;
29
- retriever: (params: any) => Promise<T[]>;
30
- retrieverParams?: any;
31
- search?: string;
32
- addAdditionalParameter?: (key: string, value: any | null) => void;
33
- requiresSearch?: boolean;
34
- module: any;
35
- }): DataListRetriever<T>;
36
-
37
- interface OriginalData {
38
- features: FeatureInterface[];
39
- roles: RoleInterface[];
40
- permissionMappings: PermissionMappingInterface[];
41
- moduleRelationshipPaths: Map<string, string[]>;
42
- }
43
- declare function useRbacState(): {
44
- original: OriginalData | null;
45
- isDirty: boolean;
46
- init: (features: FeatureInterface[], roles: RoleInterface[], permissionMappings: PermissionMappingInterface[], modulePaths: ModulePathsInterface[]) => void;
47
- setFeatureIsCore: (featureId: string, isCore: boolean) => void;
48
- setModuleDefaultPermission: (moduleId: string, actionType: ActionType, value: PermissionValue) => void;
49
- setRolePermission: (roleId: string, moduleId: string, actionType: ActionType, value: PermissionValue) => void;
50
- clearRolePermission: (roleId: string, moduleId: string, actionType: ActionType) => void;
51
- clearAllRolePermissions: (roleId: string, moduleId: string) => void;
52
- resetModulePermissions: (moduleId: string, roles: {
53
- id: string;
54
- }[]) => void;
55
- reset: () => void;
56
- getFeatureIsCore: (featureId: string) => boolean;
57
- getModuleDefaultPermission: (moduleId: string, actionType: ActionType) => PermissionValue | undefined;
58
- getRolePermission: (roleId: string, moduleId: string, actionType: ActionType) => PermissionValue | undefined | null;
59
- getEffectiveConfiguration: () => {
60
- features: {
61
- id: string;
62
- name: string;
63
- isCore: boolean;
64
- modules: {
65
- id: string;
66
- name: string;
67
- permissions: PermissionsMap;
68
- }[];
69
- }[];
70
- roles: RoleInterface[];
71
- rolePermissionsMap: Map<string, PermissionsMap>;
72
- } | null;
73
- getModuleRelationshipPaths: (moduleId: string) => string[];
74
- };
75
- type RbacStateApi = ReturnType<typeof useRbacState>;
76
-
77
- export { type DataListRetriever as D, type RbacStateApi as R, useDataListRetriever as a, useRbacState as u };
@@ -1,77 +0,0 @@
1
- import { F as FeatureInterface } from './feature.interface-CXb1-vNq.mjs';
2
- import { R as RoleInterface } from './notification.interface-DG6obXUH.mjs';
3
- import { P as PermissionMappingInterface, M as ModulePathsInterface, A as ActionType, a as PermissionValue, b as PermissionsMap } from './ModulePathsInterface-BrdqgteS.mjs';
4
-
5
- type PageInfo = {
6
- startItem: number;
7
- endItem: number;
8
- pageSize: number;
9
- };
10
- type DataListRetriever<T> = {
11
- ready?: boolean;
12
- setReady: (state: boolean) => void;
13
- isLoaded: boolean;
14
- data: T[] | undefined;
15
- total?: number;
16
- next?: (onlyNewRecords?: boolean) => Promise<void>;
17
- previous?: (onlyNewRecords?: boolean) => Promise<void>;
18
- search: (search: string) => Promise<void>;
19
- refresh: () => Promise<void>;
20
- addAdditionalParameter: (key: string, value: any | null) => void;
21
- removeAdditionalParameter: (key: string) => void;
22
- setRefreshedElement: (element: T) => void;
23
- removeElement: (element: T) => void;
24
- isSearch: boolean;
25
- pageInfo?: PageInfo;
26
- };
27
- declare function useDataListRetriever<T>(params: {
28
- ready?: boolean;
29
- retriever: (params: any) => Promise<T[]>;
30
- retrieverParams?: any;
31
- search?: string;
32
- addAdditionalParameter?: (key: string, value: any | null) => void;
33
- requiresSearch?: boolean;
34
- module: any;
35
- }): DataListRetriever<T>;
36
-
37
- interface OriginalData {
38
- features: FeatureInterface[];
39
- roles: RoleInterface[];
40
- permissionMappings: PermissionMappingInterface[];
41
- moduleRelationshipPaths: Map<string, string[]>;
42
- }
43
- declare function useRbacState(): {
44
- original: OriginalData | null;
45
- isDirty: boolean;
46
- init: (features: FeatureInterface[], roles: RoleInterface[], permissionMappings: PermissionMappingInterface[], modulePaths: ModulePathsInterface[]) => void;
47
- setFeatureIsCore: (featureId: string, isCore: boolean) => void;
48
- setModuleDefaultPermission: (moduleId: string, actionType: ActionType, value: PermissionValue) => void;
49
- setRolePermission: (roleId: string, moduleId: string, actionType: ActionType, value: PermissionValue) => void;
50
- clearRolePermission: (roleId: string, moduleId: string, actionType: ActionType) => void;
51
- clearAllRolePermissions: (roleId: string, moduleId: string) => void;
52
- resetModulePermissions: (moduleId: string, roles: {
53
- id: string;
54
- }[]) => void;
55
- reset: () => void;
56
- getFeatureIsCore: (featureId: string) => boolean;
57
- getModuleDefaultPermission: (moduleId: string, actionType: ActionType) => PermissionValue | undefined;
58
- getRolePermission: (roleId: string, moduleId: string, actionType: ActionType) => PermissionValue | undefined | null;
59
- getEffectiveConfiguration: () => {
60
- features: {
61
- id: string;
62
- name: string;
63
- isCore: boolean;
64
- modules: {
65
- id: string;
66
- name: string;
67
- permissions: PermissionsMap;
68
- }[];
69
- }[];
70
- roles: RoleInterface[];
71
- rolePermissionsMap: Map<string, PermissionsMap>;
72
- } | null;
73
- getModuleRelationshipPaths: (moduleId: string) => string[];
74
- };
75
- type RbacStateApi = ReturnType<typeof useRbacState>;
76
-
77
- export { type DataListRetriever as D, type RbacStateApi as R, useDataListRetriever as a, useRbacState as u };
@@ -1,46 +0,0 @@
1
- "use client";
2
-
3
- import Link from "next/link";
4
- import { useTranslations } from "next-intl";
5
- import type { ApiDataInterface } from "../../../../core";
6
- import { usePageUrlGenerator } from "../../../../hooks";
7
- import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
8
-
9
- interface Props {
10
- references: ApiDataInterface[];
11
- }
12
-
13
- export function ReferenceBadges({ references }: Props) {
14
- const t = useTranslations();
15
- const generate = usePageUrlGenerator();
16
-
17
- if (references.length === 0) return null;
18
-
19
- return (
20
- <div className="mt-2 flex flex-wrap items-center gap-2">
21
- <span className="text-muted-foreground text-xs">{t("features.assistant.references_label")}</span>
22
- {references.map((ref) => {
23
- // ref.type is the JSON:API type string (same as module.name)
24
- let module;
25
- try {
26
- module = ModuleRegistry.findByName(ref.type);
27
- } catch {
28
- return null;
29
- }
30
- const href = generate({ page: module, id: ref.id });
31
- return (
32
- <Link
33
- key={`${ref.type}/${ref.id}`}
34
- href={href}
35
- target="_blank"
36
- rel="noopener noreferrer"
37
- className="bg-background border-border text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs"
38
- >
39
- <span className="text-muted-foreground text-[10px]">{module.name}</span>
40
- <span className="font-medium">{ref.identifier}</span>
41
- </Link>
42
- );
43
- })}
44
- </div>
45
- );
46
- }
@@ -1,52 +0,0 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
- import { useTranslations } from "next-intl";
5
- import { ChevronRight, ChevronDown } from "lucide-react";
6
-
7
- interface Props {
8
- questions: string[];
9
- onSelect: (q: string) => void;
10
- }
11
-
12
- export function SuggestedFollowUps({ questions, onSelect }: Props) {
13
- const t = useTranslations();
14
- const [open, setOpen] = useState(false);
15
- if (questions.length === 0) return null;
16
-
17
- return (
18
- <div className="mt-2">
19
- <button
20
- type="button"
21
- onClick={() => setOpen((v) => !v)}
22
- className="text-primary inline-flex items-center gap-1 text-xs font-medium"
23
- >
24
- {open ? (
25
- <>
26
- <ChevronDown className="h-3 w-3" />
27
- {t("features.assistant.hide_suggestions")}
28
- </>
29
- ) : (
30
- <>
31
- <ChevronRight className="h-3 w-3" />
32
- {t("features.assistant.show_suggestions", { count: questions.length })}
33
- </>
34
- )}
35
- </button>
36
- {open && (
37
- <div className="mt-2 flex flex-col gap-1">
38
- {questions.map((q) => (
39
- <button
40
- key={q}
41
- type="button"
42
- onClick={() => onSelect(q)}
43
- className="border-border bg-muted/30 hover:bg-muted rounded-md border px-3 py-1.5 text-left text-sm"
44
- >
45
- {q}
46
- </button>
47
- ))}
48
- </div>
49
- )}
50
- </div>
51
- );
52
- }
@@ -1,59 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
- import { render, screen } from "@testing-library/react";
3
- import type { ApiDataInterface } from "../../../../../core";
4
- import { AbstractApiData } from "../../../../../core/abstracts/AbstractApiData";
5
- import { DataClassRegistry } from "../../../../../core/registry/DataClassRegistry";
6
- import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
7
- import type { ApiRequestDataTypeInterface } from "../../../../../core/interfaces/ApiRequestDataTypeInterface";
8
- import { ReferenceBadges } from "../ReferenceBadges";
9
-
10
- class TestAccount extends AbstractApiData {
11
- static identifierFields: string[] = ["name"];
12
- rehydrate(data: any): this {
13
- super.rehydrate(data);
14
- return this;
15
- }
16
- createJsonApi(): any {
17
- return {};
18
- }
19
- }
20
-
21
- const testAccountModule = {
22
- name: "test-accounts",
23
- pageUrl: "/accounts",
24
- model: TestAccount,
25
- } as unknown as ApiRequestDataTypeInterface;
26
-
27
- beforeAll(() => {
28
- DataClassRegistry.clear();
29
- DataClassRegistry.registerObjectClass(testAccountModule, TestAccount);
30
- ModuleRegistry.register("TestAccount" as any, testAccountModule as any);
31
- });
32
-
33
- afterAll(() => {
34
- DataClassRegistry.clear();
35
- });
36
-
37
- function makeRehydratedAccount({ id, name }: { id: string; name: string }): ApiDataInterface {
38
- const acct = new TestAccount();
39
- acct.rehydrate({
40
- jsonApi: { type: "test-accounts", id, attributes: { name } },
41
- included: [],
42
- });
43
- return acct as unknown as ApiDataInterface;
44
- }
45
-
46
- describe("ReferenceBadges", () => {
47
- it("renders a chip per reference with type label + identifier", () => {
48
- const references = [makeRehydratedAccount({ id: "acc-1", name: "Acme" })];
49
- render(<ReferenceBadges references={references} />);
50
- const link = screen.getByRole("link", { name: /acme/i });
51
- expect(link).toBeInTheDocument();
52
- expect(link.getAttribute("href")).toContain("/accounts/acc-1");
53
- });
54
-
55
- it("renders nothing for empty references", () => {
56
- const { container } = render(<ReferenceBadges references={[]} />);
57
- expect(container.firstChild).toBeNull();
58
- });
59
- });
@@ -1,29 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { render, screen } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { SuggestedFollowUps } from "../SuggestedFollowUps";
5
-
6
- describe("SuggestedFollowUps", () => {
7
- it("is collapsed by default; toggling reveals the buttons", async () => {
8
- render(<SuggestedFollowUps questions={["q1", "q2", "q3"]} onSelect={vi.fn()} />);
9
- expect(screen.queryByRole("button", { name: "q1" })).not.toBeInTheDocument();
10
- const toggle = screen.getByRole("button", { name: /show_suggestions/ });
11
- await userEvent.click(toggle);
12
- expect(screen.getByRole("button", { name: "q1" })).toBeInTheDocument();
13
- expect(screen.getByRole("button", { name: "q2" })).toBeInTheDocument();
14
- expect(screen.getByRole("button", { name: "q3" })).toBeInTheDocument();
15
- });
16
-
17
- it("clicking a question calls onSelect immediately", async () => {
18
- const onSelect = vi.fn();
19
- render(<SuggestedFollowUps questions={["q1"]} onSelect={onSelect} />);
20
- await userEvent.click(screen.getByRole("button", { name: /show_suggestions/ }));
21
- await userEvent.click(screen.getByRole("button", { name: "q1" }));
22
- expect(onSelect).toHaveBeenCalledWith("q1");
23
- });
24
-
25
- it("renders nothing for empty list", () => {
26
- const { container } = render(<SuggestedFollowUps questions={[]} onSelect={vi.fn()} />);
27
- expect(container.firstChild).toBeNull();
28
- });
29
- });
@@ -1,66 +0,0 @@
1
- "use client";
2
-
3
- import { cn } from "../../../lib/utils";
4
- import { ChevronDownIcon } from "lucide-react";
5
- import { useTranslations } from "next-intl";
6
- import { useState } from "react";
7
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../shadcnui";
8
- import { Switch } from "../../../shadcnui";
9
- import { FeatureInterface } from "../../feature";
10
- import { RoleInterface } from "../../role";
11
- import { RbacStateApi } from "../hooks/useRbacState";
12
- import RbacModuleTable from "./RbacModuleTable";
13
-
14
- interface RbacFeatureSectionProps {
15
- feature: FeatureInterface;
16
- roles: RoleInterface[];
17
- stateApi: RbacStateApi;
18
- }
19
-
20
- export default function RbacFeatureSection({ feature, roles, stateApi }: RbacFeatureSectionProps) {
21
- const t = useTranslations();
22
- const [isOpen, setIsOpen] = useState(true);
23
- const featureIsCore = stateApi.getFeatureIsCore(feature.id);
24
-
25
- return (
26
- <Collapsible open={isOpen} onOpenChange={setIsOpen}>
27
- <div className="rounded-lg border bg-card">
28
- {/* Feature header */}
29
- <CollapsibleTrigger className="w-full">
30
- <div className="flex cursor-pointer items-center justify-between px-4 py-3 hover:bg-muted/50 transition-colors">
31
- <div className="flex items-center gap-3">
32
- <ChevronDownIcon
33
- className={cn("h-4 w-4 text-muted-foreground transition-transform", !isOpen && "-rotate-90")}
34
- />
35
- <h3 className="text-base font-semibold">{feature.name}</h3>
36
- <span className="text-xs text-muted-foreground">
37
- {t("rbac.module_count", { count: feature.modules.length })}
38
- </span>
39
- </div>
40
- <div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
41
- <span className="text-xs text-muted-foreground">{t("rbac.core")}</span>
42
- <Switch
43
- checked={featureIsCore}
44
- onCheckedChange={(checked) => stateApi.setFeatureIsCore(feature.id, checked)}
45
- className="data-checked:bg-accent data-unchecked:bg-gray-300"
46
- />
47
- </div>
48
- </div>
49
- </CollapsibleTrigger>
50
-
51
- <CollapsibleContent>
52
- <div className="space-y-3 p-4 pt-0">
53
- {feature.modules.map((mod) => (
54
- <RbacModuleTable key={mod.id} module={mod} roles={roles} stateApi={stateApi} />
55
- ))}
56
- {feature.modules.length === 0 && (
57
- <p className="text-sm text-muted-foreground italic py-4 text-center">{t("rbac.no_modules")}</p>
58
- )}
59
- </div>
60
- </CollapsibleContent>
61
- </div>
62
- </Collapsible>
63
- );
64
- }
65
-
66
- export { RbacFeatureSection };
@@ -1,121 +0,0 @@
1
- "use client";
2
-
3
- import { RotateCcwIcon } from "lucide-react";
4
- import { useTranslations } from "next-intl";
5
- import { Button } from "../../../shadcnui";
6
- import { ModuleInterface } from "../../module";
7
- import { RoleInterface } from "../../role";
8
- import { ACTION_TYPES, ActionType, COMPANY_ADMINISTRATOR_ROLE_ID } from "../data/RbacTypes";
9
- import { RbacStateApi } from "../hooks/useRbacState";
10
- import RbacPermissionPicker from "./RbacPermissionPicker";
11
-
12
- interface RbacModuleTableProps {
13
- module: ModuleInterface;
14
- roles: RoleInterface[];
15
- stateApi: RbacStateApi;
16
- }
17
-
18
- const ACTION_LABELS: Record<ActionType, string> = {
19
- read: "Read",
20
- create: "Create",
21
- update: "Update",
22
- delete: "Delete",
23
- };
24
-
25
- export default function RbacModuleTable({ module, roles, stateApi }: RbacModuleTableProps) {
26
- const t = useTranslations();
27
- const handleReset = () => {
28
- stateApi.resetModulePermissions(module.id, roles);
29
- };
30
-
31
- return (
32
- <div className="rounded-lg border border-accent bg-card">
33
- {/* Module header */}
34
- <div className="flex items-center justify-between border-b px-4 py-2">
35
- <h4 className="text-sm font-medium">{module.name}</h4>
36
- <Button
37
- variant="ghost"
38
- size="sm"
39
- onClick={handleReset}
40
- className="h-7 px-2 text-muted-foreground hover:text-foreground"
41
- >
42
- <RotateCcwIcon className="h-3.5 w-3.5" />
43
- </Button>
44
- </div>
45
-
46
- {/* Permission grid - Rows = Default + Roles, Columns = Actions */}
47
- <div className="overflow-x-auto">
48
- <table className="w-full text-sm">
49
- <thead>
50
- <tr className="border-b bg-muted/50">
51
- <th className="px-4 py-2 text-left text-xs font-medium text-muted-foreground w-40">{t("rbac.role")}</th>
52
- {ACTION_TYPES.map((actionType) => (
53
- <th
54
- key={actionType}
55
- className="px-2 py-2 text-center text-xs font-medium text-muted-foreground min-w-28"
56
- >
57
- {ACTION_LABELS[actionType]}
58
- </th>
59
- ))}
60
- </tr>
61
- </thead>
62
- <tbody>
63
- {/* Default permissions row */}
64
- <tr className="border-b">
65
- <td className="px-4 py-1 text-xs font-medium text-muted-foreground">{t("rbac.defaults")}</td>
66
- {ACTION_TYPES.map((actionType) => {
67
- const defaultValue = stateApi.getModuleDefaultPermission(module.id, actionType) ?? false;
68
- const originalDefaultValue = module.permissions[actionType] ?? false;
69
-
70
- return (
71
- <td key={actionType} className="px-2 py-1">
72
- <RbacPermissionPicker
73
- value={defaultValue}
74
- originalValue={originalDefaultValue}
75
- isRoleColumn={false}
76
- knownSegments={stateApi.getModuleRelationshipPaths(module.id)}
77
- onSetValue={(value) => stateApi.setModuleDefaultPermission(module.id, actionType, value)}
78
- />
79
- </td>
80
- );
81
- })}
82
- </tr>
83
-
84
- {/* Role rows (CompanyAdministrator hidden — always all-true in migration) */}
85
- {roles
86
- .filter((role) => role.id !== COMPANY_ADMINISTRATOR_ROLE_ID)
87
- .map((role) => (
88
- <tr key={role.id} className="border-b last:border-b-0">
89
- <td className="px-4 py-1 text-xs font-medium text-muted-foreground">{role.name}</td>
90
- {ACTION_TYPES.map((actionType) => {
91
- const roleValue = stateApi.getRolePermission(role.id, module.id, actionType);
92
- const originalMapping = stateApi.original?.permissionMappings.find(
93
- (pm) => pm.roleId === role.id && pm.moduleId === module.id,
94
- );
95
- const originalRoleValue = originalMapping
96
- ? (originalMapping.permissions[actionType] ?? null)
97
- : undefined;
98
-
99
- return (
100
- <td key={actionType} className="px-2 py-1">
101
- <RbacPermissionPicker
102
- value={roleValue}
103
- originalValue={originalRoleValue}
104
- isRoleColumn={true}
105
- knownSegments={stateApi.getModuleRelationshipPaths(module.id)}
106
- onSetValue={(value) => stateApi.setRolePermission(role.id, module.id, actionType, value)}
107
- onClear={() => stateApi.clearRolePermission(role.id, module.id, actionType)}
108
- />
109
- </td>
110
- );
111
- })}
112
- </tr>
113
- ))}
114
- </tbody>
115
- </table>
116
- </div>
117
- </div>
118
- );
119
- }
120
-
121
- export { RbacModuleTable };
@@ -1,40 +0,0 @@
1
- "use client";
2
-
3
- import { DownloadIcon, RotateCcwIcon } from "lucide-react";
4
- import { useTranslations } from "next-intl";
5
- import { Badge, Button } from "../../../shadcnui";
6
-
7
- interface RbacToolbarProps {
8
- isDirty: boolean;
9
- onGenerate: () => void;
10
- onReset: () => void;
11
- }
12
-
13
- export default function RbacToolbar({ isDirty, onGenerate, onReset }: RbacToolbarProps) {
14
- const t = useTranslations();
15
-
16
- return (
17
- <div className="flex items-center justify-between rounded-lg border bg-card px-4 py-3">
18
- <div className="flex items-center gap-3">
19
- <h2 className="text-lg font-semibold">{t("rbac.title")}</h2>
20
- {isDirty && (
21
- <Badge variant="outline" className="border-amber-400 text-amber-600">
22
- {t("rbac.unsaved_changes")}
23
- </Badge>
24
- )}
25
- </div>
26
- <div className="flex items-center gap-2">
27
- <Button variant="outline" size="sm" onClick={onReset} disabled={!isDirty} className="gap-1">
28
- <RotateCcwIcon className="h-3.5 w-3.5" />
29
- {t("rbac.reset")}
30
- </Button>
31
- <Button size="sm" onClick={onGenerate} disabled={!isDirty} className="gap-1">
32
- <DownloadIcon className="h-3.5 w-3.5" />
33
- {t("rbac.generate_migration")}
34
- </Button>
35
- </div>
36
- </div>
37
- );
38
- }
39
-
40
- export { RbacToolbar };