feat: show confirm when deleting project

This commit is contained in:
2026-05-07 17:01:22 +02:00
parent 84afd15746
commit 73155dcb46
3 changed files with 137 additions and 23 deletions
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { defaultPlant, lottaFaces, plant, simple } from '$lib/graph-templates'; import { defaultPlant, lottaFaces, plant, simple } from '$lib/graph-templates';
import type { Graph } from '$lib/types'; import type { Graph } from '$lib/types';
import { InputSelect } from '@nodarium/ui'; import { Button, ConfirmDialog, InputSelect, Spinner } from '@nodarium/ui';
import type { ProjectManager } from './project-manager.svelte'; import type { ProjectManager } from './project-manager.svelte';
const { projectManager } = $props<{ projectManager: ProjectManager }>(); const { projectManager } = $props<{ projectManager: ProjectManager }>();
@@ -31,16 +31,27 @@
newProjectName = ''; newProjectName = '';
showNewProject = false; showNewProject = false;
} }
let pendingDeleteId = $state<number | null>(null);
let confirmOpen = $state(false);
function requestDelete(id: number, e: MouseEvent) {
e.stopPropagation();
pendingDeleteId = id;
confirmOpen = true;
}
function confirmDelete() {
if (pendingDeleteId !== null) {
projectManager.handleDeleteProject(pendingDeleteId);
pendingDeleteId = null;
}
}
</script> </script>
<header class="flex justify-between px-4 h-[70px] border-b-1 border-outline items-center bg-layer-2"> <header class="flex justify-between px-4 h-[70px] border-b-1 border-outline items-center bg-layer-2">
<h3>Project</h3> <h3>Project</h3>
<button <Button onclick={() => (showNewProject = !showNewProject)}>New</Button>
class="px-3 py-1 bg-layer-1 rounded"
onclick={() => (showNewProject = !showNewProject)}
>
New
</button>
</header> </header>
{#if showNewProject} {#if showNewProject}
@@ -53,20 +64,11 @@
onkeydown={(e) => e.key === 'Enter' && handleCreate()} onkeydown={(e) => e.key === 'Enter' && handleCreate()}
/> />
<InputSelect options={templates.map(t => t.name)} bind:value={selectedTemplateIndex} /> <InputSelect options={templates.map(t => t.name)} bind:value={selectedTemplateIndex} />
<button <Button variant="primary" class="self-end" onclick={() => handleCreate()}>Create</Button>
class="cursor-pointer self-end px-3 py-1 bg-selected rounded"
onclick={() => handleCreate()}
>
Create
</button>
</div> </div>
{/if} {/if}
<div class="text-white min-h-screen"> <div class="text-white min-h-screen">
{#if projectManager.loading}
<p>Loading...</p>
{/if}
<ul> <ul>
{#each projectManager.projects as project (project.id)} {#each projectManager.projects as project (project.id)}
<li> <li>
@@ -89,16 +91,35 @@
<div class="flex justify-between items-center grow"> <div class="flex justify-between items-center grow">
<span>{project.meta?.title || 'Untitled'}</span> <span>{project.meta?.title || 'Untitled'}</span>
<button <button
class="text-layer-1! bg-red-500 w-7 text-xl rounded-sm cursor-pointer opacity-20 hover:opacity-80" class="opacity-20 hover:opacity-70 transition-opacity cursor-pointer p-1 rounded text-red-400"
onclick={() => { onclick={(e) => requestDelete(project.id!, e)}
projectManager.handleDeleteProject(project.id!); aria-label="Delete project"
}}
> >
× <span class="i-[tabler--trash] w-4 h-4 block"></span>
</button> </button>
</div> </div>
</div> </div>
</li> </li>
{:else}
{#if projectManager.loading}
<div class="flex items-center gap-2 p-4">
<Spinner size={12} />
<p>Loading</p>
</div>
{:else}
<li class="px-4 py-8 text-center opacity-40 text-sm">
No projects yet.<br />Press <b>New</b> to create one.
</li>
{/if}
{/each} {/each}
</ul> </ul>
</div> </div>
<ConfirmDialog
bind:open={confirmOpen}
title="Delete project?"
message="This cannot be undone. The project and all its data will be permanently removed."
confirmLabel="Delete"
cancelLabel="Cancel"
onconfirm={confirmDelete}
/>
@@ -10,7 +10,9 @@ export class ProjectManager {
'node.activeProjectId', 'node.activeProjectId',
undefined undefined
); );
public readonly loading = $derived(this.graph?.id !== this.activeProjectId.value); public readonly loading = $derived(
this.projects.length && this.graph?.id !== this.activeProjectId.value
);
constructor() { constructor() {
this.init(); this.init();
+91
View File
@@ -0,0 +1,91 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import Button from './Button.svelte';
interface Props {
open?: boolean;
title?: string;
message?: string;
confirmLabel?: string;
cancelLabel?: string;
onconfirm?: () => void;
oncancel?: () => void;
children?: Snippet;
}
let {
open = $bindable(false),
title = 'Are you sure?',
message,
confirmLabel = 'Confirm',
cancelLabel = 'Cancel',
onconfirm,
oncancel,
children
}: Props = $props();
let dialogEl: HTMLDialogElement;
$effect(() => {
if (!dialogEl) return;
if (open) {
dialogEl.showModal();
} else {
dialogEl.close();
}
});
function confirm() {
open = false;
onconfirm?.();
}
function cancel() {
open = false;
oncancel?.();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') { e.preventDefault(); confirm(); }
}
function handleCancel(e: Event) {
e.preventDefault();
cancel();
}
</script>
<!-- svelte-ignore a11y_interactive_supports_focus -->
<dialog
bind:this={dialogEl}
class="m-auto bg-layer-1 border border-outline rounded-md p-0 text-text max-w-md w-full backdrop:bg-black/50"
oncancel={handleCancel}
onkeydown={handleKeydown}
onclick={(e) => { if (e.target === dialogEl) cancel(); }}
>
<div class="px-6 py-5 flex flex-col gap-3">
<h3 class="m-0 text-sm font-semibold">{title}</h3>
{#if message}
<p class="m-0 text-xs opacity-75 leading-relaxed">{message}</p>
{/if}
{#if children}
<div class="text-xs">
{@render children()}
</div>
{/if}
<div class="flex justify-end gap-2 mt-1">
<Button onclick={cancel}>{cancelLabel}</Button>
<Button variant="primary" onclick={confirm}>{confirmLabel}</Button>
</div>
</div>
</dialog>
<style>
dialog {
font-family: var(--font-family);
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
</style>