Skip to content

Commit 9f6eec4

Browse files
committed
#446 - Enhance tab management and global hotkeys functionality
1 parent f2fc792 commit 9f6eec4

File tree

5 files changed

+313
-43
lines changed

5 files changed

+313
-43
lines changed
Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,39 @@
11
import { InputTabs } from '@/components/mainWindow/bodyTabs/InputTabs/InputTabs';
22
import { OutputTabs } from '@/components/mainWindow/bodyTabs/OutputTabs';
3+
import { useState, useCallback } from 'react';
4+
import { useGlobalHotkeys } from '@/hooks/useGlobalHotkeys';
35

46
export function MainBody() {
7+
const [activeRequestTab, setActiveRequestTab] = useState<number | undefined>();
8+
const [activeResponseTab, setActiveResponseTab] = useState<number | undefined>();
9+
10+
const handleRequestTabSwitch = useCallback((tabIndex: number) => {
11+
setActiveRequestTab(tabIndex);
12+
setTimeout(() => setActiveRequestTab(undefined), 100);
13+
}, []);
14+
15+
const handleResponseTabSwitch = useCallback((tabIndex: number) => {
16+
setActiveResponseTab(tabIndex);
17+
setTimeout(() => setActiveResponseTab(undefined), 100);
18+
}, []);
19+
20+
useGlobalHotkeys({
21+
onSwitchRequestTab: handleRequestTabSwitch,
22+
onSwitchResponseTab: handleResponseTabSwitch,
23+
});
24+
525
return (
626
<div className="grid h-full grid-rows-[48%_48%] gap-6 xl:grid-cols-2 xl:grid-rows-1">
7-
<InputTabs className="min-h-0 min-w-0" />
8-
<OutputTabs className="min-h-0 min-w-0" />
27+
<InputTabs
28+
className="min-h-0 min-w-0"
29+
activeTab={activeRequestTab}
30+
onTabChange={() => setActiveRequestTab(undefined)}
31+
/>
32+
<OutputTabs
33+
className="min-h-0 min-w-0"
34+
activeTab={activeResponseTab}
35+
onTabChange={() => setActiveResponseTab(undefined)}
36+
/>
937
</div>
1038
);
1139
}

src/renderer/components/mainWindow/MainTopBar.tsx

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,25 @@ import { useResponseActions } from '@/state/responseStore';
1212
import { ArrowRight, Loader2 } from 'lucide-react';
1313
import { showError } from '@/error/errorHandler';
1414
import { REQUEST_MODEL } from '@/lib/monaco/models';
15+
import { useGlobalHotkeys } from '@/hooks/useGlobalHotkeys';
16+
import { NamingModal } from '@/components/sidebar/SidebarRequestList/Nav/Dropdown/modals/NamingModal';
17+
import { Collection } from 'shim/objects/collection';
1518

1619
const httpService = HttpService.instance;
1720
const eventService = RendererEventService.instance;
1821

1922
export function MainTopBar() {
2023
const [hasError, setHasError] = useState(false);
2124
const [isLoading, setIsLoading] = useState(false);
25+
const [modalState, setModalState] = useState({
26+
isOpen: false,
27+
type: 'request' as 'request' | 'folder',
28+
});
2229

2330
const { updateRequest } = useCollectionActions();
2431
const { addResponse } = useResponseActions();
2532
const request = useCollectionStore(selectRequest);
33+
const collection = useCollectionStore((state) => state.collection);
2634
const selectedHttpMethod = request?.method;
2735
const url = request?.url;
2836

@@ -62,57 +70,54 @@ export function MainTopBar() {
6270
useErrorHandler(async () => {
6371
if (request == null) return;
6472

65-
// save request draft with the current editor content
6673
console.info('Saving request:', request);
6774
await eventService.saveRequest(request, REQUEST_MODEL.getValue());
68-
69-
// override existing request with the saved draft
7075
updateRequest(await eventService.saveChanges(request), true);
7176
}),
7277
[request]
7378
);
7479

75-
// useEffect to reset error state when URL and method are valid
80+
const openRequestModal = () => setModalState({ isOpen: true, type: 'request' });
81+
82+
useGlobalHotkeys({
83+
onSendRequest: sendRequest,
84+
onSaveRequest: saveRequest,
85+
onCreateNewRequest: openRequestModal,
86+
});
87+
7688
useEffect(() => {
7789
if (request?.url && request?.method) {
7890
setHasError(false);
7991
}
8092
}, [request]);
8193

82-
useEffect(() => {
83-
const handleKeyDown = (event: KeyboardEvent) => {
84-
//isSaveShortcut is true if save combination is recorded
85-
const isSaveShortcut = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's';
86-
//if save combination is pressed and request is in draft mode, perform save
87-
if (isSaveShortcut && request?.draft) {
88-
event.preventDefault();
89-
saveRequest();
90-
}
91-
};
92-
//add keyboard event listener
93-
window.addEventListener('keydown', handleKeyDown);
94-
//cleanup
95-
return () => {
96-
window.removeEventListener('keydown', handleKeyDown);
97-
};
98-
}, [saveRequest]);
99-
10094
return (
101-
<div className="mb-[24px] flex gap-6">
102-
<div className="relative flex w-full">
103-
<HttpMethodSelect
104-
selectedHttpMethod={selectedHttpMethod}
105-
onHttpMethodChange={handleHttpMethodChange}
106-
/>
107-
108-
<UrlInput url={url} onUrlChange={handleUrlChange} hasError={hasError} />
109-
110-
<SaveButton isDisabled={!request?.draft} onClick={saveRequest} />
95+
<>
96+
<div className="mb-[24px] flex gap-6">
97+
<div className="relative flex w-full">
98+
<HttpMethodSelect
99+
selectedHttpMethod={selectedHttpMethod}
100+
onHttpMethodChange={handleHttpMethodChange}
101+
/>
102+
103+
<UrlInput url={url} onUrlChange={handleUrlChange} hasError={hasError} />
104+
105+
<SaveButton isDisabled={!request?.draft} onClick={saveRequest} />
106+
</div>
107+
108+
<SendButton onClick={sendRequest} disabled={isLoading}>
109+
{isLoading ? <Loader2 className="h-5 w-5 animate-spin" /> : <ArrowRight />}
110+
</SendButton>
111111
</div>
112112

113-
<SendButton onClick={sendRequest} disabled={isLoading}>
114-
{isLoading ? <Loader2 className="h-5 w-5 animate-spin" /> : <ArrowRight />}
115-
</SendButton>
116-
</div>
113+
{modalState.isOpen && (
114+
<NamingModal
115+
isOpen={modalState.isOpen}
116+
trufosObject={collection as Collection}
117+
createType={modalState.type}
118+
setOpen={(open) => setModalState({ isOpen: open, type: modalState.type })}
119+
/>
120+
)}
121+
</>
117122
);
118123
}

