Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add loading and error components #542

Merged
merged 3 commits into from
Jul 30, 2023
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
40 changes: 40 additions & 0 deletions lib/components/ErrorComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { tw, tx } from '../twind'
import type { Languages } from '../hooks/useLanguage'
import type { PropsWithLanguage } from '../hooks/useTranslation'
import { useTranslation } from '../hooks/useTranslation'
import { AlertOctagon } from 'lucide-react'

type ErrorComponentTranslation = {
errorOccurred: string
}

const defaultErrorComponentTranslation: Record<Languages, ErrorComponentTranslation> = {
en: {
errorOccurred: 'An error occurred'
},
de: {
errorOccurred: 'Ein Fehler ist aufgetreten'
}
}

export type ErrorComponentProps = {
errorText?: string,
classname?: string
}

/**
* The Component to show when an error occurred
*/
export const ErrorComponent = ({
language,
errorText,
classname
}: PropsWithLanguage<ErrorComponentTranslation, ErrorComponentProps>) => {
const translation = useTranslation(language, defaultErrorComponentTranslation)
return (
<div className={tx('flex flex-col items-center justify-center gap-y-4 w-full h-24', classname)}>
<AlertOctagon size={64} className={tw('text-hw-warning-400')}/>
DasProffi marked this conversation as resolved.
Show resolved Hide resolved
{errorText ?? `${translation.errorOccurred} :(`}
</div>
)
}
47 changes: 47 additions & 0 deletions lib/components/LoadingAndErrorComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { PropsWithChildren } from 'react'
import type { LoadingAnimationProps } from './LoadingAnimation'
import type { ErrorComponentProps } from './ErrorComponent'
import { LoadingAnimation } from './LoadingAnimation'
import { ErrorComponent } from './ErrorComponent'
import { useState } from 'react'

export type LoadingAndErrorComponentProps = PropsWithChildren<{
isLoading?: boolean,
hasError?: boolean,
loadingProps?: LoadingAnimationProps,
errorProps?: ErrorComponentProps,
/**
* in milliseconds
*/
minimumLoadingDuration?: number
}>

/**
* A Component that shows the Error and Loading animation, when appropriate and the children otherwise
*/
export const LoadingAndErrorComponent = ({
children,
isLoading = false,
hasError = false,
errorProps,
loadingProps,
minimumLoadingDuration
}: LoadingAndErrorComponentProps) => {
const [isInMinimumLoading, setIsInMinimumLoading] = useState(false)
const [hasUsedMinimumLoading, setHasUsedMinimumLoading] = useState(false)
if (minimumLoadingDuration && !isInMinimumLoading && !hasUsedMinimumLoading) {
setIsInMinimumLoading(true)
setTimeout(() => {
setIsInMinimumLoading(false)
setHasUsedMinimumLoading(true)
}, minimumLoadingDuration)
}

if (isLoading || (minimumLoadingDuration && isInMinimumLoading)) {
return <LoadingAnimation {...loadingProps}/>
}
if (hasError) {
return <ErrorComponent {...errorProps}/>
}
return children
}
40 changes: 40 additions & 0 deletions lib/components/LoadingAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { tx } from '../twind'
import type { Languages } from '../hooks/useLanguage'
import type { PropsWithLanguage } from '../hooks/useTranslation'
import { useTranslation } from '../hooks/useTranslation'
import { HelpwaveSpinner } from '../icons/HelpwaveSpinner'

type LoadingAnimationTranslation = {
loading: string
}

const defaultLoadingAnimationTranslation: Record<Languages, LoadingAnimationTranslation> = {
en: {
loading: 'Loading data'
},
de: {
loading: 'Lade Daten'
}
}

export type LoadingAnimationProps = {
loadingText?: string,
classname?: string
}

