@gomjellie/lazyapi 0.0.4 → 0.0.5
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/components/detail-view/DetailView.js +172 -10
- package/dist/components/detail-view/LeftPanel.d.ts +2 -1
- package/dist/components/detail-view/LeftPanel.js +39 -15
- package/dist/components/detail-view/RightPanel.d.ts +5 -6
- package/dist/components/detail-view/RightPanel.js +48 -232
- package/dist/store/appSlice.d.ts +1 -1
- package/dist/store/appSlice.js +18 -1
- package/dist/store/navigationActions.js +4 -10
- package/dist/types/app-state.d.ts +2 -2
- package/package.json +1 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* API 상세 화면
|
|
3
3
|
*/
|
|
4
4
|
import React from 'react';
|
|
5
|
-
import { Box, Text, useStdout } from 'ink';
|
|
5
|
+
import { Box, Text, useStdout, useStdin } from 'ink';
|
|
6
6
|
import { useAppSelector, useAppDispatch } from '../../store/hooks.js';
|
|
7
|
-
import { setMode, detailSetFocus, detailSetPanel, detailSetRequestTab, detailSetResponseTab, detailSetResponse, detailSetParam, detailSetBody, } from '../../store/appSlice.js';
|
|
7
|
+
import { setMode, detailSetFocus, detailSetPanel, detailSetRequestTab, detailSetResponseTab, detailSetResponse, detailSetParam, detailSetBody, detailSetHeader, } from '../../store/appSlice.js';
|
|
8
8
|
import { navigateUp, navigateDown, navigateLeft, navigateRight, } from '../../store/navigationActions.js';
|
|
9
9
|
import { useNavigation } from '../../hooks/useNavigation.js';
|
|
10
10
|
import { useKeyPress } from '../../hooks/useKeyPress.js';
|
|
@@ -15,6 +15,10 @@ import LeftPanel from './LeftPanel.js';
|
|
|
15
15
|
import RightPanel from './RightPanel.js';
|
|
16
16
|
import { getServerUrl } from '../../types/openapi.js';
|
|
17
17
|
import { setCommandInput } from '../../store/appSlice.js';
|
|
18
|
+
import { spawnSync } from 'child_process';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
import path from 'path';
|
|
18
22
|
export default function DetailView() {
|
|
19
23
|
const detail = useAppSelector((state) => state.app.detail);
|
|
20
24
|
const document = useAppSelector((state) => state.app.document);
|
|
@@ -24,11 +28,151 @@ export default function DetailView() {
|
|
|
24
28
|
const { goBack } = useNavigation();
|
|
25
29
|
const { executeRequest, loading } = useApiClient();
|
|
26
30
|
const { stdout } = useStdout();
|
|
31
|
+
const { isRawModeSupported, setRawMode } = useStdin();
|
|
27
32
|
// detail이 없는 경우를 대비한 기본값
|
|
28
33
|
const endpoint = detail?.endpoint;
|
|
29
34
|
const activePanel = detail?.activePanel || 'left';
|
|
30
35
|
const activeRequestTab = detail?.activeRequestTab || 'headers';
|
|
31
36
|
const activeResponseTab = detail?.activeResponseTab || 'body';
|
|
37
|
+
const handleOpenEditor = () => {
|
|
38
|
+
if (!detail || !endpoint)
|
|
39
|
+
return;
|
|
40
|
+
let content = '';
|
|
41
|
+
let fileName = `lazyapi-edit-${Date.now()}`;
|
|
42
|
+
let extension = '.json';
|
|
43
|
+
let isEditable = false;
|
|
44
|
+
let updateType = 'NONE';
|
|
45
|
+
// 1. Determine content and metadata based on active panel and tab
|
|
46
|
+
if (activePanel === 'request') {
|
|
47
|
+
if (activeRequestTab === 'body') {
|
|
48
|
+
content = detail.requestBody || '';
|
|
49
|
+
isEditable = true;
|
|
50
|
+
updateType = 'BODY';
|
|
51
|
+
}
|
|
52
|
+
else if (activeRequestTab === 'headers') {
|
|
53
|
+
content = JSON.stringify(detail.headers, null, 2);
|
|
54
|
+
isEditable = true; // Later could support bulk edit
|
|
55
|
+
updateType = 'HEADERS';
|
|
56
|
+
}
|
|
57
|
+
else if (activeRequestTab === 'query') {
|
|
58
|
+
content = JSON.stringify(detail.parameterValues, null, 2);
|
|
59
|
+
isEditable = true;
|
|
60
|
+
updateType = 'PARAMS';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (activePanel === 'response' && detail.response) {
|
|
64
|
+
if (activeResponseTab === 'body') {
|
|
65
|
+
content =
|
|
66
|
+
typeof detail.response.body === 'string'
|
|
67
|
+
? detail.response.body
|
|
68
|
+
: JSON.stringify(detail.response.body, null, 2);
|
|
69
|
+
}
|
|
70
|
+
else if (activeResponseTab === 'headers') {
|
|
71
|
+
content = JSON.stringify(detail.response.headers, null, 2);
|
|
72
|
+
}
|
|
73
|
+
else if (activeResponseTab === 'cookies') {
|
|
74
|
+
content = JSON.stringify(detail.response.cookies || {}, null, 2);
|
|
75
|
+
}
|
|
76
|
+
else if (activeResponseTab === 'curl') {
|
|
77
|
+
extension = '.sh';
|
|
78
|
+
// Calculate curl command
|
|
79
|
+
let pathStr = endpoint.path;
|
|
80
|
+
endpoint.parameters
|
|
81
|
+
.filter((p) => p.in === 'path')
|
|
82
|
+
.forEach((param) => {
|
|
83
|
+
const value = detail.parameterValues[param.name] || '';
|
|
84
|
+
pathStr = pathStr.replace(`{${param.name}}`, value);
|
|
85
|
+
});
|
|
86
|
+
const queryParams = endpoint.parameters
|
|
87
|
+
.filter((p) => p.in === 'query')
|
|
88
|
+
.filter((param) => detail.parameterValues[param.name])
|
|
89
|
+
.map((param) => `${param.name}=${encodeURIComponent(detail.parameterValues[param.name])}`)
|
|
90
|
+
.join('&');
|
|
91
|
+
const url = queryParams ? `${baseURL}${pathStr}?${queryParams}` : `${baseURL}${pathStr}`;
|
|
92
|
+
let headerStr = '';
|
|
93
|
+
Object.entries(detail.headers).forEach(([key, val]) => {
|
|
94
|
+
headerStr += ` -H '${key}: ${val}'`;
|
|
95
|
+
});
|
|
96
|
+
let bodyStr = '';
|
|
97
|
+
if (detail.requestBody) {
|
|
98
|
+
bodyStr = ` -d '${detail.requestBody.replace(/'/g, "'\\''")}'`;
|
|
99
|
+
}
|
|
100
|
+
const method = endpoint.method.toUpperCase();
|
|
101
|
+
content = `curl -X ${method} '${url}'${headerStr}${bodyStr}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (activePanel === 'left') {
|
|
105
|
+
// LeftPanel handles body params or requestBody
|
|
106
|
+
content = detail.requestBody || '';
|
|
107
|
+
isEditable = true;
|
|
108
|
+
updateType = 'BODY';
|
|
109
|
+
}
|
|
110
|
+
if (!content && activePanel === 'response' && activeResponseTab === 'curl') {
|
|
111
|
+
// Re-calculate curl if needed or just show placeholder
|
|
112
|
+
content = 'Curl command calculation not available in this view';
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const tempFile = path.join(os.tmpdir(), `${fileName}${extension}`);
|
|
116
|
+
fs.writeFileSync(tempFile, content);
|
|
117
|
+
const editor = process.env.EDITOR || 'vim';
|
|
118
|
+
if (isRawModeSupported) {
|
|
119
|
+
setRawMode(false);
|
|
120
|
+
}
|
|
121
|
+
process.stdin.pause();
|
|
122
|
+
try {
|
|
123
|
+
spawnSync(editor, [tempFile], {
|
|
124
|
+
stdio: 'inherit',
|
|
125
|
+
});
|
|
126
|
+
if (fs.existsSync(tempFile) && isEditable) {
|
|
127
|
+
const newContent = fs.readFileSync(tempFile, 'utf-8');
|
|
128
|
+
if (newContent !== content) {
|
|
129
|
+
if (updateType === 'BODY') {
|
|
130
|
+
dispatch(detailSetBody(newContent));
|
|
131
|
+
}
|
|
132
|
+
else if (updateType === 'HEADERS') {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(newContent);
|
|
135
|
+
Object.entries(parsed).forEach(([key, value]) => {
|
|
136
|
+
dispatch(detailSetHeader({ key, value: String(value) }));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Ignore parse errors for bulk edit for now
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (updateType === 'PARAMS') {
|
|
144
|
+
try {
|
|
145
|
+
const parsed = JSON.parse(newContent);
|
|
146
|
+
Object.entries(parsed).forEach(([key, value]) => {
|
|
147
|
+
dispatch(detailSetParam({ key, value: String(value) }));
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Ignore
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (fs.existsSync(tempFile)) {
|
|
157
|
+
fs.unlinkSync(tempFile);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (innerError) {
|
|
161
|
+
console.error('Error executing editor:', innerError);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
process.stdin.resume();
|
|
165
|
+
if (isRawModeSupported) {
|
|
166
|
+
setRawMode(true);
|
|
167
|
+
}
|
|
168
|
+
dispatch(setMode('NORMAL'));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
console.error('Failed to open editor', e);
|
|
173
|
+
dispatch(setMode('NORMAL'));
|
|
174
|
+
}
|
|
175
|
+
};
|
|
32
176
|
// 파라미터 필드 개수 계산
|
|
33
177
|
const pathParams = endpoint?.parameters.filter((p) => p.in === 'path') || [];
|
|
34
178
|
const queryParams = endpoint?.parameters.filter((p) => p.in === 'query') || [];
|
|
@@ -42,6 +186,23 @@ export default function DetailView() {
|
|
|
42
186
|
formDataParams.length +
|
|
43
187
|
(endpoint?.requestBody && bodyParams.length === 0 ? 1 : 0) +
|
|
44
188
|
(endpoint?.responses?.length || 0);
|
|
189
|
+
// Body 영역 인덱스 계산
|
|
190
|
+
const bodyStartIndex = pathParams.length + queryParams.length + headerParams.length;
|
|
191
|
+
const bodyEndIndex = bodyStartIndex + bodyParams.length;
|
|
192
|
+
// OA3 Request Body 인덱스 (Body Params가 없을 때만 존재)
|
|
193
|
+
const requestBodyIndex = endpoint?.requestBody && bodyParams.length === 0 ? bodyEndIndex + formDataParams.length : -1;
|
|
194
|
+
// 'e' 키 (Edit) 표시 여부 결정
|
|
195
|
+
const showEditKey =
|
|
196
|
+
// 1. Request or Response Panel > TABS area
|
|
197
|
+
((activePanel === 'request' || activePanel === 'response') &&
|
|
198
|
+
detail?.subFocus === 'TABS') ||
|
|
199
|
+
// 2. Left Panel > Body Parameter (in: body)
|
|
200
|
+
(activePanel === 'left' &&
|
|
201
|
+
detail &&
|
|
202
|
+
detail.focusedFieldIndex >= bodyStartIndex &&
|
|
203
|
+
detail.focusedFieldIndex < bodyEndIndex) ||
|
|
204
|
+
// 3. Left Panel > Request Body (OA3)
|
|
205
|
+
(activePanel === 'left' && detail && detail.focusedFieldIndex === requestBodyIndex);
|
|
45
206
|
const handleExecuteRequest = async () => {
|
|
46
207
|
if (!detail || !document)
|
|
47
208
|
return;
|
|
@@ -63,8 +224,8 @@ export default function DetailView() {
|
|
|
63
224
|
if (cmd === 'q' || cmd === 'quit' || cmd === 'exit') {
|
|
64
225
|
process.exit(0);
|
|
65
226
|
}
|
|
66
|
-
// 2. 접두사 있는 명령어 처리 (pp1, qp1, h1,
|
|
67
|
-
const match = cmd.match(/^(pp|qp|h|
|
|
227
|
+
// 2. 접두사 있는 명령어 처리 (pp1, qp1, h1, bt1, rs1, rq1, rt1...)
|
|
228
|
+
const match = cmd.match(/^(pp|qp|h|bt|rs|rq|rt)(\d+)$/);
|
|
68
229
|
if (match) {
|
|
69
230
|
const type = match[1];
|
|
70
231
|
const index = parseInt(match[2], 10);
|
|
@@ -100,8 +261,8 @@ export default function DetailView() {
|
|
|
100
261
|
dispatch(detailSetPanel('request'));
|
|
101
262
|
}
|
|
102
263
|
}
|
|
103
|
-
else if (type === '
|
|
104
|
-
// Request Body
|
|
264
|
+
else if (type === 'bt') {
|
|
265
|
+
// Request Body
|
|
105
266
|
const bodyStartIndex = pathParams.length + queryParams.length + headerParams.length;
|
|
106
267
|
if (bodyParams.length > 0) {
|
|
107
268
|
if (targetIndex >= 0 && targetIndex < bodyParams.length) {
|
|
@@ -117,8 +278,8 @@ export default function DetailView() {
|
|
|
117
278
|
}
|
|
118
279
|
}
|
|
119
280
|
}
|
|
120
|
-
else if (type === '
|
|
121
|
-
// Responses
|
|
281
|
+
else if (type === 'rt') {
|
|
282
|
+
// Responses
|
|
122
283
|
const responsesStartIndex = pathParams.length +
|
|
123
284
|
queryParams.length +
|
|
124
285
|
headerParams.length +
|
|
@@ -235,6 +396,7 @@ export default function DetailView() {
|
|
|
235
396
|
{ key: ':', description: 'Command' },
|
|
236
397
|
{ key: 'Tab', description: 'Cycle Panel' },
|
|
237
398
|
{ key: 'i', description: 'Insert' },
|
|
399
|
+
...(showEditKey ? [{ key: 'e', description: 'Editor' }] : []),
|
|
238
400
|
{ key: 'Enter', description: 'Execute' },
|
|
239
401
|
{ key: 'u', description: 'Back' },
|
|
240
402
|
]
|
|
@@ -260,7 +422,7 @@ export default function DetailView() {
|
|
|
260
422
|
dispatch(detailSetBody(value));
|
|
261
423
|
dispatch(detailSetPanel('request'));
|
|
262
424
|
dispatch(detailSetRequestTab('body'));
|
|
263
|
-
}, onSetMode: (m) => dispatch(setMode(m)) }),
|
|
264
|
-
React.createElement(RightPanel, { activeRequestTab: activeRequestTab, activeResponseTab: activeResponseTab, activePanel: activePanel, subFocus: detail.subFocus, response: detail.response, endpoint: endpoint,
|
|
425
|
+
}, onSetMode: (m) => dispatch(setMode(m)), onOpenEditor: handleOpenEditor }),
|
|
426
|
+
React.createElement(RightPanel, { activeRequestTab: activeRequestTab, activeResponseTab: activeResponseTab, activePanel: activePanel, subFocus: detail.subFocus, response: detail.response, endpoint: endpoint, parameterValues: detail.parameterValues, requestBody: detail.requestBody, requestHeaders: detail.headers, baseURL: baseURL, isLoading: loading, commandMode: mode === 'COMMAND', onOpenEditor: handleOpenEditor })),
|
|
265
427
|
React.createElement(KeybindingFooter, { bindings: keyBindings, mode: mode, commandInput: commandInput, onCommandChange: (value) => dispatch(setCommandInput(value)), onCommandSubmit: handleCommandSubmit })));
|
|
266
428
|
}
|
|
@@ -17,6 +17,7 @@ interface LeftPanelProps {
|
|
|
17
17
|
onExitInsertMode?: () => void;
|
|
18
18
|
onRequestBodyEdit?: (value: string) => void;
|
|
19
19
|
onSetMode?: (mode: AppMode) => void;
|
|
20
|
+
onOpenEditor?: () => void;
|
|
20
21
|
}
|
|
21
|
-
export default function LeftPanel({ endpoint, parameterValues, requestBodyValue, focusedFieldIndex, mode, isFocused, document, commandMode, onParameterChange, onExitInsertMode, onRequestBodyEdit, onSetMode, }: LeftPanelProps): React.JSX.Element;
|
|
22
|
+
export default function LeftPanel({ endpoint, parameterValues, requestBodyValue, focusedFieldIndex, mode, isFocused, document, commandMode, onParameterChange, onExitInsertMode, onRequestBodyEdit, onSetMode, onOpenEditor, }: LeftPanelProps): React.JSX.Element;
|
|
22
23
|
export {};
|
|
@@ -35,7 +35,7 @@ function getStatusColor(status) {
|
|
|
35
35
|
return 'red';
|
|
36
36
|
return 'gray';
|
|
37
37
|
}
|
|
38
|
-
export default function LeftPanel({ endpoint, parameterValues, requestBodyValue, focusedFieldIndex, mode, isFocused, document, commandMode = false, onParameterChange, onExitInsertMode, onRequestBodyEdit, onSetMode, }) {
|
|
38
|
+
export default function LeftPanel({ endpoint, parameterValues, requestBodyValue, focusedFieldIndex, mode, isFocused, document, commandMode = false, onParameterChange, onExitInsertMode, onRequestBodyEdit, onSetMode, onOpenEditor, }) {
|
|
39
39
|
const { stdout } = useStdout();
|
|
40
40
|
const [localValue, setLocalValue] = useState('');
|
|
41
41
|
const [collapsedSchemas, setCollapsedSchemas] = useState(() => {
|
|
@@ -429,12 +429,23 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
|
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
};
|
|
432
|
+
const handleOpenRequestBodyEditor = () => {
|
|
433
|
+
if (!isFocused || mode !== 'NORMAL' || !onOpenEditor)
|
|
434
|
+
return;
|
|
435
|
+
const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
|
|
436
|
+
const item = flatItems[flatIndex];
|
|
437
|
+
const isBodyParam = item?.type === 'requestBody' || (item?.type === 'param' && item.param.in === 'body');
|
|
438
|
+
if (isBodyParam) {
|
|
439
|
+
onOpenEditor();
|
|
440
|
+
}
|
|
441
|
+
};
|
|
432
442
|
useKeyPress({
|
|
433
443
|
'>': handleExpandSchema,
|
|
434
444
|
'<': handleCollapseSchema,
|
|
435
445
|
j: handleSchemaScrollDown,
|
|
436
446
|
k: handleSchemaScrollUp,
|
|
437
447
|
i: handleEnterInsertMode,
|
|
448
|
+
e: handleOpenRequestBodyEditor,
|
|
438
449
|
}, mode === 'NORMAL' && isFocused);
|
|
439
450
|
// Render Helpers
|
|
440
451
|
const getSchemaName = (schema) => {
|
|
@@ -530,7 +541,7 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
|
|
|
530
541
|
if (item.param.schema) {
|
|
531
542
|
if (isReferenceObject(item.param.schema)) {
|
|
532
543
|
refInfo = item.param.schema.$ref.split('/').pop() || item.param.schema.$ref;
|
|
533
|
-
typeInfo =
|
|
544
|
+
typeInfo = refInfo;
|
|
534
545
|
}
|
|
535
546
|
else {
|
|
536
547
|
const schemaType = item.param.schema.type;
|
|
@@ -557,8 +568,8 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
|
|
|
557
568
|
commandHint = `h${hIndex}`;
|
|
558
569
|
}
|
|
559
570
|
else if (item.param.in === 'body') {
|
|
560
|
-
const
|
|
561
|
-
commandHint = `
|
|
571
|
+
const btIndex = item.globalIndex - bodyStartIndex + 1;
|
|
572
|
+
commandHint = `bt${btIndex}`;
|
|
562
573
|
}
|
|
563
574
|
}
|
|
564
575
|
return (React.createElement(Box, { key: key, flexDirection: "column" },
|
|
@@ -581,7 +592,11 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
|
|
|
581
592
|
React.createElement(Box, { marginLeft: 2, flexGrow: 1 }, mode === 'INSERT' && isFieldFocused ? (React.createElement(SearchInput, { value: localValue, onChange: (val) => {
|
|
582
593
|
setLocalValue(val);
|
|
583
594
|
onParameterChange(item.param.name, val);
|
|
584
|
-
}, suggestions: [], placeholder: "", focus: true, onSubmit: onExitInsertMode })) : (React.createElement(Text, { color: value ? 'white' : 'gray', backgroundColor: isFieldFocused ? 'green' : undefined, bold: isFieldFocused && !value },
|
|
595
|
+
}, suggestions: [], placeholder: "", focus: true, onSubmit: onExitInsertMode })) : (React.createElement(Text, { color: value ? 'white' : 'gray', backgroundColor: isFieldFocused ? 'green' : undefined, bold: isFieldFocused && !value }, item.param.in === 'body' && value
|
|
596
|
+
? isFieldFocused
|
|
597
|
+
? ' (Press e to edit) '
|
|
598
|
+
: '(Value set)'
|
|
599
|
+
: value || (isFieldFocused ? ' (empty) ' : '(empty)'))))),
|
|
585
600
|
item.param.description && (React.createElement(Box, { marginLeft: 2, marginTop: 0 },
|
|
586
601
|
React.createElement(Text, { color: "gray", dimColor: true }, item.param.description))),
|
|
587
602
|
(() => {
|
|
@@ -625,15 +640,24 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
|
|
|
625
640
|
const isFieldFocused = item.globalIndex === focusedFieldIndex && isFocused;
|
|
626
641
|
const isExpanded = !collapsedSchemas.has('__requestBody__');
|
|
627
642
|
const hasSchema = !!endpoint.requestBody?.schema;
|
|
628
|
-
const commandHint = commandMode ? '
|
|
643
|
+
const commandHint = commandMode ? 'bt1' : '';
|
|
629
644
|
return (React.createElement(Box, { key: key, flexDirection: "column" },
|
|
630
|
-
React.createElement(Box, { flexDirection: "row",
|
|
631
|
-
|
|
632
|
-
React.createElement(
|
|
633
|
-
"
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
645
|
+
React.createElement(Box, { flexDirection: "row", marginLeft: 1 },
|
|
646
|
+
React.createElement(Box, { flexDirection: "row", backgroundColor: isFieldFocused ? 'green' : undefined },
|
|
647
|
+
commandMode && (React.createElement(Box, { width: 8 },
|
|
648
|
+
React.createElement(Text, { color: "yellow" },
|
|
649
|
+
"[",
|
|
650
|
+
commandHint,
|
|
651
|
+
"]"))),
|
|
652
|
+
React.createElement(Text, { color: isFieldFocused ? 'black' : 'gray' }, item.endpoint.requestBody?.description || 'Request Body Content')),
|
|
653
|
+
React.createElement(Box, { marginLeft: 2 },
|
|
654
|
+
React.createElement(Text, { color: requestBodyValue ? 'white' : 'gray', backgroundColor: isFieldFocused ? 'green' : undefined, bold: isFieldFocused && !requestBodyValue }, requestBodyValue
|
|
655
|
+
? isFieldFocused
|
|
656
|
+
? ' (Press e to edit) '
|
|
657
|
+
: '(Value set)'
|
|
658
|
+
: isFieldFocused
|
|
659
|
+
? ' (empty) '
|
|
660
|
+
: '(empty)'))),
|
|
637
661
|
hasSchema && !isExpanded && (React.createElement(Box, { marginLeft: 1, marginTop: 0 },
|
|
638
662
|
React.createElement(Text, { color: "yellow", dimColor: true },
|
|
639
663
|
"Press > to view ",
|
|
@@ -661,13 +685,13 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
|
|
|
661
685
|
(endpoint.requestBody && endpoint.parameters.filter((p) => p.in === 'body').length === 0
|
|
662
686
|
? 1
|
|
663
687
|
: 0);
|
|
664
|
-
const
|
|
688
|
+
const rtIndex = item.globalIndex - responsesStartIndex + 1;
|
|
665
689
|
return (React.createElement(Box, { key: key, flexDirection: "column" },
|
|
666
690
|
React.createElement(Box, { flexDirection: "row", backgroundColor: isFieldFocused ? 'green' : undefined, marginLeft: 1 },
|
|
667
691
|
commandMode && (React.createElement(Box, { width: 8 },
|
|
668
692
|
React.createElement(Text, { color: "yellow" },
|
|
669
693
|
"[",
|
|
670
|
-
`
|
|
694
|
+
`rt${rtIndex}`,
|
|
671
695
|
"]"))),
|
|
672
696
|
React.createElement(Text, { color: isFieldFocused ? 'black' : getStatusColor(item.response.status), bold: true }, item.response.status),
|
|
673
697
|
React.createElement(Text, { color: isFieldFocused ? 'black' : 'white' },
|
|
@@ -2,23 +2,22 @@
|
|
|
2
2
|
* API Detail 우측 패널 - Request/Response 분할
|
|
3
3
|
*/
|
|
4
4
|
import React from 'react';
|
|
5
|
-
import { ApiResponse
|
|
6
|
-
import { Endpoint
|
|
5
|
+
import { ApiResponse } from '../../types/app-state.js';
|
|
6
|
+
import { Endpoint } from '../../types/openapi.js';
|
|
7
7
|
interface RightPanelProps {
|
|
8
8
|
activeRequestTab: 'headers' | 'body' | 'query';
|
|
9
9
|
activeResponseTab: 'body' | 'headers' | 'cookies' | 'curl';
|
|
10
10
|
activePanel: 'left' | 'request' | 'response';
|
|
11
|
-
subFocus: 'TITLE' | 'TABS'
|
|
11
|
+
subFocus: 'TITLE' | 'TABS';
|
|
12
12
|
response: ApiResponse | null;
|
|
13
13
|
endpoint: Endpoint;
|
|
14
|
-
document?: OpenAPIDocument;
|
|
15
14
|
parameterValues: Record<string, string>;
|
|
16
15
|
requestBody: string;
|
|
17
16
|
requestHeaders: Record<string, string>;
|
|
18
17
|
baseURL: string;
|
|
19
|
-
mode: AppMode;
|
|
20
18
|
isLoading: boolean;
|
|
21
19
|
commandMode?: boolean;
|
|
20
|
+
onOpenEditor: () => void;
|
|
22
21
|
}
|
|
23
|
-
export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, subFocus, response, endpoint,
|
|
22
|
+
export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, subFocus, response, endpoint, parameterValues, requestBody, requestHeaders, baseURL, isLoading, commandMode, onOpenEditor, }: RightPanelProps): React.JSX.Element;
|
|
24
23
|
export {};
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API Detail 우측 패널 - Request/Response 분할
|
|
3
3
|
*/
|
|
4
|
-
import React, {
|
|
5
|
-
import { Box, Text, useStdout
|
|
4
|
+
import React, { useMemo } from 'react';
|
|
5
|
+
import { Box, Text, useStdout } from 'ink';
|
|
6
6
|
import { useKeyPress } from '../../hooks/useKeyPress.js';
|
|
7
|
-
import { useAppDispatch } from '../../store/hooks.js';
|
|
8
|
-
import { detailSetPanel, detailSetBody, detailSetSubFocus, setMode } from '../../store/appSlice.js';
|
|
9
|
-
import { spawnSync } from 'child_process';
|
|
10
|
-
import fs from 'fs';
|
|
11
|
-
import os from 'os';
|
|
12
|
-
import path from 'path';
|
|
13
|
-
import { scaffoldSchema } from '../../utils/schema-scaffolder.js';
|
|
14
7
|
import Spinner from 'ink-spinner';
|
|
15
8
|
const TabHeader = ({ tabs, active, isFocused, shortcutPrefix, commandMode }) => (React.createElement(Box, { marginBottom: 0, borderStyle: "single", borderBottom: false, borderTop: false, borderLeft: false, borderRight: false, gap: 1 }, tabs.map((t, index) => {
|
|
16
9
|
let showHint = false;
|
|
@@ -34,129 +27,18 @@ const TabHeader = ({ tabs, active, isFocused, shortcutPrefix, commandMode }) =>
|
|
|
34
27
|
React.createElement(Text, { color: "yellow" }, showIndicator ? '>' : ' '),
|
|
35
28
|
React.createElement(Text, { bold: isActive, underline: showIndicator, color: isActive ? 'black' : 'gray', backgroundColor: isActive ? 'green' : undefined }, t.label + ' ')));
|
|
36
29
|
})));
|
|
37
|
-
export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, subFocus, response, endpoint,
|
|
30
|
+
export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, subFocus, response, endpoint, parameterValues, requestBody, requestHeaders, baseURL, isLoading, commandMode = false, onOpenEditor, }) {
|
|
38
31
|
const { stdout } = useStdout();
|
|
39
|
-
const { isRawModeSupported, setRawMode } = useStdin();
|
|
40
|
-
const dispatch = useAppDispatch();
|
|
41
32
|
// 높이 계산
|
|
42
33
|
const totalHeight = (stdout?.rows || 24) - 4;
|
|
43
34
|
const requestBoxHeight = Math.floor(totalHeight / 2);
|
|
44
35
|
const responseBoxHeight = totalHeight - requestBoxHeight; // 나머지 높이를 Response 박스에 할당
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
setReqScroll(0);
|
|
50
|
-
}, [activeRequestTab, endpoint]);
|
|
51
|
-
// Request Body 자동 스캐폴딩
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (activeRequestTab === 'body' && activePanel === 'request' && !requestBody) {
|
|
54
|
-
// 1. requestBody 필드 (OpenAPI 3.0)
|
|
55
|
-
if (endpoint.requestBody?.schema) {
|
|
56
|
-
const scaffolded = scaffoldSchema(endpoint.requestBody.schema, document);
|
|
57
|
-
const value = JSON.stringify(scaffolded, null, 2);
|
|
58
|
-
dispatch(detailSetBody(value));
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
// 2. in: body 파라미터 (Swagger 2.0)
|
|
62
|
-
const bodyParam = endpoint.parameters.find(p => p.in === 'body');
|
|
63
|
-
if (bodyParam && bodyParam.schema) {
|
|
64
|
-
const scaffolded = scaffoldSchema(bodyParam.schema, document);
|
|
65
|
-
const value = JSON.stringify(scaffolded, null, 2);
|
|
66
|
-
dispatch(detailSetBody(value));
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}, [activeRequestTab, activePanel, requestBody, endpoint, document, dispatch]);
|
|
71
|
-
// Request Content Lines 계산
|
|
72
|
-
const getRequestLines = () => {
|
|
73
|
-
if (activeRequestTab === 'headers') {
|
|
74
|
-
const keys = Object.keys(requestHeaders);
|
|
75
|
-
return keys.length === 0 ? 1 : keys.length;
|
|
76
|
-
}
|
|
77
|
-
if (activeRequestTab === 'body') {
|
|
78
|
-
return requestBody ? requestBody.split('\n').length : 1;
|
|
79
|
-
}
|
|
80
|
-
if (activeRequestTab === 'query') {
|
|
81
|
-
const queries = endpoint.parameters.filter((p) => p.in === 'query');
|
|
82
|
-
return queries.length === 0 ? 1 : queries.length;
|
|
83
|
-
}
|
|
84
|
-
return 0;
|
|
85
|
-
};
|
|
86
|
-
const reqTotalLines = getRequestLines();
|
|
87
|
-
// Request Scroll Handler (j/k)
|
|
88
|
-
useKeyPress({
|
|
89
|
-
j: () => {
|
|
90
|
-
setReqScroll((prev) => {
|
|
91
|
-
const maxScroll = Math.max(0, reqTotalLines - contentHeight);
|
|
92
|
-
if (prev < maxScroll) {
|
|
93
|
-
return Math.min(prev + 1, maxScroll);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
// 스크롤이 끝에 도달하면 Response 패널로 이동
|
|
97
|
-
dispatch(detailSetPanel('response'));
|
|
98
|
-
dispatch(detailSetSubFocus('TITLE'));
|
|
99
|
-
return prev;
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
},
|
|
103
|
-
k: () => {
|
|
104
|
-
setReqScroll((prev) => {
|
|
105
|
-
if (prev > 0)
|
|
106
|
-
return prev - 1;
|
|
107
|
-
dispatch(detailSetSubFocus('TABS'));
|
|
108
|
-
return 0;
|
|
109
|
-
});
|
|
110
|
-
},
|
|
111
|
-
}, mode === 'NORMAL' && activePanel === 'request' && subFocus === 'CONTENT');
|
|
112
|
-
const openExternalEditor = () => {
|
|
113
|
-
try {
|
|
114
|
-
const tempFile = path.join(os.tmpdir(), `lazyapi-body-${Date.now()}.json`);
|
|
115
|
-
fs.writeFileSync(tempFile, requestBody || '');
|
|
116
|
-
const editor = process.env.EDITOR || 'vim';
|
|
117
|
-
// Ink의 Raw 모드를 해제하고 stdin을 일시 중지하여 외부 에디터에 제어권 이양
|
|
118
|
-
if (isRawModeSupported) {
|
|
119
|
-
setRawMode(false);
|
|
120
|
-
}
|
|
121
|
-
process.stdin.pause();
|
|
122
|
-
try {
|
|
123
|
-
// 동기적으로 실행하여 에디터 종료까지 대기
|
|
124
|
-
spawnSync(editor, [tempFile], {
|
|
125
|
-
stdio: 'inherit',
|
|
126
|
-
});
|
|
127
|
-
if (fs.existsSync(tempFile)) {
|
|
128
|
-
const content = fs.readFileSync(tempFile, 'utf-8');
|
|
129
|
-
dispatch(detailSetBody(content));
|
|
130
|
-
fs.unlinkSync(tempFile);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
catch (innerError) {
|
|
134
|
-
console.error('Error executing editor:', innerError);
|
|
135
|
-
}
|
|
136
|
-
finally {
|
|
137
|
-
// 에디터 종료 후 stdin 재개 및 Raw 모드 복구
|
|
138
|
-
process.stdin.resume();
|
|
139
|
-
if (isRawModeSupported) {
|
|
140
|
-
setRawMode(true);
|
|
141
|
-
}
|
|
142
|
-
dispatch(setMode('NORMAL'));
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
catch (e) {
|
|
146
|
-
console.error('Failed to open editor', e);
|
|
147
|
-
dispatch(setMode('NORMAL'));
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
// e 키: Request Body 탭이 활성화되어 있으면 항상 에디터 오픈 (NORMAL/INSERT 모두)
|
|
36
|
+
// 콘텐츠 가용 높이 계산 (박스 테두리 2 + 제목 1 + 탭 1 + 하단 안내 1 = 5줄 제외)
|
|
37
|
+
const contentHeight = Math.max(requestBoxHeight - 5, 1);
|
|
38
|
+
// e 키: 해당 패널의 TABS에 포커스가 있을 때만 동작
|
|
151
39
|
useKeyPress({
|
|
152
|
-
e:
|
|
153
|
-
}, activePanel
|
|
154
|
-
// -- Response Box State --
|
|
155
|
-
const [resScroll, setResScroll] = useState(0);
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
// 탭이 변경될 때만 스크롤 리셋 (response 변경 시에는 리셋하지 않음)
|
|
158
|
-
setResScroll(0);
|
|
159
|
-
}, [activeResponseTab]);
|
|
40
|
+
e: onOpenEditor,
|
|
41
|
+
}, activePanel !== 'left' && subFocus === 'TABS');
|
|
160
42
|
// Curl Command 생성 (메모이제이션)
|
|
161
43
|
const curlCommand = useMemo(() => {
|
|
162
44
|
let pathStr = endpoint.path;
|
|
@@ -183,103 +65,41 @@ export default function RightPanel({ activeRequestTab, activeResponseTab, active
|
|
|
183
65
|
const method = endpoint.method.toUpperCase();
|
|
184
66
|
return `curl -X ${method} '${url}'${headerStr}${bodyStr}`;
|
|
185
67
|
}, [endpoint, parameterValues, requestHeaders, requestBody, baseURL]);
|
|
186
|
-
//
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return 1;
|
|
193
|
-
if (activeResponseTab === 'body') {
|
|
194
|
-
const bodyStr = typeof response.body === 'string' ? response.body : JSON.stringify(response.body, null, 2);
|
|
195
|
-
return bodyStr.split('\n').length;
|
|
196
|
-
}
|
|
197
|
-
if (activeResponseTab === 'headers') {
|
|
198
|
-
return Object.keys(response.headers).length || 1;
|
|
199
|
-
}
|
|
200
|
-
if (activeResponseTab === 'cookies') {
|
|
201
|
-
return response.cookies ? Object.keys(response.cookies).length || 1 : 1;
|
|
202
|
-
}
|
|
203
|
-
return 0;
|
|
204
|
-
}, [activeResponseTab, response, curlCommand]);
|
|
205
|
-
// Response Scroll Handler (j/k)
|
|
206
|
-
useKeyPress({
|
|
207
|
-
j: () => {
|
|
208
|
-
setResScroll((prev) => {
|
|
209
|
-
const maxScroll = Math.max(0, resTotalLines - contentHeight);
|
|
210
|
-
if (prev < maxScroll) {
|
|
211
|
-
return Math.min(prev + 1, maxScroll);
|
|
212
|
-
}
|
|
213
|
-
return prev;
|
|
214
|
-
});
|
|
215
|
-
},
|
|
216
|
-
k: () => {
|
|
217
|
-
setResScroll((prev) => {
|
|
218
|
-
if (prev > 0)
|
|
219
|
-
return prev - 1;
|
|
220
|
-
dispatch(detailSetSubFocus('TABS'));
|
|
221
|
-
return 0;
|
|
222
|
-
});
|
|
223
|
-
},
|
|
224
|
-
}, mode === 'NORMAL' && activePanel === 'response' && subFocus === 'CONTENT');
|
|
225
|
-
// 렌더링 헬퍼
|
|
226
|
-
const renderLines = (lines, offset, limit) => {
|
|
227
|
-
const visible = lines.slice(offset, offset + limit);
|
|
228
|
-
return visible.map((line, i) => React.createElement(Text, { key: offset + i }, line));
|
|
229
|
-
};
|
|
230
|
-
const renderKeyValue = (data, offset, limit) => {
|
|
231
|
-
const entries = Object.entries(data);
|
|
232
|
-
if (entries.length === 0)
|
|
233
|
-
return React.createElement(Text, { color: "gray" }, "No data");
|
|
234
|
-
const visible = entries.slice(offset, offset + limit);
|
|
235
|
-
return visible.map(([k, v], i) => (React.createElement(Box, { key: offset + i },
|
|
236
|
-
React.createElement(Text, { color: "green" },
|
|
237
|
-
k,
|
|
238
|
-
": "),
|
|
239
|
-
React.createElement(Text, null, v))));
|
|
68
|
+
// 렌더링 헬퍼 (스크롤 제거, 단순히 잘라서 표시)
|
|
69
|
+
const renderLines = (lines, limit) => {
|
|
70
|
+
const visible = lines.slice(0, limit);
|
|
71
|
+
return visible.map((line, i) => (React.createElement(Text, { key: i, wrap: "truncate" },
|
|
72
|
+
' ',
|
|
73
|
+
line)));
|
|
240
74
|
};
|
|
241
|
-
// Request Box Render
|
|
75
|
+
// Request Box Content Render
|
|
242
76
|
const renderRequestContent = () => {
|
|
243
77
|
if (activeRequestTab === 'headers') {
|
|
244
78
|
if (Object.keys(requestHeaders).length === 0)
|
|
245
|
-
return React.createElement(Text, { color: "gray" }, "No headers");
|
|
246
|
-
|
|
79
|
+
return React.createElement(Text, { color: "gray" }, " No headers");
|
|
80
|
+
const json = JSON.stringify(requestHeaders, null, 2);
|
|
81
|
+
return renderLines(json.split('\n'), contentHeight);
|
|
247
82
|
}
|
|
248
83
|
if (activeRequestTab === 'body') {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
else {
|
|
254
|
-
const lines = requestBody.split('\n');
|
|
255
|
-
content = renderLines(lines, reqScroll, contentHeight);
|
|
256
|
-
}
|
|
257
|
-
// Body 탭이고 Request 패널 활성 상태면 항상 안내 문구 표시
|
|
258
|
-
if (activePanel === 'request') {
|
|
259
|
-
return (React.createElement(Box, { flexDirection: "column" },
|
|
260
|
-
content,
|
|
261
|
-
React.createElement(Box, { borderStyle: "single", borderColor: "yellow", marginTop: 0 },
|
|
262
|
-
React.createElement(Text, { color: "yellow" }, "Press 'e' key to open external editor."))));
|
|
263
|
-
}
|
|
264
|
-
return content;
|
|
84
|
+
if (!requestBody)
|
|
85
|
+
return React.createElement(Text, { color: "gray" }, " No body");
|
|
86
|
+
const lines = requestBody.split('\n');
|
|
87
|
+
return renderLines(lines, contentHeight);
|
|
265
88
|
}
|
|
266
89
|
if (activeRequestTab === 'query') {
|
|
267
90
|
const queries = endpoint.parameters.filter((p) => p.in === 'query');
|
|
268
91
|
if (queries.length === 0)
|
|
269
|
-
return React.createElement(Text, { color: "gray" }, "No query parameters");
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
q.name,
|
|
274
|
-
": "),
|
|
275
|
-
React.createElement(Text, null, parameterValues[q.name] || (React.createElement(Text, { color: "gray", dimColor: true }, "(empty)"))))));
|
|
92
|
+
return React.createElement(Text, { color: "gray" }, " No query parameters");
|
|
93
|
+
// 파라미터 값만 추출하여 JSON으로 표시
|
|
94
|
+
const json = JSON.stringify(parameterValues, null, 2);
|
|
95
|
+
return renderLines(json.split('\n'), contentHeight);
|
|
276
96
|
}
|
|
277
97
|
return null;
|
|
278
98
|
};
|
|
279
|
-
// Response Box Render
|
|
99
|
+
// Response Box Content Render
|
|
280
100
|
const renderResponseContent = () => {
|
|
281
101
|
if (activeResponseTab === 'curl') {
|
|
282
|
-
return renderLines(curlCommand.split('\n'),
|
|
102
|
+
return renderLines(curlCommand.split('\n'), contentHeight);
|
|
283
103
|
}
|
|
284
104
|
if (isLoading) {
|
|
285
105
|
return (React.createElement(Box, null,
|
|
@@ -288,20 +108,22 @@ export default function RightPanel({ activeRequestTab, activeResponseTab, active
|
|
|
288
108
|
React.createElement(Text, { color: "green" }, " Sending request...")));
|
|
289
109
|
}
|
|
290
110
|
if (!response)
|
|
291
|
-
return React.createElement(Text, { color: "gray" }, "Press Enter to execute request.");
|
|
111
|
+
return React.createElement(Text, { color: "gray" }, " Press Enter to execute request.");
|
|
292
112
|
if (activeResponseTab === 'body') {
|
|
293
113
|
const bodyStr = typeof response.body === 'string' ? response.body : JSON.stringify(response.body, null, 2);
|
|
294
|
-
return renderLines(bodyStr.split('\n'),
|
|
114
|
+
return renderLines(bodyStr.split('\n'), contentHeight);
|
|
295
115
|
}
|
|
296
116
|
if (activeResponseTab === 'headers') {
|
|
297
117
|
if (!response.headers)
|
|
298
|
-
return React.createElement(Text, { color: "gray" }, "No headers");
|
|
299
|
-
|
|
118
|
+
return React.createElement(Text, { color: "gray" }, " No headers");
|
|
119
|
+
const json = JSON.stringify(response.headers, null, 2);
|
|
120
|
+
return renderLines(json.split('\n'), contentHeight);
|
|
300
121
|
}
|
|
301
122
|
if (activeResponseTab === 'cookies') {
|
|
302
123
|
if (!response.cookies || Object.keys(response.cookies).length === 0)
|
|
303
|
-
return React.createElement(Text, { color: "gray" }, "No cookies");
|
|
304
|
-
|
|
124
|
+
return React.createElement(Text, { color: "gray" }, " No cookies");
|
|
125
|
+
const json = JSON.stringify(response.cookies, null, 2);
|
|
126
|
+
return renderLines(json.split('\n'), contentHeight);
|
|
305
127
|
}
|
|
306
128
|
return null;
|
|
307
129
|
};
|
|
@@ -317,34 +139,28 @@ export default function RightPanel({ activeRequestTab, activeResponseTab, active
|
|
|
317
139
|
{ id: 'cookies', label: 'Cookies' },
|
|
318
140
|
{ id: 'curl', label: 'Curl' },
|
|
319
141
|
];
|
|
142
|
+
const renderEditorHint = (isVisible) => (React.createElement(Box, { height: 1 }, isVisible && React.createElement(Text, { color: "yellow" }, " Press 'e' to open external editor.")));
|
|
320
143
|
return (React.createElement(Box, { flexDirection: "column", width: "50%", marginLeft: 1 },
|
|
321
|
-
React.createElement(Box, { flexDirection: "column", height: requestBoxHeight, borderStyle: "single", borderColor: activePanel === 'request' ? 'green' : 'gray', paddingX:
|
|
144
|
+
React.createElement(Box, { flexDirection: "column", height: requestBoxHeight, borderStyle: "single", borderColor: activePanel === 'request' ? 'green' : 'gray', paddingX: 0 },
|
|
322
145
|
React.createElement(Box, { justifyContent: "space-between", height: 1, marginBottom: 0 },
|
|
323
146
|
React.createElement(Box, null,
|
|
324
147
|
React.createElement(Text, { color: activePanel === 'request' && subFocus === 'TITLE' ? 'yellow' : 'white', bold: activePanel === 'request' && subFocus === 'TITLE' },
|
|
325
148
|
activePanel === 'request' && subFocus === 'TITLE' ? '>' : ' ',
|
|
326
|
-
"Request")),
|
|
327
|
-
React.createElement(Box, { width: 15 },
|
|
328
|
-
React.createElement(Text, { color: "gray" }, reqTotalLines > contentHeight
|
|
329
|
-
? `${reqScroll + 1}-${Math.min(reqScroll + contentHeight, reqTotalLines)}/${reqTotalLines}`
|
|
330
|
-
: ' '))),
|
|
149
|
+
"Request"))),
|
|
331
150
|
React.createElement(TabHeader, { tabs: reqTabs, active: activeRequestTab, isFocused: activePanel === 'request' && subFocus === 'TABS', shortcutPrefix: "rq", commandMode: commandMode }),
|
|
332
|
-
React.createElement(Box, {
|
|
333
|
-
|
|
151
|
+
React.createElement(Box, { flexDirection: "column", paddingX: 0, marginTop: 0, flexGrow: 1 }, renderRequestContent()),
|
|
152
|
+
renderEditorHint(activePanel === 'request' && subFocus === 'TABS')),
|
|
153
|
+
React.createElement(Box, { flexDirection: "column", height: responseBoxHeight, borderStyle: "single", borderColor: activePanel === 'response' ? 'green' : 'gray', paddingX: 0 },
|
|
334
154
|
React.createElement(Box, { justifyContent: "space-between", height: 1, marginBottom: 0 },
|
|
335
155
|
React.createElement(Box, null,
|
|
336
156
|
React.createElement(Text, { color: activePanel === 'response' && subFocus === 'TITLE' ? 'yellow' : 'white', bold: activePanel === 'response' && subFocus === 'TITLE' },
|
|
337
157
|
activePanel === 'response' && subFocus === 'TITLE' ? '>' : ' ',
|
|
338
158
|
"Response")),
|
|
339
|
-
React.createElement(Box, { width: 30, flexDirection: "row", justifyContent: "flex-end" },
|
|
340
|
-
response
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
response.statusText)) : (React.createElement(Text, null, " ")),
|
|
344
|
-
React.createElement(Box, { width: 15 },
|
|
345
|
-
React.createElement(Text, { color: "gray" }, resTotalLines > contentHeight
|
|
346
|
-
? `${resScroll + 1}-${Math.min(resScroll + contentHeight, resTotalLines)}/${resTotalLines}`
|
|
347
|
-
: ' ')))),
|
|
159
|
+
React.createElement(Box, { width: 30, flexDirection: "row", justifyContent: "flex-end" }, response ? (React.createElement(Text, { color: response.status < 300 ? 'green' : 'red' },
|
|
160
|
+
response.status,
|
|
161
|
+
" ",
|
|
162
|
+
response.statusText)) : (React.createElement(Text, null, " ")))),
|
|
348
163
|
React.createElement(TabHeader, { tabs: resTabs, active: activeResponseTab, isFocused: activePanel === 'response' && subFocus === 'TABS', shortcutPrefix: "rs", commandMode: commandMode }),
|
|
349
|
-
React.createElement(Box, {
|
|
164
|
+
React.createElement(Box, { flexDirection: "column", paddingX: 0, marginTop: 0, flexGrow: 1 }, renderResponseContent()),
|
|
165
|
+
renderEditorHint(activePanel === 'response' && subFocus === 'TABS'))));
|
|
350
166
|
}
|
package/dist/store/appSlice.d.ts
CHANGED
|
@@ -9,6 +9,6 @@ export declare const setDocument: import("@reduxjs/toolkit").ActionCreatorWithPa
|
|
|
9
9
|
}, "app/detailSetParam">, detailSetBody: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/detailSetBody">, detailSetHeader: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
10
10
|
key: string;
|
|
11
11
|
value: string;
|
|
12
|
-
}, "app/detailSetHeader">, detailSetPanel: import("@reduxjs/toolkit").ActionCreatorWithPayload<"left" | "request" | "response", "app/detailSetPanel">, detailSetRequestTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"headers" | "query" | "body", "app/detailSetRequestTab">, detailSetResponseTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"headers" | "body" | "cookies" | "curl", "app/detailSetResponseTab">, detailSetSubFocus: import("@reduxjs/toolkit").ActionCreatorWithPayload<"TITLE" | "TABS"
|
|
12
|
+
}, "app/detailSetHeader">, detailSetPanel: import("@reduxjs/toolkit").ActionCreatorWithPayload<"left" | "request" | "response", "app/detailSetPanel">, detailSetRequestTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"headers" | "query" | "body", "app/detailSetRequestTab">, detailSetResponseTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"headers" | "body" | "cookies" | "curl", "app/detailSetResponseTab">, detailSetSubFocus: import("@reduxjs/toolkit").ActionCreatorWithPayload<"TITLE" | "TABS", "app/detailSetSubFocus">, detailSetResponse: import("@reduxjs/toolkit").ActionCreatorWithPayload<ApiResponse | null, "app/detailSetResponse">, reset: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"app/reset">;
|
|
13
13
|
declare const _default: import("redux").Reducer<AppState>;
|
|
14
14
|
export default _default;
|
package/dist/store/appSlice.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* 애플리케이션 Redux Slice
|
|
3
3
|
*/
|
|
4
4
|
import { createSlice } from '@reduxjs/toolkit';
|
|
5
|
+
import { scaffoldSchema } from '../utils/schema-scaffolder.js';
|
|
5
6
|
// 초기 상태
|
|
6
7
|
const initialState = {
|
|
7
8
|
document: null,
|
|
@@ -122,12 +123,28 @@ const appSlice = createSlice({
|
|
|
122
123
|
},
|
|
123
124
|
detailSet: (state, action) => {
|
|
124
125
|
const endpoint = action.payload;
|
|
126
|
+
let initialRequestBody = '';
|
|
127
|
+
if (state.document) {
|
|
128
|
+
// 1. requestBody (OpenAPI 3.0)
|
|
129
|
+
if (endpoint.requestBody?.schema) {
|
|
130
|
+
const scaffolded = scaffoldSchema(endpoint.requestBody.schema, state.document);
|
|
131
|
+
initialRequestBody = JSON.stringify(scaffolded, null, 2);
|
|
132
|
+
}
|
|
133
|
+
// 2. in: body parameter (Swagger 2.0)
|
|
134
|
+
else {
|
|
135
|
+
const bodyParam = endpoint.parameters.find((p) => p.in === 'body');
|
|
136
|
+
if (bodyParam && bodyParam.schema) {
|
|
137
|
+
const scaffolded = scaffoldSchema(bodyParam.schema, state.document);
|
|
138
|
+
initialRequestBody = JSON.stringify(scaffolded, null, 2);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
125
142
|
state.screen = 'DETAIL';
|
|
126
143
|
state.detail = {
|
|
127
144
|
endpoint,
|
|
128
145
|
focusedFieldIndex: 0,
|
|
129
146
|
parameterValues: {},
|
|
130
|
-
requestBody:
|
|
147
|
+
requestBody: initialRequestBody,
|
|
131
148
|
headers: {},
|
|
132
149
|
activePanel: 'left',
|
|
133
150
|
activeRequestTab: 'headers',
|
|
@@ -37,17 +37,15 @@ export const navigateUp = () => (dispatch, getState) => {
|
|
|
37
37
|
if (detail.subFocus === 'TABS') {
|
|
38
38
|
dispatch(detailSetSubFocus('TITLE'));
|
|
39
39
|
}
|
|
40
|
-
// CONTENT: Handled by component (scroll)
|
|
41
40
|
}
|
|
42
41
|
else if (detail.activePanel === 'response') {
|
|
43
42
|
if (detail.subFocus === 'TITLE') {
|
|
44
43
|
dispatch(detailSetPanel('request'));
|
|
45
|
-
dispatch(detailSetSubFocus('
|
|
44
|
+
dispatch(detailSetSubFocus('TABS'));
|
|
46
45
|
}
|
|
47
46
|
else if (detail.subFocus === 'TABS') {
|
|
48
47
|
dispatch(detailSetSubFocus('TITLE'));
|
|
49
48
|
}
|
|
50
|
-
// CONTENT: Handled by component (scroll)
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
51
|
};
|
|
@@ -105,18 +103,14 @@ export const navigateDown = () => (dispatch, getState) => {
|
|
|
105
103
|
dispatch(detailSetSubFocus('TABS'));
|
|
106
104
|
}
|
|
107
105
|
else if (detail.subFocus === 'TABS') {
|
|
108
|
-
dispatch(
|
|
106
|
+
dispatch(detailSetPanel('response'));
|
|
107
|
+
dispatch(detailSetSubFocus('TITLE'));
|
|
109
108
|
}
|
|
110
|
-
// CONTENT: Handled by component (scroll) -> Move to Response
|
|
111
109
|
}
|
|
112
110
|
else if (detail.activePanel === 'response') {
|
|
113
111
|
if (detail.subFocus === 'TITLE') {
|
|
114
112
|
dispatch(detailSetSubFocus('TABS'));
|
|
115
113
|
}
|
|
116
|
-
else if (detail.subFocus === 'TABS') {
|
|
117
|
-
dispatch(detailSetSubFocus('CONTENT'));
|
|
118
|
-
}
|
|
119
|
-
// CONTENT: Handled by component (scroll)
|
|
120
114
|
}
|
|
121
115
|
}
|
|
122
116
|
};
|
|
@@ -147,7 +141,7 @@ export const navigateLeft = () => (dispatch, getState) => {
|
|
|
147
141
|
// Already at left, do nothing or go back to list (handled by u key usually)
|
|
148
142
|
}
|
|
149
143
|
else {
|
|
150
|
-
if (detail.subFocus === 'TITLE'
|
|
144
|
+
if (detail.subFocus === 'TITLE') {
|
|
151
145
|
dispatch(detailSetPanel('left'));
|
|
152
146
|
}
|
|
153
147
|
else if (detail.subFocus === 'TABS') {
|
|
@@ -41,7 +41,7 @@ export interface DetailViewState {
|
|
|
41
41
|
activePanel: 'left' | 'request' | 'response';
|
|
42
42
|
activeRequestTab: 'headers' | 'body' | 'query';
|
|
43
43
|
activeResponseTab: 'body' | 'headers' | 'cookies' | 'curl';
|
|
44
|
-
subFocus: 'TITLE' | 'TABS'
|
|
44
|
+
subFocus: 'TITLE' | 'TABS';
|
|
45
45
|
response: ApiResponse | null;
|
|
46
46
|
}
|
|
47
47
|
export interface ApiResponse {
|
|
@@ -130,7 +130,7 @@ export type AppAction = {
|
|
|
130
130
|
payload: 'body' | 'headers' | 'cookies' | 'curl';
|
|
131
131
|
} | {
|
|
132
132
|
type: 'DETAIL_SET_SUB_FOCUS';
|
|
133
|
-
payload: 'TITLE' | 'TABS'
|
|
133
|
+
payload: 'TITLE' | 'TABS';
|
|
134
134
|
} | {
|
|
135
135
|
type: 'DETAIL_SET_RESPONSE';
|
|
136
136
|
payload: ApiResponse | null;
|