@gomjellie/lazyapi 0.0.3 → 0.0.4
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 +6 -7
- package/dist/components/detail-view/DetailView.js +1 -1
- package/dist/components/detail-view/RightPanel.d.ts +2 -1
- package/dist/components/detail-view/RightPanel.js +33 -22
- package/dist/components/list-view/ListView.js +8 -2
- package/dist/store/appSlice.d.ts +2 -2
- package/dist/store/appSlice.js +7 -1
- package/dist/store/navigationActions.js +113 -15
- package/dist/types/app-state.d.ts +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
# lazyapi
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-
|
|
5
|
-
**⚠️ Work in Progress**
|
|
6
|
-
|
|
7
3
|
<div align="center">
|
|
8
4
|
|
|
9
|
-
A **Vim-style TUI (Terminal User Interface)** tool
|
|
5
|
+
A **Vim-style TUI (Terminal User Interface)** tool
|
|
6
|
+
for exploring Swagger/OpenAPI documents and testing APIs in the terminal
|
|
10
7
|
|
|
11
8
|
[](https://opensource.org/licenses/MIT)
|
|
12
9
|
[](https://nodejs.org)
|
|
13
10
|
|
|
11
|
+

|
|
12
|
+
|
|
14
13
|
</div>
|
|
15
14
|
|
|
16
15
|
---
|
|
@@ -57,7 +56,7 @@ lazyapi openapi.json
|
|
|
57
56
|
# YAML file
|
|
58
57
|
lazyapi swagger.yaml
|
|
59
58
|
|
|
60
|
-
# URL
|
|
59
|
+
# URL
|
|
61
60
|
lazyapi https://api.example.com/openapi.json
|
|
62
61
|
```
|
|
63
62
|
|
|
@@ -83,6 +82,7 @@ lazyapi https://api.example.com/openapi.json
|
|
|
83
82
|
| Key | Description |
|
|
84
83
|
| ---------- | ----------------------------------------------------- |
|
|
85
84
|
| Text input | Enter search query (searches path, description, tags) |
|
|
85
|
+
| `Enter` | Apply search and update results |
|
|
86
86
|
| `Esc` | Cancel search and return to NORMAL mode |
|
|
87
87
|
|
|
88
88
|
### Detail View
|
|
@@ -125,7 +125,6 @@ lazyapi https://api.example.com/openapi.json
|
|
|
125
125
|
|
|
126
126
|
## 📝 Future Plans
|
|
127
127
|
|
|
128
|
-
- [ ] Load OpenAPI documents from URL
|
|
129
128
|
- [ ] Multiple environment support (dev, staging, prod)
|
|
130
129
|
- [ ] Request history storage
|
|
131
130
|
- [ ] Favorites feature
|
|
@@ -261,6 +261,6 @@ export default function DetailView() {
|
|
|
261
261
|
dispatch(detailSetPanel('request'));
|
|
262
262
|
dispatch(detailSetRequestTab('body'));
|
|
263
263
|
}, onSetMode: (m) => dispatch(setMode(m)) }),
|
|
264
|
-
React.createElement(RightPanel, { activeRequestTab: activeRequestTab, activeResponseTab: activeResponseTab, activePanel: activePanel, response: detail.response, endpoint: endpoint, document: document, parameterValues: detail.parameterValues, requestBody: detail.requestBody, requestHeaders: detail.headers, baseURL: baseURL, mode: mode, isLoading: loading, commandMode: mode === 'COMMAND' })),
|
|
264
|
+
React.createElement(RightPanel, { activeRequestTab: activeRequestTab, activeResponseTab: activeResponseTab, activePanel: activePanel, subFocus: detail.subFocus, response: detail.response, endpoint: endpoint, document: document, parameterValues: detail.parameterValues, requestBody: detail.requestBody, requestHeaders: detail.headers, baseURL: baseURL, mode: mode, isLoading: loading, commandMode: mode === 'COMMAND' })),
|
|
265
265
|
React.createElement(KeybindingFooter, { bindings: keyBindings, mode: mode, commandInput: commandInput, onCommandChange: (value) => dispatch(setCommandInput(value)), onCommandSubmit: handleCommandSubmit })));
|
|
266
266
|
}
|
|
@@ -8,6 +8,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' | 'CONTENT';
|
|
11
12
|
response: ApiResponse | null;
|
|
12
13
|
endpoint: Endpoint;
|
|
13
14
|
document?: OpenAPIDocument;
|
|
@@ -19,5 +20,5 @@ interface RightPanelProps {
|
|
|
19
20
|
isLoading: boolean;
|
|
20
21
|
commandMode?: boolean;
|
|
21
22
|
}
|
|
22
|
-
export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, response, endpoint, document, parameterValues, requestBody, requestHeaders, baseURL, mode, isLoading, commandMode, }: RightPanelProps): React.JSX.Element;
|
|
23
|
+
export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, subFocus, response, endpoint, document, parameterValues, requestBody, requestHeaders, baseURL, mode, isLoading, commandMode, }: RightPanelProps): React.JSX.Element;
|
|
23
24
|
export {};
|
|
@@ -5,7 +5,7 @@ import React, { useState, useEffect, useMemo } from 'react';
|
|
|
5
5
|
import { Box, Text, useStdout, useStdin } from 'ink';
|
|
6
6
|
import { useKeyPress } from '../../hooks/useKeyPress.js';
|
|
7
7
|
import { useAppDispatch } from '../../store/hooks.js';
|
|
8
|
-
import { detailSetPanel, detailSetBody, setMode } from '../../store/appSlice.js';
|
|
8
|
+
import { detailSetPanel, detailSetBody, detailSetSubFocus, setMode } from '../../store/appSlice.js';
|
|
9
9
|
import { spawnSync } from 'child_process';
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import os from 'os';
|
|
@@ -27,11 +27,14 @@ const TabHeader = ({ tabs, active, isFocused, shortcutPrefix, commandMode }) =>
|
|
|
27
27
|
showHint = true;
|
|
28
28
|
hintText = `[${index + 1}] `;
|
|
29
29
|
}
|
|
30
|
+
const isActive = t.id === active;
|
|
31
|
+
const showIndicator = isFocused && isActive;
|
|
30
32
|
return (React.createElement(Box, { key: t.id },
|
|
31
33
|
showHint && React.createElement(Text, { color: "yellow" }, hintText),
|
|
32
|
-
React.createElement(Text, {
|
|
34
|
+
React.createElement(Text, { color: "yellow" }, showIndicator ? '>' : ' '),
|
|
35
|
+
React.createElement(Text, { bold: isActive, underline: showIndicator, color: isActive ? 'black' : 'gray', backgroundColor: isActive ? 'green' : undefined }, t.label + ' ')));
|
|
33
36
|
})));
|
|
34
|
-
export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, response, endpoint, document, parameterValues, requestBody, requestHeaders, baseURL, mode, isLoading, commandMode = false, }) {
|
|
37
|
+
export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, subFocus, response, endpoint, document, parameterValues, requestBody, requestHeaders, baseURL, mode, isLoading, commandMode = false, }) {
|
|
35
38
|
const { stdout } = useStdout();
|
|
36
39
|
const { isRawModeSupported, setRawMode } = useStdin();
|
|
37
40
|
const dispatch = useAppDispatch();
|
|
@@ -92,14 +95,20 @@ export default function RightPanel({ activeRequestTab, activeResponseTab, active
|
|
|
92
95
|
else {
|
|
93
96
|
// 스크롤이 끝에 도달하면 Response 패널로 이동
|
|
94
97
|
dispatch(detailSetPanel('response'));
|
|
98
|
+
dispatch(detailSetSubFocus('TITLE'));
|
|
95
99
|
return prev;
|
|
96
100
|
}
|
|
97
101
|
});
|
|
98
102
|
},
|
|
99
103
|
k: () => {
|
|
100
|
-
setReqScroll((prev) =>
|
|
104
|
+
setReqScroll((prev) => {
|
|
105
|
+
if (prev > 0)
|
|
106
|
+
return prev - 1;
|
|
107
|
+
dispatch(detailSetSubFocus('TABS'));
|
|
108
|
+
return 0;
|
|
109
|
+
});
|
|
101
110
|
},
|
|
102
|
-
}, mode === 'NORMAL' && activePanel === 'request');
|
|
111
|
+
}, mode === 'NORMAL' && activePanel === 'request' && subFocus === 'CONTENT');
|
|
103
112
|
const openExternalEditor = () => {
|
|
104
113
|
try {
|
|
105
114
|
const tempFile = path.join(os.tmpdir(), `lazyapi-body-${Date.now()}.json`);
|
|
@@ -206,17 +215,13 @@ export default function RightPanel({ activeRequestTab, activeResponseTab, active
|
|
|
206
215
|
},
|
|
207
216
|
k: () => {
|
|
208
217
|
setResScroll((prev) => {
|
|
209
|
-
if (prev > 0)
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// 스크롤이 맨 위에 도달하면 Request 패널로 이동
|
|
214
|
-
dispatch(detailSetPanel('request'));
|
|
215
|
-
return prev;
|
|
216
|
-
}
|
|
218
|
+
if (prev > 0)
|
|
219
|
+
return prev - 1;
|
|
220
|
+
dispatch(detailSetSubFocus('TABS'));
|
|
221
|
+
return 0;
|
|
217
222
|
});
|
|
218
223
|
},
|
|
219
|
-
}, mode === 'NORMAL' && activePanel === 'response');
|
|
224
|
+
}, mode === 'NORMAL' && activePanel === 'response' && subFocus === 'CONTENT');
|
|
220
225
|
// 렌더링 헬퍼
|
|
221
226
|
const renderLines = (lines, offset, limit) => {
|
|
222
227
|
const visible = lines.slice(offset, offset + limit);
|
|
@@ -314,17 +319,23 @@ export default function RightPanel({ activeRequestTab, activeResponseTab, active
|
|
|
314
319
|
];
|
|
315
320
|
return (React.createElement(Box, { flexDirection: "column", width: "50%", marginLeft: 1 },
|
|
316
321
|
React.createElement(Box, { flexDirection: "column", height: requestBoxHeight, borderStyle: "single", borderColor: activePanel === 'request' ? 'green' : 'gray', paddingX: 1 },
|
|
317
|
-
React.createElement(Box, { justifyContent: "space-between", height: 1 },
|
|
318
|
-
React.createElement(
|
|
322
|
+
React.createElement(Box, { justifyContent: "space-between", height: 1, marginBottom: 0 },
|
|
323
|
+
React.createElement(Box, null,
|
|
324
|
+
React.createElement(Text, { color: activePanel === 'request' && subFocus === 'TITLE' ? 'yellow' : 'white', bold: activePanel === 'request' && subFocus === 'TITLE' },
|
|
325
|
+
activePanel === 'request' && subFocus === 'TITLE' ? '>' : ' ',
|
|
326
|
+
"Request")),
|
|
319
327
|
React.createElement(Box, { width: 15 },
|
|
320
328
|
React.createElement(Text, { color: "gray" }, reqTotalLines > contentHeight
|
|
321
329
|
? `${reqScroll + 1}-${Math.min(reqScroll + contentHeight, reqTotalLines)}/${reqTotalLines}`
|
|
322
330
|
: ' '))),
|
|
323
|
-
React.createElement(TabHeader, { tabs: reqTabs, active: activeRequestTab, isFocused: activePanel === 'request', shortcutPrefix: "rq", commandMode: commandMode }),
|
|
324
|
-
React.createElement(Box, { flexGrow: 1, flexDirection: "column", overflow: "hidden" }, renderRequestContent())),
|
|
331
|
+
React.createElement(TabHeader, { tabs: reqTabs, active: activeRequestTab, isFocused: activePanel === 'request' && subFocus === 'TABS', shortcutPrefix: "rq", commandMode: commandMode }),
|
|
332
|
+
React.createElement(Box, { flexGrow: 1, flexDirection: "column", overflow: "hidden", borderStyle: "single", borderColor: activePanel === 'request' && subFocus === 'CONTENT' ? 'green' : 'gray', paddingX: 1 }, renderRequestContent())),
|
|
325
333
|
React.createElement(Box, { flexDirection: "column", height: responseBoxHeight, borderStyle: "single", borderColor: activePanel === 'response' ? 'green' : 'gray', paddingX: 1 },
|
|
326
|
-
React.createElement(Box, { justifyContent: "space-between", height: 1 },
|
|
327
|
-
React.createElement(
|
|
334
|
+
React.createElement(Box, { justifyContent: "space-between", height: 1, marginBottom: 0 },
|
|
335
|
+
React.createElement(Box, null,
|
|
336
|
+
React.createElement(Text, { color: activePanel === 'response' && subFocus === 'TITLE' ? 'yellow' : 'white', bold: activePanel === 'response' && subFocus === 'TITLE' },
|
|
337
|
+
activePanel === 'response' && subFocus === 'TITLE' ? '>' : ' ',
|
|
338
|
+
"Response")),
|
|
328
339
|
React.createElement(Box, { width: 30, flexDirection: "row", justifyContent: "flex-end" },
|
|
329
340
|
response ? (React.createElement(Text, { color: response.status < 300 ? 'green' : 'red' },
|
|
330
341
|
response.status,
|
|
@@ -334,6 +345,6 @@ export default function RightPanel({ activeRequestTab, activeResponseTab, active
|
|
|
334
345
|
React.createElement(Text, { color: "gray" }, resTotalLines > contentHeight
|
|
335
346
|
? `${resScroll + 1}-${Math.min(resScroll + contentHeight, resTotalLines)}/${resTotalLines}`
|
|
336
347
|
: ' ')))),
|
|
337
|
-
React.createElement(TabHeader, { tabs: resTabs, active: activeResponseTab, isFocused: activePanel === 'response', shortcutPrefix: "rs", commandMode: commandMode }),
|
|
338
|
-
React.createElement(Box, { flexGrow: 1, flexDirection: "column", overflow: "hidden" }, renderResponseContent()))));
|
|
348
|
+
React.createElement(TabHeader, { tabs: resTabs, active: activeResponseTab, isFocused: activePanel === 'response' && subFocus === 'TABS', shortcutPrefix: "rs", commandMode: commandMode }),
|
|
349
|
+
React.createElement(Box, { flexGrow: 1, flexDirection: "column", overflow: "hidden", borderStyle: "single", borderColor: activePanel === 'response' && subFocus === 'CONTENT' ? 'green' : 'gray', paddingX: 1 }, renderResponseContent()))));
|
|
339
350
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* API 목록 화면
|
|
3
3
|
*/
|
|
4
4
|
import React, { useMemo } from 'react';
|
|
5
|
-
import { Box, useStdout } from 'ink';
|
|
5
|
+
import { Box, Text, useStdout } from 'ink';
|
|
6
6
|
import { useAppSelector, useAppDispatch } from '../../store/hooks.js';
|
|
7
7
|
import { setMode, listSelect, listSetMethodFilter, listSetTag, listSetFocus, listSelectTag, listSelectMethod, listSetSearch, listSetTagSearch, } from '../../store/appSlice.js';
|
|
8
8
|
import { navigateUp, navigateDown, navigateLeft, navigateRight, } from '../../store/navigationActions.js';
|
|
@@ -266,7 +266,13 @@ export default function ListView() {
|
|
|
266
266
|
React.createElement(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden" },
|
|
267
267
|
React.createElement(Box, null,
|
|
268
268
|
React.createElement(Explorer, { tags: tags, selectedTagIndex: list.selectedTagIndex, activeTag: list.activeTag, isFocused: list.focus === 'TAGS', searchQuery: list.tagSearchQuery, isSearchMode: mode === 'TAG_SEARCH', commandMode: mode === 'COMMAND', onSearchChange: (query) => dispatch(listSetTagSearch(query)) })),
|
|
269
|
-
React.createElement(Box, { flexDirection: "column", flexGrow: 1, marginLeft: 1, borderStyle: "single", borderColor: list.focus === 'LIST' || list.focus === 'METHODS'
|
|
269
|
+
React.createElement(Box, { flexDirection: "column", flexGrow: 1, marginLeft: 1, borderStyle: "single", borderColor: list.focus === 'LIST' || list.focus === 'METHODS' || list.focus === 'TITLE'
|
|
270
|
+
? 'green'
|
|
271
|
+
: 'gray', paddingX: 1, paddingBottom: 1, paddingTop: 0 },
|
|
272
|
+
React.createElement(Box, { marginBottom: 0 },
|
|
273
|
+
React.createElement(Text, { color: list.focus === 'TITLE' ? 'yellow' : 'white', bold: list.focus === 'TITLE' },
|
|
274
|
+
list.focus === 'TITLE' ? '>' : ' ',
|
|
275
|
+
"Endpoints")),
|
|
270
276
|
React.createElement(Box, { flexDirection: "row", alignItems: "center", gap: 2 },
|
|
271
277
|
React.createElement(MethodsBar, { activeMethod: list.activeMethod, commandMode: mode === 'COMMAND', isFocused: list.focus === 'METHODS', focusedIndex: list.focusedMethodIndex, onMethodChange: (method) => dispatch(listSetMethodFilter(method)) })),
|
|
272
278
|
React.createElement(SearchBar, { query: list.searchQuery, onQueryChange: (query) => dispatch(listSetSearch(query)), isSearchMode: mode === 'SEARCH' }),
|
package/dist/store/appSlice.d.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { AppState, AppMode, Screen, ApiResponse } from '../types/app-state.js';
|
|
5
5
|
import { Endpoint, HttpMethod, OpenAPIDocument } from '../types/openapi.js';
|
|
6
|
-
export declare const setDocument: import("@reduxjs/toolkit").ActionCreatorWithPayload<OpenAPIDocument, "app/setDocument">, setEndpoints: import("@reduxjs/toolkit").ActionCreatorWithPayload<Endpoint[], "app/setEndpoints">, setScreen: import("@reduxjs/toolkit").ActionCreatorWithPayload<Screen, "app/setScreen">, setMode: import("@reduxjs/toolkit").ActionCreatorWithPayload<AppMode, "app/setMode">, setCommandInput: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/setCommandInput">, setNavigationCount: import("@reduxjs/toolkit").ActionCreatorWithPayload<number | null, "app/setNavigationCount">, setError: import("@reduxjs/toolkit").ActionCreatorWithPayload<string | null, "app/setError">, setLoading: import("@reduxjs/toolkit").ActionCreatorWithPayload<boolean, "app/setLoading">, listSelect: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listSelect">, listSetMethodFilter: import("@reduxjs/toolkit").ActionCreatorWithPayload<HttpMethod | "ALL", "app/listSetMethodFilter">, listSetTag: import("@reduxjs/toolkit").ActionCreatorWithPayload<string | null, "app/listSetTag">, listSetFocus: import("@reduxjs/toolkit").ActionCreatorWithPayload<"LIST" | "TAGS" | "METHODS", "app/listSetFocus">, listSelectTag: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listSelectTag">, listSelectMethod: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listSelectMethod">, listSetSearch: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/listSetSearch">, listSetTagSearch: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/listSetTagSearch">, listScroll: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listScroll">, detailSet: import("@reduxjs/toolkit").ActionCreatorWithPayload<Endpoint, "app/detailSet">, detailSetFocus: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/detailSetFocus">, detailSetParam: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
6
|
+
export declare const setDocument: import("@reduxjs/toolkit").ActionCreatorWithPayload<OpenAPIDocument, "app/setDocument">, setEndpoints: import("@reduxjs/toolkit").ActionCreatorWithPayload<Endpoint[], "app/setEndpoints">, setScreen: import("@reduxjs/toolkit").ActionCreatorWithPayload<Screen, "app/setScreen">, setMode: import("@reduxjs/toolkit").ActionCreatorWithPayload<AppMode, "app/setMode">, setCommandInput: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/setCommandInput">, setNavigationCount: import("@reduxjs/toolkit").ActionCreatorWithPayload<number | null, "app/setNavigationCount">, setError: import("@reduxjs/toolkit").ActionCreatorWithPayload<string | null, "app/setError">, setLoading: import("@reduxjs/toolkit").ActionCreatorWithPayload<boolean, "app/setLoading">, listSelect: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listSelect">, listSetMethodFilter: import("@reduxjs/toolkit").ActionCreatorWithPayload<HttpMethod | "ALL", "app/listSetMethodFilter">, listSetTag: import("@reduxjs/toolkit").ActionCreatorWithPayload<string | null, "app/listSetTag">, listSetFocus: import("@reduxjs/toolkit").ActionCreatorWithPayload<"LIST" | "TAGS" | "METHODS" | "TITLE", "app/listSetFocus">, listSelectTag: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listSelectTag">, listSelectMethod: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listSelectMethod">, listSetSearch: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/listSetSearch">, listSetTagSearch: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/listSetTagSearch">, listScroll: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/listScroll">, detailSet: import("@reduxjs/toolkit").ActionCreatorWithPayload<Endpoint, "app/detailSet">, detailSetFocus: import("@reduxjs/toolkit").ActionCreatorWithPayload<number, "app/detailSetFocus">, detailSetParam: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
7
7
|
key: string;
|
|
8
8
|
value: string;
|
|
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">, detailSetResponse: import("@reduxjs/toolkit").ActionCreatorWithPayload<ApiResponse | null, "app/detailSetResponse">, reset: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"app/reset">;
|
|
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" | "CONTENT", "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
|
@@ -132,6 +132,7 @@ const appSlice = createSlice({
|
|
|
132
132
|
activePanel: 'left',
|
|
133
133
|
activeRequestTab: 'headers',
|
|
134
134
|
activeResponseTab: 'body',
|
|
135
|
+
subFocus: 'TABS',
|
|
135
136
|
response: null,
|
|
136
137
|
};
|
|
137
138
|
},
|
|
@@ -170,6 +171,11 @@ const appSlice = createSlice({
|
|
|
170
171
|
state.detail.activeResponseTab = action.payload;
|
|
171
172
|
}
|
|
172
173
|
},
|
|
174
|
+
detailSetSubFocus: (state, action) => {
|
|
175
|
+
if (state.detail) {
|
|
176
|
+
state.detail.subFocus = action.payload;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
173
179
|
detailSetResponse: (state, action) => {
|
|
174
180
|
if (state.detail) {
|
|
175
181
|
state.detail.response = action.payload;
|
|
@@ -178,5 +184,5 @@ const appSlice = createSlice({
|
|
|
178
184
|
reset: () => initialState,
|
|
179
185
|
},
|
|
180
186
|
});
|
|
181
|
-
export const { setDocument, setEndpoints, setScreen, setMode, setCommandInput, setNavigationCount, setError, setLoading, listSelect, listSetMethodFilter, listSetTag, listSetFocus, listSelectTag, listSelectMethod, listSetSearch, listSetTagSearch, listScroll, detailSet, detailSetFocus, detailSetParam, detailSetBody, detailSetHeader, detailSetPanel, detailSetRequestTab, detailSetResponseTab, detailSetResponse, reset, } = appSlice.actions;
|
|
187
|
+
export const { setDocument, setEndpoints, setScreen, setMode, setCommandInput, setNavigationCount, setError, setLoading, listSelect, listSetMethodFilter, listSetTag, listSetFocus, listSelectTag, listSelectMethod, listSetSearch, listSetTagSearch, listScroll, detailSet, detailSetFocus, detailSetParam, detailSetBody, detailSetHeader, detailSetPanel, detailSetRequestTab, detailSetResponseTab, detailSetSubFocus, detailSetResponse, reset, } = appSlice.actions;
|
|
182
188
|
export default appSlice.reducer;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { listSetFocus, listSelectMethod, listSetMethodFilter, listSelect, listSelectTag, detailSetFocus, detailSetPanel, } from './appSlice.js';
|
|
1
|
+
import { listSetFocus, listSelectMethod, listSetMethodFilter, listSelect, listSelectTag, detailSetFocus, detailSetPanel, detailSetSubFocus, detailSetRequestTab, detailSetResponseTab, } from './appSlice.js';
|
|
2
2
|
const METHODS = ['ALL', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
3
|
+
const REQUEST_TABS = ['headers', 'body', 'query'];
|
|
4
|
+
const RESPONSE_TABS = ['body', 'headers', 'cookies', 'curl'];
|
|
3
5
|
// k Key (Up)
|
|
4
6
|
export const navigateUp = () => (dispatch, getState) => {
|
|
5
7
|
const { app } = getState();
|
|
@@ -9,8 +11,11 @@ export const navigateUp = () => (dispatch, getState) => {
|
|
|
9
11
|
const prevIndex = Math.max(list.selectedTagIndex - 1, 0);
|
|
10
12
|
dispatch(listSelectTag(prevIndex));
|
|
11
13
|
}
|
|
14
|
+
else if (list.focus === 'TITLE') {
|
|
15
|
+
// No action
|
|
16
|
+
}
|
|
12
17
|
else if (list.focus === 'METHODS') {
|
|
13
|
-
|
|
18
|
+
dispatch(listSetFocus('TITLE'));
|
|
14
19
|
}
|
|
15
20
|
else {
|
|
16
21
|
// LIST
|
|
@@ -28,7 +33,22 @@ export const navigateUp = () => (dispatch, getState) => {
|
|
|
28
33
|
const prevIndex = Math.max(detail.focusedFieldIndex - 1, 0);
|
|
29
34
|
dispatch(detailSetFocus(prevIndex));
|
|
30
35
|
}
|
|
31
|
-
|
|
36
|
+
else if (detail.activePanel === 'request') {
|
|
37
|
+
if (detail.subFocus === 'TABS') {
|
|
38
|
+
dispatch(detailSetSubFocus('TITLE'));
|
|
39
|
+
}
|
|
40
|
+
// CONTENT: Handled by component (scroll)
|
|
41
|
+
}
|
|
42
|
+
else if (detail.activePanel === 'response') {
|
|
43
|
+
if (detail.subFocus === 'TITLE') {
|
|
44
|
+
dispatch(detailSetPanel('request'));
|
|
45
|
+
dispatch(detailSetSubFocus('CONTENT'));
|
|
46
|
+
}
|
|
47
|
+
else if (detail.subFocus === 'TABS') {
|
|
48
|
+
dispatch(detailSetSubFocus('TITLE'));
|
|
49
|
+
}
|
|
50
|
+
// CONTENT: Handled by component (scroll)
|
|
51
|
+
}
|
|
32
52
|
}
|
|
33
53
|
};
|
|
34
54
|
// j Key (Down)
|
|
@@ -37,14 +57,20 @@ export const navigateDown = () => (dispatch, getState) => {
|
|
|
37
57
|
const { screen, list, detail, endpoints } = app;
|
|
38
58
|
if (screen === 'LIST') {
|
|
39
59
|
if (list.focus === 'TAGS') {
|
|
40
|
-
// 태그 목록 계산
|
|
41
|
-
const
|
|
42
|
-
endpoints.forEach((ep) => ep.tags?.forEach((tag) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
// 태그 목록 계산
|
|
61
|
+
const uniqueTags = new Set();
|
|
62
|
+
endpoints.forEach((ep) => ep.tags?.forEach((tag) => uniqueTags.add(tag)));
|
|
63
|
+
const sortedTags = Array.from(uniqueTags).sort();
|
|
64
|
+
const allTags = ['Overview', ...sortedTags];
|
|
65
|
+
// 검색어 필터링 적용
|
|
66
|
+
const filteredTags = list.tagSearchQuery
|
|
67
|
+
? allTags.filter((tag) => tag.toLowerCase().includes(list.tagSearchQuery.toLowerCase()))
|
|
68
|
+
: allTags;
|
|
69
|
+
const nextIndex = Math.min(list.selectedTagIndex + 1, filteredTags.length - 1);
|
|
70
|
+
dispatch(listSelectTag(nextIndex));
|
|
71
|
+
}
|
|
72
|
+
else if (list.focus === 'TITLE') {
|
|
73
|
+
dispatch(listSetFocus('METHODS'));
|
|
48
74
|
}
|
|
49
75
|
else if (list.focus === 'METHODS') {
|
|
50
76
|
dispatch(listSetFocus('LIST'));
|
|
@@ -74,19 +100,40 @@ export const navigateDown = () => (dispatch, getState) => {
|
|
|
74
100
|
const nextIndex = Math.min(detail.focusedFieldIndex + 1, totalFields - 1);
|
|
75
101
|
dispatch(detailSetFocus(nextIndex));
|
|
76
102
|
}
|
|
103
|
+
else if (detail.activePanel === 'request') {
|
|
104
|
+
if (detail.subFocus === 'TITLE') {
|
|
105
|
+
dispatch(detailSetSubFocus('TABS'));
|
|
106
|
+
}
|
|
107
|
+
else if (detail.subFocus === 'TABS') {
|
|
108
|
+
dispatch(detailSetSubFocus('CONTENT'));
|
|
109
|
+
}
|
|
110
|
+
// CONTENT: Handled by component (scroll) -> Move to Response
|
|
111
|
+
}
|
|
112
|
+
else if (detail.activePanel === 'response') {
|
|
113
|
+
if (detail.subFocus === 'TITLE') {
|
|
114
|
+
dispatch(detailSetSubFocus('TABS'));
|
|
115
|
+
}
|
|
116
|
+
else if (detail.subFocus === 'TABS') {
|
|
117
|
+
dispatch(detailSetSubFocus('CONTENT'));
|
|
118
|
+
}
|
|
119
|
+
// CONTENT: Handled by component (scroll)
|
|
120
|
+
}
|
|
77
121
|
}
|
|
78
122
|
};
|
|
79
123
|
// h Key (Left)
|
|
80
124
|
export const navigateLeft = () => (dispatch, getState) => {
|
|
81
125
|
const { app } = getState();
|
|
82
|
-
const { screen, list } = app;
|
|
126
|
+
const { screen, list, detail } = app;
|
|
83
127
|
if (screen === 'LIST') {
|
|
84
128
|
if (list.focus === 'LIST') {
|
|
85
129
|
dispatch(listSetFocus('TAGS'));
|
|
86
130
|
}
|
|
131
|
+
else if (list.focus === 'TITLE') {
|
|
132
|
+
dispatch(listSetFocus('TAGS'));
|
|
133
|
+
}
|
|
87
134
|
else if (list.focus === 'METHODS') {
|
|
88
135
|
if (list.focusedMethodIndex === 0) {
|
|
89
|
-
dispatch(listSetFocus('
|
|
136
|
+
dispatch(listSetFocus('TITLE'));
|
|
90
137
|
}
|
|
91
138
|
else {
|
|
92
139
|
const prevIndex = Math.max(list.focusedMethodIndex - 1, 0);
|
|
@@ -95,8 +142,34 @@ export const navigateLeft = () => (dispatch, getState) => {
|
|
|
95
142
|
}
|
|
96
143
|
}
|
|
97
144
|
}
|
|
98
|
-
else if (screen === 'DETAIL') {
|
|
99
|
-
|
|
145
|
+
else if (screen === 'DETAIL' && detail) {
|
|
146
|
+
if (detail.activePanel === 'left') {
|
|
147
|
+
// Already at left, do nothing or go back to list (handled by u key usually)
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
if (detail.subFocus === 'TITLE' || detail.subFocus === 'CONTENT') {
|
|
151
|
+
dispatch(detailSetPanel('left'));
|
|
152
|
+
}
|
|
153
|
+
else if (detail.subFocus === 'TABS') {
|
|
154
|
+
const tabs = detail.activePanel === 'request' ? REQUEST_TABS : RESPONSE_TABS;
|
|
155
|
+
const currentTab = detail.activePanel === 'request'
|
|
156
|
+
? detail.activeRequestTab
|
|
157
|
+
: detail.activeResponseTab;
|
|
158
|
+
const index = tabs.indexOf(currentTab);
|
|
159
|
+
if (index === 0) {
|
|
160
|
+
dispatch(detailSetSubFocus('TITLE'));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const prevTab = tabs[index - 1];
|
|
164
|
+
if (detail.activePanel === 'request') {
|
|
165
|
+
dispatch(detailSetRequestTab(prevTab));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
dispatch(detailSetResponseTab(prevTab));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
100
173
|
}
|
|
101
174
|
};
|
|
102
175
|
// l Key (Right)
|
|
@@ -109,6 +182,9 @@ export const navigateRight = () => (dispatch, getState) => {
|
|
|
109
182
|
dispatch(listSelectTag(list.selectedTagIndex)); // 현재 태그 선택 유지하며 포커스만 이동
|
|
110
183
|
dispatch(listSetFocus('LIST'));
|
|
111
184
|
}
|
|
185
|
+
else if (list.focus === 'TITLE') {
|
|
186
|
+
dispatch(listSetFocus('METHODS'));
|
|
187
|
+
}
|
|
112
188
|
else if (list.focus === 'LIST') {
|
|
113
189
|
const nextIndex = (list.focusedMethodIndex + 1) % METHODS.length;
|
|
114
190
|
dispatch(listSetFocus('METHODS'));
|
|
@@ -124,6 +200,28 @@ export const navigateRight = () => (dispatch, getState) => {
|
|
|
124
200
|
else if (screen === 'DETAIL' && detail) {
|
|
125
201
|
if (detail.activePanel === 'left') {
|
|
126
202
|
dispatch(detailSetPanel('request'));
|
|
203
|
+
dispatch(detailSetSubFocus('TITLE'));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
if (detail.subFocus === 'TITLE') {
|
|
207
|
+
dispatch(detailSetSubFocus('TABS'));
|
|
208
|
+
}
|
|
209
|
+
else if (detail.subFocus === 'TABS') {
|
|
210
|
+
const tabs = detail.activePanel === 'request' ? REQUEST_TABS : RESPONSE_TABS;
|
|
211
|
+
const currentTab = detail.activePanel === 'request'
|
|
212
|
+
? detail.activeRequestTab
|
|
213
|
+
: detail.activeResponseTab;
|
|
214
|
+
const index = tabs.indexOf(currentTab);
|
|
215
|
+
const nextIndex = (index + 1) % tabs.length;
|
|
216
|
+
const nextTab = tabs[nextIndex];
|
|
217
|
+
if (detail.activePanel === 'request') {
|
|
218
|
+
dispatch(detailSetRequestTab(nextTab));
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
dispatch(detailSetResponseTab(nextTab));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// CONTENT: Do nothing or handle specific logic
|
|
127
225
|
}
|
|
128
226
|
}
|
|
129
227
|
};
|
|
@@ -21,7 +21,7 @@ export interface ListViewState {
|
|
|
21
21
|
filteredEndpoints: Endpoint[];
|
|
22
22
|
activeMethod: HttpMethod | 'ALL';
|
|
23
23
|
activeTag: string | null;
|
|
24
|
-
focus: 'TAGS' | 'LIST' | 'METHODS';
|
|
24
|
+
focus: 'TAGS' | 'LIST' | 'METHODS' | 'TITLE';
|
|
25
25
|
selectedTagIndex: number;
|
|
26
26
|
focusedMethodIndex: number;
|
|
27
27
|
searchQuery: string;
|
|
@@ -41,6 +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' | 'CONTENT';
|
|
44
45
|
response: ApiResponse | null;
|
|
45
46
|
}
|
|
46
47
|
export interface ApiResponse {
|
|
@@ -81,7 +82,7 @@ export type AppAction = {
|
|
|
81
82
|
payload: string | null;
|
|
82
83
|
} | {
|
|
83
84
|
type: 'LIST_SET_FOCUS';
|
|
84
|
-
payload: 'TAGS' | 'LIST' | 'METHODS';
|
|
85
|
+
payload: 'TAGS' | 'LIST' | 'METHODS' | 'TITLE';
|
|
85
86
|
} | {
|
|
86
87
|
type: 'LIST_SELECT_TAG';
|
|
87
88
|
payload: number;
|
|
@@ -127,6 +128,9 @@ export type AppAction = {
|
|
|
127
128
|
} | {
|
|
128
129
|
type: 'DETAIL_SET_RESPONSE_TAB';
|
|
129
130
|
payload: 'body' | 'headers' | 'cookies' | 'curl';
|
|
131
|
+
} | {
|
|
132
|
+
type: 'DETAIL_SET_SUB_FOCUS';
|
|
133
|
+
payload: 'TITLE' | 'TABS' | 'CONTENT';
|
|
130
134
|
} | {
|
|
131
135
|
type: 'DETAIL_SET_RESPONSE';
|
|
132
136
|
payload: ApiResponse | null;
|