feat: team filters (#9543)
* feat: team filers Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: type error Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: remove filter from mobile Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: show heading when one item selected Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> --------- Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
1 parent
4e529e5c6c
commit
8816a18854
6 files changed
+195
-251
No files matched your search
@@ -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 (
|
||||
<div className="hidden items-center md:flex">
|
||||
<OrganizationEventTypeFilter />
|
||||
<VerticalDivider />
|
||||
<TeamsFilter popoverTriggerClassNames="mb-0" showVerticalDivider={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<div>
|
||||
@@ -879,47 +879,55 @@ const EventTypesPage = () => {
|
||||
CTA={<CTA />}>
|
||||
<WithQuery
|
||||
customLoader={<SkeletonLoader />}
|
||||
success={({ data }) => (
|
||||
<>
|
||||
{data.eventTypeGroups.length > 1 ? (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
|
||||
) : (
|
||||
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
|
||||
<div className="flex flex-col" key={group.profile.slug}>
|
||||
<EventTypeListHeading
|
||||
profile={group.profile}
|
||||
membershipCount={group.metadata.membershipCount}
|
||||
teamId={group.teamId}
|
||||
orgSlug={orgBranding?.slug}
|
||||
/>
|
||||
success={({ data }) => {
|
||||
const isFilteredByOnlyOneItem =
|
||||
(filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) &&
|
||||
data.eventTypeGroups.length === 1;
|
||||
|
||||
<EventTypeList
|
||||
types={group.eventTypes}
|
||||
group={group}
|
||||
groupIndex={index}
|
||||
readOnly={group.metadata.readOnly}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
) : data.eventTypeGroups.length === 1 ? (
|
||||
<EventTypeList
|
||||
types={data.eventTypeGroups[0].eventTypes}
|
||||
group={data.eventTypeGroups[0]}
|
||||
groupIndex={0}
|
||||
readOnly={data.eventTypeGroups[0].metadata.readOnly}
|
||||
/>
|
||||
) : (
|
||||
<CreateFirstEventTypeView />
|
||||
)}
|
||||
return (
|
||||
<>
|
||||
{data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
|
||||
) : (
|
||||
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
|
||||
<div className="flex flex-col" key={group.profile.slug}>
|
||||
<EventTypeListHeading
|
||||
profile={group.profile}
|
||||
membershipCount={group.metadata.membershipCount}
|
||||
teamId={group.teamId}
|
||||
orgSlug={orgBranding?.slug}
|
||||
/>
|
||||
|
||||
<EmbedDialog />
|
||||
{router.query.dialog === "duplicate" && <DuplicateDialog />}
|
||||
</>
|
||||
)}
|
||||
<EventTypeList
|
||||
types={group.eventTypes}
|
||||
group={group}
|
||||
groupIndex={index}
|
||||
readOnly={group.metadata.readOnly}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
data.eventTypeGroups.length === 1 && (
|
||||
<EventTypeList
|
||||
types={data.eventTypeGroups[0].eventTypes}
|
||||
group={data.eventTypeGroups[0]}
|
||||
groupIndex={0}
|
||||
readOnly={data.eventTypeGroups[0].metadata.readOnly}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
|
||||
|
||||
<EmbedDialog />
|
||||
{router.query.dialog === "duplicate" && <DuplicateDialog />}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Shell>
|
||||
</div>
|
||||
|
||||
@@ -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<string>(t("all_apps"));
|
||||
|
||||
const { data: teams, status } = trpc.viewer.teams.list.useQuery();
|
||||
const isNotEmpty = !!teams?.length;
|
||||
|
||||
return status === "success" ? (
|
||||
<AnimatedPopover text={dropdownTitle} popoverTriggerClassNames="!mb-0">
|
||||
<CheckboxFieldContainer>
|
||||
<CheckboxField
|
||||
id="all-eventtypes-checkbox"
|
||||
icon={<Layers className="h-4 w-4" />}
|
||||
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")}
|
||||
/>
|
||||
</CheckboxFieldContainer>
|
||||
<CheckboxFieldContainer>
|
||||
<CheckboxField
|
||||
id="all-eventtypes-checkbox"
|
||||
icon={<User className="h-4 w-4" />}
|
||||
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")}
|
||||
/>
|
||||
</CheckboxFieldContainer>
|
||||
|
||||
{isNotEmpty && (
|
||||
<Fragment>
|
||||
<div className="text-subtle px-4 py-2.5 text-xs font-medium uppercase leading-none">TEAMS</div>
|
||||
{teams?.map((team) => (
|
||||
<CheckboxFieldContainer key={team.id}>
|
||||
<CheckboxField
|
||||
id={team.name}
|
||||
label={team.name}
|
||||
icon={
|
||||
<Avatar
|
||||
alt={team?.name}
|
||||
imageSrc={getPlaceholderAvatar(team.logo, team?.name as string)}
|
||||
size="xs"
|
||||
/>
|
||||
}
|
||||
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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</CheckboxFieldContainer>
|
||||
))}
|
||||
</Fragment>
|
||||
)}
|
||||
</AnimatedPopover>
|
||||
) : null;
|
||||
};
|
||||
|
||||
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
const CheckboxField = forwardRef<HTMLInputElement, Props>(({ label, icon, ...rest }, ref) => {
|
||||
return (
|
||||
<label className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="text-default flex h-6 w-6 items-center justify-center ltr:mr-2 rtl:ml-2">{icon}</div>
|
||||
<span className="text-sm">{label}</span>
|
||||
</div>
|
||||
<div className="flex h-5 items-center">
|
||||
<input
|
||||
{...rest}
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
className="text-primary-600 focus:ring-primary-500 border-default bg-default h-4 w-4 rounded hover:cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
});
|
||||
|
||||
const CheckboxFieldContainer = ({ children }: { children: ReactNode }) => {
|
||||
return <div className="flex items-center px-3 py-2">{children}</div>;
|
||||
};
|
||||
|
||||
CheckboxField.displayName = "CheckboxField";
|
||||
@@ -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 (
|
||||
<AnimatedPopover text={getCheckedOptionsNames()}>
|
||||
<FilterCheckboxFieldsContainer>
|
||||
<FilterCheckboxField
|
||||
id="all"
|
||||
icon={<Layers className="h-4 w-4" />}
|
||||
checked={!query.teamIds && !query.userIds?.includes(session.data?.user.id || 0)}
|
||||
onChange={(e) => {
|
||||
removeAllQueryParams();
|
||||
}}
|
||||
label={t("all_apps")}
|
||||
/>
|
||||
|
||||
<FilterCheckboxField
|
||||
id="yours"
|
||||
icon={<User className="h-4 w-4" />}
|
||||
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")}
|
||||
/>
|
||||
<Divider />
|
||||
{teams?.map((team) => (
|
||||
<div className="flex items-center">
|
||||
<AnimatedPopover text={getCheckedOptionsNames()} popoverTriggerClassNames={popoverTriggerClassNames}>
|
||||
<FilterCheckboxFieldsContainer>
|
||||
<FilterCheckboxField
|
||||
key={team.id}
|
||||
id={team.name}
|
||||
label={team.name}
|
||||
checked={!!query.teamIds?.includes(team.id)}
|
||||
id="all"
|
||||
icon={<Layers className="h-4 w-4" />}
|
||||
checked={!query.teamIds && !query.userIds?.includes(session.data?.user.id || 0)}
|
||||
onChange={(e) => {
|
||||
removeAllQueryParams();
|
||||
}}
|
||||
label={t("all_apps")}
|
||||
/>
|
||||
|
||||
<FilterCheckboxField
|
||||
id="yours"
|
||||
icon={<User className="h-4 w-4" />}
|
||||
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={
|
||||
<Avatar
|
||||
alt={team?.name}
|
||||
imageSrc={getPlaceholderAvatar(team.logo, team?.name as string)}
|
||||
size="xs"
|
||||
/>
|
||||
}
|
||||
label={t("yours")}
|
||||
/>
|
||||
))}
|
||||
</FilterCheckboxFieldsContainer>
|
||||
</AnimatedPopover>
|
||||
<Divider />
|
||||
{teams?.map((team) => (
|
||||
<FilterCheckboxField
|
||||
key={team.id}
|
||||
id={team.name}
|
||||
label={team.name}
|
||||
checked={!!query.teamIds?.includes(team.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
pushItemToKey("teamIds", team.id);
|
||||
} else if (!e.target.checked) {
|
||||
removeItemByKeyAndValue("teamIds", team.id);
|
||||
}
|
||||
}}
|
||||
icon={
|
||||
<Avatar
|
||||
alt={team?.name}
|
||||
imageSrc={getPlaceholderAvatar(team.logo, team?.name as string)}
|
||||
size="xs"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</FilterCheckboxFieldsContainer>
|
||||
</AnimatedPopover>
|
||||
{showVerticalDivider && <VerticalDivider />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<TrpcSessionUser>;
|
||||
prisma: PrismaClient;
|
||||
};
|
||||
input: TEventTypeInputSchema;
|
||||
};
|
||||
|
||||
export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
|
||||
const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
// 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<Prisma.EventTypeSelect>()({
|
||||
// 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<number, EventTypeGroup["eventTypes"][number]>);
|
||||
|
||||
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<typeof mapEventType>) => {
|
||||
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<TEventTypeInputSchema>["filters"]
|
||||
) {
|
||||
if (!filters || !hasFilter(filters)) {
|
||||
return userId;
|
||||
} else if (filters.userIds?.[0] === userId) {
|
||||
return userId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -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<typeof ZEventTypeInputSchema>;
|
||||
Reference in new issue
Block a user