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
	
	 Alex Johansson
						Alex Johansson