Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion packages/web/src/components/editor/EditorWithExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
DEFAULT_REQUEST_WORKSPACE_TAB,
type RequestWorkspaceTabId,
RequestWorkspaceTabs,
useRequestHeaderDraftController,
useRequestParseDetails
} from '../request-workspace';
import { ScriptPanel } from '../script';
Expand Down Expand Up @@ -74,6 +75,17 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
path: () => props.path,
requestIndex: () => selectedRequest()?.index
});
const requestHeaderDraft = useRequestHeaderDraftController({
path: () => props.path,
selectedRequest,
sourceHeaders: requestParseDetails.headers,
sourceUrl: () => selectedRequest()?.url,
getFileContent: () => workspace.fileContents()[props.path]?.content,
setFileContent: (content) => workspace.updateFileContent(props.path, content),
saveFile: (path) => workspace.saveFile(path),
reloadRequests: (path) => workspace.loadRequests(path),
refetchRequestDetails: requestParseDetails.refetch
});

const saveCollapsedState = (collapsed: boolean) => {
if (typeof localStorage !== 'undefined') {
Expand Down Expand Up @@ -213,10 +225,18 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
onTabChange={setActiveRequestTab}
selectedRequest={selectedRequest()}
requestCount={requests().length}
requestHeaders={requestParseDetails.headers()}
requestHeaders={requestHeaderDraft.draftHeaders()}
requestBodySummary={requestParseDetails.bodySummary()}
requestDetailsLoading={requestParseDetails.loading()}
requestDetailsError={requestParseDetails.error()}
headerDraftDirty={requestHeaderDraft.isDirty()}
headerDraftSaving={requestHeaderDraft.isSaving()}
headerDraftSaveError={requestHeaderDraft.saveError()}
onHeaderChange={requestHeaderDraft.onHeaderChange}
onAddHeader={requestHeaderDraft.onAddHeader}
onRemoveHeader={requestHeaderDraft.onRemoveHeader}
onSaveHeaders={requestHeaderDraft.onSave}
onDiscardHeaders={requestHeaderDraft.onDiscard}
/>
<div class="flex-1 min-h-0">
<HttpEditor path={props.path} onExecute={handleHttpExecute} />
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/components/request-workspace/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export {
type RequestWorkspaceTabId
} from './model';
export { RequestWorkspaceTabs } from './request-workspace-tabs';
export { useRequestHeaderDraftController } from './use-request-header-draft-controller';
export { useRequestParseDetails } from './use-request-parse-details';
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { For, Index, Match, Show, Switch } from 'solid-js';
import type { RequestBodySummary, RequestDetailsRow } from '../../utils/request-details';

interface RequestWorkspaceParamsPanelProps {
requestMethod: string;
requestParams: RequestDetailsRow[];
}

interface RequestWorkspaceHeadersPanelProps {
hasRequest: boolean;
requestHeaders: RequestDetailsRow[];
headerDraftDirty: boolean;
headerDraftSaving: boolean;
headerDraftSaveError?: string;
onHeaderChange: (index: number, field: 'key' | 'value', value: string) => void;
onAddHeader: () => void;
onRemoveHeader: (index: number) => void;
onSaveHeaders: () => void;
onDiscardHeaders: () => void;
}

interface RequestWorkspaceBodyPanelProps {
requestBodySummary: RequestBodySummary;
}

export function RequestWorkspaceParamsPanel(props: RequestWorkspaceParamsPanelProps) {
return (
<Show
when={props.requestParams.length > 0}
fallback={<p>No query params in URL for {props.requestMethod.toUpperCase()} requests.</p>}
>
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
<table class="table table-xs">
<thead>
<tr>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Name</th>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Value</th>
</tr>
</thead>
<tbody>
<For each={props.requestParams}>
{(param) => (
<tr>
<td class="font-mono text-xs text-base-content">{param.key}</td>
<td class="font-mono text-xs text-base-content/80">{param.value}</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</Show>
);
}

export function RequestWorkspaceHeadersPanel(props: RequestWorkspaceHeadersPanelProps) {
return (
<div class="space-y-2">
<Show when={props.headerDraftSaveError}>
{(message) => (
<div class="rounded-box border border-error/35 bg-error/10 px-2 py-1.5 text-xs text-base-content">
{message()}
</div>
)}
</Show>

<div class="flex flex-wrap items-center justify-between gap-2">
<button
type="button"
class="btn btn-ghost btn-xs font-mono"
onClick={props.onAddHeader}
disabled={!props.hasRequest || props.headerDraftSaving}
>
Add Header
</button>

<div class="flex items-center gap-2">
<Show when={props.headerDraftDirty && !props.headerDraftSaving}>
<span class="badge badge-sm badge-warning font-mono">Unsaved</span>
</Show>
<button
type="button"
class="btn btn-ghost btn-xs font-mono"
onClick={props.onDiscardHeaders}
disabled={!props.hasRequest || !props.headerDraftDirty || props.headerDraftSaving}
>
Discard
</button>
<button
type="button"
class="btn btn-primary btn-xs font-mono"
onClick={props.onSaveHeaders}
disabled={!props.hasRequest || !props.headerDraftDirty || props.headerDraftSaving}
>
{props.headerDraftSaving ? 'Saving…' : 'Save'}
</button>
</div>
</div>

<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
<table class="table table-xs">
<thead>
<tr>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Name</th>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Value</th>
<th class="font-mono uppercase tracking-[0.06em] text-[11px] text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show
when={props.requestHeaders.length > 0}
fallback={
<tr>
<td colSpan={3} class="font-mono text-xs text-base-content/70 text-center py-3">
No headers configured for this request.
</td>
</tr>
}
>
<Index each={props.requestHeaders}>
{(header, index) => (
<tr>
<td>
<input
type="text"
class="input input-xs w-full border-base-300 bg-base-100 font-mono text-xs"
value={header().key}
onInput={(event) =>
props.onHeaderChange(index, 'key', event.currentTarget.value)
}
disabled={!props.hasRequest || props.headerDraftSaving}
/>
</td>
<td>
<input
type="text"
class="input input-xs w-full border-base-300 bg-base-100 font-mono text-xs"
value={header().value}
onInput={(event) =>
props.onHeaderChange(index, 'value', event.currentTarget.value)
}
disabled={!props.hasRequest || props.headerDraftSaving}
/>
</td>
<td class="text-right">
<button
type="button"
class="btn btn-ghost btn-xs text-error"
onClick={() => props.onRemoveHeader(index)}
disabled={!props.hasRequest || props.headerDraftSaving}
>
Remove
</button>
</td>
</tr>
)}
</Index>
</Show>
</tbody>
</table>
</div>
</div>
);
}

export function RequestWorkspaceBodyPanel(props: RequestWorkspaceBodyPanelProps) {
return (
<div class="space-y-2">
<p>{props.requestBodySummary.description}</p>

<Switch>
<Match
when={
props.requestBodySummary.kind === 'inline' &&
props.requestBodySummary.text !== undefined
}
>
<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">
{props.requestBodySummary.text}
</pre>
</Match>

<Match when={props.requestBodySummary.kind === 'form-data'}>
<Show
when={(props.requestBodySummary.fields?.length ?? 0) > 0}
fallback={<p>No form-data fields were parsed.</p>}
>
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
<table class="table table-xs">
<thead>
<tr>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Name</th>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Type</th>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Value</th>
</tr>
</thead>
<tbody>
<For each={props.requestBodySummary.fields}>
{(field) => (
<tr>
<td class="font-mono text-xs text-base-content">{field.name}</td>
<td class="font-mono text-xs text-base-content/80">
{field.isFile ? 'file' : 'text'}
</td>
<td class="font-mono text-xs text-base-content/80">
{field.isFile
? (field.path ?? field.filename ?? field.value)
: field.value}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</Show>
</Match>

<Match when={props.requestBodySummary.kind === 'file'}>
<Show
when={props.requestBodySummary.filePath}
fallback={<p>No request body file path was parsed.</p>}
>
{(filePath) => (
<div class="rounded-box border border-base-300 bg-base-100/80 p-2">
<p class="font-mono text-xs text-base-content/80">{filePath()}</p>
</div>
)}
</Show>
</Match>
</Switch>
</div>
);
}
Loading