Project: workspace-doctolib · Branch: HEAD · Mode: ` · **Model:**claude-haiku-4-5-20251001`
Stats: 1 user messages, 14 tool calls, tools used: Bash, Read.
Conversation
Turn 1 — User
Explore the error handling in the tpf-national-health-records-ui and tpf-national-health-records packages.
Key files to read fully: 1. /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentListModal/NhrDocumentListModal.tsx 2. /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/ (list all files) 3. /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src/ (list all files) 4. Any file in tpf-national-health-records-ui that deals with errors, toasts, or status handling
Also search for:
- How status field from search results is consumed
- Any toast or notification imports/usage
- The NhrEntryPoint component if it exists
- How searchDocuments callback is wired up in the modal
I need to understand WHY errors returned by searchDocuments (with status="error") are not triggering a toast. Report the full content of NhrDocumentListModal.tsx and any related error-handling components.
Turn 1 — Assistant
I'll explore the error handling in these packages systematically. Let me start by examining the directory structure and key files.
Tools used:
Bash:find /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src -type f -name "*.tsx" -o -name "*.ts" | head -30
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/i18n.ts /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/entry.ts /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/types.ts /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/index.ts /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/i18n.test.ts /U …(truncated, 3503 more chars)
Now let me look at the NhrDocumentListModal component and related files:
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentListModal/NhrDocumentListModal.tsx
Tool results:
→ result (ok): 1 import { Modal } from '@doctolib/design-system/Modal'
2 import { ModalBody } from '@doctolib/design-system/ModalBody'
3 import type { NhrDocument } from '@doctolib/tpf-national-health-records'
4 import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react'
5
6 import { addToast } from '../../business/toast'
7 import type { ActionConfig, ColumnConfig, FilterConfig, NhrPatientInfo, SearchDocumentsFn } from '../../types'
8 import { NhrDocumentList } from '../NhrDocumen
…(truncated, 3857 more chars)
Tools used:
Bash:find /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components -type f \( -name "*.tsx" -o -name "*.ts" \)
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDateRangeFilter/NhrDateRangeFilter.tsx /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDateRangeFilter/NhrDateRangeFilter.test.tsx /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDateRangeFilter/NhrDateRangeFilter.stories.tsx /Users/USER/workspace/doctolib/treatment-plan/packages/tpf …(truncated, 12748 more chars)
Tools used:
Bash:find /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src -type f \( -name "*.tsx" -o -name "*.ts" \)
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src/index.ts /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src/business/nhr-service-factory.ts /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src/business/types/document.ts /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src/business/types/participant.ts /Users/USER/workspace/doctolib/treatment-plan/packages …(truncated, 660 more chars)
Now let me examine the toast implementation and types:
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/business/toast.ts
Tool results:
→ result (ok): 1 import { GLOBAL_PUBSUB_REGISTRY } from '@doctolib/upfront-core'
2 import type { ReactNode } from 'react'
3
4 type ToastProps = {
5 message: ReactNode
6 title: string
7 uiStyle: 'info' | 'success' | 'error' | 'warning'
8 action?: { message: string; onClick: () => void }
9 }
10
11 export function addToast(props: ToastProps) {
12 GLOBAL_PUBSUB_REGISTRY.dispatch({ type: 'toasts:add', payload: props })
13 }
14
<system-reminder> Whenever you read a file, you should consider whether it …(truncated, 262 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/types.ts
Tool results:
→ result (ok): 1 import type { DesignSystemIconComponent } from '@doctolib/design-system-icons'
2 import type { NhrDocument, NhrResultStatus } from '@doctolib/tpf-national-health-records'
3 import type { ReactNode } from 'react'
4
5 export type NhrPatientInfo = {
6 fullName: string
7 gender: 'male' | 'female' | 'other'
8 birthDate: string
9 age: number
10 }
11
12 export type ColumnConfig = {
13 key: string
14 header: string
15 sortable: boolean
16 /** Only required when sortable: true. Return
…(truncated, 4043 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrEntryPoint/NhrEntryPoint.tsx
Tool results:
→ result (ok): 1 import { useState } from 'react'
2
3 import type { ActionConfig, ColumnConfig, FilterConfig, NhrPatientInfo, SearchDocumentsFn } from '../../types'
4 import { NhrDocumentListModal } from '../NhrDocumentListModal/NhrDocumentListModal'
5 import { NhrEntryPointButton } from '../NhrEntryPointButton/NhrEntryPointButton'
6
7 export type NhrEntryPointProps = {
8 patient: {
9 show_ins_features?: boolean
10 }
11 patientInfo: NhrPatientInfo
12 title: string
13 columns: ColumnConfig[]
14
…(truncated, 1502 more chars)
Now let me check the NhrResultStatus type definition and the search documents callback usage pattern:
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src/business/types/api.ts
Tool results:
→ result (ok): 1 import type { DateRange, NhrDocument, NhrDocumentFile } from '#business/types/document'
2 import type { NhrPatientInformation } from '#business/types/participant'
3
4 export type NhrResultStatus = 'success' | 'skipped' | 'error' | 'partial success'
5
6 export type NhrResult = {
7 status: NhrResultStatus
8 message?: string
9 code?: string
10 }
11
12 // Document Search
13 export type NhrDocumentSearchParams = {
14 formats?: string[]
15 categories?: string[]
16 status?: string
17
…(truncated, 1910 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src/index.ts
Tool results:
→ result (ok): 1 export { createNhrService } from '#business/nhr-service-factory'
2
3 export type {
4 // api
5 NhrResult,
6 NhrResultStatus,
7 NhrDocumentSearchParams,
8 NhrDocumentSearchProps,
9 NhrDocumentSearchResult,
10 NhrGetDocumentsContentParams,
11 NhrGetDocumentsContentProps,
12 NhrDocumentContentResult,
13 NhrDocumentMetadataUpdatePayload,
14 NhrDocumentMetadataUpdateParams,
15 NhrDocumentMetadataUpdateProps,
16 NhrDocumentMetadataUpdatePayloadResult,
17 NhrDocumentMetada
…(truncated, 937 more chars)
Tools used:
Bash:grep -r "NhrResultStatus\|SearchDocumentsFn" /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src --include="*.ts" | head -20
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src/index.ts: NhrResultStatus, /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src/business/types/api.ts:export type NhrResultStatus = 'success' | 'skipped' | 'error' | 'partial success' /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src/business/types/api.ts: status: NhrResultStatus /Users/USER/workspace/doctolib/treatment-plan/packages/tpf- …(truncated, 70 more chars)
Now let me look at the NhrDocumentList component and how it uses the modal:
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentList/NhrDocumentList.tsx
Tool results:
→ result (ok): 1 import { getPagedData } from '#utils/nhrPaginationHelpers'
2 import { SpinnerLoader } from '@doctolib/design-system/SpinnerLoader'
3 import type { NhrDocument } from '@doctolib/tpf-national-health-records'
4 import { useMemo, useState } from 'react'
5
6 import type { ActionConfig, ColumnConfig, FilterConfig, NhrPatientInfo } from '../../types'
7 import { NhrDocumentListFilterPanel } from '../NhrDocumentListFilterPanel/NhrDocumentListFilterPanel'
8 import { NhrDocumentListHeader } from '../Nhr
…(truncated, 3453 more chars)
Tools used:
Bash:grep -r "searchDocuments\|NhrDocumentListModal" /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src --include="*.tsx" --include="*.ts" -B 2 -A 2 | head -50
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentList/NhrDocumentList.stories.tsx- description: { /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentList/NhrDocumentList.stories.tsx- story: /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentList/NhrDocumentList.stories.tsx: 'Shows the full loa …(truncated, 7748 more chars)
Now let me check the test file for NhrDocumentListModal to see if there are tests for error handling:
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentListModal/NhrDocumentListModal.test.tsx
Tool results:
→ result (ok): 1 import type { DesignSystemIconComponent } from '@doctolib/design-system-icons'
2 import { setup } from '@doctolib/test-helpers'
3 import { screen, waitFor } from '@testing-library/react'
4 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
6 import { createMoreActionsAction } from '../../business/factories/actions/createMoreActionsAction'
7 import { mockColumns, mockPatientInfo } from '../../test-helpers'
8 import { nhrDocumentFixtureBuilder } from '../../test-helpers/
…(truncated, 14037 more chars)
Tools used:
Bash:find /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui -name "*.tsx" -o -name "*.ts" | xargs grep -l "status.*error\|toast\|error" | head -10
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/business/toast.ts /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentListModal/NhrDocumentListModal.test.tsx /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentListModal/NhrDocumentListModal.tsx /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/dist/bu …(truncated, 824 more chars)
Perfect! I now have all the information I need to provide a comprehensive analysis. Let me compile the findings.
Error Handling Analysis: tpf-national-health-records-ui & tpf-national-health-records
Summary
The error handling is correctly implemented in the NhrDocumentListModal component. When searchDocuments returns a result with status="error", a toast notification IS triggered. Here's the complete analysis:
File: NhrDocumentListModal.tsx (Complete Content)
Location: /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentListModal/NhrDocumentListModal.tsx
import { Modal } from '@doctolib/design-system/Modal'
import { ModalBody } from '@doctolib/design-system/ModalBody'
import type { NhrDocument } from '@doctolib/tpf-national-health-records'
import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react'
import { addToast } from '../../business/toast'
import type { ActionConfig, ColumnConfig, FilterConfig, NhrPatientInfo, SearchDocumentsFn } from '../../types'
import { NhrDocumentList } from '../NhrDocumentList/NhrDocumentList'
type DocumentsState = {
rows: NhrDocument[]
total: number | undefined
isLoading: boolean
}
async function fetchDocuments(
searchDocuments: SearchDocumentsFn,
appliedFilters: Record<string, unknown>,
setDocumentsState: Dispatch<SetStateAction<DocumentsState>>
): Promise<void> {
const result = await searchDocuments(appliedFilters)
if (result.status === 'error') {
setDocumentsState({ rows: [], total: undefined, isLoading: false })
const errorTitle = 'Unable to load DMP documents'
addToast({
title: errorTitle,
message: result.message ?? errorTitle,
uiStyle: 'error',
})
} else {
setDocumentsState({
rows: result.data,
total: result.total,
isLoading: false,
})
}
}
export type NhrDocumentListModalProps = {
isOpen: boolean
onClose: () => void
title: string
patient: NhrPatientInfo
columns: ColumnConfig[]
filters: FilterConfig[]
searchDocuments: SearchDocumentsFn
actions?: ActionConfig[]
}
export const NhrDocumentListModal = ({
isOpen,
onClose,
title,
patient,
columns,
filters,
searchDocuments,
actions,
}: NhrDocumentListModalProps) => {
const [{ rows, total, isLoading }, setDocumentsState] = useState<DocumentsState>({
rows: [],
total: undefined,
isLoading: true,
})
const [appliedFilters, setAppliedFilters] = useState<Record<string, unknown>>({})
const [isActionLoading, setIsActionLoading] = useState(false)
useEffect(() => {
setDocumentsState((prev) => ({ ...prev, isLoading: true }))
void fetchDocuments(searchDocuments, appliedFilters, setDocumentsState)
}, [searchDocuments, appliedFilters])
const wrappedActions = useMemo(
() =>
actions?.map((action) => {
if (action.kind === 'dropdown') {
return {
...action,
options: action.options.map((option) => ({
...option,
onClick: async (doc: NhrDocument) => {
setIsActionLoading(true)
try {
await option.onClick(doc)
} finally {
setIsActionLoading(false)
}
},
})),
}
}
return {
...action,
onClick: async (doc: NhrDocument) => {
setIsActionLoading(true)
try {
await action.onClick(doc)
} finally {
setIsActionLoading(false)
}
},
}
}),
[actions]
)
return (
<Modal show={isOpen} onHide={onClose} size='fullscreen' title={title} aria-label={title}>
<ModalBody className='flex flex-1 flex-col overflow-hidden'>
<NhrDocumentList
columns={columns}
filters={filters}
patient={patient}
data={rows}
isLoading={isLoading}
appliedFilters={appliedFilters}
onApplyFilters={setAppliedFilters}
actions={wrappedActions}
isActionLoading={isActionLoading}
resultCount={total}
/>
</ModalBody>
</Modal>
)
}
Error Handling Flow
1. Status Check (Lines 21-38 in fetchDocuments)
const result = await searchDocuments(appliedFilters)
if (result.status === 'error') {
// Error handling
setDocumentsState({ rows: [], total: undefined, isLoading: false })
const errorTitle = 'Unable to load DMP documents'
addToast({
title: errorTitle,
message: result.message ?? errorTitle,
uiStyle: 'error',
})
} else {
// Success handling
setDocumentsState({
rows: result.data,
total: result.total,
isLoading: false,
})
}
Key Points:
- Line 23: Checks if result.status === 'error' explicitly
- Line 28: Calls addToast() with error details
- Message fallback: Uses result.message ?? errorTitle to ensure a message is always displayed
- State cleanup: Sets rows: [] and isLoading: false on error
2. Toast Implementation
File: /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/business/toast.ts
import { GLOBAL_PUBSUB_REGISTRY } from '@doctolib/upfront-core'
import type { ReactNode } from 'react'
type ToastProps = {
message: ReactNode
title: string
uiStyle: 'info' | 'success' | 'error' | 'warning'
action?: { message: string; onClick: () => void }
}
export function addToast(props: ToastProps) {
GLOBAL_PUBSUB_REGISTRY.dispatch({ type: 'toasts:add', payload: props })
}
Implementation Details:
- Uses global pub/sub registry from @doctolib/upfront-core
- Dispatches a toasts:add event with the toast configuration
- Supports error, warning, info, and success styles
3. Result Status Type Definition
File: /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records/src/business/types/api.ts
export type NhrResultStatus = 'success' | 'skipped' | 'error' | 'partial success'
export type SearchDocumentsResult = {
data: NhrDocument[]
total?: number
status: NhrResultStatus
message?: string
code?: string
}
export type SearchDocumentsFn = (appliedFilters?: Record<string, unknown>) => Promise<SearchDocumentsResult>
Status Values:
- 'success' - Normal successful fetch
- 'error' - Errors in the fetch operation
- 'skipped' - Operation skipped (e.g., feature disabled)
- 'partial success' - Partial data retrieved
Test Coverage (NhrDocumentListModal.test.tsx)
File: /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-national-health-records-ui/src/components/NhrDocumentListModal/NhrDocumentListModal.test.tsx (Lines 250-354)
The test suite includes comprehensive error handling tests:
Test 1: Error Toast Dispatch (Lines 251-271)
it('should dispatch an error toast with the error message when searchDocuments returns an error status', async () => {
vi.useRealTimers()
const { addToast } = await import('../../business/toast')
searchDocuments = vi.fn().mockResolvedValue({
data: [],
status: 'error',
message: 'Technical software error: Operation timed out.',
})
setup(<NhrDocumentListModal {...defaultProps()} isOpen={true} onClose={vi.fn()} />)
await vi.waitFor(() => {
expect(addToast).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Unable to load DMP documents',
message: 'Technical software error: Operation timed out.',
uiStyle: 'error',
})
)
})
})
Test 2: Message Fallback (Lines 273-289)
it('should use the fixed error title as message fallback when no message is provided', async () => {
vi.useRealTimers()
const { addToast } = await import('../../business/toast')
searchDocuments = vi.fn().mockResolvedValue({
…(truncated, 4336 more chars)