Skip to content

Commit 70a39fc

Browse files
committed
feat(web): add form-data and file body editing in request workspace
1 parent bee3d55 commit 70a39fc

File tree

7 files changed

+757
-46
lines changed

7 files changed

+757
-46
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
240240
requestHeaders={requestHeaderDraft.draftHeaders()}
241241
requestBodySummary={requestParseDetails.bodySummary()}
242242
requestBodyDraft={requestBodyDraft.draftBody()}
243+
requestBodyFormDataDraft={requestBodyDraft.draftFormData()}
244+
requestBodyFilePathDraft={requestBodyDraft.draftFilePath()}
243245
requestDetailsLoading={requestParseDetails.loading()}
244246
requestDetailsError={requestParseDetails.error()}
245247
headerDraftDirty={requestHeaderDraft.isDirty()}
@@ -257,6 +259,13 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
257259
bodyDraftIsJsonEditable={requestBodyDraft.isJsonBody()}
258260
bodyDraftTemplateWarnings={requestBodyDraft.templateWarnings()}
259261
onBodyChange={requestBodyDraft.onBodyChange}
262+
onBodyFilePathChange={requestBodyDraft.onFilePathChange}
263+
onBodyFormDataNameChange={requestBodyDraft.onFormDataNameChange}
264+
onBodyFormDataTypeChange={requestBodyDraft.onFormDataTypeChange}
265+
onBodyFormDataValueChange={requestBodyDraft.onFormDataValueChange}
266+
onBodyFormDataFilenameChange={requestBodyDraft.onFormDataFilenameChange}
267+
onBodyFormDataAddField={requestBodyDraft.onAddFormDataField}
268+
onBodyFormDataRemoveField={requestBodyDraft.onRemoveFormDataField}
260269
onBodyPrettify={requestBodyDraft.onBodyPrettify}
261270
onBodyMinify={requestBodyDraft.onBodyMinify}
262271
onBodyCopy={() => void requestBodyDraft.onBodyCopy()}

packages/web/src/components/request-workspace/request-workspace-tab-panels.tsx

Lines changed: 184 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { For, Index, Match, Show, Switch } from 'solid-js';
2-
import type { RequestBodySummary, RequestDetailsRow } from '../../utils/request-details';
2+
import type {
3+
RequestBodyField,
4+
RequestBodySummary,
5+
RequestDetailsRow
6+
} from '../../utils/request-details';
37

