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 commentpull/1078/merge
parent
78523f7a57
commit
1790aeb577
|
@ -5,5 +5,9 @@
|
|||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"eslint.run": "onSave"
|
||||
"eslint.run": "onSave",
|
||||
"workbench.colorCustomizations": {
|
||||
"titleBar.activeBackground": "#888888",
|
||||
"titleBar.inactiveBackground": "#292929"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}</>;
|
||||
};
|
|
@ -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: {
|
||||
type: string;
|
||||
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
||||
onOpenChange: (isOpen: boolean) => void | Promise<void>;
|
||||
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
|
||||
}) {
|
||||
const { type } = props;
|
||||
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 */
|
||||
id: number;
|
||||
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
||||
onOpenChange: (isOpen: boolean) => void | Promise<void>;
|
||||
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
|
||||
}) {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const mutation = useMutation(
|
||||
|
|
|
@ -13,36 +13,37 @@ import { Alert } from "@components/ui/Alert";
|
|||
type ErrorLike = {
|
||||
message: string;
|
||||
};
|
||||
type JSXElementOrNull = JSX.Element | null;
|
||||
|
||||
interface QueryCellOptionsBase<TData, TError extends ErrorLike> {
|
||||
query: UseQueryResult<TData, TError>;
|
||||
error?: (
|
||||
query: QueryObserverLoadingErrorResult<TData, TError> | QueryObserverRefetchErrorResult<TData, TError>
|
||||
) => JSX.Element;
|
||||
loading?: (query: QueryObserverLoadingResult<TData, TError>) => JSX.Element;
|
||||
idle?: (query: QueryObserverIdleResult<TData, TError>) => JSX.Element;
|
||||
) => JSXElementOrNull;
|
||||
loading?: (query: QueryObserverLoadingResult<TData, TError>) => JSXElementOrNull;
|
||||
idle?: (query: QueryObserverIdleResult<TData, TError>) => JSXElementOrNull;
|
||||
}
|
||||
|
||||
interface QueryCellOptionsNoEmpty<TData, TError extends ErrorLike>
|
||||
extends QueryCellOptionsBase<TData, TError> {
|
||||
success: (query: QueryObserverSuccessResult<TData, TError>) => JSX.Element;
|
||||
success: (query: QueryObserverSuccessResult<TData, TError>) => JSXElementOrNull;
|
||||
}
|
||||
|
||||
interface QueryCellOptionsWithEmpty<TData, TError extends ErrorLike>
|
||||
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
|
||||
*/
|
||||
empty: (query: QueryObserverSuccessResult<TData, TError>) => JSX.Element;
|
||||
empty: (query: QueryObserverSuccessResult<TData, TError>) => JSXElementOrNull;
|
||||
}
|
||||
|
||||
export function QueryCell<TData, TError extends ErrorLike>(
|
||||
opts: QueryCellOptionsWithEmpty<TData, TError>
|
||||
): JSX.Element;
|
||||
): JSXElementOrNull;
|
||||
export function QueryCell<TData, TError extends ErrorLike>(
|
||||
opts: QueryCellOptionsNoEmpty<TData, TError>
|
||||
): JSX.Element;
|
||||
): JSXElementOrNull;
|
||||
export function QueryCell<TData, TError extends ErrorLike>(
|
||||
opts: QueryCellOptionsNoEmpty<TData, TError> | QueryCellOptionsWithEmpty<TData, TError>
|
||||
) {
|
||||
|
|
|
@ -5,4 +5,4 @@ export const WEBHOOK_TRIGGER_EVENTS = [
|
|||
WebhookTriggerEvents.BOOKING_CANCELLED,
|
||||
WebhookTriggerEvents.BOOKING_CREATED,
|
||||
WebhookTriggerEvents.BOOKING_RESCHEDULED,
|
||||
] as const;
|
||||
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"];
|
||||
|
|
|
@ -36,8 +36,8 @@
|
|||
"@heroicons/react": "^1.0.4",
|
||||
"@hookform/resolvers": "^2.8.1",
|
||||
"@jitsu/sdk-js": "^2.2.4",
|
||||
"@prisma/client": "^2.30.2",
|
||||
"@next/bundle-analyzer": "11.1.2",
|
||||
"@prisma/client": "^2.30.2",
|
||||
"@radix-ui/react-avatar": "^0.1.0",
|
||||
"@radix-ui/react-collapsible": "^0.1.0",
|
||||
"@radix-ui/react-dialog": "^0.1.0",
|
||||
|
@ -75,8 +75,8 @@
|
|||
"nodemailer": "^6.6.3",
|
||||
"otplib": "^12.0.1",
|
||||
"qrcode": "^1.4.4",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-easy-crop": "^3.5.2",
|
||||
"react-hook-form": "^7.17.5",
|
||||
"react-hot-toast": "^2.1.0",
|
||||
|
|
|
@ -19,11 +19,9 @@ import getIntegrations from "@lib/integrations/getIntegrations";
|
|||
import prisma from "@lib/prisma";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import { ClientSuspense } from "@components/ClientSuspense";
|
||||
import Loader from "@components/Loader";
|
||||
import { ShellSubHeading } from "@components/Shell";
|
||||
import CalendarsList from "@components/integrations/CalendarsList";
|
||||
import ConnectedCalendarsList from "@components/integrations/ConnectedCalendarsList";
|
||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
||||
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
import Button from "@components/ui/Button";
|
||||
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 router = useRouter();
|
||||
|
||||
const refreshData = () => {
|
||||
router.replace(router.asPath);
|
||||
};
|
||||
|
||||
const DEFAULT_EVENT_TYPES = [
|
||||
{
|
||||
title: t("15min_meeting"),
|
||||
|
@ -123,12 +117,9 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
const bioRef = useRef<HTMLInputElement>(null);
|
||||
/** End Name */
|
||||
/** TimeZone */
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState({
|
||||
value: props.user.timeZone ?? dayjs.tz.guess(),
|
||||
label: null,
|
||||
});
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState(props.user.timeZone ?? dayjs.tz.guess());
|
||||
const currentTime = React.useMemo(() => {
|
||||
return dayjs().tz(selectedTimeZone.value).format("H:mm A");
|
||||
return dayjs().tz(selectedTimeZone).format("H:mm A");
|
||||
}, [selectedTimeZone]);
|
||||
/** End TimeZone */
|
||||
|
||||
|
@ -269,7 +260,9 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
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"
|
||||
/>
|
||||
</fieldset>
|
||||
|
@ -285,7 +278,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
setSubmitting(true);
|
||||
await updateUser({
|
||||
name: nameRef.current?.value,
|
||||
timeZone: selectedTimeZone.value,
|
||||
timeZone: selectedTimeZone,
|
||||
});
|
||||
setEnteredName(nameRef.current?.value || "");
|
||||
setSubmitting(true);
|
||||
|
@ -300,28 +293,9 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
title: t("connect_your_calendar"),
|
||||
description: t("connect_your_calendar_instructions"),
|
||||
Component: (
|
||||
<>
|
||||
{props.connectedCalendars.length > 0 && (
|
||||
<>
|
||||
<ConnectedCalendarsList
|
||||
connectedCalendars={props.connectedCalendars}
|
||||
onChanged={() => {
|
||||
refreshData();
|
||||
}}
|
||||
/>
|
||||
<ShellSubHeading
|
||||
className="mt-6"
|
||||
title={<SubHeadingTitleWithConnections title="Connect an additional calendar" />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<CalendarsList
|
||||
calendars={props.integrations}
|
||||
onChanged={() => {
|
||||
refreshData();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<ClientSuspense fallback={<Loader />}>
|
||||
<CalendarListContainer heading={false} />
|
||||
</ClientSuspense>
|
||||
),
|
||||
hideConfirm: true,
|
||||
confirmText: t("continue"),
|
||||
|
|
|
@ -19,15 +19,16 @@ import showToast from "@lib/notification";
|
|||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
import { WEBHOOK_TRIGGER_EVENTS } from "@lib/webhooks/constants";
|
||||
|
||||
import { ClientSuspense } from "@components/ClientSuspense";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogTrigger } from "@components/Dialog";
|
||||
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
|
||||
import Loader from "@components/Loader";
|
||||
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 CalendarsList from "@components/integrations/CalendarsList";
|
||||
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
||||
import ConnectIntegration from "@components/integrations/ConnectIntegrations";
|
||||
import ConnectedCalendarsList from "@components/integrations/ConnectedCalendarsList";
|
||||
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
||||
import IntegrationListItem from "@components/integrations/IntegrationListItem";
|
||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
||||
|
@ -35,15 +36,14 @@ import { Alert } from "@components/ui/Alert";
|
|||
import Button from "@components/ui/Button";
|
||||
import Switch from "@components/ui/Switch";
|
||||
|
||||
type TIntegrations = inferQueryOutput<"viewer.integrations">;
|
||||
type TWebhook = TIntegrations["webhooks"][number];
|
||||
type TWebhook = inferQueryOutput<"viewer.webhook.list">[number];
|
||||
|
||||
function WebhookListItem(props: { webhook: TWebhook; onEditWebhook: () => void }) {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const deleteWebhook = trpc.useMutation("viewer.webhook.delete", {
|
||||
async onSuccess() {
|
||||
await utils.invalidateQueries(["viewer.integrations"]);
|
||||
await utils.invalidateQueries(["viewer.webhhook.list"]);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -195,11 +195,11 @@ function WebhookDialogForm(props: {
|
|||
.handleSubmit(async (values) => {
|
||||
if (values.id) {
|
||||
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");
|
||||
} else {
|
||||
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");
|
||||
}
|
||||
|
||||
|
@ -269,19 +269,17 @@ function WebhookDialogForm(props: {
|
|||
);
|
||||
}
|
||||
|
||||
function WebhookEmbed(props: { webhooks: TWebhook[] }) {
|
||||
function WebhookListContainer() {
|
||||
const { t } = useLocale();
|
||||
const user = trpc.useQuery(["viewer.me"]).data;
|
||||
|
||||
const iframeTemplate = `<iframe src="${process.env.NEXT_PUBLIC_BASE_URL}/${user?.username}" frameborder="0" allowfullscreen></iframe>`;
|
||||
const htmlTemplate = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${t(
|
||||
"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>`;
|
||||
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>
|
||||
|
@ -293,7 +291,10 @@ function WebhookEmbed(props: { webhooks: TWebhook[] }) {
|
|||
<ListItemText component="p">Automation</ListItemText>
|
||||
</div>
|
||||
<div>
|
||||
<Button color="secondary" onClick={() => setNewWebhookModal(true)} data-testid="new_webhook">
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => setNewWebhookModal(true)}
|
||||
data-testid="new_webhook">
|
||||
{t("new_webhook")}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -301,9 +302,9 @@ function WebhookEmbed(props: { webhooks: TWebhook[] }) {
|
|||
</ListItem>
|
||||
</List>
|
||||
|
||||
{props.webhooks.length ? (
|
||||
{data.length ? (
|
||||
<List>
|
||||
{props.webhooks.map((item) => (
|
||||
{data.map((item) => (
|
||||
<WebhookListItem
|
||||
key={item.id}
|
||||
webhook={item}
|
||||
|
@ -315,20 +316,44 @@ function WebhookEmbed(props: { webhooks: TWebhook[] }) {
|
|||
))}
|
||||
</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")} />
|
||||
{/* 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 iframeTemplate = `<iframe src="${process.env.NEXT_PUBLIC_BASE_URL}/${user?.username}" frameborder="0" allowfullscreen></iframe>`;
|
||||
const htmlTemplate = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${t(
|
||||
"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>`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShellSubHeading title={t("iframe_embed")} subtitle={t("embed_calcom")} className="mt-10" />
|
||||
<div className="lg:pb-8 lg:col-span-9">
|
||||
<List>
|
||||
<ListItem className={classNames("flex-col")}>
|
||||
|
@ -398,25 +423,6 @@ function WebhookEmbed(props: { webhooks: TWebhook[] }) {
|
|||
{t("browse_api_documentation")}
|
||||
</a>
|
||||
</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,19 +480,13 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||
);
|
||||
}
|
||||
|
||||
export default function IntegrationsPage() {
|
||||
const query = trpc.useQuery(["viewer.integrations"]);
|
||||
const utils = trpc.useContext();
|
||||
const handleOpenChange = () => {
|
||||
utils.invalidateQueries(["viewer.integrations"]);
|
||||
};
|
||||
function IntegrationsContainer() {
|
||||
const query = trpc.useQuery(["viewer.integrations"], { suspense: true });
|
||||
|
||||
return (
|
||||
<Shell heading="Integrations" subtitle="Connect your favourite apps.">
|
||||
<QueryCell
|
||||
query={query}
|
||||
success={({ data }) => {
|
||||
return (
|
||||
success={({ data }) => (
|
||||
<>
|
||||
<ShellSubHeading
|
||||
title={
|
||||
|
@ -508,9 +508,7 @@ export default function IntegrationsPage() {
|
|||
|
||||
<ShellSubHeading
|
||||
className="mt-10"
|
||||
title={
|
||||
<SubHeadingTitleWithConnections title="Payment" numConnections={data.payment.numActive} />
|
||||
}
|
||||
title={<SubHeadingTitleWithConnections title="Payment" numConnections={data.payment.numActive} />}
|
||||
/>
|
||||
<List>
|
||||
{data.payment.items.map((item) => (
|
||||
|
@ -521,42 +519,20 @@ export default function IntegrationsPage() {
|
|||
/>
|
||||
))}
|
||||
</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} />
|
||||
</>
|
||||
)}></QueryCell>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
export default function IntegrationsPage() {
|
||||
return (
|
||||
<Shell heading="Integrations" subtitle="Connect your favourite apps.">
|
||||
<ClientSuspense fallback={<Loader />}>
|
||||
<IntegrationsContainer />
|
||||
<CalendarListContainer />
|
||||
<WebhookListContainer />
|
||||
<IframeEmbedContainer />
|
||||
</ClientSuspense>
|
||||
</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", {
|
||||
async resolve({ ctx }) {
|
||||
const { user } = ctx;
|
||||
|
@ -338,11 +350,6 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
// get all the connected integrations' calendars (from third party)
|
||||
const connectedCalendars = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
|
||||
|
||||
const webhooks = await ctx.prisma.webhook.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
return {
|
||||
conferencing: {
|
||||
items: conferencing,
|
||||
|
@ -357,7 +364,6 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
numActive: countActive(payment),
|
||||
},
|
||||
connectedCalendars,
|
||||
webhooks,
|
||||
};
|
||||
},
|
||||
})
|
||||
|
|
|
@ -38,7 +38,8 @@
|
|||
"jest-playwright-preset",
|
||||
"expect-playwright"
|
||||
],
|
||||
"allowJs": false
|
||||
"allowJs": false,
|
||||
"incremental": true
|
||||
},
|
||||
"include": [
|
||||
"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"
|
||||
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:
|
||||
version "1.1.11"
|
||||
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"
|
||||
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:
|
||||
version "3.2.7"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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:
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
update-input-width "^1.2.2"
|
||||
|
||||
react-dom@17.0.2:
|
||||
react-dom@^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:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
@ -7197,9 +7169,10 @@ react-use-intercom@1.4.0:
|
|||
version "1.4.0"
|
||||
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"
|
||||
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:
|
||||
loose-envify "^1.1.0"
|
||||
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"
|
||||
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:
|
||||
version "1.20.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
|
||||
|
@ -7431,7 +7399,8 @@ saxes@^5.0.1:
|
|||
|
||||
scheduler@^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:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
|
Loading…
Reference in New Issue