Skip to content

Commit 9f9cfdf

Browse files
committed
feat(specs/client): nomenclature support
1 parent cdfa22a commit 9f9cfdf

4 files changed

Lines changed: 186 additions & 28 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { createContext, useContext, useState, type ReactNode } from "react";
2+
import {
3+
Sheet,
4+
SheetContent,
5+
SheetHeader,
6+
SheetTitle,
7+
} from "@/components/ui/sheet";
8+
import { Skeleton } from "@/components/ui/skeleton";
9+
import { useNomenclature } from "@/hooks/use-nomenclature";
10+
11+
const NomenclatureDrawerContext = createContext<
12+
((name: string) => void) | null
13+
>(null);
14+
15+
function useNomenclatureDrawer() {
16+
const open = useContext(NomenclatureDrawerContext);
17+
if (!open) {
18+
throw new Error(
19+
"useNomenclatureDrawer must be used within a NomenclatureDrawerProvider",
20+
);
21+
}
22+
return open;
23+
}
24+
25+
function NomenclatureHeader({ name }: { name: string }) {
26+
const { data, isPending } = useNomenclature(name);
27+
28+
if (isPending) {
29+
return (
30+
<div className="space-y-2">
31+
<Skeleton className="h-5 w-40" />
32+
<Skeleton className="h-4 w-64" />
33+
</div>
34+
);
35+
}
36+
37+
return (
38+
<>
39+
<SheetTitle className="font-bold text-xl">{data?.title ?? name}</SheetTitle>
40+
{data?.description && (
41+
<p className="text-sm text-muted-foreground">{data.description}</p>
42+
)}
43+
</>
44+
);
45+
}
46+
47+
function NomenclatureContent({ name }: { name: string }) {
48+
const { data, isPending, isError } = useNomenclature(name);
49+
50+
if (isPending) {
51+
return (
52+
<div className="space-y-2 p-4">
53+
{Array.from({ length: 4 }).map((_, i) => (
54+
<Skeleton key={i} className="h-6 w-full" />
55+
))}
56+
</div>
57+
);
58+
}
59+
60+
if (isError) {
61+
return (
62+
<p className="p-4 text-sm text-muted-foreground">
63+
Impossible de charger la nomenclature.
64+
</p>
65+
);
66+
}
67+
68+
return (
69+
<table className="w-full text-sm">
70+
<tbody>
71+
{data.oneOf?.map((option) => (
72+
<tr key={option.const} className="border-b last:border-0">
73+
<td className="w-px whitespace-nowrap p-4 align-top">
74+
<span className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-muted-foreground">
75+
{option.const}
76+
</span>
77+
</td>
78+
<td className="p-4 pl-0 align-top">
79+
<p className="font-bold text-sm">{option.title}</p>
80+
<p className="italic text-xs">{option?.description}</p>
81+
</td>
82+
</tr>
83+
))}
84+
</tbody>
85+
</table>
86+
);
87+
}
88+
89+
export function NomenclatureDrawerProvider({
90+
children,
91+
}: {
92+
children: ReactNode;
93+
}) {
94+
const [name, setName] = useState<string | null>(null);
95+
96+
return (
97+
<NomenclatureDrawerContext.Provider value={setName}>
98+
{children}
99+
<Sheet open={name !== null} onOpenChange={(open) => !open && setName(null)}>
100+
<SheetContent className={"w-full max-w-4xl!"}>
101+
<SheetHeader className="border-b">{name && <NomenclatureHeader name={name} />}</SheetHeader>
102+
<div className="min-h-0 flex-1 overflow-y-auto w-full">
103+
{name && <NomenclatureContent name={name} />}
104+
</div>
105+
</SheetContent>
106+
</Sheet>
107+
</NomenclatureDrawerContext.Provider>
108+
);
109+
}
110+
111+
export function NomenclatureBadge({ name }: { name: string }) {
112+
const open = useNomenclatureDrawer();
113+
114+
return (
115+
<button
116+
type="button"
117+
onClick={(e) => {
118+
e.stopPropagation();
119+
open(name);
120+
}}
121+
className="rounded bg-primary px-1.5 py-0.5 font-mono text-xs text-primary-foreground hover:bg-primary/90"
122+
>
123+
{name}
124+
</button>
125+
);
126+
}

