diff --git a/packages/web/package.json b/packages/web/package.json index 63b50366..7b6878b7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -49,6 +49,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tabs": "^1.1.2", diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 03739561..6b2be718 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -34,7 +34,7 @@ export const withAuth = async (fn: (session: Session) => Promise) => { return fn(session); } -export const withOrgMembership = async (session: Session, domain: string, fn: (orgId: number) => Promise) => { +export const withOrgMembership = async (session: Session, domain: string, fn: (params: { orgId: number, userRole: OrgRole }) => Promise, minRequiredRole: OrgRole = OrgRole.MEMBER) => { const org = await prisma.org.findUnique({ where: { domain, @@ -58,38 +58,28 @@ export const withOrgMembership = async (session: Session, domain: string, fn: return notFound(); } - return fn(org.id); -} - -export const withOwner = async (session: Session, domain: string, fn: (orgId: number) => Promise) => { - const org = await prisma.org.findUnique({ - where: { - domain, - }, - }); - - if (!org) { - return notFound(); + const getAuthorizationPrecendence = (role: OrgRole): number => { + switch (role) { + case OrgRole.MEMBER: + return 0; + case OrgRole.OWNER: + return 1; + } } - const userRole = await prisma.userToOrg.findUnique({ - where: { - orgId_userId: { - orgId: org.id, - userId: session.user.id, - }, - }, - }); - if (!userRole || userRole.role !== OrgRole.OWNER) { + if (getAuthorizationPrecendence(membership.role) < getAuthorizationPrecendence(minRequiredRole)) { return { statusCode: StatusCodes.FORBIDDEN, - errorCode: ErrorCode.MEMBER_NOT_OWNER, - message: "Only org owners can perform this action", + errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS, + message: "You do not have sufficient permissions to perform this action.", } satisfies ServiceError; } - - return fn(org.id); + + return fn({ + orgId: org.id, + userRole: membership.role, + }); } export const isAuthed = async () => { @@ -126,7 +116,7 @@ export const createOrg = (name: string, domain: string, stripeCustomerId?: strin export const getSecrets = (domain: string): Promise<{ createdAt: Date; key: string; }[] | ServiceError> => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const secrets = await prisma.secret.findMany({ where: { orgId, @@ -146,7 +136,7 @@ export const getSecrets = (domain: string): Promise<{ createdAt: Date; key: stri export const createSecret = async (key: string, value: string, domain: string): Promise<{ success: boolean } | ServiceError> => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { try { const encrypted = encrypt(value); await prisma.secret.create({ @@ -168,7 +158,7 @@ export const createSecret = async (key: string, value: string, domain: string): export const deleteSecret = async (key: string, domain: string): Promise<{ success: boolean } | ServiceError> => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { await prisma.secret.delete({ where: { orgId_key: { @@ -195,7 +185,7 @@ export const getConnections = async (domain: string): Promise< }[] | ServiceError > => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const connections = await prisma.connection.findMany({ where: { orgId, @@ -216,7 +206,7 @@ export const getConnections = async (domain: string): Promise< export const createConnection = async (name: string, type: string, connectionConfig: string, domain: string): Promise<{ id: number } | ServiceError> => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const parsedConfig = parseConnectionConfig(type, connectionConfig); if (isServiceError(parsedConfig)) { return parsedConfig; @@ -238,7 +228,7 @@ export const createConnection = async (name: string, type: string, connectionCon export const getConnectionInfoAction = async (connectionId: number, domain: string): Promise<{ connection: Connection, linkedRepos: Repo[] } | ServiceError> => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const connection = await getConnection(connectionId, orgId); if (!connection) { return notFound(); @@ -255,7 +245,7 @@ export const getConnectionInfoAction = async (connectionId: number, domain: stri export const getOrgFromDomainAction = async (domain: string): Promise => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const org = await prisma.org.findUnique({ where: { id: orgId, @@ -273,7 +263,7 @@ export const getOrgFromDomainAction = async (domain: string): Promise => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const connection = await getConnection(connectionId, orgId); if (!connection) { return notFound(); @@ -296,7 +286,7 @@ export const updateConnectionDisplayName = async (connectionId: number, name: st export const updateConnectionConfigAndScheduleSync = async (connectionId: number, config: string, domain: string): Promise<{ success: boolean } | ServiceError> => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const connection = await getConnection(connectionId, orgId); if (!connection) { return notFound(); @@ -335,7 +325,7 @@ export const updateConnectionConfigAndScheduleSync = async (connectionId: number export const flagConnectionForSync = async (connectionId: number, domain: string): Promise<{ success: boolean } | ServiceError> => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const connection = await getConnection(connectionId, orgId); if (!connection || connection.orgId !== orgId) { return notFound(); @@ -365,7 +355,7 @@ export const flagConnectionForSync = async (connectionId: number, domain: string export const deleteConnection = async (connectionId: number, domain: string): Promise<{ success: boolean } | ServiceError> => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const connection = await getConnection(connectionId, orgId); if (!connection) { return notFound(); @@ -385,57 +375,98 @@ export const deleteConnection = async (connectionId: number, domain: string): Pr export const getCurrentUserRole = async (domain: string): Promise => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { - const userRole = await prisma.userToOrg.findUnique({ + withOrgMembership(session, domain, async ({ userRole }) => { + return userRole; + }) + ); + +export const createInvites = async (emails: string[], domain: string): Promise<{ success: boolean } | ServiceError> => + withAuth((session) => + withOrgMembership(session, domain, async ({ orgId }) => { + // Check for existing invites + const existingInvites = await prisma.invite.findMany({ where: { - orgId_userId: { - orgId, - userId: session.user.id, + recipientEmail: { + in: emails }, - }, + orgId, + } }); - if (!userRole) { - return notFound(); + if (existingInvites.length > 0) { + return { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.INVALID_INVITE, + message: `A pending invite already exists for one or more of the provided emails.`, + } satisfies ServiceError; } - return userRole.role; - }) - ); - -export const createInvite = async (email: string, userId: string, domain: string): Promise<{ success: boolean } | ServiceError> => - withAuth((session) => - withOwner(session, domain, async (orgId) => { - console.log("Creating invite for", email, userId, orgId); + // Check for members that are already in the org + const existingMembers = await prisma.userToOrg.findMany({ + where: { + user: { + email: { + in: emails, + } + }, + orgId, + }, + }); - if (email === session.user.email) { - console.error("User tried to invite themselves"); + if (existingMembers.length > 0) { return { statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.SELF_INVITE, - message: "❌ You can't invite yourself to an org", + errorCode: ErrorCode.INVALID_INVITE, + message: `One or more of the provided emails are already members of this org.`, } satisfies ServiceError; } - try { - await prisma.invite.create({ - data: { - recipientEmail: email, - hostUserId: userId, - orgId, - } - }); - } catch (error) { - console.error("Failed to create invite:", error); - return unexpectedError("Failed to create invite"); + await prisma.$transaction(async (tx) => { + for (const email of emails) { + await tx.invite.create({ + data: { + recipientEmail: email, + hostUserId: session.user.id, + orgId, + } + }); + } + }); + + + return { + success: true, + } + }, /* minRequiredRole = */ OrgRole.OWNER) + ); + +export const cancelInvite = async (inviteId: string, domain: string): Promise<{ success: boolean } | ServiceError> => + withAuth((session) => + withOrgMembership(session, domain, async ({ orgId }) => { + const invite = await prisma.invite.findUnique({ + where: { + id: inviteId, + orgId, + }, + }); + + if (!invite) { + return notFound(); } + await prisma.invite.delete({ + where: { + id: inviteId, + }, + }); + return { success: true, } - }) + }, /* minRequiredRole = */ OrgRole.OWNER) ); + export const redeemInvite = async (invite: Invite, userId: string): Promise<{ success: boolean } | ServiceError> => withAuth(async () => { try { @@ -498,9 +529,9 @@ export const redeemInvite = async (invite: Invite, userId: string): Promise<{ su } }); -export const makeOwner = async (newOwnerId: string, domain: string): Promise<{ success: boolean } | ServiceError> => +export const transferOwnership = async (newOwnerId: string, domain: string): Promise<{ success: boolean } | ServiceError> => withAuth((session) => - withOwner(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const currentUserId = session.user.id; if (newOwnerId === currentUserId) { @@ -556,7 +587,7 @@ export const makeOwner = async (newOwnerId: string, domain: string): Promise<{ s return { success: true, } - }) + }, /* minRequiredRole = */ OrgRole.OWNER) ); const parseConnectionConfig = (connectionType: string, config: string) => { @@ -702,7 +733,7 @@ export const setupInitialStripeCustomer = async (name: string, domain: string) = export const getSubscriptionCheckoutRedirect = async (domain: string) => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const org = await prisma.org.findUnique({ where: { id: orgId, @@ -762,7 +793,7 @@ export async function fetchStripeSession(sessionId: string) { export const getCustomerPortalSessionLink = async (domain: string): Promise => withAuth((session) => - withOwner(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const org = await prisma.org.findUnique({ where: { id: orgId, @@ -781,7 +812,8 @@ export const getCustomerPortalSessionLink = async (domain: string): Promise => withAuth(async () => { @@ -808,7 +840,7 @@ export const fetchSubscription = (domain: string): Promise => withAuth(async (session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const org = await prisma.org.findUnique({ where: { id: orgId, @@ -830,24 +862,7 @@ export const getSubscriptionBillingEmail = async (domain: string): Promise => withAuth((session) => - withOrgMembership(session, domain, async (orgId) => { - const userRole = await prisma.userToOrg.findUnique({ - where: { - orgId_userId: { - orgId, - userId: session.user.id, - } - } - }); - - if (!userRole || userRole.role !== "OWNER") { - return { - statusCode: StatusCodes.FORBIDDEN, - errorCode: ErrorCode.MEMBER_NOT_OWNER, - message: "Only org owners can change billing email", - } satisfies ServiceError; - } - + withOrgMembership(session, domain, async ({ orgId }) => { const org = await prisma.org.findUnique({ where: { id: orgId, @@ -866,7 +881,7 @@ export const changeSubscriptionBillingEmail = async (domain: string, newEmail: s return { success: true, } - }) + }, /* minRequiredRole = */ OrgRole.OWNER) ); export const checkIfUserHasOrg = async (userId: string): Promise => { @@ -890,9 +905,9 @@ export const checkIfOrgDomainExists = async (domain: string): Promise => +export const removeMemberFromOrg = async (memberId: string, domain: string): Promise<{ success: boolean } | ServiceError> => withAuth(async (session) => - withOrgMembership(session, domain, async (orgId) => { + withOrgMembership(session, domain, async ({ orgId }) => { const targetMember = await prisma.userToOrg.findUnique({ where: { orgId_userId: { @@ -944,6 +959,61 @@ export const removeMember = async (memberId: string, domain: string): Promise<{ } }); + return { + success: true, + } + }, /* minRequiredRole = */ OrgRole.OWNER) + ); + +export const leaveOrg = async (domain: string): Promise<{ success: boolean } | ServiceError> => + withAuth(async (session) => + withOrgMembership(session, domain, async ({ orgId, userRole }) => { + if (userRole === OrgRole.OWNER) { + return { + statusCode: StatusCodes.FORBIDDEN, + errorCode: ErrorCode.OWNER_CANNOT_LEAVE_ORG, + message: "Organization owners cannot leave their own organization", + } satisfies ServiceError; + } + + const org = await prisma.org.findUnique({ + where: { + id: orgId, + }, + }); + + if (!org) { + return notFound(); + } + + if (org.stripeCustomerId) { + const subscription = await fetchSubscription(domain); + if (isServiceError(subscription)) { + return orgInvalidSubscription(); + } + + const existingSeatCount = subscription.items.data[0].quantity; + const newSeatCount = (existingSeatCount || 1) - 1; + + const stripe = getStripe(); + await stripe.subscriptionItems.update( + subscription.items.data[0].id, + { + quantity: newSeatCount, + proration_behavior: 'create_prorations', + } + ) + } + + await prisma.userToOrg.delete({ + where: { + orgId_userId: { + orgId, + userId: session.user.id, + } + } + }); + return { success: true, } @@ -967,3 +1037,44 @@ export const getSubscriptionData = async (domain: string) => } }) ); + +export const getOrgMembers = async (domain: string) => + withAuth(async (session) => + withOrgMembership(session, domain, async ({ orgId }) => { + const members = await prisma.userToOrg.findMany({ + where: { + orgId, + }, + include: { + user: true, + }, + }); + + return members.map((member) => ({ + id: member.userId, + email: member.user.email!, + name: member.user.name ?? undefined, + avatarUrl: member.user.image ?? undefined, + role: member.role, + joinedAt: member.joinedAt, + })); + }) + ); + + +export const getOrgInvites = async (domain: string) => + withAuth(async (session) => + withOrgMembership(session, domain, async ({ orgId }) => { + const invites = await prisma.invite.findMany({ + where: { + orgId, + }, + }); + + return invites.map((invite) => ({ + id: invite.id, + email: invite.recipientEmail, + createdAt: invite.createdAt, + })); + }) + ); diff --git a/packages/web/src/app/[domain]/secrets/secretsTable.tsx b/packages/web/src/app/[domain]/secrets/secretsTable.tsx index 0bd96b38..097d9922 100644 --- a/packages/web/src/app/[domain]/secrets/secretsTable.tsx +++ b/packages/web/src/app/[domain]/secrets/secretsTable.tsx @@ -17,7 +17,7 @@ import { useDomain } from "@/hooks/useDomain"; const formSchema = z.object({ key: z.string().min(2).max(40), - value: z.string().min(2).max(40), + value: z.string().min(2), }); interface SecretsTableProps { @@ -30,18 +30,15 @@ export const SecretsTable = ({ initialSecrets }: SecretsTableProps) => { const { toast } = useToast(); const domain = useDomain(); - const fetchSecretKeys = async () => { - const keys = await getSecrets(domain); - if ('keys' in keys) { - setSecrets(keys); - } else { - console.error(keys); - } - }; - useEffect(() => { - fetchSecretKeys(); - }, [fetchSecretKeys]); + getSecrets(domain).then((keys) => { + if ('keys' in keys) { + setSecrets(keys); + } else { + console.error(keys); + } + }) + }, []); const form = useForm>({ resolver: zodResolver(formSchema), diff --git a/packages/web/src/app/[domain]/settings/(general)/page.tsx b/packages/web/src/app/[domain]/settings/(general)/page.tsx new file mode 100644 index 00000000..0f643c2b --- /dev/null +++ b/packages/web/src/app/[domain]/settings/(general)/page.tsx @@ -0,0 +1,7 @@ + +export default async function GeneralSettingsPage() { + return ( +

