Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 92 additions & 53 deletions src/components/Base/Base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface BaseComponentInterface<
TResourceKey extends keyof Resources = keyof Resources,
> extends CommonComponentInterface<TResourceKey> {
FallbackComponent?: BaseBoundariesProps['FallbackComponent']
LoaderComponent?: BaseBoundariesProps['LoaderComponent']
LoaderComponent?: LoadingIndicatorContextProps['LoadingIndicator']
onEvent: OnEventType<EventType, unknown>
}

Expand Down Expand Up @@ -74,54 +74,111 @@ export const BaseComponent = <TResourceKey extends keyof Resources = keyof Resou
>
<BaseBoundaries
FallbackComponent={FallbackComponent}
LoaderComponent={LoaderComponent}
onErrorBoundaryError={onErrorBoundaryError}
componentName={componentName}
>
<BaseLayout error={error}>{children}</BaseLayout>
<Suspense
fallback={
<LoaderWithMetrics
LoaderComponent={LoaderComponent}
observability={observability}
componentName={componentName}
/>
}
>
<BaseLayout error={error}>{children}</BaseLayout>
</Suspense>
</BaseBoundaries>
</BaseContext.Provider>
)
}

interface BaseLayoutProps {
export interface BaseLayoutProps {
children?: ReactNode
error: SDKError | null
error?: SDKError | SDKError[] | null
isLoading?: boolean
}

export const BaseLayout = ({ children, error }: BaseLayoutProps) => {
function SingleErrorContent({ error }: { error: SDKError }) {
const Components = useComponentContext()
const { t } = useTranslation()
const hasFieldErrors = error !== null && error.fieldErrors.length > 0
const hasFieldErrors = error.fieldErrors.length > 0

return (
<FadeIn>
{error && (
<Components.Alert label={t('status.errorEncountered')} status="error">
{hasFieldErrors && (
<Components.UnorderedList
items={error.fieldErrors
.filter(fe => fe.message)
.map(fe => (
<span key={fe.field}>{fe.message}</span>
))}
/>
)}
{!hasFieldErrors && error.category === 'validation_error' && (
<Components.Text as="pre">
{error.raw &&
typeof error.raw === 'object' &&
'pretty' in error.raw &&
typeof (error.raw as { pretty: unknown }).pretty === 'function'
? (error.raw as { pretty: () => string }).pretty()
: error.message}
</Components.Text>
)}
{!hasFieldErrors && error.category !== 'validation_error' && (
<Components.Text>{error.message || t('errors.unknownError')}</Components.Text>
)}
</Components.Alert>
<Components.Alert label={t('status.errorEncountered')} status="error">
{hasFieldErrors && (
<Components.UnorderedList
items={error.fieldErrors
.filter(fieldError => fieldError.message)
.map(fieldError => (
<span key={fieldError.field}>{fieldError.message}</span>
))}
/>
)}
{!hasFieldErrors && error.category === 'validation_error' && (
<Components.Text as="pre">
{error.raw &&
typeof error.raw === 'object' &&
'pretty' in error.raw &&
typeof (error.raw as { pretty: unknown }).pretty === 'function'
? (error.raw as { pretty: () => string }).pretty()
: error.message}
</Components.Text>
)}
{!hasFieldErrors && error.category !== 'validation_error' && (
<Components.Text>{error.message || t('errors.unknownError')}</Components.Text>
)}
</Components.Alert>
)
}

function MultipleErrorsContent({ errors }: { errors: SDKError[] }) {
const Components = useComponentContext()
const { t } = useTranslation()

return (
<Components.Alert label={t('status.multipleErrorsEncountered')} status="error">
<Components.UnorderedList
items={errors
.filter(error => error.message || error.fieldErrors.length > 0)
.map((error, index) => {
const visibleFieldErrors = error.fieldErrors.filter(fieldError => fieldError.message)

if (visibleFieldErrors.length === 0) {
return <span key={index}>{error.message || t('errors.unknownError')}</span>
}

return (
<span key={index}>
{error.message || t('errors.unknownError')}
<Components.UnorderedList
items={visibleFieldErrors.map(fieldError => (
<span key={fieldError.field}>{fieldError.message}</span>
))}
/>
</span>
)
})}
/>
</Components.Alert>
)
}

export const BaseLayout = ({ children, error, isLoading }: BaseLayoutProps) => {
const { LoadingIndicator } = useLoadingIndicator()

const errors = Array.isArray(error) ? error : error ? [error] : []
const hasErrors = errors.length > 0

if (isLoading && !hasErrors) {
return <LoadingIndicator />
}

const [firstError] = errors

return (
<FadeIn>
{errors.length > 1 && <MultipleErrorsContent errors={errors} />}
{errors.length === 1 && firstError && <SingleErrorContent error={firstError} />}
{children}
</FadeIn>
)
Expand Down Expand Up @@ -166,22 +223,14 @@ const LoaderWithMetrics = ({
export interface BaseBoundariesProps {
children?: ReactNode
FallbackComponent?: (props: FallbackProps) => JSX.Element
LoaderComponent?: LoadingIndicatorContextProps['LoadingIndicator']
onErrorBoundaryError?: (error: unknown, info: ErrorInfo) => void
componentName?: string
}

export const BaseBoundaries = ({
children,
FallbackComponent = InternalError,
LoaderComponent: LoadingIndicatorFromProps,
onErrorBoundaryError,
componentName,
}: BaseBoundariesProps) => {
const { LoadingIndicator: LoadingIndicatorFromContext } = useLoadingIndicator()
const LoaderComponent = LoadingIndicatorFromProps ?? LoadingIndicatorFromContext
const { observability } = useObservability()

return (
<QueryErrorResetBoundary>
{({ reset: resetQueries }) => (
Expand All @@ -190,17 +239,7 @@ export const BaseBoundaries = ({
onReset={resetQueries}
onError={onErrorBoundaryError}
>
<Suspense
fallback={
<LoaderWithMetrics
LoaderComponent={LoaderComponent}
observability={observability}
componentName={componentName}
/>
}
>
{children}
</Suspense>
{children}
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
Expand Down
3 changes: 3 additions & 0 deletions src/components/Base/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export {
BaseComponent,
BaseBoundaries,
BaseLayout,
type BaseComponentInterface,
type BaseLayoutProps,
type BaseBoundariesProps,
type CommonComponentInterface,
} from './Base'
export { createCompoundContext } from './createCompoundContext'
Expand Down
6 changes: 4 additions & 2 deletions src/components/InformationRequests/InformationRequests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,10 @@ export function InformationRequestsFlow({
onClose={handleCloseModal}
footer={
Footer && (
<BaseBoundaries LoaderComponent={() => <LoadingSpinner size="sm" />}>
<Footer onEvent={handleEvent} />
<BaseBoundaries>
<Suspense fallback={<LoadingSpinner size="sm" />}>
<Footer onEvent={handleEvent} />
</Suspense>
</BaseBoundaries>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createMachine } from 'robot3'
import { useMachine } from 'react-robot'
import { useMemo, useRef, useState } from 'react'
import { Suspense, useMemo, useRef, useState } from 'react'
import { useWireInRequestsListSuspense } from '@gusto/embedded-api/react-query/wireInRequestsList'
import { ConfirmWireDetailsBanner } from './ConfirmWireDetailsBanner'
import { confirmWireDetailsMachine } from './confirmWireDetailsStateMachine'
Expand Down Expand Up @@ -115,14 +115,16 @@ function Root({ companyId, wireInId, onEvent = () => {} }: ConfirmWireDetailsInt
containerRef={modalContainerRef}
footer={
Footer && (
<BaseBoundaries
LoaderComponent={() => (
<div className={styles.footer}>
<LoadingSpinner size="sm" />
</div>
)}
>
<Footer onEvent={handleEvent} />
<BaseBoundaries>
<Suspense
fallback={
<div className={styles.footer}>
<LoadingSpinner size="sm" />
</div>
}
>
<Footer onEvent={handleEvent} />
</Suspense>
</BaseBoundaries>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import type { ReactNode } from 'react'
import { Suspense, type ReactNode } from 'react'
import { usePayrollsGetSuspense } from '@gusto/embedded-api/react-query/payrollsGet'
import {
PayrollConfigurationContext,
type PayrollConfigurationContextValue,
} from './PayrollConfigurationContext'
import { BaseBoundaries, type BaseBoundariesProps } from '@/components/Base/Base'
import { useLoadingIndicator } from '@/contexts/LoadingIndicatorProvider/useLoadingIndicator'
import type { LoadingIndicatorContextProps } from '@/contexts/LoadingIndicatorProvider/useLoadingIndicator'

export interface PayrollConfigurationProviderProps extends BaseBoundariesProps {
companyId: string
payrollId: string
children: ReactNode
LoaderComponent?: LoadingIndicatorContextProps['LoadingIndicator']
}

function PayrollConfigurationProvider({
Expand Down Expand Up @@ -39,18 +42,22 @@ function ComposedPayrollConfigurationProvider({
payrollId,
children,
FallbackComponent,
LoaderComponent,
LoaderComponent: LoaderComponentFromProps,
onErrorBoundaryError,
}: PayrollConfigurationProviderProps) {
const { LoadingIndicator: LoaderComponentFromContext } = useLoadingIndicator()
const LoaderComponent = LoaderComponentFromProps ?? LoaderComponentFromContext

return (
<BaseBoundaries
FallbackComponent={FallbackComponent}
LoaderComponent={LoaderComponent}
onErrorBoundaryError={onErrorBoundaryError}
>
<PayrollConfigurationProvider companyId={companyId} payrollId={payrollId}>
{children}
</PayrollConfigurationProvider>
<Suspense fallback={<LoaderComponent />}>
<PayrollConfigurationProvider companyId={companyId} payrollId={payrollId}>
{children}
</PayrollConfigurationProvider>
</Suspense>
</BaseBoundaries>
)
}
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"loading": "Loading component...",
"loadingOptions": "Loading options...",
"requiredField": "is a required field",
"errorEncountered": "There was a problem with your submission"
"errorEncountered": "There was a problem with your submission",
"multipleErrorsEncountered": "There were multiple problems with your submission"
},
"optionalLabel": "(optional)",
"progressBarLabel": "You are on step {{currentStep}} of {{totalSteps}}",
Expand Down
1 change: 1 addition & 0 deletions src/types/i18next.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2286,6 +2286,7 @@ export interface common{
"loadingOptions":string;
"requiredField":string;
"errorEncountered":string;
"multipleErrorsEncountered":string;
};
"optionalLabel":string;
"progressBarLabel":string;
Expand Down
Loading