cba160ecb8
- Extract shared permission logic into boardPermissions.ts utility - Fix DnD drag revert: add dirty flag to prevent overwrite - Wrap OAuth group sync in Prisma transaction (N+1 fix) - Add empty widgetIds validation in widget reorder API - Add invalidateAll() after guest toggle PATCH - Replace console.error with user-visible error banners - Extract WidgetCreationForm component (DraggableSection was 448 lines) - Remove unused boardId prop from DraggableSection - Always include OAuth state parameter + validate in callback - Clean up copyLink timer on component destroy - Add type-specific widget config validation in addWidget action
143 lines
3.4 KiB
Svelte
143 lines
3.4 KiB
Svelte
<script lang="ts">
|
|
import { t } from 'svelte-i18n';
|
|
import { dndzone } from 'svelte-dnd-action';
|
|
import DraggableSection from '$lib/components/section/DraggableSection.svelte';
|
|
|
|
interface WidgetData {
|
|
id: string;
|
|
type: string;
|
|
order: number;
|
|
config: string;
|
|
appId: string | null;
|
|
sectionId: string;
|
|
app: {
|
|
id: string;
|
|
name: string;
|
|
url: string;
|
|
icon: string | null;
|
|
iconType: string;
|
|
description: string | null;
|
|
statuses: Array<{ status: string; responseTime: number | null }>;
|
|
} | null;
|
|
}
|
|
|
|
interface SectionData {
|
|
id: string;
|
|
title: string;
|
|
icon: string | null;
|
|
order: number;
|
|
isExpandedByDefault: boolean;
|
|
widgets: WidgetData[];
|
|
}
|
|
|
|
interface Props {
|
|
boardId: string;
|
|
sections: SectionData[];
|
|
apps: Array<{ id: string; name: string }>;
|
|
addWidgetSectionId: string | null;
|
|
onToggleAddWidget: (sectionId: string) => void;
|
|
onDeleteSection: (sectionId: string) => void;
|
|
onAddWidget: (sectionId: string, widgetData: string) => void;
|
|
onDeleteWidget: (widgetId: string) => void;
|
|
}
|
|
|
|
let {
|
|
boardId,
|
|
sections: initialSections,
|
|
apps,
|
|
addWidgetSectionId,
|
|
onToggleAddWidget,
|
|
onDeleteSection,
|
|
onAddWidget,
|
|
onDeleteWidget
|
|
}: Props = $props();
|
|
|
|
let sections = $state<SectionData[]>([...initialSections]);
|
|
let dirty = $state(false);
|
|
let errorMessage = $state('');
|
|
|
|
// Keep local state in sync when parent data changes (skip during drag)
|
|
$effect(() => {
|
|
if (!dirty) {
|
|
sections = [...initialSections];
|
|
}
|
|
});
|
|
|
|
const flipDurationMs = 200;
|
|
|
|
function handleConsider(e: CustomEvent<{ items: SectionData[] }>) {
|
|
dirty = true;
|
|
sections = e.detail.items;
|
|
}
|
|
|
|
async function handleFinalize(e: CustomEvent<{ items: SectionData[] }>) {
|
|
dirty = true;
|
|
sections = e.detail.items;
|
|
const sectionIds = sections.map((s) => s.id);
|
|
|
|
try {
|
|
await fetch(`/api/boards/${boardId}/reorder`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ sectionIds })
|
|
});
|
|
} catch (err) {
|
|
errorMessage = err instanceof Error ? err.message : 'Failed to persist section reorder';
|
|
} finally {
|
|
dirty = false;
|
|
}
|
|
}
|
|
|
|
async function handleWidgetsUpdate(sectionId: string, widgets: WidgetData[]) {
|
|
// Update local state
|
|
dirty = true;
|
|
sections = sections.map((s) => (s.id === sectionId ? { ...s, widgets } : s));
|
|
|
|
const widgetIds = widgets.map((w) => w.id);
|
|
|
|
try {
|
|
await fetch(`/api/boards/${boardId}/sections/${sectionId}/reorder`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ widgetIds })
|
|
});
|
|
} catch (err) {
|
|
errorMessage = err instanceof Error ? err.message : 'Failed to persist widget reorder';
|
|
} finally {
|
|
dirty = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#if errorMessage}
|
|
<p class="mb-2 text-sm text-destructive">{errorMessage}</p>
|
|
{/if}
|
|
|
|
{#if sections.length === 0}
|
|
<div class="rounded-xl border border-border bg-card/50 p-8 text-center">
|
|
<p class="text-muted-foreground">{$t('board.no_sections')}</p>
|
|
</div>
|
|
{:else}
|
|
<div
|
|
use:dndzone={{ items: sections, flipDurationMs, dropTargetStyle: {} }}
|
|
onconsider={handleConsider}
|
|
onfinalize={handleFinalize}
|
|
class="space-y-4"
|
|
>
|
|
{#each sections as section (section.id)}
|
|
<div>
|
|
<DraggableSection
|
|
{section}
|
|
{apps}
|
|
onWidgetsUpdate={handleWidgetsUpdate}
|
|
{addWidgetSectionId}
|
|
{onToggleAddWidget}
|
|
{onDeleteSection}
|
|
{onAddWidget}
|
|
{onDeleteWidget}
|
|
/>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|