@djangocfg/ui-tools 2.1.284 → 2.1.286
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/DocsLayout-BCVU6TTX.cjs +2027 -0
- package/dist/DocsLayout-BCVU6TTX.cjs.map +1 -0
- package/dist/DocsLayout-ERETJLLV.mjs +2020 -0
- package/dist/DocsLayout-ERETJLLV.mjs.map +1 -0
- package/dist/{PlaygroundLayout-O52C6HK5.css → DocsLayout-MBFIB4NO.css} +1 -1
- package/dist/{PrettyCode.client-SGDGQTYT.cjs → PrettyCode.client-5GABIN2I.cjs} +57 -35
- package/dist/PrettyCode.client-5GABIN2I.cjs.map +1 -0
- package/dist/{PrettyCode.client-DW5LTG47.mjs → PrettyCode.client-IZTXXYHG.mjs} +57 -35
- package/dist/PrettyCode.client-IZTXXYHG.mjs.map +1 -0
- package/dist/chunk-IULI4XII.cjs +1129 -0
- package/dist/chunk-IULI4XII.cjs.map +1 -0
- package/dist/chunk-VZGQC3NG.mjs +1100 -0
- package/dist/chunk-VZGQC3NG.mjs.map +1 -0
- package/dist/index.cjs +88 -552
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.mjs +25 -496
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/tools/OpenapiViewer/.claude/.sidecar/activity.jsonl +4 -0
- package/src/tools/OpenapiViewer/.claude/.sidecar/map_cache.json +30 -0
- package/src/tools/OpenapiViewer/.claude/.sidecar/usage.json +5 -0
- package/src/tools/OpenapiViewer/.claude/project-map.md +23 -0
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +28 -2
- package/src/tools/OpenapiViewer/README.md +104 -51
- package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +64 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +137 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +268 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +139 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +211 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +101 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +57 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/anchor.ts +11 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +71 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/index.tsx +166 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +121 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/sidebarLabel.ts +60 -0
- package/src/tools/OpenapiViewer/components/index.ts +5 -2
- package/src/tools/OpenapiViewer/components/shared/BodyFormEditor.tsx +422 -0
- package/src/tools/OpenapiViewer/components/shared/EndpointDraftSync.tsx +108 -0
- package/src/tools/OpenapiViewer/components/shared/EndpointResetButton.tsx +50 -0
- package/src/tools/OpenapiViewer/components/{PlaygroundLayout → shared}/RequestPanel.tsx +174 -87
- package/src/tools/OpenapiViewer/components/shared/SendButton.tsx +91 -0
- package/src/tools/OpenapiViewer/components/{PlaygroundLayout → shared}/ui.tsx +5 -4
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +82 -8
- package/src/tools/OpenapiViewer/hooks/useEndpointDraft.ts +142 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +126 -13
- package/src/tools/OpenapiViewer/index.tsx +3 -7
- package/src/tools/OpenapiViewer/lazy.tsx +6 -27
- package/src/tools/OpenapiViewer/types.ts +44 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +2 -23
- package/src/tools/OpenapiViewer/utils/index.ts +3 -1
- package/src/tools/OpenapiViewer/utils/schemaExport.ts +206 -0
- package/src/tools/OpenapiViewer/utils/url.ts +202 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +42 -8
- package/src/tools/PrettyCode/index.tsx +6 -0
- package/dist/PlaygroundLayout-DHUATCHB.cjs +0 -798
- package/dist/PlaygroundLayout-DHUATCHB.cjs.map +0 -1
- package/dist/PlaygroundLayout-NONWOVQR.mjs +0 -791
- package/dist/PlaygroundLayout-NONWOVQR.mjs.map +0 -1
- package/dist/PrettyCode.client-DW5LTG47.mjs.map +0 -1
- package/dist/PrettyCode.client-SGDGQTYT.cjs.map +0 -1
- package/dist/chunk-5FKE7OME.cjs +0 -369
- package/dist/chunk-5FKE7OME.cjs.map +0 -1
- package/dist/chunk-BKWDHJKF.mjs +0 -356
- package/dist/chunk-BKWDHJKF.mjs.map +0 -1
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/EndpointList.tsx +0 -228
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/index.tsx +0 -107
- /package/dist/{PlaygroundLayout-O52C6HK5.css.map → DocsLayout-MBFIB4NO.css.map} +0 -0
- /package/src/tools/OpenapiViewer/components/{PlaygroundLayout → shared}/ResponsePanel.tsx +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { ApiEndpoint } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Per-endpoint draft kept in localStorage so user input survives across
|
|
9
|
+
* reloads and across switching back-and-forth between endpoints.
|
|
10
|
+
*
|
|
11
|
+
* Scoped by ``schemaId`` + ``method`` + ``path`` — two endpoints on the
|
|
12
|
+
* same path with different methods don't share drafts. Two different
|
|
13
|
+
* APIs don't collide either.
|
|
14
|
+
*
|
|
15
|
+
* Storage shape:
|
|
16
|
+
* { parameters: { [name]: string }, requestBody: string }
|
|
17
|
+
*
|
|
18
|
+
* Implementation note: we deliberately DO NOT build on top of
|
|
19
|
+
* ``useLocalStorage`` here. That hook's ``setValue`` callback reference
|
|
20
|
+
* changes whenever its internal state changes, and when EndpointDraftSync
|
|
21
|
+
* mirrors context → draft, the chain
|
|
22
|
+
* state change ⇒ persist call ⇒ hook state change ⇒ new callback ⇒
|
|
23
|
+
* effect rerun ⇒ persist call …
|
|
24
|
+
* blew up into a maximum-update-depth loop. Writing straight to
|
|
25
|
+
* ``localStorage`` with a stable ref-based writer breaks the cycle.
|
|
26
|
+
*/
|
|
27
|
+
export interface EndpointDraft {
|
|
28
|
+
parameters: Record<string, string>;
|
|
29
|
+
requestBody: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const EMPTY_DRAFT: EndpointDraft = { parameters: {}, requestBody: '' };
|
|
33
|
+
|
|
34
|
+
function storageKey(schemaId: string | null, ep: ApiEndpoint | null): string | null {
|
|
35
|
+
if (!schemaId || !ep) return null;
|
|
36
|
+
return `openapi-playground:draft:${schemaId}:${ep.method}:${ep.path}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readDraft(key: string | null): EndpointDraft {
|
|
40
|
+
if (!key || typeof window === 'undefined') return EMPTY_DRAFT;
|
|
41
|
+
try {
|
|
42
|
+
const raw = window.localStorage.getItem(key);
|
|
43
|
+
if (!raw) return EMPTY_DRAFT;
|
|
44
|
+
const parsed = JSON.parse(raw) as Partial<EndpointDraft>;
|
|
45
|
+
return {
|
|
46
|
+
parameters: parsed?.parameters ?? {},
|
|
47
|
+
requestBody: typeof parsed?.requestBody === 'string' ? parsed.requestBody : '',
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
return EMPTY_DRAFT;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeDraft(key: string | null, value: EndpointDraft): void {
|
|
55
|
+
if (!key || typeof window === 'undefined') return;
|
|
56
|
+
try {
|
|
57
|
+
// Skip writes for empty drafts — reduces storage noise and keeps
|
|
58
|
+
// "never edited" endpoints out of storage entirely.
|
|
59
|
+
if (Object.keys(value.parameters).length === 0 && !value.requestBody) {
|
|
60
|
+
window.localStorage.removeItem(key);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
64
|
+
} catch {
|
|
65
|
+
// Quota / private mode — silently drop; UI state still works.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface UseEndpointDraftResult {
|
|
70
|
+
/** Draft snapshot loaded on mount / endpoint change. Does not update
|
|
71
|
+
* as the user types — the caller owns the "live" state (context). */
|
|
72
|
+
draft: EndpointDraft;
|
|
73
|
+
/** Persist the current parameters. Safe to call on every change;
|
|
74
|
+
* writes skip when the endpoint has no key yet. */
|
|
75
|
+
setParameters: (params: Record<string, string>) => void;
|
|
76
|
+
setRequestBody: (body: string) => void;
|
|
77
|
+
/** Wipe the persisted draft for the current endpoint. */
|
|
78
|
+
reset: () => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function useEndpointDraft(
|
|
82
|
+
schemaId: string | null,
|
|
83
|
+
endpoint: ApiEndpoint | null,
|
|
84
|
+
): UseEndpointDraftResult {
|
|
85
|
+
const key = storageKey(schemaId, endpoint);
|
|
86
|
+
|
|
87
|
+
// ``draft`` is reloaded from storage only when the key changes —
|
|
88
|
+
// not when we write. Writes don't need to come back to us because
|
|
89
|
+
// the caller keeps the live state and re-reads on key change.
|
|
90
|
+
const [draft, setDraftSnapshot] = useState<EndpointDraft>(() => readDraft(key));
|
|
91
|
+
|
|
92
|
+
// Track the last loaded key so we reload exactly once per endpoint.
|
|
93
|
+
const loadedKeyRef = useRef<string | null>(key);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (loadedKeyRef.current === key) return;
|
|
96
|
+
loadedKeyRef.current = key;
|
|
97
|
+
setDraftSnapshot(readDraft(key));
|
|
98
|
+
}, [key]);
|
|
99
|
+
|
|
100
|
+
// Keep a ref of the current key so the writers below stay stable
|
|
101
|
+
// across renders — React callback identity was the root cause of
|
|
102
|
+
// the infinite render loop we had before.
|
|
103
|
+
const keyRef = useRef(key);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
keyRef.current = key;
|
|
106
|
+
}, [key]);
|
|
107
|
+
|
|
108
|
+
// Same for the latest full draft value — both writers need to merge
|
|
109
|
+
// their field into the last known shape before writing.
|
|
110
|
+
const latestRef = useRef<EndpointDraft>(draft);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
latestRef.current = draft;
|
|
113
|
+
}, [draft]);
|
|
114
|
+
|
|
115
|
+
const setParameters = useCallback((params: Record<string, string>) => {
|
|
116
|
+
const next: EndpointDraft = {
|
|
117
|
+
parameters: params,
|
|
118
|
+
requestBody: latestRef.current.requestBody,
|
|
119
|
+
};
|
|
120
|
+
latestRef.current = next;
|
|
121
|
+
writeDraft(keyRef.current, next);
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
const setRequestBody = useCallback((body: string) => {
|
|
125
|
+
const next: EndpointDraft = {
|
|
126
|
+
parameters: latestRef.current.parameters,
|
|
127
|
+
requestBody: body,
|
|
128
|
+
};
|
|
129
|
+
latestRef.current = next;
|
|
130
|
+
writeDraft(keyRef.current, next);
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
const reset = useCallback(() => {
|
|
134
|
+
latestRef.current = EMPTY_DRAFT;
|
|
135
|
+
if (keyRef.current && typeof window !== 'undefined') {
|
|
136
|
+
try { window.localStorage.removeItem(keyRef.current); } catch { /* noop */ }
|
|
137
|
+
}
|
|
138
|
+
setDraftSnapshot(EMPTY_DRAFT);
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
return { draft, setParameters, setRequestBody, reset };
|
|
142
|
+
}
|
|
@@ -3,27 +3,90 @@
|
|
|
3
3
|
import consola from 'consola';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
5
|
|
|
6
|
-
import { ApiEndpoint, OpenApiSchema, SchemaSource, UseOpenApiSchemaReturn } from '../types';
|
|
6
|
+
import { ApiEndpoint, OpenApiInfo, OpenApiSchema, SchemaSource, UseOpenApiSchemaReturn } from '../types';
|
|
7
|
+
import { dereferenceSchema } from '../utils/schemaExport';
|
|
8
|
+
import { joinUrl, resolveBaseUrl } from '../utils/url';
|
|
9
|
+
|
|
10
|
+
// ─── JSON Schema → example value ──────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
type JsonSchemaNode = Record<string, unknown> & {
|
|
13
|
+
type?: string;
|
|
14
|
+
properties?: Record<string, JsonSchemaNode>;
|
|
15
|
+
required?: string[];
|
|
16
|
+
items?: JsonSchemaNode;
|
|
17
|
+
enum?: unknown[];
|
|
18
|
+
example?: unknown;
|
|
19
|
+
default?: unknown;
|
|
20
|
+
format?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Walk a JSON Schema and build a realistic-looking example value. */
|
|
24
|
+
function exampleFromSchema(schema: JsonSchemaNode | undefined, depth = 0): unknown {
|
|
25
|
+
if (!schema || depth > 8) return null;
|
|
26
|
+
|
|
27
|
+
// Respect schema-provided examples first — no need to invent a value
|
|
28
|
+
// when the spec author already did the work.
|
|
29
|
+
if (schema.example !== undefined) return schema.example;
|
|
30
|
+
if (schema.default !== undefined) return schema.default;
|
|
31
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
|
|
32
|
+
|
|
33
|
+
switch (schema.type) {
|
|
34
|
+
case 'object': {
|
|
35
|
+
const out: Record<string, unknown> = {};
|
|
36
|
+
const props = schema.properties ?? {};
|
|
37
|
+
for (const [k, v] of Object.entries(props)) {
|
|
38
|
+
out[k] = exampleFromSchema(v, depth + 1);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
case 'array':
|
|
43
|
+
return [exampleFromSchema(schema.items, depth + 1)];
|
|
44
|
+
case 'integer':
|
|
45
|
+
case 'number':
|
|
46
|
+
return 0;
|
|
47
|
+
case 'boolean':
|
|
48
|
+
return false;
|
|
49
|
+
case 'string':
|
|
50
|
+
if (schema.format === 'date-time') return new Date().toISOString();
|
|
51
|
+
if (schema.format === 'date') return new Date().toISOString().slice(0, 10);
|
|
52
|
+
if (schema.format === 'email') return 'user@example.com';
|
|
53
|
+
if (schema.format === 'uri' || schema.format === 'url') return 'https://example.com';
|
|
54
|
+
if (schema.format === 'uuid') return '00000000-0000-0000-0000-000000000000';
|
|
55
|
+
return '';
|
|
56
|
+
default:
|
|
57
|
+
// No type (or composed schema like allOf/oneOf we don't unpack) —
|
|
58
|
+
// fall back to an empty object rather than ``null`` so the resulting
|
|
59
|
+
// JSON is still valid-looking.
|
|
60
|
+
if (schema.properties) {
|
|
61
|
+
const out: Record<string, unknown> = {};
|
|
62
|
+
for (const [k, v] of Object.entries(schema.properties)) {
|
|
63
|
+
out[k] = exampleFromSchema(v, depth + 1);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
7
70
|
|
|
8
71
|
// HTTP methods to extract from OpenAPI schema
|
|
9
72
|
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const;
|
|
10
73
|
|
|
11
|
-
// Extract endpoints from OpenAPI schema (all methods)
|
|
12
|
-
|
|
74
|
+
// Extract endpoints from OpenAPI schema (all methods). ``baseUrl`` is
|
|
75
|
+
// resolved by the caller via ``resolveBaseUrl`` — we just paste it onto
|
|
76
|
+
// the front of each path here.
|
|
77
|
+
const extractEndpoints = (schema: OpenApiSchema, baseUrl: string): ApiEndpoint[] => {
|
|
13
78
|
const endpoints: ApiEndpoint[] = [];
|
|
14
79
|
|
|
15
80
|
if (!schema.paths) return [];
|
|
16
81
|
|
|
17
|
-
// Get base URL from servers
|
|
18
|
-
const baseUrl = schema.servers && schema.servers.length > 0 ? schema.servers[0].url : '';
|
|
19
|
-
|
|
20
82
|
for (const [path, methods] of Object.entries(schema.paths)) {
|
|
21
83
|
for (const method of HTTP_METHODS) {
|
|
22
84
|
const op = (methods as any)[method];
|
|
23
85
|
if (!op) continue;
|
|
24
86
|
|
|
25
87
|
const methodUpper = method.toUpperCase();
|
|
26
|
-
const
|
|
88
|
+
const summary = (op.summary || '').trim();
|
|
89
|
+
const description = op.description || summary || `${methodUpper} ${path}`;
|
|
27
90
|
const category = op.tags?.[0] || 'Other';
|
|
28
91
|
|
|
29
92
|
const parameters: Array<{
|
|
@@ -59,21 +122,29 @@ const extractEndpoints = (schema: OpenApiSchema): ApiEndpoint[] => {
|
|
|
59
122
|
}
|
|
60
123
|
}
|
|
61
124
|
|
|
62
|
-
// Extract request body info
|
|
63
|
-
|
|
125
|
+
// Extract request body info — keep the dereferenced schema so
|
|
126
|
+
// downstream UI can render a fields table and generate a starter
|
|
127
|
+
// example instead of showing an opaque ``object`` / ``array`` tag.
|
|
128
|
+
let requestBody: ApiEndpoint['requestBody'];
|
|
64
129
|
if (op.requestBody) {
|
|
65
130
|
const content = op.requestBody.content;
|
|
66
131
|
const mediaType = content?.['application/json'] || content?.[Object.keys(content || {})[0]];
|
|
132
|
+
const rawSchema = mediaType?.schema as JsonSchemaNode | undefined;
|
|
67
133
|
requestBody = {
|
|
68
|
-
type:
|
|
134
|
+
type: rawSchema?.type || 'object',
|
|
69
135
|
description: op.requestBody.description,
|
|
136
|
+
schema: rawSchema,
|
|
137
|
+
example: rawSchema
|
|
138
|
+
? JSON.stringify(exampleFromSchema(rawSchema), null, 2)
|
|
139
|
+
: undefined,
|
|
70
140
|
};
|
|
71
141
|
}
|
|
72
142
|
|
|
73
143
|
const endpoint: ApiEndpoint = {
|
|
74
144
|
name: path.split('/').pop() || path,
|
|
75
145
|
method: methodUpper,
|
|
76
|
-
path: baseUrl
|
|
146
|
+
path: baseUrl ? joinUrl(baseUrl, path) : path,
|
|
147
|
+
summary,
|
|
77
148
|
description,
|
|
78
149
|
category,
|
|
79
150
|
parameters: parameters.length > 0 ? parameters : undefined,
|
|
@@ -111,11 +182,15 @@ const fetchSchema = async (url: string): Promise<OpenApiSchema> => {
|
|
|
111
182
|
interface UseOpenApiSchemaProps {
|
|
112
183
|
schemas: SchemaSource[];
|
|
113
184
|
defaultSchemaId?: string;
|
|
185
|
+
/** Global base URL override from ``PlaygroundConfig.baseUrl``.
|
|
186
|
+
* Per-schema ``SchemaSource.baseUrl`` takes precedence over this. */
|
|
187
|
+
baseUrl?: string;
|
|
114
188
|
}
|
|
115
189
|
|
|
116
190
|
export default function useOpenApiSchema({
|
|
117
191
|
schemas,
|
|
118
192
|
defaultSchemaId,
|
|
193
|
+
baseUrl: configBaseUrl,
|
|
119
194
|
}: UseOpenApiSchemaProps): UseOpenApiSchemaReturn {
|
|
120
195
|
const [loading, setLoading] = useState(true);
|
|
121
196
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -136,13 +211,45 @@ export default function useOpenApiSchema({
|
|
|
136
211
|
[loadedSchemas, currentSchemaId]
|
|
137
212
|
);
|
|
138
213
|
|
|
214
|
+
// Dereference once per schema load so endpoint extraction sees fully
|
|
215
|
+
// resolved ``$ref`` graphs. The raw schema is still exposed via
|
|
216
|
+
// ``rawSchema`` for Copy-for-AI (juniors may want to hand the raw
|
|
217
|
+
// document to an LLM and have it resolve refs itself).
|
|
218
|
+
const dereferencedSchema = useMemo(
|
|
219
|
+
() => (currentOpenApiSchema ? dereferenceSchema(currentOpenApiSchema) : null),
|
|
220
|
+
[currentOpenApiSchema],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Resolve base URL with priority chain. Centralised in ``resolveBaseUrl``
|
|
224
|
+
// so the same logic is reused by the schema-export utilities.
|
|
225
|
+
const resolvedBaseUrl = useMemo(
|
|
226
|
+
() =>
|
|
227
|
+
resolveBaseUrl({
|
|
228
|
+
schemaSource: currentSchema?.baseUrl,
|
|
229
|
+
config: configBaseUrl,
|
|
230
|
+
fromServers: currentOpenApiSchema?.servers?.[0]?.url,
|
|
231
|
+
}),
|
|
232
|
+
[currentSchema?.baseUrl, configBaseUrl, currentOpenApiSchema],
|
|
233
|
+
);
|
|
234
|
+
|
|
139
235
|
const endpoints = useMemo(
|
|
140
|
-
() => (
|
|
141
|
-
[
|
|
236
|
+
() => (dereferencedSchema ? extractEndpoints(dereferencedSchema, resolvedBaseUrl) : []),
|
|
237
|
+
[dereferencedSchema, resolvedBaseUrl]
|
|
142
238
|
);
|
|
143
239
|
|
|
144
240
|
const categories = useMemo(() => getCategories(endpoints), [endpoints]);
|
|
145
241
|
|
|
242
|
+
const schemaInfo = useMemo<OpenApiInfo | null>(() => {
|
|
243
|
+
if (!currentOpenApiSchema?.info) return null;
|
|
244
|
+
const { title, version, description } = currentOpenApiSchema.info;
|
|
245
|
+
return {
|
|
246
|
+
title,
|
|
247
|
+
version,
|
|
248
|
+
description,
|
|
249
|
+
servers: currentOpenApiSchema.servers,
|
|
250
|
+
};
|
|
251
|
+
}, [currentOpenApiSchema]);
|
|
252
|
+
|
|
146
253
|
// Load schema when current schema changes
|
|
147
254
|
useEffect(() => {
|
|
148
255
|
if (!currentSchema) return;
|
|
@@ -206,6 +313,12 @@ export default function useOpenApiSchema({
|
|
|
206
313
|
categories,
|
|
207
314
|
schemas,
|
|
208
315
|
currentSchema,
|
|
316
|
+
schemaInfo,
|
|
317
|
+
rawSchema: currentOpenApiSchema ?? null,
|
|
318
|
+
// Consumers expect ``undefined`` when no base URL was resolved (for
|
|
319
|
+
// conditional ``{ baseUrl?: … }`` plumbing). Turn the empty-string
|
|
320
|
+
// convention from the resolver into undefined at the API boundary.
|
|
321
|
+
resolvedBaseUrl: resolvedBaseUrl || undefined,
|
|
209
322
|
setCurrentSchema,
|
|
210
323
|
refresh,
|
|
211
324
|
};
|
|
@@ -4,12 +4,10 @@ import React, { lazy, Suspense } from 'react';
|
|
|
4
4
|
import { PlaygroundProvider } from './context/PlaygroundContext';
|
|
5
5
|
import type { PlaygroundConfig } from './types';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import('./components/PlaygroundLayout').then((mod) => ({ default: mod.PlaygroundLayout }))
|
|
7
|
+
const DocsLayout = lazy(() =>
|
|
8
|
+
import('./components/DocsLayout').then((mod) => ({ default: mod.DocsLayout }))
|
|
10
9
|
);
|
|
11
10
|
|
|
12
|
-
// Loading fallback component
|
|
13
11
|
const LoadingFallback = () => (
|
|
14
12
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
15
13
|
<div className="text-muted-foreground">Loading API Playground...</div>
|
|
@@ -24,14 +22,12 @@ export const Playground: React.FC<PlaygroundProps> = ({ config }) => {
|
|
|
24
22
|
return (
|
|
25
23
|
<PlaygroundProvider config={config}>
|
|
26
24
|
<Suspense fallback={<LoadingFallback />}>
|
|
27
|
-
<
|
|
25
|
+
<DocsLayout />
|
|
28
26
|
</Suspense>
|
|
29
27
|
</PlaygroundProvider>
|
|
30
28
|
);
|
|
31
29
|
};
|
|
32
30
|
|
|
33
|
-
// Re-export types for convenience
|
|
34
31
|
export type { PlaygroundConfig, SchemaSource } from './types';
|
|
35
32
|
|
|
36
|
-
// Default export for dynamic import
|
|
37
33
|
export default Playground;
|
|
@@ -7,28 +7,20 @@
|
|
|
7
7
|
* Use this for automatic code-splitting with Suspense fallback.
|
|
8
8
|
*
|
|
9
9
|
* For direct imports without lazy loading, use:
|
|
10
|
-
*
|
|
10
|
+
* import OpenapiViewer from '@djangocfg/ui-tools/openapi'
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import * as React from 'react';
|
|
14
|
-
import { createLazyComponent
|
|
14
|
+
import { createLazyComponent } from '../../components';
|
|
15
15
|
import { PlaygroundProvider } from './context/PlaygroundContext';
|
|
16
16
|
import type { ApiKey, PlaygroundConfig, SchemaSource } from './types';
|
|
17
17
|
|
|
18
|
-
// ============================================================================
|
|
19
|
-
// Types
|
|
20
|
-
// ============================================================================
|
|
21
|
-
|
|
22
18
|
export interface PlaygroundProps {
|
|
23
19
|
config: PlaygroundConfig;
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
export type { ApiKey, PlaygroundConfig, SchemaSource };
|
|
27
23
|
|
|
28
|
-
// ============================================================================
|
|
29
|
-
// OpenAPI Loading Fallback
|
|
30
|
-
// ============================================================================
|
|
31
|
-
|
|
32
24
|
function OpenapiLoadingFallback() {
|
|
33
25
|
return (
|
|
34
26
|
<div className="flex items-center justify-center min-h-[400px] bg-muted/30 rounded-lg">
|
|
@@ -40,31 +32,18 @@ function OpenapiLoadingFallback() {
|
|
|
40
32
|
);
|
|
41
33
|
}
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// ============================================================================
|
|
46
|
-
|
|
47
|
-
const LazyPlaygroundLayout = createLazyComponent(
|
|
48
|
-
() => import('./components/PlaygroundLayout').then((mod) => ({ default: mod.PlaygroundLayout })),
|
|
35
|
+
const LazyDocsLayout = createLazyComponent(
|
|
36
|
+
() => import('./components/DocsLayout').then((mod) => ({ default: mod.DocsLayout })),
|
|
49
37
|
{
|
|
50
|
-
displayName: '
|
|
38
|
+
displayName: 'LazyDocsLayout',
|
|
51
39
|
fallback: <OpenapiLoadingFallback />,
|
|
52
40
|
}
|
|
53
41
|
);
|
|
54
42
|
|
|
55
|
-
// ============================================================================
|
|
56
|
-
// LazyOpenapiViewer
|
|
57
|
-
// ============================================================================
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* LazyOpenapiViewer - Lazy-loaded OpenAPI schema viewer & playground
|
|
61
|
-
*
|
|
62
|
-
* Automatically shows loading state while OpenAPI components load (~400KB)
|
|
63
|
-
*/
|
|
64
43
|
export const LazyOpenapiViewer: React.FC<PlaygroundProps> = ({ config }) => {
|
|
65
44
|
return (
|
|
66
45
|
<PlaygroundProvider config={config}>
|
|
67
|
-
<
|
|
46
|
+
<LazyDocsLayout />
|
|
68
47
|
</PlaygroundProvider>
|
|
69
48
|
);
|
|
70
49
|
};
|
|
@@ -19,6 +19,9 @@ export interface ApiEndpoint {
|
|
|
19
19
|
name: string;
|
|
20
20
|
method: string;
|
|
21
21
|
path: string;
|
|
22
|
+
/** Short human label from OpenAPI ``operation.summary``. Empty when
|
|
23
|
+
* the spec provides none. Prefer this for sidebar rows and breadcrumbs. */
|
|
24
|
+
summary: string;
|
|
22
25
|
description: string;
|
|
23
26
|
category: string;
|
|
24
27
|
parameters?: Array<{
|
|
@@ -30,6 +33,12 @@ export interface ApiEndpoint {
|
|
|
30
33
|
requestBody?: {
|
|
31
34
|
type: string;
|
|
32
35
|
description?: string;
|
|
36
|
+
/** Dereferenced JSON Schema for the body. Kept so the docs layout
|
|
37
|
+
* can render a fields table and the playground can seed the body
|
|
38
|
+
* editor with a generated example. */
|
|
39
|
+
schema?: Record<string, unknown>;
|
|
40
|
+
/** Pre-generated example JSON ready to drop into a textarea. */
|
|
41
|
+
example?: string;
|
|
33
42
|
};
|
|
34
43
|
responses?: Array<{
|
|
35
44
|
code: string;
|
|
@@ -59,12 +68,23 @@ export interface SchemaSource {
|
|
|
59
68
|
id: string;
|
|
60
69
|
name: string;
|
|
61
70
|
url: string; // URL to fetch OpenAPI schema from (full URL)
|
|
71
|
+
/** Per-schema override for the request base URL. Wins over the
|
|
72
|
+
* global ``PlaygroundConfig.baseUrl`` and over ``schema.servers[0].url``
|
|
73
|
+
* from the OpenAPI document. Use when a single playground hosts
|
|
74
|
+
* several APIs that live on different domains. */
|
|
75
|
+
baseUrl?: string;
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
// Playground configuration
|
|
65
79
|
export interface PlaygroundConfig {
|
|
66
80
|
schemas: SchemaSource[]; // Array of schema URLs (full URLs from API)
|
|
67
81
|
defaultSchemaId?: string; // Default schema to select
|
|
82
|
+
/** Global override for the request base URL. Used when the OpenAPI
|
|
83
|
+
* document has no ``servers`` entry, or when docs are hosted on a
|
|
84
|
+
* different origin than the API. Resolution order (highest wins):
|
|
85
|
+
* ``SchemaSource.baseUrl`` → ``PlaygroundConfig.baseUrl`` →
|
|
86
|
+
* ``schema.servers[0].url`` → empty string (relative paths). */
|
|
87
|
+
baseUrl?: string;
|
|
68
88
|
/** Optional API keys the user can pick from in the request panel.
|
|
69
89
|
* When provided, the playground auto-selects the first one and
|
|
70
90
|
* syncs the ``X-API-Key`` header from ``ApiKey.secret``. Pass
|
|
@@ -101,6 +121,11 @@ export interface PlaygroundState {
|
|
|
101
121
|
|
|
102
122
|
// UI state
|
|
103
123
|
sidebarOpen: boolean;
|
|
124
|
+
|
|
125
|
+
/** Id of the schema the viewer is currently on. Drives per-endpoint
|
|
126
|
+
* draft scoping in localStorage and is pushed in from the layout that
|
|
127
|
+
* owns the ``useOpenApiSchema`` hook. ``null`` before first load. */
|
|
128
|
+
activeSchemaId: string | null;
|
|
104
129
|
}
|
|
105
130
|
|
|
106
131
|
export type PlaygroundStep = 'endpoints' | 'request' | 'response';
|
|
@@ -148,12 +173,23 @@ export interface PlaygroundContextType {
|
|
|
148
173
|
|
|
149
174
|
// UI management
|
|
150
175
|
setSidebarOpen: (open: boolean) => void;
|
|
176
|
+
setActiveSchemaId: (id: string | null) => void;
|
|
151
177
|
|
|
152
178
|
// Actions
|
|
153
179
|
clearAll: () => void;
|
|
154
180
|
sendRequest: () => Promise<void>;
|
|
155
181
|
}
|
|
156
182
|
|
|
183
|
+
// Subset of OpenAPI ``info`` surfaced to consumers — lets the docs
|
|
184
|
+
// layout render an intro section (title, version, description) without
|
|
185
|
+
// re-parsing the schema. ``servers`` piggy-backs here for the same reason.
|
|
186
|
+
export interface OpenApiInfo {
|
|
187
|
+
title: string;
|
|
188
|
+
version: string;
|
|
189
|
+
description?: string;
|
|
190
|
+
servers?: Array<{ url: string; description?: string }>;
|
|
191
|
+
}
|
|
192
|
+
|
|
157
193
|
// Hook return types
|
|
158
194
|
export interface UseOpenApiSchemaReturn {
|
|
159
195
|
loading: boolean;
|
|
@@ -162,6 +198,14 @@ export interface UseOpenApiSchemaReturn {
|
|
|
162
198
|
categories: string[];
|
|
163
199
|
schemas: SchemaSource[];
|
|
164
200
|
currentSchema: SchemaSource | null;
|
|
201
|
+
/** Parsed ``info`` from the active schema. ``null`` while loading. */
|
|
202
|
+
schemaInfo: OpenApiInfo | null;
|
|
203
|
+
/** Raw parsed OpenAPI document — exposed so consumers can serialise it
|
|
204
|
+
* (Copy-for-AI) without re-fetching. ``null`` while loading. */
|
|
205
|
+
rawSchema: OpenApiSchema | null;
|
|
206
|
+
/** Base URL used for endpoint paths, after applying priority chain
|
|
207
|
+
* (SchemaSource.baseUrl → config.baseUrl → schema.servers[0].url). */
|
|
208
|
+
resolvedBaseUrl: string | undefined;
|
|
165
209
|
setCurrentSchema: (schemaId: string) => void;
|
|
166
210
|
refresh: () => void;
|
|
167
211
|
}
|
|
@@ -46,26 +46,5 @@ export const parseRequestHeaders = (headersString: string): Record<string, strin
|
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
url: string,
|
|
52
|
-
parameters: Record<string, string>
|
|
53
|
-
): string => {
|
|
54
|
-
let substitutedUrl = url;
|
|
55
|
-
|
|
56
|
-
Object.entries(parameters).forEach(([key, value]) => {
|
|
57
|
-
if (value && value.trim() !== '') {
|
|
58
|
-
// Replace both {key} and %7Bkey%7D patterns (URL encoded version)
|
|
59
|
-
const patterns = [
|
|
60
|
-
new RegExp(`\\{${key}\\}`, 'g'),
|
|
61
|
-
new RegExp(`%7B${key}%7D`, 'gi'),
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
patterns.forEach((pattern) => {
|
|
65
|
-
substitutedUrl = substitutedUrl.replace(pattern, encodeURIComponent(value));
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
return substitutedUrl;
|
|
71
|
-
};
|
|
49
|
+
// URL helpers moved to ./url.ts — keep this file focused on non-URL
|
|
50
|
+
// formatters (method/status colour maps, JSON validation, header parsing).
|