fix: address all code review findings

- 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
This commit is contained in:
2026-03-25 00:03:32 +03:00
parent 5a6002be76
commit cba160ecb8
15 changed files with 588 additions and 447 deletions
@@ -1,21 +1,14 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import { TargetType, PermissionLevel } from '$lib/utils/constants.js';
interface PermissionRecord {
id: string;
entityType: string;
entityId: string;
targetType: string;
targetId: string;
level: string;
createdAt: string;
}
interface SelectOption {
id: string;
name: string;
}
import {
loadBoardPermissions,
grantBoardPermission,
revokeBoardPermission,
getTargetName as resolveTargetName,
type PermissionRecord,
type SelectOption
} from '$lib/utils/boardPermissions.js';
interface Props {
boardId: string;
@@ -41,6 +34,7 @@
let loading = $state(true);
let errorMessage = $state('');
let copySuccess = $state(false);
let copyTimerId = $state<ReturnType<typeof setTimeout> | null>(null);
let selectedTargetType = $state<string>(TargetType.USER);
let selectedTargetId = $state('');
@@ -63,15 +57,9 @@
loading = true;
errorMessage = '';
try {
const res = await fetch(`/api/boards/${boardId}/permissions`);
const json = await res.json();
if (json.success) {
permissions = json.data;
} else {
errorMessage = json.error ?? 'Failed to load permissions';
}
} catch {
errorMessage = 'Network error';
permissions = await loadBoardPermissions(boardId);
} catch (err) {
errorMessage = err instanceof Error ? err.message : 'Network error';
} finally {
loading = false;
}
@@ -81,53 +69,27 @@
if (!selectedTargetId) return;
errorMessage = '';
try {
const res = await fetch(`/api/boards/${boardId}/permissions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
targetType: selectedTargetType,
targetId: selectedTargetId,
level: selectedLevel
})
});
const json = await res.json();
if (json.success) {
selectedTargetId = '';
searchQuery = '';
await loadPermissions();
} else {
errorMessage = json.error ?? 'Failed to grant permission';
}
} catch {
errorMessage = 'Network error';
await grantBoardPermission(boardId, selectedTargetType, selectedTargetId, selectedLevel);
selectedTargetId = '';
searchQuery = '';
await loadPermissions();
} catch (err) {
errorMessage = err instanceof Error ? err.message : 'Network error';
}
}
async function handleRevoke(perm: PermissionRecord) {
errorMessage = '';
try {
const res = await fetch(`/api/boards/${boardId}/permissions`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
targetType: perm.targetType,
targetId: perm.targetId
})
});
const json = await res.json();
if (json.success) {
await loadPermissions();
} else {
errorMessage = json.error ?? 'Failed to revoke permission';
}
} catch {
errorMessage = 'Network error';
await revokeBoardPermission(boardId, perm.targetType, perm.targetId);
await loadPermissions();
} catch (err) {
errorMessage = err instanceof Error ? err.message : 'Network error';
}
}
function getTargetName(targetType: string, targetId: string): string {
const list = targetType === TargetType.USER ? users : groups;
return list.find((item) => item.id === targetId)?.name ?? targetId;
return resolveTargetName(targetType, targetId, users, groups);
}
function getLevelLabel(level: string): string {
@@ -148,8 +110,12 @@
const url = `${window.location.origin}/boards/${boardId}`;
await navigator.clipboard.writeText(url);
copySuccess = true;
setTimeout(() => {
if (copyTimerId !== null) {
clearTimeout(copyTimerId);
}
copyTimerId = setTimeout(() => {
copySuccess = false;
copyTimerId = null;
}, 2000);
} catch {
// Fallback: ignore if clipboard API not available
@@ -168,9 +134,14 @@
}
}
// Load permissions on mount
// Load permissions on mount; clean up copy timer on destroy
$effect(() => {
loadPermissions();
return () => {
if (copyTimerId !== null) {
clearTimeout(copyTimerId);
}
};
});
</script>