Skip to content

Commit 0bb0b57

Browse files
feat(web): add request headres draft controller (#107)
* feat(web): add request headres draft controller
1 parent 77c6a0d commit 0bb0b57

File tree

9 files changed

+1330
-138
lines changed

9 files changed

+1330
-138
lines changed

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
DEFAULT_REQUEST_WORKSPACE_TAB,
2323
type RequestWorkspaceTabId,
2424
RequestWorkspaceTabs,
25+
useRequestHeaderDraftController,
2526
useRequestParseDetails
2627
} from '../request-workspace';
2728
import { ScriptPanel } from '../script';
@@ -74,6 +75,17 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
7475
path: () => props.path,
7576
requestIndex: () => selectedRequest()?.index
7677
});
78+
const requestHeaderDraft = useRequestHeaderDraftController({
79+
path: () => props.path,
80+
selectedRequest,
81+
sourceHeaders: requestParseDetails.headers,
82+
sourceUrl: () => selectedRequest()?.url,
83+
getFileContent: () => workspace.fileContents()[props.path]?.content,
84+
setFileContent: (content) => workspace.updateFileContent(props.path, content),
85+
saveFile: (path) => workspace.saveFile(path),
86+
reloadRequests: (path) => workspace.loadRequests(path),
87+
refetchRequestDetails: requestParseDetails.refetch
88+
});
7789

