@carlonicora/nextjs-jsonapi 1.68.0 → 1.70.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/{AuthComponent-NwQ_ZXsv.d.mts → AuthComponent-DXe3kPzb.d.mts} +1 -1
- package/dist/{AuthComponent-DL1D3y7f.d.ts → AuthComponent-Di8DsZ2I.d.ts} +1 -1
- package/dist/{BlockNoteEditor-6FDECIS2.mjs → BlockNoteEditor-6XV2IXLY.mjs} +15 -9
- package/dist/BlockNoteEditor-6XV2IXLY.mjs.map +1 -0
- package/dist/{BlockNoteEditor-DXHROT4C.js → BlockNoteEditor-NVPUPZXB.js} +25 -19
- package/dist/BlockNoteEditor-NVPUPZXB.js.map +1 -0
- package/dist/HowToInterface-DtVWAE1s.d.mts +17 -0
- package/dist/HowToInterface-NaqSG9sE.d.ts +17 -0
- package/dist/{auth.interface-BX_1qZZJ.d.ts → auth.interface-BTco8PWs.d.ts} +1 -1
- package/dist/{auth.interface-yeLelxdI.d.mts → auth.interface-C4uJzBec.d.mts} +1 -1
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-37KYO2UD.js → chunk-56VU7A4I.js} +172 -18
- package/dist/chunk-56VU7A4I.js.map +1 -0
- package/dist/{chunk-WOJIRXIP.js → chunk-6ROMPIIP.js} +11 -11
- package/dist/{chunk-WOJIRXIP.js.map → chunk-6ROMPIIP.js.map} +1 -1
- package/dist/{chunk-IOMDNRX5.mjs → chunk-GZNHBAZF.mjs} +155 -1
- package/dist/chunk-GZNHBAZF.mjs.map +1 -0
- package/dist/{chunk-H4ZS3R76.mjs → chunk-LQEKQYUJ.mjs} +2569 -1603
- package/dist/chunk-LQEKQYUJ.mjs.map +1 -0
- package/dist/{chunk-WVTBEVAL.mjs → chunk-WJYWWOTG.mjs} +2 -2
- package/dist/{chunk-ELTHSXBI.js → chunk-ZKOLKFAS.js} +1664 -698
- package/dist/chunk-ZKOLKFAS.js.map +1 -0
- package/dist/client/index.d.mts +5 -6
- package/dist/client/index.d.ts +5 -6
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +83 -10
- package/dist/components/index.d.ts +83 -10
- package/dist/components/index.js +26 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +25 -3
- package/dist/{config-D-mqttuF.d.mts → config-Bmr_0qTn.d.mts} +1 -1
- package/dist/{config-CyCAWW-d.d.ts → config-n0lfSf27.d.ts} +1 -1
- package/dist/contexts/index.d.mts +16 -4
- package/dist/contexts/index.d.ts +16 -4
- package/dist/contexts/index.js +8 -4
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +7 -3
- package/dist/core/index.d.mts +61 -11
- package/dist/core/index.d.ts +61 -11
- package/dist/core/index.js +10 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +9 -1
- package/dist/index.d.mts +9 -10
- package/dist/index.d.ts +9 -10
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10 -2
- package/dist/{notification.interface-ItBxq2au.d.ts → notification.interface-DYDZENx2.d.ts} +18 -1
- package/dist/{notification.interface-C6UcmJqu.d.mts → notification.interface-DrHu_1MM.d.mts} +18 -1
- package/dist/{s3.service-N1g0piXD.d.ts → s3.service-DK2KKXbR.d.ts} +2 -3
- package/dist/{s3.service-CHOTwfWA.d.mts → s3.service-TsN2unZr.d.mts} +2 -3
- package/dist/server/index.d.mts +3 -4
- package/dist/server/index.d.ts +3 -4
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/{useRbacState-CUj0hp8t.d.ts → useRbacState-BYaSdA78.d.ts} +1 -1
- package/dist/{useRbacState-Btk1gkQg.d.mts → useRbacState-CQEJ_ysV.d.mts} +1 -1
- package/dist/{useSocket-BSUN9s3p.d.ts → useSocket-Cjt_qvkI.d.ts} +1 -1
- package/dist/{useSocket-DKI92Fbg.d.mts → useSocket-VAGetcT3.d.mts} +1 -1
- package/package.json +1 -1
- package/src/components/editors/BlockNoteEditor.tsx +7 -1
- package/src/components/forms/FormBlockNote.tsx +6 -0
- package/src/components/forms/FormSelect.tsx +3 -0
- package/src/components/index.ts +1 -0
- package/src/contexts/index.ts +1 -0
- package/src/core/index.ts +2 -0
- package/src/core/registry/ModuleRegistry.ts +19 -0
- package/src/features/how-to/HowToModule.ts +18 -0
- package/src/features/how-to/components/containers/HowToCommand.tsx +230 -0
- package/src/features/how-to/components/containers/HowToCommandViewer.tsx +76 -0
- package/src/features/how-to/components/containers/HowToContainer.tsx +27 -0
- package/src/features/how-to/components/containers/HowToListContainer.tsx +17 -0
- package/src/features/how-to/components/details/HowToContent.tsx +16 -0
- package/src/features/how-to/components/details/HowToDetails.tsx +52 -0
- package/src/features/how-to/components/forms/HowToDeleter.tsx +31 -0
- package/src/features/how-to/components/forms/HowToEditor.tsx +270 -0
- package/src/features/how-to/components/forms/HowToMultiSelector.tsx +152 -0
- package/src/features/how-to/components/forms/HowToSelector.tsx +164 -0
- package/src/features/how-to/components/index.ts +11 -0
- package/src/features/how-to/components/lists/HowToList.tsx +39 -0
- package/src/features/how-to/contexts/HowToContext.tsx +101 -0
- package/src/features/how-to/data/HowTo.ts +69 -0
- package/src/features/how-to/data/HowToFields.ts +10 -0
- package/src/features/how-to/data/HowToInterface.ts +11 -0
- package/src/features/how-to/data/HowToService.ts +61 -0
- package/src/features/how-to/data/index.ts +4 -0
- package/src/features/how-to/hooks/useHowToTableStructure.tsx +86 -0
- package/src/features/how-to/index.ts +2 -0
- package/src/features/how-to/utils/blocknote.ts +108 -0
- package/src/features/how-to/utils/index.ts +1 -0
- package/dist/BlockNoteEditor-6FDECIS2.mjs.map +0 -1
- package/dist/BlockNoteEditor-DXHROT4C.js.map +0 -1
- package/dist/breadcrumb.item.data.interface-CgB4_1EE.d.mts +0 -6
- package/dist/breadcrumb.item.data.interface-CgB4_1EE.d.ts +0 -6
- package/dist/chunk-37KYO2UD.js.map +0 -1
- package/dist/chunk-ELTHSXBI.js.map +0 -1
- package/dist/chunk-H4ZS3R76.mjs.map +0 -1
- package/dist/chunk-IOMDNRX5.mjs.map +0 -1
- package/dist/content.interface-8T5-G84c.d.mts +0 -21
- package/dist/content.interface-D-xdYxjt.d.ts +0 -21
- /package/dist/{chunk-WVTBEVAL.mjs.map → chunk-WJYWWOTG.mjs.map} +0 -0
package/dist/server/index.d.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { A as ApiData } from '../ApiData-DPKNfY-9.js';
|
|
2
|
-
import { M as ModuleWithPermissions, A as Action } from '../notification.interface-
|
|
2
|
+
import { M as ModuleWithPermissions, A as Action } from '../notification.interface-DYDZENx2.js';
|
|
3
3
|
import { A as ApiRequestDataTypeInterface } from '../ApiRequestDataTypeInterface-CYEcRUrh.js';
|
|
4
4
|
import { A as ApiResponseInterface } from '../ApiResponseInterface-CAIAeP5d.js';
|
|
5
|
-
export { A as ServerAuthService, C as ServerCompanyService, a as ServerContentService, F as ServerFeatureService, N as ServerNotificationService, P as ServerPushService, R as ServerRoleService, S as ServerS3Service, U as ServerUserService } from '../s3.service-
|
|
5
|
+
export { A as ServerAuthService, C as ServerCompanyService, a as ServerContentService, F as ServerFeatureService, N as ServerNotificationService, P as ServerPushService, R as ServerRoleService, S as ServerS3Service, U as ServerUserService } from '../s3.service-DK2KKXbR.js';
|
|
6
6
|
import 'lucide-react';
|
|
7
7
|
import '../ApiDataInterface-DPP8s46n.js';
|
|
8
8
|
import '../feature.interface-CIWxo8NP.js';
|
|
9
|
-
import '../auth.interface-
|
|
10
|
-
import '../content.interface-D-xdYxjt.js';
|
|
9
|
+
import '../auth.interface-BTco8PWs.js';
|
|
11
10
|
|
|
12
11
|
type CacheProfile = "seconds" | "minutes" | "hours" | "days" | "weeks" | "max" | "default";
|
|
13
12
|
/**
|
package/dist/server/index.js
CHANGED
|
@@ -15,7 +15,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
var
|
|
18
|
+
var _chunk56VU7A4Ijs = require('../chunk-56VU7A4I.js');
|
|
19
19
|
require('../chunk-LXKSUWAV.js');
|
|
20
20
|
require('../chunk-IBS6NI7D.js');
|
|
21
21
|
|
|
@@ -86,7 +86,7 @@ var ServerSession = class {
|
|
|
86
86
|
if (!rawModules) return false;
|
|
87
87
|
const modules = JSON.parse(_pako2.default.ungzip(Buffer.from(rawModules, "base64"), { to: "string" }));
|
|
88
88
|
const selectedModule = modules.find((module) => module.id === params.module.moduleId);
|
|
89
|
-
return
|
|
89
|
+
return _chunk56VU7A4Ijs.checkPermissionsFromServer.call(void 0, {
|
|
90
90
|
module: params.module,
|
|
91
91
|
action: params.action,
|
|
92
92
|
data: params.data,
|
|
@@ -296,5 +296,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
|
|
|
296
296
|
|
|
297
297
|
|
|
298
298
|
|
|
299
|
-
exports.ServerAuthService =
|
|
299
|
+
exports.ServerAuthService = _chunk56VU7A4Ijs.AuthService; exports.ServerCompanyService = _chunk56VU7A4Ijs.CompanyService; exports.ServerContentService = _chunk56VU7A4Ijs.ContentService; exports.ServerFeatureService = _chunk56VU7A4Ijs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunk56VU7A4Ijs.NotificationService; exports.ServerPushService = _chunk56VU7A4Ijs.PushService; exports.ServerRoleService = _chunk56VU7A4Ijs.RoleService; exports.ServerS3Service = _chunk56VU7A4Ijs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunk56VU7A4Ijs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
|
|
300
300
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { F as FeatureInterface } from './feature.interface-CIWxo8NP.js';
|
|
2
|
-
import { R as RoleInterface } from './notification.interface-
|
|
2
|
+
import { R as RoleInterface } from './notification.interface-DYDZENx2.js';
|
|
3
3
|
import { P as PermissionMappingInterface, M as ModulePathsInterface, A as ActionType, a as PermissionValue, b as PermissionsMap } from './ModulePathsInterface-wVS5Raa4.js';
|
|
4
4
|
|
|
5
5
|
type PageInfo = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { F as FeatureInterface } from './feature.interface-BxFFOPNq.mjs';
|
|
2
|
-
import { R as RoleInterface } from './notification.interface-
|
|
2
|
+
import { R as RoleInterface } from './notification.interface-DrHu_1MM.mjs';
|
|
3
3
|
import { P as PermissionMappingInterface, M as ModulePathsInterface, A as ActionType, a as PermissionValue, b as PermissionsMap } from './ModulePathsInterface-49EWvbWy.mjs';
|
|
4
4
|
|
|
5
5
|
type PageInfo = {
|
package/package.json
CHANGED
|
@@ -27,6 +27,8 @@ export type BlockNoteEditorProps = {
|
|
|
27
27
|
diffContent?: PartialBlock[];
|
|
28
28
|
placeholder?: string;
|
|
29
29
|
bordered?: boolean;
|
|
30
|
+
inlineContentSpecs?: Record<string, any>;
|
|
31
|
+
renderOverlays?: (editor: any) => React.ReactNode;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
const createDiffActionsInlineContentSpec = (
|
|
@@ -88,6 +90,8 @@ export default function BlockNoteEditor({
|
|
|
88
90
|
diffContent,
|
|
89
91
|
placeholder,
|
|
90
92
|
bordered,
|
|
93
|
+
inlineContentSpecs,
|
|
94
|
+
renderOverlays,
|
|
91
95
|
}: BlockNoteEditorProps): React.JSX.Element {
|
|
92
96
|
const t = useTranslations();
|
|
93
97
|
const { company } = useCurrentUserContext<UserInterface>();
|
|
@@ -141,9 +145,10 @@ export default function BlockNoteEditor({
|
|
|
141
145
|
inlineContentSpecs: {
|
|
142
146
|
...defaultInlineContentSpecs,
|
|
143
147
|
diffActions: DiffActionsInlineContent,
|
|
148
|
+
...inlineContentSpecs,
|
|
144
149
|
},
|
|
145
150
|
} as any),
|
|
146
|
-
[DiffActionsInlineContent],
|
|
151
|
+
[DiffActionsInlineContent, inlineContentSpecs],
|
|
147
152
|
);
|
|
148
153
|
|
|
149
154
|
const uploadImage = useCallback(
|
|
@@ -422,6 +427,7 @@ export default function BlockNoteEditor({
|
|
|
422
427
|
className={cn(`BlockNoteView flex-1 ${onChange ? "p-4" : ""}`, size === "sm" && "small")}
|
|
423
428
|
>
|
|
424
429
|
<BlockNoteEditorFormattingToolbar />
|
|
430
|
+
{renderOverlays?.(editor)}
|
|
425
431
|
</BlockNoteView>
|
|
426
432
|
</div>
|
|
427
433
|
);
|
|
@@ -14,6 +14,8 @@ export function FormBlockNote({
|
|
|
14
14
|
description,
|
|
15
15
|
testId,
|
|
16
16
|
onEmptyChange,
|
|
17
|
+
inlineContentSpecs,
|
|
18
|
+
renderOverlays,
|
|
17
19
|
}: {
|
|
18
20
|
form: any;
|
|
19
21
|
id: string;
|
|
@@ -24,6 +26,8 @@ export function FormBlockNote({
|
|
|
24
26
|
description?: string;
|
|
25
27
|
testId?: string;
|
|
26
28
|
onEmptyChange?: (isEmpty: boolean) => void;
|
|
29
|
+
inlineContentSpecs?: Record<string, any>;
|
|
30
|
+
renderOverlays?: (editor: any) => React.ReactNode;
|
|
27
31
|
}) {
|
|
28
32
|
return (
|
|
29
33
|
<div className="flex w-full flex-col">
|
|
@@ -46,6 +50,8 @@ export function FormBlockNote({
|
|
|
46
50
|
}}
|
|
47
51
|
placeholder={placeholder}
|
|
48
52
|
bordered
|
|
53
|
+
inlineContentSpecs={inlineContentSpecs}
|
|
54
|
+
renderOverlays={renderOverlays}
|
|
49
55
|
/>
|
|
50
56
|
)}
|
|
51
57
|
</FormFieldWrapper>
|
|
@@ -16,6 +16,7 @@ export function FormSelect({
|
|
|
16
16
|
useRows,
|
|
17
17
|
testId,
|
|
18
18
|
allowEmpty,
|
|
19
|
+
isRequired,
|
|
19
20
|
}: {
|
|
20
21
|
form: any;
|
|
21
22
|
id: string;
|
|
@@ -27,6 +28,7 @@ export function FormSelect({
|
|
|
27
28
|
useRows?: boolean;
|
|
28
29
|
testId?: string;
|
|
29
30
|
allowEmpty?: boolean;
|
|
31
|
+
isRequired?: boolean;
|
|
30
32
|
}) {
|
|
31
33
|
return (
|
|
32
34
|
<div className="flex w-full flex-col">
|
|
@@ -34,6 +36,7 @@ export function FormSelect({
|
|
|
34
36
|
form={form}
|
|
35
37
|
name={id}
|
|
36
38
|
label={name}
|
|
39
|
+
isRequired={isRequired}
|
|
37
40
|
orientation={useRows ? "horizontal" : "vertical"}
|
|
38
41
|
testId={testId}
|
|
39
42
|
>
|
package/src/components/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from "../features/auth/components";
|
|
|
19
19
|
// Billing components moved to separate entry point: @carlonicora/nextjs-jsonapi/billing
|
|
20
20
|
export * from "../features/company/components";
|
|
21
21
|
export * from "../features/content/components";
|
|
22
|
+
export * from "../features/how-to/components";
|
|
22
23
|
export * from "../features/feature/components";
|
|
23
24
|
export * from "../features/notification/components";
|
|
24
25
|
export * from "../features/onboarding/components";
|
package/src/contexts/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "../features/notification/contexts/NotificationContext";
|
|
|
3
3
|
export * from "../features/onboarding/contexts";
|
|
4
4
|
export * from "../features/role/contexts/RoleContext";
|
|
5
5
|
export * from "../features/user/contexts";
|
|
6
|
+
export * from "../features/how-to/contexts/HowToContext";
|
|
6
7
|
export * from "./CommonContext";
|
|
7
8
|
export * from "./HeaderChildrenContext";
|
|
8
9
|
export * from "./SharedContext";
|
package/src/core/index.ts
CHANGED
|
@@ -59,6 +59,8 @@ export * from "../features/company/company.module";
|
|
|
59
59
|
export * from "../features/company/data";
|
|
60
60
|
export * from "../features/content/content.module";
|
|
61
61
|
export * from "../features/content/data";
|
|
62
|
+
export * from "../features/how-to/HowToModule";
|
|
63
|
+
export * from "../features/how-to/data";
|
|
62
64
|
export * from "../features/feature/data";
|
|
63
65
|
export * from "../features/feature/feature.module";
|
|
64
66
|
export * from "../features/module";
|
|
@@ -15,6 +15,7 @@ export interface FoundationModuleDefinitions {
|
|
|
15
15
|
Feature: ModuleWithPermissions;
|
|
16
16
|
Module: ModuleWithPermissions;
|
|
17
17
|
Content: ModuleWithPermissions;
|
|
18
|
+
HowTo: ModuleWithPermissions;
|
|
18
19
|
// Billing modules - READ: all users, UPDATE: CompanyAdministrator, ADMIN: Administrator
|
|
19
20
|
Billing: ModuleWithPermissions;
|
|
20
21
|
StripeCustomer: ModuleWithPermissions;
|
|
@@ -122,6 +123,24 @@ class ModuleRegistryClass {
|
|
|
122
123
|
throw new Error(`Module not found: ${moduleName}`);
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
getAllPageUrls(): { id: string; text: string }[] {
|
|
127
|
+
if (this._modules.size === 0) {
|
|
128
|
+
tryBootstrap();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const seen = new Set<string>();
|
|
132
|
+
const result: { id: string; text: string }[] = [];
|
|
133
|
+
for (const [key, module] of this._modules.entries()) {
|
|
134
|
+
const m = module as ModuleWithPermissions;
|
|
135
|
+
if (m.pageUrl && !seen.has(m.pageUrl)) {
|
|
136
|
+
seen.add(m.pageUrl);
|
|
137
|
+
result.push({ id: m.pageUrl, text: key });
|
|
138
|
+
result.push({ id: `${m.pageUrl}/:id`, text: `${key} (detail)` });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return result.sort((a, b) => a.text.localeCompare(b.text));
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
findByModelName(modelName: string): ModuleWithPermissions {
|
|
126
145
|
// Direct lookup by registry key (e.g., "Article", "Document")
|
|
127
146
|
let module = this._modules.get(modelName);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LifeBuoyIcon } from "lucide-react";
|
|
2
|
+
import { createJsonApiInclusion } from "../../core";
|
|
3
|
+
import { ModuleFactory } from "../../permissions";
|
|
4
|
+
import { HowTo } from "./data/HowTo";
|
|
5
|
+
|
|
6
|
+
export const HowToModule = (factory: ModuleFactory) =>
|
|
7
|
+
factory({
|
|
8
|
+
moduleId: "6f975207-0df3-4c0d-b541-ed5dc04487b2",
|
|
9
|
+
pageUrl: "/administration/howtos",
|
|
10
|
+
name: "howtos",
|
|
11
|
+
model: HowTo,
|
|
12
|
+
icon: LifeBuoyIcon,
|
|
13
|
+
inclusions: {
|
|
14
|
+
lists: {
|
|
15
|
+
fields: [createJsonApiInclusion("howtos", [`name`, `description`, `pages`])],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowRight, LifeBuoyIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Command,
|
|
9
|
+
CommandEmpty,
|
|
10
|
+
CommandGroup,
|
|
11
|
+
CommandInput,
|
|
12
|
+
CommandItem,
|
|
13
|
+
CommandList,
|
|
14
|
+
Dialog,
|
|
15
|
+
DialogContent,
|
|
16
|
+
DialogDescription,
|
|
17
|
+
DialogHeader,
|
|
18
|
+
DialogTitle,
|
|
19
|
+
SidebarMenuButton,
|
|
20
|
+
} from "../../../../shadcnui";
|
|
21
|
+
import { Modules } from "../../../../core";
|
|
22
|
+
import { DataListRetriever, useDataListRetriever, useDebounce } from "../../../../hooks";
|
|
23
|
+
import { HowTo } from "../../data/HowTo";
|
|
24
|
+
import { HowToInterface } from "../../data/HowToInterface";
|
|
25
|
+
import { HowToService } from "../../data/HowToService";
|
|
26
|
+
import HowToCommandViewer from "./HowToCommandViewer";
|
|
27
|
+
|
|
28
|
+
function matchPage(pathname: string, pattern: string): boolean {
|
|
29
|
+
if (pattern.includes(":")) {
|
|
30
|
+
const pathSegments = pathname.split("/").filter(Boolean);
|
|
31
|
+
const patternSegments = pattern.split("/").filter(Boolean);
|
|
32
|
+
if (pathSegments.length !== patternSegments.length) return false;
|
|
33
|
+
return patternSegments.every((seg, i) => (seg.startsWith(":") ? true : seg === pathSegments[i]));
|
|
34
|
+
}
|
|
35
|
+
return pathname.toLowerCase().includes(pattern.toLowerCase());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type HowToCommandProps = {
|
|
39
|
+
/** Current pathname for page relevance matching */
|
|
40
|
+
pathname: string;
|
|
41
|
+
/** Optional extra command groups to render when not searching */
|
|
42
|
+
extraGroups?: ReactNode;
|
|
43
|
+
/** Called when user starts a chat from the viewer */
|
|
44
|
+
onStartChat?: () => void;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default function HowToCommand({ pathname, extraGroups, onStartChat }: HowToCommandProps) {
|
|
48
|
+
const t = useTranslations();
|
|
49
|
+
|
|
50
|
+
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
|
51
|
+
const [selectedHowTo, setSelectedHowTo] = useState<HowToInterface | null>(null);
|
|
52
|
+
|
|
53
|
+
const searchTermRef = useRef<string>("");
|
|
54
|
+
const [searchTerm, setSearchTerm] = useState<string>("");
|
|
55
|
+
|
|
56
|
+
const data: DataListRetriever<HowToInterface> = useDataListRetriever({
|
|
57
|
+
retriever: (params) => {
|
|
58
|
+
return HowToService.findMany(params);
|
|
59
|
+
},
|
|
60
|
+
retrieverParams: {},
|
|
61
|
+
module: Modules.HowTo,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Split HowTos into relevant (matching current page) and others
|
|
65
|
+
const { relevantHowTos, otherHowTos } = useMemo(() => {
|
|
66
|
+
if (!data.data) return { relevantHowTos: [], otherHowTos: [] };
|
|
67
|
+
|
|
68
|
+
const relevant: HowToInterface[] = [];
|
|
69
|
+
const other: HowToInterface[] = [];
|
|
70
|
+
|
|
71
|
+
(data.data as HowToInterface[]).forEach((howTo) => {
|
|
72
|
+
const pages = HowTo.parsePagesFromString(howTo.pages);
|
|
73
|
+
const isRelevant = pages.some((page) => page && matchPage(pathname, page));
|
|
74
|
+
if (isRelevant) {
|
|
75
|
+
relevant.push(howTo);
|
|
76
|
+
} else {
|
|
77
|
+
other.push(howTo);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return { relevantHowTos: relevant, otherHowTos: other };
|
|
82
|
+
}, [data.data, pathname]);
|
|
83
|
+
|
|
84
|
+
const search = useCallback(
|
|
85
|
+
async (searchedTerm: string) => {
|
|
86
|
+
if (searchedTerm === searchTermRef.current) return;
|
|
87
|
+
searchTermRef.current = searchedTerm;
|
|
88
|
+
await data.search(searchedTerm);
|
|
89
|
+
},
|
|
90
|
+
[searchTermRef, data],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const updateSearchTerm = useDebounce(search, 500);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
updateSearchTerm(searchTerm);
|
|
97
|
+
}, [updateSearchTerm, searchTerm]);
|
|
98
|
+
|
|
99
|
+
// Reset search when dialog closes
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!dialogOpen) {
|
|
102
|
+
setSearchTerm("");
|
|
103
|
+
searchTermRef.current = "";
|
|
104
|
+
}
|
|
105
|
+
}, [dialogOpen]);
|
|
106
|
+
|
|
107
|
+
// Keyboard shortcut: Cmd+K or Ctrl+K to toggle dialog
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const down = (e: KeyboardEvent) => {
|
|
110
|
+
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
setDialogOpen((open) => !open);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
document.addEventListener("keydown", down);
|
|
117
|
+
return () => document.removeEventListener("keydown", down);
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const handleStartChat = () => {
|
|
121
|
+
setDialogOpen(false);
|
|
122
|
+
setSelectedHowTo(null);
|
|
123
|
+
if (onStartChat) onStartChat();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<>
|
|
128
|
+
<SidebarMenuButton
|
|
129
|
+
tooltip={t(`howto.command.trigger`)}
|
|
130
|
+
onClick={() => setDialogOpen(true)}
|
|
131
|
+
className="text-muted-foreground"
|
|
132
|
+
>
|
|
133
|
+
<LifeBuoyIcon />
|
|
134
|
+
<span>{t(`howto.command.trigger`)}</span>
|
|
135
|
+
</SidebarMenuButton>
|
|
136
|
+
|
|
137
|
+
{/* Search HowTos Dialog */}
|
|
138
|
+
<Dialog
|
|
139
|
+
open={dialogOpen}
|
|
140
|
+
onOpenChange={(open) => {
|
|
141
|
+
setDialogOpen(open);
|
|
142
|
+
if (!open) setSelectedHowTo(null);
|
|
143
|
+
}}
|
|
144
|
+
modal={true}
|
|
145
|
+
>
|
|
146
|
+
<DialogContent
|
|
147
|
+
className={`flex flex-col gap-0 overflow-hidden p-0 ${selectedHowTo ? "h-[80vh] max-w-3xl" : "max-w-lg"}`}
|
|
148
|
+
>
|
|
149
|
+
<DialogHeader className="sr-only">
|
|
150
|
+
<DialogTitle>{selectedHowTo ? selectedHowTo.name : t("howto.command.title")}</DialogTitle>
|
|
151
|
+
<DialogDescription> </DialogDescription>
|
|
152
|
+
</DialogHeader>
|
|
153
|
+
|
|
154
|
+
{selectedHowTo ? (
|
|
155
|
+
<HowToCommandViewer
|
|
156
|
+
howTo={selectedHowTo}
|
|
157
|
+
onBack={() => setSelectedHowTo(null)}
|
|
158
|
+
onStartChat={handleStartChat}
|
|
159
|
+
/>
|
|
160
|
+
) : (
|
|
161
|
+
<Command
|
|
162
|
+
shouldFilter={false}
|
|
163
|
+
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
|
164
|
+
>
|
|
165
|
+
<CommandInput
|
|
166
|
+
onKeyDown={(e) => {
|
|
167
|
+
if (e.key === "Escape") {
|
|
168
|
+
setSearchTerm("");
|
|
169
|
+
searchTermRef.current = "";
|
|
170
|
+
}
|
|
171
|
+
}}
|
|
172
|
+
onValueChange={setSearchTerm}
|
|
173
|
+
value={searchTerm}
|
|
174
|
+
placeholder={t(`howto.search.placeholder`)}
|
|
175
|
+
/>
|
|
176
|
+
<CommandList className="max-h-[60vh] overflow-y-auto">
|
|
177
|
+
<CommandEmpty>{t(`howto.command.empty`)}</CommandEmpty>
|
|
178
|
+
|
|
179
|
+
{/* App-specific extra groups (support, tours, etc.) */}
|
|
180
|
+
{!searchTerm && extraGroups}
|
|
181
|
+
|
|
182
|
+
{/* Relevant to this page */}
|
|
183
|
+
{relevantHowTos.length > 0 && (
|
|
184
|
+
<CommandGroup heading={t(`howto.command.relevant`)}>
|
|
185
|
+
{relevantHowTos.map((howTo: HowToInterface) => (
|
|
186
|
+
<CommandItem key={howTo.id} onSelect={() => setSelectedHowTo(howTo)} className="cursor-pointer">
|
|
187
|
+
<ArrowRight className="h-4 w-4" />
|
|
188
|
+
<span>{howTo.name}</span>
|
|
189
|
+
</CommandItem>
|
|
190
|
+
))}
|
|
191
|
+
</CommandGroup>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{/* All HowTos / Search Results */}
|
|
195
|
+
{otherHowTos.length > 0 && (
|
|
196
|
+
<CommandGroup heading={searchTerm ? undefined : t(`howto.command.all`)}>
|
|
197
|
+
{otherHowTos.map((howTo: HowToInterface) => (
|
|
198
|
+
<CommandItem key={howTo.id} onSelect={() => setSelectedHowTo(howTo)} className="cursor-pointer">
|
|
199
|
+
<ArrowRight className="h-4 w-4" />
|
|
200
|
+
<span>{howTo.name}</span>
|
|
201
|
+
</CommandItem>
|
|
202
|
+
))}
|
|
203
|
+
</CommandGroup>
|
|
204
|
+
)}
|
|
205
|
+
</CommandList>
|
|
206
|
+
|
|
207
|
+
{/* Keyboard hints footer */}
|
|
208
|
+
<div className="text-muted-foreground flex items-center justify-center gap-4 border-t px-3 py-2 text-xs">
|
|
209
|
+
<span className="flex items-center gap-1">
|
|
210
|
+
<kbd className="bg-muted rounded border px-1.5 py-0.5 font-mono text-[10px]">⏎</kbd>
|
|
211
|
+
{t(`howto.command.keyboard.select`)}
|
|
212
|
+
</span>
|
|
213
|
+
<span className="flex items-center gap-1">
|
|
214
|
+
<kbd className="bg-muted rounded border px-1.5 py-0.5 font-mono text-[10px]">↑↓</kbd>
|
|
215
|
+
{t(`howto.command.keyboard.navigate`)}
|
|
216
|
+
</span>
|
|
217
|
+
<span className="flex items-center gap-1">
|
|
218
|
+
<kbd className="bg-muted rounded border px-1.5 py-0.5 font-mono text-[10px]">
|
|
219
|
+
{t(`howto.keyboard.esc`)}
|
|
220
|
+
</kbd>
|
|
221
|
+
{t(`howto.command.keyboard.close`)}
|
|
222
|
+
</span>
|
|
223
|
+
</div>
|
|
224
|
+
</Command>
|
|
225
|
+
)}
|
|
226
|
+
</DialogContent>
|
|
227
|
+
</Dialog>
|
|
228
|
+
</>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowLeft, BookOpen, MessageSquare } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
|
|
7
|
+
import { BlockNoteEditorContainer } from "../../../../components";
|
|
8
|
+
import { Button } from "../../../../shadcnui";
|
|
9
|
+
import { HowToInterface } from "../../data/HowToInterface";
|
|
10
|
+
import { calculateReadingTime, extractHeadings } from "../../utils/blocknote";
|
|
11
|
+
|
|
12
|
+
type HowToCommandViewerProps = {
|
|
13
|
+
howTo: HowToInterface;
|
|
14
|
+
onBack: () => void;
|
|
15
|
+
onStartChat?: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function HowToCommandViewer({ howTo, onBack, onStartChat }: HowToCommandViewerProps) {
|
|
19
|
+
const t = useTranslations();
|
|
20
|
+
|
|
21
|
+
const readingTime = useMemo(() => calculateReadingTime(howTo.description), [howTo.description]);
|
|
22
|
+
const headings = useMemo(() => extractHeadings(howTo.description), [howTo.description]);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex h-full flex-col">
|
|
26
|
+
{/* Header with back button, title, and reading time */}
|
|
27
|
+
<div className="flex items-center gap-3 border-b px-4 py-3">
|
|
28
|
+
<Button variant="ghost" size="sm" onClick={onBack} className="h-8 px-2">
|
|
29
|
+
<ArrowLeft className="h-4 w-4" />
|
|
30
|
+
<span className="ml-1">{t("howto.command.back")}</span>
|
|
31
|
+
</Button>
|
|
32
|
+
<h2 className="flex-1 truncate text-lg font-semibold">{howTo.name}</h2>
|
|
33
|
+
<div className="text-muted-foreground flex items-center gap-1.5 text-sm">
|
|
34
|
+
<BookOpen className="h-4 w-4" />
|
|
35
|
+
<span>{t("howto.reading_time.label", { minutes: readingTime })}</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{/* Two-column body */}
|
|
40
|
+
<div className="flex min-h-0 flex-1">
|
|
41
|
+
{/* Left sidebar - table of contents */}
|
|
42
|
+
{headings.length > 0 && (
|
|
43
|
+
<div className="w-50 shrink-0 overflow-y-auto border-r p-4">
|
|
44
|
+
<nav className="space-y-1">
|
|
45
|
+
{headings.map((heading) => (
|
|
46
|
+
<a
|
|
47
|
+
key={heading.id}
|
|
48
|
+
href={`#${heading.id}`}
|
|
49
|
+
className="text-muted-foreground hover:text-foreground block truncate text-sm"
|
|
50
|
+
style={{ paddingLeft: `${(heading.level - 1) * 0.75}rem` }}
|
|
51
|
+
>
|
|
52
|
+
{heading.text}
|
|
53
|
+
</a>
|
|
54
|
+
))}
|
|
55
|
+
</nav>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{/* Right content - scrollable */}
|
|
60
|
+
<div id="howto-viewer-content" className="min-w-0 flex-1 overflow-y-auto p-4">
|
|
61
|
+
<BlockNoteEditorContainer id={howTo.id} type="howto" initialContent={howTo.description} />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Full-width footer */}
|
|
66
|
+
{onStartChat && (
|
|
67
|
+
<div className="flex items-center justify-end gap-2 border-t px-4 py-3">
|
|
68
|
+
<Button onClick={onStartChat} variant="default" size="sm">
|
|
69
|
+
<MessageSquare className="mr-2 h-4 w-4" />
|
|
70
|
+
{t("howto.command.chat_button")}
|
|
71
|
+
</Button>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { RoundPageContainer } from "../../../../components";
|
|
4
|
+
import { Modules } from "../../../../core";
|
|
5
|
+
import { useHowToContext } from "../../contexts/HowToContext";
|
|
6
|
+
import { HowToInterface } from "../../data/HowToInterface";
|
|
7
|
+
import HowToContent from "../details/HowToContent";
|
|
8
|
+
import HowToDetails from "../details/HowToDetails";
|
|
9
|
+
|
|
10
|
+
type HowToContainerProps = {
|
|
11
|
+
howTo: HowToInterface;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function HowToContainerInternal({ howTo }: HowToContainerProps) {
|
|
15
|
+
return (
|
|
16
|
+
<RoundPageContainer module={Modules.HowTo} details={<HowToDetails />}>
|
|
17
|
+
<HowToContent />
|
|
18
|
+
</RoundPageContainer>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function HowToContainer() {
|
|
23
|
+
const { howTo } = useHowToContext();
|
|
24
|
+
if (!howTo) return null;
|
|
25
|
+
|
|
26
|
+
return <HowToContainerInternal howTo={howTo} />;
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { RoundPageContainer } from "../../../../components";
|
|
4
|
+
import { Modules } from "../../../../core";
|
|
5
|
+
import HowToList from "../lists/HowToList";
|
|
6
|
+
|
|
7
|
+
function HowToListContainerInternal() {
|
|
8
|
+
return (
|
|
9
|
+
<RoundPageContainer module={Modules.HowTo} fullWidth>
|
|
10
|
+
<HowToList fullWidth />
|
|
11
|
+
</RoundPageContainer>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function HowToListContainer() {
|
|
16
|
+
return <HowToListContainerInternal />;
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { BlockNoteEditorContainer } from "../../../../components";
|
|
4
|
+
import { Card } from "../../../../shadcnui";
|
|
5
|
+
import { useHowToContext } from "../../contexts/HowToContext";
|
|
6
|
+
|
|
7
|
+
export default function HowToContent() {
|
|
8
|
+
const { howTo } = useHowToContext();
|
|
9
|
+
if (!howTo) return null;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Card className="flex w-full flex-col p-4">
|
|
13
|
+
<BlockNoteEditorContainer id={howTo.id} type="howto" initialContent={howTo.description} />
|
|
14
|
+
</Card>
|
|
15
|
+
);
|
|
16
|
+
}
|