added request panel and collections sidebar
This commit is contained in:
261
src/lib/components/request-panel.svelte
Normal file
261
src/lib/components/request-panel.svelte
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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 SendIcon from "@lucide/svelte/icons/send";
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
import type { Request, HttpMethod } from "$lib/types/request";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
request: Request;
|
||||||
|
onSend: (request: Request) => void;
|
||||||
|
onUpdate: (request: Request) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { request, onSend, onUpdate }: Props = $props();
|
||||||
|
|
||||||
|
let localRequest = $state<Request>({ ...request });
|
||||||
|
let activeTab = $state<"params" | "headers" | "body">("params");
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
localRequest = { ...request };
|
||||||
|
});
|
||||||
|
|
||||||
|
const methods: HttpMethod[] = [
|
||||||
|
"GET",
|
||||||
|
"POST",
|
||||||
|
"PUT",
|
||||||
|
"PATCH",
|
||||||
|
"DELETE",
|
||||||
|
"HEAD",
|
||||||
|
"OPTIONS",
|
||||||
|
];
|
||||||
|
|
||||||
|
function getMethodColor(method: string): string {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
GET: "bg-green-500/10 text-green-500 hover:bg-green-500/20",
|
||||||
|
POST: "bg-blue-500/10 text-blue-500 hover:bg-blue-500/20",
|
||||||
|
PUT: "bg-orange-500/10 text-orange-500 hover:bg-orange-500/20",
|
||||||
|
PATCH: "bg-yellow-500/10 text-yellow-500 hover:bg-yellow-500/20",
|
||||||
|
DELETE: "bg-red-500/10 text-red-500 hover:bg-red-500/20",
|
||||||
|
HEAD: "bg-purple-500/10 text-purple-500 hover:bg-purple-500/20",
|
||||||
|
OPTIONS: "bg-gray-500/10 text-gray-500 hover:bg-gray-500/20",
|
||||||
|
};
|
||||||
|
return colors[method] || "bg-gray-500/10 text-gray-500";
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMethodChange(method: HttpMethod) {
|
||||||
|
localRequest.method = method;
|
||||||
|
onUpdate(localRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUrlChange(e: Event) {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
localRequest.url = target.value;
|
||||||
|
onUpdate(localRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSend() {
|
||||||
|
onSend(localRequest);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<div class="border-b p-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class={`font-mono font-semibold min-w-[100px] ${getMethodColor(localRequest.method)}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{localRequest.method}
|
||||||
|
<ChevronDownIcon class="ml-1 size-4" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
{#each methods as method (method)}
|
||||||
|
<DropdownMenu.Item onclick={() => handleMethodChange(method)}>
|
||||||
|
<span
|
||||||
|
class={`font-mono font-semibold ${getMethodColor(method).split(" ")[1]}`}
|
||||||
|
>
|
||||||
|
{method}
|
||||||
|
</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/each}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
class="flex-1 font-mono text-sm"
|
||||||
|
placeholder="Enter request URL"
|
||||||
|
value={localRequest.url}
|
||||||
|
oninput={handleUrlChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onclick={handleSend}>
|
||||||
|
<SendIcon class="size-4 mr-2" />
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<div class="border-b">
|
||||||
|
<div class="flex">
|
||||||
|
<button
|
||||||
|
class={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||||
|
activeTab === "params"
|
||||||
|
? "border-primary text-primary"
|
||||||
|
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
onclick={() => (activeTab = "params")}
|
||||||
|
>
|
||||||
|
Params
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||||
|
activeTab === "headers"
|
||||||
|
? "border-primary text-primary"
|
||||||
|
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
onclick={() => (activeTab = "headers")}
|
||||||
|
>
|
||||||
|
Headers
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||||
|
activeTab === "body"
|
||||||
|
? "border-primary text-primary"
|
||||||
|
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
onclick={() => (activeTab = "body")}
|
||||||
|
>
|
||||||
|
Body
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 p-4 overflow-auto">
|
||||||
|
{#if activeTab === "params"}
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each localRequest.params as param, i (i)}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
class="flex-1"
|
||||||
|
placeholder="Key"
|
||||||
|
value={param.key}
|
||||||
|
oninput={(e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
localRequest.params[i].key = target.value;
|
||||||
|
onUpdate(localRequest);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
class="flex-1"
|
||||||
|
placeholder="Value"
|
||||||
|
value={param.value}
|
||||||
|
oninput={(e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
localRequest.params[i].value = target.value;
|
||||||
|
onUpdate(localRequest);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => {
|
||||||
|
localRequest.params = localRequest.params.filter(
|
||||||
|
(_, idx) => idx !== i
|
||||||
|
);
|
||||||
|
onUpdate(localRequest);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => {
|
||||||
|
localRequest.params = [
|
||||||
|
...localRequest.params,
|
||||||
|
{ key: "", value: "", enabled: true },
|
||||||
|
];
|
||||||
|
onUpdate(localRequest);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Parameter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{:else if activeTab === "headers"}
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each localRequest.headers as header, i (i)}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
class="flex-1"
|
||||||
|
placeholder="Key"
|
||||||
|
value={header.key}
|
||||||
|
oninput={(e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
localRequest.headers[i].key = target.value;
|
||||||
|
onUpdate(localRequest);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
class="flex-1"
|
||||||
|
placeholder="Value"
|
||||||
|
value={header.value}
|
||||||
|
oninput={(e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
localRequest.headers[i].value = target.value;
|
||||||
|
onUpdate(localRequest);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => {
|
||||||
|
localRequest.headers = localRequest.headers.filter(
|
||||||
|
(_, idx) => idx !== i
|
||||||
|
);
|
||||||
|
onUpdate(localRequest);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => {
|
||||||
|
localRequest.headers = [
|
||||||
|
...localRequest.headers,
|
||||||
|
{ key: "", value: "", enabled: true },
|
||||||
|
];
|
||||||
|
onUpdate(localRequest);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Header
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{:else if activeTab === "body"}
|
||||||
|
<textarea
|
||||||
|
class="w-full h-64 p-3 font-mono text-sm border rounded-md bg-background resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
placeholder="Request body (JSON, XML, etc.)"
|
||||||
|
value={localRequest.body}
|
||||||
|
oninput={(e) => {
|
||||||
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
localRequest.body = target.value;
|
||||||
|
onUpdate(localRequest);
|
||||||
|
}}
|
||||||
|
></textarea>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
247
src/lib/components/workspace-sidebar.svelte
Normal file
247
src/lib/components/workspace-sidebar.svelte
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||||
|
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||||
|
import FolderIcon from "@lucide/svelte/icons/folder";
|
||||||
|
import FolderOpenIcon from "@lucide/svelte/icons/folder-open";
|
||||||
|
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||||
|
import MoreHorizontalIcon from "@lucide/svelte/icons/more-horizontal";
|
||||||
|
import FileIcon from "@lucide/svelte/icons/file";
|
||||||
|
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import type { Collection } from "$lib/types/collection";
|
||||||
|
import type { Request } from "$lib/types/request";
|
||||||
|
|
||||||
|
type Props = ComponentProps<typeof Sidebar.Root> & {
|
||||||
|
workspaceName: string;
|
||||||
|
collections: Collection[];
|
||||||
|
standaloneRequests: Request[];
|
||||||
|
selectedRequestId: string | null;
|
||||||
|
onRequestSelect: (request: Request) => void;
|
||||||
|
onCreateCollection: () => void;
|
||||||
|
onCreateRequest: (collectionId: string | null) => void;
|
||||||
|
onEditCollection: (collection: Collection) => void;
|
||||||
|
onDeleteCollection: (collection: Collection) => void;
|
||||||
|
onEditRequest: (request: Request) => void;
|
||||||
|
onDeleteRequest: (request: Request) => void;
|
||||||
|
onBack: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
workspaceName,
|
||||||
|
collections,
|
||||||
|
standaloneRequests,
|
||||||
|
selectedRequestId,
|
||||||
|
onRequestSelect,
|
||||||
|
onCreateCollection,
|
||||||
|
onCreateRequest,
|
||||||
|
onEditCollection,
|
||||||
|
onDeleteCollection,
|
||||||
|
onEditRequest,
|
||||||
|
onDeleteRequest,
|
||||||
|
onBack,
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
function getMethodColor(method: string): string {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
GET: "text-green-500",
|
||||||
|
POST: "text-blue-500",
|
||||||
|
PUT: "text-orange-500",
|
||||||
|
PATCH: "text-yellow-500",
|
||||||
|
DELETE: "text-red-500",
|
||||||
|
HEAD: "text-purple-500",
|
||||||
|
OPTIONS: "text-gray-500",
|
||||||
|
};
|
||||||
|
return colors[method] || "text-gray-500";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Root bind:ref {...restProps}>
|
||||||
|
<Sidebar.Header>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton size="lg" onclick={onBack}>
|
||||||
|
<div
|
||||||
|
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-0.5 leading-none">
|
||||||
|
<span class="font-medium truncate">{workspaceName}</span>
|
||||||
|
<span class="text-xs text-muted-foreground">Back to workspaces</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.Header>
|
||||||
|
|
||||||
|
<Sidebar.Content class="gap-0">
|
||||||
|
<Sidebar.Group>
|
||||||
|
<Sidebar.GroupLabel class="flex items-center justify-between pr-2">
|
||||||
|
<span>Collections</span>
|
||||||
|
<button
|
||||||
|
class="hover:bg-sidebar-accent rounded p-1"
|
||||||
|
onclick={onCreateCollection}
|
||||||
|
>
|
||||||
|
<PlusIcon class="size-4" />
|
||||||
|
</button>
|
||||||
|
</Sidebar.GroupLabel>
|
||||||
|
<Sidebar.GroupContent>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
{#each collections as collection (collection.id)}
|
||||||
|
<Collapsible.Root open class="group/collapsible">
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Collapsible.Trigger class="w-full">
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Sidebar.MenuButton {...props}>
|
||||||
|
<FolderIcon
|
||||||
|
class="size-4 group-data-[state=closed]/collapsible:block hidden"
|
||||||
|
/>
|
||||||
|
<FolderOpenIcon
|
||||||
|
class="size-4 group-data-[state=open]/collapsible:block hidden"
|
||||||
|
/>
|
||||||
|
<span class="truncate">{collection.name}</span>
|
||||||
|
<ChevronRightIcon
|
||||||
|
class="ms-auto size-4 transition-transform group-data-[state=open]/collapsible:rotate-90"
|
||||||
|
/>
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
{/snippet}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Sidebar.MenuAction {...props}>
|
||||||
|
<MoreHorizontalIcon class="size-4" />
|
||||||
|
</Sidebar.MenuAction>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content side="right" align="start">
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onclick={() => onCreateRequest(collection.id)}
|
||||||
|
>
|
||||||
|
Add Request
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onclick={() => onEditCollection(collection)}
|
||||||
|
>
|
||||||
|
Edit Collection
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="text-destructive"
|
||||||
|
onclick={() => onDeleteCollection(collection)}
|
||||||
|
>
|
||||||
|
Delete Collection
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<Sidebar.MenuSub>
|
||||||
|
{#each collection.requests as request (request.id)}
|
||||||
|
<Sidebar.MenuSubItem>
|
||||||
|
<Sidebar.MenuSubButton
|
||||||
|
isActive={selectedRequestId === request.id}
|
||||||
|
onclick={() => onRequestSelect(request)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class={`text-xs font-mono font-semibold ${getMethodColor(request.method)}`}
|
||||||
|
>
|
||||||
|
{request.method.substring(0, 3)}
|
||||||
|
</span>
|
||||||
|
<span class="truncate">{request.name}</span>
|
||||||
|
</Sidebar.MenuSubButton>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Sidebar.MenuAction {...props}>
|
||||||
|
<MoreHorizontalIcon class="size-4" />
|
||||||
|
</Sidebar.MenuAction>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content side="right" align="start">
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onclick={() => onEditRequest(request)}
|
||||||
|
>
|
||||||
|
Edit Request
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="text-destructive"
|
||||||
|
onclick={() => onDeleteRequest(request)}
|
||||||
|
>
|
||||||
|
Delete Request
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Sidebar.MenuSubItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.MenuSub>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.GroupContent>
|
||||||
|
</Sidebar.Group>
|
||||||
|
|
||||||
|
<Sidebar.Separator />
|
||||||
|
|
||||||
|
<Sidebar.Group>
|
||||||
|
<Sidebar.GroupLabel class="flex items-center justify-between pr-2">
|
||||||
|
<span>Requests</span>
|
||||||
|
<button
|
||||||
|
class="hover:bg-sidebar-accent rounded p-1"
|
||||||
|
onclick={() => onCreateRequest(null)}
|
||||||
|
>
|
||||||
|
<PlusIcon class="size-4" />
|
||||||
|
</button>
|
||||||
|
</Sidebar.GroupLabel>
|
||||||
|
<Sidebar.GroupContent>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
{#each standaloneRequests as request (request.id)}
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton
|
||||||
|
isActive={selectedRequestId === request.id}
|
||||||
|
onclick={() => onRequestSelect(request)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class={`text-xs font-mono font-semibold ${getMethodColor(request.method)}`}
|
||||||
|
>
|
||||||
|
{request.method.substring(0, 3)}
|
||||||
|
</span>
|
||||||
|
<span class="truncate">{request.name}</span>
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Sidebar.MenuAction {...props}>
|
||||||
|
<MoreHorizontalIcon class="size-4" />
|
||||||
|
</Sidebar.MenuAction>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content side="right" align="start">
|
||||||
|
<DropdownMenu.Item onclick={() => onEditRequest(request)}>
|
||||||
|
Edit Request
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="text-destructive"
|
||||||
|
onclick={() => onDeleteRequest(request)}
|
||||||
|
>
|
||||||
|
Delete Request
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.GroupContent>
|
||||||
|
</Sidebar.Group>
|
||||||
|
</Sidebar.Content>
|
||||||
|
|
||||||
|
<Sidebar.Rail />
|
||||||
|
</Sidebar.Root>
|
||||||
180
src/lib/services/collections.ts
Normal file
180
src/lib/services/collections.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import type { Collection } from "$lib/types/collection";
|
||||||
|
import type { Request, HttpMethod } from "$lib/types/request";
|
||||||
|
|
||||||
|
const collections: Map<string, Collection> = new Map<string, Collection>([
|
||||||
|
[
|
||||||
|
"col-1",
|
||||||
|
{
|
||||||
|
id: "col-1",
|
||||||
|
name: "User API",
|
||||||
|
description: "User management endpoints",
|
||||||
|
workspaceId: "1",
|
||||||
|
requests: [
|
||||||
|
{
|
||||||
|
id: "req-1",
|
||||||
|
name: "Get Users",
|
||||||
|
method: "GET",
|
||||||
|
url: "https://api.example.com/users",
|
||||||
|
headers: [],
|
||||||
|
params: [],
|
||||||
|
body: "",
|
||||||
|
collectionId: "col-1",
|
||||||
|
workspaceId: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "req-2",
|
||||||
|
name: "Create User",
|
||||||
|
method: "POST",
|
||||||
|
url: "https://api.example.com/users",
|
||||||
|
headers: [
|
||||||
|
{ key: "Content-Type", value: "application/json", enabled: true },
|
||||||
|
],
|
||||||
|
params: [],
|
||||||
|
body: '{"name": "John", "email": "john@example.com"}',
|
||||||
|
collectionId: "col-1",
|
||||||
|
workspaceId: "1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"col-2",
|
||||||
|
{
|
||||||
|
id: "col-2",
|
||||||
|
name: "Products API",
|
||||||
|
description: "Product catalog endpoints",
|
||||||
|
workspaceId: "1",
|
||||||
|
requests: [
|
||||||
|
{
|
||||||
|
id: "req-3",
|
||||||
|
name: "List Products",
|
||||||
|
method: "GET",
|
||||||
|
url: "https://api.example.com/products",
|
||||||
|
headers: [],
|
||||||
|
params: [{ key: "limit", value: "10", enabled: true }],
|
||||||
|
body: "",
|
||||||
|
collectionId: "col-2",
|
||||||
|
workspaceId: "1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const standaloneRequests: Map<string, Request> = new Map<string, Request>([
|
||||||
|
[
|
||||||
|
"req-standalone-1",
|
||||||
|
{
|
||||||
|
id: "req-standalone-1",
|
||||||
|
name: "Health Check",
|
||||||
|
method: "GET",
|
||||||
|
url: "https://api.example.com/health",
|
||||||
|
headers: [],
|
||||||
|
params: [],
|
||||||
|
body: "",
|
||||||
|
collectionId: null,
|
||||||
|
workspaceId: "1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export async function get_collections_by_workspace(
|
||||||
|
workspaceId: string
|
||||||
|
): Promise<Collection[]> {
|
||||||
|
return [...collections.values()].filter((c) => c.workspaceId === workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_collection(
|
||||||
|
id: string
|
||||||
|
): Promise<Collection | undefined> {
|
||||||
|
return collections.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create_collection(
|
||||||
|
collection: Omit<Collection, "id" | "requests">
|
||||||
|
): Promise<Collection> {
|
||||||
|
const newCollection: Collection = {
|
||||||
|
...collection,
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
requests: [],
|
||||||
|
};
|
||||||
|
collections.set(newCollection.id, newCollection);
|
||||||
|
return newCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update_collection(
|
||||||
|
id: string,
|
||||||
|
updates: Partial<Pick<Collection, "name" | "description">>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const collection = collections.get(id);
|
||||||
|
if (!collection) return false;
|
||||||
|
|
||||||
|
if (updates.name !== undefined) collection.name = updates.name;
|
||||||
|
if (updates.description !== undefined)
|
||||||
|
collection.description = updates.description;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function delete_collection(id: string): Promise<boolean> {
|
||||||
|
return collections.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_standalone_requests_by_workspace(
|
||||||
|
workspaceId: string
|
||||||
|
): Promise<Request[]> {
|
||||||
|
return [...standaloneRequests.values()].filter(
|
||||||
|
(r) => r.workspaceId === workspaceId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_request(id: string): Promise<Request | undefined> {
|
||||||
|
for (const collection of collections.values()) {
|
||||||
|
const request = collection.requests.find((r) => r.id === id);
|
||||||
|
if (request) return request;
|
||||||
|
}
|
||||||
|
return standaloneRequests.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create_request(
|
||||||
|
request: Omit<Request, "id">
|
||||||
|
): Promise<Request> {
|
||||||
|
const newRequest: Request = {
|
||||||
|
...request,
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.collectionId) {
|
||||||
|
const collection = collections.get(request.collectionId);
|
||||||
|
if (collection) {
|
||||||
|
collection.requests.push(newRequest);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
standaloneRequests.set(newRequest.id, newRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update_request(
|
||||||
|
id: string,
|
||||||
|
updates: Partial<Omit<Request, "id">>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const request = await get_request(id);
|
||||||
|
if (!request) return false;
|
||||||
|
|
||||||
|
Object.assign(request, updates);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function delete_request(id: string): Promise<boolean> {
|
||||||
|
if (standaloneRequests.delete(id)) return true;
|
||||||
|
|
||||||
|
for (const collection of collections.values()) {
|
||||||
|
const index = collection.requests.findIndex((r) => r.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
collection.requests.splice(index, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -52,6 +52,10 @@ export async function get_workspaces(): Promise<Workspace[]> {
|
|||||||
return convert_to_list(ws);
|
return convert_to_list(ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function get_workspace(id: string): Promise<Workspace | null> {
|
||||||
|
return ws.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function update_workspace(workspace: Workspace): Promise<boolean> {
|
export async function update_workspace(workspace: Workspace): Promise<boolean> {
|
||||||
let w = ws.get(workspace.Id);
|
let w = ws.get(workspace.Id);
|
||||||
if (w != undefined) {
|
if (w != undefined) {
|
||||||
|
|||||||
9
src/lib/types/collection.ts
Normal file
9
src/lib/types/collection.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Request } from "./request";
|
||||||
|
|
||||||
|
export type Collection = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
workspaceId: string;
|
||||||
|
requests: Request[];
|
||||||
|
};
|
||||||
32
src/lib/types/request.ts
Normal file
32
src/lib/types/request.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export type HttpMethod =
|
||||||
|
| "GET"
|
||||||
|
| "POST"
|
||||||
|
| "PUT"
|
||||||
|
| "PATCH"
|
||||||
|
| "DELETE"
|
||||||
|
| "HEAD"
|
||||||
|
| "OPTIONS";
|
||||||
|
|
||||||
|
export type RequestHeader = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RequestParam = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Request = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
method: HttpMethod;
|
||||||
|
url: string;
|
||||||
|
headers: RequestHeader[];
|
||||||
|
params: RequestParam[];
|
||||||
|
body: string;
|
||||||
|
collectionId: string | null;
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
@@ -1,23 +1,344 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
|
import { Label } from "$lib/components/ui/label/index.js";
|
||||||
|
import WorkspaceSidebar from "$lib/components/workspace-sidebar.svelte";
|
||||||
|
import RequestPanel from "$lib/components/request-panel.svelte";
|
||||||
|
import * as Empty from "$lib/components/ui/empty/index.js";
|
||||||
|
import SendIcon from "@lucide/svelte/icons/send";
|
||||||
|
import { get_workspace } from "$lib/services/workspaces";
|
||||||
|
import {
|
||||||
|
get_collections_by_workspace,
|
||||||
|
get_standalone_requests_by_workspace,
|
||||||
|
create_collection,
|
||||||
|
update_collection,
|
||||||
|
delete_collection,
|
||||||
|
create_request,
|
||||||
|
update_request,
|
||||||
|
delete_request,
|
||||||
|
} from "$lib/services/collections";
|
||||||
|
import type { Workspace } from "$lib/types/workspace";
|
||||||
|
import type { Collection } from "$lib/types/collection";
|
||||||
|
import type { Request, HttpMethod } from "$lib/types/request";
|
||||||
|
|
||||||
const { params } = $props<{ params: { id: string } }>();
|
const { params } = $props<{ params: { id: string } }>();
|
||||||
|
|
||||||
|
let workspace = $state<Workspace | null>(null);
|
||||||
|
let collections = $state<Collection[]>([]);
|
||||||
|
let standaloneRequests = $state<Request[]>([]);
|
||||||
|
let selectedRequest = $state<Request | null>(null);
|
||||||
|
|
||||||
|
let collectionDialogOpen = $state(false);
|
||||||
|
let collectionDialogMode = $state<"create" | "edit">("create");
|
||||||
|
let editingCollection = $state<Collection | null>(null);
|
||||||
|
let collectionName = $state("");
|
||||||
|
let collectionDescription = $state("");
|
||||||
|
|
||||||
|
let requestDialogOpen = $state(false);
|
||||||
|
let requestDialogMode = $state<"create" | "edit">("create");
|
||||||
|
let editingRequest = $state<Request | null>(null);
|
||||||
|
let requestName = $state("");
|
||||||
|
let requestMethod = $state<HttpMethod>("GET");
|
||||||
|
let requestCollectionId = $state<string | null>(null);
|
||||||
|
|
||||||
|
let deleteDialogOpen = $state(false);
|
||||||
|
let deleteTarget = $state<{
|
||||||
|
type: "collection" | "request";
|
||||||
|
item: Collection | Request;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
workspace = await get_workspace(params.id);
|
||||||
|
collections = await get_collections_by_workspace(params.id);
|
||||||
|
standaloneRequests = await get_standalone_requests_by_workspace(params.id);
|
||||||
|
}
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
goto("/workspaces");
|
goto("/workspaces");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRequestSelect(request: Request) {
|
||||||
|
selectedRequest = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateCollection() {
|
||||||
|
collectionDialogMode = "create";
|
||||||
|
editingCollection = null;
|
||||||
|
collectionName = "";
|
||||||
|
collectionDescription = "";
|
||||||
|
collectionDialogOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditCollection(collection: Collection) {
|
||||||
|
collectionDialogMode = "edit";
|
||||||
|
editingCollection = collection;
|
||||||
|
collectionName = collection.name;
|
||||||
|
collectionDescription = collection.description;
|
||||||
|
collectionDialogOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteCollection(collection: Collection) {
|
||||||
|
deleteTarget = { type: "collection", item: collection };
|
||||||
|
deleteDialogOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateRequest(collectionId: string | null) {
|
||||||
|
requestDialogMode = "create";
|
||||||
|
editingRequest = null;
|
||||||
|
requestName = "";
|
||||||
|
requestMethod = "GET";
|
||||||
|
requestCollectionId = collectionId;
|
||||||
|
requestDialogOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditRequest(request: Request) {
|
||||||
|
requestDialogMode = "edit";
|
||||||
|
editingRequest = request;
|
||||||
|
requestName = request.name;
|
||||||
|
requestMethod = request.method;
|
||||||
|
requestCollectionId = request.collectionId;
|
||||||
|
requestDialogOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteRequest(request: Request) {
|
||||||
|
deleteTarget = { type: "request", item: request };
|
||||||
|
deleteDialogOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCollectionSubmit() {
|
||||||
|
if (collectionDialogMode === "create") {
|
||||||
|
await create_collection({
|
||||||
|
name: collectionName,
|
||||||
|
description: collectionDescription,
|
||||||
|
workspaceId: params.id,
|
||||||
|
});
|
||||||
|
} else if (editingCollection) {
|
||||||
|
await update_collection(editingCollection.id, {
|
||||||
|
name: collectionName,
|
||||||
|
description: collectionDescription,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await loadData();
|
||||||
|
collectionDialogOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRequestSubmit() {
|
||||||
|
if (requestDialogMode === "create") {
|
||||||
|
await create_request({
|
||||||
|
name: requestName,
|
||||||
|
method: requestMethod,
|
||||||
|
url: "",
|
||||||
|
headers: [],
|
||||||
|
params: [],
|
||||||
|
body: "",
|
||||||
|
collectionId: requestCollectionId,
|
||||||
|
workspaceId: params.id,
|
||||||
|
});
|
||||||
|
} else if (editingRequest) {
|
||||||
|
await update_request(editingRequest.id, {
|
||||||
|
name: requestName,
|
||||||
|
method: requestMethod,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await loadData();
|
||||||
|
requestDialogOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteConfirm() {
|
||||||
|
if (!deleteTarget) return;
|
||||||
|
|
||||||
|
if (deleteTarget.type === "collection") {
|
||||||
|
await delete_collection((deleteTarget.item as Collection).id);
|
||||||
|
} else {
|
||||||
|
const request = deleteTarget.item as Request;
|
||||||
|
await delete_request(request.id);
|
||||||
|
if (selectedRequest?.id === request.id) {
|
||||||
|
selectedRequest = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await loadData();
|
||||||
|
deleteDialogOpen = false;
|
||||||
|
deleteTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSendRequest(request: Request) {
|
||||||
|
console.log("Sending request:", request);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdateRequest(request: Request) {
|
||||||
|
await update_request(request.id, request);
|
||||||
|
await loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
const methods: HttpMethod[] = [
|
||||||
|
"GET",
|
||||||
|
"POST",
|
||||||
|
"PUT",
|
||||||
|
"PATCH",
|
||||||
|
"DELETE",
|
||||||
|
"HEAD",
|
||||||
|
"OPTIONS",
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="min-h-[calc(100vh-4rem)] p-6 space-y-4">
|
<Sidebar.Provider>
|
||||||
<h1 class="text-2xl font-semibold">Workspace ID</h1>
|
<WorkspaceSidebar
|
||||||
<p class="text-muted-foreground">
|
workspaceName={workspace?.Name ?? "Loading..."}
|
||||||
Current workspace id: <strong>{params.id}</strong>
|
{collections}
|
||||||
</p>
|
{standaloneRequests}
|
||||||
|
selectedRequestId={selectedRequest?.id ?? null}
|
||||||
|
onRequestSelect={handleRequestSelect}
|
||||||
|
onCreateCollection={handleCreateCollection}
|
||||||
|
onCreateRequest={handleCreateRequest}
|
||||||
|
onEditCollection={handleEditCollection}
|
||||||
|
onDeleteCollection={handleDeleteCollection}
|
||||||
|
onEditRequest={handleEditRequest}
|
||||||
|
onDeleteRequest={handleDeleteRequest}
|
||||||
|
onBack={goBack}
|
||||||
|
/>
|
||||||
|
|
||||||
<button
|
<Sidebar.Inset>
|
||||||
class="mt-4 inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
|
<main class="flex flex-col h-screen">
|
||||||
onclick={goBack}
|
<header class="flex h-14 shrink-0 items-center gap-2 border-b px-4">
|
||||||
>
|
<Sidebar.Trigger class="-ml-1" />
|
||||||
Back to workspaces
|
<div class="flex-1">
|
||||||
</button>
|
{#if selectedRequest}
|
||||||
</div>
|
<h1 class="text-sm font-medium">{selectedRequest.name}</h1>
|
||||||
|
{:else}
|
||||||
|
<h1 class="text-sm font-medium text-muted-foreground">
|
||||||
|
Select a request
|
||||||
|
</h1>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-hidden">
|
||||||
|
{#if selectedRequest}
|
||||||
|
<RequestPanel
|
||||||
|
request={selectedRequest}
|
||||||
|
onSend={handleSendRequest}
|
||||||
|
onUpdate={handleUpdateRequest}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Empty.Root class="flex h-full items-center justify-center">
|
||||||
|
<Empty.Header>
|
||||||
|
<Empty.Media variant="icon">
|
||||||
|
<SendIcon />
|
||||||
|
</Empty.Media>
|
||||||
|
<Empty.Title>No Request Selected</Empty.Title>
|
||||||
|
<Empty.Description>
|
||||||
|
Select a request from the sidebar or create a new one to get
|
||||||
|
started.
|
||||||
|
</Empty.Description>
|
||||||
|
</Empty.Header>
|
||||||
|
</Empty.Root>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</Sidebar.Inset>
|
||||||
|
</Sidebar.Provider>
|
||||||
|
|
||||||
|
<Dialog.Root bind:open={collectionDialogOpen}>
|
||||||
|
<Dialog.Content class="sm:max-w-[425px]">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>
|
||||||
|
{collectionDialogMode === "create"
|
||||||
|
? "Create Collection"
|
||||||
|
: "Edit Collection"}
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
{collectionDialogMode === "create"
|
||||||
|
? "Create a new collection to organize your requests."
|
||||||
|
: "Update your collection details."}
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<form class="grid gap-4 py-4" onsubmit={handleCollectionSubmit}>
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label for="collection-name" class="text-end">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="collection-name"
|
||||||
|
class="col-span-3"
|
||||||
|
bind:value={collectionName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label for="collection-description" class="text-end">Description</Label>
|
||||||
|
<Input
|
||||||
|
id="collection-description"
|
||||||
|
class="col-span-3"
|
||||||
|
bind:value={collectionDescription}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button type="submit">
|
||||||
|
{collectionDialogMode === "create" ? "Create" : "Save"}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</form>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
|
<Dialog.Root bind:open={requestDialogOpen}>
|
||||||
|
<Dialog.Content class="sm:max-w-[425px]">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>
|
||||||
|
{requestDialogMode === "create" ? "Create Request" : "Edit Request"}
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
{requestDialogMode === "create"
|
||||||
|
? "Create a new API request."
|
||||||
|
: "Update your request details."}
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<form class="grid gap-4 py-4" onsubmit={handleRequestSubmit}>
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label for="request-name" class="text-end">Name</Label>
|
||||||
|
<Input id="request-name" class="col-span-3" bind:value={requestName} />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label for="request-method" class="text-end">Method</Label>
|
||||||
|
<select
|
||||||
|
id="request-method"
|
||||||
|
class="col-span-3 flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
|
bind:value={requestMethod}
|
||||||
|
>
|
||||||
|
{#each methods as method (method)}
|
||||||
|
<option value={method}>{method}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button type="submit">
|
||||||
|
{requestDialogMode === "create" ? "Create" : "Save"}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</form>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
|
<Dialog.Root bind:open={deleteDialogOpen}>
|
||||||
|
<Dialog.Content class="sm:max-w-[425px]">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Confirm Delete</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Are you sure you want to delete this {deleteTarget?.type}? This action
|
||||||
|
cannot be undone.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button variant="outline" onclick={() => (deleteDialogOpen = false)}
|
||||||
|
>Cancel</Button
|
||||||
|
>
|
||||||
|
<Button variant="destructive" onclick={handleDeleteConfirm}>Delete</Button
|
||||||
|
>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|||||||
Reference in New Issue
Block a user