48
interface RequestWorkspaceParamsPanelProps {
59
requestMethod: string;
@@ -23,13 +27,22 @@ interface RequestWorkspaceBodyPanelProps {
2327
hasRequest: boolean;
2428
requestBodySummary: RequestBodySummary;
2529
requestBodyDraft: string;
30+
requestBodyFormDataDraft: RequestBodyField[];
31+
requestBodyFilePathDraft: string;
2632
bodyDraftDirty: boolean;
2733
bodyDraftSaving: boolean;
2834
bodyDraftSaveError?: string;
2935
bodyDraftValidationError?: string;
3036
bodyDraftIsJsonEditable: boolean;
3137
bodyDraftTemplateWarnings: string[];
3238
onBodyChange: (value: string) => void;
39+
onBodyFilePathChange: (value: string) => void;
40+
onBodyFormDataNameChange: (index: number, value: string) => void;
41+
onBodyFormDataTypeChange: (index: number, isFile: boolean) => void;
42+
onBodyFormDataValueChange: (index: number, value: string) => void;
43+
onBodyFormDataFilenameChange: (index: number, value: string) => void;
44+
onBodyFormDataAddField: () => void;
45+
onBodyFormDataRemoveField: (index: number) => void;
3346
onBodyPrettify: () => void;
3447
onBodyMinify: () => void;
3548
onBodyCopy: () => void;
@@ -301,52 +314,196 @@ export function RequestWorkspaceBodyPanel(props: RequestWorkspaceBodyPanelProps)
301314
</Match>
302315

303316
<Match when={props.requestBodySummary.kind === 'form-data'}>
304-
<Show
305-
when={(props.requestBodySummary.fields?.length ?? 0) > 0}
306-
fallback={<p>No form-data fields were parsed.</p>}
307-
>
317+
<div class="space-y-2">
318+
<div class="flex flex-wrap items-center justify-between gap-2">
319+
<button
320+
type="button"
321+
class="btn btn-ghost btn-xs font-mono"
322+
onClick={props.onBodyFormDataAddField}
323+
disabled={!props.hasRequest || props.bodyDraftSaving}
324+
>
325+
Add Field
326+
</button>
327+
328+
<div class="flex items-center gap-2">
329+
<Show when={props.bodyDraftDirty && !props.bodyDraftSaving}>
330+
<span class="badge badge-sm badge-warning font-mono">Unsaved</span>
331+
</Show>
332+
<button
333+
type="button"
334+
class="btn btn-ghost btn-xs font-mono"
335+
onClick={props.onDiscardBody}
336+
disabled={!props.hasRequest || !props.bodyDraftDirty || props.bodyDraftSaving}
337+
>
338+
Discard
339+
</button>
340+
<button
341+
type="button"
342+
class="btn btn-primary btn-xs font-mono"
343+
onClick={props.onSaveBody}
344+
disabled={!props.hasRequest || !props.bodyDraftDirty || props.bodyDraftSaving}
345+
>
346+
{props.bodyDraftSaving ? 'Saving…' : 'Save'}
347+
</button>
348+
</div>
349+
</div>
350+
308351
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
309352
<table class="table table-xs">
310353
<thead>
311354
<tr>
312355
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Name</th>
313356
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Type</th>
314357
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Value</th>
358+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Filename</th>
359+
<th class="font-mono uppercase tracking-[0.06em] text-[11px] text-right">
360+
Actions
361+
</th>
315362
</tr>
316363
</thead>
317364
<tbody>
318-
<For each={props.requestBodySummary.fields}>
319-
{(field) => (
365+
<Show
366+
when={props.requestBodyFormDataDraft.length > 0}
367+
fallback={
320368
<tr>
321-
<td class="font-mono text-xs text-base-content">{field.name}</td>
322-
<td class="font-mono text-xs text-base-content/80">
323-
{field.isFile ? 'file' : 'text'}
324-
</td>
325-
<td class="font-mono text-xs text-base-content/80">
326-
{field.isFile
327-
? (field.path ?? field.filename ?? field.value)
328-
: field.value}
369+
<td
370+
colSpan={5}
371+
class="font-mono text-xs text-base-content/70 text-center py-3"
372+
>
373+
No form-data fields configured.
329374
</td>
330375
</tr>
331-
)}
332-
</For>
376+
}
377+
>
378+
<Index each={props.requestBodyFormDataDraft}>
379+
{(field, index) => (
380+
<tr>
381+
<td>
382+
<input
383+
type="text"
384+
class="input input-xs w-full border-base-300 bg-base-100 font-mono text-xs"
385+
value={field().name}
386+
onInput={(event) =>
387+
props.onBodyFormDataNameChange(index, event.currentTarget.value)
388+
}
389+
disabled={!props.hasRequest || props.bodyDraftSaving}
390+
/>
391+
</td>
392+
<td>
393+
<select
394+
class="select select-xs w-full border-base-300 bg-base-100 font-mono text-xs"
395+
value={field().isFile ? 'file' : 'text'}
396+
onChange={(event) =>
397+
props.onBodyFormDataTypeChange(
398+
index,
399+
event.currentTarget.value === 'file'
400+
)
401+
}
402+
disabled={!props.hasRequest || props.bodyDraftSaving}
403+
>
404+
<option value="text">text</option>
405+
<option value="file">file</option>
406+
</select>
407+
</td>
408+
<td>
409+
<input
410+
type="text"
411+
class="input input-xs w-full border-base-300 bg-base-100 font-mono text-xs"
412+
value={field().isFile ? (field().path ?? '') : field().value}
413+
onInput={(event) =>
414+
props.onBodyFormDataValueChange(index, event.currentTarget.value)
415+
}
416+
placeholder={field().isFile ? './path/to/file' : 'value'}
417+
disabled={!props.hasRequest || props.bodyDraftSaving}
418+
/>
419+
</td>
420+
<td>
421+
<input
422+
type="text"
423+
class="input input-xs w-full border-base-300 bg-base-100 font-mono text-xs"
424+
value={field().filename ?? ''}
425+
onInput={(event) =>
426+
props.onBodyFormDataFilenameChange(index, event.currentTarget.value)
427+
}
428+
placeholder="optional"
429+
disabled={
430+
!props.hasRequest || props.bodyDraftSaving || !field().isFile
431+
}
432+
/>
433+
</td>
434+
<td class="text-right">
435+
<button
436+
type="button"
437+
class="btn btn-ghost btn-xs text-error"
438+
onClick={() => props.onBodyFormDataRemoveField(index)}
439+
disabled={!props.hasRequest || props.bodyDraftSaving}
440+
>
441+
Remove
442+
</button>
443+
</td>
444+
</tr>
445+
)}
446+
</Index>
447+
</Show>
333448
</tbody>
334449
</table>
335450
</div>
336-
</Show>
451+
</div>
337452
</Match>
338453

339454
<Match when={props.requestBodySummary.kind === 'file'}>
340-
<Show
341-
when={props.requestBodySummary.filePath}
342-
fallback={<p>No request body file path was parsed.</p>}
343-
>
344-
{(filePath) => (
345-
<div class="rounded-box border border-base-300 bg-base-100/80 p-2">
346-
<p class="font-mono text-xs text-base-content/80">{filePath()}</p>
455+
<div class="space-y-2">
456+
<div class="flex flex-wrap items-center justify-between gap-2">
457+
<button
458+
type="button"
459+
class="btn btn-ghost btn-xs font-mono"
460+
onClick={props.onBodyCopy}
461+
disabled={!props.hasRequest}
462+
>
463+
Copy
464+
</button>
465+
466+
<div class="flex items-center gap-2">
467+
<Show when={props.bodyDraftDirty && !props.bodyDraftSaving}>
468+
<span class="badge badge-sm badge-warning font-mono">Unsaved</span>
469+
</Show>
470+
<button
471+
type="button"
472+
class="btn btn-ghost btn-xs font-mono"
473+
onClick={props.onDiscardBody}
474+
disabled={!props.hasRequest || !props.bodyDraftDirty || props.bodyDraftSaving}
475+
>
476+
Discard
477+
</button>
478+
<button
479+
type="button"
480+
class="btn btn-primary btn-xs font-mono"
481+
onClick={props.onSaveBody}
482+
disabled={!props.hasRequest || !props.bodyDraftDirty || props.bodyDraftSaving}
483+
>
484+
{props.bodyDraftSaving ? 'Saving…' : 'Save'}
485+
</button>
347486
</div>
348-
)}
349-
</Show>
487+
</div>
488+
489+
<div class="rounded-box border border-base-300 bg-base-100/80 p-2">
490+
<label
491+
for="request-workspace-body-file-path"
492+
class="mb-1 block font-mono text-[11px] uppercase tracking-[0.06em] text-base-content/70"
493+
>
494+
File Path
495+
</label>
496+
<input
497+
id="request-workspace-body-file-path"
498+
type="text"
499+
class="input input-sm w-full border-base-300 bg-base-100 font-mono text-xs"
500+
value={props.requestBodyFilePathDraft}
501+
onInput={(event) => props.onBodyFilePathChange(event.currentTarget.value)}
502+
disabled={!props.hasRequest || props.bodyDraftSaving}
503+
placeholder="./payload.json"
504+
/>
505+
</div>
506+
</div>
350507
</Match>
351508
</Switch>
352509
</div>

packages/web/src/components/request-workspace/request-workspace-tabs.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { createMemo, For, Match, Show, Switch } from 'solid-js';
22
import type { WorkspaceRequest } from '../../sdk';
3-
import type { RequestBodySummary, RequestDetailsRow } from '../../utils/request-details';
3+
import type {
4+
RequestBodyField,
5+
RequestBodySummary,
6+
RequestDetailsRow
7+
} from '../../utils/request-details';
48
import { toRequestParams } from '../../utils/request-details';
59
import { REQUEST_WORKSPACE_TABS, type RequestWorkspaceTabId } from './model';
610
import {
@@ -17,6 +21,8 @@ interface RequestWorkspaceTabsProps {
1721
requestHeaders: RequestDetailsRow[];
1822
requestBodySummary: RequestBodySummary;
1923
requestBodyDraft: string;
24+
requestBodyFormDataDraft: RequestBodyField[];
25+
requestBodyFilePathDraft: string;
2026
requestDetailsLoading: boolean;
2127
requestDetailsError?: string;
2228
headerDraftDirty: boolean;
@@ -34,6 +40,13 @@ interface RequestWorkspaceTabsProps {
3440
bodyDraftIsJsonEditable: boolean;
3541
bodyDraftTemplateWarnings: string[];
3642
onBodyChange: (value: string) => void;
43+
onBodyFilePathChange: (value: string) => void;
44+
onBodyFormDataNameChange: (index: number, value: string) => void;
45+
onBodyFormDataTypeChange: (index: number, isFile: boolean) => void;
46+
onBodyFormDataValueChange: (index: number, value: string) => void;
47+
onBodyFormDataFilenameChange: (index: number, value: string) => void;
48+
onBodyFormDataAddField: () => void;
49+
onBodyFormDataRemoveField: (index: number) => void;
3750
onBodyPrettify: () => void;
3851
onBodyMinify: () => void;
3952
onBodyCopy: () => void;
@@ -135,13 +148,22 @@ export function RequestWorkspaceTabs(props: RequestWorkspaceTabsProps) {
135148
hasRequest={Boolean(props.selectedRequest)}
136149
requestBodySummary={props.requestBodySummary}
137150
requestBodyDraft={props.requestBodyDraft}
151+
requestBodyFormDataDraft={props.requestBodyFormDataDraft}
152+
requestBodyFilePathDraft={props.requestBodyFilePathDraft}
138153
bodyDraftDirty={props.bodyDraftDirty}
139154
bodyDraftSaving={props.bodyDraftSaving}
140155
bodyDraftSaveError={props.bodyDraftSaveError}
141156
bodyDraftValidationError={props.bodyDraftValidationError}
142157
bodyDraftIsJsonEditable={props.bodyDraftIsJsonEditable}
143158
bodyDraftTemplateWarnings={props.bodyDraftTemplateWarnings}
144159
onBodyChange={props.onBodyChange}
160+
onBodyFilePathChange={props.onBodyFilePathChange}
161+
onBodyFormDataNameChange={props.onBodyFormDataNameChange}
162+
onBodyFormDataTypeChange={props.onBodyFormDataTypeChange}
163+
onBodyFormDataValueChange={props.onBodyFormDataValueChange}
164+
onBodyFormDataFilenameChange={props.onBodyFormDataFilenameChange}
165+
onBodyFormDataAddField={props.onBodyFormDataAddField}
166+
onBodyFormDataRemoveField={props.onBodyFormDataRemoveField}
145167
onBodyPrettify={props.onBodyPrettify}
146168
onBodyMinify={props.onBodyMinify}
147169
onBodyCopy={props.onBodyCopy}

0 commit comments

Comments
 (0)