Skip to content

Commit 35289c3

Browse files
committed
Enhance JsonEditor and RequestBuilder to support JSON Schema autocompletion; add getRequestBodySchema utility for improved request handling
1 parent cecbb3b commit 35289c3

7 files changed

Lines changed: 735 additions & 344 deletions

File tree

spa/src/components/api-tester/ApiTesterModal.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,8 @@ function ApiTesterModalContent({ endpoint, spec, onClose }: ApiTesterModalProps)
222222
examples={tester.examples}
223223
selectedExampleKey={tester.selectedExampleKey}
224224
onExampleSelect={tester.selectExample}
225+
bodySchema={tester.bodySchema}
226+
spec={spec}
225227
/>
226228
</div>
227229

spa/src/components/api-tester/JsonEditor.tsx

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
import { useMemo } from 'react';
1+
import { useMemo, useRef, useCallback } from 'react';
22
import CodeMirror from '@uiw/react-codemirror';
33
import { json } from '@codemirror/lang-json';
44
import { keymap, EditorView } from '@codemirror/view';
55
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
6+
import { autocompletion } from '@codemirror/autocomplete';
7+
import type { CompletionContext } from '@codemirror/autocomplete';
68
import { tags } from '@lezer/highlight';
79
import { useTheme } from '../../hooks/useTheme';
10+
import type { SchemaObject, OpenApiSpec } from '../../openapi';
11+
import { jsonSchemaComplete } from '../../utils/jsonSchemaCompletion';
812

913
interface JsonEditorProps {
1014
value: string;
1115
onChange: (value: string) => void;
16+
schema?: SchemaObject | null;
17+
spec?: OpenApiSpec | null;
1218
}
1319

