329 lines
10 KiB
Svelte
329 lines
10 KiB
Svelte
<script lang="ts">
|
|
import * as Empty from "$lib/components/ui/empty/index.js";
|
|
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
|
import { Button } from "$lib/components/ui/button/index.js";
|
|
import { Badge } from "$lib/components/ui/badge/index.js";
|
|
import FolderCodeIcon from "@lucide/svelte/icons/folder-code";
|
|
import SettingsIcon from "@lucide/svelte/icons/settings";
|
|
import VariableIcon from "@lucide/svelte/icons/variable";
|
|
import MoreVerticalIcon from "@lucide/svelte/icons/more-vertical";
|
|
import CopyIcon from "@lucide/svelte/icons/copy";
|
|
import PencilIcon from "@lucide/svelte/icons/pencil";
|
|
import TrashIcon from "@lucide/svelte/icons/trash-2";
|
|
import {
|
|
create_workspace,
|
|
get_workspaces,
|
|
update_workspace,
|
|
delete_workspace,
|
|
} from "$lib/services/workspaces";
|
|
import {
|
|
duplicate_workspace,
|
|
type DuplicateWorkspaceOptions,
|
|
} from "$lib/services/sync";
|
|
import type { Workspace } from "$lib/types/workspace";
|
|
import * as Card from "$lib/components/ui/card/index";
|
|
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
|
import { Input } from "$lib/components/ui/input/index.js";
|
|
import { Label } from "$lib/components/ui/label/index.js";
|
|
import { goto } from "$app/navigation";
|
|
import SettingsDialog from "$lib/components/settings-dialog.svelte";
|
|
import VariablesPanel from "$lib/components/variables-panel.svelte";
|
|
import DuplicateWorkspaceDialog from "$lib/components/duplicate-workspace-dialog.svelte";
|
|
|
|
$effect(() => {
|
|
loadWorkspaces();
|
|
});
|
|
|
|
async function loadWorkspaces() {
|
|
const ws = await get_workspaces();
|
|
if (ws.length == 0) {
|
|
showPrompt = true;
|
|
} else {
|
|
workspaces = ws;
|
|
showPrompt = false;
|
|
}
|
|
}
|
|
|
|
let showPrompt = $state(false);
|
|
let workspaces = $state<Workspace[]>([]);
|
|
let dialogOpen = $state(false);
|
|
let dialogMode = $state<"create" | "edit">("create");
|
|
let selectedWorkspace = $state<Workspace | null>(null);
|
|
let workspaceName = $state("");
|
|
let workspaceDescription = $state("");
|
|
|
|
let settingsOpen = $state(false);
|
|
let variablesOpen = $state(false);
|
|
let duplicateDialogOpen = $state(false);
|
|
let workspaceToDuplicate = $state<Workspace | null>(null);
|
|
let deleteDialogOpen = $state(false);
|
|
let workspaceToDelete = $state<Workspace | null>(null);
|
|
|
|
function openCreateDialog() {
|
|
dialogMode = "create";
|
|
selectedWorkspace = null;
|
|
workspaceName = "";
|
|
workspaceDescription = "";
|
|
dialogOpen = true;
|
|
}
|
|
|
|
function openEditDialog(workspace: Workspace) {
|
|
dialogMode = "edit";
|
|
selectedWorkspace = workspace;
|
|
workspaceName = workspace.Name ?? "";
|
|
workspaceDescription = workspace.Description ?? "";
|
|
dialogOpen = true;
|
|
}
|
|
|
|
function openDuplicateDialog(workspace: Workspace) {
|
|
workspaceToDuplicate = workspace;
|
|
duplicateDialogOpen = true;
|
|
}
|
|
|
|
function openDeleteDialog(workspace: Workspace) {
|
|
workspaceToDelete = workspace;
|
|
deleteDialogOpen = true;
|
|
}
|
|
|
|
function handleWorkspaceClick(id: string) {
|
|
goto(`/workspaces/${id}`);
|
|
}
|
|
|
|
async function handleDialogSubmit() {
|
|
if (dialogMode === "create") {
|
|
await create_workspace({
|
|
Name: workspaceName,
|
|
Description: workspaceDescription,
|
|
});
|
|
} else if (selectedWorkspace != null) {
|
|
const w: Workspace = {
|
|
Id: selectedWorkspace.Id,
|
|
Name: workspaceName,
|
|
Description: workspaceDescription,
|
|
};
|
|
await update_workspace(w);
|
|
}
|
|
|
|
await loadWorkspaces();
|
|
dialogOpen = false;
|
|
}
|
|
|
|
async function handleDuplicate(options: DuplicateWorkspaceOptions) {
|
|
await duplicate_workspace(options, async (ws) => {
|
|
return await create_workspace(ws);
|
|
});
|
|
await loadWorkspaces();
|
|
}
|
|
|
|
async function handleDeleteConfirm() {
|
|
if (workspaceToDelete) {
|
|
await delete_workspace(workspaceToDelete.Id);
|
|
await loadWorkspaces();
|
|
}
|
|
deleteDialogOpen = false;
|
|
workspaceToDelete = null;
|
|
}
|
|
</script>
|
|
|
|
<Dialog.Root bind:open={dialogOpen}>
|
|
{#if showPrompt}
|
|
<Empty.Root
|
|
class="flex min-h-[calc(100vh-4rem)] items-center justify-center"
|
|
>
|
|
<Empty.Header>
|
|
<Empty.Media variant="icon">
|
|
<FolderCodeIcon />
|
|
</Empty.Media>
|
|
<Empty.Title>No Workspaces Yet</Empty.Title>
|
|
<Empty.Description>
|
|
You haven't created any Workspaces yet. Get started by creating your
|
|
first project.
|
|
</Empty.Description>
|
|
</Empty.Header>
|
|
<Empty.Content>
|
|
<div class="flex gap-2">
|
|
<Button onclick={openCreateDialog}>Create Workspace</Button>
|
|
</div>
|
|
</Empty.Content>
|
|
</Empty.Root>
|
|
{:else}
|
|
<div class="min-h-[calc(100vh-4rem)] p-6 space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<h1 class="text-2xl font-semibold">Workspaces</h1>
|
|
<div class="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onclick={() => (variablesOpen = true)}
|
|
aria-label="Global Variables"
|
|
>
|
|
<VariableIcon class="size-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onclick={() => (settingsOpen = true)}
|
|
aria-label="Settings"
|
|
>
|
|
<SettingsIcon class="size-4" />
|
|
</Button>
|
|
<Button onclick={openCreateDialog}>Create Workspace</Button>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="grid gap-6
|
|
grid-cols-1
|
|
sm:grid-cols-2
|
|
md:grid-cols-3
|
|
xl:grid-cols-4
|
|
2xl:grid-cols-5"
|
|
>
|
|
{#each workspaces as workspace (workspace.Id)}
|
|
<Card.Root
|
|
class="min-h-40 w-full max-w-xs mx-auto flex flex-col justify-between hover:shadow-md transition-shadow group"
|
|
>
|
|
<Card.Header class="relative">
|
|
<div
|
|
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
>
|
|
<DropdownMenu.Root>
|
|
<DropdownMenu.Trigger>
|
|
{#snippet child({ props })}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
class="h-8 w-8"
|
|
{...props}
|
|
aria-label="Workspace options"
|
|
>
|
|
<MoreVerticalIcon class="size-4" />
|
|
</Button>
|
|
{/snippet}
|
|
</DropdownMenu.Trigger>
|
|
<DropdownMenu.Content align="end">
|
|
<DropdownMenu.Item
|
|
onclick={() => openEditDialog(workspace)}
|
|
>
|
|
<PencilIcon class="size-4 mr-2" />
|
|
Edit
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Item
|
|
onclick={() => openDuplicateDialog(workspace)}
|
|
>
|
|
<CopyIcon class="size-4 mr-2" />
|
|
Duplicate
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Separator />
|
|
<DropdownMenu.Item
|
|
class="text-destructive"
|
|
onclick={() => openDeleteDialog(workspace)}
|
|
>
|
|
<TrashIcon class="size-4 mr-2" />
|
|
Delete
|
|
</DropdownMenu.Item>
|
|
</DropdownMenu.Content>
|
|
</DropdownMenu.Root>
|
|
</div>
|
|
<Card.Title class="truncate pr-8">{workspace.Name}</Card.Title>
|
|
<Card.Description
|
|
class="text-xs text-muted-foreground line-clamp-2"
|
|
>
|
|
{workspace.Description}
|
|
</Card.Description>
|
|
{#if workspace.environment}
|
|
<Badge variant="secondary" class="w-fit mt-2"
|
|
>{workspace.environment}</Badge
|
|
>
|
|
{/if}
|
|
</Card.Header>
|
|
<Card.Footer class="flex items-center justify-center gap-2">
|
|
<Button
|
|
size="sm"
|
|
class="justify-center flex-1"
|
|
onclick={() => handleWorkspaceClick(workspace.Id)}
|
|
>
|
|
Open
|
|
</Button>
|
|
</Card.Footer>
|
|
</Card.Root>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<Dialog.Content class="sm:max-w-[425px]">
|
|
<Dialog.Header>
|
|
<Dialog.Title>
|
|
{dialogMode === "create" ? "Create workspace" : "Edit workspace"}
|
|
</Dialog.Title>
|
|
<Dialog.Description>
|
|
{#if dialogMode === "create"}
|
|
Create a new workspace. Fill in the details below and click create.
|
|
{:else}
|
|
Update your workspace details and click save.
|
|
{/if}
|
|
</Dialog.Description>
|
|
</Dialog.Header>
|
|
<form class="grid gap-4 py-4" onsubmit={handleDialogSubmit}>
|
|
<div class="grid grid-cols-4 items-center gap-4">
|
|
<Label for="workspace-name" class="text-end">Name</Label>
|
|
<Input
|
|
id="workspace-name"
|
|
class="col-span-3"
|
|
bind:value={workspaceName}
|
|
/>
|
|
</div>
|
|
<div class="grid grid-cols-4 items-center gap-4">
|
|
<Label for="workspace-description" class="text-end">Description</Label>
|
|
<Input
|
|
id="workspace-description"
|
|
class="col-span-3"
|
|
bind:value={workspaceDescription}
|
|
/>
|
|
</div>
|
|
<Dialog.Footer>
|
|
<Button type="submit">
|
|
{dialogMode === "create" ? "Create workspace" : "Save changes"}
|
|
</Button>
|
|
</Dialog.Footer>
|
|
</form>
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|
|
|
|
<Dialog.Root bind:open={deleteDialogOpen}>
|
|
<Dialog.Content class="sm:max-w-[425px]">
|
|
<Dialog.Header>
|
|
<Dialog.Title>Delete Workspace</Dialog.Title>
|
|
<Dialog.Description>
|
|
Are you sure you want to delete "{workspaceToDelete?.Name}"? This will
|
|
permanently delete all collections, requests, and variables in this
|
|
workspace.
|
|
</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>
|
|
|
|
<SettingsDialog
|
|
bind:open={settingsOpen}
|
|
onOpenChange={(v) => (settingsOpen = v)}
|
|
/>
|
|
|
|
<VariablesPanel
|
|
bind:open={variablesOpen}
|
|
onOpenChange={(v) => (variablesOpen = v)}
|
|
/>
|
|
|
|
<DuplicateWorkspaceDialog
|
|
bind:open={duplicateDialogOpen}
|
|
onOpenChange={(v) => (duplicateDialogOpen = v)}
|
|
sourceWorkspace={workspaceToDuplicate}
|
|
onDuplicate={handleDuplicate}
|
|
/>
|