feat: show confirm when deleting project
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user