adding duplication and syncing of workspaces, a settings dialog and variable listing and editing

This commit is contained in:
xyroscar
2025-11-26 11:25:57 -08:00
parent e2a7761388
commit ce75694ffb
9 changed files with 1587 additions and 35 deletions

View File

@@ -1,30 +1,49 @@
<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(() => {
get_workspaces().then((ws) => {
if (ws.length == 0) {
showPrompt = true;
} else {
console.log(ws);
workspaces = ws;
}
});
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);
@@ -33,6 +52,13 @@
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;
@@ -49,37 +75,54 @@
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}`)
goto(`/workspaces/${id}`);
}
async function handleDialogSubmit() {
let success = false;
if (dialogMode === "create") {
const w: Workspace = {
// Id will be assigned in create_workspace
Id: "",
await create_workspace({
Name: workspaceName,
Description: workspaceDescription,
};
success = await create_workspace(w);
});
} else if (selectedWorkspace != null) {
const w: Workspace = {
Id: selectedWorkspace.Id,
Name: workspaceName,
Description: workspaceDescription,
};
success = await update_workspace(w);
}
if (success) {
const updated = await get_workspaces();
workspaces = updated;
showPrompt = updated.length === 0;
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}>
@@ -107,7 +150,25 @@
<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>
<Button onclick={openCreateDialog}>Create Workspace</Button>
<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
@@ -119,22 +180,69 @@
>
{#each workspaces as workspace (workspace.Id)}
<Card.Root
class="min-h-40 w-full max-w-xs mx-auto flex flex-col justify-between cursor-pointer hover:shadow-md transition-shadow"
class="min-h-40 w-full max-w-xs mx-auto flex flex-col justify-between hover:shadow-md transition-shadow group"
>
<Card.Header>
<Card.Title class="truncate">{workspace.Name}</Card.Title>
<Card.Description class="text-xs text-muted-foreground">
<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" onclick={() => handleWorkspaceClick(workspace.Id)}>Open</Button>
<Button
size="sm"
class="justify-center"
onclick={() => openEditDialog(workspace)}
class="justify-center flex-1"
onclick={() => handleWorkspaceClick(workspace.Id)}
>
Edit
Open
</Button>
</Card.Footer>
</Card.Root>
@@ -181,3 +289,40 @@
</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}
/>

View File

@@ -10,7 +10,11 @@
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 VariableIcon from "@lucide/svelte/icons/variable";
import SettingsIcon from "@lucide/svelte/icons/settings";
import { get_workspace } from "$lib/services/workspaces";
import VariablesPanel from "$lib/components/variables-panel.svelte";
import SettingsDialog from "$lib/components/settings-dialog.svelte";
import {
get_collections_by_workspace,
get_standalone_requests_by_workspace,
@@ -39,6 +43,9 @@
let loading = $state(false);
let resolvedVariables = $state<ResolvedVariable[]>([]);
let variablesOpen = $state(false);
let settingsOpen = $state(false);
let collectionDialogOpen = $state(false);
let collectionDialogMode = $state<"create" | "edit">("create");
let editingCollection = $state<Collection | null>(null);
@@ -280,6 +287,24 @@
</h1>
{/if}
</div>
<div class="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
onclick={() => (variablesOpen = true)}
aria-label="Variables"
>
<VariableIcon class="size-4" />
</Button>
<Button
variant="ghost"
size="icon"
onclick={() => (settingsOpen = true)}
aria-label="Settings"
>
<SettingsIcon class="size-4" />
</Button>
</div>
</header>
<div class="flex-1 overflow-hidden flex flex-col">
@@ -412,3 +437,15 @@
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
<VariablesPanel
bind:open={variablesOpen}
onOpenChange={(v) => (variablesOpen = v)}
workspaceId={params.id}
collectionId={selectedRequest?.collectionId ?? undefined}
/>
<SettingsDialog
bind:open={settingsOpen}
onOpenChange={(v) => (settingsOpen = v)}
/>