feat: remove members
This commit is contained in:
44
src/components/Dropdown.tsx
Normal file
44
src/components/Dropdown.tsx
Normal file
@@ -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 (
|
||||||
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
|
<div>
|
||||||
|
<Menu.Button className="flex h-8 w-8 items-center justify-center rounded-[5px] hover:bg-light-200 dark:hover:bg-dark-200">
|
||||||
|
{children}
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="absolute right-0 z-30 mt-2 w-56 origin-top-right rounded-md border border-light-200 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
|
||||||
|
<div className="flex">
|
||||||
|
{items.map((item) => (
|
||||||
|
<Menu.Item key={item.label}>
|
||||||
|
<button
|
||||||
|
onClick={item.action}
|
||||||
|
className="m-1 w-full rounded-[5px] px-3 py-2 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { WorkspaceProvider } from "~/providers/workspace";
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
import Dashboard from "~/components/dashboard";
|
import Dashboard from "~/components/dashboard";
|
||||||
|
import Popup from "~/components/Popup";
|
||||||
import MembersView from "~/views/members";
|
import MembersView from "~/views/members";
|
||||||
|
|
||||||
export default function MembersPage() {
|
export default function MembersPage() {
|
||||||
@@ -8,6 +9,7 @@ export default function MembersPage() {
|
|||||||
<Dashboard>
|
<Dashboard>
|
||||||
<MembersView />
|
<MembersView />
|
||||||
</Dashboard>
|
</Dashboard>
|
||||||
|
<Popup />
|
||||||
</WorkspaceProvider>
|
</WorkspaceProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import { createContext, useContext, useState } from "react";
|
|||||||
|
|
||||||
type ModalContextType = {
|
type ModalContextType = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
openModal: (contentType: string, entityId?: string) => void;
|
openModal: (
|
||||||
|
contentType: string,
|
||||||
|
entityId?: string,
|
||||||
|
entityLabel?: string,
|
||||||
|
) => void;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
modalContentType: string;
|
modalContentType: string;
|
||||||
entityId: string;
|
entityId: string;
|
||||||
|
entityLabel: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -17,12 +22,18 @@ const ModalContext = createContext<ModalContextType | undefined>(undefined);
|
|||||||
export const ModalProvider: React.FC<Props> = ({ children }) => {
|
export const ModalProvider: React.FC<Props> = ({ children }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [entityId, setEntityId] = useState("");
|
const [entityId, setEntityId] = useState("");
|
||||||
|
const [entityLabel, setEntityLabel] = useState("");
|
||||||
const [modalContentType, setModalContentType] = useState("");
|
const [modalContentType, setModalContentType] = useState("");
|
||||||
|
|
||||||
const openModal = (contentType: string, entityId?: string) => {
|
const openModal = (
|
||||||
|
contentType: string,
|
||||||
|
entityId?: string,
|
||||||
|
entityLabel?: string,
|
||||||
|
) => {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
setModalContentType(contentType);
|
setModalContentType(contentType);
|
||||||
if (entityId) setEntityId(entityId);
|
if (entityId) setEntityId(entityId);
|
||||||
|
if (entityLabel) setEntityLabel(entityLabel);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
@@ -31,7 +42,14 @@ export const ModalProvider: React.FC<Props> = ({ children }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContext.Provider
|
<ModalContext.Provider
|
||||||
value={{ isOpen, openModal, closeModal, modalContentType, entityId }}
|
value={{
|
||||||
|
isOpen,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
modalContentType,
|
||||||
|
entityId,
|
||||||
|
entityLabel,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ModalContext.Provider>
|
</ModalContext.Provider>
|
||||||
|
|||||||
@@ -131,4 +131,38 @@ export const memberRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return invite;
|
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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
6
src/server/db/migrations/0004_rainy_archangel.sql
Normal file
6
src/server/db/migrations/0004_rainy_archangel.sql
Normal file
@@ -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 $$;
|
||||||
1037
src/server/db/migrations/meta/0004_snapshot.json
Normal file
1037
src/server/db/migrations/meta/0004_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,13 @@
|
|||||||
"when": 1728246215706,
|
"when": 1728246215706,
|
||||||
"tag": "0003_naive_secret_warriors",
|
"tag": "0003_naive_secret_warriors",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1730205607613,
|
||||||
|
"tag": "0004_rainy_archangel",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ export const getByPublicId = async (
|
|||||||
publicId,
|
publicId,
|
||||||
members:workspace_members (
|
members:workspace_members (
|
||||||
publicId,
|
publicId,
|
||||||
user (
|
user!workspace_members_userId_user_id_fk (
|
||||||
name
|
name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -61,7 +61,7 @@ export const getByPublicId = async (
|
|||||||
),
|
),
|
||||||
members:workspace_members${filters.members.length > 0 ? "!inner" : ""} (
|
members:workspace_members${filters.members.length > 0 ? "!inner" : ""} (
|
||||||
publicId,
|
publicId,
|
||||||
user (
|
user!workspace_members_userId_user_id_fk (
|
||||||
name
|
name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -72,7 +72,9 @@ export const getByPublicId = async (
|
|||||||
.eq("publicId", boardPublicId)
|
.eq("publicId", boardPublicId)
|
||||||
.is("deletedAt", null)
|
.is("deletedAt", null)
|
||||||
.is("lists.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) {
|
if (filters.labels.length > 0) {
|
||||||
query = query.in("lists.cards.labels.publicId", filters.labels);
|
query = query.in("lists.cards.labels.publicId", filters.labels);
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export const getWithListAndMembersByPublicId = async (
|
|||||||
publicId,
|
publicId,
|
||||||
members:workspace_members (
|
members:workspace_members (
|
||||||
publicId,
|
publicId,
|
||||||
user (
|
user!workspace_members_userId_user_id_fk (
|
||||||
id,
|
id,
|
||||||
name
|
name
|
||||||
)
|
)
|
||||||
@@ -219,7 +219,7 @@ export const getWithListAndMembersByPublicId = async (
|
|||||||
),
|
),
|
||||||
members:workspace_members (
|
members:workspace_members (
|
||||||
publicId,
|
publicId,
|
||||||
user (
|
user!workspace_members_userId_user_id_fk (
|
||||||
id,
|
id,
|
||||||
name
|
name
|
||||||
)
|
)
|
||||||
@@ -229,6 +229,8 @@ export const getWithListAndMembersByPublicId = async (
|
|||||||
.eq("publicId", cardPublicId)
|
.eq("publicId", cardPublicId)
|
||||||
.is("deletedAt", null)
|
.is("deletedAt", null)
|
||||||
.is("list.board.lists.deletedAt", null)
|
.is("list.board.lists.deletedAt", null)
|
||||||
|
.is("list.board.workspace.members.deletedAt", null)
|
||||||
|
.is("members.deletedAt", null)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
|||||||
@@ -54,3 +54,20 @@ export const acceptInvite = async (
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const softDelete = async (
|
||||||
|
db: SupabaseClient<Database>,
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const getByPublicIdWithMembers = async (
|
|||||||
publicId,
|
publicId,
|
||||||
role,
|
role,
|
||||||
status,
|
status,
|
||||||
user (
|
user!workspace_members_userId_user_id_fk (
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
email
|
email
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ export const workspaceMembers = pgTable("workspace_members", {
|
|||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updatedAt"),
|
updatedAt: timestamp("updatedAt"),
|
||||||
deletedAt: timestamp("deletedAt"),
|
deletedAt: timestamp("deletedAt"),
|
||||||
|
deletedBy: uuid("deletedBy").references(() => users.id),
|
||||||
role: memberRoleEnum("role").notNull(),
|
role: memberRoleEnum("role").notNull(),
|
||||||
status: memberStatusEnum("status").default("invited").notNull(),
|
status: memberStatusEnum("status").default("invited").notNull(),
|
||||||
});
|
});
|
||||||
@@ -309,6 +310,10 @@ export const usersToWorkspacesRelations = relations(
|
|||||||
fields: [workspaceMembers.createdBy],
|
fields: [workspaceMembers.createdBy],
|
||||||
references: [users.id],
|
references: [users.id],
|
||||||
}),
|
}),
|
||||||
|
deletedBy: one(users, {
|
||||||
|
fields: [workspaceMembers.deletedBy],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
user: one(users, {
|
user: one(users, {
|
||||||
fields: [workspaceMembers.userId],
|
fields: [workspaceMembers.userId],
|
||||||
references: [users.id],
|
references: [users.id],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import { RequestCookies } from "@edge-runtime/cookies";
|
import { RequestCookies } from "@edge-runtime/cookies";
|
||||||
import { type Database } from "~/types/database.types";
|
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) {
|
export function createNextClient(req: NextRequest, res: NextResponse) {
|
||||||
const supabase = createServerClient<Database>(
|
const supabase = createServerClient<Database>(
|
||||||
|
|||||||
57
src/views/members/components/DeleteMemberConfirmation.tsx
Normal file
57
src/views/members/components/DeleteMemberConfirmation.tsx
Normal file
@@ -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 (
|
||||||
|
<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 want to remove ${entityLabel}?`}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
||||||
|
{"They won't be able to access this workspace."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
||||||
|
<Button onClick={() => closeModal()} variant="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleDeleteMember} isLoading={deleteMember.isPending}>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { HiOutlinePlusSmall } from "react-icons/hi2";
|
import { HiOutlinePlusSmall, HiEllipsisHorizontal } from "react-icons/hi2";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useWorkspace } from "~/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
import Modal from "~/components/modal";
|
import Modal from "~/components/modal";
|
||||||
|
|
||||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
import { InviteMemberForm } from "./components/InviteMemberForm";
|
import { InviteMemberForm } from "./components/InviteMemberForm";
|
||||||
|
import { DeleteMemberConfirmation } from "./components/DeleteMemberConfirmation";
|
||||||
|
import Dropdown from "~/components/Dropdown";
|
||||||
|
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { getInitialsFromName, inferInitialsFromEmail } from "~/utils/helpers";
|
import { getInitialsFromName, inferInitialsFromEmail } from "~/utils/helpers";
|
||||||
|
|
||||||
@@ -41,71 +44,107 @@ export default function MembersPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flow-root">
|
<div className="mt-8 flow-root">
|
||||||
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
<div className="-mx-4 -my-2 overflow-x-visible 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="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 className="h-full shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
|
||||||
<table className="min-w-full divide-y divide-light-600 dark:divide-dark-600">
|
<table className="min-w-full divide-y divide-light-600 dark:divide-dark-600">
|
||||||
<thead className="bg-light-300 dark:bg-dark-200">
|
<thead className="rounded-t-lg bg-light-300 dark:bg-dark-200">
|
||||||
<tr>
|
<tr className="">
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-light-900 dark:text-dark-900 sm:pl-6"
|
className="rounded-tl-lg py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-light-900 dark:text-dark-900 sm:pl-6"
|
||||||
>
|
>
|
||||||
User
|
User
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-light-900 dark:text-dark-900"
|
className="rounded-tr-lg px-3 py-3.5 text-left text-sm font-semibold text-light-900 dark:text-dark-900"
|
||||||
>
|
>
|
||||||
Role
|
Role
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-light-600 bg-light-50 dark:divide-dark-600 dark:bg-dark-100">
|
<tbody className="divide-y divide-light-600 bg-light-50 dark:divide-dark-600 dark:bg-dark-100">
|
||||||
{data?.members.map((member) => {
|
{data?.members.map((member, index) => {
|
||||||
const initials = member.user?.name
|
const initials = member.user?.name
|
||||||
? getInitialsFromName(member.user.name)
|
? getInitialsFromName(member.user.name)
|
||||||
: inferInitialsFromEmail(member.user?.email ?? "");
|
: inferInitialsFromEmail(member.user?.email ?? "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={member.publicId}>
|
<>
|
||||||
<td>
|
<tr key={member.publicId} className="rounded-b-lg">
|
||||||
<div className="flex items-center p-4">
|
<td
|
||||||
<div className="flex-shrink-0">
|
className={
|
||||||
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-light-1000 dark:bg-dark-400">
|
index === data.members.length - 1
|
||||||
<span className="text-sm font-medium leading-none text-white">
|
? "rounded-bl-lg"
|
||||||
{initials}
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex items-center p-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-light-1000 dark:bg-dark-400">
|
||||||
|
<span className="text-sm font-medium leading-none text-white">
|
||||||
|
{initials}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
<div className="ml-2 min-w-0 flex-1">
|
||||||
<div className="ml-2 min-w-0 flex-1">
|
<div>
|
||||||
<div>
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<p className="mr-2 text-sm font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
<p className="mr-2 text-sm font-medium text-neutral-900 dark:text-dark-1000">
|
{member.user?.name}
|
||||||
{member.user?.name}
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="truncate text-sm text-dark-900">
|
||||||
|
{member.user?.email}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="truncate text-sm text-dark-900">
|
|
||||||
{member.user?.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
<td
|
||||||
<td>
|
className={
|
||||||
<div className="px-3">
|
index === data.members.length - 1
|
||||||
<span className="inline-flex items-center rounded-md bg-emerald-500/10 px-1.5 py-0.5 text-[11px] font-medium text-emerald-400 ring-1 ring-inset ring-emerald-500/20">
|
? "rounded-br-lg"
|
||||||
{member.role.charAt(0).toUpperCase() +
|
: ""
|
||||||
member.role.slice(1)}
|
}
|
||||||
</span>
|
>
|
||||||
{member.status === "invited" && (
|
<div className="flex items-center justify-between px-3">
|
||||||
<span className="ml-2 inline-flex items-center rounded-md bg-gray-500/10 px-1.5 py-0.5 text-[11px] font-medium text-gray-400 ring-1 ring-inset ring-gray-500/20">
|
<div>
|
||||||
Pending
|
<span className="inline-flex items-center rounded-md bg-emerald-500/10 px-1.5 py-0.5 text-[11px] font-medium text-emerald-400 ring-1 ring-inset ring-emerald-500/20">
|
||||||
</span>
|
{member.role.charAt(0).toUpperCase() +
|
||||||
)}
|
member.role.slice(1)}
|
||||||
</div>
|
</span>
|
||||||
</td>
|
{member.status === "invited" && (
|
||||||
</tr>
|
<span className="ml-2 inline-flex items-center rounded-md bg-gray-500/10 px-1.5 py-0.5 text-[11px] font-medium text-gray-400 ring-1 ring-inset ring-gray-500/20">
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Dropdown
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: "Remove member",
|
||||||
|
action: () =>
|
||||||
|
openModal(
|
||||||
|
"REMOVE_MEMBER",
|
||||||
|
member.publicId,
|
||||||
|
member.user?.email ?? "",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<HiEllipsisHorizontal
|
||||||
|
size={25}
|
||||||
|
className="text-light-900 dark:text-dark-900"
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -118,6 +157,7 @@ export default function MembersPage() {
|
|||||||
<Modal>
|
<Modal>
|
||||||
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
|
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
|
||||||
{modalContentType === "INVITE_MEMBER" && <InviteMemberForm />}
|
{modalContentType === "INVITE_MEMBER" && <InviteMemberForm />}
|
||||||
|
{modalContentType === "REMOVE_MEMBER" && <DeleteMemberConfirmation />}
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user