7890
const saveCollapsedState = (collapsed: boolean) => {
7991
if (typeof localStorage !== 'undefined') {
@@ -213,10 +225,18 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
213225
onTabChange={setActiveRequestTab}
214226
selectedRequest={selectedRequest()}
215227
requestCount={requests().length}
216-
requestHeaders={requestParseDetails.headers()}
228+
requestHeaders={requestHeaderDraft.draftHeaders()}
217229
requestBodySummary={requestParseDetails.bodySummary()}
218230
requestDetailsLoading={requestParseDetails.loading()}
219231
requestDetailsError={requestParseDetails.error()}
232+
headerDraftDirty={requestHeaderDraft.isDirty()}
233+
headerDraftSaving={requestHeaderDraft.isSaving()}
234+
headerDraftSaveError={requestHeaderDraft.saveError()}
235+
onHeaderChange={requestHeaderDraft.onHeaderChange}
236+
onAddHeader={requestHeaderDraft.onAddHeader}
237+
onRemoveHeader={requestHeaderDraft.onRemoveHeader}
238+
onSaveHeaders={requestHeaderDraft.onSave}
239+
onDiscardHeaders={requestHeaderDraft.onDiscard}
220240
/>
221241
<div class="flex-1 min-h-0">
222242
<HttpEditor path={props.path} onExecute={handleHttpExecute} />

packages/web/src/components/request-workspace/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export {
55
type RequestWorkspaceTabId
66
} from './model';
77
export { RequestWorkspaceTabs } from './request-workspace-tabs';
8+
export { useRequestHeaderDraftController } from './use-request-header-draft-controller';
89
export { useRequestParseDetails } from './use-request-parse-details';
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { For, Index, Match, Show, Switch } from 'solid-js';
2+
import type { RequestBodySummary, RequestDetailsRow } from '../../utils/request-details';
3+
4+
interface RequestWorkspaceParamsPanelProps {
5+
requestMethod: string;
6+
requestParams: RequestDetailsRow[];
7+
}
8+
9+
interface RequestWorkspaceHeadersPanelProps {
10+
hasRequest: boolean;
11+
requestHeaders: RequestDetailsRow[];
12+
headerDraftDirty: boolean;
13+
headerDraftSaving: boolean;
14+
headerDraftSaveError?: string;
15+
onHeaderChange: (index: number, field: 'key' | 'value', value: string) => void;
16+
onAddHeader: () => void;
17+
onRemoveHeader: (index: number) => void;
18+
onSaveHeaders: () => void;
19+
onDiscardHeaders: () => void;
20+
}
21+
22+
interface RequestWorkspaceBodyPanelProps {
23+
requestBodySummary: RequestBodySummary;
24+
}
25+
26+
export function RequestWorkspaceParamsPanel(props: RequestWorkspaceParamsPanelProps) {
27+
return (
28+
<Show
29+
when={props.requestParams.length > 0}
30+
fallback={<p>No query params in URL for {props.requestMethod.toUpperCase()} requests.</p>}
31+
>
32+
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
33+
<table class="table table-xs">
34+
<thead>
35+
<tr>
36+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Name</th>
37+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Value</th>
38+
</tr>
39+
</thead>
40+
<tbody>
41+
<For each={props.requestParams}>
42+
{(param) => (
43+
<tr>
44+
<td class="font-mono text-xs text-base-content">{param.key}</td>
45+
<td class="font-mono text-xs text-base-content/80">{param.value}</td>
46+
</tr>
47+
)}
48+
</For>
49+
</tbody>
50+
</table>
51+
</div>
52+
</Show>
53+
);
54+
}
55+
56+
export function RequestWorkspaceHeadersPanel(props: RequestWorkspaceHeadersPanelProps) {
57+
return (
58+
<div class="space-y-2">
59+
<Show when={props.headerDraftSaveError}>
60+
{(message) => (
61+
<div class="rounded-box border border-error/35 bg-error/10 px-2 py-1.5 text-xs text-base-content">
62+
{message()}
63+
</div>
64+
)}
65+
</Show>
66+
67+
<div class="flex flex-wrap items-center justify-between gap-2">
68+
<button
69+
type="button"
70+
class="btn btn-ghost btn-xs font-mono"
71+
onClick={props.onAddHeader}
72+
disabled={!props.hasRequest || props.headerDraftSaving}
73+
>
74+
Add Header
75+
</button>
76+
77+
<div class="flex items-center gap-2">
78+
<Show when={props.headerDraftDirty && !props.headerDraftSaving}>
79+
<span class="badge badge-sm badge-warning font-mono">Unsaved</span>
80+
</Show>
81+
<button
82+
type="button"
83+
class="btn btn-ghost btn-xs font-mono"
84+
onClick={props.onDiscardHeaders}
85+
disabled={!props.hasRequest || !props.headerDraftDirty || props.headerDraftSaving}
86+
>
87+
Discard
88+
</button>
89+
<button
90+
type="button"
91+
class="btn btn-primary btn-xs font-mono"
92+
onClick={props.onSaveHeaders}
93+
disabled={!props.hasRequest || !props.headerDraftDirty || props.headerDraftSaving}
94+
>
95+
{props.headerDraftSaving ? 'Saving…' : 'Save'}
96+
</button>
97+
</div>
98+
</div>
99+
100+
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
101+
<table class="table table-xs">
102+
<thead>
103+
<tr>
104+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Name</th>
105+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Value</th>
106+
<th class="font-mono uppercase tracking-[0.06em] text-[11px] text-right">Actions</th>
107+
</tr>
108+
</thead>
109+
<tbody>
110+
<Show
111+
when={props.requestHeaders.length > 0}
112+
fallback={
113+
<tr>
114+
<td colSpan={3} class="font-mono text-xs text-base-content/70 text-center py-3">
115+
No headers configured for this request.
116+
</td>
117+
</tr>
118+
}
119+
>
120+
<Index each={props.requestHeaders}>
121+
{(header, index) => (
122+
<tr>
123+
<td>
124+
<input
125+
type="text"
126+
class="input input-xs w-full border-base-300 bg-base-100 font-mono text-xs"
127+
value={header().key}
128+
onInput={(event) =>
129+
props.onHeaderChange(index, 'key', event.currentTarget.value)
130+
}
131+
disabled={!props.hasRequest || props.headerDraftSaving}
132+
/>
133+
</td>
134+
<td>
135+
<input
136+
type="text"
137+
class="input input-xs w-full border-base-300 bg-base-100 font-mono text-xs"
138+
value={header().value}
139+
onInput={(event) =>
140+
props.onHeaderChange(index, 'value', event.currentTarget.value)
141+
}
142+
disabled={!props.hasRequest || props.headerDraftSaving}
143+
/>
144+
</td>
145+
<td class="text-right">
146+
<button
147+
type="button"
148+
class="btn btn-ghost btn-xs text-error"
149+
onClick={() => props.onRemoveHeader(index)}
150+
disabled={!props.hasRequest || props.headerDraftSaving}
151+
>
152+
Remove
153+
</button>
154+
</td>
155+
</tr>
156+
)}
157+
</Index>
158+
</Show>
159+
</tbody>
160+
</table>
161+
</div>
162+
</div>
163+
);
164+
}
165+
166+
export function RequestWorkspaceBodyPanel(props: RequestWorkspaceBodyPanelProps) {
167+
return (
168+
<div class="space-y-2">
169+
<p>{props.requestBodySummary.description}</p>
170+
171+
<Switch>
172+
<Match
173+
when={
174+
props.requestBodySummary.kind === 'inline' &&
175+
props.requestBodySummary.text !== undefined
176+
}
177+
>
178+
<pre class="max-h-52 overflow-auto rounded-box border border-base-300 bg-base-100/80 p-2 font-mono text-xs text-base-content">
179+
{props.requestBodySummary.text}
180+
</pre>
181+
</Match>
182+
183+
<Match when={props.requestBodySummary.kind === 'form-data'}>
184+
<Show
185+
when={(props.requestBodySummary.fields?.length ?? 0) > 0}
186+
fallback={<p>No form-data fields were parsed.</p>}
187+
>
188+
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
189+
<table class="table table-xs">
190+
<thead>
191+
<tr>
192+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Name</th>
193+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Type</th>
194+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Value</th>
195+
</tr>
196+
</thead>
197+
<tbody>
198+
<For each={props.requestBodySummary.fields}>
199+
{(field) => (
200+
<tr>
201+
<td class="font-mono text-xs text-base-content">{field.name}</td>
202+
<td class="font-mono text-xs text-base-content/80">
203+
{field.isFile ? 'file' : 'text'}
204+
</td>
205+
<td class="font-mono text-xs text-base-content/80">
206+
{field.isFile
207+
? (field.path ?? field.filename ?? field.value)
208+
: field.value}
209+
</td>
210+
</tr>
211+
)}
212+
</For>
213+
</tbody>
214+
</table>
215+
</div>
216+
</Show>
217+
</Match>
218+
219+
<Match when={props.requestBodySummary.kind === 'file'}>
220+
<Show
221+
when={props.requestBodySummary.filePath}
222+
fallback={<p>No request body file path was parsed.</p>}
223+
>
224+
{(filePath) => (
225+
<div class="rounded-box border border-base-300 bg-base-100/80 p-2">
226+
<p class="font-mono text-xs text-base-content/80">{filePath()}</p>
227+
</div>
228+
)}
229+
</Show>
230+
</Match>
231+
</Switch>
232+
</div>
233+
);
234+
}

0 commit comments

Comments
 (0)