diff --git a/apps/dashboard/app/(app)/settings/team/client.tsx b/apps/dashboard/app/(app)/settings/team/client.tsx new file mode 100644 index 0000000000..43dc72dba3 --- /dev/null +++ b/apps/dashboard/app/(app)/settings/team/client.tsx @@ -0,0 +1,342 @@ +"use client"; +import { Badge } from "@/components/ui/badge"; +import { Empty } from "@unkey/ui"; +import { Button } from "@unkey/ui"; +import type React from "react"; +import { useState } from "react"; +import { InviteButton } from "./invite"; + +import Confirm from "@/components/dashboard/confirm"; +import { PageHeader } from "@/components/dashboard/page-header"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useAuth, useClerk, useOrganization } from "@clerk/nextjs"; + +import { Loading } from "@/components/dashboard/loading"; +import { Navbar as SubMenu } from "@/components/dashboard/navbar"; +import { Navigation } from "@/components/navigation/navigation"; +import { PageContent } from "@/components/page-content"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { toast } from "@/components/ui/toaster"; +import type { MembershipRole } from "@clerk/types"; +import { Gear } from "@unkey/icons"; +import Link from "next/link"; +import { navigation } from "../constants"; + +type Member = { + id: string; + name: string; + image: string; + role: MembershipRole; + email?: string; +}; + +export default function TeamPage({ team }: { team: boolean }) { + const { user, organization } = useClerk(); + + if (organization && !team) { + return ( +
+ } /> + + +
+ + Invites are not available on the Free tier + + Please upgrade your workspace to a paid plan to enable invites. + + + + + + + +
+
+
+ ); + } + if (!organization) { + return ( +
+ } /> + + +
+ + Invites are not available on the Free tier + + Please create a workspace and upgrade to the pro tier. + + + + + + + +
+
+
+ ); + } + + const isAdmin = + user?.organizationMemberships.find((m) => m.organization.id === organization.id)?.role === + "admin"; + + type Tab = "members" | "invitations"; + const [tab, setTab] = useState("members"); + + const actions: React.ReactNode[] = []; + + if (isAdmin) { + actions.push( + , + ); + } + + if (isAdmin) { + actions.push(); + } + + return ( +
+ } name="Settings" /> + + +
+ + + {tab === "members" ? : } +
+
+
+ ); +} + +const Members: React.FC = () => { + const { user } = useClerk(); + + const { isLoaded, membershipList, membership, organization } = useOrganization({ + membershipList: { limit: 20, offset: 0 }, + }); + + if (!isLoaded) { + return ( +
+
+ +
+
+ ); + } + + return ( + + + + Member + Role + + {/*/ empty */} + + + + {membershipList?.map(({ id, role, publicUserData }) => ( + + +
+ + + {publicUserData.identifier.slice(0, 2)} + +
+ {`${ + publicUserData.firstName ? publicUserData.firstName : publicUserData.identifier + } ${publicUserData.lastName ? publicUserData.lastName : ""}`} + + {publicUserData.firstName ? publicUserData.identifier : ""} + +
+
+
+ + + + + {membership?.role === "admin" && publicUserData.userId !== user?.id ? ( + { + if (publicUserData.userId) { + organization?.removeMember(publicUserData.userId); + } + }} + trigger={} + /> + ) : null} + +
+ ))} +
+
+ ); +}; + +const Invitations: React.FC = () => { + const { isLoaded, invitationList } = useOrganization({ + invitationList: { limit: 20, offset: 0 }, + }); + + if (!isLoaded) { + return ( +
+
+ +
+
+ ); + } + + if (!invitationList || invitationList.length === 0) { + return ( + + No pending invitations + Invite members to your team + + + ); + } + + return ( + + + + Email + Status + + {/*/ empty */} + + + + {invitationList?.map((invitation) => ( + + + {invitation.emailAddress} + + + + + + + + + + ))} + +
+ ); +}; + +const RoleSwitcher: React.FC<{ + member: { id: string; role: Member["role"] }; +}> = ({ member }) => { + const [role, setRole] = useState(member.role); + const [isLoading, setLoading] = useState(false); + const { organization, membership } = useOrganization(); + const { userId } = useAuth(); + async function updateRole(role: Member["role"]) { + try { + setLoading(true); + if (!organization) { + return; + } + await organization?.updateMember({ userId: member.id, role }); + + setRole(role); + toast.success("Role updated"); + } catch (err) { + console.error(err); + toast.error((err as Error).message); + } finally { + setLoading(false); + } + } + + if (!membership) { + return null; + } + + if (membership.role === "admin") { + return ( + + ); + } + + return {role === "admin" ? "Admin" : "Member"}; +}; + +const StatusBadge: React.FC<{ status: "pending" | "accepted" | "revoked" }> = ({ status }) => { + switch (status) { + case "pending": + return Pending; + case "accepted": + return Accepted; + case "revoked": + return Revoked; + + default: + return null; + } +}; diff --git a/apps/dashboard/app/(app)/settings/team/page.tsx b/apps/dashboard/app/(app)/settings/team/page.tsx index ef79641e87..a6ba4597ac 100644 --- a/apps/dashboard/app/(app)/settings/team/page.tsx +++ b/apps/dashboard/app/(app)/settings/team/page.tsx @@ -1,317 +1,22 @@ -"use client"; -import { Badge } from "@/components/ui/badge"; -import { Empty } from "@unkey/ui"; -import { Button } from "@unkey/ui"; -import type React from "react"; -import { useState } from "react"; -import { InviteButton } from "./invite"; - -import Confirm from "@/components/dashboard/confirm"; -import { PageHeader } from "@/components/dashboard/page-header"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { useAuth, useClerk, useOrganization } from "@clerk/nextjs"; - -import { Loading } from "@/components/dashboard/loading"; -import { Navbar as SubMenu } from "@/components/dashboard/navbar"; -import { Navigation } from "@/components/navigation/navigation"; -import { PageContent } from "@/components/page-content"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { toast } from "@/components/ui/toaster"; -import type { MembershipRole } from "@clerk/types"; -import { Gear } from "@unkey/icons"; -import Link from "next/link"; -import { navigation } from "../constants"; - -type Member = { - id: string; - name: string; - image: string; - role: MembershipRole; - email?: string; -}; - -export default function TeamPage() { - const { user, organization } = useClerk(); - - if (!organization) { - return ( -
- } /> - - -
- - This is a personal account - You can only manage teams in paid workspaces. - - - - - - -
-
-
- ); - } - - const isAdmin = - user?.organizationMemberships.find((m) => m.organization.id === organization.id)?.role === - "admin"; - - type Tab = "members" | "invitations"; - const [tab, setTab] = useState("members"); - - const actions: React.ReactNode[] = []; - - if (isAdmin) { - actions.push( - , - ); - } +import { getTenantId } from "@/lib/auth"; +import { db } from "@/lib/db"; +import TeamPage from "./client"; + +export const revalidate = 0; + +export default async function SettingsKeysPage() { + const tenantId = getTenantId(); + const ws = await db.query.workspaces.findFirst({ + where: (table, { and, eq, isNull }) => + and(eq(table.tenantId, tenantId), isNull(table.deletedAtM)), + with: { quota: true }, + }); - if (isAdmin) { - actions.push(); - } + const team = ws?.quota?.team ?? false; return (
- } name="Settings" /> - - -
- - - {tab === "members" ? : } -
-
+
); } - -const Members: React.FC = () => { - const { user } = useClerk(); - - const { isLoaded, membershipList, membership, organization } = useOrganization({ - membershipList: { limit: 20, offset: 0 }, - }); - - if (!isLoaded) { - return ( -
-
- -
-
- ); - } - - return ( - - - - Member - Role - - {/*/ empty */} - - - - {membershipList?.map(({ id, role, publicUserData }) => ( - - -
- - - {publicUserData.identifier.slice(0, 2)} - -
- {`${ - publicUserData.firstName ? publicUserData.firstName : publicUserData.identifier - } ${publicUserData.lastName ? publicUserData.lastName : ""}`} - - {publicUserData.firstName ? publicUserData.identifier : ""} - -
-
-
- - - - - {membership?.role === "admin" && publicUserData.userId !== user?.id ? ( - { - if (publicUserData.userId) { - organization?.removeMember(publicUserData.userId); - } - }} - trigger={} - /> - ) : null} - -
- ))} -
-
- ); -}; - -const Invitations: React.FC = () => { - const { isLoaded, invitationList } = useOrganization({ - invitationList: { limit: 20, offset: 0 }, - }); - - if (!isLoaded) { - return ( -
-
- -
-
- ); - } - - if (!invitationList || invitationList.length === 0) { - return ( - - No pending invitations - Invite members to your team - - - ); - } - - return ( - - - - Email - Status - - {/*/ empty */} - - - - {invitationList?.map((invitation) => ( - - - {invitation.emailAddress} - - - - - - - - - - ))} - -
- ); -}; - -const RoleSwitcher: React.FC<{ - member: { id: string; role: Member["role"] }; -}> = ({ member }) => { - const [role, setRole] = useState(member.role); - const [isLoading, setLoading] = useState(false); - const { organization, membership } = useOrganization(); - const { userId } = useAuth(); - async function updateRole(role: Member["role"]) { - try { - setLoading(true); - if (!organization) { - return; - } - await organization?.updateMember({ userId: member.id, role }); - - setRole(role); - toast.success("Role updated"); - } catch (err) { - console.error(err); - toast.error((err as Error).message); - } finally { - setLoading(false); - } - } - - if (!membership) { - return null; - } - - if (membership.role === "admin") { - return ( - - ); - } - - return {role === "admin" ? "Admin" : "Member"}; -}; - -const StatusBadge: React.FC<{ status: "pending" | "accepted" | "revoked" }> = ({ status }) => { - switch (status) { - case "pending": - return Pending; - case "accepted": - return Accepted; - case "revoked": - return Revoked; - - default: - return null; - } -};