diff --git a/apps/web/app/settings/platform/managed-users/page.tsx b/apps/web/app/settings/platform/managed-users/page.tsx
new file mode 100644
index 0000000000..17cb3b45de
--- /dev/null
+++ b/apps/web/app/settings/platform/managed-users/page.tsx
@@ -0,0 +1,14 @@
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import ManagedUsersView from "~/settings/platform/managed-users/managed-users-view";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("platform_members"),
+ (t) => t("platform_members_description")
+ );
+
+export default WithLayout({
+ Page: ManagedUsersView,
+});
diff --git a/apps/web/components/settings/platform/dashboard/managed-user-header/index.tsx b/apps/web/components/settings/platform/dashboard/managed-user-header/index.tsx
deleted file mode 100644
index 40a7ece0c1..0000000000
--- a/apps/web/components/settings/platform/dashboard/managed-user-header/index.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { PlatformOAuthClient } from "@calcom/prisma/client";
-
-import { OAuthClientsDropdown } from "@components/settings/platform/dashboard/oauth-client-dropdown";
-
-type ManagedUserHeaderProps = {
- oauthClients: PlatformOAuthClient[];
- initialClientName: string;
- handleChange: (clientId: string, clientName: string) => void;
-};
-
-export const ManagedUserHeader = ({
- oauthClients,
- initialClientName,
- handleChange,
-}: ManagedUserHeaderProps) => {
- return (
-
-
-
- Managed Users
-
-
- See all the managed users created by your OAuth client.
-
-
-
-
- );
-};
diff --git a/apps/web/components/settings/platform/dashboard/managed-user-list/index.tsx b/apps/web/components/settings/platform/dashboard/managed-user-list/index.tsx
deleted file mode 100644
index 341e19a073..0000000000
--- a/apps/web/components/settings/platform/dashboard/managed-user-list/index.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import type { PlatformOAuthClient } from "@calcom/prisma/client";
-
-import type { ManagedUser } from "@lib/hooks/settings/platform/oauth-clients/useOAuthClients";
-
-import { ManagedUserHeader } from "@components/settings/platform/dashboard/managed-user-header";
-import { ManagedUserTable } from "@components/settings/platform/dashboard/managed-user-table";
-
-type ManagedUserListProps = {
- oauthClients: PlatformOAuthClient[];
- managedUsers?: ManagedUser[];
- initialClientName: string;
- initialClientId: string;
- isManagedUserLoading: boolean;
- handleChange: (clientId: string, clientName: string) => void;
-};
-
-export const ManagedUserList = ({
- initialClientName,
- initialClientId,
- oauthClients,
- managedUsers,
- isManagedUserLoading,
- handleChange,
-}: ManagedUserListProps) => {
- return (
-
-
-
-
- );
-};
diff --git a/apps/web/components/settings/platform/dashboard/managed-user-table/index.tsx b/apps/web/components/settings/platform/dashboard/managed-user-table/index.tsx
deleted file mode 100644
index af32a422e2..0000000000
--- a/apps/web/components/settings/platform/dashboard/managed-user-table/index.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { EmptyScreen } from "@calcom/ui";
-
-import type { ManagedUser } from "@lib/hooks/settings/platform/oauth-clients/useOAuthClients";
-
-type ManagedUserTableProps = {
- managedUsers?: ManagedUser[];
- isManagedUserLoading: boolean;
- initialClientId: string;
-};
-
-export const ManagedUserTable = ({
- managedUsers,
- isManagedUserLoading,
- initialClientId,
-}: ManagedUserTableProps) => {
- const showUsers = !isManagedUserLoading && managedUsers?.length;
-
- return (
-
- {showUsers ? (
- <>
-
-
-
- | Id |
- Username |
- Email |
-
- {managedUsers.map((user) => {
- return (
-
- | {user.id} |
- {user.username} |
-
- {user.email}
- |
-
- );
- })}
-
- >
- ) : (
-
- )}
-
- );
-};
diff --git a/apps/web/components/settings/platform/dashboard/oauth-client-dropdown/index.tsx b/apps/web/components/settings/platform/dashboard/oauth-client-dropdown/index.tsx
deleted file mode 100644
index 17ec4c366a..0000000000
--- a/apps/web/components/settings/platform/dashboard/oauth-client-dropdown/index.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import type { PlatformOAuthClient } from "@calcom/prisma/client";
-import {
- Button,
- Dropdown,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownItem,
-} from "@calcom/ui";
-
-type OAuthClientsDropdownProps = {
- oauthClients: PlatformOAuthClient[];
- initialClientName: string;
- handleChange: (clientId: string, clientName: string) => void;
-};
-
-export const OAuthClientsDropdown = ({
- oauthClients,
- initialClientName,
- handleChange,
-}: OAuthClientsDropdownProps) => {
- return (
-
- {Array.isArray(oauthClients) && oauthClients.length > 0 ? (
-
-
-
-
-
- {oauthClients.map((client) => {
- return (
-
- {initialClientName !== client.name ? (
-
- handleChange(client.id, client.name)}>
- {client.name}
-
-
- ) : (
- <>>
- )}
-
- );
- })}
-
-
- ) : null}
-
- );
-};
diff --git a/apps/web/modules/settings/platform/managed-users/managed-users-view.tsx b/apps/web/modules/settings/platform/managed-users/managed-users-view.tsx
new file mode 100644
index 0000000000..2311267920
--- /dev/null
+++ b/apps/web/modules/settings/platform/managed-users/managed-users-view.tsx
@@ -0,0 +1,95 @@
+"use client";
+
+import { useMemo, useState } from "react";
+
+import Shell from "@calcom/features/shell/Shell";
+import { PlatformManagedUsersTable } from "@calcom/features/users/components/UserTable/PlatformManagedUsersTable";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Select, Skeleton, SkeletonContainer, SkeletonText, Label } from "@calcom/ui";
+
+import { useOAuthClients } from "@lib/hooks/settings/platform/oauth-clients/useOAuthClients";
+
+type OAuthClientOption = { label: string; value: string };
+
+const ManagedUsersView = () => {
+ const { t } = useLocale();
+ const { data: OAuthClientsQueryData, error, isLoading: isOAuthClientLoading } = useOAuthClients();
+
+ const oAuthClientOptions: OAuthClientOption[] = useMemo(
+ () =>
+ OAuthClientsQueryData.map((client) => ({
+ label: client.name,
+ value: client.id,
+ })),
+ [OAuthClientsQueryData]
+ );
+
+ const [oAuthClient, setOAuthClient] = useState(oAuthClientOptions[0] || null);
+
+ const ManagedUsersSkeletonLoader = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ };
+
+ if (isOAuthClientLoading) {
+ return ;
+ }
+
+ if (error) {
+ return {t("Failed to load OAuth clients.")}
;
+ }
+
+ if (!OAuthClientsQueryData.length) {
+ return (
+ {t("No OAuth clients available. Please create one to view managed users.")}
+ );
+ }
+
+ return (
+
+
+ {t("select_oAuth_client")}
+
+
+ );
+};
+
+export default ManagedUsersView;
diff --git a/apps/web/modules/settings/platform/platform-view.tsx b/apps/web/modules/settings/platform/platform-view.tsx
index 50ade1b954..5a8465d5d5 100644
--- a/apps/web/modules/settings/platform/platform-view.tsx
+++ b/apps/web/modules/settings/platform/platform-view.tsx
@@ -7,15 +7,11 @@ import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { showToast } from "@calcom/ui";
-import {
- useOAuthClients,
- useGetOAuthClientManagedUsers,
-} from "@lib/hooks/settings/platform/oauth-clients/useOAuthClients";
+import { useOAuthClients } from "@lib/hooks/settings/platform/oauth-clients/useOAuthClients";
import { useDeleteOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/usePersistOAuthClient";
import { HelpCards } from "@components/settings/platform/dashboard/HelpCards";
import NoPlatformPlan from "@components/settings/platform/dashboard/NoPlatformPlan";
-import { ManagedUserList } from "@components/settings/platform/dashboard/managed-user-list";
import { OAuthClientsList } from "@components/settings/platform/dashboard/oauth-clients-list";
import { useGetUserAttributes } from "@components/settings/platform/hooks/useGetUserAttributes";
import { PlatformPricing } from "@components/settings/platform/pricing/platform-pricing";
@@ -28,11 +24,6 @@ export default function Platform() {
const [initialClientName, setInitialClientName] = useState("");
const { data, isLoading: isOAuthClientLoading, refetch: refetchClients } = useOAuthClients();
- const {
- isLoading: isManagedUserLoading,
- data: managedUserData,
- refetch: refetchManagedUsers,
- } = useGetOAuthClientManagedUsers(initialClientId);
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, isPaidUser, userBillingData, userOrgId } =
useGetUserAttributes();
@@ -41,7 +32,6 @@ export default function Platform() {
onSuccess: () => {
showToast(t("oauth_client_deletion_message"), "success");
refetchClients();
- refetchManagedUsers();
},
});
@@ -87,18 +77,6 @@ export default function Platform() {
isPlatformUser={true}>
- {
- setInitialClientId(id);
- setInitialClientName(name);
- refetchManagedUsers();
- }}
- />
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 7a1877a9f5..5e4accb1db 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -2920,5 +2920,8 @@
"choose_an_option": "Choose an option",
"enter_option_value": "Enter option value",
"remove_option": "Remove option",
+ "managed_users": "Managed Users",
+ "managed_users_description": "See all the managed users created by your OAuth client",
+ "select_oAuth_client": "Select Oauth Client",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
\ No newline at end of file
diff --git a/packages/features/shell/navigation/Navigation.tsx b/packages/features/shell/navigation/Navigation.tsx
index 101734fbc5..385cc0f091 100644
--- a/packages/features/shell/navigation/Navigation.tsx
+++ b/packages/features/shell/navigation/Navigation.tsx
@@ -167,6 +167,12 @@ const platformNavigationItems: NavigationItemType[] = [
icon: "users",
moreOnMobile: true,
},
+ {
+ name: "Managed Users",
+ href: "/settings/platform/managed-users",
+ icon: "users",
+ moreOnMobile: true,
+ },
];
const useNavigationItems = (isPlatformNavigation = false) => {
diff --git a/packages/features/users/components/UserTable/BulkActions/DeleteBulkUsers.tsx b/packages/features/users/components/UserTable/BulkActions/DeleteBulkUsers.tsx
index 55b7619d50..4d6bf4d7bb 100644
--- a/packages/features/users/components/UserTable/BulkActions/DeleteBulkUsers.tsx
+++ b/packages/features/users/components/UserTable/BulkActions/DeleteBulkUsers.tsx
@@ -6,7 +6,7 @@ import { ConfirmationDialogContent, Dialog, DialogTrigger, showToast } from "@ca
import type { UserTableUser } from "../types";
interface Props {
- users: UserTableUser[];
+ users: Array<{ id: UserTableUser["id"] }>;
onRemove: () => void;
}
diff --git a/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx b/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx
new file mode 100644
index 0000000000..d0d49c7387
--- /dev/null
+++ b/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx
@@ -0,0 +1,359 @@
+"use client";
+
+import { keepPreviousData } from "@tanstack/react-query";
+import { getCoreRowModel, getSortedRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table";
+import { useMemo, useReducer, useRef, useState } from "react";
+
+import {
+ DataTableProvider,
+ DataTable,
+ DataTableToolbar,
+ DataTableFilters,
+ DataTableSelectionBar,
+ DataTablePagination,
+ useColumnFilters,
+ useFetchMoreOnBottomReached,
+} from "@calcom/features/data-table";
+import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc";
+import { Avatar, Badge, Checkbox, SkeletonText } from "@calcom/ui";
+
+import { DeleteBulkUsers } from "./BulkActions/DeleteBulkUsers";
+import { DeleteMemberModal } from "./DeleteMemberModal";
+import type { UserTableState, UserTableAction, PlatformManagedUserTableUser } from "./types";
+
+const initialState: UserTableState = {
+ changeMemberRole: {
+ showModal: false,
+ },
+ deleteMember: {
+ showModal: false,
+ },
+ impersonateMember: {
+ showModal: false,
+ },
+ inviteMember: {
+ showModal: false,
+ },
+ editSheet: {
+ showModal: false,
+ },
+};
+
+const initalColumnVisibility = {
+ select: true,
+ member: true,
+ role: true,
+ teams: true,
+ actions: true,
+};
+
+type PlatformManagedUsersTableProps = {
+ oAuthClientId: string;
+};
+
+export function PlatformManagedUsersTable(props: PlatformManagedUsersTableProps) {
+ return (
+
+
+
+ );
+}
+
+function UserListTableContent({ oAuthClientId }: PlatformManagedUsersTableProps) {
+ const { t } = useLocale();
+
+ const tableContainerRef = useRef(null);
+
+ const [state, dispatch] = useReducer(reducer, initialState);
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
+ const [rowSelection, setRowSelection] = useState({});
+
+ const columnFilters = useColumnFilters();
+
+ const { data, isPending, hasNextPage, fetchNextPage, isFetching } =
+ trpc.viewer.organizations.listMembers.useInfiniteQuery(
+ {
+ limit: 30,
+ searchTerm: debouncedSearchTerm,
+ filters: columnFilters,
+ oAuthClientId,
+ },
+ {
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
+ placeholderData: keepPreviousData,
+ enabled: !!oAuthClientId,
+ }
+ );
+
+ const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;
+
+ //we must flatten the array of arrays from the useInfiniteQuery hook
+ const flatData = useMemo(
+ () => data?.pages?.flatMap((page) => page.rows) ?? [],
+ [data]
+ ) as PlatformManagedUserTableUser[];
+
+ const columns = useMemo(() => {
+ const cols: ColumnDef[] = [
+ // Disabling select for this PR: Will work on actions etc in a follow up
+ {
+ id: "select",
+ enableHiding: false,
+ enableSorting: false,
+ enableResizing: false,
+ size: 30,
+ meta: {
+ sticky: {
+ position: "left",
+ },
+ },
+ header: ({ table }) => (
+ table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-[2px]"
+ />
+ ),
+ cell: ({ row }) => (
+ row.toggleSelected(!!value)}
+ aria-label="Select row"
+ className="translate-y-[2px]"
+ />
+ ),
+ },
+ {
+ id: "member",
+ accessorFn: (data) => data.email,
+ enableHiding: false,
+ size: 200,
+ header: () => {
+ return `Managed Users`;
+ },
+ meta: {
+ sticky: { position: "left", gap: 24 },
+ },
+ cell: ({ row }) => {
+ if (isPending) {
+ return ;
+ }
+ const { username, email, avatarUrl } = row.original;
+ return (
+
+
+
+
+ {username || "No username"}
+
+
+ {email}
+
+
+
+ );
+ },
+ filterFn: (rows, id, filterValue) => {
+ const userEmail = rows.original.email;
+ return filterValue.includes(userEmail);
+ },
+ },
+ {
+ id: "role",
+ accessorFn: (data) => data.role,
+ header: "Role",
+ size: 100,
+ cell: ({ row, table }) => {
+ if (isPending) {
+ return ;
+ }
+ const { role, username } = row.original;
+ return (
+ {
+ table.getColumn("role")?.setFilterValue([role]);
+ }}>
+ {role}
+
+ );
+ },
+ filterFn: (rows, id, filterValue) => {
+ if (filterValue.includes("PENDING")) {
+ if (filterValue.length === 1) return !rows.original.accepted;
+ else return !rows.original.accepted || filterValue.includes(rows.getValue(id));
+ }
+
+ // Show only the selected roles
+ return filterValue.includes(rows.getValue(id));
+ },
+ },
+ {
+ id: "teams",
+ accessorFn: (data) => data.teams.map((team) => team.name),
+ header: "Teams",
+ size: 140,
+ cell: ({ row, table }) => {
+ if (isPending) {
+ return ;
+ }
+ const { teams, accepted, email, username } = row.original;
+ // TODO: Implement click to filter
+ return (
+
+ {accepted ? null : (
+ {
+ table.getColumn("role")?.setFilterValue(["PENDING"]);
+ }}>
+ Pending
+
+ )}
+
+ {teams.map((team) => (
+ {
+ table.getColumn("teams")?.setFilterValue([team.name]);
+ }}>
+ {team.name}
+
+ ))}
+
+ );
+ },
+ filterFn: (rows, _, filterValue: string[]) => {
+ const teamNames = rows.original.teams.map((team) => team.name);
+ return filterValue.some((value: string) => teamNames.includes(value));
+ },
+ },
+ ];
+ return cols;
+ }, [isPending]);
+
+ const table = useReactTable({
+ data: flatData,
+ columns,
+ enableRowSelection: true,
+ columnResizeMode: "onChange",
+ debugTable: true,
+ manualPagination: true,
+ initialState: {
+ columnVisibility: initalColumnVisibility,
+ },
+ defaultColumn: {
+ size: 150,
+ },
+ state: {
+ columnFilters,
+ rowSelection,
+ },
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ onRowSelectionChange: setRowSelection,
+ getRowId: (row) => `${row.id}`,
+ });
+
+ function reducer(state: UserTableState, action: UserTableAction): UserTableState {
+ switch (action.type) {
+ case "SET_CHANGE_MEMBER_ROLE_ID":
+ return { ...state, changeMemberRole: action.payload };
+ case "SET_DELETE_ID":
+ return { ...state, deleteMember: action.payload };
+ case "SET_IMPERSONATE_ID":
+ return { ...state, impersonateMember: action.payload };
+ case "INVITE_MEMBER":
+ return { ...state, inviteMember: action.payload };
+ case "EDIT_USER_SHEET":
+ return { ...state, editSheet: action.payload };
+ case "CLOSE_MODAL":
+ return {
+ ...state,
+ changeMemberRole: { showModal: false },
+ deleteMember: { showModal: false },
+ impersonateMember: { showModal: false },
+ inviteMember: { showModal: false },
+ editSheet: { showModal: false },
+ };
+ default:
+ return state;
+ }
+ }
+
+ const fetchMoreOnBottomReached = useFetchMoreOnBottomReached({
+ tableContainerRef,
+ hasNextPage,
+ fetchNextPage,
+ isFetching,
+ });
+
+ const numberOfSelectedRows = useMemo(() => table.getSelectedRowModel().rows.length, [table]);
+
+ return (
+ <>
+ fetchMoreOnBottomReached(e.target as HTMLDivElement)}>
+
+
+
+ setDebouncedSearchTerm(value)}
+ className="sm:max-w-64 max-w-full"
+ />
+
+
+ {/* We have to omit member because we don't want the filter to show but we can't disable filtering as we need that for the search bar */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {numberOfSelectedRows > 0 && (
+
+
+ {t("number_selected", { count: numberOfSelectedRows })}
+
+ row.original)}
+ onRemove={() => table.toggleAllPageRowsSelected(false)}
+ />
+
+ )}
+
+ {state.deleteMember.showModal && }
+ >
+ );
+}
diff --git a/packages/features/users/components/UserTable/types.ts b/packages/features/users/components/UserTable/types.ts
index 785e061ca5..cd92f7347d 100644
--- a/packages/features/users/components/UserTable/types.ts
+++ b/packages/features/users/components/UserTable/types.ts
@@ -26,11 +26,21 @@ export interface UserTableUser {
}[];
}
+export type PlatformManagedUserTableUser = Omit<
+ UserTableUser,
+ "lastActiveAt" | "attributes" | "completedOnboarding" | "disableImpersonation"
+>;
+
export type UserTablePayload = {
showModal: boolean;
user?: UserTableUser;
};
+export type PlatformUserTablePayload = {
+ showModal: boolean;
+ user?: PlatformManagedUserTableUser;
+};
+
export type UserTableState = {
changeMemberRole: UserTablePayload;
deleteMember: UserTablePayload;
@@ -39,6 +49,10 @@ export type UserTableState = {
editSheet: UserTablePayload & { user?: UserTableUser };
};
+export type PlatforManagedUserTableState = {
+ deleteMember: UserTablePayload;
+};
+
export type UserTableAction =
| {
type:
@@ -53,3 +67,12 @@ export type UserTableAction =
| {
type: "CLOSE_MODAL";
};
+
+export type PlatformManagedUserTableAction =
+ | {
+ type: "SET_DELETE_ID";
+ payload: PlatformUserTablePayload;
+ }
+ | {
+ type: "CLOSE_MODAL";
+ };
diff --git a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
index 6db851d337..22ba89b2e5 100644
--- a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
+++ b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
@@ -18,10 +18,22 @@ type GetOptions = {
const isAllString = (array: (string | number)[]): array is string[] => {
return array.every((value) => typeof value === "string");
};
+function getUserConditions(oAuthClientId?: string) {
+ if (!!oAuthClientId) {
+ return {
+ platformOAuthClients: {
+ some: { id: oAuthClientId },
+ },
+ isPlatformManaged: true,
+ };
+ }
+ return { isPlatformManaged: false };
+}
export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
const organizationId = ctx.user.organizationId ?? ctx.user.profiles[0].organizationId;
const searchTerm = input.searchTerm;
+ const oAuthClientId = input.oAuthClientId;
const expand = input.expand;
const filters = input.filters || [];
@@ -66,13 +78,16 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
const getTotalMembers = await prisma.membership.count({
where: {
+ user: {
+ ...getUserConditions(oAuthClientId),
+ },
teamId: organizationId,
},
});
let whereClause: Prisma.MembershipWhereInput = {
user: {
- isPlatformManaged: false,
+ ...getUserConditions(oAuthClientId),
},
teamId: organizationId,
...(searchTerm && {
diff --git a/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts b/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts
index 7932dab84c..e4fd34736a 100644
--- a/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts
+++ b/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts
@@ -15,6 +15,7 @@ export const ZListMembersSchema = z.object({
searchTerm: z.string().optional(),
expand: z.array(expandableColumns).optional(),
filters: z.array(ZListMembersFilter).optional(),
+ oAuthClientId: z.string().optional(),
});
export type TListMembersSchema = z.infer;