From 6d9a4346f002b398256ef6867d3f2b00b2f95880 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 7 Mar 2024 15:49:01 +0000 Subject: [PATCH] feat: assign members to cards --- src/app/boards/[...boardId]/page.tsx | 34 +++-- .../[cardId]/components/MemberSelector.tsx | 141 ++++++++++++++++++ src/app/cards/[cardId]/page.tsx | 21 ++- src/server/api/routers/card.ts | 86 ++++++++++- src/server/db/schema.ts | 3 +- 5 files changed, 264 insertions(+), 21 deletions(-) create mode 100644 src/app/cards/[cardId]/components/MemberSelector.tsx diff --git a/src/app/boards/[...boardId]/page.tsx b/src/app/boards/[...boardId]/page.tsx index bea1c4b4..0740ba22 100644 --- a/src/app/boards/[...boardId]/page.tsx +++ b/src/app/boards/[...boardId]/page.tsx @@ -255,23 +255,25 @@ export default function BoardPage() {
{label.name}
))} - {card.members?.map(({ member }) => ( - - - {member.user.name - .split(" ") - .map((namePart) => - namePart - .charAt(0) - .toUpperCase(), - ) - .join("")} +
+ {card.members?.map(({ member }) => ( + + + {member.user.name + .split(" ") + .map((namePart) => + namePart + .charAt(0) + .toUpperCase(), + ) + .join("")} + - - ))} + ))} +
) : null} diff --git a/src/app/cards/[cardId]/components/MemberSelector.tsx b/src/app/cards/[cardId]/components/MemberSelector.tsx new file mode 100644 index 00000000..7d1088d7 --- /dev/null +++ b/src/app/cards/[cardId]/components/MemberSelector.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { Fragment } from "react"; +import { api } from "~/trpc/react"; +import { Menu, Transition } from "@headlessui/react"; +import { HiMiniPlus } from "react-icons/hi2"; +import { useFormik } from "formik"; + +interface MemberSelectorProps { + cardPublicId: string; + members: { + publicId: string; + user: { + id: string; + name: string | null; + }; + selected: boolean; + }[]; +} + +export default function LabelSelector({ + cardPublicId, + members, +}: MemberSelectorProps) { + const utils = api.useUtils(); + + const refetchCard = () => utils.card.byId.refetch({ id: cardPublicId }); + + const addOrRemoveMember = api.card.addOrRemoveMember.useMutation({ + onSuccess: async () => { + await refetchCard(); + }, + }); + + const formik = useFormik({ + initialValues: { + ...Object.fromEntries( + members?.map((member) => [member.publicId, member.selected]) ?? [], + ), + }, + onSubmit: (values) => { + console.log({ values }); + }, + enableReinitialize: true, + }); + + const selectedMembers = members.filter((member) => member.selected); + + return ( + <> + + + {selectedMembers.length ? ( +
+ {selectedMembers.map((member) => ( + + + {member.user?.name + ? member.user?.name + .split(" ") + .map((namePart) => namePart.charAt(0).toUpperCase()) + .join("") + : null} + + + ))} +
+ ) : ( + <> + + {"Add member"} + + )} +
+ + + +
+
+ {members?.map((member) => ( + + {() => ( +
{ + e.preventDefault(); + await formik.setFieldValue( + member.publicId, + !formik.values[member.publicId], + ); + + addOrRemoveMember.mutate({ + cardPublicId, + workspaceMemberPublicId: member.publicId, + }); + + await formik.submitForm(); + }} + > + event.stopPropagation()} + onChange={formik.handleChange} + checked={formik.values[member.publicId]} + /> + +
+ )} +
+ ))} +
+
+
+
+
+ + ); +} diff --git a/src/app/cards/[cardId]/page.tsx b/src/app/cards/[cardId]/page.tsx index 0897f257..02d14ed9 100644 --- a/src/app/cards/[cardId]/page.tsx +++ b/src/app/cards/[cardId]/page.tsx @@ -10,6 +10,7 @@ import Dropdown from "./components/Dropdown"; import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation"; import LabelSelector from "./components/LabelSelector"; import ListSelector from "./components/ListSelector"; +import MemberSelector from "./components/MemberSelector"; import { NewLabelForm } from "./components/NewLabelForm"; import Modal from "~/app/components/modal"; @@ -35,8 +36,10 @@ export default function CardPage() { const boardId = data?.list?.board?.publicId; const labels = data?.list?.board?.labels; + const workspaceMembers = data?.list?.board?.workspace?.members; const board = data?.list?.board; const selectedLabels = data?.labels; + const selectedMembers = data?.members; const formattedLabels = labels?.map((label) => { @@ -57,6 +60,18 @@ export default function CardPage() { selected: list.publicId === data?.list.publicId, })) ?? []; + const formattedMembers = + workspaceMembers?.map((member) => { + const isSelected = selectedMembers?.some( + (assignedMember) => assignedMember.member.publicId === member.publicId, + ); + + return { + ...member, + selected: isSelected ?? false, + }; + }) ?? []; + const updateCard = api.card.update.useMutation(); const formik = useFormik({ @@ -126,10 +141,14 @@ export default function CardPage() {

