fix: attribute filters on organization member list (#20380)
* fix: attribute filters on organization member list * fix total count * add tests --------- Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
1 parent
cd952dc93c
commit
88961a1c67
4 files changed
+207
-63
No files matched your search
@@ -10,9 +10,8 @@ import {
|
||||
} from "@calcom/app-store/routing-forms/lib/FieldTypes";
|
||||
import { zodFields as routingFormFieldsSchema } from "@calcom/app-store/routing-forms/zod";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { ColumnFilterType } from "@calcom/features/data-table";
|
||||
import { makeWhereClause, makeOrderBy } from "@calcom/features/data-table/lib/server";
|
||||
import type { TypedColumnFilter } from "@calcom/features/data-table/lib/types";
|
||||
import { type TypedColumnFilter, ColumnFilterType } from "@calcom/features/data-table/lib/types";
|
||||
import type {
|
||||
RoutingFormResponsesInput,
|
||||
RoutingFormStatsInput,
|
||||
|
||||
@@ -305,6 +305,9 @@ function UserListTableContent() {
|
||||
accessorFn: (data) => data.role,
|
||||
header: "Role",
|
||||
size: 100,
|
||||
meta: {
|
||||
filter: { type: ColumnFilterType.MULTI_SELECT },
|
||||
},
|
||||
cell: ({ row, table }) => {
|
||||
const { role, username } = row.original;
|
||||
return (
|
||||
@@ -324,6 +327,9 @@ function UserListTableContent() {
|
||||
accessorFn: (data) => data.teams.map((team) => team.name),
|
||||
header: "Teams",
|
||||
size: 140,
|
||||
meta: {
|
||||
filter: { type: ColumnFilterType.MULTI_SELECT },
|
||||
},
|
||||
cell: ({ row, table }) => {
|
||||
const { teams, accepted, email, username } = row.original;
|
||||
// TODO: Implement click to filter
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { type TypedColumnFilter, ColumnFilterType } from "@calcom/features/data-table/lib/types";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { listMembersHandler } from "./listMembers.handler";
|
||||
|
||||
const prismaMock = {
|
||||
membership: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
count: vi.fn().mockResolvedValue(0),
|
||||
},
|
||||
attributeOption: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{ id: "1", value: "value1", isGroup: false },
|
||||
{ id: "2", value: "value2", isGroup: false },
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
vi.spyOn(prisma.membership, "findMany").mockImplementation(prismaMock.membership.findMany);
|
||||
vi.spyOn(prisma.membership, "count").mockImplementation(prismaMock.membership.count);
|
||||
vi.spyOn(prisma.attributeOption, "findMany").mockImplementation(prismaMock.attributeOption.findMany);
|
||||
|
||||
const ORGANIZATION_ID = 123;
|
||||
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
timeZone: "UTC",
|
||||
locale: "en",
|
||||
organizationId: ORGANIZATION_ID,
|
||||
organization: {
|
||||
id: ORGANIZATION_ID,
|
||||
isOrgAdmin: true,
|
||||
isPrivate: false,
|
||||
},
|
||||
profiles: [
|
||||
{
|
||||
id: 1,
|
||||
userId: 1,
|
||||
organizationId: ORGANIZATION_ID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe("listMembersHandler", () => {
|
||||
it("should combine multiple attribute filters with AND logic", async () => {
|
||||
const roleFilter: TypedColumnFilter<ColumnFilterType.MULTI_SELECT> = {
|
||||
id: "role",
|
||||
value: {
|
||||
type: ColumnFilterType.MULTI_SELECT,
|
||||
data: ["ADMIN"],
|
||||
},
|
||||
};
|
||||
|
||||
const teamFilter: TypedColumnFilter<ColumnFilterType.MULTI_SELECT> = {
|
||||
id: "teams",
|
||||
value: {
|
||||
type: ColumnFilterType.MULTI_SELECT,
|
||||
data: ["team1"],
|
||||
},
|
||||
};
|
||||
|
||||
const attributeFilter1: TypedColumnFilter<ColumnFilterType.MULTI_SELECT> = {
|
||||
id: "1",
|
||||
value: {
|
||||
type: ColumnFilterType.MULTI_SELECT,
|
||||
data: ["value1"],
|
||||
},
|
||||
};
|
||||
|
||||
const attributeFilter2: TypedColumnFilter<ColumnFilterType.MULTI_SELECT> = {
|
||||
id: "2",
|
||||
value: {
|
||||
type: ColumnFilterType.MULTI_SELECT,
|
||||
data: ["value2"],
|
||||
},
|
||||
};
|
||||
|
||||
await listMembersHandler({
|
||||
ctx: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
user: mockUser as any,
|
||||
},
|
||||
input: {
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
filters: [roleFilter, teamFilter, attributeFilter1, attributeFilter2],
|
||||
},
|
||||
});
|
||||
|
||||
expect(prisma.membership.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
role: {
|
||||
in: ["ADMIN"],
|
||||
},
|
||||
teamId: ORGANIZATION_ID,
|
||||
user: {
|
||||
isPlatformManaged: false,
|
||||
teams: {
|
||||
some: {
|
||||
team: {
|
||||
name: {
|
||||
in: ["team1"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AND: [
|
||||
{
|
||||
AttributeToUser: {
|
||||
some: {
|
||||
attributeOption: {
|
||||
attribute: { id: "1" },
|
||||
value: { in: ["value1"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
AttributeToUser: {
|
||||
some: {
|
||||
attributeOption: {
|
||||
attribute: { id: "2" },
|
||||
value: { in: ["value2"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { makeWhereClause } from "@calcom/features/data-table/lib/server";
|
||||
import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
|
||||
import { type TypedColumnFilter, ColumnFilterType } from "@calcom/features/data-table/lib/types";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
@@ -77,18 +77,26 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
|
||||
|
||||
const { limit, offset } = input;
|
||||
|
||||
const getTotalMembers = await prisma.membership.count({
|
||||
where: {
|
||||
user: {
|
||||
...getUserConditions(oAuthClientId),
|
||||
},
|
||||
teamId: organizationId,
|
||||
},
|
||||
});
|
||||
const roleFilter = filters.find((filter) => filter.id === "role") as
|
||||
| TypedColumnFilter<ColumnFilterType.MULTI_SELECT>
|
||||
| undefined;
|
||||
const teamFilter = filters.find((filter) => filter.id === "teams") as
|
||||
| TypedColumnFilter<ColumnFilterType.MULTI_SELECT>
|
||||
| undefined;
|
||||
|
||||
let whereClause: Prisma.MembershipWhereInput = {
|
||||
const whereClause: Prisma.MembershipWhereInput = {
|
||||
user: {
|
||||
...getUserConditions(oAuthClientId),
|
||||
...(teamFilter && {
|
||||
teams: {
|
||||
some: {
|
||||
team: makeWhereClause({
|
||||
columnName: "name",
|
||||
filterValue: teamFilter.value,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
teamId: organizationId,
|
||||
...(searchTerm && {
|
||||
@@ -99,65 +107,57 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
|
||||
],
|
||||
},
|
||||
}),
|
||||
...(roleFilter &&
|
||||
makeWhereClause({
|
||||
columnName: "role",
|
||||
filterValue: roleFilter.value,
|
||||
})),
|
||||
};
|
||||
|
||||
filters.forEach((filter) => {
|
||||
switch (filter.id) {
|
||||
case "role":
|
||||
whereClause = {
|
||||
...whereClause,
|
||||
...makeWhereClause({
|
||||
columnName: "role",
|
||||
filterValue: filter.value,
|
||||
}),
|
||||
};
|
||||
break;
|
||||
case "teams":
|
||||
whereClause.user = {
|
||||
teams: {
|
||||
some: {
|
||||
team: makeWhereClause({
|
||||
columnName: "name",
|
||||
filterValue: filter.value,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
break;
|
||||
// We assume that if the filter is not one of the above, it must be an attribute filter
|
||||
default:
|
||||
if (filter.value.type === ColumnFilterType.MULTI_SELECT && isAllString(filter.value.data)) {
|
||||
const attributeOptionValues: string[] = [];
|
||||
filter.value.data.forEach((filterValueItem) => {
|
||||
attributeOptionValues.push(filterValueItem);
|
||||
groupOptionsWithContainsOptionValues.forEach((groupOption) => {
|
||||
if (groupOption.contains.find(({ value: containValue }) => containValue === filterValueItem)) {
|
||||
attributeOptionValues.push(groupOption.value);
|
||||
}
|
||||
});
|
||||
const attributeFilters: Prisma.MembershipWhereInput["AttributeToUser"][] = filters
|
||||
.filter((filter) => filter.id !== "role" && filter.id !== "teams")
|
||||
.map((filter) => {
|
||||
if (filter.value.type === ColumnFilterType.MULTI_SELECT && isAllString(filter.value.data)) {
|
||||
const attributeOptionValues: string[] = [];
|
||||
filter.value.data.forEach((filterValueItem) => {
|
||||
attributeOptionValues.push(filterValueItem);
|
||||
groupOptionsWithContainsOptionValues.forEach((groupOption) => {
|
||||
if (groupOption.contains.find(({ value: containValue }) => containValue === filterValueItem)) {
|
||||
attributeOptionValues.push(groupOption.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
filter.value.data = attributeOptionValues;
|
||||
}
|
||||
filter.value.data = attributeOptionValues;
|
||||
}
|
||||
|
||||
whereClause.AttributeToUser = {
|
||||
some: {
|
||||
attributeOption: {
|
||||
attribute: {
|
||||
id: filter.id,
|
||||
},
|
||||
...makeWhereClause({
|
||||
columnName: "value",
|
||||
filterValue: filter.value,
|
||||
}),
|
||||
return {
|
||||
some: {
|
||||
attributeOption: {
|
||||
attribute: {
|
||||
id: filter.id,
|
||||
},
|
||||
...makeWhereClause({
|
||||
columnName: "value",
|
||||
filterValue: filter.value,
|
||||
}),
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// If we have attribute filters, add them to the where clause with AND
|
||||
if (attributeFilters.length > 0) {
|
||||
whereClause.AND = attributeFilters.map((filter) => ({
|
||||
AttributeToUser: filter,
|
||||
}));
|
||||
}
|
||||
|
||||
const totalCountPromise = prisma.membership.count({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
const teamMembers = await prisma.membership.findMany({
|
||||
const teamMembersPromise = prisma.membership.findMany({
|
||||
where: whereClause,
|
||||
select: {
|
||||
id: true,
|
||||
@@ -200,6 +200,8 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
|
||||
},
|
||||
});
|
||||
|
||||
const [totalCount, teamMembers] = await Promise.all([totalCountPromise, teamMembersPromise]);
|
||||
|
||||
const members = await Promise.all(
|
||||
teamMembers?.map(async (membership) => {
|
||||
const user = await UserRepository.enrichUserWithItsProfile({ user: membership.user });
|
||||
@@ -267,7 +269,7 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
|
||||
return {
|
||||
rows: members || [],
|
||||
meta: {
|
||||
totalRowCount: getTotalMembers || 0,
|
||||
totalRowCount: totalCount || 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user