fix: skip Select Account step if only one account is available (#23432)
* fix: skip Select Account step if only one account is available * chore * chore * chore * test fixes * chore * refactor: address biome linting issues and decompose getServerSideProps --------- Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com> Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
This commit is contained in:
3 files changed
+380
-325
No files matched your search
@@ -1,344 +1,391 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
||||
import { filterEventTypesWhereLocationUpdateIsAllowed } from "@calcom/app-store/_utils/getBulkEventTypes";
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { isConferencing as isConferencingApp } from "@calcom/app-store/utils";
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { STEPS } from "~/apps/installation/[[...step]]/constants";
|
||||
import type { OnboardingPageProps, TEventTypeGroup } from "~/apps/installation/[[...step]]/step-view";
|
||||
import { STEPS } from "../../../../modules/apps/installation/[[...step]]/constants";
|
||||
import type { OnboardingPageProps, TEventTypeGroup, TEventType } from "../../../../modules/apps/installation/[[...step]]/step-view";
|
||||
|
||||
const getUser = async (userId: number) => {
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const userAdminTeams = await userRepo.getUserAdminTeams({ userId });
|
||||
|
||||
if (!userAdminTeams?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const teams = userAdminTeams.teams.map(({ team }) => ({
|
||||
...team,
|
||||
logoUrl: team.parent
|
||||
? getPlaceholderAvatar(team.parent.logoUrl, team.parent.name)
|
||||
: getPlaceholderAvatar(team.logoUrl, team.name),
|
||||
}));
|
||||
|
||||
return {
|
||||
...userAdminTeams,
|
||||
teams,
|
||||
};
|
||||
const eventTypeSelect: Prisma.EventTypeSelect = {
|
||||
id: true, description: true, durationLimits: true, metadata: true, length: true, title: true,
|
||||
position: true, recurringEvent: true, requiresConfirmation: true, canSendCalVideoTranscriptionEmails: true,
|
||||
team: { select: { slug: true } }, schedulingType: true, teamId: true, users: { select: { username: true } },
|
||||
seatsPerTimeSlot: true, slug: true, locations: true, userId: true, destinationCalendar: true,
|
||||
bookingFields: true, calVideoSettings: true, parentId: true,
|
||||
};
|
||||
|
||||
const getOrgSubTeams = async (parentId: number) => {
|
||||
const teams = await prisma.team.findMany({
|
||||
where: {
|
||||
parentId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
isOrganization: true,
|
||||
parent: {
|
||||
select: {
|
||||
logoUrl: true,
|
||||
name: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return teams.map((team) => ({
|
||||
...team,
|
||||
logoUrl: team.parent
|
||||
? getPlaceholderAvatar(team.parent.logoUrl, team.parent.name)
|
||||
: getPlaceholderAvatar(team.logoUrl, team.name),
|
||||
}));
|
||||
type EventTypeFromDb = Prisma.EventTypeGetPayload<{
|
||||
select: typeof eventTypeSelect;
|
||||
}>;
|
||||
|
||||
type MinimalUser = {
|
||||
id: number;
|
||||
username: string | null;
|
||||
name: string | null;
|
||||
avatarUrl: string | null;
|
||||
};
|
||||
|
||||
const getAppBySlug = async (appSlug: string) => {
|
||||
const app = await prisma.app.findUnique({
|
||||
where: { slug: appSlug, enabled: true },
|
||||
select: { slug: true, keys: true, enabled: true, dirName: true },
|
||||
type MinimalTeam = {
|
||||
id: number;
|
||||
name: string;
|
||||
logoUrl: string | null;
|
||||
isOrganization: boolean;
|
||||
};
|
||||
|
||||
type RedirectResult = { redirect: { permanent: boolean; destination: string } };
|
||||
|
||||
const mapEventType = (item: EventTypeFromDb): TEventType => {
|
||||
let teamSlug = "";
|
||||
if (item.team) {
|
||||
teamSlug = `team/${item.team.slug}`;
|
||||
}
|
||||
const userSlug = item?.users?.[0]?.username;
|
||||
let urlPart = userSlug;
|
||||
if (teamSlug) {
|
||||
urlPart = teamSlug;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
URL: `${CAL_URL}/${urlPart}/${item.slug}`,
|
||||
selected: false,
|
||||
locations: item.locations as unknown as LocationObject[],
|
||||
bookingFields: eventTypeBookingFields.parse(item.bookingFields || []),
|
||||
};
|
||||
};
|
||||
|
||||
const getUser = async (userId: number): Promise<(MinimalUser & {
|
||||
teams: { id: number; name: string; logoUrl: string | null; isOrganization: boolean; parent: { id: number; name: string; logoUrl: string | null } | null }[];
|
||||
}) | null> => {
|
||||
const userRepo = new UserRepository(prisma);
|
||||
const userAdminTeams = await userRepo.getUserAdminTeams({ userId });
|
||||
|
||||
if (!userAdminTeams?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const teams = userAdminTeams.teams.map(({ team }) => {
|
||||
const parentLogoUrl = team.parent?.logoUrl;
|
||||
const parentName = team.parent?.name;
|
||||
|
||||
let logoUrl = "";
|
||||
if (team.parent) {
|
||||
logoUrl = getPlaceholderAvatar(parentLogoUrl, parentName);
|
||||
} else {
|
||||
logoUrl = getPlaceholderAvatar(team.logoUrl, team.name);
|
||||
}
|
||||
return {
|
||||
...team,
|
||||
logoUrl,
|
||||
};
|
||||
});
|
||||
return app;
|
||||
|
||||
return {
|
||||
...userAdminTeams,
|
||||
teams,
|
||||
};
|
||||
};
|
||||
|
||||
const getOrgSubTeams = async (parentId: number): Promise<{ id: number; name: string; logoUrl: string | null; isOrganization: boolean; parent: { id: number; name: string; logoUrl: string | null } | null }[]> => {
|
||||
const teams = await prisma.team.findMany({
|
||||
where: {
|
||||
parentId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
isOrganization: true,
|
||||
parent: {
|
||||
select: {
|
||||
logoUrl: true,
|
||||
name: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return teams.map((team) => {
|
||||
const parentLogoUrl = team.parent?.logoUrl;
|
||||
const parentName = team.parent?.name;
|
||||
let logoUrl = "";
|
||||
if (team.parent) {
|
||||
logoUrl = getPlaceholderAvatar(parentLogoUrl, parentName);
|
||||
} else {
|
||||
logoUrl = getPlaceholderAvatar(team.logoUrl, team.name);
|
||||
}
|
||||
return {
|
||||
...team,
|
||||
logoUrl,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getAppBySlug = async (appSlug: string): Promise<{ slug: string; keys: Prisma.JsonValue; enabled: boolean; dirName: string; } | null> => {
|
||||
const app = await prisma.app.findUnique({
|
||||
where: { slug: appSlug, enabled: true },
|
||||
select: { slug: true, keys: true, enabled: true, dirName: true },
|
||||
});
|
||||
return app;
|
||||
};
|
||||
|
||||
const getTeamEventTypes = async (teamIds: number[], isConferencing: boolean, eventTypeSelect: Prisma.EventTypeSelect): Promise<TEventTypeGroup[]> => {
|
||||
const teams = await prisma.team.findMany({
|
||||
where: { id: { in: teamIds }, isOrganization: false },
|
||||
select: { id: true, name: true, logoUrl: true, slug: true, isOrganization: true, eventTypes: { select: eventTypeSelect } },
|
||||
});
|
||||
return teams.map((team) => {
|
||||
let filteredEventTypes = team.eventTypes;
|
||||
if (isConferencing) {
|
||||
filteredEventTypes = filterEventTypesWhereLocationUpdateIsAllowed(team.eventTypes);
|
||||
}
|
||||
return {
|
||||
teamId: team.id, slug: team.slug, name: team.name, isOrganisation: team.isOrganization,
|
||||
image: getPlaceholderAvatar(team.logoUrl, team.name),
|
||||
eventTypes: (filteredEventTypes as unknown as EventTypeFromDb[]).map(mapEventType).sort((a, b) => (b.position || 0) - (a.position || 0)),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const getUserEventTypes = async (userId: number, isConferencing: boolean, eventTypeSelect: Prisma.EventTypeSelect): Promise<TEventTypeGroup[]> => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true, username: true, name: true, avatarUrl: true, eventTypes: { where: { teamId: null }, select: eventTypeSelect } },
|
||||
});
|
||||
|
||||
if (!user) return [];
|
||||
|
||||
let filteredEventTypes = user.eventTypes;
|
||||
if (isConferencing) {
|
||||
filteredEventTypes = filterEventTypesWhereLocationUpdateIsAllowed(user.eventTypes);
|
||||
}
|
||||
return [{
|
||||
userId: user.id, slug: user.username, name: user.name, image: getPlaceholderAvatar(user.avatarUrl, user.name),
|
||||
eventTypes: (filteredEventTypes as unknown as EventTypeFromDb[]).map(mapEventType).sort((a, b) => (b.position || 0) - (a.position || 0)),
|
||||
}];
|
||||
};
|
||||
|
||||
const getEventTypes = async ({
|
||||
userId,
|
||||
teamIds,
|
||||
isConferencing = false,
|
||||
userId,
|
||||
teamIds,
|
||||
isConferencing = false,
|
||||
}: {
|
||||
userId: number;
|
||||
teamIds?: number[];
|
||||
isConferencing?: boolean;
|
||||
}) => {
|
||||
const eventTypeSelect = {
|
||||
id: true,
|
||||
description: true,
|
||||
durationLimits: true,
|
||||
metadata: true,
|
||||
length: true,
|
||||
title: true,
|
||||
position: true,
|
||||
recurringEvent: true,
|
||||
requiresConfirmation: true,
|
||||
canSendCalVideoTranscriptionEmails: true,
|
||||
team: { select: { slug: true } },
|
||||
schedulingType: true,
|
||||
teamId: true,
|
||||
users: { select: { username: true } },
|
||||
seatsPerTimeSlot: true,
|
||||
slug: true,
|
||||
locations: true,
|
||||
userId: true,
|
||||
destinationCalendar: true,
|
||||
bookingFields: true,
|
||||
calVideoSettings: true,
|
||||
parentId: true,
|
||||
} satisfies Prisma.EventTypeSelect;
|
||||
|
||||
let eventTypeGroups: TEventTypeGroup[] | null = [];
|
||||
|
||||
if (teamIds && teamIds.length > 0) {
|
||||
const teams = await prisma.team.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: teamIds,
|
||||
},
|
||||
isOrganization: false,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
slug: true,
|
||||
isOrganization: true,
|
||||
eventTypes: {
|
||||
select: eventTypeSelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
eventTypeGroups = teams.map((team) => {
|
||||
const filteredEventTypes = isConferencing
|
||||
? filterEventTypesWhereLocationUpdateIsAllowed(team.eventTypes)
|
||||
: team.eventTypes;
|
||||
|
||||
return {
|
||||
teamId: team.id,
|
||||
slug: team.slug,
|
||||
name: team.name,
|
||||
isOrganisation: team.isOrganization,
|
||||
image: getPlaceholderAvatar(team.logoUrl, team.name),
|
||||
eventTypes: filteredEventTypes
|
||||
.map((item) => ({
|
||||
...item,
|
||||
URL: `${CAL_URL}/${item.team ? `team/${item.team.slug}` : item?.users?.[0]?.username}/${item.slug}`,
|
||||
selected: false,
|
||||
locations: item.locations as unknown as LocationObject[],
|
||||
bookingFields: eventTypeBookingFields.parse(item.bookingFields || []),
|
||||
}))
|
||||
.sort((eventTypeA, eventTypeB) => eventTypeB.position - eventTypeA.position),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
avatarUrl: true,
|
||||
eventTypes: {
|
||||
where: {
|
||||
teamId: null,
|
||||
},
|
||||
select: eventTypeSelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
const filteredEventTypes = isConferencing
|
||||
? filterEventTypesWhereLocationUpdateIsAllowed(user.eventTypes)
|
||||
: user.eventTypes;
|
||||
|
||||
eventTypeGroups.push({
|
||||
userId: user.id,
|
||||
slug: user.username,
|
||||
name: user.name,
|
||||
image: getPlaceholderAvatar(user.avatarUrl, user.name),
|
||||
eventTypes: filteredEventTypes
|
||||
.map((item) => ({
|
||||
...item,
|
||||
URL: `${CAL_URL}/${item.team ? `team/${item.team.slug}` : item?.users?.[0]?.username}/${
|
||||
item.slug
|
||||
}`,
|
||||
selected: false,
|
||||
locations: item.locations as unknown as LocationObject[],
|
||||
bookingFields: eventTypeBookingFields.parse(item.bookingFields || []),
|
||||
}))
|
||||
.sort((eventTypeA, eventTypeB) => eventTypeB.position - eventTypeA.position),
|
||||
});
|
||||
userId: number;
|
||||
teamIds?: number[];
|
||||
isConferencing?: boolean;
|
||||
}): Promise<TEventTypeGroup[] | null> => {
|
||||
if (teamIds && teamIds.length > 0) {
|
||||
return getTeamEventTypes(teamIds, isConferencing, eventTypeSelect);
|
||||
}
|
||||
}
|
||||
return eventTypeGroups;
|
||||
|
||||
return getUserEventTypes(userId, isConferencing, eventTypeSelect);
|
||||
};
|
||||
|
||||
const getAppInstallsBySlug = async (appSlug: string, userId: number, teamIds?: number[]) => {
|
||||
const appInstalls = await prisma.credential.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
appId: appSlug,
|
||||
userId: userId,
|
||||
},
|
||||
teamIds && Boolean(teamIds.length)
|
||||
? {
|
||||
appId: appSlug,
|
||||
teamId: { in: teamIds },
|
||||
const getAppInstallsBySlug = async (appSlug: string, userId: number, teamIds?: number[]): Promise<Prisma.CredentialGetPayload<Record<string, never>>[]> => {
|
||||
if (teamIds?.length) {
|
||||
return prisma.credential.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ appId: appSlug, userId: userId },
|
||||
{ appId: appSlug, teamId: { in: teamIds } }
|
||||
]
|
||||
}
|
||||
: {},
|
||||
],
|
||||
},
|
||||
});
|
||||
return appInstalls;
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, query, params } = context;
|
||||
let eventTypeGroups: TEventTypeGroup[] | null = null;
|
||||
let isOrg = false;
|
||||
const stepsEnum = z.enum(STEPS);
|
||||
const parsedAppSlug = z.coerce.string().parse(query?.slug);
|
||||
const parsedStepParam = z.coerce.string().parse(params?.step);
|
||||
const parsedTeamIdParam = z.coerce.number().optional().parse(query?.teamId);
|
||||
const _ = stepsEnum.parse(parsedStepParam);
|
||||
const session = await getServerSession({ req });
|
||||
if (!session?.user?.id) return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
const _locale = await getLocale(context.req);
|
||||
const app = await getAppBySlug(parsedAppSlug);
|
||||
if (!app) return { redirect: { permanent: false, destination: "/apps" } };
|
||||
const appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata];
|
||||
const extendsEventType = appMetadata?.extendsFeature === "EventType";
|
||||
|
||||
const isConferencing = isConferencingApp(appMetadata.categories);
|
||||
const showEventTypesStep = extendsEventType || isConferencing;
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) return { redirect: { permanent: false, destination: "/apps" } };
|
||||
|
||||
let userTeams = user.teams;
|
||||
const hasTeams = Boolean(userTeams.length);
|
||||
|
||||
if (parsedTeamIdParam) {
|
||||
const currentTeam = userTeams.find((team) => team.id === parsedTeamIdParam);
|
||||
if (!currentTeam?.id) {
|
||||
return { redirect: { permanent: false, destination: "/apps" } };
|
||||
}
|
||||
if (currentTeam.isOrganization) {
|
||||
const subTeams = await getOrgSubTeams(parsedTeamIdParam);
|
||||
userTeams = [...userTeams, ...subTeams];
|
||||
isOrg = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedStepParam == AppOnboardingSteps.EVENT_TYPES_STEP) {
|
||||
if (!showEventTypesStep) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `/apps/installed/${appMetadata.categories[0]}?hl=${appMetadata.slug}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (isOrg) {
|
||||
const teamIds = userTeams.map((item) => item.id);
|
||||
eventTypeGroups = await getEventTypes({ userId: user.id, teamIds, isConferencing });
|
||||
} else if (parsedTeamIdParam) {
|
||||
eventTypeGroups = await getEventTypes({ userId: user.id, teamIds: [parsedTeamIdParam], isConferencing });
|
||||
} else {
|
||||
eventTypeGroups = await getEventTypes({ userId: user.id, isConferencing });
|
||||
}
|
||||
if (isConferencing && eventTypeGroups) {
|
||||
const destinationCalendar = await prisma.destinationCalendar.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
eventTypeId: null,
|
||||
},
|
||||
});
|
||||
|
||||
eventTypeGroups.forEach((group) => {
|
||||
group.eventTypes = group.eventTypes.map((eventType) => {
|
||||
if (!eventType.destinationCalendar) {
|
||||
return { ...eventType, destinationCalendar };
|
||||
}
|
||||
return eventType;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const appInstalls = await getAppInstallsBySlug(
|
||||
parsedAppSlug,
|
||||
user.id,
|
||||
userTeams.map(({ id }) => id)
|
||||
);
|
||||
return prisma.credential.findMany({
|
||||
where: { appId: appSlug, userId: userId },
|
||||
});
|
||||
};
|
||||
|
||||
const personalAccount = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
avatarUrl: user.avatarUrl,
|
||||
alreadyInstalled: appInstalls.some((install) => !install.teamId && install.userId === user.id),
|
||||
};
|
||||
|
||||
const teamsWithIsAppInstalled = hasTeams
|
||||
? userTeams.map((team) => ({
|
||||
...team,
|
||||
alreadyInstalled: appInstalls.some(
|
||||
(install) => Boolean(install.teamId) && install.teamId === team.id
|
||||
),
|
||||
}))
|
||||
: [];
|
||||
let credentialId = null;
|
||||
if (parsedTeamIdParam) {
|
||||
credentialId = appInstalls.find((item) => !!item.teamId && item.teamId == parsedTeamIdParam)?.id ?? null;
|
||||
} else {
|
||||
credentialId = appInstalls.find((item) => !!item.userId && item.userId == user.id)?.id ?? null;
|
||||
}
|
||||
// dont allow app installation without cretendialId
|
||||
if (parsedStepParam == AppOnboardingSteps.EVENT_TYPES_STEP && !credentialId) {
|
||||
return { redirect: { permanent: false, destination: "/apps" } };
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
app,
|
||||
appMetadata,
|
||||
showEventTypesStep,
|
||||
step: parsedStepParam,
|
||||
teams: teamsWithIsAppInstalled,
|
||||
personalAccount,
|
||||
eventTypeGroups,
|
||||
teamId: parsedTeamIdParam ?? null,
|
||||
userName: user.username,
|
||||
credentialId,
|
||||
isConferencing,
|
||||
isOrg,
|
||||
// conferencing apps dont support team install
|
||||
installableOnTeams: !!appMetadata?.concurrentMeetings || !isConferencing,
|
||||
} as OnboardingPageProps,
|
||||
};
|
||||
const handleAutoInstall = async (user: MinimalUser, appMetadata: import("@calcom/types/App").AppMeta, parsedAppSlug: string): Promise<number | null> => {
|
||||
try {
|
||||
const newCredential = await createDefaultInstallation({
|
||||
appType: appMetadata.type,
|
||||
user: { id: user.id },
|
||||
slug: parsedAppSlug,
|
||||
key: {},
|
||||
teamId: undefined,
|
||||
});
|
||||
return newCredential.id;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
const existing = await prisma.credential.findFirst({
|
||||
where: { appId: parsedAppSlug, userId: user.id, teamId: null },
|
||||
select: { id: true },
|
||||
});
|
||||
return existing?.id ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getEventTypeGroupsForStep = async (user: MinimalUser, userTeams: MinimalTeam[], parsedTeamIdParam: number | undefined, isOrg: boolean, isConferencing: boolean): Promise<TEventTypeGroup[] | null> => {
|
||||
let groups: TEventTypeGroup[] | null = null;
|
||||
if (isOrg) {
|
||||
groups = await getEventTypes({ userId: user.id, teamIds: userTeams.map((item) => item.id), isConferencing });
|
||||
} else if (parsedTeamIdParam) {
|
||||
groups = await getEventTypes({ userId: user.id, teamIds: [parsedTeamIdParam], isConferencing });
|
||||
} else {
|
||||
groups = await getEventTypes({ userId: user.id, isConferencing });
|
||||
}
|
||||
|
||||
if (isConferencing && groups) {
|
||||
const destinationCalendar = await prisma.destinationCalendar.findFirst({
|
||||
where: { userId: user.id, eventTypeId: null },
|
||||
});
|
||||
|
||||
groups.forEach((group) => {
|
||||
group.eventTypes = group.eventTypes.map((eventType) => {
|
||||
if (!eventType.destinationCalendar) {
|
||||
return { ...eventType, destinationCalendar };
|
||||
}
|
||||
return eventType;
|
||||
});
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
};
|
||||
|
||||
const getInitialStep = (parsedStepParam: string | undefined, hasTeams: boolean, showEventTypesStep: boolean, parsedAppSlug: string): { step: string | undefined; redirect?: RedirectResult } => {
|
||||
if (!hasTeams && parsedStepParam === AppOnboardingSteps.ACCOUNTS_STEP && showEventTypesStep) {
|
||||
return {
|
||||
step: parsedStepParam,
|
||||
redirect: { redirect: { permanent: false, destination: `/apps/installation/event-types?slug=${parsedAppSlug}` } }
|
||||
};
|
||||
}
|
||||
let step = parsedStepParam;
|
||||
if (!parsedStepParam) {
|
||||
if (hasTeams) {
|
||||
step = AppOnboardingSteps.ACCOUNTS_STEP;
|
||||
} else {
|
||||
step = AppOnboardingSteps.EVENT_TYPES_STEP;
|
||||
}
|
||||
}
|
||||
return { step };
|
||||
};
|
||||
|
||||
const getAppAndMetadata = async (parsedAppSlug: string): Promise<{ app: { slug: string; keys: Prisma.JsonValue; enabled: boolean; dirName: string } | null; appMetadata: import("@calcom/types/App").AppMeta | null; redirect?: RedirectResult }> => {
|
||||
const app = await getAppBySlug(parsedAppSlug);
|
||||
if (!app) return { app: null, appMetadata: null, redirect: { redirect: { permanent: false, destination: "/apps" } } };
|
||||
const appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata];
|
||||
if (!appMetadata) return { app: null, appMetadata: null, redirect: { redirect: { permanent: false, destination: "/apps" } } };
|
||||
return { app, appMetadata };
|
||||
};
|
||||
|
||||
const getCredentialId = (parsedTeamIdParam: number | undefined, appInstalls: Prisma.CredentialGetPayload<Record<string, never>>[], userId: number): number | null => {
|
||||
if (parsedTeamIdParam) {
|
||||
return appInstalls.find((item) => item.teamId === parsedTeamIdParam)?.id ?? null;
|
||||
}
|
||||
return appInstalls.find((item) => item.userId === userId)?.id ?? null;
|
||||
};
|
||||
|
||||
const prepareUserTeams = async (user: MinimalUser & { teams: MinimalTeam[] }, parsedTeamIdParam: number | undefined): Promise<{ userTeams: MinimalTeam[]; isOrg: boolean; redirect?: RedirectResult }> => {
|
||||
let userTeams = user.teams;
|
||||
let isOrg = false;
|
||||
if (parsedTeamIdParam) {
|
||||
const currentTeam = userTeams.find((team: MinimalTeam) => team.id === parsedTeamIdParam);
|
||||
if (!currentTeam?.id) return { userTeams: [], isOrg: false, redirect: { redirect: { permanent: false, destination: "/apps" } } };
|
||||
if (currentTeam.isOrganization) {
|
||||
userTeams = [...userTeams, ...await getOrgSubTeams(parsedTeamIdParam)];
|
||||
isOrg = true;
|
||||
}
|
||||
}
|
||||
return { userTeams, isOrg };
|
||||
};
|
||||
|
||||
const getCredential = async (parsedTeamIdParam: number | undefined, appInstalls: Prisma.CredentialGetPayload<Record<string, never>>[], user: MinimalUser & { teams: MinimalTeam[] }, initialStep: string, appMetadata: import("@calcom/types/App").AppMeta, parsedAppSlug: string): Promise<{ credentialId: number | null; redirect?: RedirectResult }> => {
|
||||
let credentialId = getCredentialId(parsedTeamIdParam, appInstalls, user.id);
|
||||
if (!credentialId && !user.teams.length && initialStep === AppOnboardingSteps.EVENT_TYPES_STEP) {
|
||||
credentialId = await handleAutoInstall(user, appMetadata, parsedAppSlug);
|
||||
if (!credentialId) return { credentialId: null, redirect: { redirect: { permanent: false, destination: "/apps" } } };
|
||||
}
|
||||
return { credentialId };
|
||||
};
|
||||
|
||||
const getTeamsInstalled = (hasTeams: boolean, userTeams: MinimalTeam[], appInstalls: Prisma.CredentialGetPayload<Record<string, never>>[]): import("../../../../modules/apps/installation/[[...step]]/step-view").TTeams => {
|
||||
if (!hasTeams) return [];
|
||||
return userTeams.map((team: MinimalTeam) => ({
|
||||
...team, alreadyInstalled: appInstalls.some((install) => !!install.teamId && install.teamId === team.id),
|
||||
}));
|
||||
};
|
||||
|
||||
const getParsedStep = (paramsStep: string | string[] | undefined): string | undefined => {
|
||||
if (Array.isArray(paramsStep)) return paramsStep[0];
|
||||
return paramsStep;
|
||||
};
|
||||
|
||||
const getInstallationParams = async (context: GetServerSidePropsContext): Promise<{ parsedAppSlug: string; parsedStepParam: string | undefined; parsedTeamIdParam: number | undefined; sessionId: number | null }> => {
|
||||
const { query, params, req } = context;
|
||||
const parsedAppSlug = z.coerce.string().parse(query?.slug);
|
||||
const parsedStepParam = z.string().optional().parse(getParsedStep(params?.step));
|
||||
const parsedTeamIdParam = z.coerce.number().optional().parse(query?.teamId);
|
||||
const session = await getServerSession({ req });
|
||||
const sessionId = (session?.user as { id?: number })?.id ?? null;
|
||||
return { parsedAppSlug, parsedStepParam, parsedTeamIdParam, sessionId };
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<OnboardingPageProps>> => {
|
||||
const { parsedAppSlug, parsedStepParam, parsedTeamIdParam, sessionId } = await getInstallationParams(context);
|
||||
if (!sessionId) return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
|
||||
const { app, appMetadata, redirect: appRedirect } = await getAppAndMetadata(parsedAppSlug);
|
||||
if (appRedirect) return appRedirect;
|
||||
|
||||
const user = await getUser(sessionId);
|
||||
if (!user?.username || !appMetadata) return { redirect: { permanent: false, destination: "/apps" } };
|
||||
const appMetadataFinal = appMetadata;
|
||||
|
||||
const showEventTypesStep = appMetadataFinal.extendsFeature === "EventType" || isConferencingApp(appMetadataFinal.categories);
|
||||
const { userTeams, isOrg, redirect: teamRedirect } = await prepareUserTeams(user, parsedTeamIdParam);
|
||||
if (teamRedirect) return teamRedirect;
|
||||
const { step: initialStep, redirect: stepRedirect } = getInitialStep(parsedStepParam, !!userTeams.length, showEventTypesStep, parsedAppSlug);
|
||||
if (stepRedirect) return stepRedirect;
|
||||
|
||||
let initialStepValidated: (typeof STEPS)[number];
|
||||
try {
|
||||
initialStepValidated = z.enum(STEPS).parse(initialStep);
|
||||
} catch {
|
||||
return { redirect: { permanent: false, destination: "/apps" } };
|
||||
}
|
||||
|
||||
const appInstalls = await getAppInstallsBySlug(parsedAppSlug, user.id, userTeams.map(({ id }: MinimalTeam) => id));
|
||||
const { credentialId, redirect: credRedirect } = await getCredential(parsedTeamIdParam, appInstalls, user, initialStepValidated, appMetadataFinal, parsedAppSlug);
|
||||
if (credRedirect) return credRedirect;
|
||||
|
||||
let eventTypeGroups = null;
|
||||
if (initialStepValidated === AppOnboardingSteps.EVENT_TYPES_STEP) {
|
||||
if (!showEventTypesStep) {
|
||||
return { redirect: { permanent: false, destination: `/apps/installed/${appMetadataFinal.categories[0]}?hl=${appMetadataFinal.slug}` } };
|
||||
}
|
||||
eventTypeGroups = await getEventTypeGroupsForStep(user, userTeams, parsedTeamIdParam, isOrg, isConferencingApp(appMetadataFinal.categories));
|
||||
if (!credentialId) return { redirect: { permanent: false, destination: "/apps" } };
|
||||
}
|
||||
|
||||
const personalAccount = {
|
||||
id: user.id, name: user.name, avatarUrl: user.avatarUrl,
|
||||
alreadyInstalled: appInstalls.some((install) => !install.teamId && install.userId === user.id),
|
||||
};
|
||||
const teams = getTeamsInstalled(!!user.teams.length, userTeams, appInstalls);
|
||||
|
||||
return {
|
||||
props: {
|
||||
app, appMetadata: appMetadataFinal, showEventTypesStep, step: initialStepValidated, personalAccount, teams,
|
||||
eventTypeGroups, userName: user.username, credentialId: credentialId || undefined,
|
||||
isConferencing: isConferencingApp(appMetadataFinal.categories), isOrg,
|
||||
installableOnTeams: !!appMetadataFinal?.concurrentMeetings || !isConferencingApp(appMetadataFinal.categories),
|
||||
} as OnboardingPageProps,
|
||||
};
|
||||
};
|
||||
@@ -102,7 +102,7 @@ type TUpdateObject = {
|
||||
|
||||
const OnboardingPage = ({
|
||||
step,
|
||||
teams,
|
||||
teams = [],
|
||||
personalAccount,
|
||||
appMetadata,
|
||||
eventTypeGroups,
|
||||
@@ -116,6 +116,8 @@ const OnboardingPage = ({
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const [configureStep, setConfigureStep] = useState(false);
|
||||
|
||||
const STEPS_MAP: StepObj = {
|
||||
[AppOnboardingSteps.ACCOUNTS_STEP]: {
|
||||
getTitle: () => `${t("select_account_header")}`,
|
||||
@@ -127,19 +129,18 @@ const OnboardingPage = ({
|
||||
getTitle: () => `${t("select_event_types_header")}`,
|
||||
getDescription: (appName) =>
|
||||
`${t("select_event_types_description", { appName, interpolation: { escapeValue: false } })}`,
|
||||
stepNumber: installableOnTeams ? 2 : 1,
|
||||
stepNumber: teams.length > 0 && installableOnTeams ? 2 : 1,
|
||||
},
|
||||
[AppOnboardingSteps.CONFIGURE_STEP]: {
|
||||
getTitle: (appName) =>
|
||||
`${t("configure_app_header", { appName, interpolation: { escapeValue: false } })}`,
|
||||
getDescription: () => `${t("configure_app_description")}`,
|
||||
stepNumber: installableOnTeams ? 3 : 2,
|
||||
stepNumber: teams.length > 0 && installableOnTeams ? 3 : 2,
|
||||
},
|
||||
} as const;
|
||||
const [configureStep, setConfigureStep] = useState(false);
|
||||
|
||||
const currentStep: AppOnboardingSteps = useMemo(() => {
|
||||
if (step == AppOnboardingSteps.EVENT_TYPES_STEP && configureStep) {
|
||||
if (step === AppOnboardingSteps.EVENT_TYPES_STEP && configureStep) {
|
||||
return AppOnboardingSteps.CONFIGURE_STEP;
|
||||
}
|
||||
return step;
|
||||
@@ -150,8 +151,8 @@ const OnboardingPage = ({
|
||||
if (!showEventTypesStep) {
|
||||
return 1;
|
||||
}
|
||||
return installableOnTeams ? STEPS.length : STEPS.length - 1;
|
||||
}, [showEventTypesStep, installableOnTeams]);
|
||||
return teams.length > 0 && installableOnTeams ? STEPS.length : STEPS.length - 1;
|
||||
}, [showEventTypesStep, teams, installableOnTeams]);
|
||||
|
||||
const utils = trpc.useContext();
|
||||
|
||||
|
||||
@@ -18,20 +18,27 @@ export function createAppsFixture(page: Page) {
|
||||
installAnalyticsAppSkipConfigure: async (app: string) => {
|
||||
await page.getByTestId(`app-store-app-card-${app}`).click();
|
||||
await page.getByTestId("install-app-button").click();
|
||||
await page.waitForURL(`apps/installation/accounts?slug=${app}`);
|
||||
await page.waitForURL(`apps/installation/**?slug=${app}`);
|
||||
await page.reload();
|
||||
await page.click('[data-testid="install-app-button-personal"]');
|
||||
await page.waitForURL(`apps/installation/event-types?slug=${app}`);
|
||||
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes("apps/installation/accounts")) {
|
||||
await page.click('[data-testid="install-app-button-personal"]');
|
||||
await page.waitForURL(`apps/installation/event-types?slug=${app}`);
|
||||
}
|
||||
await page.click('[data-testid="set-up-later"]');
|
||||
},
|
||||
installAnalyticsApp: async (app: string, eventTypeIds: number[]) => {
|
||||
await page.getByTestId(`app-store-app-card-${app}`).click();
|
||||
(await page.waitForSelector('[data-testid="install-app-button"]')).click();
|
||||
await page.waitForURL(`apps/installation/accounts?slug=${app}`);
|
||||
await page.getByTestId("install-app-button").click();
|
||||
await page.waitForURL(`apps/installation/**?slug=${app}`);
|
||||
await page.reload();
|
||||
|
||||
await page.click('[data-testid="install-app-button-personal"]');
|
||||
await page.waitForURL(`apps/installation/event-types?slug=${app}`);
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes("apps/installation/accounts")) {
|
||||
await page.click('[data-testid="install-app-button-personal"]');
|
||||
await page.waitForURL(`apps/installation/event-types?slug=${app}`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
Reference in new issue
Block a user