fix: Connect selected calendar to credential (#11283)

* Reduce the unhandled promise

* Connect selected calendar to credential

* Write credential to selected calendar

* Type fix

* Handle catching promises in separate threads

* Only call to list calendars if there are no credentialIds

* Changed type of IntegrationCalendar from undef to null

* Adding missing property to getting started

* Also add calendars.tsx

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Joe Au-Yeung 2023-09-12 13:09:05 -04:00 committed by GitHub
parent 1c1625d3fc
commit 3d4a3c7a93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 64 additions and 40 deletions

View File

@ -116,6 +116,7 @@ function ConnectedCalendarsList(props: Props) {
type={item.integration.type}
isChecked={cal.isSelected}
destination={cal.externalId === props.destinationCalendarId}
credentialId={cal.credentialId}
/>
))}
</ul>

View File

@ -53,6 +53,7 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
<ul className="p-4">
{calendars?.map((calendar, i) => (
<CalendarSwitch
credentialId={calendar.credentialId}
key={calendar.externalId}
externalId={calendar.externalId}
title={calendar.name || "Nameless Calendar"}

View File

@ -9,6 +9,7 @@ import prisma from "@calcom/prisma";
const selectedCalendarSelectSchema = z.object({
integration: z.string(),
externalId: z.string(),
credentialId: z.number().optional(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -37,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { credentials, ...user } = userWithCredentials;
if (req.method === "POST") {
const { integration, externalId } = selectedCalendarSelectSchema.parse(req.body);
const { integration, externalId, credentialId } = selectedCalendarSelectSchema.parse(req.body);
await prisma.selectedCalendar.upsert({
where: {
userId_integration_externalId: {
@ -50,6 +51,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
userId: user.id,
integration,
externalId,
credentialId,
},
// already exists
update: {},

View File

@ -182,6 +182,7 @@ const CalendarsView = () => {
{item.calendars.map((cal) => (
<CalendarSwitch
key={cal.externalId}
credentialId={cal.credentialId}
externalId={cal.externalId}
title={cal.name || "Nameless calendar"}
name={cal.name || "Nameless calendar"}

View File

@ -15,9 +15,10 @@ interface ICalendarSwitchProps {
name: string;
isLastItemInList?: boolean;
destination?: boolean;
credentialId: number;
}
const CalendarSwitch = (props: ICalendarSwitchProps) => {
const { title, externalId, type, isChecked, name, isLastItemInList = false } = props;
const { title, externalId, type, isChecked, name, isLastItemInList = false, credentialId } = props;
const [checkedInternal, setCheckedInternal] = useState(isChecked);
const utils = trpc.useContext();
const { t } = useLocale();
@ -40,7 +41,7 @@ const CalendarSwitch = (props: ICalendarSwitchProps) => {
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
body: JSON.stringify({ ...body, credentialId }),
});
if (!res.ok) {

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "SelectedCalendar" ADD COLUMN "credentialId" INTEGER;
-- AddForeignKey
ALTER TABLE "SelectedCalendar" ADD CONSTRAINT "SelectedCalendar_credentialId_fkey" FOREIGN KEY ("credentialId") REFERENCES "Credential"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -137,6 +137,7 @@ model Credential {
// How to make it a required column?
appId String?
destinationCalendars DestinationCalendar[]
selectedCalendars SelectedCalendar[]
invalid Boolean? @default(false)
@@index([userId])
@ -448,10 +449,12 @@ model Availability {
}
model SelectedCalendar {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
integration String
externalId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
integration String
externalId String
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
credentialId Int?
@@id([userId, integration, externalId])
@@index([userId])

View File

@ -307,38 +307,6 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
}
}
// If it's a calendar remove it from the SelectedCalendars
if (credential.app?.categories.includes(AppCategories.calendar)) {
const calendar = await getCalendar(credential);
const calendars = await calendar?.listCalendars();
if (calendars && calendars.length > 0) {
calendars.map(async (cal) => {
prisma.selectedCalendar
.delete({
where: {
userId_integration_externalId: {
userId: user.id,
externalId: cal.externalId,
integration: cal.integration as string,
},
},
})
.catch((error) => {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
console.log(
`Error deleting selected calendars for user ${user.id} and calendar ${credential.appId}. Could not find selected calendar.`
);
}
console.log(
`Error deleting selected calendars for user ${user.id} and calendar ${credential.appId} with error: ${error}`
);
});
});
}
}
// if zapier get disconnected, delete zapier apiKey, delete zapier webhooks and cancel all scheduled jobs from zapier
if (credential.app?.slug === "zapier") {
await prisma.apiKey.deleteMany({
@ -372,4 +340,46 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
id: id,
},
});
// Backwards compatibility. Selected calendars cascade on delete when deleting a credential
// If it's a calendar remove it from the SelectedCalendars
if (credential.app?.categories.includes(AppCategories.calendar)) {
const selectedCalendars = await prisma.selectedCalendar.findMany({
where: {
userId: user.id,
integration: credential.type as string,
},
});
if (selectedCalendars.length) {
const calendar = await getCalendar(credential);
const calendars = await calendar?.listCalendars();
if (calendars && calendars.length > 0) {
calendars.map(async (cal) => {
prisma.selectedCalendar
.delete({
where: {
userId_integration_externalId: {
userId: user.id,
externalId: cal.externalId,
integration: cal.integration as string,
},
},
})
.catch((error) => {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
console.log(
`Error deleting selected calendars for user ${user.id} and calendar ${credential.appId}. Could not find selected calendar.`
);
}
console.log(
`Error deleting selected calendars for user ${user.id} and calendar ${credential.appId} with error: ${error}`
);
});
});
}
}
}
};

View File

@ -214,7 +214,7 @@ export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "
// For displaying the connected email address
email?: string;
primaryEmail?: string;
credentialId?: number;
credentialId?: number | null;
integrationTitle?: string;
}