fix: eventTypeId filter on Download at /insights (#20463)

* fix: eventTypeId filter on Download at /insights

* show toast for error
This commit is contained in:
Eunjae Lee
2025-03-31 22:00:15 +00:00
committed by GitHub
parent 93621baea5
commit f039e116d7
3 changed files with 182 additions and 136 deletions
@@ -1,41 +1,46 @@
import { useState } from "react";
import { downloadAsCsv } from "@calcom/lib/csvUtils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc";
import { trpc } from "@calcom/trpc";
import { Dropdown, DropdownItem, DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui/components/dropdown";
import { Button } from "@calcom/ui/components/button";
import {
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@calcom/ui/components/dropdown";
import { showToast } from "@calcom/ui/components/toast";
import { useInsightsParameters } from "../../hooks/useInsightsParameters";
const Download = () => {
const { t } = useLocale();
const { startDate, endDate, teamId, userId, eventTypeId, memberUserId } = useInsightsParameters();
const { startDate, endDate, teamId, userId, eventTypeId, memberUserId, isAll } = useInsightsParameters();
const [isDownloading, setIsDownloading] = useState(false);
const utils = trpc.useUtils();
const { data, isPending } = trpc.viewer.insights.rawData.useQuery(
{
startDate,
endDate,
teamId,
userId,
eventTypeId,
memberUserId,
},
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
staleTime: Infinity,
trpc: {
context: { skipBatch: true },
},
const handleDownloadClick = async () => {
try {
setIsDownloading(true);
const data = await utils.viewer.insights.rawData.fetch({
startDate,
endDate,
teamId,
userId,
eventTypeId,
memberUserId,
isAll,
});
if (!data) return;
const { data: csvRaw, filename } = data;
downloadAsCsv(csvRaw, filename);
} catch (error) {
showToast(t("unexpected_error_try_again"), "error");
} finally {
setIsDownloading(false);
}
);
type RawData = RouterOutputs["viewer"]["insights"]["rawData"] | undefined;
const handleDownloadClick = async (data: RawData) => {
if (!data) return;
const { data: csvRaw, filename } = data;
downloadAsCsv(csvRaw, filename);
};
return (
@@ -44,13 +49,13 @@ const Download = () => {
<Button
EndIcon="file-down"
color="secondary"
{...(isPending && { loading: isPending })}
loading={isDownloading}
className="h-full self-end sm:self-baseline">
{t("download")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownItem onClick={() => handleDownloadClick(data)}>{t("as_csv")}</DropdownItem>
<DropdownItem onClick={handleDownloadClick}>{t("as_csv")}</DropdownItem>
</DropdownMenuContent>
</Dropdown>
);
+6 -11
View File
@@ -381,7 +381,7 @@ class EventsInsights {
}
) => {
// Obtain the where conditional
const whereConditional = await this.obtainWhereConditional(props);
const whereConditional = await this.obtainWhereConditionalForDownload(props);
const csvData = await prisma.bookingTimeStatus.findMany({
select: {
@@ -452,7 +452,9 @@ class EventsInsights {
};
/*
* This is meant to be used for all functions inside insights router, ideally we should have a view that have all of this data
* This is meant to be used for all functions inside insights router,
* but it's currently used only for CSV download.
* Ideally we should have a view that have all of this data
* The order where will be from the most specific to the least specific
* starting from the top will be:
* - memberUserId
@@ -466,7 +468,7 @@ class EventsInsights {
* @param props
* @returns
*/
static obtainWhereConditional = async (
static obtainWhereConditionalForDownload = async (
props: RawDataInput & { organizationId: number | null; isOrgAdminOrOwner: boolean | null }
) => {
const {
@@ -492,14 +494,7 @@ class EventsInsights {
}
if (eventTypeId) {
whereConditional["OR"] = [
{
eventTypeId,
},
{
eventParentId: eventTypeId,
},
];
whereConditional["eventTypeId"] = eventTypeId;
}
if (memberUserId) {
whereConditional["userId"] = memberUserId;
+142 -96
View File
@@ -1080,103 +1080,19 @@ export const insightsRouter = router({
const { prisma, user } = ctx;
const { teamId, userId, isAll } = input;
if (!teamId && !userId) {
return [];
}
const membershipWhereConditional: Prisma.MembershipWhereInput = {};
let childrenTeamIds: number[] = [];
if (isAll && teamId && user.organizationId && user.isOwnerAdminOfParentTeam) {
const childTeams = await prisma.team.findMany({
where: {
parentId: user.organizationId,
},
select: {
id: true,
},
});
if (childTeams.length > 0) {
childrenTeamIds = childTeams.map((team) => team.id);
}
membershipWhereConditional["teamId"] = {
in: [user.organizationId, ...childrenTeamIds],
};
}
if (teamId && !isAll) {
membershipWhereConditional["teamId"] = teamId;
membershipWhereConditional["userId"] = user.id;
}
if (userId) {
membershipWhereConditional["userId"] = userId;
}
// I'm not using unique here since when userId comes from input we should look for every
// event type that user owns
const membership = await prisma.membership.findFirst({
where: membershipWhereConditional,
});
if (!membership && !user.isOwnerAdminOfParentTeam) {
throw new Error("User is not part of a team/org");
}
const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {};
if (isAll && childrenTeamIds.length > 0 && user.organizationId && user.isOwnerAdminOfParentTeam) {
eventTypeWhereConditional["teamId"] = {
in: [user.organizationId, ...childrenTeamIds],
};
}
if (teamId && !isAll) {
eventTypeWhereConditional["teamId"] = teamId;
}
if (userId) {
eventTypeWhereConditional["userId"] = userId;
}
let eventTypeResult: Prisma.EventTypeGetPayload<{
select: {
id: true;
slug: true;
teamId: true;
title: true;
team: {
select: {
name: true;
};
};
};
}>[] = [];
let isMember = membership?.role === "MEMBER";
if (user.isOwnerAdminOfParentTeam) {
isMember = false;
}
if (isMember) {
eventTypeWhereConditional["OR"] = [
{ userId: user.id },
{ users: { some: { id: user.id } } },
// @TODO this is not working as expected
// hosts: { some: { id: user.id } },
];
}
eventTypeResult = await prisma.eventType.findMany({
select: {
id: true,
slug: true,
title: true,
teamId: true,
team: {
select: {
name: true,
},
},
const eventTypeList = await getEventTypeList({
prisma,
teamId,
userId,
isAll,
user: {
id: user.id,
organizationId: user.organizationId,
isOwnerAdminOfParentTeam: user.isOwnerAdminOfParentTeam,
},
where: eventTypeWhereConditional,
});
return eventTypeResult;
return eventTypeList;
}),
recentRatings: userBelongsToTeamProcedure.input(rawDataInputSchema).query(async ({ ctx, input }) => {
const { teamId, startDate, endDate, eventTypeId, isAll, userId, memberUserId } = input;
@@ -1534,7 +1450,22 @@ export const insightsRouter = router({
return result;
}),
rawData: userBelongsToTeamProcedure.input(rawDataInputSchema).query(async ({ ctx, input }) => {
const { startDate, endDate, teamId, userId, memberUserId, isAll, eventTypeId } = input;
const { startDate, endDate, teamId, userId, memberUserId, isAll } = input;
const eventTypeList = await getEventTypeList({
prisma: ctx.prisma,
teamId,
userId,
isAll,
user: {
id: ctx.user.id,
organizationId: ctx.user.organizationId,
isOwnerAdminOfParentTeam: ctx.user.isOwnerAdminOfParentTeam,
},
});
// use eventTypeId filter only if it's accessible by this user
const eventTypeId = eventTypeList.find((eventType) => eventType.id === input.eventTypeId)?.id;
const isOrgAdminOrOwner = ctx.user.isOwnerAdminOfParentTeam;
try {
@@ -1560,7 +1491,6 @@ export const insightsRouter = router({
} catch (e) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
return { data: "", filename: "" };
}),
getRoutingFormsForFilters: userBelongsToTeamProcedure
@@ -1758,3 +1688,119 @@ export const insightsRouter = router({
}
}),
});
async function getEventTypeList({
prisma,
teamId,
userId,
isAll,
user,
}: {
prisma: typeof readonlyPrisma;
teamId: number | null | undefined;
userId: number | null | undefined;
isAll: boolean | undefined;
user: {
id: number;
organizationId: number | null;
isOwnerAdminOfParentTeam: boolean;
};
}) {
if (!teamId && !userId) {
return [];
}
const membershipWhereConditional: Prisma.MembershipWhereInput = {};
let childrenTeamIds: number[] = [];
if (isAll && teamId && user.organizationId && user.isOwnerAdminOfParentTeam) {
const childTeams = await prisma.team.findMany({
where: {
parentId: user.organizationId,
},
select: {
id: true,
},
});
if (childTeams.length > 0) {
childrenTeamIds = childTeams.map((team) => team.id);
}
membershipWhereConditional["teamId"] = {
in: [user.organizationId, ...childrenTeamIds],
};
}
if (teamId && !isAll) {
membershipWhereConditional["teamId"] = teamId;
membershipWhereConditional["userId"] = user.id;
}
if (userId) {
membershipWhereConditional["userId"] = userId;
}
// I'm not using unique here since when userId comes from input we should look for every
// event type that user owns
const membership = await prisma.membership.findFirst({
where: membershipWhereConditional,
});
if (!membership && !user.isOwnerAdminOfParentTeam) {
throw new Error("User is not part of a team/org");
}
const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {};
if (isAll && childrenTeamIds.length > 0 && user.organizationId && user.isOwnerAdminOfParentTeam) {
eventTypeWhereConditional["teamId"] = {
in: [user.organizationId, ...childrenTeamIds],
};
}
if (teamId && !isAll) {
eventTypeWhereConditional["teamId"] = teamId;
}
if (userId) {
eventTypeWhereConditional["userId"] = userId;
}
let eventTypeResult: Prisma.EventTypeGetPayload<{
select: {
id: true;
slug: true;
teamId: true;
title: true;
team: {
select: {
name: true;
};
};
};
}>[] = [];
let isMember = membership?.role === "MEMBER";
if (user.isOwnerAdminOfParentTeam) {
isMember = false;
}
if (isMember) {
eventTypeWhereConditional["OR"] = [
{ userId: user.id },
{ users: { some: { id: user.id } } },
// @TODO this is not working as expected
// hosts: { some: { id: user.id } },
];
}
eventTypeResult = await prisma.eventType.findMany({
select: {
id: true,
slug: true,
title: true,
teamId: true,
team: {
select: {
name: true,
},
},
},
where: eventTypeWhereConditional,
});
return eventTypeResult;
}