@djangocfg/ui-tools 2.1.289 → 2.1.291
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/README.md +14 -3
- package/dist/{DocsLayout-YDR7DSMM.cjs → DocsLayout-IKH7BLSU.cjs} +1537 -682
- package/dist/DocsLayout-IKH7BLSU.cjs.map +1 -0
- package/dist/{DocsLayout-TKJQ5W5E.mjs → DocsLayout-JPXFUKAR.mjs} +1429 -574
- package/dist/DocsLayout-JPXFUKAR.mjs.map +1 -0
- package/dist/{PrettyCode.client-5GABIN2I.cjs → PrettyCode.client-RPDIE5CH.cjs} +104 -3
- package/dist/PrettyCode.client-RPDIE5CH.cjs.map +1 -0
- package/dist/{PrettyCode.client-IZTXXYHG.mjs → PrettyCode.client-SPMTQEG4.mjs} +106 -5
- package/dist/PrettyCode.client-SPMTQEG4.mjs.map +1 -0
- package/dist/{chunk-IULI4XII.cjs → chunk-5Q4UMSWB.cjs} +355 -9
- package/dist/chunk-5Q4UMSWB.cjs.map +1 -0
- package/dist/{chunk-VZGQC3NG.mjs → chunk-EFWOJPA6.mjs} +349 -9
- package/dist/chunk-EFWOJPA6.mjs.map +1 -0
- package/dist/index.cjs +18 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.mjs +13 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +20 -15
- package/src/components/markdown/MarkdownMessage.tsx +46 -0
- package/src/tools/MarkdownEditor/MarkdownEditor.tsx +42 -1
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +87 -178
- package/src/tools/OpenapiViewer/README.md +114 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +20 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +6 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx +36 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/index.tsx +56 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts +77 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +146 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MethodBadge.tsx +6 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/PathDisplay.tsx +26 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +87 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamGroup.tsx +30 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamRow.tsx +36 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/index.tsx +22 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/RequestBody/index.tsx +33 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +76 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseRow.tsx +80 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +32 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/index.tsx +21 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +106 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +127 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/index.tsx +31 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/types.ts +28 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +87 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/defaults.ts +27 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +45 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/context.tsx +56 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/hooks/useSectionHash.ts +63 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +96 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +133 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/selectors.ts +40 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/types.ts +17 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +8 -2
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +48 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/CategoryBlock.tsx +33 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/EndpointRow.tsx +73 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +43 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SchemaSection.tsx +27 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SearchInput.tsx +45 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SidebarBody.tsx +50 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +64 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/buildVM.ts +126 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +112 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/types.ts +42 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/useDebouncedValue.ts +14 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +10 -7
- package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +9 -6
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +55 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PreviewView.tsx +115 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/RawView.tsx +24 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/StatusBar.tsx +63 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/ViewTabs.tsx +45 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/detectContent.ts +97 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/index.tsx +93 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/types.ts +26 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/useResponseView.ts +62 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +41 -71
- package/src/tools/OpenapiViewer/types.ts +10 -0
- package/src/tools/OpenapiViewer/utils/codeSamples.ts +287 -0
- package/src/tools/OpenapiViewer/utils/index.ts +3 -0
- package/src/tools/OpenapiViewer/utils/operationToHar.ts +119 -0
- package/src/tools/OpenapiViewer/utils/sampler.ts +72 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +88 -1
- package/src/tools/PrettyCode/PrettyCode.story.tsx +114 -361
- package/src/tools/PrettyCode/index.tsx +13 -0
- package/src/tools/PrettyCode/lazy.tsx +5 -0
- package/src/tools/PrettyCode/registerPrismLanguages.ts +111 -0
- package/dist/DocsLayout-TKJQ5W5E.mjs.map +0 -1
- package/dist/DocsLayout-YDR7DSMM.cjs.map +0 -1
- package/dist/PrettyCode.client-5GABIN2I.cjs.map +0 -1
- package/dist/PrettyCode.client-IZTXXYHG.mjs.map +0 -1
- package/dist/chunk-IULI4XII.cjs.map +0 -1
- package/dist/chunk-VZGQC3NG.mjs.map +0 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +0 -273
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +0 -439
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel.tsx +0 -127
|
@@ -1,439 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Search } from 'lucide-react';
|
|
4
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
Combobox,
|
|
8
|
-
Input,
|
|
9
|
-
Tooltip,
|
|
10
|
-
TooltipContent,
|
|
11
|
-
TooltipTrigger,
|
|
12
|
-
} from '@djangocfg/ui-core/components';
|
|
13
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
14
|
-
|
|
15
|
-
import type { ApiEndpoint, OpenApiInfo, OpenApiSchema, SchemaSource } from '../../types';
|
|
16
|
-
import { deduplicateEndpoints } from '../../utils/versionManager';
|
|
17
|
-
import { MethodBadge, ScrollArea } from '../shared/ui';
|
|
18
|
-
import { endpointAnchor } from './anchor';
|
|
19
|
-
import { buildSchemaSections, groupEndpoints, type EndpointGroup } from './grouping';
|
|
20
|
-
import { SchemaCopyMenu } from './SchemaCopyMenu';
|
|
21
|
-
import { sidebarLabel, sidebarTooltip } from './sidebarLabel';
|
|
22
|
-
|
|
23
|
-
// ─── Public props ────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
export interface DocsSidebarProps {
|
|
26
|
-
info: OpenApiInfo | null;
|
|
27
|
-
/** Active-schema endpoints — used by ``selector`` mode. */
|
|
28
|
-
endpoints: ApiEndpoint[];
|
|
29
|
-
/** All configured schemas (used by both modes). */
|
|
30
|
-
schemas: SchemaSource[];
|
|
31
|
-
currentSchemaId: string | null;
|
|
32
|
-
onSchemaChange: (id: string) => void;
|
|
33
|
-
activeEndpointId: string | null;
|
|
34
|
-
selectedVersion: string;
|
|
35
|
-
onNavigate: (anchor: string, schemaId?: string | null) => void;
|
|
36
|
-
/** Presentation mode. Default: ``selector`` (back-compat). */
|
|
37
|
-
grouping?: 'selector' | 'sections';
|
|
38
|
-
/** Required for ``sections`` mode — endpoints grouped by their source
|
|
39
|
-
* schema id. The sidebar renders them as two-level sections. */
|
|
40
|
-
endpointsBySchema?: Record<string, ApiEndpoint[]>;
|
|
41
|
-
/** Raw active schema + resolved base URL — used by the Copy-for-AI
|
|
42
|
-
* dropdown in the brand row. ``null`` disables the button. */
|
|
43
|
-
rawSchema?: OpenApiSchema | null;
|
|
44
|
-
resolvedBaseUrl?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** HTTP methods the sidebar can filter by. Reordered like the docs
|
|
48
|
-
* themselves: read, then write. ``ALL`` is a sentinel for no filter. */
|
|
49
|
-
const METHOD_FILTERS = ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
|
|
50
|
-
type MethodFilter = (typeof METHOD_FILTERS)[number];
|
|
51
|
-
|
|
52
|
-
// ─── View-model types ────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
interface EndpointRowVM {
|
|
55
|
-
key: string;
|
|
56
|
-
anchor: string;
|
|
57
|
-
schemaId: string | null;
|
|
58
|
-
label: string;
|
|
59
|
-
tooltip: string;
|
|
60
|
-
method: string;
|
|
61
|
-
/** Summary-less endpoints get a monospace font for the path-tail label. */
|
|
62
|
-
useMono: boolean;
|
|
63
|
-
isActive: boolean;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface CategoryVM {
|
|
67
|
-
key: string;
|
|
68
|
-
category: string;
|
|
69
|
-
rows: EndpointRowVM[];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
interface SchemaSectionVM {
|
|
73
|
-
sourceId: string;
|
|
74
|
-
sourceName: string;
|
|
75
|
-
categories: CategoryVM[];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
type SidebarBodyVM =
|
|
79
|
-
| { kind: 'flat'; categories: CategoryVM[]; emptyText: string }
|
|
80
|
-
| { kind: 'sections'; sections: SchemaSectionVM[]; emptyText: string };
|
|
81
|
-
|
|
82
|
-
// ─── Pure builders ───────────────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
function filterEndpoints(
|
|
85
|
-
list: ApiEndpoint[],
|
|
86
|
-
query: string,
|
|
87
|
-
method: MethodFilter,
|
|
88
|
-
): ApiEndpoint[] {
|
|
89
|
-
let out = list;
|
|
90
|
-
if (method !== 'ALL') {
|
|
91
|
-
out = out.filter((e) => e.method === method);
|
|
92
|
-
}
|
|
93
|
-
if (query) {
|
|
94
|
-
const q = query.toLowerCase();
|
|
95
|
-
out = out.filter(
|
|
96
|
-
(e) =>
|
|
97
|
-
e.summary.toLowerCase().includes(q) ||
|
|
98
|
-
e.name.toLowerCase().includes(q) ||
|
|
99
|
-
e.description.toLowerCase().includes(q) ||
|
|
100
|
-
e.path.toLowerCase().includes(q),
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
return out;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function buildCategory(
|
|
107
|
-
group: EndpointGroup,
|
|
108
|
-
activeEndpointId: string | null,
|
|
109
|
-
schemaId: string | null,
|
|
110
|
-
keyPrefix: string,
|
|
111
|
-
): CategoryVM {
|
|
112
|
-
const rows: EndpointRowVM[] = group.endpoints.map((ep) => {
|
|
113
|
-
const anchor = endpointAnchor(ep, schemaId ?? ep.schemaId ?? null);
|
|
114
|
-
return {
|
|
115
|
-
key: `${ep.method}-${ep.path}`,
|
|
116
|
-
anchor,
|
|
117
|
-
schemaId: schemaId ?? ep.schemaId ?? null,
|
|
118
|
-
label: sidebarLabel(ep, group.commonPrefix),
|
|
119
|
-
tooltip: sidebarTooltip(ep),
|
|
120
|
-
method: ep.method,
|
|
121
|
-
useMono: !ep.summary,
|
|
122
|
-
isActive: activeEndpointId === anchor,
|
|
123
|
-
};
|
|
124
|
-
});
|
|
125
|
-
return {
|
|
126
|
-
key: `${keyPrefix}${group.category}`,
|
|
127
|
-
category: group.category,
|
|
128
|
-
rows,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const emptyTextFor = (query: string, method: MethodFilter, defaultText: string): string => {
|
|
133
|
-
if (query && method !== 'ALL') return `No ${method} endpoints match "${query}"`;
|
|
134
|
-
if (query) return `No endpoints match "${query}"`;
|
|
135
|
-
if (method !== 'ALL') return `No ${method} endpoints`;
|
|
136
|
-
return defaultText;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
function buildFlatVM(
|
|
140
|
-
endpoints: ApiEndpoint[],
|
|
141
|
-
selectedVersion: string,
|
|
142
|
-
query: string,
|
|
143
|
-
method: MethodFilter,
|
|
144
|
-
activeEndpointId: string | null,
|
|
145
|
-
): SidebarBodyVM {
|
|
146
|
-
const filtered = filterEndpoints(deduplicateEndpoints(endpoints, selectedVersion), query, method);
|
|
147
|
-
const groups = groupEndpoints(filtered);
|
|
148
|
-
return {
|
|
149
|
-
kind: 'flat',
|
|
150
|
-
categories: groups.map((g) => buildCategory(g, activeEndpointId, null, '')),
|
|
151
|
-
emptyText: emptyTextFor(query, method, 'No endpoints in this schema'),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function buildSectionsVM(
|
|
156
|
-
schemas: SchemaSource[],
|
|
157
|
-
endpointsBySchema: Record<string, ApiEndpoint[]>,
|
|
158
|
-
selectedVersion: string,
|
|
159
|
-
query: string,
|
|
160
|
-
method: MethodFilter,
|
|
161
|
-
activeEndpointId: string | null,
|
|
162
|
-
): SidebarBodyVM {
|
|
163
|
-
const filteredMap: Record<string, ApiEndpoint[]> = {};
|
|
164
|
-
for (const src of schemas) {
|
|
165
|
-
const raw = endpointsBySchema[src.id] ?? [];
|
|
166
|
-
filteredMap[src.id] = filterEndpoints(deduplicateEndpoints(raw, selectedVersion), query, method);
|
|
167
|
-
}
|
|
168
|
-
const rawSections = buildSchemaSections(schemas, filteredMap);
|
|
169
|
-
const sections: SchemaSectionVM[] = rawSections
|
|
170
|
-
.filter((s) => s.groups.length > 0)
|
|
171
|
-
.map((s) => ({
|
|
172
|
-
sourceId: s.source.id,
|
|
173
|
-
sourceName: s.source.name,
|
|
174
|
-
categories: s.groups.map((g) => buildCategory(g, activeEndpointId, s.source.id, `${s.source.id}-`)),
|
|
175
|
-
}));
|
|
176
|
-
return {
|
|
177
|
-
kind: 'sections',
|
|
178
|
-
sections,
|
|
179
|
-
emptyText: emptyTextFor(query, method, 'No endpoints in any schema'),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ─── Component ───────────────────────────────────────────────────────────────
|
|
184
|
-
|
|
185
|
-
export function DocsSidebar({
|
|
186
|
-
info,
|
|
187
|
-
endpoints,
|
|
188
|
-
schemas,
|
|
189
|
-
currentSchemaId,
|
|
190
|
-
onSchemaChange,
|
|
191
|
-
activeEndpointId,
|
|
192
|
-
selectedVersion,
|
|
193
|
-
onNavigate,
|
|
194
|
-
grouping = 'selector',
|
|
195
|
-
endpointsBySchema,
|
|
196
|
-
rawSchema,
|
|
197
|
-
resolvedBaseUrl,
|
|
198
|
-
}: DocsSidebarProps) {
|
|
199
|
-
const [search, setSearch] = useState('');
|
|
200
|
-
const [debounced, setDebounced] = useState('');
|
|
201
|
-
const [methodFilter, setMethodFilter] = useState<MethodFilter>('ALL');
|
|
202
|
-
|
|
203
|
-
useEffect(() => {
|
|
204
|
-
const id = setTimeout(() => setDebounced(search), 120);
|
|
205
|
-
return () => clearTimeout(id);
|
|
206
|
-
}, [search]);
|
|
207
|
-
|
|
208
|
-
const body = useMemo<SidebarBodyVM>(() => {
|
|
209
|
-
if (grouping === 'sections') {
|
|
210
|
-
return buildSectionsVM(
|
|
211
|
-
schemas,
|
|
212
|
-
endpointsBySchema ?? {},
|
|
213
|
-
selectedVersion,
|
|
214
|
-
debounced,
|
|
215
|
-
methodFilter,
|
|
216
|
-
activeEndpointId,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
return buildFlatVM(endpoints, selectedVersion, debounced, methodFilter, activeEndpointId);
|
|
220
|
-
}, [
|
|
221
|
-
grouping,
|
|
222
|
-
schemas,
|
|
223
|
-
endpointsBySchema,
|
|
224
|
-
endpoints,
|
|
225
|
-
selectedVersion,
|
|
226
|
-
debounced,
|
|
227
|
-
methodFilter,
|
|
228
|
-
activeEndpointId,
|
|
229
|
-
]);
|
|
230
|
-
|
|
231
|
-
const schemaOptions = useMemo(
|
|
232
|
-
() => schemas.map((s) => ({ value: s.id, label: s.name })),
|
|
233
|
-
[schemas],
|
|
234
|
-
);
|
|
235
|
-
const hasMultipleSchemas = schemas.length > 1;
|
|
236
|
-
const apiTitle = info?.title ?? 'API Reference';
|
|
237
|
-
const showCombobox = grouping === 'selector' && hasMultipleSchemas;
|
|
238
|
-
const copyReady = rawSchema !== null && rawSchema !== undefined && endpoints.length > 0;
|
|
239
|
-
|
|
240
|
-
return (
|
|
241
|
-
<aside className="flex flex-col h-full min-h-0 border-r bg-muted/10">
|
|
242
|
-
{/* Brand row */}
|
|
243
|
-
<div className="shrink-0 border-b px-3 h-12 flex items-center gap-2">
|
|
244
|
-
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
245
|
-
<span className="text-[13px] font-semibold text-foreground truncate">
|
|
246
|
-
{apiTitle}
|
|
247
|
-
</span>
|
|
248
|
-
{info?.version && (
|
|
249
|
-
<span className="font-mono text-[10px] text-muted-foreground/70 shrink-0">
|
|
250
|
-
v{info.version}
|
|
251
|
-
</span>
|
|
252
|
-
)}
|
|
253
|
-
</div>
|
|
254
|
-
{copyReady && (
|
|
255
|
-
<SchemaCopyMenu
|
|
256
|
-
schema={rawSchema ?? null}
|
|
257
|
-
endpoints={endpoints}
|
|
258
|
-
baseUrl={resolvedBaseUrl}
|
|
259
|
-
variant="icon"
|
|
260
|
-
/>
|
|
261
|
-
)}
|
|
262
|
-
</div>
|
|
263
|
-
|
|
264
|
-
{/* Controls */}
|
|
265
|
-
<div className="shrink-0 border-b px-3 py-3 space-y-2">
|
|
266
|
-
{showCombobox && (
|
|
267
|
-
<Combobox
|
|
268
|
-
options={schemaOptions}
|
|
269
|
-
value={currentSchemaId ?? ''}
|
|
270
|
-
onValueChange={(id) => id && onSchemaChange(id)}
|
|
271
|
-
placeholder="Select API"
|
|
272
|
-
searchPlaceholder="Search APIs…"
|
|
273
|
-
emptyText="No APIs found"
|
|
274
|
-
className="w-full h-8 text-xs"
|
|
275
|
-
/>
|
|
276
|
-
)}
|
|
277
|
-
<div className="relative">
|
|
278
|
-
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/50 pointer-events-none" />
|
|
279
|
-
<Input
|
|
280
|
-
placeholder="Search endpoints…"
|
|
281
|
-
value={search}
|
|
282
|
-
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}
|
|
283
|
-
className="pl-8 h-8 text-xs"
|
|
284
|
-
/>
|
|
285
|
-
</div>
|
|
286
|
-
<MethodChips value={methodFilter} onChange={setMethodFilter} />
|
|
287
|
-
</div>
|
|
288
|
-
|
|
289
|
-
<ScrollArea>
|
|
290
|
-
<SidebarBody body={body} onNavigate={onNavigate} />
|
|
291
|
-
</ScrollArea>
|
|
292
|
-
</aside>
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function MethodChips({
|
|
297
|
-
value,
|
|
298
|
-
onChange,
|
|
299
|
-
}: {
|
|
300
|
-
value: MethodFilter;
|
|
301
|
-
onChange: (v: MethodFilter) => void;
|
|
302
|
-
}) {
|
|
303
|
-
return (
|
|
304
|
-
<div className="flex items-center gap-1 flex-wrap">
|
|
305
|
-
{METHOD_FILTERS.map((m) => {
|
|
306
|
-
const active = value === m;
|
|
307
|
-
return (
|
|
308
|
-
<button
|
|
309
|
-
key={m}
|
|
310
|
-
type="button"
|
|
311
|
-
onClick={() => onChange(m)}
|
|
312
|
-
aria-pressed={active}
|
|
313
|
-
className={cn(
|
|
314
|
-
'px-2 py-0.5 rounded font-mono text-[10px] font-semibold tracking-wide transition-colors border',
|
|
315
|
-
active
|
|
316
|
-
? 'bg-primary/15 border-primary/40 text-foreground'
|
|
317
|
-
: 'bg-transparent border-border/40 text-muted-foreground hover:text-foreground hover:border-border',
|
|
318
|
-
)}
|
|
319
|
-
>
|
|
320
|
-
{m}
|
|
321
|
-
</button>
|
|
322
|
-
);
|
|
323
|
-
})}
|
|
324
|
-
</div>
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// ─── Rendering primitives ────────────────────────────────────────────────────
|
|
329
|
-
|
|
330
|
-
type NavigateFn = (anchor: string, schemaId?: string | null) => void;
|
|
331
|
-
|
|
332
|
-
function SidebarBody({ body, onNavigate }: { body: SidebarBodyVM; onNavigate: NavigateFn }) {
|
|
333
|
-
if (body.kind === 'flat') {
|
|
334
|
-
if (body.categories.length === 0) {
|
|
335
|
-
return (
|
|
336
|
-
<div className="py-10 px-4 text-center text-xs text-muted-foreground">
|
|
337
|
-
{body.emptyText}
|
|
338
|
-
</div>
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
return (
|
|
342
|
-
<nav className="py-2">
|
|
343
|
-
{body.categories.map((cat) => (
|
|
344
|
-
<CategoryBlock key={cat.key} category={cat} onNavigate={onNavigate} />
|
|
345
|
-
))}
|
|
346
|
-
</nav>
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (body.sections.length === 0) {
|
|
351
|
-
return (
|
|
352
|
-
<div className="py-10 px-4 text-center text-xs text-muted-foreground">
|
|
353
|
-
{body.emptyText}
|
|
354
|
-
</div>
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return (
|
|
359
|
-
<nav className="py-2">
|
|
360
|
-
{body.sections.map((section) => (
|
|
361
|
-
<div key={section.sourceId} className="mb-5 last:mb-2">
|
|
362
|
-
<div className="px-4 py-2 sticky top-0 z-[1] bg-muted/30 backdrop-blur-[2px] border-b border-border/30">
|
|
363
|
-
<span className="text-[11px] font-bold uppercase tracking-[0.12em] text-foreground/80">
|
|
364
|
-
{section.sourceName}
|
|
365
|
-
</span>
|
|
366
|
-
</div>
|
|
367
|
-
{section.categories.map((cat) => (
|
|
368
|
-
<CategoryBlock key={cat.key} category={cat} onNavigate={onNavigate} />
|
|
369
|
-
))}
|
|
370
|
-
</div>
|
|
371
|
-
))}
|
|
372
|
-
</nav>
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const CategoryBlock = React.memo(function CategoryBlock({
|
|
377
|
-
category,
|
|
378
|
-
onNavigate,
|
|
379
|
-
}: {
|
|
380
|
-
category: CategoryVM;
|
|
381
|
-
onNavigate: NavigateFn;
|
|
382
|
-
}) {
|
|
383
|
-
return (
|
|
384
|
-
<div className="mb-4 last:mb-2">
|
|
385
|
-
<div className="px-4 py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/50 select-none">
|
|
386
|
-
{category.category}
|
|
387
|
-
</div>
|
|
388
|
-
<div>
|
|
389
|
-
{category.rows.map((row) => (
|
|
390
|
-
<EndpointRow key={row.key} row={row} onNavigate={onNavigate} />
|
|
391
|
-
))}
|
|
392
|
-
</div>
|
|
393
|
-
</div>
|
|
394
|
-
);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
const EndpointRow = React.memo(function EndpointRow({
|
|
398
|
-
row,
|
|
399
|
-
onNavigate,
|
|
400
|
-
}: {
|
|
401
|
-
row: EndpointRowVM;
|
|
402
|
-
onNavigate: NavigateFn;
|
|
403
|
-
}) {
|
|
404
|
-
return (
|
|
405
|
-
<Tooltip delayDuration={350}>
|
|
406
|
-
<TooltipTrigger asChild>
|
|
407
|
-
<button
|
|
408
|
-
onClick={() => onNavigate(row.anchor, row.schemaId)}
|
|
409
|
-
aria-current={row.isActive ? 'location' : undefined}
|
|
410
|
-
className={cn(
|
|
411
|
-
'relative group w-full text-left flex items-start gap-2 pl-4 pr-3 py-1.5 transition-colors',
|
|
412
|
-
row.isActive
|
|
413
|
-
? 'bg-primary/10 text-foreground'
|
|
414
|
-
: 'hover:bg-muted/40 text-foreground/75 hover:text-foreground',
|
|
415
|
-
)}
|
|
416
|
-
>
|
|
417
|
-
{row.isActive && (
|
|
418
|
-
<span className="absolute left-0 top-1 bottom-1 w-0.5 rounded-r bg-primary" />
|
|
419
|
-
)}
|
|
420
|
-
<span className="shrink-0 mt-[1px]">
|
|
421
|
-
<MethodBadge method={row.method} />
|
|
422
|
-
</span>
|
|
423
|
-
<span
|
|
424
|
-
className={cn(
|
|
425
|
-
'line-clamp-2 leading-snug flex-1 min-w-0',
|
|
426
|
-
row.useMono ? 'font-mono text-[11px] break-all' : 'text-[12px]',
|
|
427
|
-
row.isActive && 'text-foreground font-medium',
|
|
428
|
-
)}
|
|
429
|
-
>
|
|
430
|
-
{row.label}
|
|
431
|
-
</span>
|
|
432
|
-
</button>
|
|
433
|
-
</TooltipTrigger>
|
|
434
|
-
<TooltipContent side="right" align="center" className="font-mono text-[11px]">
|
|
435
|
-
{row.tooltip}
|
|
436
|
-
</TooltipContent>
|
|
437
|
-
</Tooltip>
|
|
438
|
-
);
|
|
439
|
-
});
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Loader2, Send, Terminal, WifiOff } from 'lucide-react';
|
|
4
|
-
import { useMemo } from 'react';
|
|
5
|
-
|
|
6
|
-
import { CopyButton } from '@djangocfg/ui-core/components';
|
|
7
|
-
|
|
8
|
-
import JsonTree from '../../../JsonTree';
|
|
9
|
-
import { usePlaygroundContext } from '../../context/PlaygroundContext';
|
|
10
|
-
import { EmptyState, ScrollArea, StatusBadge } from './ui';
|
|
11
|
-
|
|
12
|
-
// ─── JsonTree config (static, no re-creation on render) ──────────────────────
|
|
13
|
-
|
|
14
|
-
const JSON_TREE_CONFIG = {
|
|
15
|
-
maxAutoExpandDepth: 2,
|
|
16
|
-
maxAutoExpandArrayItems: 10,
|
|
17
|
-
maxAutoExpandObjectKeys: 5,
|
|
18
|
-
maxStringLength: 200,
|
|
19
|
-
collectionLimit: 50,
|
|
20
|
-
showCollectionInfo: true,
|
|
21
|
-
showExpandControls: true,
|
|
22
|
-
showActionButtons: false,
|
|
23
|
-
preserveKeyOrder: true,
|
|
24
|
-
className: 'border-0 rounded-none',
|
|
25
|
-
} as const;
|
|
26
|
-
|
|
27
|
-
// ─── ResponsePanel ────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export function ResponsePanel() {
|
|
30
|
-
const { state } = usePlaygroundContext();
|
|
31
|
-
const { response, loading, selectedEndpoint } = state;
|
|
32
|
-
|
|
33
|
-
// ── Normalise response data ───────────────────────────────────────────────
|
|
34
|
-
// Always try to parse as JSON first so JsonTree gets an object, not a string.
|
|
35
|
-
// Falls back to raw text for non-JSON responses (HTML errors, plain text, etc.)
|
|
36
|
-
const { treeData, rawText } = useMemo(() => {
|
|
37
|
-
const d = response?.data;
|
|
38
|
-
if (d == null) return { treeData: null, rawText: '' };
|
|
39
|
-
|
|
40
|
-
if (typeof d === 'string') {
|
|
41
|
-
try {
|
|
42
|
-
return { treeData: JSON.parse(d), rawText: d };
|
|
43
|
-
} catch {
|
|
44
|
-
return { treeData: null, rawText: d };
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return { treeData: d, rawText: JSON.stringify(d, null, 2) };
|
|
49
|
-
}, [response?.data]);
|
|
50
|
-
|
|
51
|
-
// ── Derived ───────────────────────────────────────────────────────────────
|
|
52
|
-
const sizeKb = rawText ? `${(rawText.length / 1024).toFixed(1)} KB` : '';
|
|
53
|
-
const duration = response?.duration != null ? `${response.duration}ms` : '';
|
|
54
|
-
const hasError = Boolean(response?.error);
|
|
55
|
-
const hasStatus = response?.status != null;
|
|
56
|
-
const hasCopy = Boolean(rawText);
|
|
57
|
-
|
|
58
|
-
// ── Early returns ─────────────────────────────────────────────────────────
|
|
59
|
-
if (loading) {
|
|
60
|
-
return (
|
|
61
|
-
<div className="flex items-center justify-center h-full gap-2">
|
|
62
|
-
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
63
|
-
<span className="text-xs text-muted-foreground">Sending…</span>
|
|
64
|
-
</div>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (!selectedEndpoint) return <EmptyState icon={Terminal} text="Response will appear here" />;
|
|
69
|
-
if (!response) return <EmptyState icon={Send} text='Press "Send Request" to see the response' />;
|
|
70
|
-
|
|
71
|
-
// Pure network error (no HTTP response at all — CORS, offline, timeout)
|
|
72
|
-
if (hasError && !hasStatus) {
|
|
73
|
-
return (
|
|
74
|
-
<EmptyState
|
|
75
|
-
icon={WifiOff}
|
|
76
|
-
text={response.error!}
|
|
77
|
-
className="text-destructive [&_svg]:text-destructive"
|
|
78
|
-
/>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ── Render ────────────────────────────────────────────────────────────────
|
|
83
|
-
return (
|
|
84
|
-
<>
|
|
85
|
-
{/* Status bar */}
|
|
86
|
-
<div className="shrink-0 border-b px-4 py-2 flex items-center justify-between gap-3 bg-muted/20">
|
|
87
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
88
|
-
{hasStatus && <StatusBadge status={response.status!} />}
|
|
89
|
-
{response.statusText && (
|
|
90
|
-
<span className="text-xs text-muted-foreground truncate">{response.statusText}</span>
|
|
91
|
-
)}
|
|
92
|
-
{sizeKb && (
|
|
93
|
-
<span className="text-[10px] text-muted-foreground/50 tabular-nums shrink-0">{sizeKb}</span>
|
|
94
|
-
)}
|
|
95
|
-
{duration && (
|
|
96
|
-
<span className="text-[10px] text-muted-foreground/50 tabular-nums shrink-0">{duration}</span>
|
|
97
|
-
)}
|
|
98
|
-
</div>
|
|
99
|
-
{hasCopy && (
|
|
100
|
-
<CopyButton value={rawText} variant="ghost" size="sm" className="h-6 px-2 text-[10px] text-muted-foreground shrink-0">
|
|
101
|
-
Copy
|
|
102
|
-
</CopyButton>
|
|
103
|
-
)}
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
{/* HTTP-level error body (4xx/5xx — has status but also error flag) */}
|
|
107
|
-
{hasError && (
|
|
108
|
-
<div className="shrink-0 mx-4 mt-3 rounded border border-destructive/20 bg-destructive/5 px-3 py-2">
|
|
109
|
-
<p className="text-xs text-destructive">{response.error}</p>
|
|
110
|
-
</div>
|
|
111
|
-
)}
|
|
112
|
-
|
|
113
|
-
{/* Body */}
|
|
114
|
-
<ScrollArea>
|
|
115
|
-
{treeData != null ? (
|
|
116
|
-
<JsonTree title="Response Body" data={treeData} config={JSON_TREE_CONFIG} />
|
|
117
|
-
) : rawText ? (
|
|
118
|
-
<pre className="p-4 text-[11px] font-mono text-foreground/70 whitespace-pre-wrap break-all leading-relaxed">
|
|
119
|
-
{rawText}
|
|
120
|
-
</pre>
|
|
121
|
-
) : (
|
|
122
|
-
<div className="py-10 text-center text-xs text-muted-foreground">Empty response body</div>
|
|
123
|
-
)}
|
|
124
|
-
</ScrollArea>
|
|
125
|
-
</>
|
|
126
|
-
);
|
|
127
|
-
}
|