feat: swap out drizzle for supabase in label, list and workspace routers

This commit is contained in:
Henry
2024-04-15 20:29:01 +01:00
parent 1712a1a41b
commit b695f7bdf3
10 changed files with 257 additions and 176 deletions

View File

@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { HiXMark } from "react-icons/hi2"; import { HiXMark } from "react-icons/hi2";
@@ -28,15 +29,23 @@ export function NewWorkspaceForm() {
}, },
}); });
useEffect(() => {
const nameElement: HTMLElement | null =
document?.querySelector<HTMLElement>("#workspace-name");
if (nameElement) nameElement.focus();
}, []);
return ( return (
<> <>
<div className="flex w-full justify-between pb-4"> <div className="flex w-full items-center justify-between pb-4">
<h2 className="text-sm font-bold text-dark-1000">New workspace</h2> <h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
New workspace
</h2>
<button <button
className="rounded p-1 hover:bg-dark-300 focus:outline-none" className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
onClick={() => closeModal()} onClick={() => closeModal()}
> >
<HiXMark size={18} className="text-dark-900" /> <HiXMark size={18} className="text-light-900 dark:text-dark-900" />
</button> </button>
</div> </div>
@@ -52,20 +61,20 @@ export function NewWorkspaceForm() {
> >
<Form> <Form>
<label <label
htmlFor="name" htmlFor="workspace-name"
className="block pb-2 text-sm font-normal leading-6 text-dark-1000" className="block pb-2 text-sm font-normal leading-6 text-neutral-900 dark:text-dark-1000"
> >
Name Name
</label> </label>
<Field <Field
id="name" id="workspace-name"
name="name" name="name"
className="block w-full rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 text-dark-1000 shadow-sm ring-1 ring-inset ring-dark-700 focus:ring-2 focus:ring-inset focus:ring-dark-700 sm:text-sm sm:leading-6" className="block w-full rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 text-neutral-900 shadow-sm ring-1 ring-inset ring-light-600 focus:ring-2 focus:ring-inset focus:ring-light-600 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
/> />
<div className="mt-5 sm:mt-6"> <div className="mt-5 sm:mt-6">
<button <button
type="submit" type="submit"
className="inline-flex w-full justify-center rounded-md bg-dark-1000 px-3 py-2 text-sm font-semibold text-dark-50 shadow-sm focus-visible:outline-none" className="inline-flex w-full justify-center rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
> >
Create workspace Create workspace
</button> </button>

View File

@@ -54,7 +54,7 @@ export default function SideNavigation({ user }: SideNavigationProps) {
<li key={item.name}> <li key={item.name}>
<ReactiveButton <ReactiveButton
href={item.href} href={item.href}
current={pathname.includes(item.href)} current={pathname?.includes(item.href)}
name={item.name} name={item.name}
json={item.icon} json={item.icon}
/> />

View File

@@ -1,5 +1,4 @@
import { z } from "zod"; import { z } from "zod";
import { generateUID } from "~/utils/generateUID";
import { import {
createTRPCRouter, createTRPCRouter,
@@ -10,7 +9,7 @@ export const authRouter = createTRPCRouter({
login: publicProcedure login: publicProcedure
.input(z.object({ email: z.string() })) .input(z.object({ email: z.string() }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const { data, error } = await ctx.supabase.auth.signInWithOtp({ const { data } = await ctx.supabase.auth.signInWithOtp({
email: input.email, email: input.email,
options: { options: {
emailRedirectTo: '/boards', emailRedirectTo: '/boards',

View File

@@ -107,11 +107,9 @@ export const boardRouter = createTRPCRouter({
.limit(1) .limit(1)
.single(); .single();
console.log({ workspace })
if (!workspace.data) return; if (!workspace.data) return;
const { data, error } = await ctx.supabase const { data } = await ctx.supabase
.from('board') .from('board')
.insert({ .insert({
publicId: generateUID(), publicId: generateUID(),
@@ -168,23 +166,26 @@ export const boardRouter = createTRPCRouter({
const listIds = board.data.lists.map((list) => list.id); const listIds = board.data.lists.map((list) => list.id);
const deletedAt = new Date().toString(); const deletedAt = new Date().toISOString();
await ctx.supabase await ctx.supabase
.from('board') .from('board')
.update({ deletedAt, deletedBy: userId }) .update({ deletedAt, deletedBy: userId })
.eq('id', board.data.id); .eq('id', board.data.id)
.is('deletedAt', null);
if (listIds.length) { if (listIds.length) {
await ctx.supabase await ctx.supabase
.from('list') .from('list')
.update({ deletedAt, deletedBy: userId }) .update({ deletedAt, deletedBy: userId })
.eq('boardId', board.data.id); .eq('boardId', board.data.id)
.is('deletedAt', null);
await ctx.supabase await ctx.supabase
.from('card') .from('card')
.update({ deletedAt, deletedBy: userId }) .update({ deletedAt, deletedBy: userId })
.in('listId', listIds); .in('listId', listIds)
.is('deletedAt', null);
} }
}) })
}); });

View File

@@ -1,7 +1,4 @@
import { z } from "zod"; import { z } from "zod";
import { and, eq, isNull } from "drizzle-orm";
import { cards, cardsToLabels, labels } from "~/server/db/schema";
import { generateUID } from "~/utils/generateUID"; import { generateUID } from "~/utils/generateUID";
import { import {
@@ -18,41 +15,43 @@ export const labelRouter = createTRPCRouter({
colourCode: z.string().length(7), colourCode: z.string().length(7),
}), }),
) )
.mutation(({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const userId = ctx.session?.user.id; const userId = ctx.user?.id;
if (!userId) return; if (!userId) return;
return ctx.db.transaction(async (tx) => { const card = await ctx.supabase
const card = await tx.query.cards.findFirst({ .from('card')
where: and(eq(cards.publicId, input.cardPublicId), isNull(cards.deletedAt)), .select(`id, list (boardId)`)
with: { .eq('publicId', input.cardPublicId)
list: { .is('deletedAt', null)
with: { .limit(1)
board: true .single();
}
}
}
});
if (!card) return; if (!card.data?.list) return;
const newLabel = await tx.insert(labels).values({ const newLabel = await ctx.supabase
.from('label')
.insert({
publicId: generateUID(), publicId: generateUID(),
name: input.name, name: input.name,
colourCode: input.colourCode, colourCode: input.colourCode,
createdBy: userId, createdBy: userId,
boardId: card.list.boardId, boardId: card.data.list.boardId,
}).returning({ id: labels.id }); })
.select(`id`)
.limit(1)
.single();
const newLabelId = newLabel[0]?.id if (!newLabel.data) return;
if (!newLabelId) return; await ctx.supabase
.from('_card_labels')
.insert({
cardId: card.data.id,
labelId: newLabel.data.id,
})
return tx.insert(cardsToLabels).values({ return newLabel.data;
cardId: card.id,
labelId: newLabelId,
});
})
}), }),
}); });

View File

@@ -1,7 +1,4 @@
import { z } from "zod"; import { z } from "zod";
import { and, desc, eq, sql, isNull } from "drizzle-orm";
import { boards, cards, lists } from "~/server/db/schema";
import { generateUID } from "~/utils/generateUID"; import { generateUID } from "~/utils/generateUID";
import { import {
@@ -17,37 +14,39 @@ export const listRouter = createTRPCRouter({
boardPublicId: z.string().min(12) boardPublicId: z.string().min(12)
}), }),
) )
.mutation(({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const userId = ctx.session?.user.id; const userId = ctx.user?.id;
if (!userId) return; if (!userId) return;
return ctx.db.transaction(async (tx) => { const board = await ctx.supabase
const board = await tx.query.boards.findFirst({ .from('board')
where: eq(boards.publicId, input.boardPublicId), .select(`id, lists:list (index)`)
columns: { .eq('publicId', input.boardPublicId)
id: true .order('index', { foreignTable: 'list', ascending: false })
} .is('list.deletedAt', null)
}); .limit(1)
.single();
if (!board) return; if (!board.data) return;
const latestList = await tx.query.lists.findFirst({ const latestListIndex = board.data.lists[0]?.index
where: eq(lists.boardId, board.id),
columns: {
index: true
},
orderBy: desc(lists.index)
});
return tx.insert(lists).values({ const { data } = await ctx.supabase
.from('list')
.insert({
publicId: generateUID(), publicId: generateUID(),
name: input.name, name: input.name,
createdBy: userId, createdBy: userId,
boardId: board.id, boardId: board.data.id,
index: latestList ? latestList.index + 1 : 0 index: latestListIndex ? latestListIndex + 1 : 0
}); })
}) .select(`
publicId,
name
`);
return data;
}), }),
reorder: publicProcedure reorder: publicProcedure
.input( .input(
@@ -57,52 +56,70 @@ export const listRouter = createTRPCRouter({
currentIndex: z.number(), currentIndex: z.number(),
newIndex: z.number(), newIndex: z.number(),
})) }))
.mutation(({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const userId = ctx.session?.user.id; const userId = ctx.user?.id;
if (!userId) return; if (!userId) return;
return ctx.db.transaction(async (tx) => { const list = await ctx.supabase
const [list] = await tx.select({ id: lists.id, boardId: lists.boardId }).from(lists).where(eq(lists.publicId, input.listId)) .from('list')
.select(`id, boardId`)
.eq('publicId', input.listId)
.limit(1)
.single();
if (!list) return; if (!list?.data) return;
await tx.execute(sql` const { data } = await ctx.supabase
UPDATE ${lists} .rpc('reorder_list', {
SET index = board_id: list.data.boardId,
CASE list_id: list.data.id,
WHEN ${lists.index} = ${input.currentIndex} AND ${lists.id} = ${list.id} THEN ${input.newIndex} current_index: input.currentIndex,
WHEN ${input.currentIndex} < ${input.newIndex} AND ${lists.index} > ${input.currentIndex} AND ${lists.index} <= ${input.newIndex} THEN ${lists.index} - 1 new_index: input.newIndex,
WHEN ${input.currentIndex} > ${input.newIndex} AND ${lists.index} >= ${input.newIndex} AND ${lists.index} < ${input.currentIndex} THEN ${lists.index} + 1 })
ELSE ${lists.index}
END return data;
WHERE ${lists.boardId} = ${list.boardId};
`);
})
}), }),
delete: publicProcedure delete: publicProcedure
.input( .input(
z.object({ z.object({
listPublicId: z.string().min(12), listPublicId: z.string().min(12),
})) }))
.mutation(({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const userId = ctx.session?.user.id; const userId = ctx.user?.id;
if (!userId) return; if (!userId) return;
return ctx.db.transaction(async (tx) => { const list = await ctx.supabase
const list = await tx.query.lists.findFirst({ .from('list')
where: eq(lists.publicId, input.listPublicId), .select(`id, boardId, index`)
.eq('publicId', input.listPublicId)
.limit(1)
.single();
if (!list.data) return;
const deletedAt = new Date().toISOString();
await ctx.supabase
.from('list')
.update({ deletedAt, deletedBy: userId })
.eq('id', list.data.id)
.is('deletedAt', null);
await ctx.supabase
.from('card')
.update({ deletedAt, deletedBy: userId })
.eq('listId', list.data.id)
.is('deletedAt', null);
const { data } = await ctx.supabase
.rpc('shift_list_index', {
board_id: list.data.boardId,
list_index: list.data.index
}) })
if (!list) return; return data;
await tx.update(lists).set({ deletedAt: new Date(), deletedBy: userId}).where(eq(lists.id, list.id));
await tx.update(cards).set({ deletedAt: new Date(), deletedBy: userId}).where(eq(cards.listId, list.id));
await tx.execute(sql`UPDATE ${lists} SET ${lists.index} = ${lists.index} - 1 WHERE ${lists.boardId} = ${list.boardId} AND ${lists.index} > ${list.index} AND ${lists.deletedAt} IS NULL;`);
})
}), }),
update: publicProcedure update: publicProcedure
.input( .input(
@@ -110,11 +127,18 @@ export const listRouter = createTRPCRouter({
listPublicId: z.string().min(12), listPublicId: z.string().min(12),
name: z.string().min(1), name: z.string().min(1),
})) }))
.mutation(({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const userId = ctx.session?.user.id; const userId = ctx.user?.id;
if (!userId) return; if (!userId) return;
return ctx.db.update(lists).set({ name: input.name }).where(and(eq(lists.publicId, input.listPublicId), isNull(lists.deletedAt))); const { data } = await ctx.supabase
.from('list')
.update({ name: input.name })
.eq('publicId', input.listPublicId)
.is('deletedAt', null)
.select(`publicId, name`);
return data;
}), }),
}); });