tools/specs/client/src/components/schema-detail.tsx

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
} from "@/components/ui/accordion";
88
import { Button } from "@/components/ui/button";
99
import { Skeleton } from "@/components/ui/skeleton";
10+
import {
11+
NomenclatureBadge,
12+
NomenclatureDrawerProvider,
13+
} from "@/components/nomenclature-drawer";
1014
import type {
1115
JsonSchemaDefinitions,
1216
JsonSchemaDocument,
@@ -88,10 +92,14 @@ function FieldHeader({
8892
<span className="font-mono text-sm">
8993
{name}
9094
{required && <span className="text-destructive"> *</span>}
95+
9196
</span>
9297
<span className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-muted-foreground">
9398
{fieldType(prop, definitions)}
9499
</span>
100+
{prop["x-nomenclature"] && (
101+
<NomenclatureBadge name={prop["x-nomenclature"]} />
102+
)}
95103
</div>
96104
);
97105
}
@@ -201,39 +209,41 @@ export function SchemaDetail({ schema }: SchemaDetailProps) {
201209
const definitions = schema.definitions ?? schema.$defs ?? {};
202210

203211
return (
204-
<div className="min-h-0 flex-1 overflow-y-auto p-8 w-full max-w-7xl mx-auto">
205-
<div className="flex items-start justify-between gap-4">
206-
<div>
207-
<h1 className="text-lg font-semibold">{schema.title}</h1>
208-
{schema.description && (
209-
<p className="mt-1 text-sm text-muted-foreground">
210-
{schema.description}
211-
</p>
212+
<NomenclatureDrawerProvider>
213+
<div className="min-h-0 flex-1 overflow-y-auto p-8 w-full max-w-7xl mx-auto">
214+
<div className="flex items-start justify-between gap-4">
215+
<div>
216+
<h1 className="text-lg font-semibold">{schema.title}</h1>
217+
{schema.description && (
218+
<p className="mt-1 text-sm text-muted-foreground">
219+
{schema.description}
220+
</p>
221+
)}
222+
</div>
223+
{hasProperties && (
224+
<Button
225+
variant="outline"
226+
onClick={() =>
227+
setExpandSignal((s) => ({ key: s.key + 1, expand: !s.expand }))
228+
}
229+
>
230+
{expandSignal.expand ? "Tout replier" : "Tout déplier"}
231+
</Button>
212232
)}
213233
</div>
234+
214235
{hasProperties && (
215-
<Button
216-
variant="outline"
217-
onClick={() =>
218-
setExpandSignal((s) => ({ key: s.key + 1, expand: !s.expand }))
219-
}
220-
>
221-
{expandSignal.expand ? "Tout replier" : "Tout déplier"}
222-
</Button>
236+
<div className="mt-6">
237+
<SchemaFields
238+
properties={properties}
239+
required={schema.required}
240+
definitions={definitions}
241+
expandSignal={expandSignal}
242+
/>
243+
</div>
223244
)}
224245
</div>
225-
226-
{hasProperties && (
227-
<div className="mt-6">
228-
<SchemaFields
229-
properties={properties}
230-
required={schema.required}
231-
definitions={definitions}
232-
expandSignal={expandSignal}
233-
/>
234-
</div>
235-
)}
236-
</div>
246+
</NomenclatureDrawerProvider>
237247
);
238248
}
239249

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { defaultBranch } from "@/config";
3+
import { buildNomenclatureUrl } from "@/lib/utils";
4+
import type { NomenclatureSchema } from "@/types";
5+
6+
export function useNomenclature(name: string) {
7+
return useQuery({
8+
queryKey: ["nomenclature", name],
9+
queryFn: async (): Promise<NomenclatureSchema> => {
10+
const res = await fetch(buildNomenclatureUrl(name, defaultBranch));
11+
if (!res.ok) throw new Error("not found");
12+
return res.json();
13+
},
14+
});
15+
}

tools/specs/client/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type JsonSchemaProperty = {
1313
required?: string[];
1414
items?: JsonSchemaProperty;
1515
$ref?: string;
16+
"x-nomenclature"?: string;
1617
};
1718

1819
export type JsonSchemaDefinitions = Record<string, JsonSchemaProperty>;
@@ -22,3 +23,9 @@ export type JsonSchemaDocument = JsonSchemaProperty & {
2223
definitions?: JsonSchemaDefinitions;
2324
$defs?: JsonSchemaDefinitions;
2425
};
26+
27+
export type NomenclatureSchema = {
28+
title?: string;
29+
description?: string;
30+
oneOf?: { const: string; title: string; description?: string }[];
31+
};

0 commit comments

Comments
 (0)