todo

+ ) +} + diff --git a/packages/web/src/app/[domain]/settings/components/header.tsx b/packages/web/src/app/[domain]/settings/components/header.tsx new file mode 100644 index 00000000..79a24ee4 --- /dev/null +++ b/packages/web/src/app/[domain]/settings/components/header.tsx @@ -0,0 +1,22 @@ +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import clsx from "clsx"; + +interface HeaderProps { + children: React.ReactNode; + withTopMargin?: boolean; + className?: string; +} + +export const Header = ({ + children, + withTopMargin = true, + className, +}: HeaderProps) => { + return ( +
+ {children} + +
+ ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/settings/components/inviteTable.tsx b/packages/web/src/app/[domain]/settings/components/inviteTable.tsx deleted file mode 100644 index a2b05671..00000000 --- a/packages/web/src/app/[domain]/settings/components/inviteTable.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; -import { useMemo } from "react"; -import { DataTable } from "@/components/ui/data-table"; -import { InviteColumnInfo, inviteTableColumns } from "./inviteTableColumns" -import { useToast } from "@/components/hooks/use-toast"; - -export interface InviteInfo { - id: string; - email: string; - createdAt: Date; -} - -interface InviteTableProps { - initialInvites: InviteInfo[]; -} - -export const InviteTable = ({ initialInvites }: InviteTableProps) => { - const { toast } = useToast(); - - const displayToast = (message: string) => { - toast({ - description: message, - }); - } - - const inviteRows: InviteColumnInfo[] = useMemo(() => { - return initialInvites.map(invite => { - return { - id: invite.id!, - email: invite.email!, - createdAt: invite.createdAt!, - } - }) - }, [initialInvites]); - - return ( -
-

Invites

- -
- ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/settings/components/inviteTableColumns.tsx b/packages/web/src/app/[domain]/settings/components/inviteTableColumns.tsx deleted file mode 100644 index f5365199..00000000 --- a/packages/web/src/app/[domain]/settings/components/inviteTableColumns.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use client' - -import { Button } from "@/components/ui/button"; -import { ColumnDef } from "@tanstack/react-table" -import { resolveServerPath } from "@/app/api/(client)/client"; -import { createPathWithQueryParams } from "@/lib/utils"; -import { useToast } from "@/components/hooks/use-toast"; - -export type InviteColumnInfo = { - id: string; - email: string; - createdAt: Date; -} - -export const inviteTableColumns = (displayToast: (message: string) => void): ColumnDef[] => { - return [ - { - accessorKey: "email", - cell: ({ row }) => { - const invite = row.original; - return
{invite.email}
; - } - }, - { - accessorKey: "createdAt", - cell: ({ row }) => { - const invite = row.original; - return invite.createdAt.toISOString(); - } - }, - { - id: "copy", - cell: ({ row }) => { - const invite = row.original; - return ( - - ) - } - } - ] -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/settings/components/memberInviteForm.tsx b/packages/web/src/app/[domain]/settings/components/memberInviteForm.tsx deleted file mode 100644 index c9433170..00000000 --- a/packages/web/src/app/[domain]/settings/components/memberInviteForm.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client' -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { useToast } from "@/components/hooks/use-toast"; -import { createInvite } from "@/actions" -import { isServiceError } from "@/lib/utils"; -import { useDomain } from "@/hooks/useDomain"; -import { ErrorCode } from "@/lib/errorCodes"; -import { useRouter } from "next/navigation"; -import { OrgRole } from "@sourcebot/db"; - -const formSchema = z.object({ - email: z.string().min(2).max(40), -}); - -export const MemberInviteForm = ({ userId, currentUserRole }: { userId: string, currentUserRole: OrgRole }) => { - const router = useRouter(); - const { toast } = useToast(); - const domain = useDomain(); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - email: "", - }, - }); - - const handleCreateInvite = async (values: { email: string }) => { - const res = await createInvite(values.email, userId, domain); - if (isServiceError(res)) { - toast({ - description: res.errorCode == ErrorCode.SELF_INVITE ? res.message :`❌ Failed to create invite` - }); - return; - } else { - toast({ - description: `✅ Invite created successfully!` - }); - - router.refresh(); - } - } - - const isOwner = currentUserRole === OrgRole.OWNER; - return ( -
-

Invite a member

-
- -
-
- ( - - Email - - - - - - )} - /> - -
-
-
- -
- ); -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/settings/components/memberTable.tsx b/packages/web/src/app/[domain]/settings/components/memberTable.tsx deleted file mode 100644 index d5a7f2ce..00000000 --- a/packages/web/src/app/[domain]/settings/components/memberTable.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; -import { useMemo } from "react"; -import { DataTable } from "@/components/ui/data-table"; -import { MemberColumnInfo, MemberTableColumns } from "./memberTableColumns"; - -export interface MemberInfo { - id: string; - name: string; - email: string; - role: string; -} - -interface MemberTableProps { - currentUserRole: string; - currentUserId: string; - initialMembers: MemberInfo[]; -} - -export const MemberTable = ({ currentUserRole, currentUserId, initialMembers }: MemberTableProps) => { - const memberRows: MemberColumnInfo[] = useMemo(() => { - return initialMembers.map(member => { - return { - id: member.id!, - name: member.name!, - email: member.email!, - role: member.role!, - } - }) - }, [initialMembers]); - - return ( -
-

Members

- -
- ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/settings/components/memberTableColumns.tsx b/packages/web/src/app/[domain]/settings/components/memberTableColumns.tsx deleted file mode 100644 index a85aa69a..00000000 --- a/packages/web/src/app/[domain]/settings/components/memberTableColumns.tsx +++ /dev/null @@ -1,199 +0,0 @@ -'use client' - -import { Button } from "@/components/ui/button" -import { ColumnDef } from "@tanstack/react-table" -import { - Dialog, - DialogContent, - DialogClose, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { removeMember, makeOwner } from "@/actions" -import { useToast } from "@/components/hooks/use-toast" -import { useDomain } from "@/hooks/useDomain"; -import { isServiceError } from "@/lib/utils"; -import { useRouter } from "next/navigation"; - -export type MemberColumnInfo = { - id: string; - name: string; - email: string; - role: string; -} - -export const MemberTableColumns = (currentUserRole: string, currentUserId: string): ColumnDef[] => { - const { toast } = useToast(); - const domain = useDomain(); - const router = useRouter(); - - const isOwner = currentUserRole === "OWNER"; - return [ - { - accessorKey: "name", - cell: ({ row }) => { - const member = row.original; - return
{member.name}
; - } - }, - { - accessorKey: "email", - cell: ({ row }) => { - const member = row.original; - return
{member.email}
; - } - }, - { - accessorKey: "role", - cell: ({ row }) => { - const member = row.original; - return
{member.role}
; - } - }, - { - id: "makeOwner", - cell: ({ row }) => { - const member = row.original; - if (!isOwner || member.id === currentUserId) return null; - - return ( - - - - - - - Make Owner - -
-
-

Are you sure you want to make this member the owner?

-
-

- This action will make {member.email} the owner of your organization. -
-
- You will be demoted to a regular member. -

-
-
-
- - - - - - - - -
-
- ); - } - }, - { - id: "remove", - cell: ({ row }) => { - const member = row.original; - if (!isOwner || member.id === currentUserId) { - return null; - } - return ( - - - - - - - Remove Member - -
-
-

Are you sure you want to remove this member?

-
-

- This action will remove {member.email} from your organization. -
-
- Your subscription's seat count will be automatically adjusted. -

-
-
-
- - - - - - - - -
-
- ); - } - } - ] -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/settings/components/sidebar-nav.tsx b/packages/web/src/app/[domain]/settings/components/sidebar-nav.tsx index c8b99744..72858962 100644 --- a/packages/web/src/app/[domain]/settings/components/sidebar-nav.tsx +++ b/packages/web/src/app/[domain]/settings/components/sidebar-nav.tsx @@ -2,7 +2,6 @@ import Link from "next/link" import { usePathname } from "next/navigation" - import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" @@ -19,7 +18,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) { return (