@carlonicora/nextjs-jsonapi 3.12.0 → 3.13.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-DwdBzS9f.d.ts → AssistantInterface-RvZzlcA_.d.ts} +2 -0
- package/dist/{AssistantInterface-DMh-zApr.d.mts → AssistantInterface-jL56dyNc.d.mts} +2 -0
- package/dist/{AssistantMessageInterface-0zyVingf.d.ts → AssistantMessageInterface-CuaMDs2i.d.ts} +1 -1
- package/dist/{AssistantMessageInterface-ddyG114R.d.mts → AssistantMessageInterface-Du3ShThp.d.mts} +1 -1
- package/dist/{BlockNoteEditor-6G27S6P3.js → BlockNoteEditor-CFVSHS46.js} +19 -19
- package/dist/{BlockNoteEditor-6G27S6P3.js.map → BlockNoteEditor-CFVSHS46.js.map} +1 -1
- package/dist/{BlockNoteEditor-CR5KH4AI.mjs → BlockNoteEditor-SERY337S.mjs} +4 -4
- package/dist/{BlockNoteViewer-5MITJ7AD.js → BlockNoteViewer-CWFPYYIU.js} +3 -3
- package/dist/{BlockNoteViewer-5MITJ7AD.js.map → BlockNoteViewer-CWFPYYIU.js.map} +1 -1
- package/dist/{BlockNoteViewer-NJKFUNXP.mjs → BlockNoteViewer-P2VKEFRN.mjs} +2 -2
- package/dist/{ai-connection.module-28f-hux0.d.mts → HandbookSectionInterface-BOxIdBdd.d.mts} +54 -1
- package/dist/{ai-connection.module-Bqwh_DnR.d.ts → HandbookSectionInterface-CAXxbDw0.d.ts} +54 -1
- package/dist/{auth.interface-B6ZQLGjS.d.ts → auth.interface-BC1jp_ni.d.ts} +1 -1
- package/dist/{auth.interface-jYD8Gc4P.d.mts → auth.interface-dR2-6gPM.d.mts} +1 -1
- package/dist/billing/index.js +401 -401
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-L7DZSTFU.js → chunk-IB7DTXWG.js} +846 -129
- package/dist/chunk-IB7DTXWG.js.map +1 -0
- package/dist/{chunk-OKGGYCO5.js → chunk-LVE3BJ6K.js} +2088 -1134
- package/dist/chunk-LVE3BJ6K.js.map +1 -0
- package/dist/{chunk-Q2BREXH3.mjs → chunk-OSFYCSTW.mjs} +724 -7
- package/dist/chunk-OSFYCSTW.mjs.map +1 -0
- package/dist/{chunk-JEGL7KHK.mjs → chunk-UQXE3SME.mjs} +2081 -1127
- package/dist/chunk-UQXE3SME.mjs.map +1 -0
- package/dist/{chunk-HVNEWDRF.mjs → chunk-VZKJIAND.mjs} +2 -2
- package/dist/{chunk-4ETVCRQD.js → chunk-XH4IURUD.js} +7 -7
- package/dist/{chunk-4ETVCRQD.js.map → chunk-XH4IURUD.js.map} +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +258 -7
- package/dist/components/index.d.ts +258 -7
- package/dist/components/index.js +30 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +29 -3
- package/dist/contexts/index.d.mts +17 -5
- package/dist/contexts/index.d.ts +17 -5
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +491 -11
- package/dist/core/index.d.ts +491 -11
- package/dist/core/index.js +38 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +37 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +101 -101
- package/dist/features/tokenusage/index.mjs +3 -3
- package/dist/index.d.mts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +39 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +38 -2
- package/dist/{notification.interface-DYF56CtE.d.ts → notification.interface-CjzV_KII.d.ts} +1 -1
- package/dist/{notification.interface-BWOCvKRP.d.mts → notification.interface-MnRt1Tjm.d.mts} +1 -1
- package/dist/{s3.service-D_dswjgz.d.mts → s3.service-C0Yq2Sx3.d.mts} +2 -2
- package/dist/{s3.service-DserU9VK.d.ts → s3.service-CUGarZs3.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/{useSocket-BZG9oX-L.d.mts → useSocket-BcH7l7B8.d.mts} +1 -1
- package/dist/{useSocket-CiyTtYFj.d.ts → useSocket-DCZd_eJk.d.ts} +1 -1
- package/package.json +1 -1
- package/src/components/containers/ReactMarkdownContainer.tsx +61 -11
- package/src/components/index.ts +1 -0
- package/src/core/index.ts +5 -0
- package/src/core/registry/ModuleRegistry.ts +4 -0
- package/src/features/assistant/components/parts/AssistantComposer.tsx +22 -3
- package/src/features/assistant/components/parts/AssistantSidebar.tsx +13 -3
- package/src/features/assistant/components/parts/AssistantThreadHeader.tsx +10 -3
- package/src/features/assistant/contexts/AssistantContext.tsx +30 -3
- package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +32 -0
- package/src/features/assistant/data/Assistant.ts +2 -0
- package/src/features/assistant/data/AssistantInterface.ts +2 -0
- package/src/features/assistant/data/AssistantService.ts +4 -0
- package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +5 -1
- package/src/features/assistant-message/data/AssistantMessage.ts +27 -2
- package/src/features/content/data/content.interface.ts +1 -1
- package/src/features/handbook/HandbookPageModule.ts +41 -0
- package/src/features/handbook/HandbookSectionModule.ts +35 -0
- package/src/features/handbook/HandbookThreadMessageModule.ts +21 -0
- package/src/features/handbook/HandbookThreadModule.ts +29 -0
- package/src/features/handbook/components/containers/HandbookAdminContainer.tsx +17 -0
- package/src/features/handbook/components/containers/HandbookAskContainer.tsx +142 -0
- package/src/features/handbook/components/containers/HandbookAskSheet.tsx +149 -0
- package/src/features/handbook/components/containers/HandbookPageContainer.tsx +143 -0
- package/src/features/handbook/components/containers/__tests__/HandbookAskContainer.spec.tsx +268 -0
- package/src/features/handbook/components/containers/__tests__/HandbookAskSheet.spec.tsx +143 -0
- package/src/features/handbook/components/containers/__tests__/HandbookPageContainer.spec.tsx +288 -0
- package/src/features/handbook/components/details/HandbookPageDetails.tsx +27 -0
- package/src/features/handbook/components/details/HandbookPageNeighbours.tsx +72 -0
- package/src/features/handbook/components/details/HandbookPageToc.tsx +165 -0
- package/src/features/handbook/components/forms/HandbookSyncButton.tsx +43 -0
- package/src/features/handbook/components/forms/__tests__/HandbookSyncButton.spec.tsx +42 -0
- package/src/features/handbook/components/index.ts +12 -0
- package/src/features/handbook/components/lists/HandbookContents.tsx +196 -0
- package/src/features/handbook/components/lists/HandbookSectionGroup.tsx +153 -0
- package/src/features/handbook/components/lists/__tests__/HandbookContents.spec.tsx +247 -0
- package/src/features/handbook/components/parts/HandbookIndexStatus.tsx +62 -0
- package/src/features/handbook/components/parts/HandbookPageNavigator.tsx +143 -0
- package/src/features/handbook/contexts/HandbookContext.tsx +109 -0
- package/src/features/handbook/contexts/__tests__/HandbookContext.spec.tsx +131 -0
- package/src/features/handbook/data/HandbookPage.spec.ts +136 -0
- package/src/features/handbook/data/HandbookPage.ts +99 -0
- package/src/features/handbook/data/HandbookPageFields.ts +31 -0
- package/src/features/handbook/data/HandbookPageInput.ts +8 -0
- package/src/features/handbook/data/HandbookPageInterface.ts +29 -0
- package/src/features/handbook/data/HandbookPageService.ts +60 -0
- package/src/features/handbook/data/HandbookSection.spec.ts +58 -0
- package/src/features/handbook/data/HandbookSection.ts +73 -0
- package/src/features/handbook/data/HandbookSectionInput.ts +12 -0
- package/src/features/handbook/data/HandbookSectionInterface.ts +26 -0
- package/src/features/handbook/data/HandbookSectionService.ts +26 -0
- package/src/features/handbook/data/HandbookThread.ts +114 -0
- package/src/features/handbook/data/HandbookThreadInterface.ts +31 -0
- package/src/features/handbook/data/HandbookThreadMessage.ts +144 -0
- package/src/features/handbook/data/HandbookThreadMessageInterface.ts +35 -0
- package/src/features/handbook/data/HandbookThreadMessageService.ts +30 -0
- package/src/features/handbook/data/HandbookThreadService.ts +86 -0
- package/src/features/handbook/data/__tests__/HandbookThreadService.spec.ts +112 -0
- package/src/features/handbook/data/handbookDisplay.spec.ts +92 -0
- package/src/features/handbook/data/handbookDisplay.ts +55 -0
- package/src/features/handbook/data/index.ts +16 -0
- package/src/features/handbook/hooks/__tests__/useHandbookAsk.spec.ts +198 -0
- package/src/features/handbook/hooks/useHandbookAsk.ts +213 -0
- package/src/features/handbook/i18n-keys.ts +46 -0
- package/src/features/handbook/index.ts +7 -0
- package/src/features/how-to/components/forms/HowToEditor.tsx +0 -1
- package/src/features/how-to/components/forms/HowToReindexButton.spec.tsx +84 -0
- package/src/features/how-to/components/forms/HowToReindexButton.tsx +46 -0
- package/src/features/how-to/components/index.ts +1 -0
- package/src/features/how-to/components/lists/HowToList.tsx +5 -1
- package/src/features/how-to/data/HowTo.spec.ts +61 -1
- package/src/features/how-to/data/HowToService.ts +13 -0
- package/dist/chunk-JEGL7KHK.mjs.map +0 -1
- package/dist/chunk-L7DZSTFU.js.map +0 -1
- package/dist/chunk-OKGGYCO5.js.map +0 -1
- package/dist/chunk-Q2BREXH3.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-CR5KH4AI.mjs.map → BlockNoteEditor-SERY337S.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-NJKFUNXP.mjs.map → BlockNoteViewer-P2VKEFRN.mjs.map} +0 -0
- /package/dist/{chunk-HVNEWDRF.mjs.map → chunk-VZKJIAND.mjs.map} +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { useSharedContext } from "../../../../contexts";
|
|
5
|
+
import { ApiRequestDataTypeInterface } from "../../../../core/interfaces/ApiRequestDataTypeInterface";
|
|
6
|
+
import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
|
|
7
|
+
import { HandbookProvider, useHandbookContext } from "../HandbookContext";
|
|
8
|
+
|
|
9
|
+
vi.mock("next-intl", () => ({
|
|
10
|
+
useTranslations: () => (key: string) => key,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// The provider builds its breadcrumbs with `generateUrl({ page: Modules.HandbookPage })`,
|
|
14
|
+
// and the `Modules` proxy throws for a key the registry does not hold. Registering is the
|
|
15
|
+
// package's spec convention — copied from HandbookAskContainer.spec.tsx, which registers
|
|
16
|
+
// the same module for the same reason.
|
|
17
|
+
const registerIfAbsent = (key: string, module: ApiRequestDataTypeInterface) => {
|
|
18
|
+
try {
|
|
19
|
+
ModuleRegistry.get(key as any);
|
|
20
|
+
} catch {
|
|
21
|
+
ModuleRegistry.register(key, module);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeAll(() => {
|
|
26
|
+
registerIfAbsent("HandbookPage", { name: "handbookpages", pageUrl: "/administration/handbook" } as any);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function Probe() {
|
|
30
|
+
const { title, breadcrumbs } = useSharedContext();
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<span data-testid="type">{String(title.type)}</span>
|
|
34
|
+
<span data-testid="element">{title.element ?? ""}</span>
|
|
35
|
+
<span data-testid="crumbs">{breadcrumbs.map((b) => b.name).join(" / ")}</span>
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("HandbookProvider", () => {
|
|
41
|
+
it("titles the contents surface with the handbook title and no element", () => {
|
|
42
|
+
render(
|
|
43
|
+
<HandbookProvider>
|
|
44
|
+
<Probe />
|
|
45
|
+
</HandbookProvider>,
|
|
46
|
+
);
|
|
47
|
+
expect(screen.getByTestId("type")).toHaveTextContent("handbook.title");
|
|
48
|
+
expect(screen.getByTestId("element")).toHaveTextContent("");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("titles the reader with the section as type and the page as element", () => {
|
|
52
|
+
render(
|
|
53
|
+
<HandbookProvider
|
|
54
|
+
page={{ id: "p1", title: "Testing the API", section: "03-backend" } as any}
|
|
55
|
+
section={{ key: "03-backend", title: "Backend" } as any}
|
|
56
|
+
>
|
|
57
|
+
<Probe />
|
|
58
|
+
</HandbookProvider>,
|
|
59
|
+
);
|
|
60
|
+
expect(screen.getByTestId("type")).toHaveTextContent("Backend");
|
|
61
|
+
expect(screen.getByTestId("element")).toHaveTextContent("Testing the API");
|
|
62
|
+
expect(screen.getByTestId("crumbs")).toHaveTextContent("handbook.title / Testing the API");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("falls back to the raw section key when no section resource resolved", () => {
|
|
66
|
+
render(
|
|
67
|
+
<HandbookProvider page={{ id: "p1", title: "X", section: "09-workflow" } as any}>
|
|
68
|
+
<Probe />
|
|
69
|
+
</HandbookProvider>,
|
|
70
|
+
);
|
|
71
|
+
expect(screen.getByTestId("type")).toHaveTextContent("09-workflow");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("lets the ask surface override the type and passes functions through", () => {
|
|
75
|
+
function FunctionsProbe() {
|
|
76
|
+
const { title } = useSharedContext();
|
|
77
|
+
return <span data-testid="functions">{title.functions}</span>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
render(
|
|
81
|
+
<HandbookProvider titleType="handbook.chat.title" functions={<button type="button">ask</button>}>
|
|
82
|
+
<FunctionsProbe />
|
|
83
|
+
</HandbookProvider>,
|
|
84
|
+
);
|
|
85
|
+
expect(screen.getByTestId("functions")).toHaveTextContent("ask");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("titles the reader with the translation when the page and section carry one", () => {
|
|
89
|
+
render(
|
|
90
|
+
<HandbookProvider
|
|
91
|
+
page={{ id: "p1", title: "Testing the API", displayTitle: "Testare le API", section: "03-backend" } as any}
|
|
92
|
+
section={{ key: "03-backend", title: "Backend", displayTitle: "Backend (API)" } as any}
|
|
93
|
+
>
|
|
94
|
+
<Probe />
|
|
95
|
+
</HandbookProvider>,
|
|
96
|
+
);
|
|
97
|
+
expect(screen.getByTestId("type")).toHaveTextContent("Backend (API)");
|
|
98
|
+
expect(screen.getByTestId("element")).toHaveTextContent("Testare le API");
|
|
99
|
+
expect(screen.getByTestId("crumbs")).toHaveTextContent("handbook.title / Testare le API");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("falls back to the English title when neither carries a translation", () => {
|
|
103
|
+
render(
|
|
104
|
+
<HandbookProvider
|
|
105
|
+
page={{ id: "p1", title: "Testing the API", displayTitle: undefined, section: "03-backend" } as any}
|
|
106
|
+
section={{ key: "03-backend", title: "Backend", displayTitle: undefined } as any}
|
|
107
|
+
>
|
|
108
|
+
<Probe />
|
|
109
|
+
</HandbookProvider>,
|
|
110
|
+
);
|
|
111
|
+
expect(screen.getByTestId("type")).toHaveTextContent("Backend");
|
|
112
|
+
expect(screen.getByTestId("element")).toHaveTextContent("Testing the API");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("exposes the page and section to useHandbookContext", () => {
|
|
116
|
+
function ContextProbe() {
|
|
117
|
+
const { page, section } = useHandbookContext();
|
|
118
|
+
return <span data-testid="context">{`${page?.title} / ${section?.title}`}</span>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
render(
|
|
122
|
+
<HandbookProvider
|
|
123
|
+
page={{ id: "p1", title: "Testing the API", section: "03-backend" } as any}
|
|
124
|
+
section={{ key: "03-backend", title: "Backend" } as any}
|
|
125
|
+
>
|
|
126
|
+
<ContextProbe />
|
|
127
|
+
</HandbookProvider>,
|
|
128
|
+
);
|
|
129
|
+
expect(screen.getByTestId("context")).toHaveTextContent("Testing the API / Backend");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { ApiRequestDataTypeInterface } from "../../../core/interfaces/ApiRequestDataTypeInterface";
|
|
3
|
+
import { ModuleRegistry } from "../../../core/registry/ModuleRegistry";
|
|
4
|
+
import { HandbookPage } from "./HandbookPage";
|
|
5
|
+
|
|
6
|
+
// createJsonApi reads Modules.HandbookPage.name (resolved lazily via
|
|
7
|
+
// ModuleRegistry), so the module must be registered before the model
|
|
8
|
+
// serialises — mirrors the HowTo.spec setup convention in this package.
|
|
9
|
+
const handbookPageModule: ApiRequestDataTypeInterface = {
|
|
10
|
+
name: "handbookpages",
|
|
11
|
+
model: HandbookPage,
|
|
12
|
+
} as any;
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
ModuleRegistry.register("HandbookPage", handbookPageModule);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("HandbookPage", () => {
|
|
19
|
+
it("rehydrates every attribute", () => {
|
|
20
|
+
const page = new HandbookPage().rehydrate({
|
|
21
|
+
jsonApi: {
|
|
22
|
+
id: "id-1",
|
|
23
|
+
type: "handbookpages",
|
|
24
|
+
attributes: {
|
|
25
|
+
path: "03-backend/module-anatomy.md",
|
|
26
|
+
title: "Module anatomy",
|
|
27
|
+
content: "# Module anatomy",
|
|
28
|
+
contentHash: "abc",
|
|
29
|
+
wordCount: 3,
|
|
30
|
+
aiStatus: "completed",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
} as any);
|
|
34
|
+
|
|
35
|
+
expect(page.path).toBe("03-backend/module-anatomy.md");
|
|
36
|
+
expect(page.title).toBe("Module anatomy");
|
|
37
|
+
expect(page.content).toBe("# Module anatomy");
|
|
38
|
+
expect(page.contentHash).toBe("abc");
|
|
39
|
+
expect(page.wordCount).toBe(3);
|
|
40
|
+
expect(page.aiStatus).toBe("completed");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("defaults a missing word count to zero and a missing status to undefined", () => {
|
|
44
|
+
const page = new HandbookPage().rehydrate({
|
|
45
|
+
jsonApi: {
|
|
46
|
+
id: "id-1",
|
|
47
|
+
type: "handbookpages",
|
|
48
|
+
attributes: { path: "a.md", title: "A", content: "#A", contentHash: "h" },
|
|
49
|
+
},
|
|
50
|
+
} as any);
|
|
51
|
+
|
|
52
|
+
expect(page.wordCount).toBe(0);
|
|
53
|
+
expect(page.aiStatus).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("creates an identity-only JSON:API payload", () => {
|
|
57
|
+
const payload = new HandbookPage().createJsonApi({ id: "id-1" });
|
|
58
|
+
expect(payload.data.type).toBe("handbookpages");
|
|
59
|
+
expect(payload.data.id).toBe("id-1");
|
|
60
|
+
expect(payload.data.attributes).toEqual({});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rehydrates section, order and summary", () => {
|
|
64
|
+
const page = new HandbookPage().rehydrate({
|
|
65
|
+
jsonApi: {
|
|
66
|
+
type: "handbookpages",
|
|
67
|
+
id: "p1",
|
|
68
|
+
attributes: {
|
|
69
|
+
path: "03-backend/testing.md",
|
|
70
|
+
title: "Testing the API",
|
|
71
|
+
section: "03-backend",
|
|
72
|
+
order: "03-backend/testing.md",
|
|
73
|
+
summary: "The spec layout and what is mocked.",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
included: [],
|
|
77
|
+
} as any);
|
|
78
|
+
|
|
79
|
+
expect(page.section).toBe("03-backend");
|
|
80
|
+
expect(page.order).toBe("03-backend/testing.md");
|
|
81
|
+
expect(page.summary).toBe("The spec layout and what is mocked.");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("returns undefined for a missing summary", () => {
|
|
85
|
+
const page = new HandbookPage().rehydrate({
|
|
86
|
+
jsonApi: { type: "handbookpages", id: "p1", attributes: { title: "X" } },
|
|
87
|
+
included: [],
|
|
88
|
+
} as any);
|
|
89
|
+
expect(page.summary).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("rehydrates the translation that travels beside the English original", () => {
|
|
93
|
+
const page = new HandbookPage().rehydrate({
|
|
94
|
+
jsonApi: {
|
|
95
|
+
type: "handbookpages",
|
|
96
|
+
id: "p1",
|
|
97
|
+
attributes: {
|
|
98
|
+
path: "03-backend/testing.md",
|
|
99
|
+
title: "Testing the API",
|
|
100
|
+
summary: "The spec layout and what is mocked.",
|
|
101
|
+
content: "# Testing the API",
|
|
102
|
+
displayTitle: "Testare le API",
|
|
103
|
+
displaySummary: "Come sono organizzati gli spec.",
|
|
104
|
+
displayContent: "# Testare le API",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
included: [],
|
|
108
|
+
} as any);
|
|
109
|
+
|
|
110
|
+
// The translation lives on the SAME node, matched by `path` at ingest
|
|
111
|
+
// time: no second id, no locale in the payload.
|
|
112
|
+
expect(page.id).toBe("p1");
|
|
113
|
+
expect(page.displayTitle).toBe("Testare le API");
|
|
114
|
+
expect(page.displaySummary).toBe("Come sono organizzati gli spec.");
|
|
115
|
+
expect(page.displayContent).toBe("# Testare le API");
|
|
116
|
+
// The English original is still there, and `path` is untouched: ordering
|
|
117
|
+
// and grouping read it and must not move.
|
|
118
|
+
expect(page.title).toBe("Testing the API");
|
|
119
|
+
expect(page.path).toBe("03-backend/testing.md");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("leaves an untranslated page's display fields undefined", () => {
|
|
123
|
+
const page = new HandbookPage().rehydrate({
|
|
124
|
+
jsonApi: {
|
|
125
|
+
type: "handbookpages",
|
|
126
|
+
id: "p1",
|
|
127
|
+
attributes: { title: "Testing the API", summary: "S", content: "# C" },
|
|
128
|
+
},
|
|
129
|
+
included: [],
|
|
130
|
+
} as any);
|
|
131
|
+
|
|
132
|
+
expect(page.displayTitle).toBeUndefined();
|
|
133
|
+
expect(page.displaySummary).toBeUndefined();
|
|
134
|
+
expect(page.displayContent).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { HandbookPageInput } from "./HandbookPageInput";
|
|
3
|
+
import { HandbookPageInterface } from "./HandbookPageInterface";
|
|
4
|
+
|
|
5
|
+
export class HandbookPage extends AbstractApiData implements HandbookPageInterface {
|
|
6
|
+
private _path?: string;
|
|
7
|
+
private _title?: string;
|
|
8
|
+
private _content?: string;
|
|
9
|
+
private _contentHash?: string;
|
|
10
|
+
private _wordCount?: number;
|
|
11
|
+
private _aiStatus?: string;
|
|
12
|
+
private _section?: string;
|
|
13
|
+
private _order?: string;
|
|
14
|
+
private _summary?: string;
|
|
15
|
+
private _displayTitle?: string;
|
|
16
|
+
private _displaySummary?: string;
|
|
17
|
+
private _displayContent?: string;
|
|
18
|
+
|
|
19
|
+
get path(): string {
|
|
20
|
+
return this._path ?? "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get title(): string {
|
|
24
|
+
return this._title ?? "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get content(): string {
|
|
28
|
+
return this._content ?? "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get contentHash(): string {
|
|
32
|
+
return this._contentHash ?? "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get wordCount(): number {
|
|
36
|
+
return this._wordCount ?? 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get aiStatus(): string | undefined {
|
|
40
|
+
return this._aiStatus;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get section(): string {
|
|
44
|
+
return this._section ?? "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get order(): string {
|
|
48
|
+
return this._order ?? "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get summary(): string | undefined {
|
|
52
|
+
return this._summary;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get displayTitle(): string | undefined {
|
|
56
|
+
return this._displayTitle;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get displaySummary(): string | undefined {
|
|
60
|
+
return this._displaySummary;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get displayContent(): string | undefined {
|
|
64
|
+
return this._displayContent;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
68
|
+
super.rehydrate(data);
|
|
69
|
+
|
|
70
|
+
this._path = data.jsonApi.attributes.path;
|
|
71
|
+
this._title = data.jsonApi.attributes.title;
|
|
72
|
+
this._content = data.jsonApi.attributes.content;
|
|
73
|
+
this._contentHash = data.jsonApi.attributes.contentHash;
|
|
74
|
+
this._wordCount = data.jsonApi.attributes.wordCount;
|
|
75
|
+
this._aiStatus = data.jsonApi.attributes.aiStatus;
|
|
76
|
+
this._section = data.jsonApi.attributes.section;
|
|
77
|
+
this._order = data.jsonApi.attributes.order;
|
|
78
|
+
this._summary = data.jsonApi.attributes.summary;
|
|
79
|
+
this._displayTitle = data.jsonApi.attributes.displayTitle;
|
|
80
|
+
this._displaySummary = data.jsonApi.attributes.displaySummary;
|
|
81
|
+
this._displayContent = data.jsonApi.attributes.displayContent;
|
|
82
|
+
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
createJsonApi(data: HandbookPageInput) {
|
|
87
|
+
const response: any = {
|
|
88
|
+
data: {
|
|
89
|
+
type: Modules.HandbookPage.name,
|
|
90
|
+
id: data.id,
|
|
91
|
+
attributes: {},
|
|
92
|
+
relationships: {},
|
|
93
|
+
},
|
|
94
|
+
included: [],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return response;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An ENUM, not a string array: `ContentListTable` indexes its column map by
|
|
3
|
+
* these members and `useDataListRetriever` keys rows by the id member, exactly
|
|
4
|
+
* as HowToFields does.
|
|
5
|
+
*
|
|
6
|
+
* `content` and `contentHash` are deliberately absent: they are detail-only,
|
|
7
|
+
* and a list of a thousand markdown files must not carry their bodies.
|
|
8
|
+
* `displayContent` — the same body in the reader's language — carries the same
|
|
9
|
+
* caveat: it names the field for the detail read and is deliberately NOT in the
|
|
10
|
+
* module's list inclusion.
|
|
11
|
+
*
|
|
12
|
+
* `section`, `order` and `summary` are list-safe for the opposite reason: they
|
|
13
|
+
* are the index fields the contents page groups, orders and describes pages by,
|
|
14
|
+
* so a list that omitted them could not render a table of contents at all.
|
|
15
|
+
* `displayTitle` and `displaySummary` travel with them: the manual is indexed
|
|
16
|
+
* in English and shown translated, so the contents page needs both halves.
|
|
17
|
+
*/
|
|
18
|
+
export enum HandbookPageFields {
|
|
19
|
+
handbookPageId = "handbookPageId",
|
|
20
|
+
|
|
21
|
+
path = "path",
|
|
22
|
+
title = "title",
|
|
23
|
+
displayTitle = "displayTitle",
|
|
24
|
+
section = "section",
|
|
25
|
+
order = "order",
|
|
26
|
+
summary = "summary",
|
|
27
|
+
displaySummary = "displaySummary",
|
|
28
|
+
displayContent = "displayContent",
|
|
29
|
+
wordCount = "wordCount",
|
|
30
|
+
aiStatus = "aiStatus",
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handbook pages are created by the backend ingest from files, never by a
|
|
3
|
+
* client. The input therefore carries identity only — it exists so the model
|
|
4
|
+
* satisfies AbstractApiData's contract, not because anything writes attributes.
|
|
5
|
+
*/
|
|
6
|
+
export type HandbookPageInput = {
|
|
7
|
+
id: string;
|
|
8
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export interface HandbookPageInterface extends ApiDataInterface {
|
|
4
|
+
get path(): string;
|
|
5
|
+
get title(): string;
|
|
6
|
+
get content(): string;
|
|
7
|
+
get contentHash(): string;
|
|
8
|
+
get wordCount(): number;
|
|
9
|
+
get aiStatus(): string | undefined;
|
|
10
|
+
/** The top-level directory of the documentation tree this page belongs to. */
|
|
11
|
+
get section(): string;
|
|
12
|
+
/** The repo-relative path; sorting on it reproduces filesystem order. */
|
|
13
|
+
get order(): string;
|
|
14
|
+
/** The one-line description the tree's README index gives this page, when it has one. */
|
|
15
|
+
get summary(): string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The translated rendering of `title`, when the ingest matched a translation
|
|
18
|
+
* of this page's `path`. The manual is INDEXED in English and SHOWN in the
|
|
19
|
+
* reader's language, so every surface renders `displayTitle ?? title`.
|
|
20
|
+
*/
|
|
21
|
+
get displayTitle(): string | undefined;
|
|
22
|
+
/** The translated `summary`. Absent when the page has no translation. */
|
|
23
|
+
get displaySummary(): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* The translated body. Detail-only, exactly as `content` is: it is the whole
|
|
26
|
+
* markdown file, so it never travels in a list payload.
|
|
27
|
+
*/
|
|
28
|
+
get displayContent(): string | undefined;
|
|
29
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { AbstractService, EndpointCreator, HttpMethod, Modules, NextRef, PreviousRef } from "../../../core";
|
|
2
|
+
import { HandbookPageInterface } from "./HandbookPageInterface";
|
|
3
|
+
|
|
4
|
+
export class HandbookPageService extends AbstractService {
|
|
5
|
+
/**
|
|
6
|
+
* `next` / `prev` are not optional extras: `useDataListRetriever` drives
|
|
7
|
+
* pagination by handing the cursor back here, so a findMany that drops them
|
|
8
|
+
* renders exactly one page and no navigation, however many rows exist.
|
|
9
|
+
* Mirrors HowToService.findMany.
|
|
10
|
+
*/
|
|
11
|
+
static async findMany(
|
|
12
|
+
params: {
|
|
13
|
+
search?: string;
|
|
14
|
+
fetchAll?: boolean;
|
|
15
|
+
next?: NextRef;
|
|
16
|
+
prev?: PreviousRef;
|
|
17
|
+
} = {},
|
|
18
|
+
): Promise<HandbookPageInterface[]> {
|
|
19
|
+
const endpoint = new EndpointCreator({ endpoint: Modules.HandbookPage });
|
|
20
|
+
|
|
21
|
+
if (params.fetchAll) endpoint.addAdditionalParam("fetchAll", "true");
|
|
22
|
+
if (params.search) endpoint.addAdditionalParam("search", params.search);
|
|
23
|
+
if (Modules.HandbookPage.inclusions?.lists?.fields)
|
|
24
|
+
endpoint.limitToFields(Modules.HandbookPage.inclusions.lists.fields);
|
|
25
|
+
if (Modules.HandbookPage.inclusions?.lists?.types)
|
|
26
|
+
endpoint.limitToType(Modules.HandbookPage.inclusions.lists.types);
|
|
27
|
+
|
|
28
|
+
return this.callApi({
|
|
29
|
+
type: Modules.HandbookPage,
|
|
30
|
+
method: HttpMethod.GET,
|
|
31
|
+
endpoint: endpoint.generate(),
|
|
32
|
+
next: params.next,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static async findOne(params: { id: string }): Promise<HandbookPageInterface> {
|
|
37
|
+
return this.callApi<HandbookPageInterface>({
|
|
38
|
+
type: Modules.HandbookPage,
|
|
39
|
+
method: HttpMethod.GET,
|
|
40
|
+
endpoint: new EndpointCreator({ endpoint: Modules.HandbookPage, id: params.id }).generate(),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** POST /handbookpages/sync — replies 204; the caller refetches the list. */
|
|
45
|
+
static async sync(): Promise<void> {
|
|
46
|
+
await this.callApi({
|
|
47
|
+
type: Modules.HandbookPage,
|
|
48
|
+
method: HttpMethod.POST,
|
|
49
|
+
endpoint: new EndpointCreator({ endpoint: Modules.HandbookPage, id: "sync" }).generate(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static async delete(params: { id: string }): Promise<void> {
|
|
54
|
+
await this.callApi({
|
|
55
|
+
type: Modules.HandbookPage,
|
|
56
|
+
method: HttpMethod.DELETE,
|
|
57
|
+
endpoint: new EndpointCreator({ endpoint: Modules.HandbookPage, id: params.id }).generate(),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { HandbookSection } from "./HandbookSection";
|
|
3
|
+
|
|
4
|
+
describe("HandbookSection", () => {
|
|
5
|
+
it("rehydrates every attribute", () => {
|
|
6
|
+
const section = new HandbookSection().rehydrate({
|
|
7
|
+
jsonApi: {
|
|
8
|
+
type: "handbooksections",
|
|
9
|
+
id: "s1",
|
|
10
|
+
attributes: { key: "03-backend", title: "Backend", summary: "The API.", order: "03-backend" },
|
|
11
|
+
},
|
|
12
|
+
included: [],
|
|
13
|
+
} as any);
|
|
14
|
+
|
|
15
|
+
expect(section.key).toBe("03-backend");
|
|
16
|
+
expect(section.title).toBe("Backend");
|
|
17
|
+
expect(section.summary).toBe("The API.");
|
|
18
|
+
expect(section.order).toBe("03-backend");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("defaults missing strings and leaves summary undefined", () => {
|
|
22
|
+
const section = new HandbookSection().rehydrate({
|
|
23
|
+
jsonApi: { type: "handbooksections", id: "s1", attributes: {} },
|
|
24
|
+
included: [],
|
|
25
|
+
} as any);
|
|
26
|
+
|
|
27
|
+
expect(section.key).toBe("");
|
|
28
|
+
expect(section.title).toBe("");
|
|
29
|
+
expect(section.summary).toBeUndefined();
|
|
30
|
+
expect(section.displayTitle).toBeUndefined();
|
|
31
|
+
expect(section.displaySummary).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("rehydrates the translation beside the English original, leaving the key alone", () => {
|
|
35
|
+
const section = new HandbookSection().rehydrate({
|
|
36
|
+
jsonApi: {
|
|
37
|
+
type: "handbooksections",
|
|
38
|
+
id: "s1",
|
|
39
|
+
attributes: {
|
|
40
|
+
key: "03-backend",
|
|
41
|
+
title: "Backend",
|
|
42
|
+
summary: "The API.",
|
|
43
|
+
order: "03-backend",
|
|
44
|
+
displayTitle: "Backend (API)",
|
|
45
|
+
displaySummary: "Le API.",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
included: [],
|
|
49
|
+
} as any);
|
|
50
|
+
|
|
51
|
+
expect(section.displayTitle).toBe("Backend (API)");
|
|
52
|
+
expect(section.displaySummary).toBe("Le API.");
|
|
53
|
+
// `key` and `order` are what pages are matched and sorted by; they stay
|
|
54
|
+
// English whatever the translation says.
|
|
55
|
+
expect(section.key).toBe("03-backend");
|
|
56
|
+
expect(section.order).toBe("03-backend");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { HandbookSectionInput } from "./HandbookSectionInput";
|
|
3
|
+
import { HandbookSectionInterface } from "./HandbookSectionInterface";
|
|
4
|
+
|
|
5
|
+
export class HandbookSection extends AbstractApiData implements HandbookSectionInterface {
|
|
6
|
+
private _key?: string;
|
|
7
|
+
private _title?: string;
|
|
8
|
+
private _summary?: string;
|
|
9
|
+
private _order?: string;
|
|
10
|
+
private _displayTitle?: string;
|
|
11
|
+
private _displaySummary?: string;
|
|
12
|
+
|
|
13
|
+
get key(): string {
|
|
14
|
+
return this._key ?? "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get title(): string {
|
|
18
|
+
return this._title ?? "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get summary(): string | undefined {
|
|
22
|
+
return this._summary;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get order(): string {
|
|
26
|
+
return this._order ?? "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get displayTitle(): string | undefined {
|
|
30
|
+
return this._displayTitle;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get displaySummary(): string | undefined {
|
|
34
|
+
return this._displaySummary;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
38
|
+
super.rehydrate(data);
|
|
39
|
+
|
|
40
|
+
this._key = data.jsonApi.attributes.key;
|
|
41
|
+
this._title = data.jsonApi.attributes.title;
|
|
42
|
+
this._summary = data.jsonApi.attributes.summary;
|
|
43
|
+
this._order = data.jsonApi.attributes.order;
|
|
44
|
+
this._displayTitle = data.jsonApi.attributes.displayTitle;
|
|
45
|
+
this._displaySummary = data.jsonApi.attributes.displaySummary;
|
|
46
|
+
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Nothing calls this: sections are read-only over HTTP and written only by
|
|
52
|
+
* the ingest. It exists because every model implements `createJsonApi()`, and
|
|
53
|
+
* a model that throws on serialisation is a trap for the next writer.
|
|
54
|
+
*/
|
|
55
|
+
createJsonApi(data: HandbookSectionInput) {
|
|
56
|
+
const response: any = {
|
|
57
|
+
data: {
|
|
58
|
+
type: Modules.HandbookSection.name,
|
|
59
|
+
id: data.id,
|
|
60
|
+
attributes: {},
|
|
61
|
+
relationships: {},
|
|
62
|
+
},
|
|
63
|
+
included: [],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (data.key !== undefined) response.data.attributes.key = data.key;
|
|
67
|
+
if (data.title !== undefined) response.data.attributes.title = data.title;
|
|
68
|
+
if (data.summary !== undefined) response.data.attributes.summary = data.summary;
|
|
69
|
+
if (data.order !== undefined) response.data.attributes.order = data.order;
|
|
70
|
+
|
|
71
|
+
return response;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handbook sections are written only by the backend ingest, which replaces
|
|
3
|
+
* every one of them on each sync. No client ever posts one; this type exists so
|
|
4
|
+
* the model can satisfy AbstractApiData's `createJsonApi()` contract.
|
|
5
|
+
*/
|
|
6
|
+
export type HandbookSectionInput = {
|
|
7
|
+
id: string;
|
|
8
|
+
key: string;
|
|
9
|
+
title: string;
|
|
10
|
+
summary?: string;
|
|
11
|
+
order: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One top-level directory of the configured documentation tree.
|
|
5
|
+
*
|
|
6
|
+
* A page names its section by `key`, not by edge, so this interface carries no
|
|
7
|
+
* relationships: the contents page reads sections and pages in two calls and
|
|
8
|
+
* groups them client side.
|
|
9
|
+
*/
|
|
10
|
+
export interface HandbookSectionInterface extends ApiDataInterface {
|
|
11
|
+
/** The directory name, e.g. `03-backend`. The natural key of the ingest. */
|
|
12
|
+
get key(): string;
|
|
13
|
+
/** The prettified `key`, or the README heading when the index names one. */
|
|
14
|
+
get title(): string;
|
|
15
|
+
/** The README's blurb for the section. Absent when the tree has no index. */
|
|
16
|
+
get summary(): string | undefined;
|
|
17
|
+
/** The `key`; sorting on it reproduces the directories' numeric order. */
|
|
18
|
+
get order(): string;
|
|
19
|
+
/**
|
|
20
|
+
* The translated rendering of `title`, when the index carries one. Every
|
|
21
|
+
* reader-facing surface renders `displayTitle ?? title`.
|
|
22
|
+
*/
|
|
23
|
+
get displayTitle(): string | undefined;
|
|
24
|
+
/** The translated `summary`. Absent when the section has no translation. */
|
|
25
|
+
get displaySummary(): string | undefined;
|
|
26
|
+
}
|