From e2a77613881e57b9d3dab5e38cc7a2523f6e2477 Mon Sep 17 00:00:00 2001 From: xyroscar Date: Mon, 24 Nov 2025 16:46:02 -0800 Subject: [PATCH] Added multiple components, added CodeMirror for body editor and added variable substitution for params and headers --- bun.lockb | Bin 70942 -> 82242 bytes package.json | 6 +- src/lib/components/code-editor.svelte | 103 +++++ src/lib/components/request-panel.svelte | 432 +++++++++++------- src/lib/components/response-panel.svelte | 176 +++++++ src/lib/components/ui/badge/badge.svelte | 50 ++ src/lib/components/ui/badge/index.ts | 2 + src/lib/components/ui/scroll-area/index.ts | 10 + .../scroll-area/scroll-area-scrollbar.svelte | 31 ++ .../ui/scroll-area/scroll-area.svelte | 43 ++ src/lib/components/ui/select/index.ts | 37 ++ .../ui/select/select-content.svelte | 42 ++ .../ui/select/select-group-heading.svelte | 21 + .../components/ui/select/select-group.svelte | 7 + .../components/ui/select/select-item.svelte | 38 ++ .../components/ui/select/select-label.svelte | 20 + .../select/select-scroll-down-button.svelte | 20 + .../ui/select/select-scroll-up-button.svelte | 20 + .../ui/select/select-separator.svelte | 18 + .../ui/select/select-trigger.svelte | 29 ++ src/lib/components/ui/spinner/index.ts | 1 + src/lib/components/ui/spinner/spinner.svelte | 14 + src/lib/components/ui/tabs/index.ts | 16 + .../components/ui/tabs/tabs-content.svelte | 17 + src/lib/components/ui/tabs/tabs-list.svelte | 20 + .../components/ui/tabs/tabs-trigger.svelte | 20 + src/lib/components/ui/tabs/tabs.svelte | 19 + src/lib/components/ui/textarea/index.ts | 7 + .../components/ui/textarea/textarea.svelte | 23 + src/lib/components/variable-input.svelte | 167 +++++++ src/lib/services/collections.ts | 8 + src/lib/services/variables.ts | 199 ++++++++ src/lib/types/request.ts | 18 + src/lib/types/response.ts | 14 + src/lib/types/variable.ts | 18 + src/routes/workspaces/[id]/+page.svelte | 86 +++- vite.config.js | 17 +- 37 files changed, 1596 insertions(+), 173 deletions(-) create mode 100644 src/lib/components/code-editor.svelte create mode 100644 src/lib/components/response-panel.svelte create mode 100644 src/lib/components/ui/badge/badge.svelte create mode 100644 src/lib/components/ui/badge/index.ts create mode 100644 src/lib/components/ui/scroll-area/index.ts create mode 100644 src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte create mode 100644 src/lib/components/ui/scroll-area/scroll-area.svelte create mode 100644 src/lib/components/ui/select/index.ts create mode 100644 src/lib/components/ui/select/select-content.svelte create mode 100644 src/lib/components/ui/select/select-group-heading.svelte create mode 100644 src/lib/components/ui/select/select-group.svelte create mode 100644 src/lib/components/ui/select/select-item.svelte create mode 100644 src/lib/components/ui/select/select-label.svelte create mode 100644 src/lib/components/ui/select/select-scroll-down-button.svelte create mode 100644 src/lib/components/ui/select/select-scroll-up-button.svelte create mode 100644 src/lib/components/ui/select/select-separator.svelte create mode 100644 src/lib/components/ui/select/select-trigger.svelte create mode 100644 src/lib/components/ui/spinner/index.ts create mode 100644 src/lib/components/ui/spinner/spinner.svelte create mode 100644 src/lib/components/ui/tabs/index.ts create mode 100644 src/lib/components/ui/tabs/tabs-content.svelte create mode 100644 src/lib/components/ui/tabs/tabs-list.svelte create mode 100644 src/lib/components/ui/tabs/tabs-trigger.svelte create mode 100644 src/lib/components/ui/tabs/tabs.svelte create mode 100644 src/lib/components/ui/textarea/index.ts create mode 100644 src/lib/components/ui/textarea/textarea.svelte create mode 100644 src/lib/components/variable-input.svelte create mode 100644 src/lib/services/variables.ts create mode 100644 src/lib/types/response.ts create mode 100644 src/lib/types/variable.ts diff --git a/bun.lockb b/bun.lockb index e0f6621ce854e61a466d6091c26f3e5bcfd23519..d29945fed128d059d3ea9518a68a9d292b8d42aa 100755 GIT binary patch delta 19079 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 S_-~zQ*>4y2j1Ck(1^*{v=o5Yb diff --git a/package.json b/package.json index ee6837c..0928bae 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,12 @@ }, "license": "MIT", "dependencies": { + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-xml": "^6.1.0", "@tauri-apps/api": "^2", - "@tauri-apps/plugin-opener": "^2" + "@tauri-apps/plugin-opener": "^2", + "svelte-codemirror-editor": "^2.1.0" }, "devDependencies": { "@internationalized/date": "^3.8.1", diff --git a/src/lib/components/code-editor.svelte b/src/lib/components/code-editor.svelte new file mode 100644 index 0000000..8a3596e --- /dev/null +++ b/src/lib/components/code-editor.svelte @@ -0,0 +1,103 @@ + + +
+ +
+ + diff --git a/src/lib/components/request-panel.svelte b/src/lib/components/request-panel.svelte index 5cd2954..c74a4c8 100644 --- a/src/lib/components/request-panel.svelte +++ b/src/lib/components/request-panel.svelte @@ -2,20 +2,26 @@ import { Button } from "$lib/components/ui/button/index.js"; import { Input } from "$lib/components/ui/input/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; + import * as Select from "$lib/components/ui/select/index.js"; + import * as Tabs from "$lib/components/ui/tabs/index.js"; import SendIcon from "@lucide/svelte/icons/send"; import ChevronDownIcon from "@lucide/svelte/icons/chevron-down"; - import type { Request, HttpMethod } from "$lib/types/request"; + import XIcon from "@lucide/svelte/icons/x"; + import CodeEditor from "./code-editor.svelte"; + import VariableInput from "./variable-input.svelte"; + import type { Request, HttpMethod, BodyType } from "$lib/types/request"; + import type { ResolvedVariable } from "$lib/types/variable"; type Props = { request: Request; + variables?: ResolvedVariable[]; onSend: (request: Request) => void; onUpdate: (request: Request) => void; }; - let { request, onSend, onUpdate }: Props = $props(); + let { request, variables = [], onSend, onUpdate }: Props = $props(); let localRequest = $state({ ...request }); - let activeTab = $state<"params" | "headers" | "body">("params"); $effect(() => { localRequest = { ...request }; @@ -30,6 +36,15 @@ "HEAD", "OPTIONS", ]; + const bodyTypes: { value: BodyType; label: string }[] = [ + { value: "none", label: "None" }, + { value: "json", label: "JSON" }, + { value: "xml", label: "XML" }, + { value: "text", label: "Text" }, + { value: "html", label: "HTML" }, + { value: "form-data", label: "Form Data" }, + { value: "x-www-form-urlencoded", label: "URL Encoded" }, + ]; function getMethodColor(method: string): string { const colors: Record = { @@ -49,15 +64,39 @@ onUpdate(localRequest); } - function handleUrlChange(e: Event) { - const target = e.target as HTMLInputElement; - localRequest.url = target.value; + function handleUrlChange(value: string) { + localRequest.url = value; + onUpdate(localRequest); + } + + function handleBodyTypeChange(value: BodyType) { + localRequest.bodyType = value; + onUpdate(localRequest); + } + + function handleBodyChange(value: string) { + localRequest.body = value; onUpdate(localRequest); } function handleSend() { onSend(localRequest); } + + function formatBody() { + if (localRequest.bodyType === "json") { + try { + localRequest.body = JSON.stringify( + JSON.parse(localRequest.body), + null, + 2 + ); + onUpdate(localRequest); + } catch { + // Invalid JSON, don't format + } + } + }
@@ -89,10 +128,11 @@ - @@ -103,159 +143,229 @@
-
-
-
- - - -
-
+ + + + Params + + + Headers + + + Body + + -
- {#if activeTab === "params"} -
- {#each localRequest.params as param, i (i)} -
- { - const target = e.target as HTMLInputElement; - localRequest.params[i].key = target.value; - onUpdate(localRequest); - }} - /> - { - const target = e.target as HTMLInputElement; - localRequest.params[i].value = target.value; - onUpdate(localRequest); - }} - /> - -
- {/each} - -
- {:else if activeTab === "headers"} -
- {#each localRequest.headers as header, i (i)} -
- { - const target = e.target as HTMLInputElement; - localRequest.headers[i].key = target.value; - onUpdate(localRequest); - }} - /> - { - const target = e.target as HTMLInputElement; - localRequest.headers[i].value = target.value; - onUpdate(localRequest); - }} - /> - -
- {/each} - -
- {:else if activeTab === "body"} - - {/if} -
-
+ > + Add Parameter + + + + + +
+ {#each localRequest.headers as header, i (i)} +
+ { + localRequest.headers[i].key = value; + onUpdate(localRequest); + }} + /> + { + localRequest.headers[i].value = value; + onUpdate(localRequest); + }} + /> + +
+ {/each} + +
+
+ + +
+ Content Type: + handleBodyTypeChange(value as BodyType)} + > + + {bodyTypes.find((t) => t.value === localRequest.bodyType)?.label || + "None"} + + + {#each bodyTypes as bodyType (bodyType.value)} + {bodyType.label} + {/each} + + + {#if localRequest.bodyType === "json"} + + {/if} +
+
+ {#if localRequest.bodyType === "none"} +
+ This request does not have a body +
+ {:else if localRequest.bodyType === "form-data" || localRequest.bodyType === "x-www-form-urlencoded"} +
+ {#each localRequest.formData as item, i (i)} +
+ { + localRequest.formData[i].key = value; + onUpdate(localRequest); + }} + /> + { + localRequest.formData[i].value = value; + onUpdate(localRequest); + }} + /> + +
+ {/each} + +
+ {:else} + + {/if} +
+
+ diff --git a/src/lib/components/response-panel.svelte b/src/lib/components/response-panel.svelte new file mode 100644 index 0000000..299f493 --- /dev/null +++ b/src/lib/components/response-panel.svelte @@ -0,0 +1,176 @@ + + +
+ {#if loading} +
+
+ + Sending request... +
+
+ {:else if response} +
+ + {response.status} + {response.statusText} + + + {formatDuration(response.duration)} + + + {formatSize(response.size)} + +
+ +
+ + + + + Body + + + Headers ({response.headers.length}) + + + + +
+ +
+
+ + +
+ + + + + + + + + {#each response.headers as header (header.key)} + + + + + {/each} + +
NameValue
{header.key}{header.value}
+
+
+
+ {:else} +
+ Send a request to see the response +
+ {/if} +
diff --git a/src/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..bfaa9c5 --- /dev/null +++ b/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,50 @@ + + + + + + {@render children?.()} + diff --git a/src/lib/components/ui/badge/index.ts b/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..64e0aa9 --- /dev/null +++ b/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/src/lib/components/ui/scroll-area/index.ts b/src/lib/components/ui/scroll-area/index.ts new file mode 100644 index 0000000..e86a25b --- /dev/null +++ b/src/lib/components/ui/scroll-area/index.ts @@ -0,0 +1,10 @@ +import Scrollbar from "./scroll-area-scrollbar.svelte"; +import Root from "./scroll-area.svelte"; + +export { + Root, + Scrollbar, + //, + Root as ScrollArea, + Scrollbar as ScrollAreaScrollbar, +}; diff --git a/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte b/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte new file mode 100644 index 0000000..ff9d3cf --- /dev/null +++ b/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte @@ -0,0 +1,31 @@ + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/scroll-area/scroll-area.svelte b/src/lib/components/ui/scroll-area/scroll-area.svelte new file mode 100644 index 0000000..603d446 --- /dev/null +++ b/src/lib/components/ui/scroll-area/scroll-area.svelte @@ -0,0 +1,43 @@ + + + + + {@render children?.()} + + {#if orientation === "vertical" || orientation === "both"} + + {/if} + {#if orientation === "horizontal" || orientation === "both"} + + {/if} + + diff --git a/src/lib/components/ui/select/index.ts b/src/lib/components/ui/select/index.ts new file mode 100644 index 0000000..9e8d3e9 --- /dev/null +++ b/src/lib/components/ui/select/index.ts @@ -0,0 +1,37 @@ +import { Select as SelectPrimitive } from "bits-ui"; + +import Group from "./select-group.svelte"; +import Label from "./select-label.svelte"; +import Item from "./select-item.svelte"; +import Content from "./select-content.svelte"; +import Trigger from "./select-trigger.svelte"; +import Separator from "./select-separator.svelte"; +import ScrollDownButton from "./select-scroll-down-button.svelte"; +import ScrollUpButton from "./select-scroll-up-button.svelte"; +import GroupHeading from "./select-group-heading.svelte"; + +const Root = SelectPrimitive.Root; + +export { + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + GroupHeading, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading, +}; diff --git a/src/lib/components/ui/select/select-content.svelte b/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 0000000..1a96d3c --- /dev/null +++ b/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,42 @@ + + + + + + + {@render children?.()} + + + + diff --git a/src/lib/components/ui/select/select-group-heading.svelte b/src/lib/components/ui/select/select-group-heading.svelte new file mode 100644 index 0000000..1fab5f0 --- /dev/null +++ b/src/lib/components/ui/select/select-group-heading.svelte @@ -0,0 +1,21 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/select/select-group.svelte b/src/lib/components/ui/select/select-group.svelte new file mode 100644 index 0000000..5454fdb --- /dev/null +++ b/src/lib/components/ui/select/select-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/select/select-item.svelte b/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 0000000..123a912 --- /dev/null +++ b/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,38 @@ + + + + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + {/snippet} + diff --git a/src/lib/components/ui/select/select-label.svelte b/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 0000000..4696025 --- /dev/null +++ b/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/select/select-scroll-down-button.svelte b/src/lib/components/ui/select/select-scroll-down-button.svelte new file mode 100644 index 0000000..3629205 --- /dev/null +++ b/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/src/lib/components/ui/select/select-scroll-up-button.svelte b/src/lib/components/ui/select/select-scroll-up-button.svelte new file mode 100644 index 0000000..1aa2300 --- /dev/null +++ b/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/src/lib/components/ui/select/select-separator.svelte b/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 0000000..0eac3eb --- /dev/null +++ b/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/ui/select/select-trigger.svelte b/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 0000000..d405187 --- /dev/null +++ b/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/spinner/index.ts b/src/lib/components/ui/spinner/index.ts new file mode 100644 index 0000000..f8b1ced --- /dev/null +++ b/src/lib/components/ui/spinner/index.ts @@ -0,0 +1 @@ +export { default as Spinner } from "./spinner.svelte"; diff --git a/src/lib/components/ui/spinner/spinner.svelte b/src/lib/components/ui/spinner/spinner.svelte new file mode 100644 index 0000000..9b12131 --- /dev/null +++ b/src/lib/components/ui/spinner/spinner.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/lib/components/ui/tabs/index.ts b/src/lib/components/ui/tabs/index.ts new file mode 100644 index 0000000..12d4327 --- /dev/null +++ b/src/lib/components/ui/tabs/index.ts @@ -0,0 +1,16 @@ +import Root from "./tabs.svelte"; +import Content from "./tabs-content.svelte"; +import List from "./tabs-list.svelte"; +import Trigger from "./tabs-trigger.svelte"; + +export { + Root, + Content, + List, + Trigger, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger, +}; diff --git a/src/lib/components/ui/tabs/tabs-content.svelte b/src/lib/components/ui/tabs/tabs-content.svelte new file mode 100644 index 0000000..340d65c --- /dev/null +++ b/src/lib/components/ui/tabs/tabs-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/tabs/tabs-list.svelte b/src/lib/components/ui/tabs/tabs-list.svelte new file mode 100644 index 0000000..08932b6 --- /dev/null +++ b/src/lib/components/ui/tabs/tabs-list.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/tabs/tabs-trigger.svelte b/src/lib/components/ui/tabs/tabs-trigger.svelte new file mode 100644 index 0000000..dced992 --- /dev/null +++ b/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/tabs/tabs.svelte b/src/lib/components/ui/tabs/tabs.svelte new file mode 100644 index 0000000..ef6cada --- /dev/null +++ b/src/lib/components/ui/tabs/tabs.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/ui/textarea/index.ts b/src/lib/components/ui/textarea/index.ts new file mode 100644 index 0000000..ace797a --- /dev/null +++ b/src/lib/components/ui/textarea/index.ts @@ -0,0 +1,7 @@ +import Root from "./textarea.svelte"; + +export { + Root, + // + Root as Textarea, +}; diff --git a/src/lib/components/ui/textarea/textarea.svelte b/src/lib/components/ui/textarea/textarea.svelte new file mode 100644 index 0000000..7fcef1a --- /dev/null +++ b/src/lib/components/ui/textarea/textarea.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/variable-input.svelte b/src/lib/components/variable-input.svelte new file mode 100644 index 0000000..787a7e8 --- /dev/null +++ b/src/lib/components/variable-input.svelte @@ -0,0 +1,167 @@ + + +
+ + + {#if showSuggestions} +
+ {#each filteredVariables as variable, i (variable.name)} + + {/each} +
+ {/if} +
diff --git a/src/lib/services/collections.ts b/src/lib/services/collections.ts index 9d3703d..92139da 100644 --- a/src/lib/services/collections.ts +++ b/src/lib/services/collections.ts @@ -17,7 +17,9 @@ const collections: Map = new Map([ url: "https://api.example.com/users", headers: [], params: [], + bodyType: "none", body: "", + formData: [], collectionId: "col-1", workspaceId: "1", }, @@ -30,7 +32,9 @@ const collections: Map = new Map([ { key: "Content-Type", value: "application/json", enabled: true }, ], params: [], + bodyType: "json", body: '{"name": "John", "email": "john@example.com"}', + formData: [], collectionId: "col-1", workspaceId: "1", }, @@ -52,7 +56,9 @@ const collections: Map = new Map([ url: "https://api.example.com/products", headers: [], params: [{ key: "limit", value: "10", enabled: true }], + bodyType: "none", body: "", + formData: [], collectionId: "col-2", workspaceId: "1", }, @@ -71,7 +77,9 @@ const standaloneRequests: Map = new Map([ url: "https://api.example.com/health", headers: [], params: [], + bodyType: "none", body: "", + formData: [], collectionId: null, workspaceId: "1", }, diff --git a/src/lib/services/variables.ts b/src/lib/services/variables.ts new file mode 100644 index 0000000..b87fac4 --- /dev/null +++ b/src/lib/services/variables.ts @@ -0,0 +1,199 @@ +import type { + Variable, + VariableScope, + ResolvedVariable, +} from "$lib/types/variable"; + +const variables: Map = new Map([ + [ + "var-1", + { + id: "var-1", + name: "BASE_URL", + value: "https://api.example.com", + scope: "global", + scopeId: null, + isSecret: false, + description: "Base URL for all API requests", + }, + ], + [ + "var-2", + { + id: "var-2", + name: "API_KEY", + value: "sk-1234567890", + scope: "global", + scopeId: null, + isSecret: true, + description: "API authentication key", + }, + ], + [ + "var-3", + { + id: "var-3", + name: "USER_ID", + value: "user-123", + scope: "workspace", + scopeId: "1", + isSecret: false, + description: "Default user ID for testing", + }, + ], + [ + "var-4", + { + id: "var-4", + name: "AUTH_TOKEN", + value: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", + scope: "workspace", + scopeId: "1", + isSecret: true, + description: "Authentication token", + }, + ], +]); + +export async function get_all_variables(): Promise { + return [...variables.values()]; +} + +export async function get_variables_by_scope( + scope: VariableScope, + scopeId: string | null +): Promise { + return [...variables.values()].filter( + (v) => v.scope === scope && v.scopeId === scopeId + ); +} + +export async function get_global_variables(): Promise { + return get_variables_by_scope("global", null); +} + +export async function get_workspace_variables( + workspaceId: string +): Promise { + return get_variables_by_scope("workspace", workspaceId); +} + +export async function get_collection_variables( + collectionId: string +): Promise { + return get_variables_by_scope("collection", collectionId); +} + +export async function get_request_variables( + requestId: string +): Promise { + return get_variables_by_scope("request", requestId); +} + +export async function get_resolved_variables( + workspaceId: string, + collectionId: string | null, + requestId: string | null +): Promise { + const resolved: Map = new Map(); + + const globalVars = await get_global_variables(); + for (const v of globalVars) { + resolved.set(v.name, { + name: v.name, + value: v.value, + scope: v.scope, + isSecret: v.isSecret, + }); + } + + const workspaceVars = await get_workspace_variables(workspaceId); + for (const v of workspaceVars) { + resolved.set(v.name, { + name: v.name, + value: v.value, + scope: v.scope, + isSecret: v.isSecret, + }); + } + + if (collectionId) { + const collectionVars = await get_collection_variables(collectionId); + for (const v of collectionVars) { + resolved.set(v.name, { + name: v.name, + value: v.value, + scope: v.scope, + isSecret: v.isSecret, + }); + } + } + + if (requestId) { + const requestVars = await get_request_variables(requestId); + for (const v of requestVars) { + resolved.set(v.name, { + name: v.name, + value: v.value, + scope: v.scope, + isSecret: v.isSecret, + }); + } + } + + return [...resolved.values()]; +} + +export async function get_variable(id: string): Promise { + return variables.get(id); +} + +export async function create_variable( + variable: Omit +): Promise { + const newVariable: Variable = { + ...variable, + id: crypto.randomUUID(), + }; + variables.set(newVariable.id, newVariable); + return newVariable; +} + +export async function update_variable( + id: string, + updates: Partial> +): Promise { + const variable = variables.get(id); + if (!variable) return false; + + Object.assign(variable, updates); + return true; +} + +export async function delete_variable(id: string): Promise { + return variables.delete(id); +} + +export function interpolate_variables( + text: string, + resolvedVariables: ResolvedVariable[] +): string { + let result = text; + for (const variable of resolvedVariables) { + const pattern = new RegExp(`\\{\\{\\s*${variable.name}\\s*\\}\\}`, "g"); + result = result.replace(pattern, variable.value); + } + return result; +} + +export function extract_variable_names(text: string): string[] { + const pattern = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g; + const matches: string[] = []; + let match; + while ((match = pattern.exec(text)) !== null) { + if (!matches.includes(match[1])) { + matches.push(match[1]); + } + } + return matches; +} diff --git a/src/lib/types/request.ts b/src/lib/types/request.ts index 385fb37..6d47697 100644 --- a/src/lib/types/request.ts +++ b/src/lib/types/request.ts @@ -7,6 +7,15 @@ export type HttpMethod = | "HEAD" | "OPTIONS"; +export type BodyType = + | "none" + | "json" + | "xml" + | "text" + | "html" + | "form-data" + | "x-www-form-urlencoded"; + export type RequestHeader = { key: string; value: string; @@ -19,6 +28,13 @@ export type RequestParam = { enabled: boolean; }; +export type FormDataItem = { + key: string; + value: string; + type: "text" | "file"; + enabled: boolean; +}; + export type Request = { id: string; name: string; @@ -26,7 +42,9 @@ export type Request = { url: string; headers: RequestHeader[]; params: RequestParam[]; + bodyType: BodyType; body: string; + formData: FormDataItem[]; collectionId: string | null; workspaceId: string; }; diff --git a/src/lib/types/response.ts b/src/lib/types/response.ts new file mode 100644 index 0000000..8b44542 --- /dev/null +++ b/src/lib/types/response.ts @@ -0,0 +1,14 @@ +export type ResponseHeader = { + key: string; + value: string; +}; + +export type Response = { + status: number; + statusText: string; + headers: ResponseHeader[]; + body: string; + contentType: string; + duration: number; + size: number; +}; diff --git a/src/lib/types/variable.ts b/src/lib/types/variable.ts new file mode 100644 index 0000000..f217164 --- /dev/null +++ b/src/lib/types/variable.ts @@ -0,0 +1,18 @@ +export type VariableScope = "global" | "workspace" | "collection" | "request"; + +export type Variable = { + id: string; + name: string; + value: string; + scope: VariableScope; + scopeId: string | null; + isSecret: boolean; + description?: string; +}; + +export type ResolvedVariable = { + name: string; + value: string; + scope: VariableScope; + isSecret: boolean; +}; diff --git a/src/routes/workspaces/[id]/+page.svelte b/src/routes/workspaces/[id]/+page.svelte index 8796d04..ace3479 100644 --- a/src/routes/workspaces/[id]/+page.svelte +++ b/src/routes/workspaces/[id]/+page.svelte @@ -21,9 +21,13 @@ update_request, delete_request, } from "$lib/services/collections"; + import { get_resolved_variables } from "$lib/services/variables"; import type { Workspace } from "$lib/types/workspace"; import type { Collection } from "$lib/types/collection"; import type { Request, HttpMethod } from "$lib/types/request"; + import type { Response } from "$lib/types/response"; + import type { ResolvedVariable } from "$lib/types/variable"; + import ResponsePanel from "$lib/components/response-panel.svelte"; const { params } = $props<{ params: { id: string } }>(); @@ -31,6 +35,9 @@ let collections = $state([]); let standaloneRequests = $state([]); let selectedRequest = $state(null); + let response = $state(null); + let loading = $state(false); + let resolvedVariables = $state([]); let collectionDialogOpen = $state(false); let collectionDialogMode = $state<"create" | "edit">("create"); @@ -59,6 +66,7 @@ workspace = await get_workspace(params.id); collections = await get_collections_by_workspace(params.id); standaloneRequests = await get_standalone_requests_by_workspace(params.id); + resolvedVariables = await get_resolved_variables(params.id, null, null); } function goBack() { @@ -67,6 +75,7 @@ function handleRequestSelect(request: Request) { selectedRequest = request; + response = null; } function handleCreateCollection() { @@ -138,7 +147,9 @@ url: "", headers: [], params: [], + bodyType: "none", body: "", + formData: [], collectionId: requestCollectionId, workspaceId: params.id, }); @@ -169,8 +180,59 @@ deleteTarget = null; } - function handleSendRequest(request: Request) { - console.log("Sending request:", request); + async function handleSendRequest(request: Request) { + loading = true; + response = null; + + // Simulate API request (will be replaced with actual Tauri call later) + const startTime = Date.now(); + + try { + // Mock response for now + await new Promise((resolve) => + setTimeout(resolve, 500 + Math.random() * 1000) + ); + + const mockBody = JSON.stringify( + { + success: true, + message: "This is a mock response", + data: { + id: 1, + name: "Example", + timestamp: new Date().toISOString(), + }, + }, + null, + 2 + ); + + response = { + status: 200, + statusText: "OK", + headers: [ + { key: "Content-Type", value: "application/json" }, + { key: "X-Request-Id", value: crypto.randomUUID() }, + { key: "Cache-Control", value: "no-cache" }, + ], + body: mockBody, + contentType: "application/json", + duration: Date.now() - startTime, + size: new Blob([mockBody]).size, + }; + } catch (error) { + response = { + status: 500, + statusText: "Error", + headers: [], + body: JSON.stringify({ error: "Request failed" }), + contentType: "application/json", + duration: Date.now() - startTime, + size: 0, + }; + } finally { + loading = false; + } } async function handleUpdateRequest(request: Request) { @@ -220,13 +282,21 @@ -
+
{#if selectedRequest} - +
+
+ +
+
+ +
+
{:else} diff --git a/vite.config.js b/vite.config.js index a114008..b124783 100644 --- a/vite.config.js +++ b/vite.config.js @@ -8,6 +8,15 @@ const host = process.env.TAURI_DEV_HOST; // https://vitejs.dev/config/ export default defineConfig(async () => ({ plugins: [tailwindcss(), sveltekit()], + optimizeDeps: { + exclude: [ + "svelte-codemirror-editor", + "codemirror", + "@codemirror/lang-json", + "@codemirror/lang-xml", + "@codemirror/lang-html", + ], + }, // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent vite from obscuring rust errors @@ -18,7 +27,9 @@ export default defineConfig(async () => ({ strictPort: true, host: host || false, hmr: host ? { protocol: "ws", host, port: 1421 } : undefined, - watch: { // 3. tell vite to ignore watching `src-tauri` - ignored: ["**/src-tauri/**"] } - } + watch: { + // 3. tell vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, }));