feat: delete workspace

This commit is contained in:
Henry
2024-08-30 22:28:07 +01:00
parent 5c8a1c5069
commit a06500a949
19 changed files with 1647 additions and 365 deletions

View File

@@ -1,7 +1,7 @@
import { twMerge } from "tailwind-merge";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
variant?: "primary" | "secondary" | "danger";
isLoading?: boolean;
}
@@ -19,8 +19,11 @@ const Button = ({
"bg-light-1000 dark:bg-dark-1000 dark:text-dark-50",
variant === "secondary" &&
"border-[1px] border-light-600 bg-light-50 dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000",
variant === "danger" &&
"dark:text-red-1000 border-[1px] border-red-600 bg-red-50 dark:border-red-600 dark:bg-red-500",
props.disabled && "opacity-50",
)}
disabled={isLoading}
disabled={isLoading ?? props.disabled}
{...props}
>
<span className="relative flex items-center justify-center">

View File

@@ -1,9 +1,14 @@
import { useEffect } from "react";
import { api } from "~/utils/api";
import { HiXMark } from "react-icons/hi2";
import { useModal } from "~/providers/modal";
import { useWorkspace } from "~/providers/workspace";
import { useForm } from "react-hook-form";
import { HiXMark } from "react-icons/hi2";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { useWorkspace } from "~/providers/workspace";
import Button from "~/components/Button";
import Input from "~/components/Input";
interface FormValues {
name: string;
@@ -11,20 +16,23 @@ interface FormValues {
export function NewWorkspaceForm() {
const { closeModal } = useModal();
const { showPopup } = usePopup();
const { switchWorkspace } = useWorkspace();
const { register, handleSubmit } = useForm<FormValues>();
const createWorkspace = api.workspace.create.useMutation({
onSuccess: (values) => {
try {
if (values?.publicId && values.name) {
switchWorkspace({ publicId: values.publicId, name: values.name });
closeModal();
}
} catch (e) {
console.log(e);
if (values?.publicId && values.name) {
switchWorkspace({ publicId: values.publicId, name: values.name });
closeModal();
}
},
onError: () => {
showPopup({
header: "Unable to create workspace",
message: "Please try again later, or contact customer support.",
});
},
});
useEffect(() => {
@@ -40,40 +48,36 @@ export function NewWorkspaceForm() {
};
return (
<div className="p-5">
<div className="flex w-full items-center justify-between pb-4">
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
New workspace
</h2>
<button
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
onClick={() => closeModal()}
>
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
</button>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<label
htmlFor="workspace-name"
className="block pb-2 text-sm font-normal leading-6 text-neutral-900 dark:text-dark-1000"
>
Name
</label>
<input
id="workspace-name"
{...register("name", { required: true })}
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">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="px-5 pt-5">
<div className="flex w-full items-center justify-between pb-4">
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
New workspace
</h2>
<button
type="submit"
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"
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
onClick={(e) => {
e.preventDefault();
closeModal();
}}
>
Create workspace
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
</button>
</div>
</form>
</div>
<Input
id="workspace-name"
placeholder="Workspace name"
{...register("name")}
/>
</div>
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
<div>
<Button type="submit" isLoading={createWorkspace.isPending}>
Create workspace
</Button>
</div>
</div>
</form>
);
}

View File

@@ -114,20 +114,20 @@ export const boardRouter = createTRPCRouter({
const deletedAt = new Date().toISOString();
await boardRepo.destroy(ctx.db, {
await boardRepo.softDelete(ctx.db, {
boardId: board.id,
deletedAt,
deletedBy: userId,
});
if (listIds.length) {
await listRepo.destroyAllByBoardId(ctx.db, {
await listRepo.softDeleteAllByBoardId(ctx.db, {
boardId: board.id,
deletedAt,
deletedBy: userId,
});
await cardRepo.destroyAllByListIds(ctx.db, {
await cardRepo.softDeleteAllByListIds(ctx.db, {
listIds,
deletedAt,
deletedBy: userId,

View File

@@ -149,7 +149,7 @@ export const cardRouter = createTRPCRouter({
);
if (existingLabel) {
await cardRepo.destroyCardLabelRelationship(ctx.db, cardLabelIds);
await cardRepo.hardDeleteCardLabelRelationship(ctx.db, cardLabelIds);
return { newLabel: false };
}
@@ -200,7 +200,7 @@ export const cardRouter = createTRPCRouter({
);
if (existingMember) {
await cardRepo.destroyCardMemberRelationship(ctx.db, cardMemberIds);
await cardRepo.hardDeleteCardMemberRelationship(ctx.db, cardMemberIds);
return { newMember: false };
}
@@ -272,7 +272,7 @@ export const cardRouter = createTRPCRouter({
const deletedAt = new Date().toISOString();
await cardRepo.destroy(ctx.db, {
await cardRepo.softDelete(ctx.db, {
cardId: card.id,
deletedAt,
deletedBy: userId,

View File

@@ -81,9 +81,9 @@ export const labelRouter = createTRPCRouter({
code: "NOT_FOUND",
});
await cardRepo.destroyAllCardLabelRelationships(ctx.db, label.id);
await cardRepo.hardDeleteAllCardLabelRelationships(ctx.db, label.id);
await labelRepo.destroy(ctx.db, label.id);
await labelRepo.hardDelete(ctx.db, label.id);
return { success: true };
}),

View File

@@ -98,13 +98,13 @@ export const listRouter = createTRPCRouter({
const deletedAt = new Date().toISOString();
await listRepo.destroyById(ctx.db, {
await listRepo.softDeleteById(ctx.db, {
listId: list.id,
deletedAt,
deletedBy: userId,
});
await cardRepo.destroyAllByListIds(ctx.db, {
await cardRepo.softDeleteAllByListIds(ctx.db, {
listIds: [list.id],
deletedAt,
deletedBy: userId,

View File

@@ -49,6 +49,22 @@ export const workspaceRouter = createTRPCRouter({
createdBy: userId,
});
if (!result?.publicId)
throw new TRPCError({
message: `Unable to create workspace`,
code: "INTERNAL_SERVER_ERROR",
});
return result;
}),
delete: protectedProcedure
.input(z.object({ workspacePublicId: z.string().min(12) }))
.mutation(async ({ ctx, input }) => {
const { data } = await workspaceRepo.hardDelete(
ctx.db,
input.workspacePublicId,
);
return data;
}),
});

View File

@@ -0,0 +1,66 @@
DROP TABLE IF EXISTS "account";--> statement-breakpoint
DROP TABLE IF EXISTS "session";--> statement-breakpoint
DROP TABLE IF EXISTS "verificationToken";--> statement-breakpoint
ALTER TABLE "board" DROP CONSTRAINT "board_workspaceId_workspace_id_fk";
--> statement-breakpoint
ALTER TABLE "_card_workspace_members" DROP CONSTRAINT "_card_workspace_members_cardId_card_id_fk";
--> statement-breakpoint
ALTER TABLE "card" DROP CONSTRAINT "card_listId_list_id_fk";
--> statement-breakpoint
ALTER TABLE "_card_labels" DROP CONSTRAINT IF EXISTS "_card_labels_cardId_card_id_fk";
--> statement-breakpoint
ALTER TABLE "_card_labels" DROP CONSTRAINT IF EXISTS "_card_labels_labelId_label_id_fk";
--> statement-breakpoint
ALTER TABLE "label" DROP CONSTRAINT "label_boardId_board_id_fk";
--> statement-breakpoint
ALTER TABLE "list" DROP CONSTRAINT "list_boardId_board_id_fk";
--> statement-breakpoint
ALTER TABLE "workspace_members" DROP CONSTRAINT "workspace_members_workspaceId_workspace_id_fk";
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "board" ADD CONSTRAINT "board_workspaceId_workspace_id_fk" FOREIGN KEY ("workspaceId") REFERENCES "workspace"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "_card_workspace_members" ADD CONSTRAINT "_card_workspace_members_workspaceMemberId_workspace_members_id_fk" FOREIGN KEY ("workspaceMemberId") REFERENCES "workspace_members"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "card" ADD CONSTRAINT "card_listId_list_id_fk" FOREIGN KEY ("listId") REFERENCES "list"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "_card_labels" ADD CONSTRAINT "_card_labels_cardId_card_id_fk" FOREIGN KEY ("cardId") REFERENCES "card"("id") ON DELETE CASCADE;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "_card_labels" ADD CONSTRAINT "_card_labels_labelId_label_id_fk" FOREIGN KEY ("labelId") REFERENCES "label"("id") ON DELETE CASCADE;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "label" ADD CONSTRAINT "label_boardId_board_id_fk" FOREIGN KEY ("boardId") REFERENCES "board"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "list" ADD CONSTRAINT "list_boardId_board_id_fk" FOREIGN KEY ("boardId") REFERENCES "board"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_workspaceId_workspace_id_fk" FOREIGN KEY ("workspaceId") REFERENCES "workspace"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,13 @@
"when": 1713021051974,
"tag": "0001_stale_mattie_franklin",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1724967733894,
"tag": "0002_clever_robin_chapel",
"breakpoints": true
}
]
}
}

View File

@@ -144,7 +144,7 @@ export const update = async (
return data;
};
export const destroy = async (
export const softDelete = async (
db: SupabaseClient<Database>,
args: {
boardId: number;
@@ -160,3 +160,12 @@ export const destroy = async (
return result;
};
export const hardDelete = async (
db: SupabaseClient<Database>,
workspaceId: number,
) => {
const result = db.from("board").delete().eq("workspaceId", workspaceId);
return result;
};

View File

@@ -74,39 +74,6 @@ export const update = async (
return data;
};
export const destroy = async (
db: SupabaseClient<Database>,
args: {
cardId: number;
deletedAt: string;
deletedBy: string;
},
) => {
const result = await db
.from("card")
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
.eq("id", args.cardId);
return result;
};
export const destroyAllByListIds = async (
db: SupabaseClient<Database>,
args: {
listIds: number[];
deletedAt: string;
deletedBy: string;
},
) => {
const result = await db
.from("card")
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
.in("listId", args.listIds)
.is("deletedAt", null);
return result;
};
export const getCardWithListByPublicId = async (
db: SupabaseClient<Database>,
cardPublicId: string,
@@ -168,28 +135,6 @@ export const bulkCreate = async (
return data;
};
export const destroyCardLabelRelationship = async (
db: SupabaseClient<Database>,
args: { cardId: number; labelId: number },
) => {
const result = await db
.from("_card_labels")
.delete()
.eq("cardId", args.cardId)
.eq("labelId", args.labelId);
return result;
};
export const destroyAllCardLabelRelationships = async (
db: SupabaseClient<Database>,
labelId: number,
) => {
const result = await db.from("_card_labels").delete().eq("labelId", labelId);
return result;
};
export const createCardLabelRelationship = async (
db: SupabaseClient<Database>,
cardLabelRelationshipInput: { cardId: number; labelId: number },
@@ -217,19 +162,6 @@ export const getCardMemberRelationship = async (
return data;
};
export const destroyCardMemberRelationship = async (
db: SupabaseClient<Database>,
args: { cardId: number; memberId: number },
) => {
const result = await db
.from("_card_workspace_members")
.delete()
.eq("cardId", args.cardId)
.eq("workspaceMemberId", args.memberId);
return result;
};
export const createCardMemberRelationship = async (
db: SupabaseClient<Database>,
cardMemberRelationshipInput: { cardId: number; memberId: number },
@@ -353,3 +285,71 @@ export const pushIndex = async (
return data;
};
export const softDelete = async (
db: SupabaseClient<Database>,
args: {
cardId: number;
deletedAt: string;
deletedBy: string;
},
) => {
const result = await db
.from("card")
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
.eq("id", args.cardId);
return result;
};
export const softDeleteAllByListIds = async (
db: SupabaseClient<Database>,
args: {
listIds: number[];
deletedAt: string;
deletedBy: string;
},
) => {
const result = await db
.from("card")
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
.in("listId", args.listIds)
.is("deletedAt", null);
return result;
};
export const hardDeleteCardMemberRelationship = async (
db: SupabaseClient<Database>,
args: { cardId: number; memberId: number },
) => {
const result = await db
.from("_card_workspace_members")
.delete()
.eq("cardId", args.cardId)
.eq("workspaceMemberId", args.memberId);
return result;
};
export const hardDeleteCardLabelRelationship = async (
db: SupabaseClient<Database>,
args: { cardId: number; labelId: number },
) => {
const result = await db
.from("_card_labels")
.delete()
.eq("cardId", args.cardId)
.eq("labelId", args.labelId);
return result;
};
export const hardDeleteAllCardLabelRelationships = async (
db: SupabaseClient<Database>,
labelId: number,
) => {
const result = await db.from("_card_labels").delete().eq("labelId", labelId);
return result;
};

View File

@@ -79,7 +79,7 @@ export const update = async (
return data;
};
export const destroy = async (
export const hardDelete = async (
db: SupabaseClient<Database>,
labelId: number,
) => {

View File

@@ -85,40 +85,6 @@ export const update = async (
return data;
};
export const destroyAllByBoardId = async (
db: SupabaseClient<Database>,
args: {
boardId: number;
deletedAt: string;
deletedBy: string;
},
) => {
const result = await db
.from("list")
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
.eq("boardId", args.boardId)
.is("deletedAt", null);
return result;
};
export const destroyById = async (
db: SupabaseClient<Database>,
args: {
listId: number;
deletedAt: string;
deletedBy: string;
},
) => {
const result = await db
.from("list")
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
.eq("id", args.listId)
.is("deletedAt", null);
return result;
};
export const reorder = async (
db: SupabaseClient<Database>,
args: {
@@ -152,3 +118,37 @@ export const shiftIndex = async (
return data;
};
export const softDeleteAllByBoardId = async (
db: SupabaseClient<Database>,
args: {
boardId: number;
deletedAt: string;
deletedBy: string;
},
) => {
const result = await db
.from("list")
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
.eq("boardId", args.boardId)
.is("deletedAt", null);
return result;
};
export const softDeleteById = async (
db: SupabaseClient<Database>,
args: {
listId: number;
deletedAt: string;
deletedBy: string;
},
) => {
const result = await db
.from("list")
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
.eq("id", args.listId)
.is("deletedAt", null);
return result;
};

View File

@@ -127,3 +127,15 @@ export const getAllMembersByPublicIds = async (
return data;
};
export const hardDelete = async (
db: SupabaseClient<Database>,
workspacePublicId: string,
) => {
const result = db
.from("workspace")
.delete()
.eq("publicId", workspacePublicId);
return result;
};

View File

@@ -12,217 +12,237 @@ import {
bigint,
} from "drizzle-orm/pg-core";
export const importSourceEnum = pgEnum('source', ['trello']);
export const importStatusEnum = pgEnum('status', ['started', 'success', 'failed']);
export const memberRoleEnum = pgEnum('role', ['admin', 'member', 'guest']);
export const importSourceEnum = pgEnum("source", ["trello"]);
export const importStatusEnum = pgEnum("status", [
"started",
"success",
"failed",
]);
export const memberRoleEnum = pgEnum("role", ["admin", "member", "guest"]);
export const boards = pgTable(
"board",
{
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
name: varchar("name", { length: 255 }).notNull(),
createdBy: uuid("createdBy").notNull().references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
deletedAt: timestamp("deletedAt"),
deletedBy: uuid("deletedBy").references(() => users.id),
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
workspaceId: bigint("workspaceId", { mode: "number" }).notNull().references(() => workspaces.id),
},
);
export const boards = pgTable("board", {
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
name: varchar("name", { length: 255 }).notNull(),
createdBy: uuid("createdBy")
.notNull()
.references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
deletedAt: timestamp("deletedAt"),
deletedBy: uuid("deletedBy").references(() => users.id),
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
workspaceId: bigint("workspaceId", { mode: "number" })
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
});
export const boardsRelations = relations(boards, ({ one, many }) => ({
createdBy: one(users, {
fields: [boards.createdBy],
references: [users.id],
}),
createdBy: one(users, {
fields: [boards.createdBy],
references: [users.id],
}),
lists: many(lists),
labels: many(labels),
deletedBy: one(users, {
fields: [boards.deletedBy],
references: [users.id],
}),
fields: [boards.deletedBy],
references: [users.id],
}),
import: one(imports, {
fields: [boards.importId],
references: [imports.id],
}),
fields: [boards.importId],
references: [imports.id],
}),
workspace: one(workspaces, {
fields: [boards.workspaceId],
references: [workspaces.id],
}),
fields: [boards.workspaceId],
references: [workspaces.id],
}),
}));
export const imports = pgTable(
"import",
{
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
source: importSourceEnum('source').notNull(),
status: importStatusEnum('status').notNull(),
createdBy: uuid("createdBy").notNull().references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
},
);
export const imports = pgTable("import", {
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
source: importSourceEnum("source").notNull(),
status: importStatusEnum("status").notNull(),
createdBy: uuid("createdBy")
.notNull()
.references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export const importsRelations = relations(imports, ({ one, many }) => ({
createdBy: one(users, {
fields: [imports.createdBy],
references: [users.id],
}),
createdBy: one(users, {
fields: [imports.createdBy],
references: [users.id],
}),
boards: many(boards),
cards: many(cards),
lists: many(lists),
labels: many(labels)
labels: many(labels),
}));
export const labels = pgTable(
"label",
{
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
name: varchar("name", { length: 255 }).notNull(),
colourCode: varchar("colourCode", { length: 12 }),
createdBy: uuid("createdBy").notNull().references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
boardId: bigint("boardId", { mode: "number" }).notNull().references(() => boards.id),
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
}
);
export const labels = pgTable("label", {
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
name: varchar("name", { length: 255 }).notNull(),
colourCode: varchar("colourCode", { length: 12 }),
createdBy: uuid("createdBy")
.notNull()
.references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
boardId: bigint("boardId", { mode: "number" })
.notNull()
.references(() => boards.id, { onDelete: "cascade" }),
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
});
export const labelsRelations = relations(labels, ({ one, many }) => ({
createdBy: one(users, {
fields: [labels.createdBy],
references: [users.id],
}),
createdBy: one(users, {
fields: [labels.createdBy],
references: [users.id],
}),
board: one(boards, {
fields: [labels.boardId],
references: [boards.id],
}),
fields: [labels.boardId],
references: [boards.id],
}),
cards: many(cardsToLabels),
import: one(imports, {
fields: [labels.importId],
references: [imports.id],
}),
fields: [labels.importId],
references: [imports.id],
}),
}));
export const cardsToLabels = pgTable(
"_card_labels",
{
cardId: bigint("cardId", { mode: "number" }).notNull().references(() => cards.id),
labelId: bigint("labelId", { mode: "number" }).notNull().references(() => labels.id),
}, (t) => ({
cardId: bigint("cardId", { mode: "number" })
.notNull()
.references(() => cards.id),
labelId: bigint("labelId", { mode: "number" })
.notNull()
.references(() => labels.id, { onDelete: "cascade" }),
},
(t) => ({
pk: primaryKey(t.cardId, t.labelId),
}),
);
export const cardToLabelsRelations = relations(cardsToLabels, ({ one }) => ({
card: one(cards, {
fields: [cardsToLabels.cardId],
references: [cards.id],
}),
card: one(cards, {
fields: [cardsToLabels.cardId],
references: [cards.id],
}),
label: one(labels, {
fields: [cardsToLabels.labelId],
references: [labels.id],
}),
fields: [cardsToLabels.labelId],
references: [labels.id],
}),
}));
export const cardToWorkspaceMembers = pgTable(
"_card_workspace_members",
{
cardId: bigint("cardId", { mode: "number" }).notNull().references(() => cards.id),
workspaceMemberId: bigint("workspaceMemberId", { mode: "number" }).notNull().references(() => workspaceMembers.id),
}, (t) => ({
cardId: bigint("cardId", { mode: "number" })
.notNull()
.references(() => cards.id),
workspaceMemberId: bigint("workspaceMemberId", { mode: "number" })
.notNull()
.references(() => workspaceMembers.id, { onDelete: "cascade" }),
},
(t) => ({
pk: primaryKey(t.cardId, t.workspaceMemberId),
}),
);
export const cardToWorkspaceMembersRelations = relations(cardToWorkspaceMembers, ({ one }) => ({
card: one(cards, {
fields: [cardToWorkspaceMembers.cardId],
references: [cards.id],
}),
member: one(workspaceMembers, {
fields: [cardToWorkspaceMembers.workspaceMemberId],
references: [workspaceMembers.id],
}),
}));
export const lists = pgTable(
"list",
{
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
name: varchar("name", { length: 255 }).notNull(),
index: integer("index").notNull(),
createdBy: uuid("createdBy").notNull().references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
deletedAt: timestamp("deletedAt"),
deletedBy: uuid("deletedBy").references(() => users.id),
boardId: bigint("boardId", { mode: "number" }).notNull().references(() => boards.id),
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
}
export const cardToWorkspaceMembersRelations = relations(
cardToWorkspaceMembers,
({ one }) => ({
card: one(cards, {
fields: [cardToWorkspaceMembers.cardId],
references: [cards.id],
}),
member: one(workspaceMembers, {
fields: [cardToWorkspaceMembers.workspaceMemberId],
references: [workspaceMembers.id],
}),
}),
);
export const lists = pgTable("list", {
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
name: varchar("name", { length: 255 }).notNull(),
index: integer("index").notNull(),
createdBy: uuid("createdBy")
.notNull()
.references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
deletedAt: timestamp("deletedAt"),
deletedBy: uuid("deletedBy").references(() => users.id),
boardId: bigint("boardId", { mode: "number" })
.notNull()
.references(() => boards.id, { onDelete: "cascade" }),
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
});
export const listsRelations = relations(lists, ({ one, many }) => ({
createdBy: one(users, {
fields: [lists.createdBy],
references: [users.id],
}),
createdBy: one(users, {
fields: [lists.createdBy],
references: [users.id],
}),
board: one(boards, {
fields: [lists.boardId],
references: [boards.id],
}),
fields: [lists.boardId],
references: [boards.id],
}),
cards: many(cards),
deletedBy: one(users, {
fields: [lists.deletedBy],
references: [users.id],
}),
fields: [lists.deletedBy],
references: [users.id],
}),
import: one(imports, {
fields: [lists.importId],
references: [imports.id],
}),
fields: [lists.importId],
references: [imports.id],
}),
}));
export const cards = pgTable(
"card",
{
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
title: varchar("title", { length: 255 }).notNull(),
description: text("description"),
index: integer("index").notNull(),
createdBy: uuid("createdBy").notNull().references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
deletedAt: timestamp("deletedAt"),
deletedBy: uuid("deletedBy").references(() => users.id),
listId: bigint("listId", { mode: "number" }).notNull().references(() => lists.id),
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
}
);
export const cards = pgTable("card", {
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
title: varchar("title", { length: 255 }).notNull(),
description: text("description"),
index: integer("index").notNull(),
createdBy: uuid("createdBy")
.notNull()
.references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
deletedAt: timestamp("deletedAt"),
deletedBy: uuid("deletedBy").references(() => users.id),
listId: bigint("listId", { mode: "number" })
.notNull()
.references(() => lists.id, { onDelete: "cascade" }),
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
});
export const cardsRelations = relations(cards, ({ one, many }) => ({
createdBy: one(users, {
fields: [cards.createdBy],
references: [users.id],
}),
createdBy: one(users, {
fields: [cards.createdBy],
references: [users.id],
}),
list: one(lists, {
fields: [cards.listId],
references: [lists.id],
}),
fields: [cards.listId],
references: [lists.id],
}),
deletedBy: one(users, {
fields: [cards.deletedBy],
references: [users.id],
}),
fields: [cards.deletedBy],
references: [users.id],
}),
labels: many(cardsToLabels),
members: many(cardToWorkspaceMembers),
import: one(imports, {
fields: [cards.importId],
references: [imports.id],
}),
fields: [cards.importId],
references: [imports.id],
}),
}));
export const users = pgTable("user", {
@@ -241,51 +261,55 @@ export const usersRelations = relations(users, ({ many }) => ({
workspaces: many(workspaces),
}));
export const workspaces = pgTable(
"workspace",
{
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
name: varchar("name", { length: 255 }).notNull(),
slug: varchar("slug", { length: 255 }).notNull().unique(),
createdBy: uuid("createdBy").notNull().references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
deletedAt: timestamp("deletedAt"),
deletedBy: uuid("deletedBy").references(() => users.id),
}
);
export const workspaces = pgTable("workspace", {
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
name: varchar("name", { length: 255 }).notNull(),
slug: varchar("slug", { length: 255 }).notNull().unique(),
createdBy: uuid("createdBy")
.notNull()
.references(() => users.id),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
deletedAt: timestamp("deletedAt"),
deletedBy: uuid("deletedBy").references(() => users.id),
});
export const workspaceRelations = relations(workspaces, ({ one, many }) => ({
user: one(users, { fields: [workspaces.createdBy], references: [users.id] }),
members: many(workspaceMembers)
members: many(workspaceMembers),
}));
export const workspaceMembers = pgTable(
"workspace_members",
{
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
userId: uuid("userId").notNull().references(() => users.id),
workspaceId: bigint("workspaceId", { mode: "number" }).notNull().references(() => workspaces.id),
createdBy: uuid("createdBy").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
deletedAt: timestamp("deletedAt"),
role: memberRoleEnum('role').notNull(),
},
export const workspaceMembers = pgTable("workspace_members", {
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
userId: uuid("userId")
.notNull()
.references(() => users.id),
workspaceId: bigint("workspaceId", { mode: "number" })
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
createdBy: uuid("createdBy").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
deletedAt: timestamp("deletedAt"),
role: memberRoleEnum("role").notNull(),
});
export const usersToWorkspacesRelations = relations(
workspaceMembers,
({ one }) => ({
addedBy: one(users, {
fields: [workspaceMembers.createdBy],
references: [users.id],
}),
user: one(users, {
fields: [workspaceMembers.userId],
references: [users.id],
}),
workspace: one(workspaces, {
fields: [workspaceMembers.workspaceId],
references: [workspaces.id],
}),
}),
);
export const usersToWorkspacesRelations = relations(workspaceMembers, ({ one }) => ({
addedBy: one(users, { fields: [workspaceMembers.createdBy], references: [users.id] }),
user: one(users, {
fields: [workspaceMembers.userId],
references: [users.id],
}),
workspace: one(workspaces, {
fields: [workspaceMembers.workspaceId],
references: [workspaces.id],
}),
}));

View File

@@ -0,0 +1,96 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { api } from "~/utils/api";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { useWorkspace } from "~/providers/workspace";
import Button from "~/components/Button";
export function DeleteWorkspaceConfirmation() {
const { closeModal } = useModal();
const { workspace, switchWorkspace, availableWorkspaces } = useWorkspace();
const { showPopup } = usePopup();
const router = useRouter();
const [isAcknowledgmentChecked, setIsAcknowledgmentChecked] = useState(false);
const deleteWorkspaceMutation = api.workspace.delete.useMutation({
onSuccess: () => {
closeModal();
const filteredWorkspaces = availableWorkspaces.filter(
(ws) => ws.publicId !== workspace?.publicId,
);
if (filteredWorkspaces.length > 0 && filteredWorkspaces[0]) {
switchWorkspace(filteredWorkspaces[0]);
} else {
router.push("/");
}
},
onError: () => {
closeModal();
showPopup({
header: "Error deleting workspace",
message: "Please try again later, or contact customer support.",
});
},
});
const handleDeleteWorkspace = () => {
deleteWorkspaceMutation.mutate({
workspacePublicId: workspace?.publicId,
});
};
return (
<div className="p-5">
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
{`Are you sure you want to delete the workspace ${workspace?.name}?`}
</h2>
<p className="mb-4 text-sm text-light-900 dark:text-dark-900">
Keep in mind that this action is irreversible.
</p>
<p className="text-sm text-light-900 dark:text-dark-900">
This will result in the permanent deletion of all data associated with
this workspace.
</p>
</div>
<div className="relative flex items-start">
<div className="flex h-6 items-center">
<input
id="acknowledgment"
name="acknowledgment"
type="checkbox"
aria-describedby="acknowledgment-description"
className="mt-2 h-[14px] w-[14px] rounded border-gray-300 bg-transparent text-indigo-600 focus:shadow-none focus:ring-0 focus:ring-offset-0"
checked={isAcknowledgmentChecked}
onChange={() =>
setIsAcknowledgmentChecked(!isAcknowledgmentChecked)
}
/>
</div>
<div className="ml-3 text-sm leading-6">
<p id="comments-description" className="text-dark-1000">
I acknowledge that all of the workspace data will be permanently
deleted and want to proceed.
</p>
</div>
</div>
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
<Button variant="secondary" onClick={() => closeModal()}>
Cancel
</Button>
<Button
variant="danger"
onClick={handleDeleteWorkspace}
disabled={!isAcknowledgmentChecked}
isLoading={deleteWorkspaceMutation.isPending}
>
Delete workspace
</Button>
</div>
</div>
);
}

View File

@@ -1,10 +1,10 @@
import { useModal } from "~/providers/modal";
import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import Button from "~/components/Button";
import { DeleteWorkspaceConfirmation } from "./components/DeleteWorkspaceConfirmation";
export default function SettingsPage() {
const { modalContentType } = useModal();
const { modalContentType, openModal } = useModal();
return (
<div className="px-28 py-12">
@@ -14,16 +14,23 @@ export default function SettingsPage() {
</h1>
</div>
<div className="mt-8 flow-root">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg"></div>
</div>
</div>
<div className="border-t border-light-300 dark:border-dark-300">
<h2 className="mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
Delete workspace
</h2>
<p className="mb-8 mt-2 text-sm text-neutral-500 dark:text-dark-900">
Once you delete your workspace, there is no going back. Please be
certain.
</p>
<Button variant="primary" onClick={() => openModal("DELETE_WORKSPACE")}>
Delete workspace
</Button>
</div>
<Modal>
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
{modalContentType === "DELETE_WORKSPACE" && (
<DeleteWorkspaceConfirmation />
)}
</Modal>
</div>
);

View File

@@ -207,10 +207,25 @@ USING (
)
);
CREATE POLICY "Allow access to user's workspaces"
CREATE POLICY "Allow viewing user's workspaces"
ON public.workspace
AS PERMISSIVE
FOR ALL
FOR SELECT
TO authenticated
USING (
id IN (
SELECT "workspaceId"
FROM workspace_members
WHERE "userId" = auth.uid()
)
OR
"createdBy" = auth.uid()
);
CREATE POLICY "Allow updating user's workspaces"
ON public.workspace
AS PERMISSIVE
FOR UPDATE
TO authenticated
USING (
id IN (
@@ -220,6 +235,26 @@ USING (
)
);
CREATE POLICY "Allow deleting user's workspaces"
ON public.workspace
AS PERMISSIVE
FOR DELETE
TO authenticated
USING (
id IN (
SELECT "workspaceId"
FROM workspace_members
WHERE "userId" = auth.uid()
)
);
CREATE POLICY "Allow authenticated users to create workspaces"
ON public.workspace
AS PERMISSIVE
FOR INSERT
TO authenticated
USING (true);
CREATE POLICY "Allow access to user's own workspace membership"
ON public.workspace_members
AS PERMISSIVE