Phase 4: Rich card features — labels, checklists, comments, attachments, categories
Add full CRUD + UI for all card sub-resources (tags/labels, assignees, checklists with items, comments with markdown, file attachments). Restructure CardModal into a two-column layout with lazy-loaded data. Upgrade card thumbnails with named label pills, urgency-colored due date badges, checklist progress indicators, and SVG icons. Add board categories with filtering on the listing page. Include markdown rendering with DOMPurify sanitization and .prose CSS styles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import { withTenant } from '$lib/server/rls.js';
|
||||
import { requireAuth } from '$lib/server/guards.js';
|
||||
import { canViewBoard } from '$lib/server/permissions.js';
|
||||
import { readFile, unlink } from 'fs/promises';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, locals }) => {
|
||||
const tenant = locals.tenant;
|
||||
if (!tenant) throw error(400, 'Unknown tenant');
|
||||
|
||||
const attachment = await withTenant(tenant.id, async (tx) => {
|
||||
const att = await tx.attachment.findFirst({
|
||||
where: { id: params.attachmentId },
|
||||
include: {
|
||||
card: {
|
||||
include: {
|
||||
column: {
|
||||
include: {
|
||||
board: {
|
||||
select: { tenantId: true, visibility: true, members: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!att) throw error(404, 'Attachment not found');
|
||||
if (att.card.column.board.tenantId !== tenant.id) throw error(404, 'Attachment not found');
|
||||
|
||||
if (!canViewBoard(locals.user, att.card.column.board.visibility, att.card.column.board.members)) {
|
||||
if (!locals.user) throw error(401, 'Not authenticated');
|
||||
throw error(403, 'Access denied');
|
||||
}
|
||||
|
||||
return att;
|
||||
});
|
||||
|
||||
const buffer = await readFile(attachment.filepath);
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
'Content-Type': attachment.mimetype,
|
||||
'Content-Disposition': `attachment; filename="${attachment.filename}"`,
|
||||
'Content-Length': String(attachment.size)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params, locals }) => {
|
||||
const tenant = locals.tenant;
|
||||
if (!tenant) throw error(400, 'Unknown tenant');
|
||||
requireAuth(locals.user);
|
||||
|
||||
await withTenant(tenant.id, async (tx) => {
|
||||
const att = await tx.attachment.findFirst({
|
||||
where: { id: params.attachmentId },
|
||||
include: {
|
||||
card: {
|
||||
include: {
|
||||
column: {
|
||||
include: {
|
||||
board: { select: { tenantId: true, members: true } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!att) throw error(404, 'Attachment not found');
|
||||
if (att.card.column.board.tenantId !== tenant.id) throw error(404, 'Attachment not found');
|
||||
|
||||
// Check board editor permission
|
||||
const { canEditBoard } = await import('$lib/server/permissions.js');
|
||||
if (!canEditBoard(locals.user, att.card.column.board.members)) {
|
||||
throw error(403, 'Access denied');
|
||||
}
|
||||
|
||||
await tx.attachment.delete({ where: { id: params.attachmentId } });
|
||||
|
||||
try {
|
||||
await unlink(att.filepath);
|
||||
} catch {
|
||||
// File may already be gone
|
||||
}
|
||||
});
|
||||
|
||||
return json({ ok: true });
|
||||
};
|
||||
Reference in New Issue
Block a user