Files
calendar/apps/web/lib/apps/installation/[[...step]]/getServerSideProps.ts
T
d27ccd6f44 feat: org team app install (#15704)
* 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>
2024-09-17 04:40:56 +00:00

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,
};
};