/**
* A Component to show when loading data
*/
export const LoadingAnimation = ({
language,
loadingText,
classname
}: PropsWithLanguage<LoadingAnimationTranslation, LoadingAnimationProps>) => {
const translation = useTranslation(language, defaultLoadingAnimationTranslation)
return (
<div className={tx('flex flex-col items-center justify-center gap-y-2 w-full h-24', classname)}>
<HelpwaveSpinner />
{loadingText ?? `${translation.loading}...`}
</div>
)
}
38 changes: 27 additions & 11 deletions lib/icons/HelpwaveSpinner.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { SVGProps } from 'react'
import { tw, tx } from '../twind'
import type { SVGProps } from 'react'
import { tx } from '../twind'

export type HelpwaveSpinnerProps = SVGProps<SVGSVGElement> & { color?: string,
export type HelpwaveSpinnerProps = SVGProps<SVGSVGElement> & {
color?: string,
animate?: 'none' | 'loading' | 'pulse' | 'bounce',
size?: number
}

/**
Expand All @@ -11,24 +13,38 @@ export type HelpwaveSpinnerProps = SVGProps<SVGSVGElement> & { color?: string,
export const HelpwaveSpinner = ({
color = 'currentColor',
animate = 'loading',
size = 64,
...props
}: HelpwaveSpinnerProps) => {
const isLoadingAnimation = animate === 'loading'
let svgAnimationKey = ''

if(animate === 'pulse') {
if (animate === 'pulse') {
svgAnimationKey = 'animate-pulse'
} else if(animate === 'bounce') {
} else if (animate === 'bounce') {
svgAnimationKey = 'animate-bounce'
}

if (size < 0) {
console.error('size cannot be less than 0')
size = 64
}

return (
<svg width='888' height='888' viewBox='0 0 888 888' fill='none' strokeLinecap='round' strokeWidth='48' {...props}>
<g className={tw(svgAnimationKey)}>
<path className={tx(isLoadingAnimation && 'animate-wave-big-left-up')} d='M144 543.235C144 423.259 232.164 326 340.92 326' stroke={color} strokeDasharray='1000' />
<path className={tx(isLoadingAnimation && 'animate-wave-big-right-down')} d='M537.84 544.104C429.084 544.104 340.92 446.844 340.92 326.869' stroke={color} strokeDasharray='1000' />
<path className={tx(isLoadingAnimation && 'animate-wave-small-left-up')} d='M462.223 518.035C462.223 432.133 525.348 362.495 603.217 362.495' stroke={color} strokeDasharray='1000' />
<path className={tx(isLoadingAnimation && 'animate-wave-small-right-down')} d='M745.001 519.773C666.696 519.773 603.218 450.136 603.218 364.233' stroke={color} strokeDasharray='1000' />
<svg
width={size}
height={size}
viewBox="0 0 888 888"
fill="none"
strokeLinecap="round"
strokeWidth={48}
{...props}
>
<g className={tx(svgAnimationKey)}>
<path className={tx({ 'animate-wave-big-left-up': isLoadingAnimation })} d="M144 543.235C144 423.259 232.164 326 340.92 326" stroke={color} strokeDasharray="1000" />
<path className={tx({ 'animate-wave-big-right-down': isLoadingAnimation })} d="M537.84 544.104C429.084 544.104 340.92 446.844 340.92 326.869" stroke={color} strokeDasharray="1000" />
<path className={tx({ 'animate-wave-small-left-up': isLoadingAnimation })} d="M462.223 518.035C462.223 432.133 525.348 362.495 603.217 362.495" stroke={color} strokeDasharray="1000" />
<path className={tx({ 'animate-wave-small-right-down': isLoadingAnimation })} d="M745.001 519.773C666.696 519.773 603.218 450.136 603.218 364.233" stroke={color} strokeDasharray="1000" />
</g>
</svg>
)
Expand Down
107 changes: 55 additions & 52 deletions tasks/components/MangeBedsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { BedWithPatientWithTasksNumberDTO } from '../mutations/bed_mutation
import { useBedCreateMutation, useBedDeleteMutation } from '../mutations/bed_mutations'
import { noop } from '@helpwave/common/util/noop'
import { X } from 'lucide-react'
import { LoadingAndErrorComponent } from '@helpwave/common/components/LoadingAndErrorComponent'

type ManageBedsModalTranslation = {
manageBedsIn: string,
Expand Down Expand Up @@ -79,63 +80,65 @@ export const ManageBedsModal = ({
const addBedMutation = useBedCreateMutation()
const deleteBedMutation = useBedDeleteMutation()

// TODO add view for loading
if (isLoading || !data) {
return <div>Loading ManageBedsModal</div>
}

const room = data.find(value => value.id === roomID)
const room = data?.find(value => value.id === roomID)
const beds = room?.beds
// TODO add view for error or error handling
if (isError || !beds || !room) {
return <div>Error ManageBedsModal</div>
}

const identifierMapping = (bed: BedWithPatientWithTasksNumberDTO) => bed.id
return (
<Modal modalClassName={tx('min-w-[600px]', modalClassName)} {...ModalProps}>
<div className={tw('flex flex-row justify-between items-center mb-4')}>
<Span className={tw('text-lg font-semibold')}>{`${translation.manageBedsIn} ${room.name}`}</Span>
<div className={tw('flex flex-row gap-x-4 items-end')} onClick={() => onClose()}>
<Span>{translation.close}</Span>
<X/>
</div>
</div>
<div className={tw('flex flex-row justify-between items-end mb-2')}>
<Span type="tableName">{`${translation.beds} (${beds.length})`}</Span>
<Button color="positive" onClick={() => addBedMutation.mutate(roomID)}>{translation.addBed}</Button>
</div>
<Table
data={beds}
stateManagement={[tableState, setTableState]}
identifierMapping={identifierMapping}
header={[
<Span key="name" type="tableHeader">{translation.name}</Span>,
<Span key="patient" type="tableHeader">{translation.patient}</Span>,
<></>
]}
rowMappingToCells={bed => [
<div key="name" className={tw('flex flex-row items-center w-10/12 min-w-[50px]')}>
<Span>{`${translation.bed} ${bed.index}`}</Span>
</div>,
<div key="patient" className={tw('w-20')}>
<Span>{bed.patient ? bed.patient.name : '-'}</Span>
</div>,
<div key="remove" className={tw('flex flex-row justify-end')}>
<Button
disabled={!!bed.patient}
onClick={() => {
deleteBedMutation.mutate(bed.id)
setTableState({ pagination: tableState.pagination ? updatePagination(tableState.pagination, beds.length - 1) : undefined })
}}
color="negative"
variant="textButton"
>
{translation.remove}
</Button>
</div>
]}
/>
<LoadingAndErrorComponent
isLoading={isLoading || !data}
hasError={isError || !beds || !room}
loadingProps={{ classname: tw('!h-full min-h-[400px]') }}
errorProps={{ classname: tw('!h-full min-h-[400px]') }}
>
{room && beds && (
<>
<div className={tw('flex flex-row justify-between items-center mb-4')}>
<Span className={tw('text-lg font-semibold')}>{`${translation.manageBedsIn} ${room.name}`}</Span>
<div className={tw('flex flex-row gap-x-4 items-end')} onClick={() => onClose()}>
<Span>{translation.close}</Span>
<X/>
</div>
</div>
<div className={tw('flex flex-row justify-between items-end mb-2')}>
<Span type="tableName">{`${translation.beds} (${beds.length})`}</Span>
<Button color="positive" onClick={() => addBedMutation.mutate(roomID)}>{translation.addBed}</Button>
</div>
<Table
data={beds}
stateManagement={[tableState, setTableState]}
identifierMapping={identifierMapping}
header={[
<Span key="name" type="tableHeader">{translation.name}</Span>,
<Span key="patient" type="tableHeader">{translation.patient}</Span>,
<></>
]}
rowMappingToCells={bed => [
<div key="name" className={tw('flex flex-row items-center w-10/12 min-w-[50px]')}>
<Span>{`${translation.bed} ${bed.index}`}</Span>
</div>,
<div key="patient" className={tw('w-20')}>
<Span>{bed.patient ? bed.patient.name : '-'}</Span>
</div>,
<div key="remove" className={tw('flex flex-row justify-end')}>
<Button
disabled={!!bed.patient}
onClick={() => {
deleteBedMutation.mutate(bed.id)
setTableState({ pagination: tableState.pagination ? updatePagination(tableState.pagination, beds.length - 1) : undefined })
}}
color="negative"
variant="textButton"
>
{translation.remove}
</Button>
</div>
]}
/>
</>
)}
</LoadingAndErrorComponent>
</Modal>
)
}
20 changes: 8 additions & 12 deletions tasks/components/OrganisationInvitationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '../mutations/organization_mutations'
import { validateEmail } from '@helpwave/common/util/emailValidation'
import { InvitationState } from '@helpwave/proto-ts/proto/services/user_svc/v1/organization_svc_pb'
import { LoadingAndErrorComponent } from '@helpwave/common/components/LoadingAndErrorComponent'

type OrganisationInvitationListTranslation = {
remove: string,
Expand Down Expand Up @@ -86,18 +87,13 @@ export const OrganisationInvitationList = ({
const isValidEmail = !!inviteMemberModalEmail && validateEmail(inviteMemberModalEmail)
const isShowingInviteMemberModal = inviteMemberModalEmail !== undefined

// TODO add view for loading
if (isLoading && context.state.organizationID) {
return <div>Loading Widget</div>
}

// TODO add view for error or error handling
if (isError && context.state.organizationID) {
return <div>Error Message</div>
}

return (
<div>
<LoadingAndErrorComponent
isLoading={isLoading && context.state.organizationID !== undefined}
hasError={isError && context.state.organizationID !== undefined}
errorProps={{ classname: tw('border-b-2 border-gray-500 rounded-xl') }}
loadingProps={{ classname: tw('border-2 border-gray-500 rounded-xl') }}
>
{isShowingInviteMemberModal && (
<InputModal
modalClassName={tw('min-w-[400px]')}
Expand Down Expand Up @@ -172,6 +168,6 @@ export const OrganisationInvitationList = ({
</div>
]}
/>
</div>
</LoadingAndErrorComponent>
)
}
14 changes: 7 additions & 7 deletions tasks/components/OrganizationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Span } from '@helpwave/common/components/Span'
import type { OrganizationMinimalDTO } from '../mutations/organization_mutations'
import { emptyOrganization } from '../mutations/organization_mutations'
import { validateEmail } from '@helpwave/common/util/emailValidation'
import { LoadingAndErrorComponent } from '@helpwave/common/components/LoadingAndErrorComponent'

type OrganizationFormTranslation = {
general: string,
Expand Down Expand Up @@ -140,11 +141,6 @@ export const OrganizationForm = ({
onChange({ hasChanges: true, isValid, organization: newOrganization }, shouldUpdate && isValid) // this might lead to confusing behaviour where changes aren't saved on invalid input
}

if (!organizationForm) {
// TODO replace with loading animation
return <div>Loading OrganizationForm</div>
}

const shortNameErrorMessage: string | undefined = validateShortName(organizationForm.organization)
const longNameErrorMessage: string | undefined = validateLongName(organizationForm.organization)
const emailErrorMessage: string | undefined = validateEmailWithOrganization(organizationForm.organization)
Expand All @@ -154,7 +150,11 @@ export const OrganizationForm = ({
const isDisplayingEmailNameError = emailErrorMessage && touched.email

return (
<form>
<LoadingAndErrorComponent
isLoading={!organizationForm}
loadingProps={{ classname: tw('border-2 border-gray-500 rounded-xl min-h-[350px]') }}
minimumLoadingDuration={200} // prevents errors flickering TODO investigate this
>
<Span type="subsectionTitle">{translation.general}</Span>
<div className={tw('mt-2 mb-1')}>
<Input
Expand Down Expand Up @@ -206,6 +206,6 @@ export const OrganizationForm = ({
{isDisplayingEmailNameError && <Span type="formError">{emailErrorMessage}</Span>}
</div>
<Span type="formDescription">{translation.contactEmailDescription}</Span>
</form>
</LoadingAndErrorComponent>
)
}
Loading