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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user