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": { | ||||
|     "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: { | ||||
|   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,8 +269,81 @@ function WebhookDialogForm(props: { | |||
|   ); | ||||
| } | ||||
| 
 | ||||
| function WebhookEmbed(props: { webhooks: TWebhook[] }) { | ||||
| function WebhookListContainer() { | ||||
|   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 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" | ||||
|   )}</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 ( | ||||
|     <> | ||||
|       <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> | ||||
| 
 | ||||
|       {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")} /> | ||||
|       <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,89 +480,59 @@ 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 ( | ||||
|     <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 ( | ||||
|     <Shell heading="Integrations" subtitle="Connect your favourite apps."> | ||||
|       <QueryCell | ||||
|         query={query} | ||||
|         success={({ data }) => { | ||||
|           return ( | ||||
|             <> | ||||
|               <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> | ||||
| 
 | ||||
|               <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} /> | ||||
|             </> | ||||
|           ); | ||||
|         }} | ||||
|       /> | ||||
|       <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 a new issue
	
	 Alex Johansson
						Alex Johansson