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>
This commit is contained in:
co-authored by
Joe Au-Yeung
parent
b6ab1ef80a
commit
0e62800af6
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -40,6 +40,32 @@ const userSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||
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<Prisma.MembershipSelect>()({
|
||||
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,
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<TrpcSessionUser>;
|
||||
prisma: PrismaClient;
|
||||
};
|
||||
input: TGetTeamAndEventTypeOptionsSchema;
|
||||
};
|
||||
|
||||
type Option = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type res = Awaited<
|
||||
ReturnType<typeof MembershipRepository.findAllByUpIdIncludeMinimalEventTypes>
|
||||
>[number]["team"]["eventTypes"][number];
|
||||
|
||||
type EventType = Omit<res, "forwardParamsSuccessRedirect"> & { 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,
|
||||
};
|
||||
};
|
||||
@@ -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<typeof ZGetTeamAndEventTypeOptionsSchema>;
|
||||
Reference in New Issue
Block a user