Private
Public Access
1
0

Phase 7: Search, filters, templates, and archive UI

Add full-text card search (PostgreSQL tsvector + GIN index) with navbar
SearchBar, client-side board filters (label/assignee/due date), board
templates (3 system templates + save-as-template API), and archive
toggle for both board listing and board view.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-26 00:42:20 -05:00
parent e5f2c64e14
commit 6ed0349507
14 changed files with 727 additions and 27 deletions
+36 -2
View File
@@ -3,6 +3,8 @@ import type { RequestHandler } from './$types.js';
import { boardSchema } from '$lib/server/validation.js';
import { withTenant } from '$lib/server/rls.js';
import { requireAuth } from '$lib/server/guards.js';
import { prisma } from '$lib/server/db.js';
import { generatePositions } from '$lib/utils/fractional-index.js';
// List boards for current tenant
export const GET: RequestHandler = async ({ locals }) => {
@@ -50,10 +52,26 @@ export const POST: RequestHandler = async ({ request, locals }) => {
return json({ error: result.error.issues[0].message }, { status: 400 });
}
const { title, description, visibility, categoryId } = result.data;
const { title, description, visibility, categoryId, templateId } = result.data;
// Load template columns if templateId provided
let templateColumns: { title: string }[] = [];
if (templateId) {
// System templates have null tenantId, so query without RLS
const template = await prisma.boardTemplate.findFirst({
where: {
id: templateId,
OR: [{ tenantId: null }, { tenantId: tenant.id }]
}
});
if (template) {
const tpl = template.template as { columns?: { title: string }[] };
templateColumns = tpl.columns || [];
}
}
const board = await withTenant(tenant.id, async (tx) => {
return tx.board.create({
const newBoard = await tx.board.create({
data: {
tenantId: tenant.id,
title,
@@ -73,6 +91,22 @@ export const POST: RequestHandler = async ({ request, locals }) => {
}
}
});
// Create columns from template
if (templateColumns.length > 0) {
const positions = generatePositions(templateColumns.length);
for (let i = 0; i < templateColumns.length; i++) {
await tx.column.create({
data: {
boardId: newBoard.id,
title: templateColumns[i].title,
position: positions[i]
}
});
}
}
return newBoard;
});
return json({ board }, { status: 201 });
@@ -0,0 +1,31 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types.js';
import { withTenant } from '$lib/server/rls.js';
import { requireAuth, requireBoardView } from '$lib/server/guards.js';
export const GET: RequestHandler = async ({ params, locals }) => {
const tenant = locals.tenant;
if (!tenant) throw error(400, 'Unknown tenant');
requireAuth(locals.user);
const cards = await withTenant(tenant.id, async (tx) => {
// Verify user has access to this board
await requireBoardView(tx, locals.user, params.boardId, tenant.id);
return tx.card.findMany({
where: {
archived: true,
column: { board: { id: params.boardId, tenantId: tenant.id } }
},
include: {
labels: { include: { tag: true } },
assignees: {
include: { user: { select: { id: true, name: true, avatarUrl: true } } }
}
},
orderBy: { updatedAt: 'desc' }
});
});
return json({ cards });
};