List

-
+

Labels

+
+

Members

+ +
diff --git a/src/server/api/routers/card.ts b/src/server/api/routers/card.ts index 8bca36d1..0022d905 100644 --- a/src/server/api/routers/card.ts +++ b/src/server/api/routers/card.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { and, desc, eq, isNull, sql } from "drizzle-orm"; -import { cards, cardsToLabels, labels, lists } from "~/server/db/schema"; +import { cards, cardsToLabels, cardToWorkspaceMembers, labels, lists, workspaceMembers } from "~/server/db/schema"; import { generateUID } from "~/utils/generateUID"; import { @@ -86,6 +86,45 @@ export const cardRouter = createTRPCRouter({ }); }) }), + addOrRemoveMember: publicProcedure + .input( + z.object({ + cardPublicId: z.string().min(12), + workspaceMemberPublicId: z.string().min(12), + }), + ) + .mutation(({ ctx, input }) => { + const userId = ctx.session?.user.id; + + if (!userId) return; + + return ctx.db.transaction(async (tx) => { + const card = await tx.query.cards.findFirst({ + where: and(eq(cards.publicId, input.cardPublicId), isNull(cards.deletedAt)), + }); + + const member = await tx.query.workspaceMembers.findFirst({ + where: eq(workspaceMembers.publicId, input.workspaceMemberPublicId), + }); + + if (!card || !member) return; + + console.log({ member }) + + const memberExists = await tx.query.cardToWorkspaceMembers.findFirst({ + where: and(eq(cardToWorkspaceMembers.cardId, card.id), eq(cardToWorkspaceMembers.workspaceMemberId, member.id)), + }); + + if (memberExists) { + return tx.delete(cardToWorkspaceMembers).where(and(eq(cardToWorkspaceMembers.cardId, card.id), eq(cardToWorkspaceMembers.workspaceMemberId, member.id)),); + } + + return tx.insert(cardToWorkspaceMembers).values({ + cardId: card.id, + workspaceMemberId: member.id, + }); + }) + }), byId: publicProcedure .input(z.object({ id: z.string().min(12) })) .query(({ ctx, input }) => @@ -103,7 +142,7 @@ export const cardRouter = createTRPCRouter({ name: true, colourCode: true, } - } + }, } }, list: { @@ -130,9 +169,50 @@ export const cardRouter = createTRPCRouter({ name: true, }, where: isNull(lists.deletedAt) + }, + workspace: { + columns: { + publicId: true, + }, + with: { + members: { + columns: { + publicId: true, + }, + with: { + user: { + columns: { + id: true, + name: true, + } + } + } + } + } + } + }, + } + } + }, + members: { + columns: { + workspaceMemberId: false, + cardId: false, + }, + with: { + member: { + columns: { + publicId: true, + }, + with: { + user: { + columns: { + id: true, + name: true, + } } } - } + }, } }, }, diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index bb3b0e17..03100bd4 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -334,8 +334,9 @@ export const workspaces = mySqlTable( } ); -export const workspaceRelations = relations(workspaces, ({ one }) => ({ +export const workspaceRelations = relations(workspaces, ({ one, many }) => ({ user: one(users, { fields: [workspaces.createdBy], references: [users.id] }), + members: many(workspaceMembers) })); export const workspaceMembers = mySqlTable(