refactor /integrations
with <Suspense />
(#1078)
* suspense * iframe embeds * calendar list container * rename things as a container * use list container on onboarding * fix * rm code * newer alpha * make it work in react 17 * fix * fix * make components handle error state through `QueryCell` * fix constant * fix type error * type error * type fixes * fix package.lock * fix webhook invalidate * fix mt * fix typo * pr comment
This commit is contained in:
parent
78523f7a57
commit
1790aeb577
16 changed files with 414 additions and 472 deletions
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -5,5 +5,9 @@
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": true
|
||||||
},
|
},
|
||||||
"eslint.run": "onSave"
|
"eslint.run": "onSave",
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"titleBar.activeBackground": "#888888",
|
||||||
|
"titleBar.inactiveBackground": "#292929"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
9
components/ClientSuspense.tsx
Normal file
9
components/ClientSuspense.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Suspense, SuspenseProps } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around `<Suspense />` which will render the `fallback` when on server
|
||||||
|
* Can be simply replaced by `<Suspense />` once React 18 is ready.
|
||||||
|
*/
|
||||||
|
export const ClientSuspense = (props: SuspenseProps) => {
|
||||||
|
return <>{typeof window !== "undefined" ? <Suspense {...props} /> : props.fallback}</>;
|
||||||
|
};
|
220
components/integrations/CalendarListContainer.tsx
Normal file
220
components/integrations/CalendarListContainer.tsx
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
import React, { Fragment } from "react";
|
||||||
|
import { useMutation } from "react-query";
|
||||||
|
|
||||||
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
import showToast from "@lib/notification";
|
||||||
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import { List } from "@components/List";
|
||||||
|
import { ShellSubHeading } from "@components/Shell";
|
||||||
|
import { Alert } from "@components/ui/Alert";
|
||||||
|
import Button from "@components/ui/Button";
|
||||||
|
import Switch from "@components/ui/Switch";
|
||||||
|
|
||||||
|
import ConnectIntegration from "./ConnectIntegrations";
|
||||||
|
import DisconnectIntegration from "./DisconnectIntegration";
|
||||||
|
import IntegrationListItem from "./IntegrationListItem";
|
||||||
|
import SubHeadingTitleWithConnections from "./SubHeadingTitleWithConnections";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onChanged: () => unknown | Promise<unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="py-1">
|
||||||
|
<Switch
|
||||||
|
key={props.externalId}
|
||||||
|
name="enabled"
|
||||||
|
label={props.title}
|
||||||
|
defaultChecked={props.defaultSelected}
|
||||||
|
onCheckedChange={(isOn: boolean) => {
|
||||||
|
mutation.mutate({ isOn });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConnectedCalendarsList(props: Props) {
|
||||||
|
const query = trpc.useQuery(["viewer.connectedCalendars"], { suspense: true });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryCell
|
||||||
|
query={query}
|
||||||
|
empty={() => null}
|
||||||
|
success={({ data }) => (
|
||||||
|
<List>
|
||||||
|
{data.map((item) => (
|
||||||
|
<Fragment key={item.credentialId}>
|
||||||
|
{item.calendars ? (
|
||||||
|
<IntegrationListItem
|
||||||
|
{...item.integration}
|
||||||
|
description={item.primary?.externalId || "No external Id"}
|
||||||
|
actions={
|
||||||
|
<DisconnectIntegration
|
||||||
|
id={item.credentialId}
|
||||||
|
render={(btnProps) => (
|
||||||
|
<Button {...btnProps} color="warn">
|
||||||
|
Disconnect
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
onOpenChange={props.onChanged}
|
||||||
|
/>
|
||||||
|
}>
|
||||||
|
<ul className="p-4 space-y-2">
|
||||||
|
{item.calendars.map((cal) => (
|
||||||
|
<CalendarSwitch
|
||||||
|
key={cal.externalId}
|
||||||
|
externalId={cal.externalId as string}
|
||||||
|
title={cal.name as string}
|
||||||
|
type={item.integration.type}
|
||||||
|
defaultSelected={cal.isSelected}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</IntegrationListItem>
|
||||||
|
) : (
|
||||||
|
<Alert
|
||||||
|
severity="warning"
|
||||||
|
title="Something went wrong"
|
||||||
|
message={item.error?.message}
|
||||||
|
actions={
|
||||||
|
<DisconnectIntegration
|
||||||
|
id={item.credentialId}
|
||||||
|
render={(btnProps) => (
|
||||||
|
<Button {...btnProps} color="warn">
|
||||||
|
Disconnect
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
onOpenChange={() => props.onChanged()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarList(props: Props) {
|
||||||
|
const query = trpc.useQuery(["viewer.integrations"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryCell
|
||||||
|
query={query}
|
||||||
|
success={({ data }) => (
|
||||||
|
<List>
|
||||||
|
{data.calendar.items.map((item) => (
|
||||||
|
<IntegrationListItem
|
||||||
|
key={item.title}
|
||||||
|
{...item}
|
||||||
|
actions={
|
||||||
|
<ConnectIntegration
|
||||||
|
type={item.type}
|
||||||
|
render={(btnProps) => (
|
||||||
|
<Button color="secondary" {...btnProps}>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
onOpenChange={() => props.onChanged()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function CalendarListContainer(props: { heading?: false }) {
|
||||||
|
const { heading = true } = props;
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const onChanged = () =>
|
||||||
|
Promise.allSettled([
|
||||||
|
utils.invalidateQueries(["viewer.integrations"]),
|
||||||
|
utils.invalidateQueries(["viewer.connectedCalendars"]),
|
||||||
|
]);
|
||||||
|
const query = trpc.useQuery(["viewer.connectedCalendars"]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{heading && (
|
||||||
|
<ShellSubHeading
|
||||||
|
className="mt-10"
|
||||||
|
title={<SubHeadingTitleWithConnections title="Calendars" numConnections={query.data?.length} />}
|
||||||
|
subtitle={
|
||||||
|
<>
|
||||||
|
Configure how your links integrate with your calendars.
|
||||||
|
<br />
|
||||||
|
You can override these settings on a per event basis.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ConnectedCalendarsList onChanged={onChanged} />
|
||||||
|
{!!query.data?.length && (
|
||||||
|
<ShellSubHeading
|
||||||
|
className="mt-6"
|
||||||
|
title={<SubHeadingTitleWithConnections title="Connect an additional calendar" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<CalendarList onChanged={onChanged} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,75 +0,0 @@
|
||||||
import { useMutation } from "react-query";
|
|
||||||
|
|
||||||
import showToast from "@lib/notification";
|
|
||||||
import { trpc } from "@lib/trpc";
|
|
||||||
|
|
||||||
import Switch from "@components/ui/Switch";
|
|
||||||
|
|
||||||
export default 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 (
|
|
||||||
<div className="py-1">
|
|
||||||
<Switch
|
|
||||||
key={props.externalId}
|
|
||||||
name="enabled"
|
|
||||||
label={props.title}
|
|
||||||
defaultChecked={props.defaultSelected}
|
|
||||||
onCheckedChange={(isOn: boolean) => {
|
|
||||||
mutation.mutate({ isOn });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
import React, { ReactNode } from "react";
|
|
||||||
|
|
||||||
import { List } from "@components/List";
|
|
||||||
import Button from "@components/ui/Button";
|
|
||||||
|
|
||||||
import ConnectIntegration from "./ConnectIntegrations";
|
|
||||||
import IntegrationListItem from "./IntegrationListItem";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
calendars: {
|
|
||||||
children?: ReactNode;
|
|
||||||
description: string;
|
|
||||||
imageSrc: string;
|
|
||||||
title: string;
|
|
||||||
type: string;
|
|
||||||
}[];
|
|
||||||
onChanged: () => void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CalendarsList = (props: Props): JSX.Element => {
|
|
||||||
const { calendars, onChanged } = props;
|
|
||||||
return (
|
|
||||||
<List>
|
|
||||||
{calendars.map((item) => (
|
|
||||||
<IntegrationListItem
|
|
||||||
key={item.title}
|
|
||||||
{...item}
|
|
||||||
actions={
|
|
||||||
<ConnectIntegration
|
|
||||||
type={item.type}
|
|
||||||
render={(btnProps) => (
|
|
||||||
<Button color="secondary" {...btnProps}>
|
|
||||||
Connect
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
onOpenChange={onChanged}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarsList;
|
|
|
@ -9,7 +9,7 @@ import { ButtonBaseProps } from "@components/ui/Button";
|
||||||
export default function ConnectIntegration(props: {
|
export default function ConnectIntegration(props: {
|
||||||
type: string;
|
type: string;
|
||||||
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
||||||
onOpenChange: (isOpen: boolean) => void | Promise<void>;
|
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
|
||||||
}) {
|
}) {
|
||||||
const { type } = props;
|
const { type } = props;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
|
@ -1,98 +0,0 @@
|
||||||
import React, { Fragment, ReactNode } from "react";
|
|
||||||
|
|
||||||
import { List } from "@components/List";
|
|
||||||
import { Alert } from "@components/ui/Alert";
|
|
||||||
import Button from "@components/ui/Button";
|
|
||||||
|
|
||||||
import CalendarSwitch from "./CalendarSwitch";
|
|
||||||
import DisconnectIntegration from "./DisconnectIntegration";
|
|
||||||
import IntegrationListItem from "./IntegrationListItem";
|
|
||||||
|
|
||||||
type CalIntersection =
|
|
||||||
| {
|
|
||||||
calendars: {
|
|
||||||
externalId: string;
|
|
||||||
name: string;
|
|
||||||
isSelected: boolean;
|
|
||||||
}[];
|
|
||||||
error?: never;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
calendars?: never;
|
|
||||||
error: {
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onChanged: (isOpen: boolean) => void | Promise<void>;
|
|
||||||
connectedCalendars: (CalIntersection & {
|
|
||||||
credentialId: number;
|
|
||||||
integration: {
|
|
||||||
type: string;
|
|
||||||
imageSrc: string;
|
|
||||||
title: string;
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
primary?: { externalId: string } | undefined | null;
|
|
||||||
})[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedCalendarsList = (props: Props): JSX.Element => {
|
|
||||||
const { connectedCalendars, onChanged } = props;
|
|
||||||
return (
|
|
||||||
<List>
|
|
||||||
{connectedCalendars.map((item) => (
|
|
||||||
<Fragment key={item.credentialId}>
|
|
||||||
{item.calendars ? (
|
|
||||||
<IntegrationListItem
|
|
||||||
{...item.integration}
|
|
||||||
description={item.primary?.externalId || "No external Id"}
|
|
||||||
actions={
|
|
||||||
<DisconnectIntegration
|
|
||||||
id={item.credentialId}
|
|
||||||
render={(btnProps) => (
|
|
||||||
<Button {...btnProps} color="warn">
|
|
||||||
Disconnect
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
onOpenChange={onChanged}
|
|
||||||
/>
|
|
||||||
}>
|
|
||||||
<ul className="p-4 space-y-2">
|
|
||||||
{item.calendars.map((cal) => (
|
|
||||||
<CalendarSwitch
|
|
||||||
key={cal.externalId}
|
|
||||||
externalId={cal.externalId}
|
|
||||||
title={cal.name}
|
|
||||||
type={item.integration.type}
|
|
||||||
defaultSelected={cal.isSelected}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</IntegrationListItem>
|
|
||||||
) : (
|
|
||||||
<Alert
|
|
||||||
severity="warning"
|
|
||||||
title="Something went wrong"
|
|
||||||
message={item.error?.message}
|
|
||||||
actions={
|
|
||||||
<DisconnectIntegration
|
|
||||||
id={item.credentialId}
|
|
||||||
render={(btnProps) => (
|
|
||||||
<Button {...btnProps} color="warn">
|
|
||||||
Disconnect
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
onOpenChange={onChanged}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConnectedCalendarsList;
|
|
|
@ -9,7 +9,7 @@ export default function DisconnectIntegration(props: {
|
||||||
/** Integration credential id */
|
/** Integration credential id */
|
||||||
id: number;
|
id: number;
|
||||||
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
||||||
onOpenChange: (isOpen: boolean) => void | Promise<void>;
|
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
|
||||||
}) {
|
}) {
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const mutation = useMutation(
|
const mutation = useMutation(
|
||||||
|
|
|
@ -13,36 +13,37 @@ import { Alert } from "@components/ui/Alert";
|
||||||
type ErrorLike = {
|
type ErrorLike = {
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
type JSXElementOrNull = JSX.Element | null;
|
||||||
|
|
||||||
interface QueryCellOptionsBase<TData, TError extends ErrorLike> {
|
interface QueryCellOptionsBase<TData, TError extends ErrorLike> {
|
||||||
query: UseQueryResult<TData, TError>;
|
query: UseQueryResult<TData, TError>;
|
||||||
error?: (
|
error?: (
|
||||||
query: QueryObserverLoadingErrorResult<TData, TError> | QueryObserverRefetchErrorResult<TData, TError>
|
query: QueryObserverLoadingErrorResult<TData, TError> | QueryObserverRefetchErrorResult<TData, TError>
|
||||||
) => JSX.Element;
|
) => JSXElementOrNull;
|
||||||
loading?: (query: QueryObserverLoadingResult<TData, TError>) => JSX.Element;
|
loading?: (query: QueryObserverLoadingResult<TData, TError>) => JSXElementOrNull;
|
||||||
idle?: (query: QueryObserverIdleResult<TData, TError>) => JSX.Element;
|
idle?: (query: QueryObserverIdleResult<TData, TError>) => JSXElementOrNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryCellOptionsNoEmpty<TData, TError extends ErrorLike>
|
interface QueryCellOptionsNoEmpty<TData, TError extends ErrorLike>
|
||||||
extends QueryCellOptionsBase<TData, TError> {
|
extends QueryCellOptionsBase<TData, TError> {
|
||||||
success: (query: QueryObserverSuccessResult<TData, TError>) => JSX.Element;
|
success: (query: QueryObserverSuccessResult<TData, TError>) => JSXElementOrNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryCellOptionsWithEmpty<TData, TError extends ErrorLike>
|
interface QueryCellOptionsWithEmpty<TData, TError extends ErrorLike>
|
||||||
extends QueryCellOptionsBase<TData, TError> {
|
extends QueryCellOptionsBase<TData, TError> {
|
||||||
success: (query: QueryObserverSuccessResult<NonNullable<TData>, TError>) => JSX.Element;
|
success: (query: QueryObserverSuccessResult<NonNullable<TData>, TError>) => JSXElementOrNull;
|
||||||
/**
|
/**
|
||||||
* If there's no data (`null`, `undefined`, or `[]`), render this component
|
* If there's no data (`null`, `undefined`, or `[]`), render this component
|
||||||
*/
|
*/
|
||||||
empty: (query: QueryObserverSuccessResult<TData, TError>) => JSX.Element;
|
empty: (query: QueryObserverSuccessResult<TData, TError>) => JSXElementOrNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QueryCell<TData, TError extends ErrorLike>(
|
export function QueryCell<TData, TError extends ErrorLike>(
|
||||||
opts: QueryCellOptionsWithEmpty<TData, TError>
|
opts: QueryCellOptionsWithEmpty<TData, TError>
|
||||||
): JSX.Element;
|
): JSXElementOrNull;
|
||||||
export function QueryCell<TData, TError extends ErrorLike>(
|
export function QueryCell<TData, TError extends ErrorLike>(
|
||||||
opts: QueryCellOptionsNoEmpty<TData, TError>
|
opts: QueryCellOptionsNoEmpty<TData, TError>
|
||||||
): JSX.Element;
|
): JSXElementOrNull;
|
||||||
export function QueryCell<TData, TError extends ErrorLike>(
|
export function QueryCell<TData, TError extends ErrorLike>(
|
||||||
opts: QueryCellOptionsNoEmpty<TData, TError> | QueryCellOptionsWithEmpty<TData, TError>
|
opts: QueryCellOptionsNoEmpty<TData, TError> | QueryCellOptionsWithEmpty<TData, TError>
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -5,4 +5,4 @@ export const WEBHOOK_TRIGGER_EVENTS = [
|
||||||
WebhookTriggerEvents.BOOKING_CANCELLED,
|
WebhookTriggerEvents.BOOKING_CANCELLED,
|
||||||
WebhookTriggerEvents.BOOKING_CREATED,
|
WebhookTriggerEvents.BOOKING_CREATED,
|
||||||
WebhookTriggerEvents.BOOKING_RESCHEDULED,
|
WebhookTriggerEvents.BOOKING_RESCHEDULED,
|
||||||
] as const;
|
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"];
|
||||||
|
|
|
@ -36,8 +36,8 @@
|
||||||
"@heroicons/react": "^1.0.4",
|
"@heroicons/react": "^1.0.4",
|
||||||
"@hookform/resolvers": "^2.8.1",
|
"@hookform/resolvers": "^2.8.1",
|
||||||
"@jitsu/sdk-js": "^2.2.4",
|
"@jitsu/sdk-js": "^2.2.4",
|
||||||
"@prisma/client": "^2.30.2",
|
|
||||||
"@next/bundle-analyzer": "11.1.2",
|
"@next/bundle-analyzer": "11.1.2",
|
||||||
|
"@prisma/client": "^2.30.2",
|
||||||
"@radix-ui/react-avatar": "^0.1.0",
|
"@radix-ui/react-avatar": "^0.1.0",
|
||||||
"@radix-ui/react-collapsible": "^0.1.0",
|
"@radix-ui/react-collapsible": "^0.1.0",
|
||||||
"@radix-ui/react-dialog": "^0.1.0",
|
"@radix-ui/react-dialog": "^0.1.0",
|
||||||
|
@ -75,8 +75,8 @@
|
||||||
"nodemailer": "^6.6.3",
|
"nodemailer": "^6.6.3",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"react": "17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-easy-crop": "^3.5.2",
|
"react-easy-crop": "^3.5.2",
|
||||||
"react-hook-form": "^7.17.5",
|
"react-hook-form": "^7.17.5",
|
||||||
"react-hot-toast": "^2.1.0",
|
"react-hot-toast": "^2.1.0",
|
||||||
|
|
|
@ -19,11 +19,9 @@ import getIntegrations from "@lib/integrations/getIntegrations";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import { ClientSuspense } from "@components/ClientSuspense";
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
import { ShellSubHeading } from "@components/Shell";
|
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
||||||
import CalendarsList from "@components/integrations/CalendarsList";
|
|
||||||
import ConnectedCalendarsList from "@components/integrations/ConnectedCalendarsList";
|
|
||||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
|
||||||
import { Alert } from "@components/ui/Alert";
|
import { Alert } from "@components/ui/Alert";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
import SchedulerForm, { SCHEDULE_FORM_ID } from "@components/ui/Schedule/Schedule";
|
import SchedulerForm, { SCHEDULE_FORM_ID } from "@components/ui/Schedule/Schedule";
|
||||||
|
@ -41,10 +39,6 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const refreshData = () => {
|
|
||||||
router.replace(router.asPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_EVENT_TYPES = [
|
const DEFAULT_EVENT_TYPES = [
|
||||||
{
|
{
|
||||||
title: t("15min_meeting"),
|
title: t("15min_meeting"),
|
||||||
|
@ -123,12 +117,9 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
||||||
const bioRef = useRef<HTMLInputElement>(null);
|
const bioRef = useRef<HTMLInputElement>(null);
|
||||||
/** End Name */
|
/** End Name */
|
||||||
/** TimeZone */
|
/** TimeZone */
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState({
|
const [selectedTimeZone, setSelectedTimeZone] = useState(props.user.timeZone ?? dayjs.tz.guess());
|
||||||
value: props.user.timeZone ?? dayjs.tz.guess(),
|
|
||||||
label: null,
|
|
||||||
});
|
|
||||||
const currentTime = React.useMemo(() => {
|
const currentTime = React.useMemo(() => {
|
||||||
return dayjs().tz(selectedTimeZone.value).format("H:mm A");
|
return dayjs().tz(selectedTimeZone).format("H:mm A");
|
||||||
}, [selectedTimeZone]);
|
}, [selectedTimeZone]);
|
||||||
/** End TimeZone */
|
/** End TimeZone */
|
||||||
|
|
||||||
|
@ -269,7 +260,9 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
id="timeZone"
|
id="timeZone"
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
onChange={setSelectedTimeZone}
|
onChange={({ value }) => {
|
||||||
|
setSelectedTimeZone(value);
|
||||||
|
}}
|
||||||
className="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
className="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@ -285,7 +278,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
await updateUser({
|
await updateUser({
|
||||||
name: nameRef.current?.value,
|
name: nameRef.current?.value,
|
||||||
timeZone: selectedTimeZone.value,
|
timeZone: selectedTimeZone,
|
||||||
});
|
});
|
||||||
setEnteredName(nameRef.current?.value || "");
|
setEnteredName(nameRef.current?.value || "");
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
@ -300,28 +293,9 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
||||||
title: t("connect_your_calendar"),
|
title: t("connect_your_calendar"),
|
||||||
description: t("connect_your_calendar_instructions"),
|
description: t("connect_your_calendar_instructions"),
|
||||||
Component: (
|
Component: (
|
||||||
<>
|
<ClientSuspense fallback={<Loader />}>
|
||||||
{props.connectedCalendars.length > 0 && (
|
<CalendarListContainer heading={false} />
|
||||||
<>
|
</ClientSuspense>
|
||||||
<ConnectedCalendarsList
|
|
||||||
connectedCalendars={props.connectedCalendars}
|
|
||||||
onChanged={() => {
|
|
||||||
refreshData();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShellSubHeading
|
|
||||||
className="mt-6"
|
|
||||||
title={<SubHeadingTitleWithConnections title="Connect an additional calendar" />}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<CalendarsList
|
|
||||||
calendars={props.integrations}
|
|
||||||
onChanged={() => {
|
|
||||||
refreshData();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
hideConfirm: true,
|
hideConfirm: true,
|
||||||
confirmText: t("continue"),
|
confirmText: t("continue"),
|
||||||
|
|
|
@ -19,15 +19,16 @@ import showToast from "@lib/notification";
|
||||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||||
import { WEBHOOK_TRIGGER_EVENTS } from "@lib/webhooks/constants";
|
import { WEBHOOK_TRIGGER_EVENTS } from "@lib/webhooks/constants";
|
||||||
|
|
||||||
|
import { ClientSuspense } from "@components/ClientSuspense";
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogTrigger } from "@components/Dialog";
|
import { Dialog, DialogContent, DialogFooter, DialogTrigger } from "@components/Dialog";
|
||||||
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
|
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
|
||||||
|
import Loader from "@components/Loader";
|
||||||
import Shell, { ShellSubHeading } from "@components/Shell";
|
import Shell, { ShellSubHeading } from "@components/Shell";
|
||||||
import { Tooltip } from "@components/Tooltip";
|
import { Tooltip } from "@components/Tooltip";
|
||||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||||
import { FieldsetLegend, Form, InputGroupBox, TextField } from "@components/form/fields";
|
import { FieldsetLegend, Form, InputGroupBox, TextField } from "@components/form/fields";
|
||||||
import CalendarsList from "@components/integrations/CalendarsList";
|
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
||||||
import ConnectIntegration from "@components/integrations/ConnectIntegrations";
|
import ConnectIntegration from "@components/integrations/ConnectIntegrations";
|
||||||
import ConnectedCalendarsList from "@components/integrations/ConnectedCalendarsList";
|
|
||||||
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
||||||
import IntegrationListItem from "@components/integrations/IntegrationListItem";
|
import IntegrationListItem from "@components/integrations/IntegrationListItem";
|
||||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
||||||
|
@ -35,15 +36,14 @@ import { Alert } from "@components/ui/Alert";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
import Switch from "@components/ui/Switch";
|
import Switch from "@components/ui/Switch";
|
||||||
|
|
||||||
type TIntegrations = inferQueryOutput<"viewer.integrations">;
|
type TWebhook = inferQueryOutput<"viewer.webhook.list">[number];
|
||||||
type TWebhook = TIntegrations["webhooks"][number];
|
|
||||||
|
|
||||||
function WebhookListItem(props: { webhook: TWebhook; onEditWebhook: () => void }) {
|
function WebhookListItem(props: { webhook: TWebhook; onEditWebhook: () => void }) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const deleteWebhook = trpc.useMutation("viewer.webhook.delete", {
|
const deleteWebhook = trpc.useMutation("viewer.webhook.delete", {
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
await utils.invalidateQueries(["viewer.integrations"]);
|
await utils.invalidateQueries(["viewer.webhhook.list"]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -195,11 +195,11 @@ function WebhookDialogForm(props: {
|
||||||
.handleSubmit(async (values) => {
|
.handleSubmit(async (values) => {
|
||||||
if (values.id) {
|
if (values.id) {
|
||||||
await utils.client.mutation("viewer.webhook.edit", values);
|
await utils.client.mutation("viewer.webhook.edit", values);
|
||||||
await utils.invalidateQueries(["viewer.integrations"]);
|
await utils.invalidateQueries(["viewer.webhook.list"]);
|
||||||
showToast(t("webhook_updated_successfully"), "success");
|
showToast(t("webhook_updated_successfully"), "success");
|
||||||
} else {
|
} else {
|
||||||
await utils.client.mutation("viewer.webhook.create", values);
|
await utils.client.mutation("viewer.webhook.create", values);
|
||||||
await utils.invalidateQueries(["viewer.integrations"]);
|
await utils.invalidateQueries(["viewer.webhook.list"]);
|
||||||
showToast(t("webhook_created_successfully"), "success");
|
showToast(t("webhook_created_successfully"), "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,8 +269,81 @@ function WebhookDialogForm(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WebhookEmbed(props: { webhooks: TWebhook[] }) {
|
function WebhookListContainer() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const query = trpc.useQuery(["viewer.webhook.list"], { suspense: true });
|
||||||
|
|
||||||
|
const [newWebhookModal, setNewWebhookModal] = useState(false);
|
||||||
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
|
const [editing, setEditing] = useState<TWebhook | null>(null);
|
||||||
|
return (
|
||||||
|
<QueryCell
|
||||||
|
query={query}
|
||||||
|
success={({ data }) => (
|
||||||
|
<>
|
||||||
|
<ShellSubHeading className="mt-10" title={t("Webhooks")} subtitle={t("receive_cal_meeting_data")} />
|
||||||
|
<List>
|
||||||
|
<ListItem className={classNames("flex-col")}>
|
||||||
|
<div className={classNames("flex flex-1 space-x-2 w-full p-3 items-center")}>
|
||||||
|
<Image width={40} height={40} src="/integrations/webhooks.svg" alt="Webhooks" />
|
||||||
|
<div className="flex-grow pl-2 truncate">
|
||||||
|
<ListItemTitle component="h3">Webhooks</ListItemTitle>
|
||||||
|
<ListItemText component="p">Automation</ListItemText>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => setNewWebhookModal(true)}
|
||||||
|
data-testid="new_webhook">
|
||||||
|
{t("new_webhook")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
{data.length ? (
|
||||||
|
<List>
|
||||||
|
{data.map((item) => (
|
||||||
|
<WebhookListItem
|
||||||
|
key={item.id}
|
||||||
|
webhook={item}
|
||||||
|
onEditWebhook={() => {
|
||||||
|
setEditing(item);
|
||||||
|
setEditModalOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* New webhook dialog */}
|
||||||
|
<Dialog open={newWebhookModal} onOpenChange={(isOpen) => !isOpen && setNewWebhookModal(false)}>
|
||||||
|
<DialogContent>
|
||||||
|
<WebhookDialogForm handleClose={() => setNewWebhookModal(false)} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
{/* Edit webhook dialog */}
|
||||||
|
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||||
|
<DialogContent>
|
||||||
|
{editing && (
|
||||||
|
<WebhookDialogForm
|
||||||
|
key={editing.id}
|
||||||
|
handleClose={() => setEditModalOpen(false)}
|
||||||
|
defaultValues={editing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IframeEmbedContainer() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
// doesn't need suspense as it should already be loaded
|
||||||
const user = trpc.useQuery(["viewer.me"]).data;
|
const user = trpc.useQuery(["viewer.me"]).data;
|
||||||
|
|
||||||
const iframeTemplate = `<iframe src="${process.env.NEXT_PUBLIC_BASE_URL}/${user?.username}" frameborder="0" allowfullscreen></iframe>`;
|
const iframeTemplate = `<iframe src="${process.env.NEXT_PUBLIC_BASE_URL}/${user?.username}" frameborder="0" allowfullscreen></iframe>`;
|
||||||
|
@ -278,57 +351,9 @@ function WebhookEmbed(props: { webhooks: TWebhook[] }) {
|
||||||
"schedule_a_meeting"
|
"schedule_a_meeting"
|
||||||
)}</title><style>body {margin: 0;}iframe {height: calc(100vh - 4px);width: calc(100vw - 4px);box-sizing: border-box;}</style></head><body>${iframeTemplate}</body></html>`;
|
)}</title><style>body {margin: 0;}iframe {height: calc(100vh - 4px);width: calc(100vw - 4px);box-sizing: border-box;}</style></head><body>${iframeTemplate}</body></html>`;
|
||||||
|
|
||||||
const [newWebhookModal, setNewWebhookModal] = useState(false);
|
|
||||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
|
||||||
const [editing, setEditing] = useState<TWebhook | null>(null);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShellSubHeading className="mt-10" title={t("Webhooks")} subtitle={t("receive_cal_meeting_data")} />
|
<ShellSubHeading title={t("iframe_embed")} subtitle={t("embed_calcom")} className="mt-10" />
|
||||||
<List>
|
|
||||||
<ListItem className={classNames("flex-col")}>
|
|
||||||
<div className={classNames("flex flex-1 space-x-2 w-full p-3 items-center")}>
|
|
||||||
<Image width={40} height={40} src="/integrations/webhooks.svg" alt="Webhooks" />
|
|
||||||
<div className="flex-grow pl-2 truncate">
|
|
||||||
<ListItemTitle component="h3">Webhooks</ListItemTitle>
|
|
||||||
<ListItemText component="p">Automation</ListItemText>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button color="secondary" onClick={() => setNewWebhookModal(true)} data-testid="new_webhook">
|
|
||||||
{t("new_webhook")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
|
|
||||||
{props.webhooks.length ? (
|
|
||||||
<List>
|
|
||||||
{props.webhooks.map((item) => (
|
|
||||||
<WebhookListItem
|
|
||||||
key={item.id}
|
|
||||||
webhook={item}
|
|
||||||
onEditWebhook={() => {
|
|
||||||
setEditing(item);
|
|
||||||
setEditModalOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
) : null}
|
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
|
||||||
<div className="py-6 lg:pb-8">
|
|
||||||
<div>
|
|
||||||
{/* {!!props.webhooks.length && (
|
|
||||||
<WebhookList
|
|
||||||
webhooks={props.webhooks}
|
|
||||||
onChange={() => {}}
|
|
||||||
onEditWebhook={editWebhook}></WebhookList>
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ShellSubHeading title={t("iframe_embed")} subtitle={t("embed_calcom")} />
|
|
||||||
<div className="lg:pb-8 lg:col-span-9">
|
<div className="lg:pb-8 lg:col-span-9">
|
||||||
<List>
|
<List>
|
||||||
<ListItem className={classNames("flex-col")}>
|
<ListItem className={classNames("flex-col")}>
|
||||||
|
@ -398,25 +423,6 @@ function WebhookEmbed(props: { webhooks: TWebhook[] }) {
|
||||||
{t("browse_api_documentation")}
|
{t("browse_api_documentation")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* New webhook dialog */}
|
|
||||||
<Dialog open={newWebhookModal} onOpenChange={(isOpen) => !isOpen && setNewWebhookModal(false)}>
|
|
||||||
<DialogContent>
|
|
||||||
<WebhookDialogForm handleClose={() => setNewWebhookModal(false)} />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
{/* Edit webhook dialog */}
|
|
||||||
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
|
||||||
<DialogContent>
|
|
||||||
{editing && (
|
|
||||||
<WebhookDialogForm
|
|
||||||
key={editing.id}
|
|
||||||
handleClose={() => setEditModalOpen(false)}
|
|
||||||
defaultValues={editing}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -474,89 +480,59 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function IntegrationsPage() {
|
function IntegrationsContainer() {
|
||||||
const query = trpc.useQuery(["viewer.integrations"]);
|
const query = trpc.useQuery(["viewer.integrations"], { suspense: true });
|
||||||
const utils = trpc.useContext();
|
|
||||||
const handleOpenChange = () => {
|
|
||||||
utils.invalidateQueries(["viewer.integrations"]);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryCell
|
||||||
|
query={query}
|
||||||
|
success={({ data }) => (
|
||||||
|
<>
|
||||||
|
<ShellSubHeading
|
||||||
|
title={
|
||||||
|
<SubHeadingTitleWithConnections
|
||||||
|
title="Conferencing"
|
||||||
|
numConnections={data.conferencing.numActive}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<List>
|
||||||
|
{data.conferencing.items.map((item) => (
|
||||||
|
<IntegrationListItem
|
||||||
|
key={item.title}
|
||||||
|
{...item}
|
||||||
|
actions={<ConnectOrDisconnectIntegrationButton {...item} />}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<ShellSubHeading
|
||||||
|
className="mt-10"
|
||||||
|
title={<SubHeadingTitleWithConnections title="Payment" numConnections={data.payment.numActive} />}
|
||||||
|
/>
|
||||||
|
<List>
|
||||||
|
{data.payment.items.map((item) => (
|
||||||
|
<IntegrationListItem
|
||||||
|
key={item.title}
|
||||||
|
{...item}
|
||||||
|
actions={<ConnectOrDisconnectIntegrationButton {...item} />}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)}></QueryCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IntegrationsPage() {
|
||||||
return (
|
return (
|
||||||
<Shell heading="Integrations" subtitle="Connect your favourite apps.">
|
<Shell heading="Integrations" subtitle="Connect your favourite apps.">
|
||||||
<QueryCell
|
<ClientSuspense fallback={<Loader />}>
|
||||||
query={query}
|
<IntegrationsContainer />
|
||||||
success={({ data }) => {
|
<CalendarListContainer />
|
||||||
return (
|
<WebhookListContainer />
|
||||||
<>
|
<IframeEmbedContainer />
|
||||||
<ShellSubHeading
|
</ClientSuspense>
|
||||||
title={
|
|
||||||
<SubHeadingTitleWithConnections
|
|
||||||
title="Conferencing"
|
|
||||||
numConnections={data.conferencing.numActive}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<List>
|
|
||||||
{data.conferencing.items.map((item) => (
|
|
||||||
<IntegrationListItem
|
|
||||||
key={item.title}
|
|
||||||
{...item}
|
|
||||||
actions={<ConnectOrDisconnectIntegrationButton {...item} />}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
|
|
||||||
<ShellSubHeading
|
|
||||||
className="mt-10"
|
|
||||||
title={
|
|
||||||
<SubHeadingTitleWithConnections title="Payment" numConnections={data.payment.numActive} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<List>
|
|
||||||
{data.payment.items.map((item) => (
|
|
||||||
<IntegrationListItem
|
|
||||||
key={item.title}
|
|
||||||
{...item}
|
|
||||||
actions={<ConnectOrDisconnectIntegrationButton {...item} />}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
|
|
||||||
<ShellSubHeading
|
|
||||||
className="mt-10"
|
|
||||||
title={
|
|
||||||
<SubHeadingTitleWithConnections
|
|
||||||
title="Calendars"
|
|
||||||
numConnections={data.calendar.numActive}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
subtitle={
|
|
||||||
<>
|
|
||||||
Configure how your links integrate with your calendars.
|
|
||||||
<br />
|
|
||||||
You can override these settings on a per event basis.
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{data.connectedCalendars.length > 0 && (
|
|
||||||
<>
|
|
||||||
<ConnectedCalendarsList
|
|
||||||
connectedCalendars={data.connectedCalendars}
|
|
||||||
onChanged={handleOpenChange}
|
|
||||||
/>
|
|
||||||
<ShellSubHeading
|
|
||||||
className="mt-6"
|
|
||||||
title={<SubHeadingTitleWithConnections title="Connect an additional calendar" />}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<CalendarsList calendars={data.calendar.items} onChanged={handleOpenChange} />
|
|
||||||
<WebhookEmbed webhooks={data.webhooks} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,6 +313,18 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
.query("connectedCalendars", {
|
||||||
|
async resolve({ ctx }) {
|
||||||
|
const { user } = ctx;
|
||||||
|
// get user's credentials + their connected integrations
|
||||||
|
const calendarCredentials = getCalendarCredentials(user.credentials, user.id);
|
||||||
|
|
||||||
|
// get all the connected integrations' calendars (from third party)
|
||||||
|
const connectedCalendars = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
|
||||||
|
|
||||||
|
return connectedCalendars;
|
||||||
|
},
|
||||||
|
})
|
||||||
.query("integrations", {
|
.query("integrations", {
|
||||||
async resolve({ ctx }) {
|
async resolve({ ctx }) {
|
||||||
const { user } = ctx;
|
const { user } = ctx;
|
||||||
|
@ -338,11 +350,6 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
// get all the connected integrations' calendars (from third party)
|
// get all the connected integrations' calendars (from third party)
|
||||||
const connectedCalendars = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
|
const connectedCalendars = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
|
||||||
|
|
||||||
const webhooks = await ctx.prisma.webhook.findMany({
|
|
||||||
where: {
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
conferencing: {
|
conferencing: {
|
||||||
items: conferencing,
|
items: conferencing,
|
||||||
|
@ -357,7 +364,6 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
numActive: countActive(payment),
|
numActive: countActive(payment),
|
||||||
},
|
},
|
||||||
connectedCalendars,
|
connectedCalendars,
|
||||||
webhooks,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -38,7 +38,8 @@
|
||||||
"jest-playwright-preset",
|
"jest-playwright-preset",
|
||||||
"expect-playwright"
|
"expect-playwright"
|
||||||
],
|
],
|
||||||
"allowJs": false
|
"allowJs": false,
|
||||||
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
|
47
yarn.lock
47
yarn.lock
|
@ -2611,11 +2611,6 @@ bowser@^2.8.1:
|
||||||
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
|
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
|
||||||
integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==
|
integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==
|
||||||
|
|
||||||
bowser@^2.8.1:
|
|
||||||
version "2.11.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
|
|
||||||
integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==
|
|
||||||
|
|
||||||
brace-expansion@^1.1.7:
|
brace-expansion@^1.1.7:
|
||||||
version "1.1.11"
|
version "1.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||||
|
@ -3818,11 +3813,6 @@ fast-equals@^1.6.3:
|
||||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-1.6.3.tgz#84839a1ce20627c463e1892f2ae316380c81b459"
|
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-1.6.3.tgz#84839a1ce20627c463e1892f2ae316380c81b459"
|
||||||
integrity sha512-4WKW0AL5+WEqO0zWavAfYGY1qwLsBgE//DN4TTcVEN2UlINgkv9b3vm2iHicoenWKSX9mKWmGOsU/iI5IST7pQ==
|
integrity sha512-4WKW0AL5+WEqO0zWavAfYGY1qwLsBgE//DN4TTcVEN2UlINgkv9b3vm2iHicoenWKSX9mKWmGOsU/iI5IST7pQ==
|
||||||
|
|
||||||
fast-equals@^1.6.3:
|
|
||||||
version "1.6.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-1.6.3.tgz#84839a1ce20627c463e1892f2ae316380c81b459"
|
|
||||||
integrity sha512-4WKW0AL5+WEqO0zWavAfYGY1qwLsBgE//DN4TTcVEN2UlINgkv9b3vm2iHicoenWKSX9mKWmGOsU/iI5IST7pQ==
|
|
||||||
|
|
||||||
fast-glob@^3.1.1, fast-glob@^3.2.7:
|
fast-glob@^3.1.1, fast-glob@^3.2.7:
|
||||||
version "3.2.7"
|
version "3.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
|
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
|
||||||
|
@ -4348,18 +4338,6 @@ history@^4.9.0:
|
||||||
tiny-warning "^1.0.0"
|
tiny-warning "^1.0.0"
|
||||||
value-equal "^1.0.1"
|
value-equal "^1.0.1"
|
||||||
|
|
||||||
history@^4.9.0:
|
|
||||||
version "4.10.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
|
||||||
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.1.2"
|
|
||||||
loose-envify "^1.2.0"
|
|
||||||
resolve-pathname "^3.0.0"
|
|
||||||
tiny-invariant "^1.0.2"
|
|
||||||
tiny-warning "^1.0.0"
|
|
||||||
value-equal "^1.0.1"
|
|
||||||
|
|
||||||
hmac-drbg@^1.0.1:
|
hmac-drbg@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||||
|
@ -6556,13 +6534,6 @@ path-to-regexp@^1.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
isarray "0.0.1"
|
isarray "0.0.1"
|
||||||
|
|
||||||
path-to-regexp@^1.7.0:
|
|
||||||
version "1.8.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
|
|
||||||
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
|
|
||||||
dependencies:
|
|
||||||
isarray "0.0.1"
|
|
||||||
|
|
||||||
path-type@^3.0.0:
|
path-type@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
|
resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
|
||||||
|
@ -6998,9 +6969,10 @@ react-date-picker@^8.3.3:
|
||||||
react-fit "^1.0.3"
|
react-fit "^1.0.3"
|
||||||
update-input-width "^1.2.2"
|
update-input-width "^1.2.2"
|
||||||
|
|
||||||
react-dom@17.0.2:
|
react-dom@^17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||||
|
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
|
@ -7197,9 +7169,10 @@ react-use-intercom@1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-use-intercom/-/react-use-intercom-1.4.0.tgz#796527728c131ebf132186385bf78f69dbcd84cc"
|
resolved "https://registry.yarnpkg.com/react-use-intercom/-/react-use-intercom-1.4.0.tgz#796527728c131ebf132186385bf78f69dbcd84cc"
|
||||||
|
|
||||||
react@17.0.2:
|
react@^17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||||
|
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
|
@ -7332,11 +7305,6 @@ resolve-pathname@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
|
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
|
||||||
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
|
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
|
||||||
|
|
||||||
resolve-pathname@^3.0.0:
|
|
||||||
version "3.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
|
|
||||||
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
|
|
||||||
|
|
||||||
resolve@^1.10.0, resolve@^1.20.0:
|
resolve@^1.10.0, resolve@^1.20.0:
|
||||||
version "1.20.0"
|
version "1.20.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
|
||||||
|
@ -7431,7 +7399,8 @@ saxes@^5.0.1:
|
||||||
|
|
||||||
scheduler@^0.20.2:
|
scheduler@^0.20.2:
|
||||||
version "0.20.2"
|
version "0.20.2"
|
||||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
|
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
|
||||||
|
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
|
|
Loading…
Reference in a new issue