* feat: remove dropdown from app-store and redirect to new-app-install-flow * removed extra code * fix: account select loading issue * removed getPaymentCredential (unused) * fix: only redirect after the app has been added to all the event-types * remove overflowIndicatorStyles * refactor getUserAdminTeams * send teamId instead of id * seperate locations component * for conferencing apps skip select account page * refactor InstallAppButtonChild * installing conferencing apps shows locations dropdown in configure step * send location data to the handler * send location data to the handler * add the newly installed to the locations dropdown (prefillLocation) * fix: type errors * fix: handle es-lint errors * fix: app is added again on submit * only add app if not already added * fix: type erros * filter out managed events for now * fix: show installed count badge * remove 2 toast message * feat: added tests for conferencing apps * fix: loading indicator * Update apps/web/pages/apps/installation/[[...step]].tsx Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> * Update apps/web/pages/apps/installation/[[...step]].tsx Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> * move `getUserAdminTeams` to `UserRepository` * fix: zoom and jelly not redirecting to the new app install flow * fix: typo * add `installableOnTeams` prop * split `configureStepCard` into smaller components * fix: step count * fix: show loading indicator until the new page is loaded * fix: organizer_default_conferencing_app not visible in a team * Introduce QueryClient to app card tests * fix: typo * add installableOnTeams prop * fix: tests * seated events shouldn't have multiple locations #15174 * fix: o365 callback * fix: office365_video not redirecting to event-types step * Revert "fix: o365 callback" This reverts commit bba841035ea34f8c31201b64c77221b8d8c3e626. * add TEAM_SELECT_STEP * add apps to orgs and their sub-teams initial commit * undo team select step * small ui fix * fix: wrong step numbers * fix: dont allow app installation without cretendialId * fix: don't show acme team as it cannot have any events * refactor useAddAppMutation * removed console.log * added comment * added comment * move locationOptions from getServerSideProps a trpc query * fix: failing tests * Update conferencingApps.e2e.ts * refactor useAddAppMutation * refactor useAddAppMutation 2 * fix: test failing * fix: unit test * Revert "fix: unit test" This reverts commit 6d74032211d094478c6d7cf9aedbce696dfb768d. * fix: failing test * Increase test timeout for conferencing app tests * fix: write separate tests for each conferencing app to prevent hitting 6000 ms timeout * improved tests naming * fix: correct message and translation key #15657 * fix: write separate tests for each analytics app to prevent hitting 6000 ms timeout * fix: analytics apps test * attempt to fix failing tests * fix typo * refactor * update: replace text-gray with text-stuble - works with light mode too * update: use userRepository.getUserAdminTeams * Merge branch 'main' into feat/org-team-app-install-2 * fix: after merge conflict from app router migration * remove consoles --------- Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
327 lines
10 KiB
TypeScript
327 lines
10 KiB
TypeScript
import type { Prisma } from "@prisma/client";
|
|
import type { GetServerSidePropsContext } from "next";
|
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
|
import { z } from "zod";
|
|
|
|
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
|
import { isConferencing as isConferencingApp } from "@calcom/app-store/utils";
|
|
import type { LocationObject } from "@calcom/core/location";
|
|
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|
import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
|
|
import { CAL_URL } from "@calcom/lib/constants";
|
|
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
|
import prisma from "@calcom/prisma";
|
|
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
|
|
|
import { STEPS } from "~/apps/installation/[[...step]]/constants";
|
|
import type { OnboardingPageProps, TEventTypeGroup } from "~/apps/installation/[[...step]]/step-view";
|
|
|
|
const getUser = async (userId: number) => {
|
|
const userAdminTeams = await UserRepository.getUserAdminTeams(userId);
|
|
|
|
if (!userAdminTeams?.id) {
|
|
return null;
|
|
}
|
|
|
|
let teams = userAdminTeams.teams.map(({ team }) => ({
|
|
...team,
|
|
logoUrl: team.parent
|
|
? getPlaceholderAvatar(team.parent.logoUrl, team.parent.name)
|
|
: getPlaceholderAvatar(team.logoUrl, team.name),
|
|
}));
|
|
|
|
const orgTeam = teams.find((team) => team.isOrganization === true);
|
|
if (orgTeam?.id) {
|
|
teams = teams.filter((team) => team?.parent?.id !== orgTeam.id);
|
|
}
|
|
return {
|
|
...userAdminTeams,
|
|
teams,
|
|
};
|
|
};
|
|
|
|
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),
|
|
}));
|
|
};
|
|
|
|
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 },
|
|
});
|
|
return app;
|
|
};
|
|
|
|
const getEventTypes = async (userId: number, teamIds?: number[]) => {
|
|
const eventTypeSelect: Prisma.EventTypeSelect = {
|
|
id: true,
|
|
description: true,
|
|
durationLimits: true,
|
|
metadata: true,
|
|
length: true,
|
|
title: true,
|
|
position: true,
|
|
recurringEvent: true,
|
|
requiresConfirmation: 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,
|
|
};
|
|
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) => ({
|
|
teamId: team.id,
|
|
slug: team.slug,
|
|
name: team.name,
|
|
isOrganisation: team.isOrganization,
|
|
image: getPlaceholderAvatar(team.logoUrl, team.name),
|
|
eventTypes: team.eventTypes
|
|
.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.findFirst({
|
|
where: {
|
|
id: userId,
|
|
},
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
name: true,
|
|
avatarUrl: true,
|
|
eventTypes: {
|
|
where: {
|
|
teamId: null,
|
|
},
|
|
select: eventTypeSelect,
|
|
},
|
|
},
|
|
});
|
|
|
|
if (user) {
|
|
eventTypeGroups.push({
|
|
userId: user.id,
|
|
slug: user.username,
|
|
name: user.name,
|
|
image: getPlaceholderAvatar(user.avatarUrl, user.name),
|
|
eventTypes: user.eventTypes
|
|
.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),
|
|
});
|
|
}
|
|
}
|
|
return eventTypeGroups;
|
|
};
|
|
|
|
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 },
|
|
}
|
|
: {},
|
|
],
|
|
},
|
|
});
|
|
return appInstalls;
|
|
};
|
|
|
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
const { req, res, 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, res });
|
|
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(user.id, teamIds);
|
|
} else if (parsedTeamIdParam) {
|
|
eventTypeGroups = await getEventTypes(user.id, [parsedTeamIdParam]);
|
|
} else {
|
|
eventTypeGroups = await getEventTypes(user.id);
|
|
}
|
|
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)
|
|
);
|
|
|
|
const personalAccount = {
|
|
id: user.id,
|
|
name: user.name,
|
|
avatarUrl: user.avatarUrl,
|
|
alreadyInstalled: appInstalls.some((install) => !Boolean(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: {
|
|
...(await serverSideTranslations(locale, ["common"])),
|
|
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: !isConferencing,
|
|
} as OnboardingPageProps,
|
|
};
|
|
};
|