@carlonicora/nextjs-jsonapi 1.109.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/{AssistantMessageInterface-Cs1yb-gF.d.ts → AssistantMessageInterface-BNzf1etU.d.ts} +14 -1
- package/dist/{AssistantMessageInterface-DQ3mH5L8.d.mts → AssistantMessageInterface-DrgHDjZ5.d.mts} +14 -1
- package/dist/{BlockNoteEditor-3XYBZLWO.js → BlockNoteEditor-7OKW6HHN.js} +19 -20
- package/dist/BlockNoteEditor-7OKW6HHN.js.map +1 -0
- package/dist/{BlockNoteEditor-EBFZG7AL.mjs → BlockNoteEditor-K7IIWM3S.mjs} +4 -5
- package/dist/{BlockNoteEditor-EBFZG7AL.mjs.map → BlockNoteEditor-K7IIWM3S.mjs.map} +1 -1
- 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/billing/index.d.mts +41 -41
- package/dist/billing/index.d.ts +41 -41
- package/dist/billing/index.js +346 -347
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +3 -4
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-7E3O52U5.js → chunk-6XWW67F2.js} +25 -12
- package/dist/chunk-6XWW67F2.js.map +1 -0
- package/dist/{chunk-UHO3KUUH.js → chunk-AKXUNE3A.js} +977 -850
- package/dist/chunk-AKXUNE3A.js.map +1 -0
- package/dist/{chunk-UOYIWJEJ.mjs → chunk-CWVYU4YZ.mjs} +18 -5
- package/dist/{chunk-UOYIWJEJ.mjs.map → chunk-CWVYU4YZ.mjs.map} +1 -1
- package/dist/{chunk-CRTVAQEK.mjs → chunk-TXVVZLUQ.mjs} +1388 -1261
- package/dist/chunk-TXVVZLUQ.mjs.map +1 -0
- package/dist/{chunk-CFI4WZ5R.js → chunk-U2HR57MY.js} +111 -5
- 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-MSNNAHDB.mjs → chunk-ZYPNVEHB.mjs} +112 -6
- package/dist/{chunk-MSNNAHDB.mjs.map → chunk-ZYPNVEHB.mjs.map} +1 -1
- package/dist/client/index.d.mts +3 -4
- package/dist/client/index.d.ts +3 -4
- package/dist/client/index.js +4 -5
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -4
- package/dist/components/index.d.mts +418 -415
- package/dist/components/index.d.ts +418 -415
- package/dist/components/index.js +6 -5
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +5 -4
- package/dist/{config-CN23v3eJ.d.ts → config-BrkU_Ynz.d.ts} +2 -2
- package/dist/{config-gh88Qn4h.d.mts → config-snd2ToEB.d.mts} +2 -2
- package/dist/contexts/index.d.mts +15 -15
- package/dist/contexts/index.d.ts +15 -15
- package/dist/contexts/index.js +4 -5
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +3 -4
- package/dist/core/index.d.mts +35 -4
- package/dist/core/index.d.ts +35 -4
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/features/help/index.d.mts +25 -66
- package/dist/features/help/index.d.ts +25 -66
- package/dist/features/help/index.js +114 -124
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +95 -105
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/help/server.d.mts +27 -65
- package/dist/features/help/server.d.ts +27 -65
- package/dist/features/help/server.js +4 -119
- package/dist/features/help/server.js.map +1 -1
- package/dist/features/help/server.mjs +4 -119
- package/dist/features/help/server.mjs.map +1 -1
- 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 +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -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/package.json +1 -6
- package/src/client/config.ts +1 -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 +2 -19
- package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +4 -0
- package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +5 -5
- package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +3 -2
- package/src/features/help/components/HelpArticleBody.tsx +27 -19
- package/src/features/help/components/HelpHint.tsx +20 -11
- package/src/features/help/components/HelpSearchResultRow.tsx +16 -6
- package/src/features/help/components/HelpSideNav.tsx +54 -19
- package/src/features/help/components/__tests__/HelpAskAi.spec.tsx +1 -1
- package/src/features/help/components/__tests__/HelpHint.spec.tsx +17 -17
- package/src/features/help/components/__tests__/HelpSearchResultRow.spec.tsx +18 -19
- package/src/features/help/components/__tests__/HelpSideNav.spec.tsx +20 -22
- package/src/features/help/contexts/HelpContext.spec.tsx +1 -3
- package/src/features/help/index.ts +3 -6
- package/src/features/help/interfaces/help-content-config.interface.ts +0 -5
- package/src/features/help/server-entry.ts +6 -15
- package/src/features/help/types/help-article.types.ts +2 -4
- 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 +47 -4
- package/src/features/how-to/data/HowToInterface.ts +14 -1
- package/src/features/how-to/data/HowToService.ts +61 -0
- package/dist/BlockNoteEditor-3XYBZLWO.js.map +0 -1
- package/dist/chunk-3J7RQBF3.js +0 -123
- package/dist/chunk-3J7RQBF3.js.map +0 -1
- package/dist/chunk-7E3O52U5.js.map +0 -1
- package/dist/chunk-CFI4WZ5R.js.map +0 -1
- package/dist/chunk-CQID6RCF.mjs +0 -38
- package/dist/chunk-CQID6RCF.mjs.map +0 -1
- package/dist/chunk-CRTVAQEK.mjs.map +0 -1
- package/dist/chunk-MZTKPPET.mjs +0 -123
- package/dist/chunk-MZTKPPET.mjs.map +0 -1
- package/dist/chunk-UHO3KUUH.js.map +0 -1
- package/dist/chunk-YQQHAFBS.js +0 -38
- package/dist/chunk-YQQHAFBS.js.map +0 -1
- package/dist/features/help/server/createHelpAssetRouteHandler.d.mts +0 -11
- package/dist/features/help/server/createHelpAssetRouteHandler.d.ts +0 -11
- package/dist/features/help/server/createHelpAssetRouteHandler.js +0 -43
- package/dist/features/help/server/createHelpAssetRouteHandler.js.map +0 -1
- package/dist/features/help/server/createHelpAssetRouteHandler.mjs +0 -43
- package/dist/features/help/server/createHelpAssetRouteHandler.mjs.map +0 -1
- package/dist/help-content-config.interface-B9L02u9i.d.mts +0 -50
- package/dist/help-content-config.interface-B9L02u9i.d.ts +0 -50
- package/src/features/help/components/mdx/Callout.tsx +0 -21
- package/src/features/help/components/mdx/EntityRef.tsx +0 -18
- package/src/features/help/components/mdx/KeyBinding.tsx +0 -6
- package/src/features/help/components/mdx/Related.tsx +0 -33
- package/src/features/help/components/mdx/Screenshot.tsx +0 -9
- package/src/features/help/components/mdx/Steps.tsx +0 -21
- package/src/features/help/components/mdx/Video.tsx +0 -8
- package/src/features/help/components/mdx/mdx-server-components.ts +0 -23
- package/src/features/help/components/mdx/mdxComponents.ts +0 -9
- package/src/features/help/hooks/useHelpArticle.ts +0 -7
- package/src/features/help/hooks/useHelpManifest.ts +0 -5
- package/src/features/help/server/__tests__/createHelpAssetRouteHandler.spec.ts +0 -43
- package/src/features/help/server/createHelpAssetRouteHandler.ts +0 -35
- package/src/features/help/server/generateHelpArticleMetadata.ts +0 -18
- package/src/features/help/server/generateHelpArticleStaticParams.ts +0 -7
- package/src/features/help/server/generateHelpModeStaticParams.ts +0 -5
- package/src/features/help/server/getHelpContent.ts +0 -17
- package/src/features/help/server/index.ts +0 -8
- package/src/features/help/server/serializeHelpArticle.tsx +0 -46
- package/src/features/help/utils/__tests__/helpNavigation.spec.ts +0 -70
- package/src/features/help/utils/helpNavigation.ts +0 -29
|
@@ -1,48 +1,56 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
|
|
2
3
|
import { ReactNode } from "react";
|
|
3
4
|
import Link from "next/link";
|
|
4
5
|
import { useFormatter, useTranslations } from "next-intl";
|
|
5
|
-
import type { HelpArticle } from "../types/help-article.types";
|
|
6
6
|
import { usePageUrlGenerator } from "../../../client";
|
|
7
|
-
import { useHelp } from "../contexts/HelpContext";
|
|
8
|
-
import { articleUrl } from "../utils/articleUrl";
|
|
9
|
-
import { prevNextWithinMode } from "../utils/helpNavigation";
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
type Sibling = { howToType: string; slug: string; title: string };
|
|
9
|
+
|
|
10
|
+
export function HelpArticleBody(props: {
|
|
11
|
+
howToType: string;
|
|
12
|
+
title: string;
|
|
13
|
+
summary?: string;
|
|
14
|
+
updatedAt?: string;
|
|
15
|
+
prev?: Sibling | null;
|
|
16
|
+
next?: Sibling | null;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}) {
|
|
12
19
|
const t = useTranslations();
|
|
13
20
|
const format = useFormatter();
|
|
14
21
|
const generateUrl = usePageUrlGenerator();
|
|
15
|
-
const {
|
|
16
|
-
const { prev, next } = prevNextWithinMode(manifest, article);
|
|
22
|
+
const { howToType, title, summary, updatedAt, prev, next, children } = props;
|
|
17
23
|
|
|
18
24
|
return (
|
|
19
25
|
<article className="prose dark:prose-invert max-w-none">
|
|
20
26
|
<nav className="text-muted-foreground mb-2 text-xs">
|
|
21
27
|
<Link href={generateUrl({ page: "/help" })}>Help</Link>
|
|
22
28
|
{" · "}
|
|
23
|
-
<Link href={generateUrl({ page: `/help/${
|
|
29
|
+
<Link href={generateUrl({ page: `/help/${howToType}` })}>{t(`help.modes.${howToType}`)}</Link>
|
|
24
30
|
</nav>
|
|
25
|
-
<h1>{
|
|
26
|
-
<p className="text-muted-foreground !mt-0 text-base">{
|
|
31
|
+
<h1>{title}</h1>
|
|
32
|
+
{summary ? <p className="text-muted-foreground !mt-0 text-base">{summary}</p> : null}
|
|
27
33
|
{children}
|
|
28
34
|
<hr className="my-6" />
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
{updatedAt ? (
|
|
36
|
+
<div className="text-muted-foreground text-xs">
|
|
37
|
+
<span>
|
|
38
|
+
{t("help.article.lastUpdated", {
|
|
39
|
+
date: format.dateTime(new Date(updatedAt), { dateStyle: "short" }),
|
|
40
|
+
})}
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
) : null}
|
|
36
44
|
<div className="mt-4 flex justify-between text-sm">
|
|
37
45
|
{prev ? (
|
|
38
|
-
<Link href={
|
|
46
|
+
<Link href={generateUrl({ page: `/help/${prev.howToType}/${prev.slug}` })} className="hover:underline">
|
|
39
47
|
← {t("help.article.previous")}: {prev.title}
|
|
40
48
|
</Link>
|
|
41
49
|
) : (
|
|
42
50
|
<span />
|
|
43
51
|
)}
|
|
44
52
|
{next ? (
|
|
45
|
-
<Link href={
|
|
53
|
+
<Link href={generateUrl({ page: `/help/${next.howToType}/${next.slug}` })} className="hover:underline">
|
|
46
54
|
{t("help.article.next")}: {next.title} →
|
|
47
55
|
</Link>
|
|
48
56
|
) : (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { HelpCircleIcon } from "lucide-react";
|
|
@@ -13,20 +13,29 @@ import {
|
|
|
13
13
|
SheetDescription,
|
|
14
14
|
} from "../../../shadcnui";
|
|
15
15
|
import { usePageUrlGenerator } from "../../../client";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import type { HelpArticle } from "../types/help-article.types";
|
|
16
|
+
import { HowToService } from "../../how-to/data/HowToService";
|
|
17
|
+
import type { HowToInterface } from "../../how-to/data/HowToInterface";
|
|
19
18
|
|
|
20
19
|
export function HelpHint({ contextKey }: { contextKey: string }) {
|
|
21
20
|
const t = useTranslations();
|
|
22
21
|
const generateUrl = usePageUrlGenerator();
|
|
23
|
-
const { manifest } = useHelp();
|
|
24
22
|
const [open, setOpen] = useState(false);
|
|
25
|
-
const [picked, setPicked] = useState<
|
|
23
|
+
const [picked, setPicked] = useState<HowToInterface | null>(null);
|
|
24
|
+
|
|
25
|
+
const [articles, setArticles] = useState<HowToInterface[]>([]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
let active = true;
|
|
28
|
+
HowToService.findPublished()
|
|
29
|
+
.then((list) => active && setArticles(list))
|
|
30
|
+
.catch(() => active && setArticles([]));
|
|
31
|
+
return () => {
|
|
32
|
+
active = false;
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
26
35
|
|
|
27
36
|
const matches = useMemo(
|
|
28
|
-
() =>
|
|
29
|
-
[
|
|
37
|
+
() => articles.filter((a) => a.contextualKeys.includes(contextKey) && !a.draft),
|
|
38
|
+
[articles, contextKey],
|
|
30
39
|
);
|
|
31
40
|
|
|
32
41
|
if (matches.length === 0) return null;
|
|
@@ -45,13 +54,13 @@ export function HelpHint({ contextKey }: { contextKey: string }) {
|
|
|
45
54
|
</SheetTrigger>
|
|
46
55
|
<SheetContent side="right" className="w-full max-w-md sm:max-w-lg">
|
|
47
56
|
<SheetHeader>
|
|
48
|
-
<SheetTitle>{active ? active.
|
|
57
|
+
<SheetTitle>{active ? active.name : t("help.hint.pickArticle")}</SheetTitle>
|
|
49
58
|
<SheetDescription>{active?.summary ?? ""}</SheetDescription>
|
|
50
59
|
</SheetHeader>
|
|
51
60
|
{active ? (
|
|
52
61
|
<div className="mt-4 space-y-3">
|
|
53
62
|
<p className="text-muted-foreground text-sm">{active.summary}</p>
|
|
54
|
-
<Link href={
|
|
63
|
+
<Link href={generateUrl({ page: `/help/${active.howToType}/${active.slug}` })} className="text-sm">
|
|
55
64
|
{t("help.hint.viewArticle")}
|
|
56
65
|
</Link>
|
|
57
66
|
</div>
|
|
@@ -64,7 +73,7 @@ export function HelpHint({ contextKey }: { contextKey: string }) {
|
|
|
64
73
|
onClick={() => setPicked(a)}
|
|
65
74
|
className="hover:bg-muted block w-full rounded px-2 py-1 text-left text-sm"
|
|
66
75
|
>
|
|
67
|
-
<div className="font-medium">{a.
|
|
76
|
+
<div className="font-medium">{a.name}</div>
|
|
68
77
|
<div className="text-muted-foreground text-xs">{a.summary}</div>
|
|
69
78
|
</button>
|
|
70
79
|
</li>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
4
5
|
import { LifeBuoyIcon, ArrowUpRightIcon } from "lucide-react";
|
|
5
6
|
import { CommandItem } from "../../../shadcnui";
|
|
6
7
|
import { usePageUrlGenerator } from "../../../client";
|
|
7
|
-
import {
|
|
8
|
+
import { HowToService } from "../../how-to/data/HowToService";
|
|
9
|
+
import type { HowToInterface } from "../../how-to/data/HowToInterface";
|
|
8
10
|
|
|
9
11
|
export interface HelpSearchResultRowProps {
|
|
10
12
|
result: { id: string; name: string; entityType: string };
|
|
@@ -14,8 +16,16 @@ export interface HelpSearchResultRowProps {
|
|
|
14
16
|
export function HelpSearchResultRow({ result, onSelect }: HelpSearchResultRowProps) {
|
|
15
17
|
const t = useTranslations();
|
|
16
18
|
const generateUrl = usePageUrlGenerator();
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
+
const [article, setArticle] = useState<HowToInterface | null>(null);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let active = true;
|
|
22
|
+
HowToService.findOne({ id: result.id })
|
|
23
|
+
.then((a) => active && setArticle(a))
|
|
24
|
+
.catch(() => active && setArticle(null));
|
|
25
|
+
return () => {
|
|
26
|
+
active = false;
|
|
27
|
+
};
|
|
28
|
+
}, [result.id]);
|
|
19
29
|
|
|
20
30
|
if (!article) {
|
|
21
31
|
return (
|
|
@@ -32,16 +42,16 @@ export function HelpSearchResultRow({ result, onSelect }: HelpSearchResultRowPro
|
|
|
32
42
|
return (
|
|
33
43
|
<CommandItem value={result.id} onSelect={onSelect} className="cursor-pointer p-0">
|
|
34
44
|
<Link
|
|
35
|
-
href={generateUrl({ page: `/help/${article.
|
|
45
|
+
href={generateUrl({ page: `/help/${article.howToType}/${article.slug}` })}
|
|
36
46
|
className="hover:bg-muted flex w-full items-center gap-3 rounded px-3 py-2"
|
|
37
47
|
>
|
|
38
48
|
<div className="bg-muted text-muted-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
|
|
39
49
|
<LifeBuoyIcon className="h-5 w-5" />
|
|
40
50
|
</div>
|
|
41
51
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
42
|
-
<span className="text-foreground truncate text-sm font-semibold">{article.
|
|
52
|
+
<span className="text-foreground truncate text-sm font-semibold">{article.name}</span>
|
|
43
53
|
<span className="text-muted-foreground truncate text-xs">
|
|
44
|
-
{t(`help.modes.${article.
|
|
54
|
+
{t(`help.modes.${article.howToType}`)} · {article.summary}
|
|
45
55
|
</span>
|
|
46
56
|
</div>
|
|
47
57
|
<ArrowUpRightIcon className="text-muted-foreground h-5 w-5 shrink-0" />
|
|
@@ -1,24 +1,60 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
4
|
import { useTranslations } from "next-intl";
|
|
3
5
|
import Link from "next/link";
|
|
4
6
|
import { usePathname } from "next/navigation";
|
|
5
7
|
import { HELP_MODES } from "../types/help-article.types";
|
|
6
8
|
import { usePageUrlGenerator } from "../../../client";
|
|
7
|
-
import { useHelp } from "../contexts/HelpContext";
|
|
8
9
|
import { useHelpFilter } from "../hooks/useHelpFilter";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import { HowToService } from "../../how-to/data/HowToService";
|
|
11
|
+
import type { HowToInterface } from "../../how-to/data/HowToInterface";
|
|
11
12
|
|
|
12
13
|
export function HelpSideNav() {
|
|
13
14
|
const t = useTranslations();
|
|
14
15
|
const generateUrl = usePageUrlGenerator();
|
|
15
16
|
const pathname = usePathname();
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const [articles, setArticles] = useState<HowToInterface[]>([]);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let active = true;
|
|
21
|
+
HowToService.findPublished()
|
|
22
|
+
.then((list) => active && setArticles(list))
|
|
23
|
+
.catch(() => active && setArticles([]));
|
|
24
|
+
return () => {
|
|
25
|
+
active = false;
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
// useHelpFilter indexes by title/summary/tags — map HowTo getters into the shape it expects.
|
|
30
|
+
const indexable = useMemo(
|
|
31
|
+
() =>
|
|
32
|
+
articles.map((a) => ({
|
|
33
|
+
id: a.id,
|
|
34
|
+
title: a.name,
|
|
35
|
+
summary: a.summary ?? "",
|
|
36
|
+
tags: a.tags,
|
|
37
|
+
mode: a.howToType ?? "",
|
|
38
|
+
slug: a.slug ?? "",
|
|
39
|
+
})),
|
|
40
|
+
[articles],
|
|
41
|
+
);
|
|
42
|
+
const { query, setQuery, filtered } = useHelpFilter(indexable as any);
|
|
20
43
|
const filtering = query.trim().length > 0;
|
|
21
44
|
|
|
45
|
+
const groups = useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
HELP_MODES.map((mode) => ({
|
|
48
|
+
mode,
|
|
49
|
+
articles: articles
|
|
50
|
+
.filter((a) => a.howToType === mode && !a.draft)
|
|
51
|
+
.sort((x, y) => (x.order ?? 0) - (y.order ?? 0)),
|
|
52
|
+
})),
|
|
53
|
+
[articles],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const isActive = (mode: string, slug: string) => pathname.endsWith(`/help/${mode}/${slug}`);
|
|
57
|
+
|
|
22
58
|
return (
|
|
23
59
|
<nav aria-label="Help navigation" className="flex h-full flex-col gap-3 overflow-y-auto p-4">
|
|
24
60
|
<input
|
|
@@ -29,17 +65,17 @@ export function HelpSideNav() {
|
|
|
29
65
|
className="border-input bg-background w-full rounded-md border px-3 py-2 text-sm"
|
|
30
66
|
/>
|
|
31
67
|
{filtering ? (
|
|
32
|
-
filtered.length === 0 ? (
|
|
68
|
+
(filtered as any[]).length === 0 ? (
|
|
33
69
|
<p className="text-muted-foreground text-sm">{t("help.sideNav.noMatches")}</p>
|
|
34
70
|
) : (
|
|
35
71
|
<ul className="space-y-1">
|
|
36
|
-
{filtered.map((a) => (
|
|
72
|
+
{(filtered as any[]).map((a) => (
|
|
37
73
|
<li key={a.id}>
|
|
38
74
|
<Link
|
|
39
|
-
href={
|
|
75
|
+
href={generateUrl({ page: `/help/${a.mode}/${a.slug}` })}
|
|
40
76
|
className={
|
|
41
77
|
"hover:bg-muted block rounded px-2 py-1 text-sm " +
|
|
42
|
-
(
|
|
78
|
+
(isActive(a.mode, a.slug) ? "bg-muted font-medium" : "")
|
|
43
79
|
}
|
|
44
80
|
>
|
|
45
81
|
{a.title}
|
|
@@ -49,28 +85,27 @@ export function HelpSideNav() {
|
|
|
49
85
|
</ul>
|
|
50
86
|
)
|
|
51
87
|
) : (
|
|
52
|
-
|
|
53
|
-
const group = groups.find((g) => g.mode === mode)!;
|
|
88
|
+
groups.map((group) => {
|
|
54
89
|
if (group.articles.length === 0) return null;
|
|
55
90
|
return (
|
|
56
|
-
<div key={mode}>
|
|
91
|
+
<div key={group.mode}>
|
|
57
92
|
<Link
|
|
58
|
-
href={
|
|
93
|
+
href={generateUrl({ page: `/help/${group.mode}` })}
|
|
59
94
|
className="text-muted-foreground hover:text-foreground mb-1 block text-xs font-semibold tracking-wider uppercase"
|
|
60
95
|
>
|
|
61
|
-
{t(`help.modes.${mode}`)}
|
|
96
|
+
{t(`help.modes.${group.mode}`)}
|
|
62
97
|
</Link>
|
|
63
98
|
<ul className="space-y-0.5">
|
|
64
99
|
{group.articles.map((a) => (
|
|
65
100
|
<li key={a.id}>
|
|
66
101
|
<Link
|
|
67
|
-
href={
|
|
102
|
+
href={generateUrl({ page: `/help/${a.howToType}/${a.slug}` })}
|
|
68
103
|
className={
|
|
69
104
|
"hover:bg-muted block rounded px-2 py-1 text-sm " +
|
|
70
|
-
(
|
|
105
|
+
(isActive(a.howToType ?? "", a.slug ?? "") ? "bg-muted font-medium" : "")
|
|
71
106
|
}
|
|
72
107
|
>
|
|
73
|
-
{a.
|
|
108
|
+
{a.name}
|
|
74
109
|
</Link>
|
|
75
110
|
</li>
|
|
76
111
|
))}
|
|
@@ -17,7 +17,7 @@ vi.mock("../HelpAssistantSheet", () => ({
|
|
|
17
17
|
function setupConfig() {
|
|
18
18
|
configureJsonApi({
|
|
19
19
|
apiUrl: "http://localhost",
|
|
20
|
-
helpContent: {
|
|
20
|
+
helpContent: { brand: { label: "narr8" } },
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { configureJsonApi } from "../../../../client/config";
|
|
4
4
|
import { HelpProvider } from "../../contexts/HelpContext";
|
|
5
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
|
+
}));
|
|
6
11
|
|
|
7
12
|
const article = {
|
|
8
13
|
id: "1",
|
|
9
14
|
slug: "x",
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
howToType: "how-to",
|
|
16
|
+
name: "X",
|
|
12
17
|
summary: "S",
|
|
13
|
-
order: 1,
|
|
14
|
-
tags: [],
|
|
15
18
|
contextualKeys: ["npc.editor"],
|
|
16
|
-
aiIndexed: true,
|
|
17
19
|
draft: false,
|
|
18
|
-
|
|
19
|
-
path: "how-to/x.mdx",
|
|
20
|
-
headings: [],
|
|
21
|
-
relatedSlugs: [],
|
|
22
|
-
lastUpdated: "2026-01-01T00:00:00Z",
|
|
23
|
-
};
|
|
20
|
+
} as any;
|
|
24
21
|
|
|
25
22
|
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
vi.mocked(HowToService.findPublished).mockResolvedValue([article]);
|
|
26
25
|
configureJsonApi({
|
|
27
26
|
apiUrl: "http://localhost",
|
|
28
|
-
helpContent: {
|
|
27
|
+
helpContent: { brand: { label: "narr8" } },
|
|
29
28
|
});
|
|
30
29
|
});
|
|
31
30
|
|
|
32
31
|
describe("HelpHint", () => {
|
|
33
|
-
it("renders null when no article matches the contextKey", () => {
|
|
32
|
+
it("renders null when no published article matches the contextKey", async () => {
|
|
34
33
|
const { container } = render(
|
|
35
34
|
<HelpProvider>
|
|
36
35
|
<HelpHint contextKey="nope.absent" />
|
|
37
36
|
</HelpProvider>,
|
|
38
37
|
);
|
|
38
|
+
await waitFor(() => expect(HowToService.findPublished).toHaveBeenCalled());
|
|
39
39
|
expect(container.querySelector("button")).toBeNull();
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
it("renders the trigger when at least one article matches", () => {
|
|
42
|
+
it("renders the trigger when at least one published article matches", async () => {
|
|
43
43
|
render(
|
|
44
44
|
<HelpProvider>
|
|
45
45
|
<HelpHint contextKey="npc.editor" />
|
|
46
46
|
</HelpProvider>,
|
|
47
47
|
);
|
|
48
|
-
expect(screen.
|
|
48
|
+
expect(await screen.findByLabelText(/help on this|hint/i)).toBeTruthy();
|
|
49
49
|
});
|
|
50
50
|
});
|
|
@@ -1,37 +1,34 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { Command, CommandList } from "../../../../shadcnui";
|
|
4
4
|
import { configureJsonApi } from "../../../../client/config";
|
|
5
5
|
import { HelpProvider } from "../../contexts/HelpContext";
|
|
6
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
|
+
}));
|
|
7
12
|
|
|
8
13
|
const article = {
|
|
9
14
|
id: "abc",
|
|
10
15
|
slug: "x",
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
howToType: "how-to",
|
|
17
|
+
name: "X title",
|
|
13
18
|
summary: "S",
|
|
14
|
-
|
|
15
|
-
tags: [],
|
|
16
|
-
contextualKeys: [],
|
|
17
|
-
aiIndexed: true,
|
|
18
|
-
draft: false,
|
|
19
|
-
contentHash: "h",
|
|
20
|
-
path: "how-to/x.mdx",
|
|
21
|
-
headings: [],
|
|
22
|
-
relatedSlugs: [],
|
|
23
|
-
lastUpdated: "2026-01-01T00:00:00Z",
|
|
24
|
-
};
|
|
19
|
+
} as any;
|
|
25
20
|
|
|
26
21
|
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
27
23
|
configureJsonApi({
|
|
28
24
|
apiUrl: "http://localhost",
|
|
29
|
-
helpContent: {
|
|
25
|
+
helpContent: { brand: { label: "narr8" } },
|
|
30
26
|
});
|
|
31
27
|
});
|
|
32
28
|
|
|
33
29
|
describe("HelpSearchResultRow", () => {
|
|
34
|
-
it("renders the article title when
|
|
30
|
+
it("renders the article title when the service returns the how-to", async () => {
|
|
31
|
+
vi.mocked(HowToService.findOne).mockResolvedValue(article);
|
|
35
32
|
render(
|
|
36
33
|
<HelpProvider>
|
|
37
34
|
<Command>
|
|
@@ -41,10 +38,11 @@ describe("HelpSearchResultRow", () => {
|
|
|
41
38
|
</Command>
|
|
42
39
|
</HelpProvider>,
|
|
43
40
|
);
|
|
44
|
-
expect(screen.
|
|
41
|
+
expect(await screen.findByText("X title")).toBeTruthy();
|
|
45
42
|
});
|
|
46
43
|
|
|
47
|
-
it("renders disabled removed-state row when
|
|
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"));
|
|
48
46
|
render(
|
|
49
47
|
<HelpProvider>
|
|
50
48
|
<Command>
|
|
@@ -54,6 +52,7 @@ describe("HelpSearchResultRow", () => {
|
|
|
54
52
|
</Command>
|
|
55
53
|
</HelpProvider>,
|
|
56
54
|
);
|
|
55
|
+
await waitFor(() => expect(HowToService.findOne).toHaveBeenCalledWith({ id: "ghost" }));
|
|
57
56
|
expect(screen.queryByText("X title")).toBeNull();
|
|
58
57
|
});
|
|
59
58
|
});
|
|
@@ -1,50 +1,48 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
2
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
3
|
import { configureJsonApi } from "../../../../client/config";
|
|
4
4
|
import { HelpProvider } from "../../contexts/HelpContext";
|
|
5
5
|
import { HelpSideNav } from "../HelpSideNav";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
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
|
+
];
|
|
17
19
|
|
|
18
20
|
beforeEach(() => {
|
|
19
21
|
configureJsonApi({
|
|
20
22
|
apiUrl: "http://localhost",
|
|
21
|
-
helpContent: {
|
|
22
|
-
manifest: [
|
|
23
|
-
{ ...base, id: "a", slug: "a", mode: "how-to", title: "Alpha", summary: "s", order: 1, path: "how-to/a.mdx" },
|
|
24
|
-
{ ...base, id: "b", slug: "b", mode: "how-to", title: "Beta", summary: "s", order: 2, path: "how-to/b.mdx" },
|
|
25
|
-
],
|
|
26
|
-
namespaceUuid: "00000000-0000-5000-8000-000000000000",
|
|
27
|
-
},
|
|
23
|
+
helpContent: { brand: { label: "narr8" } },
|
|
28
24
|
});
|
|
25
|
+
(HowToService.findPublished as ReturnType<typeof vi.fn>).mockResolvedValue(published as any);
|
|
29
26
|
});
|
|
30
27
|
|
|
31
28
|
describe("HelpSideNav", () => {
|
|
32
|
-
it("lists every non-draft article", () => {
|
|
29
|
+
it("lists every non-draft article", async () => {
|
|
33
30
|
render(
|
|
34
31
|
<HelpProvider>
|
|
35
32
|
<HelpSideNav />
|
|
36
33
|
</HelpProvider>,
|
|
37
34
|
);
|
|
38
|
-
expect(screen.
|
|
39
|
-
expect(screen.
|
|
35
|
+
expect(await screen.findByText("Alpha")).toBeTruthy();
|
|
36
|
+
expect(await screen.findByText("Beta")).toBeTruthy();
|
|
40
37
|
});
|
|
41
38
|
|
|
42
|
-
it("filters by query", () => {
|
|
39
|
+
it("filters by query", async () => {
|
|
43
40
|
render(
|
|
44
41
|
<HelpProvider>
|
|
45
42
|
<HelpSideNav />
|
|
46
43
|
</HelpProvider>,
|
|
47
44
|
);
|
|
45
|
+
await screen.findByText("Alpha");
|
|
48
46
|
fireEvent.change(screen.getByPlaceholderText(/filter/i), { target: { value: "Bet" } });
|
|
49
47
|
expect(screen.queryByText("Alpha")).toBeNull();
|
|
50
48
|
expect(screen.getByText("Beta")).toBeTruthy();
|
|
@@ -6,8 +6,6 @@ import { HelpProvider, useHelp } from "./HelpContext";
|
|
|
6
6
|
|
|
7
7
|
describe("HelpProvider / useHelp", () => {
|
|
8
8
|
const cfg = {
|
|
9
|
-
manifest: [],
|
|
10
|
-
namespaceUuid: "00000000-0000-5000-8000-000000000000",
|
|
11
9
|
brand: { logo: "/logo.png", label: "Test", appHref: "/" },
|
|
12
10
|
};
|
|
13
11
|
|
|
@@ -18,8 +16,8 @@ describe("HelpProvider / useHelp", () => {
|
|
|
18
16
|
it("exposes the configured helpContent via useHelp()", () => {
|
|
19
17
|
const wrapper = ({ children }: { children: ReactNode }) => <HelpProvider>{children}</HelpProvider>;
|
|
20
18
|
const { result } = renderHook(() => useHelp(), { wrapper });
|
|
21
|
-
expect(result.current.manifest).toBe(cfg.manifest);
|
|
22
19
|
expect(result.current.brand?.label).toBe("Test");
|
|
20
|
+
expect(result.current.brand?.appHref).toBe("/");
|
|
23
21
|
});
|
|
24
22
|
|
|
25
23
|
it("throws when useHelp() is called outside HelpProvider", () => {
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// Consumed via `@carlonicora/nextjs-jsonapi/help`. Tsup adds a top-level
|
|
3
3
|
// "use client" directive to the bundled output via clientEntries.
|
|
4
4
|
//
|
|
5
|
-
// Pair this with `@carlonicora/nextjs-jsonapi/help/server`
|
|
6
|
-
//
|
|
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
7
|
// Consumers compose page shells themselves: import the client primitives below
|
|
8
8
|
// (HelpProvider, HelpHeader, HelpSideNav, HelpArticleBody, HelpTOC, etc.) and
|
|
9
|
-
//
|
|
9
|
+
// assemble pages in app routes.
|
|
10
10
|
|
|
11
11
|
export { HelpProvider, useHelp } from "./contexts/HelpContext";
|
|
12
12
|
export { HelpHeader } from "./components/HelpHeader";
|
|
@@ -16,9 +16,6 @@ export { HelpTOC } from "./components/HelpTOC";
|
|
|
16
16
|
export { HelpHint } from "./components/HelpHint";
|
|
17
17
|
export { HelpAskAi } from "./components/HelpAskAi";
|
|
18
18
|
export { HelpSearchResultRow } from "./components/HelpSearchResultRow";
|
|
19
|
-
export { MDX_COMPONENTS } from "./components/mdx/mdxComponents";
|
|
20
|
-
export { useHelpManifest } from "./hooks/useHelpManifest";
|
|
21
|
-
export { useHelpArticle } from "./hooks/useHelpArticle";
|
|
22
19
|
export { useHelpFilter } from "./hooks/useHelpFilter";
|
|
23
20
|
export { articleUrl, modeUrl } from "./utils/articleUrl";
|
|
24
21
|
export { HELP_I18N_KEYS } from "./i18n-keys";
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { HelpArticle, HelpRedirect } from "../types/help-article.types";
|
|
2
|
-
|
|
3
1
|
export interface HelpBrandConfig {
|
|
4
2
|
/** URL of the brand logo image (served by the consuming app). */
|
|
5
3
|
logo?: string;
|
|
@@ -10,8 +8,5 @@ export interface HelpBrandConfig {
|
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
export interface HelpContentConfig {
|
|
13
|
-
manifest: readonly HelpArticle[];
|
|
14
|
-
namespaceUuid: string;
|
|
15
|
-
redirects?: readonly HelpRedirect[];
|
|
16
11
|
brand?: HelpBrandConfig;
|
|
17
12
|
}
|
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
// Server-
|
|
1
|
+
// Server-safe public barrel for the help feature.
|
|
2
2
|
// Consumed via `@carlonicora/nextjs-jsonapi/help/server`. Must NOT receive a
|
|
3
|
-
// "use client" directive.
|
|
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.
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// without leaking client-only modules (react-dom/client, hook usage at module
|
|
9
|
-
// init) into server scopes. Consumers therefore compose the page shells in
|
|
10
|
-
// their own route files, importing client primitives from
|
|
11
|
-
// `@carlonicora/nextjs-jsonapi/help` and server utilities from here.
|
|
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`.
|
|
12
9
|
|
|
13
|
-
export { getHelpContent } from "./server/getHelpContent";
|
|
14
|
-
export { renderHelpArticle, serializeHelpArticle } from "./server/serializeHelpArticle";
|
|
15
|
-
export { generateHelpArticleStaticParams, generateHelpArticleMetadata, generateHelpModeStaticParams } from "./server";
|
|
16
|
-
export { findHelpArticle, prevNextWithinMode, buildHelpNav } from "./utils/helpNavigation";
|
|
17
|
-
export { setHelpReader } from "../../core/registry/helpStore";
|
|
18
|
-
export type { HelpContentConfig, HelpBrandConfig } from "./interfaces/help-content-config.interface";
|
|
19
10
|
export { HELP_MODES } from "./types/help-article.types";
|
|
20
11
|
export type { HelpArticle, HelpHeading, HelpMode, HelpRedirect } from "./types/help-article.types";
|