From af6490896a8432551549a53ab85195c3b7f78009 Mon Sep 17 00:00:00 2001 From: Arpit Soni <45828093+SoniArpit@users.noreply.github.com> Date: Tue, 18 Nov 2025 02:55:50 +0530 Subject: [PATCH] feat: add list filter to board view (#241) * feat: add list filter to board view * feat: implement API-driven list filtering * refactor: remove unnecessary list join from card filtering queries * feat: enable list filter options for public boards * refactor(perf): move to main query --------- Co-authored-by: Henry --- .../components/DeleteListConfirmation.tsx | 1 + .../src/views/board/components/Filters.tsx | 27 +++++++++++++++++- .../views/board/components/NewCardForm.tsx | 1 + .../views/board/components/NewListForm.tsx | 1 + .../board/components/UpdateBoardSlugForm.tsx | 1 + .../board/components/VisibilityButton.tsx | 1 + apps/web/src/views/board/index.tsx | 3 ++ apps/web/src/views/public/board/index.tsx | 2 ++ packages/api/src/routers/board.ts | 5 ++++ packages/db/src/repository/board.repo.ts | 28 +++++++++++++++++++ packages/db/src/schema/boards.ts | 1 + 11 files changed, 70 insertions(+), 1 deletion(-) diff --git a/apps/web/src/views/board/components/DeleteListConfirmation.tsx b/apps/web/src/views/board/components/DeleteListConfirmation.tsx index 648b900d..2be1f0a8 100644 --- a/apps/web/src/views/board/components/DeleteListConfirmation.tsx +++ b/apps/web/src/views/board/components/DeleteListConfirmation.tsx @@ -12,6 +12,7 @@ interface QueryParams { boardPublicId: string; members: string[]; labels: string[]; + lists: string[]; } export function DeleteListConfirmation({ diff --git a/apps/web/src/views/board/components/Filters.tsx b/apps/web/src/views/board/components/Filters.tsx index b8305a3e..3b281e8a 100644 --- a/apps/web/src/views/board/components/Filters.tsx +++ b/apps/web/src/views/board/components/Filters.tsx @@ -2,6 +2,7 @@ import { useRouter } from "next/router"; import { t } from "@lingui/core/macro"; import { HiMiniXMark, + HiOutlineSquare3Stack3D, HiOutlineTag, HiOutlineUserCircle, } from "react-icons/hi2"; @@ -32,15 +33,22 @@ interface Label { colourCode: string | null; } +interface List { + publicId: string; + name: string; +} + const Filters = ({ position = "right", labels, members, + lists, isLoading, }: { position?: "left" | "right"; labels: Label[]; members: Member[]; + lists: List[]; isLoading: boolean; }) => { const router = useRouter(); @@ -52,7 +60,7 @@ const Filters = ({ try { await router.push({ pathname: router.pathname, - query: { ...router.query, members: [], labels: [] }, + query: { ...router.query, members: [], labels: [], lists: [] }, }); } catch (error) { console.error(error); @@ -85,6 +93,12 @@ const Filters = ({ leftIcon: , })); + const formattedLists = lists.map((list) => ({ + key: list.publicId, + value: list.name, + selected: !!router.query.lists?.includes(list.publicId), + })); + const groups = [ ...(formattedMembers.length ? [ @@ -102,6 +116,16 @@ const Filters = ({ icon: , items: formattedLabels, }, + ...(formattedLists.length + ? [ + { + key: "lists", + label: t`Lists`, + icon: , + items: formattedLists, + }, + ] + : []), ]; const handleSelect = async ( @@ -131,6 +155,7 @@ const Filters = ({ const numOfFilters = [ ...formatToArray(router.query.members), ...formatToArray(router.query.labels), + ...formatToArray(router.query.lists), ].length; return ( diff --git a/apps/web/src/views/board/components/NewCardForm.tsx b/apps/web/src/views/board/components/NewCardForm.tsx index eacc9ed4..52a7c62e 100644 --- a/apps/web/src/views/board/components/NewCardForm.tsx +++ b/apps/web/src/views/board/components/NewCardForm.tsx @@ -33,6 +33,7 @@ interface QueryParams { boardPublicId: string; members: string[]; labels: string[]; + lists: string[]; } interface NewCardFormProps { diff --git a/apps/web/src/views/board/components/NewListForm.tsx b/apps/web/src/views/board/components/NewListForm.tsx index 68a7b892..ca6213b0 100644 --- a/apps/web/src/views/board/components/NewListForm.tsx +++ b/apps/web/src/views/board/components/NewListForm.tsx @@ -21,6 +21,7 @@ interface QueryParams { boardPublicId: string; members: string[]; labels: string[]; + lists: string[]; } export function NewListForm({ diff --git a/apps/web/src/views/board/components/UpdateBoardSlugForm.tsx b/apps/web/src/views/board/components/UpdateBoardSlugForm.tsx index a9f962e0..20b3ff5a 100644 --- a/apps/web/src/views/board/components/UpdateBoardSlugForm.tsx +++ b/apps/web/src/views/board/components/UpdateBoardSlugForm.tsx @@ -17,6 +17,7 @@ interface QueryParams { boardPublicId: string; members: string[]; labels: string[]; + lists: string[]; } export function UpdateBoardSlugForm({ diff --git a/apps/web/src/views/board/components/VisibilityButton.tsx b/apps/web/src/views/board/components/VisibilityButton.tsx index 46329b1f..f90c93d8 100644 --- a/apps/web/src/views/board/components/VisibilityButton.tsx +++ b/apps/web/src/views/board/components/VisibilityButton.tsx @@ -11,6 +11,7 @@ interface QueryParams { boardPublicId: string; members: string[]; labels: string[]; + lists: string[]; } const VisibilityButton = ({ diff --git a/apps/web/src/views/board/index.tsx b/apps/web/src/views/board/index.tsx index 24981d83..de529f30 100644 --- a/apps/web/src/views/board/index.tsx +++ b/apps/web/src/views/board/index.tsx @@ -80,11 +80,13 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) { boardPublicId: string; members: string[]; labels: string[]; + lists: string[]; type: "regular" | "template"; } = { boardPublicId: boardId ?? "", members: formatToArray(router.query.members), labels: formatToArray(router.query.labels), + lists: formatToArray(router.query.lists), type: isTemplate ? "template" : "regular", }; @@ -418,6 +420,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) { members={boardData.workspace.members.filter( (member) => member.user !== null, )} + lists={boardData.allLists} position="left" isLoading={!boardData} /> diff --git a/apps/web/src/views/public/board/index.tsx b/apps/web/src/views/public/board/index.tsx index 278f575e..20861bd2 100644 --- a/apps/web/src/views/public/board/index.tsx +++ b/apps/web/src/views/public/board/index.tsx @@ -44,6 +44,7 @@ export default function PublicBoardView() { workspaceSlug: workspaceSlug ?? "", members: formatToArray(router.query.members), labels: formatToArray(router.query.labels), + lists: formatToArray(router.query.lists), }, { enabled: router.isReady && !!boardSlug, @@ -130,6 +131,7 @@ export default function PublicBoardView() { diff --git a/packages/api/src/routers/board.ts b/packages/api/src/routers/board.ts index 55404699..6e4da4d4 100644 --- a/packages/api/src/routers/board.ts +++ b/packages/api/src/routers/board.ts @@ -78,6 +78,7 @@ export const boardRouter = createTRPCRouter({ boardPublicId: z.string().min(12), members: z.array(z.string().min(12)).optional(), labels: z.array(z.string().min(12)).optional(), + lists: z.array(z.string().min(12)).optional(), type: z.enum(["regular", "template"]).optional(), }), ) @@ -110,6 +111,7 @@ export const boardRouter = createTRPCRouter({ { members: input.members ?? [], labels: input.labels ?? [], + lists: input.lists ?? [], type: input.type, }, ); @@ -142,6 +144,7 @@ export const boardRouter = createTRPCRouter({ .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/), members: z.array(z.string().min(12)).optional(), labels: z.array(z.string().min(12)).optional(), + lists: z.array(z.string().min(12)).optional(), }), ) .output(z.custom>>()) @@ -164,6 +167,7 @@ export const boardRouter = createTRPCRouter({ { members: input.members ?? [], labels: input.labels ?? [], + lists: input.lists ?? [], }, ); @@ -234,6 +238,7 @@ export const boardRouter = createTRPCRouter({ { members: [], labels: [], + lists: [], type: sourceBoardInfo.type, }, ); diff --git a/packages/db/src/repository/board.repo.ts b/packages/db/src/repository/board.repo.ts index 4feb78e0..1fe80ae7 100644 --- a/packages/db/src/repository/board.repo.ts +++ b/packages/db/src/repository/board.repo.ts @@ -70,6 +70,7 @@ export const getByPublicId = async ( filters: { members: string[]; labels: string[]; + lists: string[]; type: "regular" | "template" | undefined; }, ) => { @@ -231,6 +232,19 @@ export const getByPublicId = async ( orderBy: [asc(cards.index)], }, }, + where: and( + isNull(lists.deletedAt), + filters.lists.length > 0 + ? inArray(lists.publicId, filters.lists) + : undefined, + ), + orderBy: [asc(lists.index)], + }, + allLists: { + columns: { + publicId: true, + name: true, + }, where: isNull(lists.deletedAt), orderBy: [asc(lists.index)], }, @@ -268,6 +282,7 @@ export const getBySlug = async ( filters: { members: string[]; labels: string[]; + lists: string[]; }, ) => { let cardIds: string[] = []; @@ -379,6 +394,19 @@ export const getBySlug = async ( orderBy: [asc(cards.index)], }, }, + where: and( + isNull(lists.deletedAt), + filters.lists.length > 0 + ? inArray(lists.publicId, filters.lists) + : undefined, + ), + orderBy: [asc(lists.index)], + }, + allLists: { + columns: { + publicId: true, + name: true, + }, where: isNull(lists.deletedAt), orderBy: [asc(lists.index)], }, diff --git a/packages/db/src/schema/boards.ts b/packages/db/src/schema/boards.ts index a8720e1f..7decf191 100644 --- a/packages/db/src/schema/boards.ts +++ b/packages/db/src/schema/boards.ts @@ -73,6 +73,7 @@ export const boardsRelations = relations(boards, ({ one, many }) => ({ relationName: "boardCreatedByUser", }), lists: many(lists), + allLists: many(lists), labels: many(labels), deletedBy: one(users, { fields: [boards.deletedBy],