From a161b766e4f1c8cf3ee2ab3dc37d90abe437594f Mon Sep 17 00:00:00 2001 From: Rahul Tyagi Date: Sat, 13 Jun 2026 20:40:39 +0530 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20v0.2=20=E2=80=94=20response=20insig?= =?UTF-8?q?ht=20(A)=20and=20request=20power=20(B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements milestone v0.2 from docs/SPEC.md. Backend - Introduce structured RequestSpec (method, url, headers, body, settings) and a new Send(spec) bound method. Builds a per-request http.Client from Settings: timeout, follow-redirects (CheckRedirect), TLS verify. - Extend RequestResult with Status, StatusText, DurationMs, SizeBytes. - Non-JSON response bodies are now returned verbatim instead of dropped on json.Indent failure. - Keep MakeRequest as a thin wrapper around Send for backward compat. - Add SaveTextFile binding (used by Save body) via runtime.SaveFileDialog. - New send_test.go covering: status/text round-trip, duration & size, timeout, TLS verify off, redirect-following off, JSON pretty + plain. Frontend - Response insight (A): status chip colored by HTTP class, duration, size formatter (B/KB/MB), Pretty/Raw body toggle, search-in-response with match count and highlighting, Copy & Save body buttons. - Request power (B): - Query params builder, two-way synced with the URL field. - Auth: None / Bearer / Basic / API key (header or query target). - Body types: None / JSON (with Format button) / Form / Raw. - Per-request Settings: timeout, follow redirects, verify TLS. - Copy as cURL. - Request editor reorganized into one tabbed panel (Headers / Params / Body / Auth / Settings). - New shared helpers: kv (key/value rows), formatters, auth, settings, url-sync, curl-builder. Reusable KVRow replaces the per-purpose RequestHeader component (deleted). - Wails bindings regenerated. Deferred to v0.3 per spec: multipart / file uploads, iframe HTML preview. --- CHANGELOG.md | 23 + app.go | 169 +++++- export.go | 36 ++ frontend/src/App.tsx | 578 ++++++++++++++----- frontend/src/components/auth-form.tsx | 123 ++++ frontend/src/components/body-editor.tsx | 142 +++++ frontend/src/components/headerform.tsx | 57 -- frontend/src/components/highlighted-text.tsx | 50 ++ frontend/src/components/kv-row.tsx | 52 ++ frontend/src/components/settings-form.tsx | 81 +++ frontend/src/lib/auth.ts | 49 ++ frontend/src/lib/curl-builder.ts | 29 + frontend/src/lib/formatters.ts | 23 + frontend/src/lib/header.ts | 6 - frontend/src/lib/kv.ts | 41 ++ frontend/src/lib/settings.ts | 11 + frontend/src/lib/url-sync.ts | 44 ++ frontend/wailsjs/go/main/App.d.ts | 6 +- frontend/wailsjs/go/main/App.js | 8 + frontend/wailsjs/go/models.ts | 75 ++- send_test.go | 282 +++++++++ 21 files changed, 1649 insertions(+), 236 deletions(-) create mode 100644 frontend/src/components/auth-form.tsx create mode 100644 frontend/src/components/body-editor.tsx delete mode 100644 frontend/src/components/headerform.tsx create mode 100644 frontend/src/components/highlighted-text.tsx create mode 100644 frontend/src/components/kv-row.tsx create mode 100644 frontend/src/components/settings-form.tsx create mode 100644 frontend/src/lib/auth.ts create mode 100644 frontend/src/lib/curl-builder.ts create mode 100644 frontend/src/lib/formatters.ts delete mode 100644 frontend/src/lib/header.ts create mode 100644 frontend/src/lib/kv.ts create mode 100644 frontend/src/lib/settings.ts create mode 100644 frontend/src/lib/url-sync.ts create mode 100644 send_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c73265e..26214c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,12 +23,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Git hooks (`.githooks/pre-commit`, `.githooks/pre-push`) that enforce formatting and mirror CI checks locally; enable with `make hooks`. - `Makefile` with `hooks`, `setup`, `lint`, `test`, and `build` targets. +- **v0.2 — Response insight (bundle A):** response status code + status text, request + duration, and body size shown next to the Response heading; status chip is colored by + HTTP class. +- **v0.2 — Body Pretty/Raw toggle** and **search-in-response** with match count, plus + **Copy** and **Save** buttons (the latter via a new `SaveTextFile` Go binding using a + native save dialog). +- **v0.2 — Request power (bundle B):** query-param builder synced two-way with the URL + field; **Auth** section (None / Bearer / Basic / API key with header-or-query target); + **Body** selector (None / JSON with a Format button / Form / Raw); per-request + **Settings** (timeout, follow-redirects, verify TLS); **Copy as cURL** action that + mirrors cURL import. ### Changed +- Backend: introduced `RequestSpec` and a new `Send(spec) RequestResult` bound method + that builds a per-request `http.Client` from settings (timeout, redirects, TLS verify). + `MakeRequest` is retained as a thin wrapper for backward compatibility. +- `RequestResult` now includes `Status`, `StatusText`, `DurationMs`, and `SizeBytes`. +- Non-JSON response bodies are returned verbatim instead of swallowed when JSON pretty- + printing fails. +- Request editor reorganized into tabbed sections (Headers / Params / Body / Auth / + Settings) within a single panel. - cURL import now populates the header rows and request body of the active tab. - The response view now follows the active request tab. - The "add header" action moved next to the Request Headers title. +### Deferred (planned for v0.3) +- Multipart / file-upload body type. +- Iframe HTML preview tab for the response body. + ### Fixed - `//go:embed all:frontend/dist` caused a compile failure on clean checkouts because `frontend/dist/` was gitignored; fixed by committing a `.gitkeep` and updating diff --git a/app.go b/app.go index 2c6848a..fa610d0 100644 --- a/app.go +++ b/app.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "crypto/tls" "encoding/json" "io" "net/http" @@ -35,7 +36,7 @@ func (a *App) startup(ctx context.Context) { a.ctx = ctx } -func makeRequest(c *http.Client, +func doRequest(c *http.Client, r *http.Request) ([]byte, *http.Response, error) { resp, err := c.Do(r) if err != nil { @@ -62,6 +63,29 @@ func HeadersToStr(h *http.Header) string { return result } +// BodySpec describes the request body type and content. +type BodySpec struct { + Type string `json:"type"` // "none" | "json" | "form" | "raw" + Raw string `json:"raw"` // raw/json text or pre-encoded form body +} + +// RequestSettings contains per-request client settings. +type RequestSettings struct { + TimeoutMs int `json:"timeoutMs"` // 0 = use default (50s) + FollowRedirects bool `json:"followRedirects"` // true = follow + VerifyTLS bool `json:"verifyTLS"` // true = verify +} + +// RequestSpec is the structured request used by Send(). +type RequestSpec struct { + Method string `json:"method"` + URL string `json:"url"` + Headers map[string]string `json:"headers"` + Body BodySpec `json:"body"` + Settings RequestSettings `json:"settings"` +} + +// RequestResult is the response shape returned to the UI. type RequestResult struct { Method string `json:"Method"` URL string `json:"URL"` @@ -70,6 +94,97 @@ type RequestResult struct { Body string `json:"Body"` HeadersStr string `json:"HeadersStr"` Error string `json:"Error"` + Status int `json:"Status"` + StatusText string `json:"StatusText"` + DurationMs int64 `json:"DurationMs"` + SizeBytes int `json:"SizeBytes"` +} + +// buildClient creates an http.Client configured from RequestSettings. +func buildClient(s RequestSettings) *http.Client { + timeout := 50 * time.Second + if s.TimeoutMs > 0 { + timeout = time.Duration(s.TimeoutMs) * time.Millisecond + } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: !s.VerifyTLS}, //nolint:gosec + } + + client := &http.Client{ + Timeout: timeout, + Transport: tr, + } + + if !s.FollowRedirects { + client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + } + } + + return client +} + +// Send builds a per-request http.Client from Settings and executes the request. +func (a *App) Send(spec RequestSpec) RequestResult { + result := RequestResult{ + URL: spec.URL, + Method: spec.Method, + } + + var bodyReader io.Reader + if spec.Body.Type != "none" && spec.Body.Raw != "" { + bodyReader = strings.NewReader(spec.Body.Raw) + result.RequestBody = spec.Body.Raw + } + + r, err := http.NewRequest(spec.Method, spec.URL, bodyReader) + if err != nil { + result.Error = err.Error() + return result + } + + for key, value := range spec.Headers { + r.Header.Add(key, value) + } + + result.ReqHeaders = HeadersToStr(&r.Header) + + client := buildClient(spec.Settings) + + start := time.Now() + res, httpResp, err := doRequest(client, r) + result.DurationMs = time.Since(start).Milliseconds() + + if err != nil { + result.Error = err.Error() + // still capture status if response was partially received + if httpResp != nil { + result.Status = httpResp.StatusCode + result.StatusText = httpResp.Status + } + return result + } + + result.Status = httpResp.StatusCode + result.StatusText = httpResp.Status + result.HeadersStr = HeadersToStr(&httpResp.Header) + result.SizeBytes = len(res) + + // Pretty-print JSON bodies; on failure, return raw body without error + b := bytes.NewBuffer(make([]byte, 0, len(res))) + if jsonErr := json.Indent(b, res, "", " "); jsonErr == nil { + result.Body = b.String() + } else { + result.Body = string(res) + } + + return result +} + +// SaveTextFile opens a native save dialog and writes text content to the chosen file. +func (a *App) SaveTextFile(filename, contents string) error { + return saveTextFile(a.ctx, filename, contents) } func (a *App) RunCurl(curl string) RequestResult { @@ -88,50 +203,42 @@ func (a *App) RunCurl(curl string) RequestResult { return res } +// Header is a single key/value header pair (kept for Export compatibility). type Header struct { Key string Value string } -// Greet returns a greeting for the given name +// MakeRequest is kept for backward compatibility; it delegates to Send. func (a *App) MakeRequest( urlIn string, method string, body string, headers Headers, ) RequestResult { - result := RequestResult{ - URL: urlIn, - Method: method, - RequestBody: body, - } - rbody := bytes.NewBuffer([]byte(body)) - r, err := http.NewRequest(method, urlIn, rbody) - if err != nil { - result.Error = err.Error() - return result + hdrs := make(map[string]string, len(headers)) + for k, v := range headers { + hdrs[k] = v } - for key, value := range headers { - r.Header.Add(key, value) - } - - res, httpResp, err := makeRequest(a.client, r) - if err != nil { - result.Error = err.Error() - return result - } - - result.HeadersStr = HeadersToStr(&httpResp.Header) - b := bytes.NewBuffer(make([]byte, 0, len(res))) - err = json.Indent(b, res, "\n", " ") - if err != nil { - return RequestResult{ - Body: string(res), - Error: err.Error(), - } + spec := RequestSpec{ + Method: method, + URL: urlIn, + Headers: hdrs, + Body: BodySpec{ + Type: "raw", + Raw: body, + }, + Settings: RequestSettings{ + FollowRedirects: true, + VerifyTLS: true, + }, } - result.Body = b.String() + result := a.Send(spec) + // preserve old fields that Send populates differently + result.Method = method + result.URL = urlIn + result.RequestBody = body return result } diff --git a/export.go b/export.go index ce62700..5bc8fb1 100644 --- a/export.go +++ b/export.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "log" "os" @@ -56,3 +57,38 @@ func (a *App) Export(req Request, reqHeaders [][]Header, reqBodies []string, r R return nil } + +// saveTextFile opens a native save dialog and writes plain text to the chosen path. +func saveTextFile(ctx context.Context, defaultName, contents string) error { + if defaultName == "" { + defaultName = "response.txt" + } + + fp, err := runtime.SaveFileDialog(ctx, runtime.SaveDialogOptions{ + DefaultFilename: defaultName, + Title: "Save response body", + }) + if err != nil { + log.Println(err) + return err + } + + if fp == "" { + // user cancelled the dialog + return nil + } + + f, err := os.Create(fp) + if err != nil { + log.Println(err) + return err + } + defer f.Close() + + if _, err := f.WriteString(contents); err != nil { + log.Println(err) + return err + } + + return nil +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ac6279b..3c8975d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import {useState} from 'react' -import {MakeRequest, RunCurl, Export} from '../wailsjs/go/main/App' +import {Send as SendBinding, RunCurl, Export, SaveTextFile} from '../wailsjs/go/main/App' import {main} from '../wailsjs/go/models' import { Send, @@ -13,15 +13,22 @@ import { AlertTriangle, Inbox, Loader2, + Copy, + Save, + Search, + KeyRound, + Settings as SettingsIcon, + Link2, } from 'lucide-react' -import {Header} from './lib/header' -import {RequestHeader} from './components/headerform' +import {KVRow} from './components/kv-row' +import {AuthForm} from './components/auth-form' +import {SettingsForm} from './components/settings-form' +import {BodyEditor, type BodyType} from './components/body-editor' import {JsonView} from './components/json-view' +import {HighlightedText, countMatches} from './components/highlighted-text' import {Button} from '@/components/ui/button' import {Input} from '@/components/ui/input' import {Textarea} from '@/components/ui/textarea' -import {Label} from '@/components/ui/label' -import {Badge} from '@/components/ui/badge' import {Separator} from '@/components/ui/separator' import { Select, @@ -39,23 +46,14 @@ import { DialogTitle, DialogDescription, } from '@/components/ui/dialog' +import {addRowAt, emptyKV, kvToRecord, removeRowAt, updateRowsAt, type KV} from './lib/kv' +import {applyAuth, emptyAuth, type AuthState} from './lib/auth' +import {defaultSettings, type ReqSettings} from './lib/settings' +import {encodeFormBody, parseUrlParams, rebuildUrlWithParams} from './lib/url-sync' +import {buildCurl} from './lib/curl-builder' +import {formatDuration, formatSize, statusChipClass} from './lib/formatters' -class Request { - method: string - url: string - headers: {[key: string]: string} - body: string - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source) - this.method = source['method'] || source['Method'] || 'GET' - this.url = source['url'] || '' - this.headers = source['headers'] || {} - this.body = source['body'] || '' - } -} - -const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const +const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] as const const METHOD_HSL: {[key: string]: string} = { GET: '152 60% 52%', @@ -63,14 +61,21 @@ const METHOD_HSL: {[key: string]: string} = { PUT: '212 90% 62%', DELETE: '0 84% 64%', PATCH: '265 85% 70%', + HEAD: '200 30% 60%', + OPTIONS: '200 30% 60%', } const methodColor = (m: string) => `hsl(${METHOD_HSL[m] ?? '84 78% 56%'})` const emptyResult = () => new main.RequestResult() -// "Key: Value\nKey: Value" -> Header[] -function parseHeaderLines(raw: string): Header[] { +interface RequestState { + method: string + url: string +} + +// "Key: Value\nKey: Value" -> KV[] +function parseHeaderLines(raw: string): KV[] { const rows = raw .split('\n') .map((line) => line.trim()) @@ -80,58 +85,184 @@ function parseHeaderLines(raw: string): Header[] { if (idx === -1) return {Key: line, Value: ''} return {Key: line.slice(0, idx).trim(), Value: line.slice(idx + 1).trim()} }) - return rows.length ? rows : [{Key: '', Value: ''}] + return rows.length ? rows : [emptyKV()] } +const initialHeaderRows = (): KV[] => [emptyKV(), emptyKV(), emptyKV()] + function App() { - const [reqBodies, setReqBodies] = useState>(['']) - const [reqHeaders, setReqHeaders] = useState>>([ - [ - {Key: '', Value: ''}, - {Key: '', Value: ''}, - {Key: '', Value: ''}, - ], - ]) - const [responses, setResponses] = useState>([emptyResult()]) const [activeTab, setActiveTab] = useState(0) - const [request, setRequest] = useState(new Request({method: 'GET'})) - const [curlBody, setCurlBody] = useState('') - const [open, setOpen] = useState(false) - const [responseTab, setResponseTab] = useState('body') + const [request, setRequest] = useState({method: 'GET', url: ''}) const [loading, setLoading] = useState(false) - const result = responses[activeTab] ?? emptyResult() - const hasResponse = Boolean(result.Body || result.Error || result.HeadersStr) + // Per-tab parallel arrays + const [headers, setHeaders] = useState([initialHeaderRows()]) + const [params, setParams] = useState([[emptyKV()]]) + const [bodyTypes, setBodyTypes] = useState(['none']) + const [jsonBodies, setJsonBodies] = useState(['']) + const [rawBodies, setRawBodies] = useState(['']) + const [formRows, setFormRows] = useState([[emptyKV()]]) + const [auths, setAuths] = useState([emptyAuth()]) + const [settings, setSettings] = useState([defaultSettings()]) + const [responses, setResponses] = useState([emptyResult()]) + + // UI state + const [requestSection, setRequestSection] = useState('headers') + const [responseTab, setResponseTab] = useState('body') + const [bodyView, setBodyView] = useState<'pretty' | 'raw'>('pretty') + const [search, setSearch] = useState('') + const [curlOpen, setCurlOpen] = useState(false) + const [curlBody, setCurlBody] = useState('') - const setMethod = (method: string) => setRequest((p) => ({...p, method})) - const setUrl = (url: string) => setRequest((p) => ({...p, url})) + const result = responses[activeTab] ?? emptyResult() + const hasResponse = Boolean( + result.Body || result.Error || result.HeadersStr || result.Status + ) + // ── Tab plumbing ────────────────────────────────────────────── const addNewTab = () => { - setReqBodies([...reqBodies, '']) - setReqHeaders([...reqHeaders, [{Key: '', Value: ''}]]) + setHeaders([...headers, initialHeaderRows()]) + setParams([...params, [emptyKV()]]) + setBodyTypes([...bodyTypes, 'none']) + setJsonBodies([...jsonBodies, '']) + setRawBodies([...rawBodies, '']) + setFormRows([...formRows, [emptyKV()]]) + setAuths([...auths, emptyAuth()]) + setSettings([...settings, defaultSettings()]) setResponses([...responses, emptyResult()]) - setActiveTab(reqBodies.length) + setActiveTab(headers.length) } const closeTab = (e: React.MouseEvent, index: number) => { e.stopPropagation() - if (reqBodies.length === 1) return - setReqBodies(reqBodies.filter((_, i) => i !== index)) - setReqHeaders(reqHeaders.filter((_, i) => i !== index)) - setResponses(responses.filter((_, i) => i !== index)) + if (headers.length === 1) return + const drop = (arr: T[]) => arr.filter((_, i) => i !== index) + setHeaders(drop(headers)) + setParams(drop(params)) + setBodyTypes(drop(bodyTypes)) + setJsonBodies(drop(jsonBodies)) + setRawBodies(drop(rawBodies)) + setFormRows(drop(formRows)) + setAuths(drop(auths)) + setSettings(drop(settings)) + setResponses(drop(responses)) if (activeTab >= index && activeTab > 0) setActiveTab(activeTab - 1) } - const updateReqBody = (e: React.ChangeEvent) => { - const newBodies = [...reqBodies] - newBodies[activeTab] = e.target.value - setReqBodies(newBodies) + // ── Header handlers ─────────────────────────────────────────── + const onHeaderChange = (idx: number, field: 'Key' | 'Value', value: string) => + setHeaders(updateRowsAt(headers, activeTab, idx, field, value)) + const onHeaderRemove = (idx: number) => setHeaders(removeRowAt(headers, activeTab, idx)) + const onHeaderAdd = () => setHeaders(addRowAt(headers, activeTab)) + + // ── Params handlers (with URL sync) ─────────────────────────── + const onParamChange = (idx: number, field: 'Key' | 'Value', value: string) => { + const next = updateRowsAt(params, activeTab, idx, field, value) + setParams(next) + // Reflect param changes back into URL field + setRequest((p) => ({...p, url: rebuildUrlWithParams(p.url, next[activeTab])})) + } + const onParamRemove = (idx: number) => { + const next = removeRowAt(params, activeTab, idx) + setParams(next) + setRequest((p) => ({...p, url: rebuildUrlWithParams(p.url, next[activeTab])})) + } + const onParamAdd = () => setParams(addRowAt(params, activeTab)) + + const onUrlChange = (url: string) => { + setRequest((p) => ({...p, url})) + const parsed = parseUrlParams(url) + if (parsed) { + const next = [...params] + next[activeTab] = parsed + setParams(next) + } + } + + // ── Body / auth / settings handlers ─────────────────────────── + const setBodyType = (t: BodyType) => { + const next = [...bodyTypes] + next[activeTab] = t + setBodyTypes(next) + } + const setJsonBody = (s: string) => { + const next = [...jsonBodies] + next[activeTab] = s + setJsonBodies(next) + } + const setRawBody = (s: string) => { + const next = [...rawBodies] + next[activeTab] = s + setRawBodies(next) + } + const setFormRowsForTab = (rows: KV[]) => { + const next = [...formRows] + next[activeTab] = rows + setFormRows(next) + } + const setAuth = (a: AuthState) => { + const next = [...auths] + next[activeTab] = a + setAuths(next) + } + const setSettingsForTab = (s: ReqSettings) => { + const next = [...settings] + next[activeTab] = s + setSettings(next) } - const addHeader = () => { - const newHeaders = [...reqHeaders] - newHeaders[activeTab] = [...newHeaders[activeTab], {Key: '', Value: ''}] - setReqHeaders(newHeaders) + // ── Build the final RequestSpec for the active tab ──────────── + function buildSpec(): main.RequestSpec { + const userHeaders = kvToRecord(headers[activeTab]) + const auth = applyAuth(auths[activeTab]) + const mergedHeaders: Record = {...userHeaders, ...auth.headers} + + // Compose URL with params + any API-key query injection + let url = rebuildUrlWithParams(request.url, params[activeTab]) + if (auth.query.length) { + try { + const u = new URL(url) + for (const [k, v] of auth.query) u.searchParams.append(k, v) + url = u.toString() + } catch { + const q = auth.query + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join('&') + url = url.includes('?') ? `${url}&${q}` : `${url}?${q}` + } + } + + const bodyType = bodyTypes[activeTab] + let body: main.BodySpec = main.BodySpec.createFrom({type: 'none', raw: ''}) + + if (bodyType === 'json') { + const raw = jsonBodies[activeTab] + if (raw && !mergedHeaders['Content-Type']) { + mergedHeaders['Content-Type'] = 'application/json' + } + body = main.BodySpec.createFrom({type: 'json', raw}) + } else if (bodyType === 'form') { + const raw = encodeFormBody(formRows[activeTab]) + if (raw && !mergedHeaders['Content-Type']) { + mergedHeaders['Content-Type'] = 'application/x-www-form-urlencoded' + } + body = main.BodySpec.createFrom({type: 'form', raw}) + } else if (bodyType === 'raw') { + body = main.BodySpec.createFrom({type: 'raw', raw: rawBodies[activeTab]}) + } + + const s = settings[activeTab] + return main.RequestSpec.createFrom({ + method: request.method, + url, + headers: mergedHeaders, + body, + settings: { + timeoutMs: s.timeoutMs, + followRedirects: s.followRedirects, + verifyTLS: s.verifyTLS, + }, + }) } const storeResponse = (r: main.RequestResult) => { @@ -139,38 +270,82 @@ function App() { next[activeTab] = r setResponses(next) setResponseTab(r.Body ? 'body' : r.HeadersStr ? 'headers' : 'body') + setBodyView('pretty') + setSearch('') } function makeRequest() { - const headers: {[key: string]: string} = {} - for (const h of reqHeaders[activeTab]) { - if (h.Key && h.Value) headers[h.Key] = h.Value - } + const spec = buildSpec() setLoading(true) - MakeRequest(request.url, request.method, reqBodies[activeTab], headers) - .then((r) => storeResponse(r)) + SendBinding(spec) + .then(storeResponse) .finally(() => setLoading(false)) } function importCurl() { - RunCurl(curlBody).then((r: main.RequestResult) => { - setRequest(new Request({method: r.Method, url: r.URL})) - const newBodies = [...reqBodies] - newBodies[activeTab] = r.RequestBody || '' - setReqBodies(newBodies) - const newHeaders = [...reqHeaders] - newHeaders[activeTab] = parseHeaderLines(r.ReqHeaders || '') - setReqHeaders(newHeaders) + RunCurl(curlBody).then((r) => { + setRequest({method: r.Method || 'GET', url: r.URL || ''}) + // Headers from response + const parsed = parseHeaderLines(r.ReqHeaders || '') + const next = [...headers] + next[activeTab] = parsed.length ? parsed : initialHeaderRows() + setHeaders(next) + // Body → raw + if (r.RequestBody) { + setBodyType('raw') + setRawBody(r.RequestBody) + } + // Params synced from URL + const p = parseUrlParams(r.URL || '') + if (p) { + const np = [...params] + np[activeTab] = p + setParams(np) + } storeResponse(r) - setOpen(false) + setCurlOpen(false) setCurlBody('') }) } function handleExport() { - Export(request as any, reqHeaders, reqBodies, result) + Export( + { + method: request.method, + url: request.url, + headers: kvToRecord(headers[activeTab]), + body: rawBodies[activeTab] || jsonBodies[activeTab] || '', + } as unknown as main.Request, + headers as unknown as Array, + rawBodies, + result + ) + } + + function copyAsCurl() { + const spec = buildSpec() + const bodyType = bodyTypes[activeTab] + const bodyText = spec.body.raw + const cmd = buildCurl({ + method: spec.method, + url: spec.url, + headers: spec.headers, + body: bodyText || undefined, + bodyFlag: bodyType === 'json' ? '--data-raw' : '-d', + }) + navigator.clipboard.writeText(cmd) } + function copyBody() { + if (result.Body) navigator.clipboard.writeText(result.Body) + } + + function saveBody() { + if (result.Body) SaveTextFile('response.txt', result.Body) + } + + const matches = result.Body ? countMatches(result.Body, search) : 0 + return (
{/* ── Top bar ─────────────────────────────────────────── */} @@ -189,10 +364,14 @@ function App() {
- +
- {/* ── Request editor ──────────────────────────────────── */} -
- {/* Headers */} -
-
-
- - -
- -
-
- {reqHeaders[activeTab].map((header, index) => ( - - ))} -
-
+ + Headers + + + + Params + + + + Body + + + + Auth + + + + Settings + + + + +
+ +
+
+ {headers[activeTab].map((row, idx) => ( + + ))} +
+
+ + +
+ +
+
+ {params[activeTab].map((row, idx) => ( + + ))} +
+
+ + + + - {/* Body */} -
-
- - -
-