@carlonicora/nextjs-jsonapi 1.108.0 → 1.110.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AssistantInterface-BYgI5z1-.d.mts → AssistantInterface-B1c8FhGA.d.mts} +2 -0
- package/dist/{AssistantInterface-DfDcz0gJ.d.ts → AssistantInterface-BBUHxOCd.d.ts} +2 -0
- package/dist/{AssistantMessageInterface-BpEhx2pC.d.ts → AssistantMessageInterface-BNzf1etU.d.ts} +16 -1
- package/dist/{AssistantMessageInterface-DJ3Me16Y.d.mts → AssistantMessageInterface-DrgHDjZ5.d.mts} +16 -1
- package/dist/{AuthComponent-B6DIk8Vf.d.ts → AuthComponent-Cd7lcYif.d.ts} +1 -1
- package/dist/{AuthComponent-BKI0ZbtD.d.mts → AuthComponent-DdxCFgUZ.d.mts} +1 -1
- package/dist/{BlockNoteEditor-2AXSTGGG.js → BlockNoteEditor-7OKW6HHN.js} +19 -19
- package/dist/{BlockNoteEditor-2AXSTGGG.js.map → BlockNoteEditor-7OKW6HHN.js.map} +1 -1
- package/dist/{BlockNoteEditor-XVIBGXHF.mjs → BlockNoteEditor-K7IIWM3S.mjs} +4 -4
- package/dist/BlockNoteViewer-OPSCTHKJ.mjs +31 -0
- package/dist/BlockNoteViewer-OPSCTHKJ.mjs.map +1 -0
- package/dist/BlockNoteViewer-UW5VZ5UF.js +31 -0
- package/dist/BlockNoteViewer-UW5VZ5UF.js.map +1 -0
- package/dist/{auth.interface-BBUgMZzs.d.ts → auth.interface-8b601idJ.d.ts} +1 -1
- package/dist/{auth.interface-XYEREOD6.d.mts → auth.interface-CXBF8Mhi.d.mts} +1 -1
- package/dist/billing/index.d.mts +41 -41
- package/dist/billing/index.d.ts +41 -41
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-VLDLERJN.js → chunk-6XWW67F2.js} +29 -9
- package/dist/chunk-6XWW67F2.js.map +1 -0
- package/dist/{chunk-HC3JFN3C.js → chunk-AKXUNE3A.js} +1004 -862
- package/dist/chunk-AKXUNE3A.js.map +1 -0
- package/dist/{chunk-CFECWLHH.mjs → chunk-CWVYU4YZ.mjs} +23 -3
- package/dist/chunk-CWVYU4YZ.mjs.map +1 -0
- package/dist/{chunk-56XBGQGU.mjs → chunk-TXVVZLUQ.mjs} +1422 -1280
- package/dist/chunk-TXVVZLUQ.mjs.map +1 -0
- package/dist/{chunk-RXXZGPC3.js → chunk-U2HR57MY.js} +265 -113
- package/dist/chunk-U2HR57MY.js.map +1 -0
- package/dist/chunk-WPAQRB5Q.mjs +7 -0
- package/dist/chunk-WPAQRB5Q.mjs.map +1 -0
- package/dist/chunk-YBZVWLPH.js +7 -0
- package/dist/chunk-YBZVWLPH.js.map +1 -0
- package/dist/{chunk-N3NVIPSU.mjs → chunk-ZYPNVEHB.mjs} +235 -83
- package/dist/{chunk-N3NVIPSU.mjs.map → chunk-ZYPNVEHB.mjs.map} +1 -1
- package/dist/client/index.d.mts +9 -18
- package/dist/client/index.d.ts +9 -18
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +421 -417
- package/dist/components/index.d.ts +421 -417
- package/dist/components/index.js +6 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +5 -3
- package/dist/{config-CLQynoaa.d.ts → config-BrkU_Ynz.d.ts} +4 -1
- package/dist/{config-k61pe_o2.d.mts → config-snd2ToEB.d.mts} +4 -1
- package/dist/contexts/index.d.mts +31 -20
- package/dist/contexts/index.d.ts +31 -20
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +75 -11
- package/dist/core/index.d.ts +75 -11
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/features/help/index.css +29 -0
- package/dist/features/help/index.css.map +1 -0
- package/dist/features/help/index.d.mts +74 -0
- package/dist/features/help/index.d.ts +74 -0
- package/dist/features/help/index.js +522 -0
- package/dist/features/help/index.js.map +1 -0
- package/dist/features/help/index.mjs +522 -0
- package/dist/features/help/index.mjs.map +1 -0
- package/dist/features/help/server.d.mts +33 -0
- package/dist/features/help/server.d.ts +33 -0
- package/dist/features/help/server.js +8 -0
- package/dist/features/help/server.js.map +1 -0
- package/dist/features/help/server.mjs +8 -0
- package/dist/features/help/server.mjs.map +1 -0
- package/dist/help-content-config.interface-C5ESIod9.d.mts +13 -0
- package/dist/help-content-config.interface-C5ESIod9.d.ts +13 -0
- package/dist/index.d.mts +10 -8
- package/dist/index.d.ts +10 -8
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/dist/{notification.interface-aLEJbA_g.d.ts → notification.interface-C1T1C2ee.d.ts} +1 -100
- package/dist/{notification.interface-DLZGtV7Z.d.mts → notification.interface-DIxR23eS.d.mts} +1 -100
- package/dist/{s3.service-CVgLWaDc.d.mts → s3.service-0BTClOYO.d.mts} +2 -2
- package/dist/{s3.service-SLlX0Zbz.d.ts → s3.service-CT27Fm1s.d.ts} +2 -2
- package/dist/server/index.d.mts +4 -3
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/testing/index.d.mts +1 -2
- package/dist/testing/index.d.ts +1 -2
- package/dist/types-CQSjy7et.d.mts +101 -0
- package/dist/types-DHOxe8rc.d.ts +101 -0
- package/dist/usePageUrlGenerator-tjq2mlDV.d.ts +14 -0
- package/dist/usePageUrlGenerator-uOnyJ6j2.d.mts +14 -0
- package/dist/{useSocket-BkxHHujj.d.mts → useSocket-B1fMIr17.d.mts} +1 -1
- package/dist/{useSocket-CMDjWFYm.d.ts → useSocket-BdJTBXKv.d.ts} +1 -1
- package/package.json +15 -1
- package/src/client/config.ts +9 -1
- package/src/components/index.ts +1 -0
- package/src/components/viewers/BlockNoteViewer.tsx +27 -0
- package/src/components/viewers/BlockNoteViewerContainer.tsx +10 -0
- package/src/core/registry/helpStore.ts +28 -0
- package/src/features/assistant/contexts/AssistantContext.tsx +35 -19
- package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +66 -6
- 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 +18 -8
- package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +4 -0
- package/src/features/assistant-message/components/parts/MessageSourcesPanel.tsx +6 -4
- package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +5 -1
- package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +3 -1
- package/src/features/assistant-message/data/AssistantMessage.ts +27 -1
- package/src/features/assistant-message/data/AssistantMessageInterface.ts +1 -0
- package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +7 -3
- package/src/features/help/components/HelpArticleBody.tsx +62 -0
- package/src/features/help/components/HelpAskAi.tsx +36 -0
- package/src/features/help/components/HelpAssistantSheet.tsx +53 -0
- package/src/features/help/components/HelpHeader.tsx +40 -0
- package/src/features/help/components/HelpHint.tsx +86 -0
- package/src/features/help/components/HelpSearchResultRow.tsx +61 -0
- package/src/features/help/components/HelpSideNav.tsx +119 -0
- package/src/features/help/components/HelpTOC.tsx +49 -0
- package/src/features/help/components/__tests__/HelpAskAi.spec.tsx +68 -0
- package/src/features/help/components/__tests__/HelpAssistantSheet.spec.tsx +36 -0
- package/src/features/help/components/__tests__/HelpHint.spec.tsx +50 -0
- package/src/features/help/components/__tests__/HelpSearchResultRow.spec.tsx +58 -0
- package/src/features/help/components/__tests__/HelpSideNav.spec.tsx +50 -0
- package/src/features/help/contexts/HelpContext.spec.tsx +26 -0
- package/src/features/help/contexts/HelpContext.tsx +24 -0
- package/src/features/help/hooks/useHelp.ts +1 -0
- package/src/features/help/hooks/useHelpFilter.ts +27 -0
- package/src/features/help/i18n-keys.ts +34 -0
- package/src/features/help/index.ts +24 -0
- package/src/features/help/interfaces/help-content-config.interface.ts +12 -0
- package/src/features/help/server-entry.ts +11 -0
- package/src/features/help/types/help-article.types.ts +35 -0
- package/src/features/help/utils/articleUrl.ts +13 -0
- package/src/features/how-to/HowToModule.ts +14 -1
- package/src/features/how-to/components/forms/HowToEditor.tsx +108 -4
- package/src/features/how-to/data/HowTo.spec.ts +43 -0
- package/src/features/how-to/data/HowTo.ts +64 -3
- package/src/features/how-to/data/HowToInterface.ts +14 -0
- package/src/features/how-to/data/HowToService.ts +61 -0
- package/src/index.ts +4 -0
- package/dist/chunk-56XBGQGU.mjs.map +0 -1
- package/dist/chunk-CFECWLHH.mjs.map +0 -1
- package/dist/chunk-HC3JFN3C.js.map +0 -1
- package/dist/chunk-RXXZGPC3.js.map +0 -1
- package/dist/chunk-VLDLERJN.js.map +0 -1
- /package/dist/{BlockNoteEditor-XVIBGXHF.mjs.map → BlockNoteEditor-K7IIWM3S.mjs.map} +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import type { HelpHeading } from "../types/help-article.types";
|
|
5
|
+
|
|
6
|
+
export function HelpTOC({ headings }: { headings: readonly HelpHeading[] }) {
|
|
7
|
+
const t = useTranslations();
|
|
8
|
+
const [active, setActive] = useState<string | null>(null);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const observer = new IntersectionObserver(
|
|
12
|
+
(entries) => {
|
|
13
|
+
for (const e of entries) {
|
|
14
|
+
if (e.isIntersecting) setActive(e.target.id);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{ rootMargin: "0px 0px -70% 0px", threshold: 0 },
|
|
18
|
+
);
|
|
19
|
+
headings.forEach((h) => {
|
|
20
|
+
const el = document.getElementById(h.slug);
|
|
21
|
+
if (el) observer.observe(el);
|
|
22
|
+
});
|
|
23
|
+
return () => observer.disconnect();
|
|
24
|
+
}, [headings]);
|
|
25
|
+
|
|
26
|
+
if (headings.length === 0) return null;
|
|
27
|
+
return (
|
|
28
|
+
<nav aria-label={t("help.toc.title")} className="hidden lg:block">
|
|
29
|
+
<div className="text-muted-foreground mb-2 text-xs font-semibold tracking-wider uppercase">
|
|
30
|
+
{t("help.toc.title")}
|
|
31
|
+
</div>
|
|
32
|
+
<ul className="space-y-1 text-sm">
|
|
33
|
+
{headings.map((h) => (
|
|
34
|
+
<li key={h.slug} style={{ paddingLeft: `${(h.depth - 2) * 0.75}rem` }}>
|
|
35
|
+
<a
|
|
36
|
+
href={`#${h.slug}`}
|
|
37
|
+
className={
|
|
38
|
+
"block truncate hover:text-foreground " +
|
|
39
|
+
(active === h.slug ? "text-foreground font-medium" : "text-muted-foreground")
|
|
40
|
+
}
|
|
41
|
+
>
|
|
42
|
+
{h.text}
|
|
43
|
+
</a>
|
|
44
|
+
</li>
|
|
45
|
+
))}
|
|
46
|
+
</ul>
|
|
47
|
+
</nav>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { configureJsonApi } from "../../../../client/config";
|
|
4
|
+
import { HelpProvider } from "../../contexts/HelpContext";
|
|
5
|
+
import { HelpAskAi } from "../HelpAskAi";
|
|
6
|
+
|
|
7
|
+
vi.mock("../../../user/contexts/CurrentUserContext", () => ({
|
|
8
|
+
useCurrentUserContext: () => ({ currentUser: (globalThis as any).__mockUser ?? null }),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
// HelpAssistantSheet is intentionally rendered ONLY when logged-in to keep the
|
|
12
|
+
// disabled-state DOM minimal. We mock it to a marker so the test asserts presence.
|
|
13
|
+
vi.mock("../HelpAssistantSheet", () => ({
|
|
14
|
+
HelpAssistantSheet: ({ open }: { open: boolean }) => (open ? <div data-testid="sheet" /> : null),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
function setupConfig() {
|
|
18
|
+
configureJsonApi({
|
|
19
|
+
apiUrl: "http://localhost",
|
|
20
|
+
helpContent: { brand: { label: "narr8" } },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("HelpAskAi", () => {
|
|
25
|
+
it("renders a disabled button + tooltip for anonymous users", () => {
|
|
26
|
+
(globalThis as any).__mockUser = null;
|
|
27
|
+
setupConfig();
|
|
28
|
+
|
|
29
|
+
render(
|
|
30
|
+
<HelpProvider>
|
|
31
|
+
<HelpAskAi />
|
|
32
|
+
</HelpProvider>,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const button = screen.getByRole("button");
|
|
36
|
+
expect(button).toHaveAttribute("disabled");
|
|
37
|
+
// vitest.setup.ts stubs next-intl's useTranslations to return the key as-is.
|
|
38
|
+
// The TooltipTrigger renders as a <span>, so the disabled <button> is the
|
|
39
|
+
// only <button> in the DOM (no nested-button hydration risk).
|
|
40
|
+
expect(screen.queryByTestId("sheet")).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("clicking the disabled button does not open the sheet", () => {
|
|
44
|
+
(globalThis as any).__mockUser = null;
|
|
45
|
+
setupConfig();
|
|
46
|
+
render(
|
|
47
|
+
<HelpProvider>
|
|
48
|
+
<HelpAskAi />
|
|
49
|
+
</HelpProvider>,
|
|
50
|
+
);
|
|
51
|
+
fireEvent.click(screen.getByRole("button"));
|
|
52
|
+
expect(screen.queryByTestId("sheet")).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("renders an enabled button for logged-in users; clicking opens the sheet", () => {
|
|
56
|
+
(globalThis as any).__mockUser = { id: "u-1" };
|
|
57
|
+
setupConfig();
|
|
58
|
+
render(
|
|
59
|
+
<HelpProvider>
|
|
60
|
+
<HelpAskAi />
|
|
61
|
+
</HelpProvider>,
|
|
62
|
+
);
|
|
63
|
+
const button = screen.getByRole("button");
|
|
64
|
+
expect(button).not.toHaveAttribute("disabled");
|
|
65
|
+
fireEvent.click(button);
|
|
66
|
+
expect(screen.getByTestId("sheet")).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { HelpAssistantSheet } from "../HelpAssistantSheet";
|
|
4
|
+
|
|
5
|
+
const sendMessage = vi.fn().mockResolvedValue(undefined);
|
|
6
|
+
|
|
7
|
+
vi.mock("../../../assistant/contexts/AssistantContext", () => ({
|
|
8
|
+
AssistantProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
9
|
+
useAssistantContext: () => ({
|
|
10
|
+
assistant: undefined,
|
|
11
|
+
messages: [],
|
|
12
|
+
sending: false,
|
|
13
|
+
status: undefined,
|
|
14
|
+
failedMessageIds: new Set(),
|
|
15
|
+
sendMessage,
|
|
16
|
+
retrySend: vi.fn(),
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe("HelpAssistantSheet", () => {
|
|
21
|
+
beforeEach(() => sendMessage.mockClear());
|
|
22
|
+
|
|
23
|
+
it("submitting the composer calls sendMessage with howToMode: true", async () => {
|
|
24
|
+
render(<HelpAssistantSheet open={true} onOpenChange={() => {}} />);
|
|
25
|
+
|
|
26
|
+
// AssistantEmptyState renders the composer (a textarea + send button).
|
|
27
|
+
const textarea = screen.getByPlaceholderText("features.assistant.composer_placeholder");
|
|
28
|
+
fireEvent.change(textarea, { target: { value: "how do scenes work?" } });
|
|
29
|
+
|
|
30
|
+
// The empty-state composer's send button is found by its visible text.
|
|
31
|
+
const sendButton = screen.getByRole("button", { name: /save|send/i });
|
|
32
|
+
fireEvent.click(sendButton);
|
|
33
|
+
|
|
34
|
+
expect(sendMessage).toHaveBeenCalledWith("how do scenes work?", { howToMode: true });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { configureJsonApi } from "../../../../client/config";
|
|
4
|
+
import { HelpProvider } from "../../contexts/HelpContext";
|
|
5
|
+
import { HelpHint } from "../HelpHint";
|
|
6
|
+
import { HowToService } from "../../../how-to/data/HowToService";
|
|
7
|
+
|
|
8
|
+
vi.mock("../../../how-to/data/HowToService", () => ({
|
|
9
|
+
HowToService: { findPublished: vi.fn() },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const article = {
|
|
13
|
+
id: "1",
|
|
14
|
+
slug: "x",
|
|
15
|
+
howToType: "how-to",
|
|
16
|
+
name: "X",
|
|
17
|
+
summary: "S",
|
|
18
|
+
contextualKeys: ["npc.editor"],
|
|
19
|
+
draft: false,
|
|
20
|
+
} as any;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
vi.mocked(HowToService.findPublished).mockResolvedValue([article]);
|
|
25
|
+
configureJsonApi({
|
|
26
|
+
apiUrl: "http://localhost",
|
|
27
|
+
helpContent: { brand: { label: "narr8" } },
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("HelpHint", () => {
|
|
32
|
+
it("renders null when no published article matches the contextKey", async () => {
|
|
33
|
+
const { container } = render(
|
|
34
|
+
<HelpProvider>
|
|
35
|
+
<HelpHint contextKey="nope.absent" />
|
|
36
|
+
</HelpProvider>,
|
|
37
|
+
);
|
|
38
|
+
await waitFor(() => expect(HowToService.findPublished).toHaveBeenCalled());
|
|
39
|
+
expect(container.querySelector("button")).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders the trigger when at least one published article matches", async () => {
|
|
43
|
+
render(
|
|
44
|
+
<HelpProvider>
|
|
45
|
+
<HelpHint contextKey="npc.editor" />
|
|
46
|
+
</HelpProvider>,
|
|
47
|
+
);
|
|
48
|
+
expect(await screen.findByLabelText(/help on this|hint/i)).toBeTruthy();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { Command, CommandList } from "../../../../shadcnui";
|
|
4
|
+
import { configureJsonApi } from "../../../../client/config";
|
|
5
|
+
import { HelpProvider } from "../../contexts/HelpContext";
|
|
6
|
+
import { HelpSearchResultRow } from "../HelpSearchResultRow";
|
|
7
|
+
import { HowToService } from "../../../how-to/data/HowToService";
|
|
8
|
+
|
|
9
|
+
vi.mock("../../../how-to/data/HowToService", () => ({
|
|
10
|
+
HowToService: { findOne: vi.fn() },
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const article = {
|
|
14
|
+
id: "abc",
|
|
15
|
+
slug: "x",
|
|
16
|
+
howToType: "how-to",
|
|
17
|
+
name: "X title",
|
|
18
|
+
summary: "S",
|
|
19
|
+
} as any;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
configureJsonApi({
|
|
24
|
+
apiUrl: "http://localhost",
|
|
25
|
+
helpContent: { brand: { label: "narr8" } },
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("HelpSearchResultRow", () => {
|
|
30
|
+
it("renders the article title when the service returns the how-to", async () => {
|
|
31
|
+
vi.mocked(HowToService.findOne).mockResolvedValue(article);
|
|
32
|
+
render(
|
|
33
|
+
<HelpProvider>
|
|
34
|
+
<Command>
|
|
35
|
+
<CommandList>
|
|
36
|
+
<HelpSearchResultRow result={{ id: "abc", name: "ignored", entityType: "howtos" }} />
|
|
37
|
+
</CommandList>
|
|
38
|
+
</Command>
|
|
39
|
+
</HelpProvider>,
|
|
40
|
+
);
|
|
41
|
+
expect(await screen.findByText("X title")).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("renders disabled removed-state row when the service has no how-to", async () => {
|
|
45
|
+
vi.mocked(HowToService.findOne).mockRejectedValue(new Error("not found"));
|
|
46
|
+
render(
|
|
47
|
+
<HelpProvider>
|
|
48
|
+
<Command>
|
|
49
|
+
<CommandList>
|
|
50
|
+
<HelpSearchResultRow result={{ id: "ghost", name: "ignored", entityType: "howtos" }} />
|
|
51
|
+
</CommandList>
|
|
52
|
+
</Command>
|
|
53
|
+
</HelpProvider>,
|
|
54
|
+
);
|
|
55
|
+
await waitFor(() => expect(HowToService.findOne).toHaveBeenCalledWith({ id: "ghost" }));
|
|
56
|
+
expect(screen.queryByText("X title")).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { configureJsonApi } from "../../../../client/config";
|
|
4
|
+
import { HelpProvider } from "../../contexts/HelpContext";
|
|
5
|
+
import { HelpSideNav } from "../HelpSideNav";
|
|
6
|
+
|
|
7
|
+
// HelpSideNav now sources its list from the public API via
|
|
8
|
+
// HowToService.findPublished() (Plan 3b) instead of the help manifest, so the
|
|
9
|
+
// test mocks the service rather than seeding the manifest.
|
|
10
|
+
vi.mock("../../../how-to/data/HowToService", () => ({
|
|
11
|
+
HowToService: { findPublished: vi.fn() },
|
|
12
|
+
}));
|
|
13
|
+
import { HowToService } from "../../../how-to/data/HowToService";
|
|
14
|
+
|
|
15
|
+
const published = [
|
|
16
|
+
{ id: "a", name: "Alpha", summary: "s", tags: [], howToType: "how-to", slug: "a", order: 1, draft: false },
|
|
17
|
+
{ id: "b", name: "Beta", summary: "s", tags: [], howToType: "how-to", slug: "b", order: 2, draft: false },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
configureJsonApi({
|
|
22
|
+
apiUrl: "http://localhost",
|
|
23
|
+
helpContent: { brand: { label: "narr8" } },
|
|
24
|
+
});
|
|
25
|
+
(HowToService.findPublished as ReturnType<typeof vi.fn>).mockResolvedValue(published as any);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("HelpSideNav", () => {
|
|
29
|
+
it("lists every non-draft article", async () => {
|
|
30
|
+
render(
|
|
31
|
+
<HelpProvider>
|
|
32
|
+
<HelpSideNav />
|
|
33
|
+
</HelpProvider>,
|
|
34
|
+
);
|
|
35
|
+
expect(await screen.findByText("Alpha")).toBeTruthy();
|
|
36
|
+
expect(await screen.findByText("Beta")).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("filters by query", async () => {
|
|
40
|
+
render(
|
|
41
|
+
<HelpProvider>
|
|
42
|
+
<HelpSideNav />
|
|
43
|
+
</HelpProvider>,
|
|
44
|
+
);
|
|
45
|
+
await screen.findByText("Alpha");
|
|
46
|
+
fireEvent.change(screen.getByPlaceholderText(/filter/i), { target: { value: "Bet" } });
|
|
47
|
+
expect(screen.queryByText("Alpha")).toBeNull();
|
|
48
|
+
expect(screen.getByText("Beta")).toBeTruthy();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { renderHook } from "@testing-library/react";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
import { configureJsonApi } from "../../../client/config";
|
|
5
|
+
import { HelpProvider, useHelp } from "./HelpContext";
|
|
6
|
+
|
|
7
|
+
describe("HelpProvider / useHelp", () => {
|
|
8
|
+
const cfg = {
|
|
9
|
+
brand: { logo: "/logo.png", label: "Test", appHref: "/" },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
configureJsonApi({ apiUrl: "http://localhost", helpContent: cfg });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("exposes the configured helpContent via useHelp()", () => {
|
|
17
|
+
const wrapper = ({ children }: { children: ReactNode }) => <HelpProvider>{children}</HelpProvider>;
|
|
18
|
+
const { result } = renderHook(() => useHelp(), { wrapper });
|
|
19
|
+
expect(result.current.brand?.label).toBe("Test");
|
|
20
|
+
expect(result.current.brand?.appHref).toBe("/");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("throws when useHelp() is called outside HelpProvider", () => {
|
|
24
|
+
expect(() => renderHook(() => useHelp())).toThrow(/outside <HelpProvider>/);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext, ReactNode } from "react";
|
|
3
|
+
import { _getStaticHelpContent } from "../../../core/registry/helpStore";
|
|
4
|
+
import type { HelpContentConfig } from "../interfaces/help-content-config.interface";
|
|
5
|
+
|
|
6
|
+
const HelpContext = createContext<HelpContentConfig | null>(null);
|
|
7
|
+
|
|
8
|
+
export function HelpProvider({ children }: { children: ReactNode }) {
|
|
9
|
+
const cfg = _getStaticHelpContent<HelpContentConfig>();
|
|
10
|
+
if (!cfg) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"Help content not configured — call configureJsonApi({ helpContent: {...} }) before importing help components.",
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return <HelpContext.Provider value={cfg}>{children}</HelpContext.Provider>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useHelp(): HelpContentConfig {
|
|
19
|
+
const ctx = useContext(HelpContext);
|
|
20
|
+
if (!ctx) {
|
|
21
|
+
throw new Error("useHelp() called outside <HelpProvider>. Wrap your help route group with <HelpProvider>.");
|
|
22
|
+
}
|
|
23
|
+
return ctx;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useHelp } from "../contexts/HelpContext";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import Fuse from "fuse.js";
|
|
4
|
+
import type { HelpArticle } from "../types/help-article.types";
|
|
5
|
+
|
|
6
|
+
export function useHelpFilter(articles: readonly HelpArticle[]) {
|
|
7
|
+
const [query, setQuery] = useState("");
|
|
8
|
+
const fuse = useMemo(
|
|
9
|
+
() =>
|
|
10
|
+
new Fuse([...articles], {
|
|
11
|
+
keys: [
|
|
12
|
+
{ name: "title", weight: 0.5 },
|
|
13
|
+
{ name: "summary", weight: 0.3 },
|
|
14
|
+
{ name: "tags", weight: 0.1 },
|
|
15
|
+
{ name: "headings.text", weight: 0.1 },
|
|
16
|
+
],
|
|
17
|
+
threshold: 0.4,
|
|
18
|
+
ignoreLocation: true,
|
|
19
|
+
}),
|
|
20
|
+
[articles],
|
|
21
|
+
);
|
|
22
|
+
const filtered = useMemo(() => {
|
|
23
|
+
if (!query.trim()) return [...articles];
|
|
24
|
+
return fuse.search(query).map((r) => r.item);
|
|
25
|
+
}, [query, articles, fuse]);
|
|
26
|
+
return { query, setQuery, filtered };
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed namespace of i18n keys this feature reads via useTranslations/getTranslations.
|
|
3
|
+
* Consuming apps must define each entry in their messages/<locale>.json.
|
|
4
|
+
*/
|
|
5
|
+
export const HELP_I18N_KEYS = [
|
|
6
|
+
"help.modes.tutorial",
|
|
7
|
+
"help.modes.how-to",
|
|
8
|
+
"help.modes.reference",
|
|
9
|
+
"help.modes.explanation",
|
|
10
|
+
"help.sideNav.filterPlaceholder",
|
|
11
|
+
"help.sideNav.noMatches",
|
|
12
|
+
"help.toc.title",
|
|
13
|
+
"help.article.lastUpdated",
|
|
14
|
+
"help.article.previous",
|
|
15
|
+
"help.article.next",
|
|
16
|
+
"help.article.related",
|
|
17
|
+
"help.askAi.button",
|
|
18
|
+
"help.askAi.loginTooltip",
|
|
19
|
+
"help.askAi.sheet.title",
|
|
20
|
+
"help.askAi.sheet.subtitle",
|
|
21
|
+
"help.header.search",
|
|
22
|
+
"help.header.login",
|
|
23
|
+
"help.header.openApp",
|
|
24
|
+
"help.hint.trigger",
|
|
25
|
+
"help.hint.viewArticle",
|
|
26
|
+
"help.hint.pickArticle",
|
|
27
|
+
"help.landing.heading",
|
|
28
|
+
"help.landing.subheading",
|
|
29
|
+
"help.landing.featuredTutorials",
|
|
30
|
+
"help.landing.browseByMode",
|
|
31
|
+
"help.modeIndex.empty",
|
|
32
|
+
"help.search.removed",
|
|
33
|
+
"help.footerLink",
|
|
34
|
+
] as const;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Client-only public barrel for the help feature.
|
|
2
|
+
// Consumed via `@carlonicora/nextjs-jsonapi/help`. Tsup adds a top-level
|
|
3
|
+
// "use client" directive to the bundled output via clientEntries.
|
|
4
|
+
//
|
|
5
|
+
// Pair this with `@carlonicora/nextjs-jsonapi/help/server` for the server-safe
|
|
6
|
+
// HELP_MODES constant + article types (imported by server-component pages).
|
|
7
|
+
// Consumers compose page shells themselves: import the client primitives below
|
|
8
|
+
// (HelpProvider, HelpHeader, HelpSideNav, HelpArticleBody, HelpTOC, etc.) and
|
|
9
|
+
// assemble pages in app routes.
|
|
10
|
+
|
|
11
|
+
export { HelpProvider, useHelp } from "./contexts/HelpContext";
|
|
12
|
+
export { HelpHeader } from "./components/HelpHeader";
|
|
13
|
+
export { HelpSideNav } from "./components/HelpSideNav";
|
|
14
|
+
export { HelpArticleBody } from "./components/HelpArticleBody";
|
|
15
|
+
export { HelpTOC } from "./components/HelpTOC";
|
|
16
|
+
export { HelpHint } from "./components/HelpHint";
|
|
17
|
+
export { HelpAskAi } from "./components/HelpAskAi";
|
|
18
|
+
export { HelpSearchResultRow } from "./components/HelpSearchResultRow";
|
|
19
|
+
export { useHelpFilter } from "./hooks/useHelpFilter";
|
|
20
|
+
export { articleUrl, modeUrl } from "./utils/articleUrl";
|
|
21
|
+
export { HELP_I18N_KEYS } from "./i18n-keys";
|
|
22
|
+
export type { HelpContentConfig, HelpBrandConfig } from "./interfaces/help-content-config.interface";
|
|
23
|
+
export { HELP_MODES } from "./types/help-article.types";
|
|
24
|
+
export type { HelpArticle, HelpHeading, HelpMode, HelpRedirect } from "./types/help-article.types";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface HelpBrandConfig {
|
|
2
|
+
/** URL of the brand logo image (served by the consuming app). */
|
|
3
|
+
logo?: string;
|
|
4
|
+
/** Brand label shown in the help header. */
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Where the "Open app" button navigates for logged-in users. Defaults to "/". */
|
|
7
|
+
appHref?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HelpContentConfig {
|
|
11
|
+
brand?: HelpBrandConfig;
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Server-safe public barrel for the help feature.
|
|
2
|
+
// Consumed via `@carlonicora/nextjs-jsonapi/help/server`. Must NOT receive a
|
|
3
|
+
// "use client" directive — server components (e.g. the /help index + mode pages)
|
|
4
|
+
// import HELP_MODES/HelpMode from here without pulling the client bundle.
|
|
5
|
+
//
|
|
6
|
+
// Now that help articles are served from the API (HowTo) rather than the MDX
|
|
7
|
+
// manifest, this barrel exposes only the shared mode constant + article types.
|
|
8
|
+
// Client primitives live in `@carlonicora/nextjs-jsonapi/help`.
|
|
9
|
+
|
|
10
|
+
export { HELP_MODES } from "./types/help-article.types";
|
|
11
|
+
export type { HelpArticle, HelpHeading, HelpMode, HelpRedirect } from "./types/help-article.types";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help-article types for the frontend help feature. Self-contained — the
|
|
3
|
+
* published frontend library stays decoupled from `@carlonicora/nestjs-neo4jsonapi`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const HELP_MODES = ["tutorial", "how-to", "reference", "explanation"] as const;
|
|
7
|
+
export type HelpMode = (typeof HELP_MODES)[number];
|
|
8
|
+
|
|
9
|
+
export interface HelpHeading {
|
|
10
|
+
depth: 2 | 3;
|
|
11
|
+
slug: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HelpArticle {
|
|
16
|
+
id: string;
|
|
17
|
+
slug: string;
|
|
18
|
+
mode: HelpMode;
|
|
19
|
+
title: string;
|
|
20
|
+
summary: string;
|
|
21
|
+
order: number;
|
|
22
|
+
tags: readonly string[];
|
|
23
|
+
contextualKeys: readonly string[];
|
|
24
|
+
aiIndexed: boolean;
|
|
25
|
+
draft: boolean;
|
|
26
|
+
path: string;
|
|
27
|
+
headings: readonly HelpHeading[];
|
|
28
|
+
relatedSlugs: readonly string[];
|
|
29
|
+
lastUpdated: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface HelpRedirect {
|
|
33
|
+
from: string;
|
|
34
|
+
to: string;
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HelpMode } from "../types/help-article.types";
|
|
2
|
+
import type { usePageUrlGenerator } from "../../../client";
|
|
3
|
+
|
|
4
|
+
export function articleUrl(
|
|
5
|
+
generateUrl: ReturnType<typeof usePageUrlGenerator>,
|
|
6
|
+
article: { mode: HelpMode; slug: string },
|
|
7
|
+
): string {
|
|
8
|
+
return generateUrl({ page: `/help/${article.mode}/${article.slug}` });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function modeUrl(generateUrl: ReturnType<typeof usePageUrlGenerator>, mode: HelpMode): string {
|
|
12
|
+
return generateUrl({ page: `/help/${mode}` });
|
|
13
|
+
}
|
|
@@ -12,7 +12,20 @@ export const HowToModule = (factory: ModuleFactory) =>
|
|
|
12
12
|
icon: LifeBuoyIcon,
|
|
13
13
|
inclusions: {
|
|
14
14
|
lists: {
|
|
15
|
-
fields: [
|
|
15
|
+
fields: [
|
|
16
|
+
createJsonApiInclusion("howtos", [
|
|
17
|
+
"name",
|
|
18
|
+
"description",
|
|
19
|
+
"pages",
|
|
20
|
+
"howToType",
|
|
21
|
+
"slug",
|
|
22
|
+
"order",
|
|
23
|
+
"summary",
|
|
24
|
+
"tags",
|
|
25
|
+
"contextualKeys",
|
|
26
|
+
"draft",
|
|
27
|
+
]),
|
|
28
|
+
],
|
|
16
29
|
},
|
|
17
30
|
},
|
|
18
31
|
});
|