@gomjellie/lazyapi 0.0.1 → 0.0.3
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 +2 -0
- package/dist/App.js +1 -1
- package/dist/cli.js +5 -3
- package/dist/components/common/LoadingSpinner.js +8 -4
- package/dist/components/common/StatusBar.js +3 -0
- package/dist/components/detail-view/DetailView.js +38 -38
- package/dist/components/detail-view/LeftPanel.d.ts +4 -1
- package/dist/components/detail-view/LeftPanel.js +402 -72
- package/dist/components/detail-view/RightPanel.d.ts +3 -2
- package/dist/components/detail-view/RightPanel.js +119 -31
- package/dist/components/list-view/EndpointList.js +16 -11
- package/dist/components/list-view/ListView.js +34 -58
- package/dist/components/list-view/MethodsBar.d.ts +14 -0
- package/dist/components/list-view/MethodsBar.js +36 -0
- package/dist/hooks/useKeyPress.js +66 -23
- package/dist/services/openapi-parser.d.ts +12 -4
- package/dist/services/openapi-parser.js +118 -21
- package/dist/store/appSlice.d.ts +2 -2
- package/dist/store/appSlice.js +43 -5
- package/dist/store/hooks.d.ts +1 -1
- package/dist/store/index.d.ts +3 -3
- package/dist/store/navigationActions.d.ts +9 -0
- package/dist/store/navigationActions.js +129 -0
- package/dist/types/app-state.d.ts +13 -4
- package/dist/types/openapi.d.ts +18 -1
- package/dist/utils/schema-formatter.js +19 -5
- package/dist/utils/schema-scaffolder.d.ts +7 -0
- package/dist/utils/schema-scaffolder.js +61 -0
- package/package.json +5 -1
- package/dist/components/list-view/FilterBar.d.ts +0 -12
- package/dist/components/list-view/FilterBar.js +0 -33
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API Detail 좌측 패널 - 파라미터 입력
|
|
3
3
|
*/
|
|
4
|
-
import React, { useState, useEffect, useLayoutEffect, useMemo } from 'react';
|
|
4
|
+
import React, { useState, useEffect, useLayoutEffect, useMemo, useCallback } from 'react';
|
|
5
5
|
import { Box, Text, useStdout } from 'ink';
|
|
6
6
|
import SearchInput from '../common/SearchInput.js';
|
|
7
7
|
import { isReferenceObject, } from '../../types/openapi.js';
|
|
8
8
|
import { useKeyPress } from '../../hooks/useKeyPress.js';
|
|
9
9
|
import { formatSchemaFull } from '../../utils/schema-formatter.js';
|
|
10
|
+
import { scaffoldSchema } from '../../utils/schema-scaffolder.js';
|
|
10
11
|
function getMethodColor(method) {
|
|
11
12
|
switch (method) {
|
|
12
13
|
case 'GET':
|
|
@@ -23,11 +24,130 @@ function getMethodColor(method) {
|
|
|
23
24
|
return 'white';
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
function getStatusColor(status) {
|
|
28
|
+
if (status.startsWith('2'))
|
|
29
|
+
return 'green';
|
|
30
|
+
if (status.startsWith('3'))
|
|
31
|
+
return 'blue';
|
|
32
|
+
if (status.startsWith('4'))
|
|
33
|
+
return 'yellow';
|
|
34
|
+
if (status.startsWith('5'))
|
|
35
|
+
return 'red';
|
|
36
|
+
return 'gray';
|
|
37
|
+
}
|
|
38
|
+
export default function LeftPanel({ endpoint, parameterValues, requestBodyValue, focusedFieldIndex, mode, isFocused, document, commandMode = false, onParameterChange, onExitInsertMode, onRequestBodyEdit, onSetMode, }) {
|
|
27
39
|
const { stdout } = useStdout();
|
|
28
40
|
const [localValue, setLocalValue] = useState('');
|
|
29
|
-
const [
|
|
30
|
-
|
|
41
|
+
const [collapsedSchemas, setCollapsedSchemas] = useState(() => {
|
|
42
|
+
const initial = new Set();
|
|
43
|
+
endpoint.responses?.forEach((res) => {
|
|
44
|
+
initial.add(`res_${res.status}`);
|
|
45
|
+
});
|
|
46
|
+
return initial;
|
|
47
|
+
});
|
|
48
|
+
const [schemaScrollOffsets, setSchemaScrollOffsets] = useState({});
|
|
49
|
+
// 엔드포인트 변경 시 Response Schema들 다시 접기
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setCollapsedSchemas((prev) => {
|
|
52
|
+
const next = new Set(prev);
|
|
53
|
+
endpoint.responses?.forEach((res) => {
|
|
54
|
+
next.add(`res_${res.status}`);
|
|
55
|
+
});
|
|
56
|
+
return next;
|
|
57
|
+
});
|
|
58
|
+
}, [endpoint.responses]);
|
|
59
|
+
// Helper to calculate dynamic item height
|
|
60
|
+
const getItemHeight = useCallback((item) => {
|
|
61
|
+
let height = 0;
|
|
62
|
+
if (item.type === 'title') {
|
|
63
|
+
// Title height calculation with wrapping estimation
|
|
64
|
+
const panelWidth = Math.floor((stdout?.columns || 80) / 2) - 2; // 50% width - padding
|
|
65
|
+
const titleText = `[${item.endpoint.method}] ${item.endpoint.path}`;
|
|
66
|
+
const titleLines = Math.ceil(titleText.length / panelWidth) || 1;
|
|
67
|
+
height = titleLines;
|
|
68
|
+
if (item.endpoint.summary || item.endpoint.description) {
|
|
69
|
+
const descText = item.endpoint.summary || item.endpoint.description || '';
|
|
70
|
+
const descLines = Math.ceil(descText.length / panelWidth) || 1;
|
|
71
|
+
height += descLines;
|
|
72
|
+
}
|
|
73
|
+
// marginBottom 제거됨
|
|
74
|
+
}
|
|
75
|
+
else if (item.type === 'section') {
|
|
76
|
+
height = 1; // Title only (no margin)
|
|
77
|
+
}
|
|
78
|
+
else if (item.type === 'param') {
|
|
79
|
+
height = 1; // Basic row
|
|
80
|
+
if (item.param.description)
|
|
81
|
+
height += 1;
|
|
82
|
+
const isFieldFocused = item.globalIndex === focusedFieldIndex && isFocused;
|
|
83
|
+
if (isFieldFocused) {
|
|
84
|
+
// Enum hints
|
|
85
|
+
let enumValues = [];
|
|
86
|
+
if (item.param.schema && !isReferenceObject(item.param.schema)) {
|
|
87
|
+
if (item.param.schema.enum) {
|
|
88
|
+
enumValues = item.param.schema.enum;
|
|
89
|
+
}
|
|
90
|
+
else if (item.param.schema.type === 'array' &&
|
|
91
|
+
item.param.schema.items &&
|
|
92
|
+
!isReferenceObject(item.param.schema.items) &&
|
|
93
|
+
item.param.schema.items.enum) {
|
|
94
|
+
enumValues = item.param.schema.items.enum;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (enumValues.length > 0)
|
|
98
|
+
height += 2;
|
|
99
|
+
}
|
|
100
|
+
// Schema expansion
|
|
101
|
+
const hasSchema = !!(item.param.schema && isReferenceObject(item.param.schema));
|
|
102
|
+
if (hasSchema) {
|
|
103
|
+
const isExpanded = !collapsedSchemas.has(item.param.name);
|
|
104
|
+
if (!isExpanded) {
|
|
105
|
+
height += 1; // "Press > to view..."
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Schema box height calculation (title is now inside the box)
|
|
109
|
+
const lines = formatSchemaFull(item.param.schema, document, 10);
|
|
110
|
+
const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
|
|
111
|
+
const visibleLinesCount = Math.min(lines.length, maxHeight);
|
|
112
|
+
height += 1 + visibleLinesCount + (lines.length > maxHeight ? 1 : 0) + 2; // +1 for title, +2 for borders
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (item.type === 'requestBody') {
|
|
117
|
+
height = 1; // Basic row
|
|
118
|
+
const hasSchema = !!endpoint.requestBody?.schema;
|
|
119
|
+
if (hasSchema) {
|
|
120
|
+
const isExpanded = !collapsedSchemas.has('__requestBody__');
|
|
121
|
+
if (!isExpanded) {
|
|
122
|
+
height += 1;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const lines = formatSchemaFull(endpoint.requestBody.schema, document, 10);
|
|
126
|
+
const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
|
|
127
|
+
const visibleLinesCount = Math.min(lines.length, maxHeight);
|
|
128
|
+
height += 1 + visibleLinesCount + (lines.length > maxHeight ? 1 : 0) + 2; // +1 for title, +2 for borders
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (item.type === 'responseItem') {
|
|
133
|
+
height = 1; // Basic row
|
|
134
|
+
const hasSchema = !!item.response.content?.[0]?.schema;
|
|
135
|
+
if (hasSchema) {
|
|
136
|
+
const resKey = `res_${item.response.status}`;
|
|
137
|
+
const isExpanded = !collapsedSchemas.has(resKey);
|
|
138
|
+
if (!isExpanded) {
|
|
139
|
+
height += 1;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const lines = formatSchemaFull(item.response.content[0].schema, document, 10);
|
|
143
|
+
const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
|
|
144
|
+
const visibleLinesCount = Math.min(lines.length, maxHeight);
|
|
145
|
+
height += 1 + visibleLinesCount + (lines.length > maxHeight ? 1 : 0) + 2; // +1 for title, +2 for borders
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return height;
|
|
150
|
+
}, [stdout, focusedFieldIndex, isFocused, collapsedSchemas, document, endpoint]);
|
|
31
151
|
// Flat Items Memoization
|
|
32
152
|
const { flatItems, paramIndexToFlatIndex } = useMemo(() => {
|
|
33
153
|
// 파라미터 분류
|
|
@@ -67,44 +187,83 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
|
|
|
67
187
|
items.push({ type: 'section', text: '▸ Request Body' });
|
|
68
188
|
indexMap[currentIndex] = items.length;
|
|
69
189
|
items.push({ type: 'requestBody', endpoint, globalIndex: currentIndex });
|
|
190
|
+
currentIndex++;
|
|
191
|
+
}
|
|
192
|
+
// Responses 섹션 추가
|
|
193
|
+
if (endpoint.responses && endpoint.responses.length > 0) {
|
|
194
|
+
items.push({ type: 'section', text: '▸ Responses' });
|
|
195
|
+
endpoint.responses.forEach((res) => {
|
|
196
|
+
const globalIndex = currentIndex;
|
|
197
|
+
indexMap[globalIndex] = items.length;
|
|
198
|
+
items.push({ type: 'responseItem', response: res, globalIndex });
|
|
199
|
+
currentIndex++;
|
|
200
|
+
});
|
|
70
201
|
}
|
|
71
202
|
return { flatItems: items, paramIndexToFlatIndex: indexMap };
|
|
72
203
|
}, [endpoint]);
|
|
73
204
|
// 스크롤 로직
|
|
74
205
|
const totalHeight = (stdout?.rows || 24) - 4;
|
|
75
206
|
const contentHeight = Math.max(totalHeight - 2, 1);
|
|
76
|
-
const visibleItemCount = Math.floor(contentHeight / 2); // 추정치
|
|
77
207
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
208
|
+
// Dynamic Scroll Logic based on Item Heights
|
|
78
209
|
useLayoutEffect(() => {
|
|
79
210
|
const targetFlatIndex = paramIndexToFlatIndex[focusedFieldIndex];
|
|
80
211
|
if (targetFlatIndex === undefined)
|
|
81
212
|
return;
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
setScrollOffset((
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
213
|
+
// 현재 포커스된 아이템의 위치와 높이를 파악해야 함.
|
|
214
|
+
// 하지만 전체 아이템의 높이를 다 계산하는 것은 비효율적일 수 있으나, 아이템 수가 많지 않으므로 가능함.
|
|
215
|
+
// 여기서는 간단히 scrollOffset을 조정하여 targetItem이 화면 내에 들어오도록 함.
|
|
216
|
+
setScrollOffset((_currentOffset) => {
|
|
217
|
+
// 0. Smart Scroll: 최상단(0)부터 현재 타겟까지 다 보여줄 수 있으면 무조건 0 반환
|
|
218
|
+
let heightFromTop = 0;
|
|
219
|
+
for (let i = 0; i <= targetFlatIndex; i++) {
|
|
220
|
+
heightFromTop += getItemHeight(flatItems[i]);
|
|
221
|
+
}
|
|
222
|
+
if (heightFromTop <= contentHeight) {
|
|
223
|
+
return 0;
|
|
89
224
|
}
|
|
90
|
-
//
|
|
91
|
-
if (targetFlatIndex <
|
|
92
|
-
|
|
93
|
-
return Math.max(0, targetFlatIndex);
|
|
225
|
+
// 1. 타겟 아이템이 현재 오프셋보다 위에 있으면 타겟을 맨 위로 (즉시 위로 스크롤)
|
|
226
|
+
if (targetFlatIndex < _currentOffset) {
|
|
227
|
+
return targetFlatIndex;
|
|
94
228
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
229
|
+
// 2. 타겟 아이템이 현재 화면 범위를 넘어 아래에 있는 경우
|
|
230
|
+
// 타겟의 '바닥'이 화면 맨 아래에 닿을 정도까지만 최소한으로 스크롤 내림
|
|
231
|
+
// 이렇게 하면 타겟 위쪽의 컨텐츠가 최대한 많이 남음
|
|
232
|
+
let currentViewHeight = 0;
|
|
233
|
+
for (let i = _currentOffset; i <= targetFlatIndex; i++) {
|
|
234
|
+
currentViewHeight += getItemHeight(flatItems[i]);
|
|
235
|
+
}
|
|
236
|
+
if (currentViewHeight > contentHeight) {
|
|
237
|
+
// 화면을 넘침 -> 타겟이 맨 아래에 오도록 하는 새로운 시작점(S) 계산
|
|
238
|
+
let acc = 0;
|
|
239
|
+
for (let i = targetFlatIndex; i >= 0; i--) {
|
|
240
|
+
const h = getItemHeight(flatItems[i]);
|
|
241
|
+
if (acc + h > contentHeight) {
|
|
242
|
+
// 이 아이템(i)을 넣으면 화면을 넘어가므로 i+1이 최적의 시작점
|
|
243
|
+
return i + 1;
|
|
244
|
+
}
|
|
245
|
+
acc += h;
|
|
246
|
+
}
|
|
247
|
+
return 0;
|
|
98
248
|
}
|
|
99
|
-
|
|
249
|
+
// 이미 화면 안에 잘 있으면 현재 오프셋 유지
|
|
250
|
+
return _currentOffset;
|
|
100
251
|
});
|
|
101
|
-
}, [
|
|
252
|
+
}, [
|
|
253
|
+
focusedFieldIndex,
|
|
254
|
+
contentHeight,
|
|
255
|
+
paramIndexToFlatIndex,
|
|
256
|
+
flatItems,
|
|
257
|
+
collapsedSchemas,
|
|
258
|
+
isFocused,
|
|
259
|
+
localValue,
|
|
260
|
+
getItemHeight,
|
|
261
|
+
]);
|
|
102
262
|
// 포커스 변경시 로컬 값 업데이트
|
|
103
263
|
useEffect(() => {
|
|
104
264
|
const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
|
|
105
265
|
const item = flatItems[flatIndex];
|
|
106
266
|
if (item && item.type === 'param') {
|
|
107
|
-
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
108
267
|
setLocalValue(parameterValues[item.param.name] || '');
|
|
109
268
|
}
|
|
110
269
|
}, [focusedFieldIndex, parameterValues, paramIndexToFlatIndex, flatItems]);
|
|
@@ -115,22 +274,31 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
|
|
|
115
274
|
const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
|
|
116
275
|
const item = flatItems[flatIndex];
|
|
117
276
|
if (item?.type === 'requestBody') {
|
|
118
|
-
|
|
277
|
+
setCollapsedSchemas((prev) => {
|
|
119
278
|
const next = new Set(prev);
|
|
120
|
-
next.
|
|
279
|
+
next.delete('__requestBody__');
|
|
121
280
|
return next;
|
|
122
281
|
});
|
|
123
|
-
|
|
282
|
+
setSchemaScrollOffsets((prev) => ({ ...prev, __requestBody__: 0 }));
|
|
124
283
|
}
|
|
125
284
|
else if (item?.type === 'param' &&
|
|
126
285
|
item.param.schema &&
|
|
127
286
|
isReferenceObject(item.param.schema)) {
|
|
128
|
-
|
|
287
|
+
setCollapsedSchemas((prev) => {
|
|
129
288
|
const next = new Set(prev);
|
|
130
|
-
next.
|
|
289
|
+
next.delete(item.param.name);
|
|
131
290
|
return next;
|
|
132
291
|
});
|
|
133
|
-
|
|
292
|
+
setSchemaScrollOffsets((prev) => ({ ...prev, [item.param.name]: 0 }));
|
|
293
|
+
}
|
|
294
|
+
else if (item?.type === 'responseItem') {
|
|
295
|
+
const resKey = `res_${item.response.status}`;
|
|
296
|
+
setCollapsedSchemas((prev) => {
|
|
297
|
+
const next = new Set(prev);
|
|
298
|
+
next.delete(resKey);
|
|
299
|
+
return next;
|
|
300
|
+
});
|
|
301
|
+
setSchemaScrollOffsets((prev) => ({ ...prev, [resKey]: 0 }));
|
|
134
302
|
}
|
|
135
303
|
};
|
|
136
304
|
const handleCollapseSchema = () => {
|
|
@@ -139,16 +307,24 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
|
|
|
139
307
|
const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
|
|
140
308
|
const item = flatItems[flatIndex];
|
|
141
309
|
if (item?.type === 'requestBody') {
|
|
142
|
-
|
|
310
|
+
setCollapsedSchemas((prev) => {
|
|
143
311
|
const next = new Set(prev);
|
|
144
|
-
next.
|
|
312
|
+
next.add('__requestBody__');
|
|
145
313
|
return next;
|
|
146
314
|
});
|
|
147
315
|
}
|
|
148
316
|
else if (item?.type === 'param') {
|
|
149
|
-
|
|
317
|
+
setCollapsedSchemas((prev) => {
|
|
150
318
|
const next = new Set(prev);
|
|
151
|
-
next.
|
|
319
|
+
next.add(item.param.name);
|
|
320
|
+
return next;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
else if (item?.type === 'responseItem') {
|
|
324
|
+
const resKey = `res_${item.response.status}`;
|
|
325
|
+
setCollapsedSchemas((prev) => {
|
|
326
|
+
const next = new Set(prev);
|
|
327
|
+
next.add(resKey);
|
|
152
328
|
return next;
|
|
153
329
|
});
|
|
154
330
|
}
|
|
@@ -159,64 +335,171 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
|
|
|
159
335
|
const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
|
|
160
336
|
const item = flatItems[flatIndex];
|
|
161
337
|
const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
|
|
162
|
-
if (item?.type === 'requestBody' &&
|
|
338
|
+
if (item?.type === 'requestBody' && !collapsedSchemas.has('__requestBody__')) {
|
|
163
339
|
if (endpoint.requestBody?.schema) {
|
|
164
|
-
const lines = formatSchemaFull(endpoint.requestBody.schema, document,
|
|
165
|
-
|
|
340
|
+
const lines = formatSchemaFull(endpoint.requestBody.schema, document, 10);
|
|
341
|
+
setSchemaScrollOffsets((prev) => {
|
|
342
|
+
const current = prev.__requestBody__ || 0;
|
|
343
|
+
return {
|
|
344
|
+
...prev,
|
|
345
|
+
__requestBody__: Math.min(current + 1, Math.max(0, lines.length - maxHeight)),
|
|
346
|
+
};
|
|
347
|
+
});
|
|
166
348
|
}
|
|
167
349
|
}
|
|
168
|
-
else if (item?.type === 'param' &&
|
|
350
|
+
else if (item?.type === 'param' && !collapsedSchemas.has(item.param.name)) {
|
|
169
351
|
if (item.param.schema && isReferenceObject(item.param.schema)) {
|
|
170
|
-
const lines = formatSchemaFull(item.param.schema, document,
|
|
171
|
-
|
|
352
|
+
const lines = formatSchemaFull(item.param.schema, document, 10);
|
|
353
|
+
setSchemaScrollOffsets((prev) => {
|
|
354
|
+
const current = prev[item.param.name] || 0;
|
|
355
|
+
return {
|
|
356
|
+
...prev,
|
|
357
|
+
[item.param.name]: Math.min(current + 1, Math.max(0, lines.length - maxHeight)),
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
else if (item?.type === 'responseItem') {
|
|
363
|
+
const resKey = `res_${item.response.status}`;
|
|
364
|
+
if (!collapsedSchemas.has(resKey)) {
|
|
365
|
+
const schema = item.response.content?.[0]?.schema;
|
|
366
|
+
if (schema) {
|
|
367
|
+
const lines = formatSchemaFull(schema, document, 10);
|
|
368
|
+
setSchemaScrollOffsets((prev) => {
|
|
369
|
+
const current = prev[resKey] || 0;
|
|
370
|
+
return {
|
|
371
|
+
...prev,
|
|
372
|
+
[resKey]: Math.min(current + 1, Math.max(0, lines.length - maxHeight)),
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
}
|
|
172
376
|
}
|
|
173
377
|
}
|
|
174
378
|
};
|
|
175
379
|
const handleSchemaScrollUp = () => {
|
|
176
|
-
|
|
380
|
+
if (!isFocused || mode !== 'NORMAL')
|
|
381
|
+
return;
|
|
382
|
+
const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
|
|
383
|
+
const item = flatItems[flatIndex];
|
|
384
|
+
if (item?.type === 'requestBody') {
|
|
385
|
+
setSchemaScrollOffsets((prev) => {
|
|
386
|
+
const current = prev.__requestBody__ || 0;
|
|
387
|
+
return { ...prev, __requestBody__: Math.max(current - 1, 0) };
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
else if (item?.type === 'param') {
|
|
391
|
+
setSchemaScrollOffsets((prev) => {
|
|
392
|
+
const current = prev[item.param.name] || 0;
|
|
393
|
+
return { ...prev, [item.param.name]: Math.max(current - 1, 0) };
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
else if (item?.type === 'responseItem') {
|
|
397
|
+
const resKey = `res_${item.response.status}`;
|
|
398
|
+
setSchemaScrollOffsets((prev) => {
|
|
399
|
+
const current = prev[resKey] || 0;
|
|
400
|
+
return { ...prev, [resKey]: Math.max(current - 1, 0) };
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
const handleEnterInsertMode = () => {
|
|
405
|
+
if (!isFocused || mode !== 'NORMAL')
|
|
406
|
+
return;
|
|
407
|
+
const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
|
|
408
|
+
const item = flatItems[flatIndex];
|
|
409
|
+
const isBodyParam = item?.type === 'requestBody' || (item?.type === 'param' && item.param.in === 'body');
|
|
410
|
+
if (isBodyParam) {
|
|
411
|
+
// 1. Request Body인 경우 (OpenAPI 3.0 requestBody OR Swagger 2.0 body param)
|
|
412
|
+
if (onRequestBodyEdit && onSetMode) {
|
|
413
|
+
// 값이 없으면 스키마 기반 생성
|
|
414
|
+
let valueToEdit = requestBodyValue || '';
|
|
415
|
+
const schema = item.type === 'requestBody' ? endpoint.requestBody?.schema : item.param.schema;
|
|
416
|
+
// 빈 문자열이거나 공백만 있는 경우에도 스캐폴딩 적용
|
|
417
|
+
if ((!valueToEdit || valueToEdit.trim() === '') && schema) {
|
|
418
|
+
const scaffolded = scaffoldSchema(schema, document);
|
|
419
|
+
valueToEdit = JSON.stringify(scaffolded, null, 2);
|
|
420
|
+
}
|
|
421
|
+
onRequestBodyEdit(valueToEdit);
|
|
422
|
+
onSetMode('INSERT');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (item?.type === 'param') {
|
|
426
|
+
// 2. 일반 파라미터인 경우
|
|
427
|
+
if (onSetMode) {
|
|
428
|
+
onSetMode('INSERT');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
177
431
|
};
|
|
178
432
|
useKeyPress({
|
|
179
433
|
'>': handleExpandSchema,
|
|
180
434
|
'<': handleCollapseSchema,
|
|
181
435
|
j: handleSchemaScrollDown,
|
|
182
436
|
k: handleSchemaScrollUp,
|
|
437
|
+
i: handleEnterInsertMode,
|
|
183
438
|
}, mode === 'NORMAL' && isFocused);
|
|
184
439
|
// Render Helpers
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
440
|
+
const getSchemaName = (schema) => {
|
|
441
|
+
if (!schema)
|
|
442
|
+
return null;
|
|
443
|
+
if (isReferenceObject(schema)) {
|
|
444
|
+
return schema.$ref.split('/').pop() || null;
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
};
|
|
448
|
+
const renderSchemaBox = (lines, scrollKey) => {
|
|
449
|
+
const offset = schemaScrollOffsets[scrollKey] || 0;
|
|
188
450
|
const maxHeight = Math.floor((stdout?.rows || 24) * 0.3);
|
|
189
|
-
const visibleLines = lines.slice(
|
|
190
|
-
return (React.createElement(Box, { marginTop:
|
|
451
|
+
const visibleLines = lines.slice(offset, offset + maxHeight);
|
|
452
|
+
return (React.createElement(Box, { marginTop: 0, borderStyle: "single", borderColor: "cyan", paddingX: 1, flexDirection: "column" },
|
|
191
453
|
visibleLines.map((line, idx) => (React.createElement(Text, { key: idx, color: "cyan", dimColor: true }, line))),
|
|
192
454
|
lines.length > maxHeight && (React.createElement(Text, { color: "gray", dimColor: true },
|
|
193
|
-
|
|
455
|
+
offset + 1,
|
|
194
456
|
"-",
|
|
195
|
-
Math.min(
|
|
196
|
-
" of",
|
|
197
|
-
' ',
|
|
457
|
+
Math.min(offset + maxHeight, lines.length),
|
|
458
|
+
" of ",
|
|
198
459
|
lines.length,
|
|
199
460
|
" lines"))));
|
|
200
461
|
};
|
|
462
|
+
const renderSchema = (ref, paramName) => {
|
|
463
|
+
const schema = { $ref: ref };
|
|
464
|
+
const lines = formatSchemaFull(schema, document, 10);
|
|
465
|
+
return renderSchemaBox(lines, paramName);
|
|
466
|
+
};
|
|
201
467
|
const renderRequestBodySchema = () => {
|
|
202
468
|
if (!endpoint.requestBody?.schema)
|
|
203
469
|
return null;
|
|
204
|
-
const lines = formatSchemaFull(endpoint.requestBody.schema, document,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
' ',
|
|
215
|
-
lines.length,
|
|
216
|
-
" lines"))));
|
|
470
|
+
const lines = formatSchemaFull(endpoint.requestBody.schema, document, 10);
|
|
471
|
+
return renderSchemaBox(lines, '__requestBody__');
|
|
472
|
+
};
|
|
473
|
+
const renderResponseSchema = (response) => {
|
|
474
|
+
const schema = response.content?.[0]?.schema;
|
|
475
|
+
if (!schema)
|
|
476
|
+
return null;
|
|
477
|
+
const lines = formatSchemaFull(schema, document, 10);
|
|
478
|
+
const resKey = `res_${response.status}`;
|
|
479
|
+
return renderSchemaBox(lines, resKey);
|
|
217
480
|
};
|
|
218
481
|
// Rendering
|
|
219
|
-
const
|
|
482
|
+
const getVisibleItemsRange = () => {
|
|
483
|
+
let currentHeight = 0;
|
|
484
|
+
let startIndex = scrollOffset;
|
|
485
|
+
let count = 0;
|
|
486
|
+
// Adjust scrollOffset if focused item is out of view (basic check)
|
|
487
|
+
// This is handled by useLayoutEffect, but we rely on scrollOffset here.
|
|
488
|
+
for (let i = startIndex; i < flatItems.length; i++) {
|
|
489
|
+
const itemHeight = getItemHeight(flatItems[i]);
|
|
490
|
+
if (currentHeight + itemHeight > contentHeight) {
|
|
491
|
+
// If the first item itself is larger than contentHeight, we must show it (clipped)
|
|
492
|
+
if (count === 0) {
|
|
493
|
+
count = 1;
|
|
494
|
+
}
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
currentHeight += itemHeight;
|
|
498
|
+
count++;
|
|
499
|
+
}
|
|
500
|
+
return flatItems.slice(startIndex, startIndex + count);
|
|
501
|
+
};
|
|
502
|
+
const visibleItems = getVisibleItemsRange();
|
|
220
503
|
const pathParamCount = endpoint.parameters.filter((p) => p.in === 'path').length;
|
|
221
504
|
const headerParamCount = endpoint.parameters.filter((p) => p.in === 'header').length;
|
|
222
505
|
const queryParamCount = endpoint.parameters.filter((p) => p.in === 'query').length;
|
|
@@ -224,7 +507,7 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
|
|
|
224
507
|
return (React.createElement(Box, { flexDirection: "column", width: "50%", height: totalHeight, borderStyle: "single", borderColor: isFocused ? 'green' : 'gray', paddingX: 1 }, visibleItems.map((item, idx) => {
|
|
225
508
|
const key = `item-${scrollOffset + idx}`;
|
|
226
509
|
if (item.type === 'title') {
|
|
227
|
-
return (React.createElement(Box, { key: key, flexDirection: "column", marginBottom:
|
|
510
|
+
return (React.createElement(Box, { key: key, flexDirection: "column", marginBottom: 0 },
|
|
228
511
|
React.createElement(Box, null,
|
|
229
512
|
React.createElement(Text, { bold: true, color: getMethodColor(item.endpoint.method) },
|
|
230
513
|
"[",
|
|
@@ -236,7 +519,7 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
|
|
|
236
519
|
(item.endpoint.summary || item.endpoint.description) && (React.createElement(Text, { dimColor: true }, item.endpoint.summary || item.endpoint.description))));
|
|
237
520
|
}
|
|
238
521
|
if (item.type === 'section') {
|
|
239
|
-
return (React.createElement(Box, { key: key, marginTop:
|
|
522
|
+
return (React.createElement(Box, { key: key, marginTop: 0, marginBottom: 0 },
|
|
240
523
|
React.createElement(Text, { bold: true, color: "green" }, item.text)));
|
|
241
524
|
}
|
|
242
525
|
if (item.type === 'param') {
|
|
@@ -257,7 +540,7 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
|
|
|
257
540
|
const hasSchema = !!(refInfo &&
|
|
258
541
|
item.param.schema &&
|
|
259
542
|
isReferenceObject(item.param.schema));
|
|
260
|
-
const isExpanded =
|
|
543
|
+
const isExpanded = !collapsedSchemas.has(item.param.name);
|
|
261
544
|
// Command Hint Calculation
|
|
262
545
|
let commandHint = '';
|
|
263
546
|
if (commandMode) {
|
|
@@ -336,27 +619,74 @@ export default function LeftPanel({ endpoint, parameterValues, focusedFieldIndex
|
|
|
336
619
|
"Schema: ",
|
|
337
620
|
refInfo,
|
|
338
621
|
" (Press < to collapse)")),
|
|
339
|
-
renderSchema(item.param.schema.$ref)))));
|
|
622
|
+
React.createElement(Box, { marginLeft: 2 }, renderSchema(item.param.schema.$ref, item.param.name))))));
|
|
340
623
|
}
|
|
341
624
|
if (item.type === 'requestBody') {
|
|
342
625
|
const isFieldFocused = item.globalIndex === focusedFieldIndex && isFocused;
|
|
343
|
-
const isExpanded =
|
|
626
|
+
const isExpanded = !collapsedSchemas.has('__requestBody__');
|
|
344
627
|
const hasSchema = !!endpoint.requestBody?.schema;
|
|
345
|
-
const commandHint = commandMode ? '
|
|
628
|
+
const commandHint = commandMode ? 'rqb1' : '';
|
|
346
629
|
return (React.createElement(Box, { key: key, flexDirection: "column" },
|
|
347
630
|
React.createElement(Box, { flexDirection: "row", backgroundColor: isFieldFocused ? 'green' : undefined, marginLeft: 1 },
|
|
348
|
-
commandMode && (React.createElement(Box, { width:
|
|
631
|
+
commandMode && (React.createElement(Box, { width: 8 },
|
|
349
632
|
React.createElement(Text, { color: "yellow" },
|
|
350
633
|
"[",
|
|
351
634
|
commandHint,
|
|
352
635
|
"]"))),
|
|
353
636
|
React.createElement(Text, { color: isFieldFocused ? 'black' : 'gray' }, item.endpoint.requestBody?.description || 'Request Body Content')),
|
|
354
637
|
hasSchema && !isExpanded && (React.createElement(Box, { marginLeft: 1, marginTop: 0 },
|
|
355
|
-
React.createElement(Text, { color: "yellow", dimColor: true },
|
|
638
|
+
React.createElement(Text, { color: "yellow", dimColor: true },
|
|
639
|
+
"Press > to view ",
|
|
640
|
+
getSchemaName(endpoint.requestBody?.schema) || 'schema',
|
|
641
|
+
' ',
|
|
642
|
+
"schema"))),
|
|
643
|
+
hasSchema && isExpanded && (React.createElement(React.Fragment, null,
|
|
644
|
+
React.createElement(Box, { marginLeft: 1, marginTop: 0 },
|
|
645
|
+
React.createElement(Text, { color: "yellow", dimColor: true },
|
|
646
|
+
"Schema: ",
|
|
647
|
+
getSchemaName(endpoint.requestBody?.schema) || 'unknown',
|
|
648
|
+
" (Press < to collapse)")),
|
|
649
|
+
React.createElement(Box, { marginLeft: 1 }, renderRequestBodySchema())))));
|
|
650
|
+
}
|
|
651
|
+
if (item.type === 'responseItem') {
|
|
652
|
+
const isFieldFocused = item.globalIndex === focusedFieldIndex && isFocused;
|
|
653
|
+
const resKey = `res_${item.response.status}`;
|
|
654
|
+
const isExpanded = !collapsedSchemas.has(resKey);
|
|
655
|
+
const hasSchema = !!item.response.content?.[0]?.schema;
|
|
656
|
+
// Responses 시작 위치 계산 (힌트용)
|
|
657
|
+
const responsesStartIndex = pathParamCount +
|
|
658
|
+
queryParamCount +
|
|
659
|
+
headerParamCount +
|
|
660
|
+
endpoint.parameters.filter((p) => p.in === 'body' || p.in === 'formData').length +
|
|
661
|
+
(endpoint.requestBody && endpoint.parameters.filter((p) => p.in === 'body').length === 0
|
|
662
|
+
? 1
|
|
663
|
+
: 0);
|
|
664
|
+
const rbIndex = item.globalIndex - responsesStartIndex + 1;
|
|
665
|
+
return (React.createElement(Box, { key: key, flexDirection: "column" },
|
|
666
|
+
React.createElement(Box, { flexDirection: "row", backgroundColor: isFieldFocused ? 'green' : undefined, marginLeft: 1 },
|
|
667
|
+
commandMode && (React.createElement(Box, { width: 8 },
|
|
668
|
+
React.createElement(Text, { color: "yellow" },
|
|
669
|
+
"[",
|
|
670
|
+
`rb${rbIndex}`,
|
|
671
|
+
"]"))),
|
|
672
|
+
React.createElement(Text, { color: isFieldFocused ? 'black' : getStatusColor(item.response.status), bold: true }, item.response.status),
|
|
673
|
+
React.createElement(Text, { color: isFieldFocused ? 'black' : 'white' },
|
|
674
|
+
' ',
|
|
675
|
+
item.response.description || '(No description)')),
|
|
676
|
+
hasSchema && !isExpanded && (React.createElement(Box, { marginLeft: 1, marginTop: 0 },
|
|
677
|
+
React.createElement(Text, { color: "yellow", dimColor: true },
|
|
678
|
+
"Press > to view",
|
|
679
|
+
' ',
|
|
680
|
+
getSchemaName(item.response.content?.[0]?.schema) || 'schema',
|
|
681
|
+
" schema"))),
|
|
356
682
|
hasSchema && isExpanded && (React.createElement(React.Fragment, null,
|
|
357
683
|
React.createElement(Box, { marginLeft: 1, marginTop: 0 },
|
|
358
|
-
React.createElement(Text, { color: "yellow", dimColor: true },
|
|
359
|
-
|
|
684
|
+
React.createElement(Text, { color: "yellow", dimColor: true },
|
|
685
|
+
"Schema: ",
|
|
686
|
+
getSchemaName(item.response.content?.[0]?.schema) || 'unknown',
|
|
687
|
+
' ',
|
|
688
|
+
"(Press < to collapse)")),
|
|
689
|
+
React.createElement(Box, { marginLeft: 1 }, renderResponseSchema(item.response))))));
|
|
360
690
|
}
|
|
361
691
|
return null;
|
|
362
692
|
})));
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import React from 'react';
|
|
5
5
|
import { ApiResponse, AppMode } from '../../types/app-state.js';
|
|
6
|
-
import { Endpoint } from '../../types/openapi.js';
|
|
6
|
+
import { Endpoint, OpenAPIDocument } 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
11
|
response: ApiResponse | null;
|
|
12
12
|
endpoint: Endpoint;
|
|
13
|
+
document?: OpenAPIDocument;
|
|
13
14
|
parameterValues: Record<string, string>;
|
|
14
15
|
requestBody: string;
|
|
15
16
|
requestHeaders: Record<string, string>;
|
|
@@ -18,5 +19,5 @@ interface RightPanelProps {
|
|
|
18
19
|
isLoading: boolean;
|
|
19
20
|
commandMode?: boolean;
|
|
20
21
|
}
|
|
21
|
-
export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, response, endpoint, parameterValues, requestBody, requestHeaders, baseURL, mode, isLoading, commandMode, }: RightPanelProps): React.JSX.Element;
|
|
22
|
+
export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, response, endpoint, document, parameterValues, requestBody, requestHeaders, baseURL, mode, isLoading, commandMode, }: RightPanelProps): React.JSX.Element;
|
|
22
23
|
export {};
|