import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
import { WebhookTriggerEvents } from "@prisma/client";
import Image from "next/image";
import { getErrorFromUnknown } from "pages/_error";
import { Fragment, ReactNode, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useMutation } from "react-query";
import { QueryCell } from "@lib/QueryCell";
import classNames from "@lib/classNames";
import * as fetcher from "@lib/core/http/fetch-wrapper";
import { useLocale } from "@lib/hooks/useLocale";
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 { Dialog, DialogContent, DialogFooter, DialogTrigger } from "@components/Dialog";
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
import Shell, { ShellSubHeading } from "@components/Shell";
import { Tooltip } from "@components/Tooltip";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import { FieldsetLegend, Form, InputGroupBox, TextField } from "@components/form/fields";
import { Alert } from "@components/ui/Alert";
import Badge from "@components/ui/Badge";
import Button, { ButtonBaseProps } from "@components/ui/Button";
import Switch from "@components/ui/Switch";
function pluralize(opts: { num: number; plural: string; singular: string }) {
if (opts.num === 0) {
return opts.singular;
}
return opts.singular;
}
type TIntegrations = inferQueryOutput<"viewer.integrations">;
type TWebhook = TIntegrations["webhooks"][number];
const ALL_TRIGGERS: WebhookTriggerEvents[] = [
//
"BOOKING_CREATED",
"BOOKING_RESCHEDULED",
"BOOKING_CANCELLED",
];
function WebhookListItem(props: { webhook: TWebhook; onEditWebhook: () => void }) {
const { t } = useLocale();
const utils = trpc.useContext();
const deleteWebhook = useMutation(async () => fetcher.remove(`/api/webhooks/${props.webhook.id}`, null), {
async onSuccess() {
await utils.invalidateQueries(["viewer.integrations"]);
},
});
return (
{props.webhook.eventTriggers.map((eventTrigger, ind) => (
{t(`${eventTrigger.toLowerCase()}`)}
))}
{props.webhook.subscriberUrl}
{!props.webhook.active && (
{t("disabled")}
)}
{!!props.webhook.active && (
{t("enabled")}
)}
);
}
function WebhookDialogForm(props: {
//
defaultValues?: TWebhook;
handleClose: () => void;
}) {
const { t } = useLocale();
const utils = trpc.useContext();
const {
defaultValues = {
id: "",
eventTriggers: ALL_TRIGGERS,
subscriberUrl: "",
active: true,
},
} = props;
const form = useForm({
defaultValues,
});
return (
);
}
function WebhookEmbed(props: { webhooks: TWebhook[] }) {
const { t } = useLocale();
const user = trpc.useQuery(["viewer.me"]).data;
const iframeTemplate = ``;
const htmlTemplate = `${t(
"schedule_a_meeting"
)}${iframeTemplate}`;
const [newWebhookModal, setNewWebhookModal] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [editing, setEditing] = useState(null);
return (
<>
Webhooks
Automation
{props.webhooks.length ? (
{props.webhooks.map((item) => (
{
setEditing(item);
setEditModalOpen(true);
}}
/>
))}
) : null}
{/* {!!props.webhooks.length && (
{}}
onEditWebhook={editWebhook}>
)} */}
{/* New webhook dialog */}
{/* Edit webhook dialog */}
>
);
}
function SubHeadingTitleWithConnections(props: { title: ReactNode; numConnections?: number }) {
const num = props.numConnections;
return (
<>
{props.title}
{num ? (
{num}{" "}
{pluralize({
num,
singular: "connection",
plural: "connections",
})}
) : null}
>
);
}
function ConnectIntegration(props: { type: string; render: (renderProps: ButtonBaseProps) => JSX.Element }) {
const { type } = props;
const [isLoading, setIsLoading] = useState(false);
const mutation = useMutation(async () => {
const res = await fetch("/api/integrations/" + type.replace("_", "") + "/add");
if (!res.ok) {
throw new Error("Something went wrong");
}
const json = await res.json();
window.location.href = json.url;
setIsLoading(true);
});
const [isModalOpen, _setIsModalOpen] = useState(false);
const utils = trpc.useContext();
const setIsModalOpen: typeof _setIsModalOpen = (v) => {
_setIsModalOpen(v);
// refetch intergrations on modal toggles
utils.invalidateQueries(["viewer.integrations"]);
};
return (
<>
{props.render({
onClick() {
if (["caldav_calendar", "apple_calendar"].includes(type)) {
// special handlers
setIsModalOpen(true);
return;
}
mutation.mutate();
},
loading: mutation.isLoading || isLoading,
disabled: isModalOpen,
})}
{type === "caldav_calendar" && (
)}
{type === "apple_calendar" && (
)}
>
);
}
function DisconnectIntegration(props: {
/**
* Integration credential id
*/
id: number;
render: (renderProps: ButtonBaseProps) => JSX.Element;
}) {
const utils = trpc.useContext();
const [modalOpen, setModalOpen] = useState(false);
const mutation = useMutation(
async () => {
const res = await fetch("/api/integrations", {
method: "DELETE",
body: JSON.stringify({ id: props.id }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error("Something went wrong");
}
},
{
async onSettled() {
await utils.invalidateQueries(["viewer.integrations"]);
},
onSuccess() {
setModalOpen(false);
},
}
);
return (
<>
{props.render({
onClick() {
setModalOpen(true);
},
disabled: modalOpen,
loading: mutation.isLoading,
})}
>
);
}
function ConnectOrDisconnectIntegrationButton(props: {
//
credentialIds: number[];
type: string;
installed: boolean;
}) {
const [credentialId] = props.credentialIds;
if (credentialId) {
return (
(
)}
/>
);
}
if (!props.installed) {
return (
);
}
/** We don't need to "Connect", just show that it's installed */
if (props.type === "daily_video") {
return (
Installed
);
}
return (
(
)}
/>
);
}
function IntegrationListItem(props: {
imageSrc: string;
title: string;
description: string;
actions?: ReactNode;
children?: ReactNode;
}) {
return (
{props.title}
{props.description}
{props.actions}
{props.children && {props.children}
}
);
}
export function CalendarSwitch(props: {
type: string;
externalId: string;
title: string;
defaultSelected: boolean;
}) {
const utils = trpc.useContext();
const mutation = useMutation<
unknown,
unknown,
{
isOn: boolean;
}
>(
async ({ isOn }) => {
const body = {
integration: props.type,
externalId: props.externalId,
};
if (isOn) {
const res = await fetch("/api/availability/calendar", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error("Something went wrong");
}
} else {
const res = await fetch("/api/availability/calendar", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error("Something went wrong");
}
}
},
{
async onSettled() {
await utils.invalidateQueries(["viewer.integrations"]);
},
onError() {
showToast(`Something went wrong when toggling "${props.title}""`, "error");
},
}
);
return (
{
mutation.mutate({ isOn });
}}
/>
);
}
export default function IntegrationsPage() {
const query = trpc.useQuery(["viewer.integrations"]);
return (
{
return (
<>
}
/>
{data.conferencing.items.map((item) => (
}
/>
))}
}
/>
{data.payment.items.map((item) => (
}
/>
))}
}
subtitle={
<>
Configure how your links integrate with your calendars.
You can override these settings on a per event basis.
>
}
/>
{data.connectedCalendars.length > 0 && (
<>
{data.connectedCalendars.map((item) => (
{item.calendars ? (
(
)}
/>
}>
{item.calendars.map((cal) => (
))}
) : (
(
)}
/>
}
/>
)}
))}
}
/>
>
)}
{data.calendar.items.map((item) => (
(
)}
/>
}
/>
))}
>
);
}}
/>
);
}