diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx
index 3afa82789d..6e0427780b 100644
--- a/apps/web/pages/event-types/index.tsx
+++ b/apps/web/pages/event-types/index.tsx
@@ -13,7 +13,8 @@ import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
-import { OrganizationEventTypeFilter } from "@calcom/features/eventtypes/components/OrganizationEventTypeFilter";
+import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
+import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import Shell from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -47,7 +48,6 @@ import {
HeadSeo,
Skeleton,
Label,
- VerticalDivider,
Alert,
} from "@calcom/ui";
import {
@@ -800,8 +800,7 @@ const CTA = () => {
const Actions = () => {
return (
-
-
+
);
};
@@ -834,9 +833,6 @@ const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
);
};
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer as any);
-
const EventTypesPage = () => {
const { t } = useLocale();
const router = useRouter();
@@ -862,6 +858,10 @@ const EventTypesPage = () => {
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+ const filters = getTeamsFiltersFromQuery(router.query);
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer as any, { filters });
return (
@@ -879,47 +879,55 @@ const EventTypesPage = () => {
CTA={
}>
}
- success={({ data }) => (
- <>
- {data.eventTypeGroups.length > 1 ? (
- <>
- {isMobile ? (
-
- ) : (
- data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
-
-
+ success={({ data }) => {
+ const isFilteredByOnlyOneItem =
+ (filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) &&
+ data.eventTypeGroups.length === 1;
-
-
- ))
- )}
- >
- ) : data.eventTypeGroups.length === 1 ? (
-
- ) : (
-
- )}
+ return (
+ <>
+ {data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
+ <>
+ {isMobile ? (
+
+ ) : (
+ data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
+
+
-
- {router.query.dialog === "duplicate" && }
- >
- )}
+
+
+ ))
+ )}
+ >
+ ) : (
+ data.eventTypeGroups.length === 1 && (
+
+ )
+ )}
+
+ {data.eventTypeGroups.length === 0 &&
}
+
+
+ {router.query.dialog === "duplicate" &&
}
+ >
+ );
+ }}
/>
diff --git a/packages/features/eventtypes/components/OrganizationEventTypeFilter.tsx b/packages/features/eventtypes/components/OrganizationEventTypeFilter.tsx
deleted file mode 100644
index 7a8a6b6c27..0000000000
--- a/packages/features/eventtypes/components/OrganizationEventTypeFilter.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import { useSession } from "next-auth/react";
-import type { ReactNode, InputHTMLAttributes } from "react";
-import { useState, forwardRef, Fragment } from "react";
-
-import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
-import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
-import { useLocale } from "@calcom/lib/hooks/useLocale";
-import type { RouterOutputs } from "@calcom/trpc/react";
-import { trpc } from "@calcom/trpc/react";
-import { AnimatedPopover, Avatar } from "@calcom/ui";
-import { Layers, User } from "@calcom/ui/components/icon";
-
-export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
-export type IEventTypeFilter = IEventTypesFilters[0];
-
-export const OrganizationEventTypeFilter = () => {
- const { t } = useLocale();
- const session = useSession();
- const { data: query, pushItemToKey, removeItemByKeyAndValue, removeAllQueryParams } = useFilterQuery();
- const [dropdownTitle, setDropdownTitle] = useState(t("all_apps"));
-
- const { data: teams, status } = trpc.viewer.teams.list.useQuery();
- const isNotEmpty = !!teams?.length;
-
- return status === "success" ? (
-
-
- }
- checked={dropdownTitle === t("all_apps")}
- onChange={(e) => {
- removeAllQueryParams();
- setDropdownTitle(t("all_apps"));
- // TODO: What to do when all event types is unchecked
- }}
- label={t("all_apps")}
- />
-
-
- }
- checked={query.userIds?.includes(session.data?.user.id || 0)}
- onChange={(e) => {
- setDropdownTitle(t("yours"));
- if (e.target.checked) {
- pushItemToKey("userIds", session.data?.user.id || 0);
- } else if (!e.target.checked) {
- removeItemByKeyAndValue("userIds", session.data?.user.id || 0);
- }
- }}
- label={t("yours")}
- />
-
-
- {isNotEmpty && (
-
- TEAMS
- {teams?.map((team) => (
-
-
- }
- checked={query.teamIds?.includes(team.id)}
- onChange={(e) => {
- setDropdownTitle(team.name);
- if (e.target.checked) {
- pushItemToKey("teamIds", team.id);
- } else if (!e.target.checked) {
- removeItemByKeyAndValue("teamIds", team.id);
- }
- }}
- />
-
- ))}
-
- )}
-
- ) : null;
-};
-
-type Props = InputHTMLAttributes & {
- label: string;
- icon: ReactNode;
-};
-
-const CheckboxField = forwardRef(({ label, icon, ...rest }, ref) => {
- return (
-
- );
-});
-
-const CheckboxFieldContainer = ({ children }: { children: ReactNode }) => {
- return {children}
;
-};
-
-CheckboxField.displayName = "CheckboxField";
diff --git a/packages/features/filters/components/TeamsFilter.tsx b/packages/features/filters/components/TeamsFilter.tsx
index 76ff1b4dd2..6e070c50fd 100644
--- a/packages/features/filters/components/TeamsFilter.tsx
+++ b/packages/features/filters/components/TeamsFilter.tsx
@@ -7,7 +7,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
-import { AnimatedPopover, Avatar, Divider } from "@calcom/ui";
+import { AnimatedPopover, Avatar, Divider, VerticalDivider } from "@calcom/ui";
import { Layers, User } from "@calcom/ui/components/icon";
import { filterQuerySchema } from "../lib/getTeamsFiltersFromQuery";
@@ -20,7 +20,13 @@ function useFilterQuery() {
return useTypedQuery(filterQuerySchema.passthrough());
}
-export const TeamsFilter = () => {
+export const TeamsFilter = ({
+ popoverTriggerClassNames,
+ showVerticalDivider = false,
+}: {
+ popoverTriggerClassNames?: string;
+ showVerticalDivider?: boolean;
+}) => {
const { t } = useLocale();
const session = useSession();
const { data: query, pushItemToKey, removeItemByKeyAndValue, removeAllQueryParams } = useFilterQuery();
@@ -40,7 +46,7 @@ export const TeamsFilter = () => {
if (selectedTeamsNames) {
checkedOptions.push(...selectedTeamsNames);
}
- return checkedOptions.join(",");
+ return `${t("team")}: ${checkedOptions.join(",")}`;
}
if (query.userIds) {
return t("yours");
@@ -51,56 +57,59 @@ export const TeamsFilter = () => {
if (!teams || !teams.length) return null;
return (
-
-
- }
- checked={!query.teamIds && !query.userIds?.includes(session.data?.user.id || 0)}
- onChange={(e) => {
- removeAllQueryParams();
- }}
- label={t("all_apps")}
- />
-
- }
- checked={!!query.userIds?.includes(session.data?.user.id || 0)}
- onChange={(e) => {
- if (e.target.checked) {
- pushItemToKey("userIds", session.data?.user.id || 0);
- } else if (!e.target.checked) {
- removeItemByKeyAndValue("userIds", session.data?.user.id || 0);
- }
- }}
- label={t("yours")}
- />
-
- {teams?.map((team) => (
+
+
+
}
+ checked={!query.teamIds && !query.userIds?.includes(session.data?.user.id || 0)}
+ onChange={(e) => {
+ removeAllQueryParams();
+ }}
+ label={t("all_apps")}
+ />
+
+ }
+ checked={!!query.userIds?.includes(session.data?.user.id || 0)}
onChange={(e) => {
if (e.target.checked) {
- pushItemToKey("teamIds", team.id);
+ pushItemToKey("userIds", session.data?.user.id || 0);
} else if (!e.target.checked) {
- removeItemByKeyAndValue("teamIds", team.id);
+ removeItemByKeyAndValue("userIds", session.data?.user.id || 0);
}
}}
- icon={
-
- }
+ label={t("yours")}
/>
- ))}
-
-
+
+ {teams?.map((team) => (
+
{
+ if (e.target.checked) {
+ pushItemToKey("teamIds", team.id);
+ } else if (!e.target.checked) {
+ removeItemByKeyAndValue("teamIds", team.id);
+ }
+ }}
+ icon={
+
+ }
+ />
+ ))}
+
+
+ {showVerticalDivider && }
+
);
};
diff --git a/packages/trpc/server/routers/viewer/eventTypes/_router.ts b/packages/trpc/server/routers/viewer/eventTypes/_router.ts
index 2e57337e94..d85d6f032a 100644
--- a/packages/trpc/server/routers/viewer/eventTypes/_router.ts
+++ b/packages/trpc/server/routers/viewer/eventTypes/_router.ts
@@ -8,6 +8,7 @@ import { ZCreateInputSchema } from "./create.schema";
import { ZDeleteInputSchema } from "./delete.schema";
import { ZDuplicateInputSchema } from "./duplicate.schema";
import { ZGetInputSchema } from "./get.schema";
+import { ZEventTypeInputSchema } from "./getByViewer.schema";
import { ZUpdateInputSchema } from "./update.schema";
import { eventOwnerProcedure } from "./util";
@@ -28,7 +29,7 @@ const UNSTABLE_HANDLER_CACHE: BookingsRouterHandlerCache = {};
export const eventTypesRouter = router({
// REVIEW: What should we name this procedure?
- getByViewer: authedProcedure.query(async ({ ctx }) => {
+ getByViewer: authedProcedure.input(ZEventTypeInputSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.getByViewer) {
UNSTABLE_HANDLER_CACHE.getByViewer = await import("./getByViewer.handler").then(
(mod) => mod.getByViewerHandler
@@ -44,6 +45,7 @@ export const eventTypesRouter = router({
const result = await UNSTABLE_HANDLER_CACHE.getByViewer({
ctx,
+ input,
});
timer();
diff --git a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts
index 2e1bd0953c..9e1436a7f5 100644
--- a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts
+++ b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts
@@ -1,6 +1,7 @@
import { type PrismaClient, Prisma } from "@prisma/client";
import { orderBy } from "lodash";
+import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import { CAL_URL } from "@calcom/lib/constants";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { baseEventTypeSelect, baseUserSelect } from "@calcom/prisma";
@@ -11,53 +12,56 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../trpc";
+import type { TEventTypeInputSchema } from "./getByViewer.schema";
type GetByViewerOptions = {
ctx: {
user: NonNullable;
prisma: PrismaClient;
};
+ input: TEventTypeInputSchema;
};
-export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
+const eventTypeSelect = Prisma.validator()({
+ // Position is required by lodash to sort on it. Don't remove it, TS won't complain but it would silently break reordering
+ position: true,
+ hashedLink: true,
+ locations: true,
+ destinationCalendar: true,
+ userId: true,
+ team: {
+ select: {
+ id: true,
+ name: true,
+ slug: true,
+ // logo: true, // Skipping to avoid 4mb limit
+ bio: true,
+ hideBranding: true,
+ },
+ },
+ metadata: true,
+ users: {
+ select: baseUserSelect,
+ },
+ children: {
+ include: {
+ users: true,
+ },
+ },
+ parentId: true,
+ hosts: {
+ select: {
+ user: {
+ select: baseUserSelect,
+ },
+ },
+ },
+ seatsPerTimeSlot: true,
+ ...baseEventTypeSelect,
+});
+
+export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) => {
const { prisma } = ctx;
- const eventTypeSelect = Prisma.validator()({
- // Position is required by lodash to sort on it. Don't remove it, TS won't complain but it would silently break reordering
- position: true,
- hashedLink: true,
- locations: true,
- destinationCalendar: true,
- userId: true,
- team: {
- select: {
- id: true,
- name: true,
- slug: true,
- // logo: true, // Skipping to avoid 4mb limit
- bio: true,
- hideBranding: true,
- },
- },
- metadata: true,
- users: {
- select: baseUserSelect,
- },
- children: {
- include: {
- users: true,
- },
- },
- parentId: true,
- hosts: {
- select: {
- user: {
- select: baseUserSelect,
- },
- },
- },
- seatsPerTimeSlot: true,
- ...baseEventTypeSelect,
- });
const user = await prisma.user.findUnique({
where: {
@@ -107,6 +111,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
eventTypes: {
where: {
team: null,
+ userId: getPrismaWhereUserIdFromFilter(ctx.user.id, input?.filters),
},
select: eventTypeSelect,
orderBy: [
@@ -137,7 +142,8 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
const typesRaw = (
await prisma.eventType.findMany({
where: {
- userId: ctx.user.id,
+ userId: getPrismaWhereUserIdFromFilter(ctx.user.id, input?.filters),
+ team: null,
},
select: eventTypeSelect,
orderBy: [
@@ -167,11 +173,13 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
};
let eventTypeGroups: EventTypeGroup[] = [];
+
const eventTypesHashMap = userEventTypes.concat(typesRaw).reduce((hashMap, newItem) => {
const oldItem = hashMap[newItem.id];
hashMap[newItem.id] = { ...oldItem, ...newItem };
return hashMap;
}, {} as Record);
+
const mergedEventTypes = Object.values(eventTypesHashMap)
.map((eventType) => eventType)
.filter((evType) => evType.schedulingType !== SchedulingType.MANAGED);
@@ -204,6 +212,13 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
return mshipToNumber(mship1) > mshipToNumber(mship2);
};
+ const filterTeamsEventTypesBasedOnInput = (eventType: ReturnType) => {
+ if (!input?.filters || !hasFilter(input?.filters)) {
+ return true;
+ }
+ return input?.filters?.teamIds?.includes(eventType?.team?.id || 0) ?? false;
+ };
+
eventTypeGroups = ([] as EventTypeGroup[]).concat(
eventTypeGroups,
user.teams
@@ -242,6 +257,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
},
eventTypes: membership.team.eventTypes
.map(mapEventType)
+ .filter(filterTeamsEventTypesBasedOnInput)
.filter((evType) => evType.userId === null || evType.userId === ctx.user.id)
.filter((evType) =>
membership.role === MembershipRole.MEMBER
@@ -251,6 +267,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
};
})
);
+
return {
// don't display event teams without event types,
eventTypeGroups: eventTypeGroups.filter((groupBy) => !!groupBy.eventTypes?.length),
@@ -264,3 +281,15 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
})),
};
};
+
+export function getPrismaWhereUserIdFromFilter(
+ userId: number,
+ filters: NonNullable["filters"]
+) {
+ if (!filters || !hasFilter(filters)) {
+ return userId;
+ } else if (filters.userIds?.[0] === userId) {
+ return userId;
+ }
+ return null;
+}
diff --git a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts
index cb0ff5c3b5..1d3dd6f41b 100644
--- a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts
+++ b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts
@@ -1 +1,15 @@
-export {};
+import { z } from "zod";
+
+export const filterQuerySchemaStrict = z.object({
+ teamIds: z.number().array().optional(),
+ // A user can only filter by only his userId
+ userIds: z.number().array().max(1).optional(),
+});
+
+export const ZEventTypeInputSchema = z
+ .object({
+ filters: filterQuerySchemaStrict.optional(),
+ })
+ .nullish();
+
+export type TEventTypeInputSchema = z.infer;