integration page follow ups (#912)
### Internals - Replace `lodash.*` packages with plain `lodash` & replace `lodash.*` imports with `lodash/` - should have no impact on bundle size and opens up for us to use all of lodash - Update `viewer.me` to cherry-pick what we actually need on that query to avoid leaking extra context info - Update `getIntegrations` to never include `.key`-property to avoid leaking ### Visual - Update calendars so `primary` is displayed last - Update connected calendars so they are in ascending order in which you connected them
This commit is contained in:
parent
9e2f8de313
commit
ec6b897191
18 changed files with 146 additions and 147 deletions
|
@ -11,7 +11,6 @@ NEXT_PUBLIC_LICENSE_CONSENT=''
|
|||
DATABASE_URL="postgresql://postgres:@localhost:5432/calendso?schema=public"
|
||||
|
||||
GOOGLE_API_CREDENTIALS='secret'
|
||||
GOOGLE_REDIRECT_URL='https://localhost:3000/integrations/googlecalendar/callback'
|
||||
|
||||
BASE_URL='http://localhost:3000'
|
||||
NEXT_PUBLIC_APP_URL='http://localhost:3000'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import merge from "lodash.merge";
|
||||
import merge from "lodash/merge";
|
||||
import { NextSeo, NextSeoProps } from "next-seo";
|
||||
import React from "react";
|
||||
|
||||
|
|
1
environment.d.ts
vendored
1
environment.d.ts
vendored
|
@ -3,7 +3,6 @@ declare namespace NodeJS {
|
|||
readonly CALENDSO_ENCRYPTION_KEY: string | undefined;
|
||||
readonly DATABASE_URL: string | undefined;
|
||||
readonly GOOGLE_API_CREDENTIALS: string | undefined;
|
||||
readonly GOOGLE_REDIRECT_URL: string | undefined;
|
||||
readonly BASE_URL: string | undefined;
|
||||
readonly NEXT_PUBLIC_BASE_URL: string | undefined;
|
||||
readonly NEXT_PUBLIC_APP_URL: string | undefined;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
import async from "async";
|
||||
import merge from "lodash.merge";
|
||||
import merge from "lodash/merge";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient";
|
||||
|
|
|
@ -2,21 +2,17 @@ import React, { useState } from "react";
|
|||
import { useForm } from "react-hook-form";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogProps,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogClose,
|
||||
DialogFooter,
|
||||
} from "@components/Dialog";
|
||||
import { Form, TextField } from "@components/form/fields";
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
type Props = {
|
||||
onSubmit: () => void;
|
||||
};
|
||||
|
||||
export const ADD_APPLE_INTEGRATION_FORM_TITLE = "addAppleIntegration";
|
||||
|
||||
export function AddAppleIntegrationModal(props: DialogProps) {
|
||||
|
@ -104,49 +100,3 @@ export function AddAppleIntegrationModal(props: DialogProps) {
|
|||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
const AddAppleIntegration = React.forwardRef<HTMLFormElement, Props>((props, ref) => {
|
||||
const onSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
props.onSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<form id={ADD_APPLE_INTEGRATION_FORM_TITLE} ref={ref} onSubmit={onSubmit}>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="email@icloud.com"
|
||||
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="•••••••••••••"
|
||||
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
||||
AddAppleIntegration.displayName = "AddAppleIntegrationForm";
|
||||
export default AddAppleIntegration;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
|
||||
import { validJson } from "@lib/jsonUtils";
|
||||
|
||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||
select: { id: true, type: true, key: true },
|
||||
select: { id: true, type: true },
|
||||
});
|
||||
|
||||
type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
|
||||
|
@ -33,6 +34,14 @@ export const ALL_INTEGRATIONS = [
|
|||
description: "Video Conferencing",
|
||||
variant: "conferencing",
|
||||
},
|
||||
{
|
||||
installed: !!process.env.DAILY_API_KEY,
|
||||
type: "daily_video",
|
||||
title: "Daily.co Video",
|
||||
imageSrc: "integrations/daily.svg",
|
||||
description: "Video Conferencing",
|
||||
variant: "conferencing",
|
||||
},
|
||||
{
|
||||
installed: true,
|
||||
type: "caldav_calendar",
|
||||
|
@ -62,23 +71,33 @@ export const ALL_INTEGRATIONS = [
|
|||
variant: "payment",
|
||||
},
|
||||
] as const;
|
||||
function getIntegrations(credentials: CredentialData[]) {
|
||||
const integrations = ALL_INTEGRATIONS.map((integration) => ({
|
||||
...integration,
|
||||
/**
|
||||
* @deprecated use `credentials.
|
||||
*/
|
||||
credential: credentials.find((credential) => credential.type === integration.type) || null,
|
||||
credentials: credentials.filter((credential) => credential.type === integration.type) || null,
|
||||
}));
|
||||
|
||||
function getIntegrations(userCredentials: CredentialData[]) {
|
||||
const integrations = ALL_INTEGRATIONS.map((integration) => {
|
||||
const credentials = userCredentials
|
||||
.filter((credential) => credential.type === integration.type)
|
||||
.map((credential) => _.pick(credential, ["id", "type"])); // ensure we don't leak `key` to frontend
|
||||
|
||||
const credential: typeof credentials[number] | null = credentials[0] || null;
|
||||
return {
|
||||
...integration,
|
||||
/**
|
||||
* @deprecated use `credentials`
|
||||
*/
|
||||
credential,
|
||||
credentials,
|
||||
};
|
||||
});
|
||||
|
||||
return integrations;
|
||||
}
|
||||
|
||||
export type IntegraionMeta = ReturnType<typeof getIntegrations>;
|
||||
export type IntegrationMeta = ReturnType<typeof getIntegrations>;
|
||||
|
||||
export function hasIntegration(integrations: ReturnType<typeof getIntegrations>, type: string): boolean {
|
||||
return !!integrations.find((i) => i.type === type && !!i.installed && !!i.credential);
|
||||
export function hasIntegration(integrations: IntegrationMeta, type: string): boolean {
|
||||
return !!integrations.find(
|
||||
(i) => i.type === type && !!i.installed && (type === "daily_video" || i.credentials.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
export default getIntegrations;
|
||||
|
|
11
package.json
11
package.json
|
@ -27,8 +27,8 @@
|
|||
"yarn": ">=1.19.0 < 2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@daily-co/daily-js": "^0.16.0",
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@heroicons/react": "^1.0.4",
|
||||
"@hookform/resolvers": "^2.8.1",
|
||||
"@jitsu/sdk-js": "^2.2.4",
|
||||
|
@ -61,9 +61,7 @@
|
|||
"ical.js": "^1.4.0",
|
||||
"ics": "^2.31.0",
|
||||
"jimp": "^0.16.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"micro": "^9.3.4",
|
||||
"next": "^11.1.1",
|
||||
"next-auth": "^3.28.0",
|
||||
|
@ -82,10 +80,10 @@
|
|||
"react-multi-email": "^0.5.3",
|
||||
"react-phone-number-input": "^3.1.25",
|
||||
"react-query": "^3.23.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "^4.3.1",
|
||||
"react-timezone-select": "^1.0.7",
|
||||
"react-use-intercom": "1.4.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"short-uuid": "^4.2.0",
|
||||
"stripe": "^8.168.0",
|
||||
"superjson": "1.7.5",
|
||||
|
@ -100,8 +98,7 @@
|
|||
"@types/async": "^3.2.7",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.merge": "^4.6.6",
|
||||
"@types/lodash": "^4.14.175",
|
||||
"@types/micro": "^7.3.6",
|
||||
"@types/node": "^16.6.1",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
|
|
|
@ -20,8 +20,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
|
||||
// Get token from Google Calendar API
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web;
|
||||
const redirect_uri = process.env.GOOGLE_REDIRECT_URL || redirect_uris[0];
|
||||
const { client_secret, client_id } = JSON.parse(credentials).web;
|
||||
const redirect_uri = process.env.BASE_URL + "/api/integrations/googlecalendar/callback";
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
|
||||
const authUrl = oAuth2Client.generateAuthUrl({
|
||||
|
|
|
@ -25,8 +25,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
return;
|
||||
}
|
||||
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web;
|
||||
const redirect_uri = process.env.GOOGLE_REDIRECT_URL || redirect_uris[0];
|
||||
const { client_secret, client_id } = JSON.parse(credentials).web;
|
||||
const redirect_uri = process.env.BASE_URL + "/api/integrations/googlecalendar/callback";
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
const token = await oAuth2Client.getToken(code);
|
||||
const key = token.res?.data;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ResetPasswordRequest } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import debounce from "lodash.debounce";
|
||||
import debounce from "lodash/debounce";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/client";
|
||||
import Link from "next/link";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import debounce from "lodash.debounce";
|
||||
import debounce from "lodash/debounce";
|
||||
import { getCsrfToken } from "next-auth/client";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
|
|
@ -1247,7 +1247,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
const locationOptions: OptionTypeBase[] = [
|
||||
{ value: LocationType.InPerson, label: "Link or In-person meeting" },
|
||||
{ value: LocationType.Phone, label: "Phone call" },
|
||||
{ value: LocationType.Zoom, label: "Zoom Video", disabled: true },
|
||||
];
|
||||
|
||||
if (hasIntegration(integrations, "zoom_video")) {
|
||||
|
@ -1257,8 +1256,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
if (hasIntegration(integrations, "google_calendar")) {
|
||||
locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" });
|
||||
}
|
||||
const hasDailyIntegration = process.env.DAILY_API_KEY;
|
||||
if (hasDailyIntegration) {
|
||||
if (hasIntegration(integrations, "daily_video")) {
|
||||
locationOptions.push({ value: LocationType.Daily, label: "Daily.co Video" });
|
||||
}
|
||||
const currency =
|
||||
|
|
|
@ -11,12 +11,12 @@ import classnames from "classnames";
|
|||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import debounce from "lodash.debounce";
|
||||
import debounce from "lodash/debounce";
|
||||
import omit from "lodash/omit";
|
||||
import { NextPageContext } from "next";
|
||||
import { useSession } from "next-auth/client";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { Integration } from "pages/integrations/_new";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import TimezoneSelect from "react-timezone-select";
|
||||
|
||||
|
@ -691,7 +691,7 @@ export async function getServerSideProps(context: NextPageContext) {
|
|||
},
|
||||
});
|
||||
|
||||
integrations = getIntegrations(credentials);
|
||||
integrations = getIntegrations(credentials).map((item) => omit(item, "key"));
|
||||
|
||||
eventTypes = await prisma.eventType.findMany({
|
||||
where: {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Maybe } from "@trpc/server";
|
||||
import Image from "next/image";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
|
@ -8,7 +7,7 @@ import classNames from "@lib/classNames";
|
|||
import { AddAppleIntegrationModal } from "@lib/integrations/Apple/components/AddAppleIntegration";
|
||||
import { AddCalDavIntegrationModal } from "@lib/integrations/CalDav/components/AddCalDavIntegration";
|
||||
import showToast from "@lib/notification";
|
||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import { Dialog } from "@components/Dialog";
|
||||
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
|
||||
|
@ -19,8 +18,6 @@ import Badge from "@components/ui/Badge";
|
|||
import Button, { ButtonBaseProps } from "@components/ui/Button";
|
||||
import Switch from "@components/ui/Switch";
|
||||
|
||||
type IntegrationCalendar = inferQueryOutput<"viewer.integrations">["calendar"]["items"][number];
|
||||
|
||||
function pluralize(opts: { num: number; plural: string; singular: string }) {
|
||||
if (opts.num === 0) {
|
||||
return opts.singular;
|
||||
|
@ -47,10 +44,7 @@ function SubHeadingTitleWithConnections(props: { title: ReactNode; numConnection
|
|||
);
|
||||
}
|
||||
|
||||
function ConnectIntegration(props: {
|
||||
type: IntegrationCalendar["type"];
|
||||
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
||||
}) {
|
||||
function ConnectIntegration(props: { type: string; render: (renderProps: ButtonBaseProps) => JSX.Element }) {
|
||||
const { type } = props;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const mutation = useMutation(async () => {
|
||||
|
@ -154,14 +148,15 @@ function DisconnectIntegration(props: {
|
|||
|
||||
function ConnectOrDisconnectIntegrationButton(props: {
|
||||
//
|
||||
credential: Maybe<{ id: number }>;
|
||||
type: IntegrationCalendar["type"];
|
||||
credentialIds: number[];
|
||||
type: string;
|
||||
installed: boolean;
|
||||
}) {
|
||||
if (props.credential) {
|
||||
const [credentialId] = props.credentialIds;
|
||||
if (credentialId) {
|
||||
return (
|
||||
<DisconnectIntegration
|
||||
id={props.credential.id}
|
||||
id={credentialId}
|
||||
render={(btnProps) => (
|
||||
<Button {...btnProps} color="warn">
|
||||
Disconnect
|
||||
|
@ -177,6 +172,14 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
/** We don't need to "Connect", just show that it's installed */
|
||||
if (props.type === "daily_video") {
|
||||
return (
|
||||
<div className="px-3 py-2 truncate">
|
||||
<h3 className="text-sm font-medium text-gray-700">Installed</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ConnectIntegration type={props.type} render={(btnProps) => <Button {...btnProps}>Connect</Button>} />
|
||||
);
|
||||
|
@ -193,7 +196,7 @@ function IntegrationListItem(props: {
|
|||
<ListItem expanded={!!props.children} className={classNames("flex-col")}>
|
||||
<div className={classNames("flex flex-1 space-x-2 w-full p-4 items-center")}>
|
||||
<Image width={40} height={40} src={`/${props.imageSrc}`} alt={props.title} />
|
||||
<div className="pl-2 flex-grow truncate">
|
||||
<div className="flex-grow pl-2 truncate">
|
||||
<ListItemTitle component="h3">{props.title}</ListItemTitle>
|
||||
<ListItemText component="p">{props.description}</ListItemText>
|
||||
</div>
|
||||
|
@ -205,7 +208,7 @@ function IntegrationListItem(props: {
|
|||
}
|
||||
|
||||
export function CalendarSwitch(props: {
|
||||
type: IntegrationCalendar["type"];
|
||||
type: string;
|
||||
externalId: string;
|
||||
title: string;
|
||||
defaultSelected: boolean;
|
||||
|
@ -353,7 +356,7 @@ export default function IntegrationsPage() {
|
|||
)}
|
||||
/>
|
||||
}>
|
||||
<ul className="space-y-2 p-4">
|
||||
<ul className="p-4 space-y-2">
|
||||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
key={cal.externalId}
|
||||
|
|
17
public/integrations/daily.svg
Normal file
17
public/integrations/daily.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 16 KiB |
|
@ -38,6 +38,9 @@ async function getUserFromSession({ session, req }: { session: Maybe<Session>; r
|
|||
type: true,
|
||||
key: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
selectedCalendars: {
|
||||
select: {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { BookingStatus, Prisma } from "@prisma/client";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import _ from "lodash";
|
||||
import { getErrorFromUnknown } from "pages/_error";
|
||||
import { z } from "zod";
|
||||
|
||||
import { checkPremiumUsername } from "@ee/lib/core/checkPremiumUsername";
|
||||
|
||||
import { checkRegularUsername } from "@lib/core/checkRegularUsername";
|
||||
import getIntegrations, { ALL_INTEGRATIONS } from "@lib/integrations/getIntegrations";
|
||||
import { ALL_INTEGRATIONS } from "@lib/integrations/getIntegrations";
|
||||
import slugify from "@lib/slugify";
|
||||
|
||||
import { getCalendarAdapterOrNull } from "../../lib/calendarClient";
|
||||
|
@ -20,7 +21,34 @@ const checkUsername =
|
|||
export const viewerRouter = createProtectedRouter()
|
||||
.query("me", {
|
||||
resolve({ ctx }) {
|
||||
return ctx.user;
|
||||
const {
|
||||
// pick only the part we want to expose in the API
|
||||
id,
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
startTime,
|
||||
endTime,
|
||||
bufferTime,
|
||||
locale,
|
||||
avatar,
|
||||
createdDate,
|
||||
completedOnboarding,
|
||||
} = ctx.user;
|
||||
const me = {
|
||||
id,
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
startTime,
|
||||
endTime,
|
||||
bufferTime,
|
||||
locale,
|
||||
avatar,
|
||||
createdDate,
|
||||
completedOnboarding,
|
||||
};
|
||||
return me;
|
||||
},
|
||||
})
|
||||
.query("bookings", {
|
||||
|
@ -98,11 +126,17 @@ export const viewerRouter = createProtectedRouter()
|
|||
async resolve({ ctx }) {
|
||||
const { user } = ctx;
|
||||
const { credentials } = user;
|
||||
const integrations = getIntegrations(credentials);
|
||||
|
||||
function countActive(items: { credentials: unknown[] }[]) {
|
||||
return items.reduce((acc, item) => acc + item.credentials.length, 0);
|
||||
function countActive(items: { credentialIds: unknown[] }[]) {
|
||||
return items.reduce((acc, item) => acc + item.credentialIds.length, 0);
|
||||
}
|
||||
const integrations = ALL_INTEGRATIONS.map((integration) => ({
|
||||
...integration,
|
||||
credentialIds: credentials
|
||||
.filter((credential) => credential.type === integration.type)
|
||||
.map((credential) => credential.id),
|
||||
}));
|
||||
// `flatMap()` these work like `.filter()` but infers the types correctly
|
||||
const conferencing = integrations.flatMap((item) => (item.variant === "conferencing" ? [item] : []));
|
||||
const payment = integrations.flatMap((item) => (item.variant === "payment" ? [item] : []));
|
||||
const calendar = integrations.flatMap((item) => (item.variant === "calendar" ? [item] : []));
|
||||
|
@ -126,25 +160,24 @@ export const viewerRouter = createProtectedRouter()
|
|||
const connectedCalendars = await Promise.all(
|
||||
calendarCredentials.map(async (item) => {
|
||||
const { adapter, integration, credential } = item;
|
||||
|
||||
const credentialId = credential.id;
|
||||
try {
|
||||
const _calendars = await adapter.listCalendars();
|
||||
const calendars = _calendars.map((cal) => ({
|
||||
...cal,
|
||||
isSelected: !!user.selectedCalendars.find((selected) => selected.externalId === cal.externalId),
|
||||
}));
|
||||
const cals = await adapter.listCalendars();
|
||||
const calendars = _(cals)
|
||||
.map((cal) => ({
|
||||
...cal,
|
||||
isSelected: user.selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
||||
}))
|
||||
.sortBy(["primary"])
|
||||
.value();
|
||||
const primary = calendars.find((item) => item.primary) ?? calendars[0];
|
||||
if (!primary) {
|
||||
return {
|
||||
integration,
|
||||
credentialId: credential.id,
|
||||
error: {
|
||||
message: "No primary calendar found",
|
||||
},
|
||||
};
|
||||
throw new Error("No primary calendar found");
|
||||
}
|
||||
return {
|
||||
integration,
|
||||
credentialId: credential.id,
|
||||
credentialId,
|
||||
primary,
|
||||
calendars,
|
||||
};
|
||||
|
@ -152,6 +185,7 @@ export const viewerRouter = createProtectedRouter()
|
|||
const error = getErrorFromUnknown(_error);
|
||||
return {
|
||||
integration,
|
||||
credentialId,
|
||||
error: {
|
||||
message: error.message,
|
||||
},
|
||||
|
|
28
yarn.lock
28
yarn.lock
|
@ -1790,26 +1790,14 @@
|
|||
version "7.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
|
||||
|
||||
"@types/lodash.debounce@^4.0.6":
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz#c5a2326cd3efc46566c47e4c0aa248dc0ee57d60"
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash.merge@^4.6.6":
|
||||
version "4.6.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash.merge/-/lodash.merge-4.6.6.tgz#b84b403c1d31bc42d51772d1cd5557fa008cd3d6"
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*", "@types/lodash@^4.14.165":
|
||||
version "4.14.175"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45"
|
||||
|
||||
"@types/lodash@4.14.168":
|
||||
version "4.14.168"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
|
||||
|
||||
"@types/lodash@^4.14.165", "@types/lodash@^4.14.175":
|
||||
version "4.14.175"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45"
|
||||
|
||||
"@types/micro@^7.3.6":
|
||||
version "7.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/micro/-/micro-7.3.6.tgz#7d68eb5a780ac4761e3b80687b4ee7328ebc3f2e"
|
||||
|
@ -5712,10 +5700,6 @@ lodash.clonedeep@^4.5.0:
|
|||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||
|
||||
lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
|
||||
lodash.flattendeep@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
|
||||
|
@ -5757,10 +5741,6 @@ lodash.sortby@^4.7.0:
|
|||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
|
||||
lodash.throttle@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
|
||||
|
||||
lodash.topath@^4.5.2:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009"
|
||||
|
|
Loading…
Reference in a new issue