import { useState, useEffect, createContext, Suspense } from 'react' import { ioSchema } from '@interval/sdk/dist/ioSchema' import InputSpreadsheet from './InputSpreadsheet/lazy' import { inferQueryOutput } from '~/utils/trpc' import useTransaction, { UI_STATE } from './useTransaction' import { RCTResponderProps } from '~/components/RenderIOCall' import { RenderIOCall } from '~/components/RenderIOCall' import { RenderContextProvider } from '~/components/RenderContext' import CompletionState from './_presentation/CompletionState' import { Indeterminate, Progress, InlineLoading, } from './_presentation/LoadingState' import ConfirmIdentity from './ConfirmIdentity' import { PendingConnectionIndicator } from './_presentation/TransactionLayout' import { useActionUrlBuilder } from '~/utils/useActionUrlBuilder' import { useUploadUrlFetcher } from '~/utils/uploads' import { ActionMode } from '~/utils/types' import useTransactionNavigationWarning from '~/utils/useTransactionNavigationWarning' export const ActionModeContext = createContext('live') export const UnimplementedComponents: { [Property in keyof typeof ioSchema]?: ( props: RCTResponderProps ) => React.ReactNode } = { // not implemented by the @ui package INPUT_SPREADSHEET: InputSpreadsheet, CONFIRM_IDENTITY: ConfirmIdentity, } export function LoadingState( state: TransactionUIProps['loadingState'] & { isInline?: boolean } ) { if (!state) return null // Inline loaders use a single component for progress + indeterminate if (state.isInline) { return } if (state.itemsInQueue !== undefined && state.itemsCompleted !== undefined) { return } return } export function StandaloneLoadingState( state: TransactionUIProps['loadingState'] & { isInline?: false } ) { return (
{state.itemsInQueue !== undefined && state.itemsCompleted !== undefined ? ( ) : ( )}
) } type TransactionUIProps = { transaction: NonNullable> mode: ActionMode onStateChange?: (state: UI_STATE) => void onValidate?: () => void } & Omit, 'uiRef'> export default function TransactionUI({ transaction, completedTransactionData, mode, state, ioCalls, currentIOCall, loadingState, onValidate, shouldUseAppendUi, didPreviousCallAcceptInput, shouldDisableTableTruncation, }: TransactionUIProps) { const [showPlaceholderIndicator, setShowPlaceholderIndicator] = useState(false) const [initialInputGroupKey, setInitialGroupKey] = useState< string | undefined >() // If no render or loading calls made by action after short period // show a default loading indicator instead of a white screen useEffect(() => { if (state === 'IN_PROGRESS') { const timeout = setTimeout(() => { setShowPlaceholderIndicator(true) }, 3_000) return () => { clearTimeout(timeout) } } else { setShowPlaceholderIndicator(false) } }, [state]) const inputGroupKey = currentIOCall?.inputGroupKey useEffect(() => { if (inputGroupKey) { setInitialGroupKey(key => key ?? inputGroupKey) } }, [inputGroupKey]) const getActionUrl = useActionUrlBuilder(mode) const getUploadUrls = useUploadUrlFetcher({ transactionId: transaction?.id, inputGroupKey: currentIOCall?.inputGroupKey, }) const { setNavigationWarning } = useTransactionNavigationWarning() const callsToRender = shouldUseAppendUi ? ioCalls : currentIOCall ? [currentIOCall] : [] const hasInteractiveElements = callsToRender.some(call => call.elements.some(element => element.isInteractive) ) if (state === 'CONNECTING') { return } if (state === 'REDIRECTING') { return } if (loadingState && !shouldUseAppendUi) { return } return (
}>
{callsToRender.map((call, idx) => ( // not having a container element here is important because we use // the :only selector in RenderIOCall to apply some styles in the append UI. ))}
{/* it's important that this appears outside of the above div so loading states don't affect the :only selector we use for previous IO call styling. */} {loadingState && shouldUseAppendUi && ( )}
{state === 'IN_PROGRESS' && !loadingState && !currentIOCall && showPlaceholderIndicator && ( )} {state === 'COMPLETED' && ( )}
) }