View File

@@ -12,56 +12,52 @@ import {
export const workspaceRouter = createTRPCRouter({ export const workspaceRouter = createTRPCRouter({
all: publicProcedure all: publicProcedure
.query(({ ctx }) => { .query(async ({ ctx }) => {
const userId = ctx.session?.user.id; const userId = ctx.user?.id;
if (!userId) return; if (!userId) return;
return ctx.db.query.workspaceMembers.findMany({ const { data } = await ctx.supabase
where: and(eq(workspaceMembers.userId, userId), isNull(workspaceMembers.deletedAt)), .from('workspace_members')
columns: { .select(`
role: true, role,
}, workspace (
with: { publicId,
workspace: { name
columns: { )
publicId: true, `)
name: true .eq('userId', userId)
} .is('deletedAt', null);
}
} return data;
});
}), }),
byId: publicProcedure byId: publicProcedure
.input(z.object({ publicId: z.string().min(12) })) .input(z.object({ publicId: z.string().min(12) }))
.query(({ ctx, input }) => { .query(async ({ ctx, input }) => {
const userId = ctx.session?.user.id; const userId = ctx.user?.id;
if (!userId) return; if (!userId) return;
return ctx.db.query.workspaces.findFirst({ const { data, error } = await ctx.supabase
where: and(eq(workspaces.publicId, input.publicId), isNull(workspaces.deletedAt)), .from('workspace')
columns: { .select(`
publicId: true, publicId,
}, members: workspace_members (
with: { publicId,
members: { role,
columns: { user (
publicId: true, id,
role: true, name,
}, email
with: { )
user: { )
columns: { `)
id: true, .eq('publicId', input.publicId)
name: true, .is('deletedAt', null)
email: true, .limit(1)
} .single();
}
} return data;
}
}
});
}), }),
create: publicProcedure create: publicProcedure
.input( .input(
@@ -70,35 +66,47 @@ export const workspaceRouter = createTRPCRouter({
}), }),
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const userId = ctx.session?.user.id; const userId = ctx.user?.id;
console.log('>>> here <<<')
if (!userId) return; if (!userId) return;
const workspace = await ctx.db.insert(workspaces).values({ const workspace = await ctx.supabase
publicId: generateUID(), .from('workspace')
name: input.name, .insert({
slug: input.name.toLowerCase(), publicId: generateUID(),
createdBy: userId, name: input.name,
}).returning({ id: workspaces.id }); slug: input.name.toLowerCase(),
createdBy: userId,
})
.select(`
id,
publicId,
name
`)
.limit(1)
.single()
const workspaceId = workspace[0]?.id; const newWorkspaceId = workspace.data?.id
if (!workspaceId) return; if (!newWorkspaceId) return;
await ctx.db.insert(workspaceMembers).values({ await ctx.supabase
publicId: generateUID(), .from('workspace_members')
userId, .insert({
workspaceId: workspaceId, publicId: generateUID(),
createdBy: userId, userId,
role: 'admin' workspaceId: newWorkspaceId,
}) createdBy: userId,
role: 'admin'
})
return ctx.db.query.workspaces.findFirst({ const newWorkspace = { ...workspace.data };
where: eq(workspaces.id, workspaceId),
columns: { delete newWorkspace.id;
publicId: true,
name: true, return newWorkspace;
},
});
}), }),
}); });

