feat(phase2): per-board access control UI

- BoardAccessControl component with user/group autocomplete
- BoardShareDialog modal with copy link, guest toggle, quick add
- Board permissions REST API (GET/POST/DELETE)
- Access indicators on BoardCard (lock, globe, shared icons)
- Guest access toggle in board editor with status preview
- Enhanced PermissionEditor with search autocomplete
- i18n translations for all new strings (EN/RU)
This commit is contained in:
2026-03-24 23:29:19 +03:00
parent 477c0e4d52
commit 5bb4fbcedf
16 changed files with 1166 additions and 57 deletions
+63 -9
View File
@@ -4,6 +4,7 @@
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import DraggableBoard from '$lib/components/board/DraggableBoard.svelte';
import BoardAccessControl from '$lib/components/board/BoardAccessControl.svelte';
import { WidgetType } from '$lib/utils/constants.js';
let { data }: { data: PageData } = $props();
@@ -161,15 +162,6 @@
/>
{$t('board.default_board')}
</label>
<label class="flex items-center gap-2 text-sm text-foreground">
<input
type="checkbox"
name="isGuestAccessible"
checked={data.board.isGuestAccessible}
class="h-4 w-4 rounded border-input accent-primary"
/>
{$t('board.guest_accessible')}
</label>
</div>
</div>
<div class="mt-4">
@@ -183,6 +175,68 @@
</form>
</section>
<!-- Guest Access -->
<section class="mb-8 rounded-xl border border-border bg-card p-6 shadow-sm">
<h2 class="mb-4 text-lg font-semibold text-card-foreground">{$t('board.guest_access_title')}</h2>
<div class="rounded-lg border border-border bg-muted/30 p-4">
<form method="POST" action="?/updateBoard" use:enhance>
<input type="hidden" name="name" value={data.board.name} />
<input type="hidden" name="isDefault" value={data.board.isDefault ? 'on' : ''} />
<label class="flex items-start gap-3 text-sm text-foreground">
<input
type="checkbox"
name="isGuestAccessible"
checked={data.board.isGuestAccessible}
class="mt-0.5 h-4 w-4 rounded border-input accent-primary"
/>
<div>
<span class="font-medium">{$t('board.guest_accessible')}</span>
<p class="mt-1 text-xs text-muted-foreground">{$t('board.guest_access_description')}</p>
{#if data.board.isGuestAccessible}
<p class="mt-1 flex items-center gap-1 text-xs text-green-500">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<line x1="2" y1="12" x2="22" y2="12" />
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</svg>
{$t('board.guest_access_enabled')}
</p>
{:else}
<p class="mt-1 flex items-center gap-1 text-xs text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
{$t('board.guest_access_disabled')}
</p>
{/if}
</div>
</label>
<div class="mt-3">
<button
type="submit"
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
{$t('board.save')}
</button>
</div>
</form>
</div>
</section>
<!-- Permissions -->
{#if data.canManagePermissions}
<section class="mb-8 rounded-xl border border-border bg-card p-6 shadow-sm">
<h2 class="mb-4 text-lg font-semibold text-card-foreground">{$t('board.permissions_title')}</h2>
<p class="mb-4 text-sm text-muted-foreground">{$t('board.permissions_description')}</p>
<BoardAccessControl
boardId={data.board.id}
users={data.users}
groups={data.groups}
/>
</section>
{/if}
<!-- Sections with Drag-and-Drop -->
<section class="mb-8">
<div class="mb-4 flex items-center justify-between">