src/renderer/components/mainWindow/bodyTabs/InputTabs/InputTabs.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
2-
import { useMemo } from 'react';
2+
import { useMemo, useState, useEffect } from 'react';
33
import { selectRequest, useCollectionStore } from '@/state/collectionStore';
44
import { HeaderTab } from '@/components/mainWindow/bodyTabs/InputTabs/tabs/HeaderTab/HeaderTab';
55
import { BodyTab } from '@/components/mainWindow/bodyTabs/InputTabs/tabs/BodyTab';
@@ -8,10 +8,15 @@ import { AuthorizationTab } from '@/components/mainWindow/bodyTabs/InputTabs/tab
88

99
interface InputTabsProps {
1010
className: string;
11+
activeTab?: number;
12+
onTabChange?: (tabIndex: number) => void;
1113
}
1214

15+
const TAB_VALUES = ['body', 'queryParams', 'headers', 'authorization'];
16+
1317
export function InputTabs(props: Readonly<InputTabsProps>) {
14-
const { className } = props;
18+
const { className, activeTab, onTabChange } = props;
19+
const [currentTab, setCurrentTab] = useState('body');
1520

1621
const headers = useCollectionStore((state) => selectRequest(state).headers);
1722

@@ -20,8 +25,23 @@ export function InputTabs(props: Readonly<InputTabsProps>) {
2025
[headers]
2126
);
2227

28+
// Handle programmatic tab changes
29+
useEffect(() => {
30+
if (activeTab !== undefined && TAB_VALUES[activeTab]) {
31+
setCurrentTab(TAB_VALUES[activeTab]);
32+
}
33+
}, [activeTab]);
34+
35+
const handleTabChange = (value: string) => {
36+
setCurrentTab(value);
37+
const tabIndex = TAB_VALUES.indexOf(value);
38+
if (tabIndex !== -1) {
39+
onTabChange?.(tabIndex);
40+
}
41+
};
42+
2343
return (
24-
<Tabs className={className} defaultValue="body">
44+
<Tabs className={className} value={currentTab} onValueChange={handleTabChange}>
2545
<TabsList>
2646
<TabsTrigger value="body">Body</TabsTrigger>
2747
<TabsTrigger value="queryParams">Params</TabsTrigger>

src/renderer/components/mainWindow/bodyTabs/OutputTabs.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,20 @@ function getContentType(headers?: HttpHeaders) {
4646

4747
interface OutputTabsProps {
4848
className: string;
49+
activeTab?: number;
50+
onTabChange?: (tabIndex: number) => void;
4951
}
5052

51-
export function OutputTabs({ className }: OutputTabsProps) {
53+
const OUTPUT_TAB_VALUES = ['body', 'header'];
54+
55+
export function OutputTabs({ className, activeTab, onTabChange }: OutputTabsProps) {
5256
const { setResponseEditor, formatResponseEditorText } = useResponseActions();
5357
const editor = useResponseStore((state) => state.editor);
5458
const requestId = useCollectionStore((state) => selectRequest(state)?.id);
5559
const response = useResponseStore((state) => selectResponse(state, requestId));
5660

5761
const [editorLanguage, setEditorLanguage] = useState<string | undefined>();
62+
const [currentTab, setCurrentTab] = useState('body');
5863

5964
const mimeType = useMemo(() => {
6065
const contentType = getContentType(response?.headers);
@@ -98,8 +103,23 @@ export function OutputTabs({ className }: OutputTabsProps) {
98103
}
99104
}, [requestId, canFormatResponseBody, formatResponseEditorText]);
100105

106+
// Handle programmatic tab changes
107+
useEffect(() => {
108+
if (activeTab !== undefined && OUTPUT_TAB_VALUES[activeTab]) {
109+
setCurrentTab(OUTPUT_TAB_VALUES[activeTab]);
110+
}
111+
}, [activeTab]);
112+
113+
const handleTabChange = (value: string) => {
114+
setCurrentTab(value);
115+
const tabIndex = OUTPUT_TAB_VALUES.indexOf(value);
116+
if (tabIndex !== -1) {
117+
onTabChange?.(tabIndex);
118+
}
119+
};
120+
101121
return (
102-
<Tabs className={className} defaultValue="body">
122+
<Tabs className={className} value={currentTab} onValueChange={handleTabChange}>
103123
<TabsList className="flex items-center justify-between">
104124
<div className="flex">
105125
<TabsTrigger value="body">Response Body</TabsTrigger>

0 commit comments

Comments
 (0)