import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline"; import { ClipboardIcon } from "@heroicons/react/solid"; 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")} )} deleteWebhook.mutate()}> {t("delete_webhook_confirmation_message")}
); } 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 (
{ form .handleSubmit(async (values) => { const { id } = values; const body = { subscriberUrl: values.subscriberUrl, enabled: values.active, eventTriggers: values.eventTriggers, }; if (id) { await fetcher.patch(`/api/webhooks/${id}`, body); await utils.invalidateQueries(["viewer.integrations"]); showToast(t("webhook_updated_successfully"), "success"); } else { await fetcher.post("/api/webhook", body); await utils.invalidateQueries(["viewer.integrations"]); showToast(t("webhook_created_successfully"), "success"); } props.handleClose(); })(event) .catch((err) => { showToast(`${getErrorFromUnknown(err).message}`, "error"); }); }} className="space-y-4">
{t("event_triggers")} {ALL_TRIGGERS.map((key) => ( ( { const value = field.value; const newValue = isChecked ? [...value, key] : value.filter((v) => v !== key); form.setValue("eventTriggers", newValue, { shouldDirty: true, }); }} /> )} /> ))}
{t("webhook_status")} ( { form.setValue("active", isChecked); }} /> )} />
); } 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
Webhooks Automation
{props.webhooks.length ? ( {props.webhooks.map((item) => ( { setEditing(item); setEditModalOpen(true); }} /> ))} ) : null}
{/* {!!props.webhooks.length && ( {}} onEditWebhook={editWebhook}> )} */}
Embed
{t("standard_iframe")} Embed your calendar within your webpage
Embed
{t("responsive_fullscreen_iframe")} A fullscreen scheduling experience on your website
{t("browse_api_documentation")}
{/* New webhook dialog */} !isOpen && setNewWebhookModal(false)}> setNewWebhookModal(false)} /> {/* Edit webhook dialog */} !isOpen && setEditModalOpen(false)}> {editing && ( setEditModalOpen(false)} defaultValues={editing} /> )} ); } 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 ( <> { mutation.mutate(); }}> Are you sure you want to disconnect this integration? {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.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) => ( ( )} /> } /> ))} ); }} />
); }