[] =
+ attributes?.map((attribute) => {
// TODO: We need to normalize AttributeOption table first
// so that we can have `number_value` column for numeric operations.
// Currently, `value` column is used for both text and number attributes.
@@ -184,7 +184,7 @@ function UserListTableContent() {
const isNumber = false;
const isText = attribute.type === "TEXT";
const isSingleSelect = attribute.type === "SINGLE_SELECT";
- const isMultiSelect = attribute.type === "MULTI_SELECT";
+ // const isMultiSelect = attribute.type === "MULTI_SELECT";
const filterType = isNumber
? ColumnFilterType.NUMBER
: isText
@@ -200,15 +200,15 @@ function UserListTableContent() {
filter: { type: filterType },
},
size: 120,
- accessorFn: (data) => data.attributes.find((attr) => attr.attributeId === attribute.id)?.value,
+ accessorFn: (data) => data.attributes?.find((attr) => attr.attributeId === attribute.id)?.value,
cell: ({ row }) => {
- const attributeValues = row.original.attributes.filter(
+ const attributeValues = row.original.attributes?.filter(
(attr) => attr.attributeId === attribute.id
);
- if (attributeValues.length === 0) return null;
+ if (attributeValues?.length === 0) return null;
return (
- {attributeValues.map((attributeValue) => {
+ {attributeValues?.map((attributeValue) => {
const isAGroupOption = attributeValue.contains?.length > 0;
const suffix = attribute.isWeightsEnabled
? `${attributeValue.weight || 100}%`
@@ -238,27 +238,22 @@ function UserListTableContent() {
);
},
filterFn: (row, id, filterValue) => {
- const attributeValues = row.original.attributes.filter((attr) => attr.attributeId === id);
+ const attributeValues = row.original.attributes?.filter((attr) => attr.attributeId === id);
if (isTextFilterValue(filterValue)) {
- return attributeValues.some((attr) => textFilter(attr.value, filterValue));
+ return attributeValues?.some((attr) => textFilter(attr.value, filterValue)) ?? false;
} else if (isSingleSelectFilterValue(filterValue)) {
- return singleSelectFilter(
- attributeValues.map((attr) => attr.value),
- filterValue
- );
+ return singleSelectFilter(attributeValues?.map((attr) => attr.value) ?? [], filterValue);
} else if (isMultiSelectFilterValue(filterValue)) {
- return multiSelectFilter(
- attributeValues.map((attr) => attr.value),
- filterValue
- );
+ return multiSelectFilter(attributeValues?.map((attr) => attr.value) ?? [], filterValue);
}
return false;
},
};
- }) as ColumnDef[]) ?? []
- );
+ }) ?? [];
+ return attributeColumns;
};
+
const cols: ColumnDef[] = [
// Disabling select for this PR: Will work on actions etc in a follow up
{
@@ -430,14 +425,16 @@ function UserListTableContent() {
];
return cols;
- }, [session?.user.id, adminOrOwner, dispatch, domain, totalDBRowCount, attributes]);
+ }, [session?.user.id, adminOrOwner, dispatch, domain, attributes, org?.canAdminImpersonate]);
const table = useReactTable({
data: flatData,
columns: memorisedColumns,
enableRowSelection: true,
- debugTable: true,
manualPagination: true,
+ state: {
+ rowSelection,
+ },
initialState: {
columnVisibility: initalColumnVisibility,
columnPinning: {
@@ -448,9 +445,6 @@ function UserListTableContent() {
defaultColumn: {
size: 150,
},
- state: {
- rowSelection,
- },
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
onRowSelectionChange: setRowSelection,
@@ -489,6 +483,8 @@ function UserListTableContent() {
},
});
+ const utils = trpc.useUtils();
+
const numberOfSelectedRows = table.getSelectedRowModel().rows.length;
const handleDownload = async () => {
@@ -502,30 +498,35 @@ function UserListTableContent() {
throw new Error("Header is missing.");
}
- const result = await exportQuery.refetch();
- if (!result.data) {
- throw new Error("There are no members found.");
- }
- const allMembers = result.data.pages.flatMap((page) => page.rows ?? []) ?? [];
- let lastPage = result.data.pages[result.data.pages.length - 1];
+ // Fetch all pages
+ let allRows: UserTableUser[] = [];
+ let offset: number | undefined = 0;
+ const limit = 100;
- while (lastPage.nextCursor) {
- const nextPage = await exportQuery.fetchNextPage();
- if (!nextPage.data) {
- break;
+ while (offset !== undefined) {
+ const result = await utils.viewer.organizations.listMembers.fetch({
+ limit,
+ offset,
+ searchTerm: debouncedSearchTerm,
+ expand: ["attributes"],
+ filters: columnFilters,
+ });
+
+ if (!result.rows?.length) {
+ offset = undefined;
+ continue;
}
- const latestPageItems = nextPage.data.pages[nextPage.data.pages.length - 1].rows ?? [];
- allMembers.push(...latestPageItems);
- lastPage = nextPage.data.pages[nextPage.data.pages.length - 1];
+
+ allRows = [...allRows, ...result.rows];
+ offset = offset + limit;
+ }
+
+ if (!allRows.length) {
+ throw new Error("There are no members found.");
}
const ATTRIBUTE_IDS = attributes?.map((attr) => attr.id) ?? [];
- const csvRaw = generateCsvRawForMembersTable(
- headers,
- allMembers as UserTableUser[],
- ATTRIBUTE_IDS,
- domain
- );
+ const csvRaw = generateCsvRawForMembersTable(headers, allRows, ATTRIBUTE_IDS, domain);
if (!csvRaw) {
throw new Error("Generating CSV file failed.");
}
@@ -550,10 +551,8 @@ function UserListTableContent() {
testId="user-list-data-table"
table={table}
isPending={isPending}
- hasNextPage={hasNextPage}
- fetchNextPage={fetchNextPage}
- isFetching={isFetching}
- totalDBRowCount={totalDBRowCount}
+ totalRowCount={data?.meta?.totalRowCount}
+ paginationMode="standard"
ToolbarLeft={
<>
setDebouncedSearchTerm(value)} />
diff --git a/packages/features/users/components/UserTable/types.ts b/packages/features/users/components/UserTable/types.ts
index cd92f7347d..4c0d27329a 100644
--- a/packages/features/users/components/UserTable/types.ts
+++ b/packages/features/users/components/UserTable/types.ts
@@ -1,30 +1,6 @@
-import type { MembershipRole } from "@calcom/prisma/enums";
+import type { RouterOutputs } from "@calcom/trpc";
-export interface UserTableUser {
- id: number;
- username: string | null;
- email: string;
- timeZone: string;
- role: MembershipRole;
- avatarUrl: string | null;
- accepted: boolean;
- disableImpersonation: boolean;
- completedOnboarding: boolean;
- lastActiveAt: string;
- teams: {
- id: number;
- name: string;
- slug: string | null;
- }[];
- attributes: {
- id: string;
- attributeId: string;
- value: string;
- slug: string;
- weight?: number | null;
- contains: string[];
- }[];
-}
+export type UserTableUser = RouterOutputs["viewer"]["organizations"]["listMembers"]["rows"][number];
export type PlatformManagedUserTableUser = Omit<
UserTableUser,
diff --git a/packages/lib/csvUtils.ts b/packages/lib/csvUtils.ts
index 529a4dbd70..2238bb4674 100644
--- a/packages/lib/csvUtils.ts
+++ b/packages/lib/csvUtils.ts
@@ -117,7 +117,7 @@ export const generateCsvRawForMembersTable = (
const { email, role, teams, username, attributes } = row;
// Create a map of attributeId to array of values
- const attributeMap = attributes.reduce((acc, attr) => {
+ const attributeMap = (attributes ?? []).reduce((acc, attr) => {
if (!acc[attr.attributeId]) {
acc[attr.attributeId] = [];
}
diff --git a/packages/trpc/server/routers/viewer/organizations/_router.tsx b/packages/trpc/server/routers/viewer/organizations/_router.tsx
index 94c6e0d470..0781a51074 100644
--- a/packages/trpc/server/routers/viewer/organizations/_router.tsx
+++ b/packages/trpc/server/routers/viewer/organizations/_router.tsx
@@ -20,7 +20,7 @@ import { ZDeleteTeamInputSchema } from "./deleteTeam.schema";
import { ZGetMembersInput } from "./getMembers.schema";
import { ZGetOtherTeamInputSchema } from "./getOtherTeam.handler";
import { ZGetUserInput } from "./getUser.schema";
-import { ZListMembersSchema } from "./listMembers.schema";
+import { ZListMembersInputSchema } from "./listMembers.schema";
import { ZListOtherTeamMembersSchema } from "./listOtherTeamMembers.handler";
import { ZRemoveHostsFromEventTypes } from "./removeHostsFromEventTypes.schema";
import { ZSetPasswordSchema } from "./setPassword.schema";
@@ -71,7 +71,7 @@ export const viewerOrganizationsRouter = router({
const handler = await importHandler(namespaced("getMembers"), () => import("./getMembers.handler"));
return handler(opts);
}),
- listMembers: authedProcedure.input(ZListMembersSchema).query(async (opts) => {
+ listMembers: authedProcedure.input(ZListMembersInputSchema).query(async (opts) => {
const handler = await importHandler(namespaced("listMembers"), () => import("./listMembers.handler"));
return handler(opts);
}),
diff --git a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
index 92a9805f7d..7a15b188a6 100644
--- a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
+++ b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
@@ -75,7 +75,7 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
};
}
- const { cursor, limit } = input;
+ const { limit, offset } = input;
const getTotalMembers = await prisma.membership.count({
where: {
@@ -190,19 +190,13 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
},
},
},
- cursor: cursor ? { id: cursor } : undefined,
- take: limit + 1, // We take +1 as itll be used for the next cursor
+ skip: offset,
+ take: limit,
orderBy: {
id: "asc",
},
});
- let nextCursor: typeof cursor | undefined = undefined;
- if (teamMembers && teamMembers.length > limit) {
- const nextItem = teamMembers.pop();
- nextCursor = nextItem?.id;
- }
-
const members = await Promise.all(
teamMembers?.map(async (membership) => {
const user = await UserRepository.enrichUserWithItsProfile({ user: membership.user });
@@ -260,7 +254,8 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
name: team.team.name,
slug: team.team.slug,
};
- }),
+ })
+ .filter((team): team is NonNullable => team !== undefined),
attributes,
};
}) || []
@@ -268,7 +263,6 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
return {
rows: members || [],
- nextCursor,
meta: {
totalRowCount: getTotalMembers || 0,
},
diff --git a/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts b/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts
index 8dee8925a7..b30f71b3bc 100644
--- a/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts
+++ b/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts
@@ -9,13 +9,13 @@ const ZListMembersFilter = z.object({
value: ZFilterValue,
});
-export const ZListMembersSchema = z.object({
+export const ZListMembersInputSchema = z.object({
limit: z.number().min(1).max(100),
- cursor: z.number().nullish(),
+ offset: z.number(),
searchTerm: z.string().optional(),
expand: z.array(expandableColumns).optional(),
filters: z.array(ZListMembersFilter).optional(),
oAuthClientId: z.string().optional(),
});
-export type TListMembersSchema = z.infer;
+export type TListMembersSchema = z.infer;
diff --git a/packages/ui/components/pagination/Pagination.tsx b/packages/ui/components/pagination/Pagination.tsx
index bf115b0846..ca99447d57 100644
--- a/packages/ui/components/pagination/Pagination.tsx
+++ b/packages/ui/components/pagination/Pagination.tsx
@@ -2,6 +2,8 @@
import { useState } from "react";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+
import { Button } from "../button";
import { ButtonGroup } from "../buttonGroup";
import { Select } from "../form/select";
@@ -29,6 +31,7 @@ export const Pagination = ({
onNext,
onPrevious,
}: PaginationProps) => {
+ const { t } = useLocale();
const [internalPageSize, setInternalPageSize] = useState(pageSize);
const totalPages = Math.ceil(totalItems / pageSize);
@@ -73,11 +76,14 @@ export const Pagination = ({
onChange={handlePageSizeChange}
size="sm"
/>
- rows per page
+ {t("rows_per_page")}
- {startItem}-{endItem} of {totalItems}
+ {t("pagination_status", {
+ currentRange: `${startItem}-${endItem}`,
+ totalItems,
+ })}