From 4759d460ac4d19b7528cfbdeda30adb73a39bf7a Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 29 Oct 2024 13:17:44 +0000 Subject: [PATCH] feat: remove members --- src/components/Dropdown.tsx | 44 + src/pages/members/index.tsx | 2 + src/providers/modal.tsx | 24 +- src/server/api/routers/member.ts | 34 + .../db/migrations/0004_rainy_archangel.sql | 6 + .../db/migrations/meta/0004_snapshot.json | 1037 +++++++++++++++++ src/server/db/migrations/meta/_journal.json | 7 + src/server/db/repository/board.repo.ts | 8 +- src/server/db/repository/card.repo.ts | 6 +- src/server/db/repository/member.repo.ts | 17 + src/server/db/repository/workspace.repo.ts | 2 +- src/server/db/schema.ts | 5 + src/utils/supabase/api.ts | 2 +- .../components/DeleteMemberConfirmation.tsx | 57 + src/views/members/index.tsx | 122 +- 15 files changed, 1322 insertions(+), 51 deletions(-) create mode 100644 src/components/Dropdown.tsx create mode 100644 src/server/db/migrations/0004_rainy_archangel.sql create mode 100644 src/server/db/migrations/meta/0004_snapshot.json create mode 100644 src/views/members/components/DeleteMemberConfirmation.tsx diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx new file mode 100644 index 00000000..b0fc53a4 --- /dev/null +++ b/src/components/Dropdown.tsx @@ -0,0 +1,44 @@ +import { Fragment } from "react"; +import { Menu, Transition } from "@headlessui/react"; +export default function Dropdown({ + items, + children, +}: { + items: { label: string; action: () => void }[]; + children: React.ReactNode; +}) { + return ( + +
+ + {children} + +
+ + + +
+ {items.map((item) => ( + + + + ))} +
+
+
+
+ ); +} diff --git a/src/pages/members/index.tsx b/src/pages/members/index.tsx index 6736584e..1961750d 100644 --- a/src/pages/members/index.tsx +++ b/src/pages/members/index.tsx @@ -1,5 +1,6 @@ import { WorkspaceProvider } from "~/providers/workspace"; import Dashboard from "~/components/dashboard"; +import Popup from "~/components/Popup"; import MembersView from "~/views/members"; export default function MembersPage() { @@ -8,6 +9,7 @@ export default function MembersPage() { + ); } diff --git a/src/providers/modal.tsx b/src/providers/modal.tsx index 4104b045..d2d796e8 100644 --- a/src/providers/modal.tsx +++ b/src/providers/modal.tsx @@ -2,10 +2,15 @@ import { createContext, useContext, useState } from "react"; type ModalContextType = { isOpen: boolean; - openModal: (contentType: string, entityId?: string) => void; + openModal: ( + contentType: string, + entityId?: string, + entityLabel?: string, + ) => void; closeModal: () => void; modalContentType: string; entityId: string; + entityLabel: string; }; interface Props { @@ -17,12 +22,18 @@ const ModalContext = createContext(undefined); export const ModalProvider: React.FC = ({ children }) => { const [isOpen, setIsOpen] = useState(false); const [entityId, setEntityId] = useState(""); + const [entityLabel, setEntityLabel] = useState(""); const [modalContentType, setModalContentType] = useState(""); - const openModal = (contentType: string, entityId?: string) => { + const openModal = ( + contentType: string, + entityId?: string, + entityLabel?: string, + ) => { setIsOpen(true); setModalContentType(contentType); if (entityId) setEntityId(entityId); + if (entityLabel) setEntityLabel(entityLabel); }; const closeModal = () => { @@ -31,7 +42,14 @@ export const ModalProvider: React.FC = ({ children }) => { return ( {children} diff --git a/src/server/api/routers/member.ts b/src/server/api/routers/member.ts index 8e4e49fc..785fc241 100644 --- a/src/server/api/routers/member.ts +++ b/src/server/api/routers/member.ts @@ -131,4 +131,38 @@ export const memberRouter = createTRPCRouter({ return invite; }), + delete: protectedProcedure + .input( + z.object({ + memberPublicId: z.string().min(12), + }), + ) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user?.id; + + if (!userId) + throw new TRPCError({ + message: `User not authenticated`, + code: "UNAUTHORIZED", + }); + + const member = await memberRepo.getByPublicId( + ctx.db, + input.memberPublicId, + ); + + if (!member) + throw new TRPCError({ + message: `Member with public ID ${input.memberPublicId} not found`, + code: "NOT_FOUND", + }); + + const deletedMember = await memberRepo.softDelete(ctx.db, { + memberId: member.id, + deletedAt: new Date().toISOString(), + deletedBy: userId, + }); + + return deletedMember; + }), }); diff --git a/src/server/db/migrations/0004_rainy_archangel.sql b/src/server/db/migrations/0004_rainy_archangel.sql new file mode 100644 index 00000000..eaf9c988 --- /dev/null +++ b/src/server/db/migrations/0004_rainy_archangel.sql @@ -0,0 +1,6 @@ +ALTER TABLE "workspace_members" ADD COLUMN "deletedBy" uuid;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_deletedBy_user_id_fk" FOREIGN KEY ("deletedBy") REFERENCES "user"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/server/db/migrations/meta/0004_snapshot.json b/src/server/db/migrations/meta/0004_snapshot.json new file mode 100644 index 00000000..9d18870b --- /dev/null +++ b/src/server/db/migrations/meta/0004_snapshot.json @@ -0,0 +1,1037 @@ +{ + "version": "5", + "dialect": "pg", + "id": "96331efe-f7dd-4d0f-8f65-e5f706cc2083", + "prevId": "4e3f672e-52ef-4b8c-8628-19f6db49d7c4", + "tables": { + "board": { + "name": "board", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "board_createdBy_user_id_fk": { + "name": "board_createdBy_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_deletedBy_user_id_fk": { + "name": "board_deletedBy_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_importId_import_id_fk": { + "name": "board_importId_import_id_fk", + "tableFrom": "board", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_workspaceId_workspace_id_fk": { + "name": "board_workspaceId_workspace_id_fk", + "tableFrom": "board", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "board_publicId_unique": { + "name": "board_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "_card_workspace_members": { + "name": "_card_workspace_members", + "schema": "", + "columns": { + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "workspaceMemberId": { + "name": "workspaceMemberId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "_card_workspace_members_cardId_card_id_fk": { + "name": "_card_workspace_members_cardId_card_id_fk", + "tableFrom": "_card_workspace_members", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "_card_workspace_members_workspaceMemberId_workspace_members_id_fk": { + "name": "_card_workspace_members_workspaceMemberId_workspace_members_id_fk", + "tableFrom": "_card_workspace_members", + "tableTo": "workspace_members", + "columnsFrom": [ + "workspaceMemberId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "_card_workspace_members_cardId_workspaceMemberId": { + "name": "_card_workspace_members_cardId_workspaceMemberId", + "columns": [ + "cardId", + "workspaceMemberId" + ] + } + }, + "uniqueConstraints": {} + }, + "card": { + "name": "card", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "listId": { + "name": "listId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_createdBy_user_id_fk": { + "name": "card_createdBy_user_id_fk", + "tableFrom": "card", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "card_deletedBy_user_id_fk": { + "name": "card_deletedBy_user_id_fk", + "tableFrom": "card", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "card_listId_list_id_fk": { + "name": "card_listId_list_id_fk", + "tableFrom": "card", + "tableTo": "list", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_importId_import_id_fk": { + "name": "card_importId_import_id_fk", + "tableFrom": "card", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_publicId_unique": { + "name": "card_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "_card_labels": { + "name": "_card_labels", + "schema": "", + "columns": { + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "labelId": { + "name": "labelId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "_card_labels_cardId_card_id_fk": { + "name": "_card_labels_cardId_card_id_fk", + "tableFrom": "_card_labels", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "_card_labels_labelId_label_id_fk": { + "name": "_card_labels_labelId_label_id_fk", + "tableFrom": "_card_labels", + "tableTo": "label", + "columnsFrom": [ + "labelId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "_card_labels_cardId_labelId": { + "name": "_card_labels_cardId_labelId", + "columns": [ + "cardId", + "labelId" + ] + } + }, + "uniqueConstraints": {} + }, + "import": { + "name": "import", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "source", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "import_createdBy_user_id_fk": { + "name": "import_createdBy_user_id_fk", + "tableFrom": "import", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "import_publicId_unique": { + "name": "import_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "label": { + "name": "label", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "colourCode": { + "name": "colourCode", + "type": "varchar(12)", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "label_createdBy_user_id_fk": { + "name": "label_createdBy_user_id_fk", + "tableFrom": "label", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "label_boardId_board_id_fk": { + "name": "label_boardId_board_id_fk", + "tableFrom": "label", + "tableTo": "board", + "columnsFrom": [ + "boardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "label_importId_import_id_fk": { + "name": "label_importId_import_id_fk", + "tableFrom": "label", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "label_publicId_unique": { + "name": "label_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "list": { + "name": "list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "list_createdBy_user_id_fk": { + "name": "list_createdBy_user_id_fk", + "tableFrom": "list", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "list_deletedBy_user_id_fk": { + "name": "list_deletedBy_user_id_fk", + "tableFrom": "list", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "list_boardId_board_id_fk": { + "name": "list_boardId_board_id_fk", + "tableFrom": "list", + "tableTo": "board", + "columnsFrom": [ + "boardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "list_importId_import_id_fk": { + "name": "list_importId_import_id_fk", + "tableFrom": "list", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "list_publicId_unique": { + "name": "list_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "workspace_members": { + "name": "workspace_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "role", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "member_status", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_members_userId_user_id_fk": { + "name": "workspace_members_userId_user_id_fk", + "tableFrom": "workspace_members", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_members_workspaceId_workspace_id_fk": { + "name": "workspace_members_workspaceId_workspace_id_fk", + "tableFrom": "workspace_members", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_members_deletedBy_user_id_fk": { + "name": "workspace_members_deletedBy_user_id_fk", + "tableFrom": "workspace_members", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_members_publicId_unique": { + "name": "workspace_members_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_createdBy_user_id_fk": { + "name": "workspace_createdBy_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_deletedBy_user_id_fk": { + "name": "workspace_deletedBy_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_publicId_unique": { + "name": "workspace_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + }, + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + } + } + }, + "enums": { + "source": { + "name": "source", + "values": { + "trello": "trello" + } + }, + "status": { + "name": "status", + "values": { + "started": "started", + "success": "success", + "failed": "failed" + } + }, + "role": { + "name": "role", + "values": { + "admin": "admin", + "member": "member", + "guest": "guest" + } + }, + "member_status": { + "name": "member_status", + "values": { + "invited": "invited", + "active": "active", + "removed": "removed" + } + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/src/server/db/migrations/meta/_journal.json b/src/server/db/migrations/meta/_journal.json index 5bcd3a2e..59e8e124 100644 --- a/src/server/db/migrations/meta/_journal.json +++ b/src/server/db/migrations/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1728246215706, "tag": "0003_naive_secret_warriors", "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1730205607613, + "tag": "0004_rainy_archangel", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/server/db/repository/board.repo.ts b/src/server/db/repository/board.repo.ts index 7e17864c..6bcfb85d 100644 --- a/src/server/db/repository/board.repo.ts +++ b/src/server/db/repository/board.repo.ts @@ -33,7 +33,7 @@ export const getByPublicId = async ( publicId, members:workspace_members ( publicId, - user ( + user!workspace_members_userId_user_id_fk ( name ) ) @@ -61,7 +61,7 @@ export const getByPublicId = async ( ), members:workspace_members${filters.members.length > 0 ? "!inner" : ""} ( publicId, - user ( + user!workspace_members_userId_user_id_fk ( name ) ) @@ -72,7 +72,9 @@ export const getByPublicId = async ( .eq("publicId", boardPublicId) .is("deletedAt", null) .is("lists.deletedAt", null) - .is("lists.cards.deletedAt", null); + .is("lists.cards.deletedAt", null) + .is("workspace.members.deletedAt", null) + .is("lists.cards.members.deletedAt", null); if (filters.labels.length > 0) { query = query.in("lists.cards.labels.publicId", filters.labels); diff --git a/src/server/db/repository/card.repo.ts b/src/server/db/repository/card.repo.ts index 2e156cfe..07d1588e 100644 --- a/src/server/db/repository/card.repo.ts +++ b/src/server/db/repository/card.repo.ts @@ -209,7 +209,7 @@ export const getWithListAndMembersByPublicId = async ( publicId, members:workspace_members ( publicId, - user ( + user!workspace_members_userId_user_id_fk ( id, name ) @@ -219,7 +219,7 @@ export const getWithListAndMembersByPublicId = async ( ), members:workspace_members ( publicId, - user ( + user!workspace_members_userId_user_id_fk ( id, name ) @@ -229,6 +229,8 @@ export const getWithListAndMembersByPublicId = async ( .eq("publicId", cardPublicId) .is("deletedAt", null) .is("list.board.lists.deletedAt", null) + .is("list.board.workspace.members.deletedAt", null) + .is("members.deletedAt", null) .limit(1) .single(); diff --git a/src/server/db/repository/member.repo.ts b/src/server/db/repository/member.repo.ts index 999d5c76..e4a00bed 100644 --- a/src/server/db/repository/member.repo.ts +++ b/src/server/db/repository/member.repo.ts @@ -54,3 +54,20 @@ export const acceptInvite = async ( return data; }; + +export const softDelete = async ( + db: SupabaseClient, + args: { + memberId: number; + deletedAt: string; + deletedBy: string; + }, +) => { + const result = await db + .from("workspace_members") + .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) + .eq("id", args.memberId) + .is("deletedAt", null); + + return result; +}; diff --git a/src/server/db/repository/workspace.repo.ts b/src/server/db/repository/workspace.repo.ts index 8f083b07..54384914 100644 --- a/src/server/db/repository/workspace.repo.ts +++ b/src/server/db/repository/workspace.repo.ts @@ -81,7 +81,7 @@ export const getByPublicIdWithMembers = async ( publicId, role, status, - user ( + user!workspace_members_userId_user_id_fk ( id, name, email diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 6331cec6..305f606b 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -298,6 +298,7 @@ export const workspaceMembers = pgTable("workspace_members", { createdAt: timestamp("createdAt").defaultNow().notNull(), updatedAt: timestamp("updatedAt"), deletedAt: timestamp("deletedAt"), + deletedBy: uuid("deletedBy").references(() => users.id), role: memberRoleEnum("role").notNull(), status: memberStatusEnum("status").default("invited").notNull(), }); @@ -309,6 +310,10 @@ export const usersToWorkspacesRelations = relations( fields: [workspaceMembers.createdBy], references: [users.id], }), + deletedBy: one(users, { + fields: [workspaceMembers.deletedBy], + references: [users.id], + }), user: one(users, { fields: [workspaceMembers.userId], references: [users.id], diff --git a/src/utils/supabase/api.ts b/src/utils/supabase/api.ts index 5b784103..8654dcb7 100644 --- a/src/utils/supabase/api.ts +++ b/src/utils/supabase/api.ts @@ -6,7 +6,7 @@ import { import { RequestCookies } from "@edge-runtime/cookies"; import { type Database } from "~/types/database.types"; -import { type NextRequest, NextResponse } from "next/server"; +import { type NextRequest, type NextResponse } from "next/server"; export function createNextClient(req: NextRequest, res: NextResponse) { const supabase = createServerClient( diff --git a/src/views/members/components/DeleteMemberConfirmation.tsx b/src/views/members/components/DeleteMemberConfirmation.tsx new file mode 100644 index 00000000..f1cb7d57 --- /dev/null +++ b/src/views/members/components/DeleteMemberConfirmation.tsx @@ -0,0 +1,57 @@ +import { api } from "~/utils/api"; +import { useModal } from "~/providers/modal"; +import { usePopup } from "~/providers/popup"; + +import Button from "~/components/Button"; + +export function DeleteMemberConfirmation() { + const utils = api.useUtils(); + const { closeModal, entityLabel, entityId } = useModal(); + const { showPopup } = usePopup(); + + const deleteMember = api.member.delete.useMutation({ + onSuccess: async () => { + closeModal(); + try { + await utils.workspace.byId.refetch(); + } catch (e) { + console.error(e); + } + }, + onError: () => { + showPopup({ + header: "Unable to remove member", + message: "Please try again later, or contact customer support.", + }); + closeModal(); + }, + }); + + const handleDeleteMember = () => { + if (entityId) + deleteMember.mutate({ + memberPublicId: entityId, + }); + }; + + return ( +
+
+

+ {`Are you sure want to remove ${entityLabel}?`} +

+

+ {"They won't be able to access this workspace."} +

+
+
+ + +
+
+ ); +} diff --git a/src/views/members/index.tsx b/src/views/members/index.tsx index 8704f877..5d87149b 100644 --- a/src/views/members/index.tsx +++ b/src/views/members/index.tsx @@ -1,10 +1,13 @@ -import { HiOutlinePlusSmall } from "react-icons/hi2"; +import { HiOutlinePlusSmall, HiEllipsisHorizontal } from "react-icons/hi2"; import { useModal } from "~/providers/modal"; import { useWorkspace } from "~/providers/workspace"; import Modal from "~/components/modal"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { InviteMemberForm } from "./components/InviteMemberForm"; +import { DeleteMemberConfirmation } from "./components/DeleteMemberConfirmation"; +import Dropdown from "~/components/Dropdown"; + import { api } from "~/utils/api"; import { getInitialsFromName, inferInitialsFromEmail } from "~/utils/helpers"; @@ -41,71 +44,107 @@ export default function MembersPage() {
-
+
-
+
- - + + - {data?.members.map((member) => { + {data?.members.map((member, index) => { const initials = member.user?.name ? getInitialsFromName(member.user.name) : inferInitialsFromEmail(member.user?.email ?? ""); return ( - - + - - + + + + ); })} @@ -118,6 +157,7 @@ export default function MembersPage() { {modalContentType === "NEW_WORKSPACE" && } {modalContentType === "INVITE_MEMBER" && } + {modalContentType === "REMOVE_MEMBER" && } );
User Role
-
-
- - - {initials} + <> +
+
+
+ + + {initials} + - -
-
-
-
-

- {member.user?.name} +

+
+
+
+

+ {member.user?.name} +

+
+

+ {member.user?.email}

-

- {member.user?.email} -

-
-
-
- - {member.role.charAt(0).toUpperCase() + - member.role.slice(1)} - - {member.status === "invited" && ( - - Pending - - )} -
-
+
+
+ + {member.role.charAt(0).toUpperCase() + + member.role.slice(1)} + + {member.status === "invited" && ( + + Pending + + )} +
+
+ + openModal( + "REMOVE_MEMBER", + member.publicId, + member.user?.email ?? "", + ), + }, + ]} + > + + +
+
+