"use client"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { Button } from "@app/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; import { useUserContext } from "@app/hooks/useUserContext"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { type PaginationState } from "@tanstack/react-table"; import { ArrowDown01Icon, ArrowRight, ArrowUp10Icon, ChevronsUpDownIcon, MoreHorizontal } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useMemo, useState, useTransition } from "react"; import { useDebouncedCallback } from "use-debounce"; import z from "zod"; import { ColumnFilterButton } from "./ColumnFilterButton"; import { ColumnMultiFilterButton } from "./ColumnMultiFilterButton"; import IdpTypeBadge from "./IdpTypeBadge"; import { ControlledDataTable, type ExtendedColumnDef } from "./ui/controlled-data-table"; import UserRoleBadges from "./UserRoleBadges"; export type UserRow = { id: string; email: string | null; displayUsername: string | null; username: string; name: string | null; idpId: number | null; idpName: string; type: string; idpVariant: string | null; status: string; roleLabels: string[]; isOwner: boolean; }; type FilterOption = { value: string; label: string }; type UsersTableProps = { users: UserRow[]; pagination: PaginationState; rowCount: number; idpFilterOptions: FilterOption[]; roleFilterOptions: FilterOption[]; }; export default function UsersTable({ users, pagination, rowCount, idpFilterOptions, roleFilterOptions }: UsersTableProps) { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const router = useRouter(); const api = createApiClient(useEnvContext()); const { user } = useUserContext(); const { org } = useOrgContext(); const t = useTranslations(); const [isNavigatingToAddPage, startNavigation] = useTransition(); const [isRefreshing, startTransition] = useTransition(); const { navigate: filter, isNavigating: isFiltering, searchParams, pathname } = useNavigationContext(); const idpIdParamSchema = z .union([z.literal("internal"), z.string().regex(/^\d+$/)]) .optional() .catch(undefined); const roleIdsFromSearchParams = useMemo(() => { const sp = new URLSearchParams(searchParams); return [ ...new Set(sp.getAll("role_id").filter((id) => /^\d+$/.test(id))) ]; }, [searchParams.toString()]); function handleFilterChange( column: string, value: string | undefined | null ) { const sp = new URLSearchParams(searchParams); sp.delete(column); sp.delete("page"); if (value) { sp.set(column, value); } startTransition(() => router.push(`${pathname}?${sp.toString()}`)); } function handleRoleIdsChange(values: string[]) { const sp = new URLSearchParams(searchParams); sp.delete("role_id"); sp.delete("page"); for (const id of values) { if (/^\d+$/.test(id)) { sp.append("role_id", id); } } startTransition(() => router.push(`${pathname}?${sp.toString()}`)); } const refreshData = async () => { startTransition(async () => { try { await new Promise((resolve) => setTimeout(resolve, 200)); router.refresh(); } catch (error) { toast({ title: t("error"), description: t("refreshError"), variant: "destructive" }); } }); }; const columns: ExtendedColumnDef[] = [ { accessorKey: "displayUsername", enableHiding: false, friendlyName: t("username"), header: ({ column }) => { const nameOrder = getSortDirection("username", searchParams); const Icon = nameOrder === "asc" ? ArrowDown01Icon : nameOrder === "desc" ? ArrowUp10Icon : ChevronsUpDownIcon; return ( ); } }, { accessorKey: "idpName", friendlyName: t("identityProvider"), header: () => { return ( handleFilterChange("idp_id", value) } searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} label={t("identityProvider")} className="p-3" /> ); }, cell: ({ row }) => { const userRow = row.original; return ( ); } }, { id: "role", accessorFn: (row) => row.roleLabels.join(", "), friendlyName: t("role"), header: () => { return ( ); }, cell: ({ row }) => { return ; } }, { id: "actions", enableHiding: false, header: () => , cell: ({ row }) => { const userRow = row.original; const isCurrentUser = `${userRow.username}-${userRow.idpId}` === `${user?.username}-${user?.idpId}`; const isDisabled = userRow.isOwner || isCurrentUser; return (
isDisabled && e.preventDefault() } > {t("accessUserManage")} {!isDisabled && ( { setIsDeleteModalOpen(true); setSelectedUser(userRow); }} > {t("accessUserRemove")} )}
{isDisabled ? ( ) : ( )}
); } } ]; async function removeUser() { if (selectedUser) { const res = await api .delete(`/org/${org!.org.orgId}/user/${selectedUser.id}`) .catch((e) => { toast({ variant: "destructive", title: t("userErrorOrgRemove"), description: formatAxiosError( e, t("userErrorOrgRemoveDescription") ) }); }); if (res && res.status === 200) { toast({ variant: "default", title: t("userOrgRemoved"), description: t("userOrgRemovedDescription", { email: selectedUser.email || "" }) }); } } router.refresh(); setIsDeleteModalOpen(false); } function toggleSort(column: string) { const newSearch = getNextSortOrder(column, searchParams); filter({ searchParams: newSearch }); } const handlePaginationChange = (newPage: PaginationState) => { searchParams.set("page", (newPage.pageIndex + 1).toString()); searchParams.set("pageSize", newPage.pageSize.toString()); filter({ searchParams }); }; const handleSearchChange = useDebouncedCallback((query: string) => { searchParams.set("query", query); searchParams.delete("page"); filter({ searchParams }); }, 300); return ( <> { setIsDeleteModalOpen(val); setSelectedUser(null); }} dialog={

{t("userQuestionOrgRemove")}

{t("userMessageOrgRemove")}

} buttonText={t("userRemoveOrgConfirm")} onConfirm={async () => startTransition(removeUser)} string={ selectedUser ? getUserDisplayName({ email: selectedUser.email, name: selectedUser.name, username: selectedUser.username }) : "" } title={t("userRemoveOrg")} /> { startNavigation(() => router.push( `/${org?.org.orgId}/settings/access/users/create` ) ); }} onRefresh={refreshData} isRefreshing={isRefreshing || isFiltering} /> ); }