From 0e62800af6dd97b7314816139667dabcb541bdf7 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:18:48 +0530 Subject: [PATCH] refactor: simplify workflow page and improve load time (#16095) * fix: simplify workflow page and improve load time * chore: use new endpoint * chore: save progress * refactor: code * refactor: remove not requried code * chore: remove schema * chore: fix typ * chore: improve * chore: change name * chore: remove unused * chore: remove page --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> --- .../workflows/components/AddActionDialog.tsx | 2 +- packages/features/ee/workflows/lib/schema.ts | 49 ++++ .../features/ee/workflows/pages/workflow.tsx | 122 +--------- packages/lib/server/repository/eventType.ts | 104 ++++++++ packages/lib/server/repository/membership.ts | 137 +++++++---- .../routers/viewer/eventTypes/_router.ts | 28 +++ .../getTeamAndEventTypeOptions.handler.ts | 223 ++++++++++++++++++ .../getTeamAndEventTypeOptions.schema.ts | 10 + 8 files changed, 519 insertions(+), 156 deletions(-) create mode 100644 packages/features/ee/workflows/lib/schema.ts create mode 100644 packages/trpc/server/routers/viewer/eventTypes/getTeamAndEventTypeOptions.handler.ts create mode 100644 packages/trpc/server/routers/viewer/eventTypes/getTeamAndEventTypeOptions.schema.ts diff --git a/packages/features/ee/workflows/components/AddActionDialog.tsx b/packages/features/ee/workflows/components/AddActionDialog.tsx index d776fb297f..a2a10af8a9 100644 --- a/packages/features/ee/workflows/components/AddActionDialog.tsx +++ b/packages/features/ee/workflows/components/AddActionDialog.tsx @@ -27,7 +27,7 @@ import { } from "@calcom/ui"; import { WORKFLOW_ACTIONS } from "../lib/constants"; -import { onlyLettersNumbersSpaces } from "../pages/workflow"; +import { onlyLettersNumbersSpaces } from "../lib/schema"; interface IAddActionDialog { isOpenDialog: boolean; diff --git a/packages/features/ee/workflows/lib/schema.ts b/packages/features/ee/workflows/lib/schema.ts new file mode 100644 index 0000000000..1d9fb6cffc --- /dev/null +++ b/packages/features/ee/workflows/lib/schema.ts @@ -0,0 +1,49 @@ +import { isValidPhoneNumber } from "libphonenumber-js"; +import { z } from "zod"; + +import { TimeUnit, WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@calcom/prisma/enums"; +import { stringOrNumber } from "@calcom/prisma/zod-utils"; + +export function onlyLettersNumbersSpaces(str: string) { + if (str.length <= 11 && /^[A-Za-z0-9\s]*$/.test(str)) { + return true; + } + return false; +} + +export const formSchema = z.object({ + name: z.string(), + activeOn: z.object({ value: z.string(), label: z.string() }).array(), + trigger: z.nativeEnum(WorkflowTriggerEvents), + time: z.number().gte(0).optional(), + timeUnit: z.nativeEnum(TimeUnit).optional(), + steps: z + .object({ + id: z.number(), + stepNumber: z.number(), + action: z.nativeEnum(WorkflowActions), + workflowId: z.number(), + reminderBody: z.string().nullable(), + emailSubject: z.string().nullable(), + template: z.nativeEnum(WorkflowTemplates), + numberRequired: z.boolean().nullable(), + includeCalendarEvent: z.boolean().nullable(), + sendTo: z + .string() + .refine((val) => isValidPhoneNumber(val) || val.includes("@")) + .optional() + .nullable(), + sender: z + .string() + .refine((val) => onlyLettersNumbersSpaces(val)) + .optional() + .nullable(), + senderName: z.string().optional().nullable(), + }) + .array(), + selectAll: z.boolean(), +}); + +export const querySchema = z.object({ + workflow: stringOrNumber, +}); diff --git a/packages/features/ee/workflows/pages/workflow.tsx b/packages/features/ee/workflows/pages/workflow.tsx index 619c24780b..8ff33cfcca 100644 --- a/packages/features/ee/workflows/pages/workflow.tsx +++ b/packages/features/ee/workflows/pages/workflow.tsx @@ -1,10 +1,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; import type { WorkflowStep } from "@prisma/client"; -import { isValidPhoneNumber } from "libphonenumber-js"; import { useSession } from "next-auth/react"; -import { useEffect, useState, useMemo } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import { z } from "zod"; import Shell, { ShellMain } from "@calcom/features/shell/Shell"; import { classNames } from "@calcom/lib"; @@ -12,14 +10,8 @@ import { SENDER_ID } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; import { HttpError } from "@calcom/lib/http-error"; -import { - MembershipRole, - TimeUnit, - WorkflowActions, - WorkflowTemplates, - WorkflowTriggerEvents, -} from "@calcom/prisma/enums"; -import { stringOrNumber } from "@calcom/prisma/zod-utils"; +import type { TimeUnit, WorkflowTriggerEvents } from "@calcom/prisma/enums"; +import { MembershipRole, WorkflowActions } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; @@ -30,6 +22,7 @@ import LicenseRequired from "../../common/components/LicenseRequired"; import SkeletonLoader from "../components/SkeletonLoaderEdit"; import WorkflowDetailsPage from "../components/WorkflowDetailsPage"; import { isSMSAction, isSMSOrWhatsappAction } from "../lib/actionHelperFunctions"; +import { formSchema, querySchema } from "../lib/schema"; import { getTranslatedText, translateVariablesToEnglish } from "../lib/variableTranslations"; export type FormValues = { @@ -42,50 +35,6 @@ export type FormValues = { selectAll: boolean; }; -export function onlyLettersNumbersSpaces(str: string) { - if (str.length <= 11 && /^[A-Za-z0-9\s]*$/.test(str)) { - return true; - } - return false; -} - -const formSchema = z.object({ - name: z.string(), - activeOn: z.object({ value: z.string(), label: z.string() }).array(), - trigger: z.nativeEnum(WorkflowTriggerEvents), - time: z.number().gte(0).optional(), - timeUnit: z.nativeEnum(TimeUnit).optional(), - steps: z - .object({ - id: z.number(), - stepNumber: z.number(), - action: z.nativeEnum(WorkflowActions), - workflowId: z.number(), - reminderBody: z.string().nullable(), - emailSubject: z.string().nullable(), - template: z.nativeEnum(WorkflowTemplates), - numberRequired: z.boolean().nullable(), - includeCalendarEvent: z.boolean().nullable(), - sendTo: z - .string() - .refine((val) => isValidPhoneNumber(val) || val.includes("@")) - .optional() - .nullable(), - sender: z - .string() - .refine((val) => onlyLettersNumbersSpaces(val)) - .optional() - .nullable(), - senderName: z.string().optional().nullable(), - }) - .array(), - selectAll: z.boolean(), -}); - -const querySchema = z.object({ - workflow: stringOrNumber, -}); - function WorkflowPage() { const { t, i18n } = useLocale(); const session = useSession(); @@ -129,69 +78,22 @@ function WorkflowPage() { teamId: workflow?.team?.id, }); - const { data: eventTypeGroups, isPending: isPendingEventTypes } = - trpc.viewer.eventTypes.getByViewer.useQuery(); - - const { data: otherTeams, isPending: isPendingTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(); const isOrg = workflow?.team?.isOrganization ?? false; const teamId = workflow?.teamId ?? undefined; - const profileTeamsOptions = - isOrg && eventTypeGroups - ? eventTypeGroups?.profiles - .filter((profile) => !!profile.teamId) - .map((profile) => { - return { - value: String(profile.teamId) || "", - label: profile.name || profile.slug || "", - }; - }) - : []; - - const otherTeamsOptions = otherTeams - ? otherTeams.map((team) => { - return { - value: String(team.id) || "", - label: team.name || team.slug || "", - }; - }) - : []; - - const teamOptions = profileTeamsOptions.concat(otherTeamsOptions); - - const eventTypeOptions = useMemo( - () => - eventTypeGroups?.eventTypeGroups.reduce((options, group) => { - /** don't show team event types for user workflow */ - if (!teamId && group.teamId) return options; - /** only show correct team event types for team workflows */ - if (teamId && teamId !== group.teamId) return options; - return [ - ...options, - ...group.eventTypes - .filter( - (evType) => - !evType.metadata?.managedEventConfig || - !!evType.metadata?.managedEventConfig.unlockedFields?.workflows || - !!teamId - ) - .map((eventType) => ({ - value: String(eventType.id), - label: `${eventType.title} ${ - eventType.children && eventType.children.length ? `(+${eventType.children.length})` : `` - }`, - })), - ]; - }, [] as Option[]) || [], - [eventTypeGroups] + const { data, isPending: isPendingEventTypes } = trpc.viewer.eventTypes.getTeamAndEventTypeOptions.useQuery( + { teamId, isOrg }, + { enabled: !isPendingWorkflow } ); - let allEventTypeOptions = eventTypeOptions; + const teamOptions = data?.teamOptions ?? []; + + let allEventTypeOptions = data?.eventTypeOptions ?? []; const distinctEventTypes = new Set(); if (!teamId && isMixedEventType) { - allEventTypeOptions = [...eventTypeOptions, ...selectedOptions]; + allEventTypeOptions = [...allEventTypeOptions, ...selectedOptions]; allEventTypeOptions = allEventTypeOptions.filter((option) => { const duplicate = distinctEventTypes.has(option.value); distinctEventTypes.add(option.value); @@ -203,7 +105,7 @@ function WorkflowPage() { workflow?.team?.members?.find((member) => member.userId === session.data?.user.id)?.role === MembershipRole.MEMBER; - const isPending = isPendingWorkflow || isPendingEventTypes || isPendingTeams; + const isPending = isPendingWorkflow || isPendingEventTypes; useEffect(() => { if (!isPending) { diff --git a/packages/lib/server/repository/eventType.ts b/packages/lib/server/repository/eventType.ts index dfbe64f731..e455779104 100644 --- a/packages/lib/server/repository/eventType.ts +++ b/packages/lib/server/repository/eventType.ts @@ -216,6 +216,110 @@ export class EventTypeRepository { } } + static async findAllByUpIdWithMinimalData( + { upId, userId }: { upId: string; userId: number }, + { + orderBy, + where = {}, + cursor: cursorId, + limit, + }: { + orderBy?: Prisma.EventTypeOrderByWithRelationInput[]; + where?: Prisma.EventTypeWhereInput; + cursor?: number | null; + limit?: number | null; + } = {} + ) { + if (!upId) return []; + const lookupTarget = ProfileRepository.getLookupTarget(upId); + const profileId = lookupTarget.type === LookupTarget.User ? null : lookupTarget.id; + const select = { + ...eventTypeSelect, + hashedLink: true, + }; + + log.debug( + "findAllByUpIdWithMinimalData", + safeStringify({ + upId, + orderBy, + argumentWhere: where, + }) + ); + + const cursor = cursorId ? { id: cursorId } : undefined; + const take = limit ? limit + 1 : undefined; // We take +1 as it'll be used for the next cursor + + if (!profileId) { + // Lookup is by userId + return await prisma.eventType.findMany({ + where: { + userId: lookupTarget.id, + ...where, + }, + select, + cursor, + take, + orderBy, + }); + } + + const profile = await ProfileRepository.findById(profileId); + if (profile?.movedFromUser) { + // Because the user has been moved to this profile, we need to get all user events except those that belong to some other profile + // This is because those event-types that are created after moving to profile would have profileId but existing event-types would have profileId set to null + return await prisma.eventType.findMany({ + where: { + OR: [ + // Existing events + { + userId: profile.movedFromUser.id, + profileId: null, + }, + // New events + { + profileId, + }, + // Fetch children event-types by userId because profileId is wrong + { + userId, + parentId: { + not: null, + }, + }, + ], + ...where, + }, + select, + cursor, + take, + orderBy, + }); + } else { + return await prisma.eventType.findMany({ + where: { + OR: [ + { + profileId, + }, + // Fetch children event-types by userId because profileId is wrong + { + userId: userId, + parentId: { + not: null, + }, + }, + ], + ...where, + }, + select, + cursor, + take, + orderBy, + }); + } + } + static async findTeamEventTypes({ teamId, parentId, diff --git a/packages/lib/server/repository/membership.ts b/packages/lib/server/repository/membership.ts index 644f6d6fbb..0b843d26cd 100644 --- a/packages/lib/server/repository/membership.ts +++ b/packages/lib/server/repository/membership.ts @@ -40,6 +40,32 @@ const userSelect = Prisma.validator()({ id: true, }); +const getWhereForfindAllByUpId = async (upId: string, where?: Prisma.MembershipWhereInput) => { + const lookupTarget = ProfileRepository.getLookupTarget(upId); + let prismaWhere; + if (lookupTarget.type === LookupTarget.Profile) { + /** + * TODO: When we add profileId to membership, we lookup by profileId + * If the profile is movedFromUser, we lookup all memberships without profileId as well. + */ + const profile = await ProfileRepository.findById(lookupTarget.id); + if (!profile) { + return []; + } + prismaWhere = { + userId: profile.user.id, + ...where, + }; + } else { + prismaWhere = { + userId: lookupTarget.id, + ...where, + }; + } + + return prismaWhere; +}; + export class MembershipRepository { static async create(data: IMembership) { return await prisma.membership.create({ @@ -60,26 +86,9 @@ export class MembershipRepository { { upId }: { upId: string }, { where }: { where?: Prisma.MembershipWhereInput } = {} ) { - const lookupTarget = ProfileRepository.getLookupTarget(upId); - let prismaWhere; - if (lookupTarget.type === LookupTarget.Profile) { - /** - * TODO: When we add profileId to membership, we lookup by profileId - * If the profile is movedFromUser, we lookup all memberships without profileId as well. - */ - const profile = await ProfileRepository.findById(lookupTarget.id); - if (!profile) { - return []; - } - prismaWhere = { - userId: profile.user.id, - ...where, - }; - } else { - prismaWhere = { - userId: lookupTarget.id, - ...where, - }; + const prismaWhere = await getWhereForfindAllByUpId(upId, where); + if (Array.isArray(prismaWhere)) { + return prismaWhere; } log.debug( @@ -132,41 +141,79 @@ export class MembershipRepository { }); } + static async findAllByUpIdIncludeMinimalEventTypes( + { upId }: { upId: string }, + { where, skipEventTypes = false }: { where?: Prisma.MembershipWhereInput; skipEventTypes?: boolean } = {} + ) { + const prismaWhere = await getWhereForfindAllByUpId(upId, where); + if (Array.isArray(prismaWhere)) { + return prismaWhere; + } + + log.debug( + "findAllByUpIdIncludeMinimalEventTypes", + safeStringify({ + prismaWhere, + }) + ); + + const select = Prisma.validator()({ + id: true, + teamId: true, + userId: true, + accepted: true, + role: true, + disableImpersonation: true, + team: { + select: { + ...teamParentSelect, + isOrganization: true, + parent: { + select: teamParentSelect, + }, + ...(!skipEventTypes + ? { + eventTypes: { + select: { + ...eventTypeSelect, + hashedLink: true, + children: { select: { id: true } }, + }, + orderBy: [ + { + position: "desc", + }, + { + id: "asc", + }, + ], + }, + } + : {}), + }, + }, + }); + + return await prisma.membership.findMany({ + where: prismaWhere, + select, + }); + } + static async findAllByUpIdIncludeTeam( { upId }: { upId: string }, { where }: { where?: Prisma.MembershipWhereInput } = {} ) { - const lookupTarget = ProfileRepository.getLookupTarget(upId); - let prismaWhere; - - if (lookupTarget.type === LookupTarget.Profile) { - /** - * TODO: When we add profileId to membership, we lookup by profileId - * If the profile is movedFromUser, we lookup all memberships without profileId as well. - */ - const profile = await ProfileRepository.findById(lookupTarget.id); - if (!profile) { - return []; - } - prismaWhere = { - userId: profile.user.id, - ...where, - }; - } else { - prismaWhere = { - userId: lookupTarget.id, - ...where, - }; + const prismaWhere = await getWhereForfindAllByUpId(upId, where); + if (Array.isArray(prismaWhere)) { + return prismaWhere; } + return await prisma.membership.findMany({ where: prismaWhere, include: { team: { include: { - // could slow down the query - // members: { - // select: membershipSelect, - // }, parent: { select: teamParentSelect, }, diff --git a/packages/trpc/server/routers/viewer/eventTypes/_router.ts b/packages/trpc/server/routers/viewer/eventTypes/_router.ts index cffc893837..852646c46e 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 { ZEventTypeInputSchema, ZGetEventTypesFromGroupSchema } from "./getByViewer.schema"; +import { ZGetTeamAndEventTypeOptionsSchema } from "./getTeamAndEventTypeOptions.schema"; import { get } from "./procedures/get"; import { ZUpdateInputSchema } from "./update.schema"; import { eventOwnerProcedure } from "./util"; @@ -16,6 +17,7 @@ type BookingsRouterHandlerCache = { getByViewer?: typeof import("./getByViewer.handler").getByViewerHandler; getUserEventGroups?: typeof import("./getUserEventGroups.handler").getUserEventGroups; getEventTypesFromGroup?: typeof import("./getEventTypesFromGroup.handler").getEventTypesFromGroup; + getTeamAndEventTypeOptions?: typeof import("./getTeamAndEventTypeOptions.handler").getTeamAndEventTypeOptions; list?: typeof import("./list.handler").listHandler; listWithTeam?: typeof import("./listWithTeam.handler").listWithTeamHandler; create?: typeof import("./create.handler").createHandler; @@ -104,6 +106,32 @@ export const eventTypesRouter = router({ return result; }), + getTeamAndEventTypeOptions: authedProcedure + .input(ZGetTeamAndEventTypeOptionsSchema) + .query(async ({ ctx, input }) => { + if (!UNSTABLE_HANDLER_CACHE.getTeamAndEventTypeOptions) { + UNSTABLE_HANDLER_CACHE.getTeamAndEventTypeOptions = await import( + "./getTeamAndEventTypeOptions.handler" + ).then((mod) => mod.getTeamAndEventTypeOptions); + } + + // Unreachable code but required for type safety + if (!UNSTABLE_HANDLER_CACHE.getTeamAndEventTypeOptions) { + throw new Error("Failed to load handler"); + } + + const timer = logP(`getTeamAndEventTypeOptions(${ctx.user.id})`); + + const result = await UNSTABLE_HANDLER_CACHE.getTeamAndEventTypeOptions({ + ctx, + input, + }); + + timer(); + + return result; + }), + list: authedProcedure.query(async ({ ctx }) => { if (!UNSTABLE_HANDLER_CACHE.list) { UNSTABLE_HANDLER_CACHE.list = await import("./list.handler").then((mod) => mod.listHandler); diff --git a/packages/trpc/server/routers/viewer/eventTypes/getTeamAndEventTypeOptions.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getTeamAndEventTypeOptions.handler.ts new file mode 100644 index 0000000000..da84e030b6 --- /dev/null +++ b/packages/trpc/server/routers/viewer/eventTypes/getTeamAndEventTypeOptions.handler.ts @@ -0,0 +1,223 @@ +import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; +import { EventTypeRepository } from "@calcom/lib/server/repository/eventType"; +import { MembershipRepository } from "@calcom/lib/server/repository/membership"; +import { ProfileRepository } from "@calcom/lib/server/repository/profile"; +import type { PrismaClient } from "@calcom/prisma"; +import { MembershipRole, SchedulingType } from "@calcom/prisma/enums"; +import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; +import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; + +import { TRPCError } from "@trpc/server"; + +import type { TrpcSessionUser } from "../../../trpc"; +import { listOtherTeamHandler } from "../organizations/listOtherTeams.handler"; +import type { TGetTeamAndEventTypeOptionsSchema } from "./getTeamAndEventTypeOptions.schema"; + +type GetTeamAndEventTypeOptions = { + ctx: { + user: NonNullable; + prisma: PrismaClient; + }; + input: TGetTeamAndEventTypeOptionsSchema; +}; + +type Option = { + value: string; + label: string; +}; + +type res = Awaited< + ReturnType +>[number]["team"]["eventTypes"][number]; + +type EventType = Omit & { children?: { id: number }[] }; + +export const getTeamAndEventTypeOptions = async ({ ctx, input }: GetTeamAndEventTypeOptions) => { + await checkRateLimitAndThrowError({ + identifier: `eventTypes:getTeamAndEventTypeOptions.handler:${ctx.user.id}`, + rateLimitingType: "common", + }); + + const user = ctx.user; + const teamId = input?.teamId; + const isOrg = input?.isOrg; + + const skipTeamOptions = !isOrg; + const skipEventTypes = !!isOrg; + + const userProfile = ctx.user.profile; + const profile = await ProfileRepository.findByUpId(userProfile.upId); + const parentOrgHasLockedEventTypes = + profile?.organization?.organizationSettings?.lockEventTypeCreationForUsers; + + const [profileMemberships, profileEventTypes] = await Promise.all([ + MembershipRepository.findAllByUpIdIncludeMinimalEventTypes( + { + upId: userProfile.upId, + }, + { + where: { + accepted: true, + }, + skipEventTypes, + } + ), + teamId + ? [] + : EventTypeRepository.findAllByUpIdWithMinimalData( + { + upId: userProfile.upId, + userId: user.id, + }, + { + where: { + teamId: null, + }, + orderBy: [ + { + position: "desc", + }, + { + id: "asc", + }, + ], + } + ), + ]); + + if (!profile) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); + } + + const memberships = profileMemberships.map((membership) => ({ + ...membership, + team: { + ...membership.team, + metadata: teamMetadataSchema.parse(membership.team.metadata), + }, + })); + + type EventTypeGroup = { + teamId?: number | null; + parentId?: number | null; + bookerUrl?: string; + profile: { + slug?: (typeof profile)["username"] | null; + name: (typeof profile)["name"]; + image?: string; + eventTypesLockedByOrg?: boolean; + }; + eventTypes?: EventType[]; + }; + + let eventTypeGroups: EventTypeGroup[] = []; + + eventTypeGroups.push({ + teamId: null, + profile: { + slug: profile.username, + name: profile.name, + eventTypesLockedByOrg: parentOrgHasLockedEventTypes, + }, + eventTypes: profileEventTypes, + }); + + eventTypeGroups = ([] as EventTypeGroup[]).concat( + eventTypeGroups, + await Promise.all( + memberships + .filter((mmship) => { + if (mmship?.team?.isOrganization) { + return false; + } + return true; + }) + .map(async (membership) => { + const team = { + ...membership.team, + metadata: teamMetadataSchema.parse(membership.team.metadata), + }; + + const eventTypes = team.eventTypes; + return { + teamId: team.id, + parentId: team.parentId, + profile: { + name: team.name, + }, + eventTypes: eventTypes + ?.filter((evType) => { + const res = evType.userId === null || evType.userId === user.id; + return res; + }) + ?.filter((evType) => + membership.role === MembershipRole.MEMBER + ? evType.schedulingType !== SchedulingType.MANAGED + : true + ), + }; + }) + ) + ); + + let teamOptions: Option[] = []; + + if (!skipTeamOptions) { + const profileTeamsOptions = eventTypeGroups + .map((group) => ({ + ...group.profile, + teamId: group.teamId, + })) + .filter((profile) => !!profile.teamId) + .map((profile) => { + return { + value: String(profile.teamId) || "", + label: profile.name || profile.slug || "", + }; + }); + + const otherTeams = await listOtherTeamHandler({ ctx }); + const otherTeamsOptions = otherTeams + ? otherTeams.map((team) => { + return { + value: String(team.id) || "", + label: team.name || team.slug || "", + }; + }) + : []; + + teamOptions = profileTeamsOptions.concat(otherTeamsOptions); + } + + const eventTypeOptions = + eventTypeGroups.reduce((options, group) => { + // /** don't show team event types for user workflow */ + if (!teamId && group.teamId) return options; + // /** only show correct team event types for team workflows */ + if (teamId && teamId !== group.teamId) return options; + + return [ + ...options, + ...(group?.eventTypes + ?.filter((evType) => { + const metadata = EventTypeMetaDataSchema.parse(evType.metadata); + return ( + !metadata?.managedEventConfig || + !!metadata?.managedEventConfig.unlockedFields?.workflows || + !!teamId + ); + }) + ?.map((eventType) => ({ + value: String(eventType.id), + label: `${eventType.title} ${ + eventType?.children && eventType.children.length ? `(+${eventType.children.length})` : `` + }`, + })) ?? []), + ]; + }, [] as Option[]) || []; + + return { + eventTypeOptions, + teamOptions, + }; +}; diff --git a/packages/trpc/server/routers/viewer/eventTypes/getTeamAndEventTypeOptions.schema.ts b/packages/trpc/server/routers/viewer/eventTypes/getTeamAndEventTypeOptions.schema.ts new file mode 100644 index 0000000000..7119179cfa --- /dev/null +++ b/packages/trpc/server/routers/viewer/eventTypes/getTeamAndEventTypeOptions.schema.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export const ZGetTeamAndEventTypeOptionsSchema = z + .object({ + teamId: z.number().optional(), + isOrg: z.boolean().default(false), + }) + .nullish(); + +export type TGetTeamAndEventTypeOptionsSchema = z.infer;