1420
const ctrlEnterPassthrough = keymap.of([
@@ -41,6 +47,32 @@ const lightTheme = EditorView.theme({
4147
borderRight: '1px solid rgb(226 232 240)', // slate-200
4248
color: 'rgb(148 163 184)', // slate-400
4349
},
50+
'.cm-tooltip-autocomplete': {
51+
backgroundColor: 'rgb(255 255 255)',
52+
border: '1px solid rgb(226 232 240)',
53+
borderRadius: '6px',
54+
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
55+
fontSize: '11px',
56+
},
57+
'.cm-tooltip-autocomplete ul li': {
58+
padding: '2px 8px',
59+
},
60+
'.cm-tooltip-autocomplete ul li[aria-selected]': {
61+
backgroundColor: 'rgb(224 231 255)', // primary-100
62+
color: 'rgb(67 56 202)', // primary-700
63+
},
64+
'.cm-completionDetail': {
65+
color: 'rgb(148 163 184)', // slate-400
66+
fontStyle: 'normal',
67+
marginLeft: '8px',
68+
},
69+
'.cm-completionInfo': {
70+
fontSize: '11px',
71+
padding: '4px 8px',
72+
backgroundColor: 'rgb(255 255 255)',
73+
border: '1px solid rgb(226 232 240)',
74+
borderRadius: '6px',
75+
},
4476
}, { dark: false });
4577

4678
const lightHighlight = HighlightStyle.define([
@@ -75,6 +107,32 @@ const darkTheme = EditorView.theme({
75107
borderRight: '1px solid rgb(51 65 85 / 0.5)', // slate-700/50
76108
color: 'rgb(71 85 105)', // slate-600
77109
},
110+
'.cm-tooltip-autocomplete': {
111+
backgroundColor: 'rgb(30 41 59)', // slate-800
112+
border: '1px solid rgb(51 65 85)', // slate-700
113+
borderRadius: '6px',
114+
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.3)',
115+
fontSize: '11px',
116+
},
117+
'.cm-tooltip-autocomplete ul li': {
118+
padding: '2px 8px',
119+
},
120+
'.cm-tooltip-autocomplete ul li[aria-selected]': {
121+
backgroundColor: 'rgb(55 48 163 / 0.4)', // primary-800/40
122+
color: 'rgb(165 180 252)', // primary-300
123+
},
124+
'.cm-completionDetail': {
125+
color: 'rgb(100 116 139)', // slate-500
126+
fontStyle: 'normal',
127+
marginLeft: '8px',
128+
},
129+
'.cm-completionInfo': {
130+
fontSize: '11px',
131+
padding: '4px 8px',
132+
backgroundColor: 'rgb(30 41 59)', // slate-800
133+
border: '1px solid rgb(51 65 85)', // slate-700
134+
borderRadius: '6px',
135+
},
78136
}, { dark: true });
79137

80138
const darkHighlight = HighlightStyle.define([
@@ -86,15 +144,25 @@ const darkHighlight = HighlightStyle.define([
86144
{ tag: tags.punctuation, color: 'rgb(148 163 184)' }, // slate-400
87145
]);
88146

89-
export function JsonEditor({ value, onChange }: JsonEditorProps) {
147+
export function JsonEditor({ value, onChange, schema, spec }: JsonEditorProps) {
90148
const { isDark } = useTheme();
91149

150+
const schemaRef = useRef(schema);
151+
const specRef = useRef(spec);
152+
schemaRef.current = schema;
153+
specRef.current = spec;
154+
155+
const completionSource = useCallback((ctx: CompletionContext) => {
156+
return jsonSchemaComplete(ctx, schemaRef.current, specRef.current);
157+
}, []);
158+
92159
const extensions = useMemo(() => [
93160
json(),
94161
ctrlEnterPassthrough,
95162
isDark ? darkTheme : lightTheme,
96163
syntaxHighlighting(isDark ? darkHighlight : lightHighlight),
97-
], [isDark]);
164+
autocompletion({ override: [completionSource], activateOnTyping: true }),
165+
], [isDark, completionSource]);
98166

99167
return (
100168
<div className="h-full [&_.cm-editor]:!h-full [&_.cm-editor]:!outline-none [&_.cm-editor]:!text-xs [&_.cm-scroller]:!font-mono">
@@ -108,6 +176,8 @@ export function JsonEditor({ value, onChange }: JsonEditorProps) {
108176
lineNumbers: false,
109177
foldGutter: false,
110178
highlightActiveLine: false,
179+
completionKeymap: false,
180+
autocompletion: false,
111181
}}
112182
/>
113183
</div>

spa/src/components/api-tester/RequestBuilder.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useRef, useEffect } from 'react';
22
import type { KeyValuePair } from './useApiTester';
33
import type { NamedExample } from '../../utils/schema';
4+
import type { SchemaObject, OpenApiSpec } from '../../openapi';
45
import { KeyValueEditor } from './KeyValueEditor';
56
import { JsonEditor } from './JsonEditor';
67
import { ExampleSelector } from './ExampleSelector';
@@ -36,6 +37,8 @@ interface RequestBuilderProps {
3637
examples: NamedExample[];
3738
selectedExampleKey: string | null;
3839
onExampleSelect: (example: NamedExample) => void;
40+
bodySchema: SchemaObject | null;
41+
spec: OpenApiSpec;
3942
}
4043

4144
type Tab = 'body' | 'params' | 'headers';
@@ -59,6 +62,8 @@ export function RequestBuilder({
5962
examples,
6063
selectedExampleKey,
6164
onExampleSelect,
65+
bodySchema,
66+
spec,
6267
}: RequestBuilderProps) {
6368
const showBody = METHODS_WITH_BODY.has(method);
6469
const [activeTab, setActiveTab] = useState<Tab>(showBody ? 'body' : 'params');
@@ -162,7 +167,7 @@ export function RequestBuilder({
162167
</div>
163168
)}
164169
<div className="flex-1 min-h-0">
165-
<JsonEditor value={body} onChange={onBodyChange} />
170+
<JsonEditor value={body} onChange={onBodyChange} schema={bodySchema} spec={spec} />
166171
</div>
167172
</div>
168173
)}

spa/src/components/api-tester/useApiTester.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useState, useCallback, useMemo, useEffect } from 'react';
22
import type { Endpoint, OpenApiSpec } from '../../openapi';
3-
import { generateExampleJson, resolveSchema, compilePathPatterns, matchUrlToPath, getRequestBodyExamples } from '../../utils/schema';
3+
import { generateExampleJson, resolveSchema, compilePathPatterns, matchUrlToPath, getRequestBodyExamples, getRequestBodySchema } from '../../utils/schema';
44
import type { NamedExample } from '../../utils/schema';
5+
import type { SchemaObject } from '../../openapi';
56

67
const METHODS_WITH_BODY = new Set(['POST', 'PUT', 'PATCH']);
78

@@ -194,6 +195,7 @@ export interface UseApiTesterReturn {
194195
examples: NamedExample[];
195196
selectedExampleKey: string | null;
196197
selectExample: (example: NamedExample) => void;
198+
bodySchema: SchemaObject | null;
197199
}
198200

199201
export function useApiTester(endpoint: Endpoint, spec: OpenApiSpec): UseApiTesterReturn {
@@ -344,6 +346,12 @@ export function useApiTester(endpoint: Endpoint, spec: OpenApiSpec): UseApiTeste
344346
return getRequestBodyExamples(spec, matchedPath, request.method);
345347
}, [request.url, request.method, baseUrl, compiledPatterns, spec]);
346348

349+
const bodySchema = useMemo(() => {
350+
const matchedPath = matchUrlToPath(request.url, baseUrl, compiledPatterns);
351+
if (!matchedPath) return null;
352+
return getRequestBodySchema(spec, matchedPath, request.method);
353+
}, [request.url, request.method, baseUrl, compiledPatterns, spec]);
354+
347355
// Clear selection when examples change and the selected key is no longer available
348356
useEffect(() => {
349357
if (selectedExampleKey && !examples.some(e => e.key === selectedExampleKey)) {
@@ -386,5 +394,6 @@ export function useApiTester(endpoint: Endpoint, spec: OpenApiSpec): UseApiTeste
386394
examples,
387395
selectedExampleKey,
388396
selectExample,
397+
bodySchema,
389398
};
390399
}

0 commit comments

Comments
 (0)