View File

@@ -600,7 +600,22 @@ export type Database = {
[_ in never]: never [_ in never]: never
} }
Functions: { Functions: {
[_ in never]: never reorder_list: {
Args: {
board_id: number
list_id: number
current_index: number
new_index: number
}
Returns: undefined
}
shift_list_index: {
Args: {
board_id: number
list_index: number
}
Returns: undefined
}
} }
Enums: { Enums: {
role: "admin" | "member" | "guest" role: "admin" | "member" | "guest"

View File

@@ -86,17 +86,20 @@ export default function BoardPage() {
enableReinitialize: true, enableReinitialize: true,
}); });
if (!boardId) return <></>; const { data, isSuccess } = api.board.byId.useQuery(
{ id: boardId ?? "" },
api.board.byId.useQuery(
{ id: boardId },
{ {
onSuccess: (data) => { enabled: !!boardId,
if (data) setBoardData(data);
},
}, },
); );
if (isSuccess && data) {
console.log({ data });
setBoardData(data);
}
if (!boardId) return <></>;
const openNewListForm = (publicBoardId: string) => { const openNewListForm = (publicBoardId: string) => {
openModal("NEW_LIST"); openModal("NEW_LIST");
setSelectedPublicListId(publicBoardId); setSelectedPublicListId(publicBoardId);

View File

@@ -0,0 +1,23 @@
CREATE OR REPLACE FUNCTION reorder_list(board_id BIGINT, list_id BIGINT, current_index INT, new_index INT)
RETURNS VOID
LANGUAGE SQL
AS $$
UPDATE list
SET index =
CASE
WHEN index = current_index AND id = list_id THEN new_index
WHEN current_index < new_index AND index > current_index AND index <= new_index THEN index - 1
WHEN current_index > new_index AND index >= new_index AND index < current_index THEN index + 1
ELSE index
END
WHERE "boardId" = board_id;
$$;
CREATE OR REPLACE FUNCTION shift_list_index(board_id BIGINT, list_index INT)
RETURNS VOID
LANGUAGE SQL
AS $$
UPDATE list
SET index = index - 1
WHERE "boardId" = board_id AND index > list_index AND "deletedAt" IS NULL;
$$;