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:
parent
1c1625d3fc
commit
3d4a3c7a93
|
@ -116,6 +116,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||||
type={item.integration.type}
|
type={item.integration.type}
|
||||||
isChecked={cal.isSelected}
|
isChecked={cal.isSelected}
|
||||||
destination={cal.externalId === props.destinationCalendarId}
|
destination={cal.externalId === props.destinationCalendarId}
|
||||||
|
credentialId={cal.credentialId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -53,6 +53,7 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
||||||
<ul className="p-4">
|
<ul className="p-4">
|
||||||
{calendars?.map((calendar, i) => (
|
{calendars?.map((calendar, i) => (
|
||||||
<CalendarSwitch
|
<CalendarSwitch
|
||||||
|
credentialId={calendar.credentialId}
|
||||||
key={calendar.externalId}
|
key={calendar.externalId}
|
||||||
externalId={calendar.externalId}
|
externalId={calendar.externalId}
|
||||||
title={calendar.name || "Nameless Calendar"}
|
title={calendar.name || "Nameless Calendar"}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import prisma from "@calcom/prisma";
|
||||||
const selectedCalendarSelectSchema = z.object({
|
const selectedCalendarSelectSchema = z.object({
|
||||||
integration: z.string(),
|
integration: z.string(),
|
||||||
externalId: z.string(),
|
externalId: z.string(),
|
||||||
|
credentialId: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
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;
|
const { credentials, ...user } = userWithCredentials;
|
||||||
|
|
||||||
if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
const { integration, externalId } = selectedCalendarSelectSchema.parse(req.body);
|
const { integration, externalId, credentialId } = selectedCalendarSelectSchema.parse(req.body);
|
||||||
await prisma.selectedCalendar.upsert({
|
await prisma.selectedCalendar.upsert({
|
||||||
where: {
|
where: {
|
||||||
userId_integration_externalId: {
|
userId_integration_externalId: {
|
||||||
|
@ -50,6 +51,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
integration,
|
integration,
|
||||||
externalId,
|
externalId,
|
||||||
|
credentialId,
|
||||||
},
|
},
|
||||||
// already exists
|
// already exists
|
||||||
update: {},
|
update: {},
|
||||||
|
|
|
@ -182,6 +182,7 @@ const CalendarsView = () => {
|
||||||
{item.calendars.map((cal) => (
|
{item.calendars.map((cal) => (
|
||||||
<CalendarSwitch
|
<CalendarSwitch
|
||||||
key={cal.externalId}
|
key={cal.externalId}
|
||||||
|
credentialId={cal.credentialId}
|
||||||
externalId={cal.externalId}
|
externalId={cal.externalId}
|
||||||
title={cal.name || "Nameless calendar"}
|
title={cal.name || "Nameless calendar"}
|
||||||
name={cal.name || "Nameless calendar"}
|
name={cal.name || "Nameless calendar"}
|
||||||
|
|
|
@ -15,9 +15,10 @@ interface ICalendarSwitchProps {
|
||||||
name: string;
|
name: string;
|
||||||
isLastItemInList?: boolean;
|
isLastItemInList?: boolean;
|
||||||
destination?: boolean;
|
destination?: boolean;
|
||||||
|
credentialId: number;
|
||||||
}
|
}
|
||||||
const CalendarSwitch = (props: ICalendarSwitchProps) => {
|
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 [checkedInternal, setCheckedInternal] = useState(isChecked);
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
@ -40,7 +41,7 @@ const CalendarSwitch = (props: ICalendarSwitchProps) => {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify({ ...body, credentialId }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
|
@ -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;
|
|
@ -137,6 +137,7 @@ model Credential {
|
||||||
// How to make it a required column?
|
// How to make it a required column?
|
||||||
appId String?
|
appId String?
|
||||||
destinationCalendars DestinationCalendar[]
|
destinationCalendars DestinationCalendar[]
|
||||||
|
selectedCalendars SelectedCalendar[]
|
||||||
invalid Boolean? @default(false)
|
invalid Boolean? @default(false)
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
@ -448,10 +449,12 @@ model Availability {
|
||||||
}
|
}
|
||||||
|
|
||||||
model SelectedCalendar {
|
model SelectedCalendar {
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
userId Int
|
userId Int
|
||||||
integration String
|
integration String
|
||||||
externalId String
|
externalId String
|
||||||
|
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
|
||||||
|
credentialId Int?
|
||||||
|
|
||||||
@@id([userId, integration, externalId])
|
@@id([userId, integration, externalId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
|
|
@ -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 zapier get disconnected, delete zapier apiKey, delete zapier webhooks and cancel all scheduled jobs from zapier
|
||||||
if (credential.app?.slug === "zapier") {
|
if (credential.app?.slug === "zapier") {
|
||||||
await prisma.apiKey.deleteMany({
|
await prisma.apiKey.deleteMany({
|
||||||
|
@ -372,4 +340,46 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
|
||||||
id: id,
|
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}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -214,7 +214,7 @@ export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "
|
||||||
// For displaying the connected email address
|
// For displaying the connected email address
|
||||||
email?: string;
|
email?: string;
|
||||||
primaryEmail?: string;
|
primaryEmail?: string;
|
||||||
credentialId?: number;
|
credentialId?: number | null;
|
||||||
integrationTitle?: string;
|
integrationTitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user