Skip to content

Commit 71a5b46

Browse files
feat(web): add request workspace tabs component (#105)
* feat(web): add request workspace tabs component Introduce tabbed request workspace UI with Params, Headers, and Body tabs for the HTTP editor.
1 parent d3d1c92 commit 71a5b46

File tree

6 files changed

+156
-9
lines changed

6 files changed

+156
-9
lines changed

bun.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/web/src/components/editor/EditorWithExecution.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import {
1818
import type { WorkspaceRequest } from '../../sdk';
1919
import { type FileType, getFileType } from '../../utils/fileType';
2020
import { ExecutionDetail } from '../execution/ExecutionDetail';
21+
import {
22+
DEFAULT_REQUEST_WORKSPACE_TAB,
23+
type RequestWorkspaceTabId,
24+
RequestWorkspaceTabs
25+
} from '../request-workspace';
2126
import { ScriptPanel } from '../script';
2227
import { CodeEditor } from './CodeEditor';
2328
import { HttpEditor } from './HttpEditor';
@@ -46,13 +51,23 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
4651
};
4752

4853
const [selectedRequestIndex, setSelectedRequestIndex] = createSignal(0);
54+
const [activeRequestTab, setActiveRequestTab] = createSignal<RequestWorkspaceTabId>(
55+
DEFAULT_REQUEST_WORKSPACE_TAB
56+
);
4957
const [resultsPanelCollapsed, setResultsPanelCollapsed] = createSignal(loadCollapsedState());
5058
const requests = createMemo<WorkspaceRequest[]>(() => {
5159
if (fileType() !== 'http') {
5260
return [];
5361
}
5462
return workspace.requestsByPath()[props.path] ?? [];
5563
});
64+
const selectedRequest = createMemo<WorkspaceRequest | undefined>(() => {
65+
const allRequests = requests();
66+
if (allRequests.length === 0) {
67+
return undefined;
68+
}
69+
return allRequests[selectedRequestIndex()];
70+
});
5671

5772
const saveCollapsedState = (collapsed: boolean) => {
5873
if (typeof localStorage !== 'undefined') {
@@ -74,6 +89,7 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
7489
// Clear previous execution results when switching files
7590
observer.clearExecutions();
7691
setSelectedRequestIndex(0);
92+
setActiveRequestTab(DEFAULT_REQUEST_WORKSPACE_TAB);
7793

7894
if (!path) {
7995
observer.clearScriptOutput();
@@ -184,7 +200,19 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
184200

185201
<div class="flex-1 min-h-0">
186202
<ResizableSplitPane
187-
left={<HttpEditor path={props.path} onExecute={handleHttpExecute} />}
203+
left={
204+
<div class="flex h-full min-h-0 flex-col">
205+
<RequestWorkspaceTabs
206+
activeTab={activeRequestTab()}
207+
onTabChange={setActiveRequestTab}
208+
selectedRequest={selectedRequest()}
209+
requestCount={requests().length}
210+
/>
211+
<div class="flex-1 min-h-0">
212+
<HttpEditor path={props.path} onExecute={handleHttpExecute} />
213+
</div>
214+
</div>
215+
}
188216
right={
189217
<div class="h-full bg-treq-bg dark:bg-treq-dark-bg overflow-hidden">
190218
<Show
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export {
2+
DEFAULT_REQUEST_WORKSPACE_TAB,
3+
isRequestWorkspaceTabId,
4+
REQUEST_WORKSPACE_TABS,
5+
type RequestWorkspaceTabId
6+
} from './model';
7+
export { RequestWorkspaceTabs } from './request-workspace-tabs';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, test } from 'bun:test';
2+
import {
3+
DEFAULT_REQUEST_WORKSPACE_TAB,
4+
isRequestWorkspaceTabId,
5+
REQUEST_WORKSPACE_TABS
6+
} from './model';
7+
8+
describe('request workspace tab model', () => {
9+
test('exports the expected tab order', () => {
10+
expect(REQUEST_WORKSPACE_TABS).toEqual(['params', 'headers', 'body']);
11+
});
12+
13+
test('uses params as the default tab', () => {
14+
expect(DEFAULT_REQUEST_WORKSPACE_TAB).toBe('params');
15+
});
16+
17+
test('validates request workspace tab ids', () => {
18+
expect(isRequestWorkspaceTabId('params')).toBe(true);
19+
expect(isRequestWorkspaceTabId('headers')).toBe(true);
20+
expect(isRequestWorkspaceTabId('body')).toBe(true);
21+
expect(isRequestWorkspaceTabId('response')).toBe(false);
22+
expect(isRequestWorkspaceTabId('')).toBe(false);
23+
expect(isRequestWorkspaceTabId('Params')).toBe(false);
24+
});
25+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const REQUEST_WORKSPACE_TABS = ['params', 'headers', 'body'] as const;
2+
3+
export type RequestWorkspaceTabId = (typeof REQUEST_WORKSPACE_TABS)[number];
4+
5+
export const DEFAULT_REQUEST_WORKSPACE_TAB: RequestWorkspaceTabId = 'params';
6+
7+
const requestWorkspaceTabSet = new Set<RequestWorkspaceTabId>(REQUEST_WORKSPACE_TABS);
8+
9+
export function isRequestWorkspaceTabId(value: string): value is RequestWorkspaceTabId {
10+
return requestWorkspaceTabSet.has(value as RequestWorkspaceTabId);
11+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { For, Match, Show, Switch } from 'solid-js';
2+
import type { WorkspaceRequest } from '../../sdk';
3+
import { REQUEST_WORKSPACE_TABS, type RequestWorkspaceTabId } from './model';
4+
5+
interface RequestWorkspaceTabsProps {
6+
activeTab: RequestWorkspaceTabId;
7+
onTabChange: (tab: RequestWorkspaceTabId) => void;
8+
selectedRequest?: WorkspaceRequest;
9+
requestCount: number;
10+
}
11+
12+
const TAB_LABELS: Record<RequestWorkspaceTabId, string> = {
13+
params: 'Params',
14+
headers: 'Headers',
15+
body: 'Body'
16+
};
17+
18+
export function RequestWorkspaceTabs(props: RequestWorkspaceTabsProps) {
19+
return (
20+
<section
21+
class="border-b border-treq-border-light dark:border-treq-dark-border-light bg-base-100/80"
22+
aria-label="Request workspace details"
23+
>
24+
<div class="flex items-center justify-between gap-3 px-3 pt-2">
25+
<p class="text-xs font-mono uppercase tracking-[0.08em] text-base-content/60">
26+
Request Workspace
27+
</p>
28+
<span class="badge badge-sm border-base-300 bg-base-200/70 font-mono text-[11px]">
29+
{props.requestCount} req
30+
</span>
31+
</div>
32+
33+
<div role="tablist" class="tabs tabs-border px-3 pt-1">
34+
<For each={REQUEST_WORKSPACE_TABS}>
35+
{(tab) => (
36+
<button
37+
type="button"
38+
role="tab"
39+
class="tab tab-sm"
40+
aria-selected={props.activeTab === tab}
41+
classList={{ 'tab-active': props.activeTab === tab }}
42+
onClick={() => props.onTabChange(tab)}
43+
>
44+
{TAB_LABELS[tab]}
45+
</button>
46+
)}
47+
</For>
48+
</div>
49+
50+
<div class="px-3 pb-3 pt-2">
51+
<div class="rounded-box border border-base-300 bg-base-100/70 px-3 py-2 text-sm text-base-content/75">
52+
<Show
53+
when={props.selectedRequest}
54+
fallback={<p>Select a request to view {TAB_LABELS[props.activeTab].toLowerCase()}.</p>}
55+
>
56+
{(request) => (
57+
<Switch>
58+
<Match when={props.activeTab === 'params'}>
59+
<p>Params editor wiring is next for {request().method.toUpperCase()} requests.</p>
60+
</Match>
61+
<Match when={props.activeTab === 'headers'}>
62+
<p>
63+
Headers editor wiring is next for {request().method.toUpperCase()} requests.
64+
</p>
65+
</Match>
66+
<Match when={props.activeTab === 'body'}>
67+
<p>Body editor wiring is next for {request().method.toUpperCase()} requests.</p>
68+
</Match>
69+
</Switch>
70+
)}
71+
</Show>
72+
</div>
73+
</div>
74+
</section>
75+
);
76+
}

0 commit comments

Comments
 (0)