Private
Public Access
1
0

Add board settings modal for editing board properties and deleting boards

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-27 18:35:00 -05:00
parent faa2608a47
commit 0e77c50411
3 changed files with 240 additions and 4 deletions
@@ -0,0 +1,189 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { trapFocus } from '$lib/utils/focus-trap.js';
import { api } from '$lib/utils/api.js';
import { toast } from '$lib/stores/toasts.svelte.js';
import BoardBackgroundPicker from './BoardBackgroundPicker.svelte';
type Props = {
board: any;
categories: any[];
canDelete: boolean;
onclose: () => void;
onsave: (updated: any) => void;
};
let { board, categories, canDelete, onclose, onsave }: Props = $props();
let title = $state(board.title);
let description = $state(board.description || '');
let visibility = $state(board.visibility);
let categoryId = $state(board.categoryId || '');
let saving = $state(false);
let confirmingDelete = $state(false);
let deleting = $state(false);
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) onclose();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') onclose();
}
async function save() {
if (!title.trim()) return;
saving = true;
const { ok, data } = await api(`/api/boards/${board.id}`, {
method: 'PATCH',
body: JSON.stringify({
title: title.trim(),
description: description.trim() || undefined,
visibility,
categoryId: categoryId || undefined
})
});
if (ok) {
toast.success('Board updated');
onsave(data.board);
}
saving = false;
}
async function deleteBoard() {
deleting = true;
const { ok } = await api(`/api/boards/${board.id}`, { method: 'DELETE' });
if (ok) {
toast.success('Board deleted');
goto('/boards');
}
deleting = false;
}
</script>
<svelte:window onkeydown={handleKeydown} />
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4 pt-12 overflow-y-auto"
onclick={handleBackdropClick}
>
<div
class="w-full max-w-lg bg-white dark:bg-gray-900 rounded-xl shadow-2xl max-sm:min-h-screen max-sm:rounded-none"
role="dialog"
aria-modal="true"
aria-labelledby="board-settings-title"
use:trapFocus
>
<!-- Header -->
<div class="flex items-center justify-between p-4 border-b border-gray-100 dark:border-gray-700">
<h2 id="board-settings-title" class="text-lg font-semibold text-gray-900 dark:text-gray-100">Board Settings</h2>
<button onclick={onclose} class="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition p-1" aria-label="Close">
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
<!-- Body -->
<div class="p-4 space-y-4">
<!-- Title -->
<div>
<label for="board-title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
<input
id="board-title"
bind:value={title}
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 px-3 py-2 text-sm focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
/>
</div>
<!-- Description -->
<div>
<label for="board-desc" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
<textarea
id="board-desc"
bind:value={description}
rows="3"
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 px-3 py-2 text-sm resize-none focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
></textarea>
</div>
<!-- Visibility -->
<div>
<label for="board-vis" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Visibility</label>
<select
id="board-vis"
bind:value={visibility}
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 px-3 py-2 text-sm focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
>
<option value="PRIVATE">Private</option>
<option value="PUBLIC">Public</option>
</select>
</div>
<!-- Category -->
{#if categories.length > 0}
<div>
<label for="board-cat" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Category</label>
<select
id="board-cat"
bind:value={categoryId}
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 px-3 py-2 text-sm focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
>
<option value="">None</option>
{#each categories as cat}
<option value={cat.id}>{cat.name}</option>
{/each}
</select>
</div>
{/if}
<!-- Background -->
<div>
<span class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Background</span>
<BoardBackgroundPicker boardId={board.id} currentBackground={board.background} />
</div>
<!-- Save -->
<button
onclick={save}
disabled={saving || !title.trim()}
class="w-full rounded-lg bg-[var(--color-primary)] px-4 py-2 text-sm font-medium text-white hover:bg-[var(--color-primary-hover)] transition disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
<!-- Delete -->
{#if canDelete}
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
<h3 class="text-sm font-medium text-[var(--color-danger)] mb-2">Danger Zone</h3>
{#if confirmingDelete}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">Are you sure? This will permanently delete the board and all its data.</p>
<div class="flex gap-2">
<button
onclick={deleteBoard}
disabled={deleting}
class="rounded-lg bg-[var(--color-danger)] px-4 py-2 text-sm font-medium text-white hover:opacity-90 transition disabled:opacity-50"
>
{deleting ? 'Deleting...' : 'Yes, delete'}
</button>
<button
onclick={() => (confirmingDelete = false)}
class="rounded-lg border border-gray-300 dark:border-gray-600 px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition"
>
Cancel
</button>
</div>
{:else}
<button
onclick={() => (confirmingDelete = true)}
class="rounded-lg border border-[var(--color-danger)] px-4 py-2 text-sm text-[var(--color-danger)] hover:bg-red-50 dark:hover:bg-red-900/30 transition"
>
Delete this board
</button>
{/if}
</div>
{/if}
</div>
</div>
</div>
+15 -4
View File
@@ -2,18 +2,29 @@ import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types.js';
import { withTenant } from '$lib/server/rls.js';
import { requireBoardView } from '$lib/server/guards.js';
import { canEditBoard } from '$lib/server/permissions.js';
import { canEditBoard, canDeleteBoard } from '$lib/server/permissions.js';
export const load: PageServerLoad = async ({ params, locals }) => {
const tenant = locals.tenant;
if (!tenant) throw error(400, 'Unknown tenant');
const board = await withTenant(tenant.id, async (tx) => {
return requireBoardView(tx, locals.user, params.boardId, tenant.id);
const { board, categories } = await withTenant(tenant.id, async (tx) => {
const [board, categories] = await Promise.all([
requireBoardView(tx, locals.user, params.boardId, tenant.id),
locals.user
? tx.category.findMany({
where: { tenantId: tenant.id },
orderBy: { name: 'asc' }
})
: []
]);
return { board, categories };
});
return {
board,
canEdit: canEditBoard(locals.user, board.members)
categories,
canEdit: canEditBoard(locals.user, board.members),
canDelete: canDeleteBoard(locals.user, board.members)
};
};
+36
View File
@@ -8,6 +8,7 @@
import { api } from '$lib/utils/api.js';
import { toast } from '$lib/stores/toasts.svelte.js';
import CardModal from '$lib/components/CardModal.svelte';
import BoardSettingsModal from '$lib/components/BoardSettingsModal.svelte';
import ActivityFeed from '$lib/components/ActivityFeed.svelte';
import BoardFilters from '$lib/components/BoardFilters.svelte';
import BulkToolbar from '$lib/components/BulkToolbar.svelte';
@@ -34,6 +35,7 @@
let editingColumnTitle = $state('');
let viewingUsers = $state<any[]>([]);
let showActivity = $state(false);
let showSettings = $state(false);
let showArchivedCards = $state(false);
let archivedCards = $state<any[]>([]);
let activeFilters = $state<FilterState>({ labelIds: [], assigneeIds: [], due: null });
@@ -205,6 +207,9 @@
if (d.board) {
data.board.title = d.board.title;
data.board.visibility = d.board.visibility;
if (d.board.description !== undefined) data.board.description = d.board.description;
if (d.board.background !== undefined) data.board.background = d.board.background;
if (d.board.categoryId !== undefined) data.board.categoryId = d.board.categoryId;
}
}
@@ -421,6 +426,23 @@
<title>{data.board.title}</title>
</svelte:head>
{#if showSettings}
<BoardSettingsModal
board={data.board}
categories={data.categories}
canDelete={data.canDelete}
onclose={() => (showSettings = false)}
onsave={(updated) => {
data.board.title = updated.title;
data.board.visibility = updated.visibility;
data.board.description = updated.description;
data.board.background = updated.background;
data.board.categoryId = updated.categoryId;
showSettings = false;
}}
/>
{/if}
{#if selectedCard}
<CardModal
card={selectedCard}
@@ -526,6 +548,20 @@
</svg>
Activity
</button>
{#if data.canEdit}
<button
onclick={() => (showSettings = true)}
class="rounded px-2 py-1 text-sm text-gray-500 hover:bg-gray-200 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 transition flex items-center gap-1"
title="Board settings"
aria-label="Board settings"
>
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" />
</svg>
Settings
</button>
{/if}
</div>
<!-- Filters bar -->