Skip to content

Commit

Permalink
Improve state handling (#95)
Browse files Browse the repository at this point in the history
# Improve state handling

## ♻️ Current situation & Problem
Errors, loading, empty should be surfaced.


### Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
arkadiuszbachorski authored Jan 14, 2025
1 parent fcff560 commit 5f72377
Show file tree
Hide file tree
Showing 26 changed files with 670 additions and 501 deletions.
2 changes: 2 additions & 0 deletions main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const router = createRouter({
entityName="page"
/>
),
defaultPendingMs: 300,
defaultPendingMinMs: 200,
})

declare module '@tanstack/react-router' {
Expand Down
27 changes: 14 additions & 13 deletions modules/notifications/NotificationsTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {
DataTable,
DataTableBasicView,
type DataTableProps,
} from '@stanfordspezi/spezi-web-design-system/components/DataTable'
import { parseNilLocalizedText } from '@/modules/firebase/localizedText'
import { type UserMessage } from '@/modules/firebase/models'
Expand Down Expand Up @@ -36,12 +36,14 @@ const columns = [
}),
]

interface NotificationsTableProps {
interface NotificationsTableProps
extends Omit<DataTableProps<UserMessage>, 'data' | 'columns'> {
notifications: UserMessage[]
}

export const NotificationsTable = ({
notifications,
...props
}: NotificationsTableProps) => (
<DataTable
columns={columns}
Expand All @@ -57,18 +59,17 @@ export const NotificationsTable = ({
initialState={{
columnFilters: [{ id: columnIds.isRead, value: false }],
}}
{...props}
>
{(props) => (
<DataTableBasicView {...props}>
{(rows) =>
rows.map((row) => {
const notification = row.original
return (
<Notification key={notification.id} notification={notification} />
)
})
}
</DataTableBasicView>
{({ rows }) => (
<div>
{rows.map((row) => {
const notification = row.original
return (
<Notification key={notification.id} notification={notification} />
)
})}
</div>
)}
</DataTable>
)
780 changes: 404 additions & 376 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@
"lint:ci": "eslint --output-file eslint_report.json --format json .",
"test": "vitest run --coverage",
"docs": "typedoc",
"docs:ci": "typedoc --out ./out/docs --githubPages true"
"docs:ci": "typedoc --out ./out/docs --githubPages true",
"prepush": "npm run lint:fix && tsc --noEmit"
},
"dependencies": {
"@stanfordbdhg/engagehf-models": "^0.4.0",
"@stanfordspezi/spezi-web-design-system": "^0.2.1",
"@stanfordspezi/spezi-web-design-system": "^0.3.0",
"@t3-oss/env-core": "^0.11.1",
"@tanstack/react-query": "^5.59.19",
"@tanstack/react-router": "^1.78.3",
"@tanstack/react-query": "^5.62.0",
"@tanstack/react-router": "^1.85.0",
"@tanstack/react-table": "^8.20.5",
"class-variance-authority": "^0.7.0",
"date-fns": "^3.6.0",
Expand All @@ -53,7 +54,7 @@
"@storybook/react": "^8.3.5",
"@storybook/react-vite": "^8.3.5",
"@storybook/test": "^8.3.5",
"@tanstack/router-plugin": "^1.78.3",
"@tanstack/router-plugin": "^1.84.4",
"@testing-library/jest-dom": "^6",
"@testing-library/react": "^16",
"@total-typescript/ts-reset": "^0.6.1",
Expand Down
9 changes: 7 additions & 2 deletions routes/~__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@

import { Toaster } from '@stanfordspezi/spezi-web-design-system/components/Toaster'
import { SpeziProvider } from '@stanfordspezi/spezi-web-design-system/SpeziProvider'
import { createRootRoute, Outlet, redirect } from '@tanstack/react-router'
import { createRootRoute, Link, Outlet, redirect } from '@tanstack/react-router'
import { type ComponentProps } from 'react'
import { Helmet } from 'react-helmet'
import { auth } from '@/modules/firebase/app'
import { AuthProvider } from '@/modules/firebase/AuthProvider'
import { ReactQueryClientProvider } from '@/modules/query/ReactQueryClientProvider'
import { routes } from '@/modules/routes'
import '../modules/globals.css'

const routerProps: ComponentProps<typeof SpeziProvider>['router'] = {
Link: ({ href, ...props }) => <Link to={href} {...props} />,
}

const Root = () => (
<AuthProvider>
<SpeziProvider>
<SpeziProvider router={routerProps}>
<ReactQueryClientProvider>
<Helmet defaultTitle="ENGAGE-HF" titleTemplate="%s - ENGAGE-HF" />
<Outlet />
Expand Down
24 changes: 24 additions & 0 deletions routes/~_dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,34 @@
// SPDX-License-Identifier: MIT
//

import { ErrorState } from '@stanfordspezi/spezi-web-design-system/components/ErrorState'
import { Spinner } from '@stanfordspezi/spezi-web-design-system/components/Spinner'
import { StateContainer } from '@stanfordspezi/spezi-web-design-system/components/StateContainer'
import { PageTitle } from '@stanfordspezi/spezi-web-design-system/molecules/DashboardLayout'
import { createFileRoute } from '@tanstack/react-router'
import { ShieldX } from 'lucide-react'
import { currentUserQueryOptions } from '@/modules/firebase/UserProvider'
import { queryClient } from '@/modules/query/queryClient'
import { DashboardLayout } from '@/routes/~_dashboard/DashboardLayout'

export const Route = createFileRoute('/_dashboard')({
loader: () => queryClient.ensureQueryData(currentUserQueryOptions()),
pendingComponent: () => (
<DashboardLayout>
<StateContainer grow className="min-h-screen">
<Spinner />
</StateContainer>
</DashboardLayout>
),
errorComponent: ({ error }) => (
<DashboardLayout title={<PageTitle title="Error" icon={<ShieldX />} />}>
<StateContainer grow>
<ErrorState>
Unhandled error happened. Please try again later.
<br />
Message: {error.message}
</ErrorState>
</StateContainer>
</DashboardLayout>
),
})
11 changes: 8 additions & 3 deletions routes/~_dashboard/MenuLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,21 @@ interface MenuLinksProps {
export const MenuLinks = ({ userType }: MenuLinksProps) => {
const location = useLocation()

const hrefProps = (href: string) => ({
const hrefProps = (href: string, exact = false) => ({
href,
isActive: location.pathname === href,
isActive:
exact ? location.pathname === href : location.pathname.startsWith(href),
})

const { hasUnreadNotification } = useHasUnreadNotification()

return (
<>
<MenuItem {...hrefProps('/')} label="Home" icon={<Home />} />
<MenuItem
{...hrefProps(routes.home, true)}
label="Home"
icon={<Home />}
/>
<MenuItem
{...hrefProps(routes.notifications)}
label="Notifications"
Expand Down
24 changes: 13 additions & 11 deletions routes/~_dashboard/NotificationsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
// SPDX-License-Identifier: MIT
//

import {
Async,
queriesToAsyncProps,
} from '@stanfordspezi/spezi-web-design-system/components/Async'
import { Button } from '@stanfordspezi/spezi-web-design-system/components/Button'
import {
Card,
CardHeader,
CardTitle,
} from '@stanfordspezi/spezi-web-design-system/components/Card'
import { EmptyState } from '@stanfordspezi/spezi-web-design-system/components/EmptyState'
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import { Loader2 } from 'lucide-react'
import { useUser } from '@/modules/firebase/UserProvider'
import { filterUnreadNotifications } from '@/modules/notifications/helpers'
import { Notification } from '@/modules/notifications/Notification'
Expand All @@ -25,29 +27,29 @@ import { routes } from '@/modules/routes'
export const NotificationsCard = () => {
const { auth } = useUser()

const { data: notifications = [], isLoading } = useQuery({
const notificationQuery = useQuery({
...notificationQueries.list({ userId: auth.uid }),
select: (notifications) =>
filterUnreadNotifications(notifications).slice(0, 3),
})
const notifications = notificationQuery.data ?? []

return (
<Card className="flex flex-col">
<CardHeader>
<CardTitle>Notifications</CardTitle>
</CardHeader>
{isLoading ?
<div className="flex-center py-8">
<Loader2 className="animate-spin text-muted-foreground" />
</div>
: notifications.length === 0 ?
<EmptyState entityName="unread notifications" className="py-8" />
: <div>
<Async
{...queriesToAsyncProps([notificationQuery])}
empty={notifications.length === 0}
entityName="unread notifications"
>
<div>
{notifications.map((notification) => (
<Notification key={notification.id} notification={notification} />
))}
</div>
}
</Async>
<Button
asChild
variant="ghostPrimary"
Expand Down
82 changes: 38 additions & 44 deletions routes/~_dashboard/UpcomingAppointmentsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// SPDX-License-Identifier: MIT
//

import { queriesToAsyncProps } from '@stanfordspezi/spezi-web-design-system/components/Async'
import {
Card,
CardHeader,
Expand All @@ -17,23 +18,24 @@ import {
} from '@stanfordspezi/spezi-web-design-system/components/DataTable'
import { Tooltip } from '@stanfordspezi/spezi-web-design-system/components/Tooltip'
import { getUserName } from '@stanfordspezi/spezi-web-design-system/modules/auth'
import { combineQueries } from '@stanfordspezi/spezi-web-design-system/utils/query'
import { useQueries, useQuery } from '@tanstack/react-query'
import { useNavigate } from '@tanstack/react-router'
import { createColumnHelper } from '@tanstack/table-core'
import { addWeeks, isBefore, isFuture } from 'date-fns'
import { Info, Loader2 } from 'lucide-react'
import { Info } from 'lucide-react'
import { useMemo } from 'react'
import { appointmentsQueries } from '@/modules/firebase/appointment'
import { routes } from '@/modules/routes'
import { patientsQueries } from '@/modules/user/patients'
import { PatientPageTab } from '@/routes/~_dashboard/~patients/~$id/~index'
import { useNavigateOrOpen } from '@/utils/useNavigateOrOpen'

export const UpcomingAppointmentsCard = () => {
const navigate = useNavigate()
const navigateOrOpen = useNavigateOrOpen()
const patientsQuery = useQuery(patientsQueries.listUserPatients())
const { data: patients } = patientsQuery

const results = useQueries({
const appointmentsQuery = useQueries({
queries:
patients?.map((patient) =>
appointmentsQueries.list({
Expand All @@ -42,19 +44,15 @@ export const UpcomingAppointmentsCard = () => {
}),
) ?? [],
combine: (results) => ({
isLoading: results.some((result) => result.isLoading),
isError: results.some((result) => result.isError),
...combineQueries(results),
data: results.map((result) => result.data),
isSuccess: results.every((result) => result.isSuccess),
}),
})

const isLoading = patientsQuery.isLoading || results.isLoading

const upcomingAppointments = useMemo(() => {
if (!results.isSuccess || !patients) return []
if (!appointmentsQuery.isSuccess || !patients) return []
const twoWeeksFromNow = addWeeks(new Date(), 2)
return results.data
return appointmentsQuery.data
.flatMap((appointments, index) => {
const patient = patients.at(index)
if (!patient || !appointments) return null
Expand All @@ -76,7 +74,7 @@ export const UpcomingAppointmentsCard = () => {
})
.filter(Boolean)
.sort((a, b) => a.date.getTime() - b.date.getTime())
}, [patients, results.data, results.isSuccess])
}, [patients, appointmentsQuery.data, appointmentsQuery.isSuccess])

const columnHelper =
createColumnHelper<(typeof upcomingAppointments)[number]>()
Expand All @@ -89,39 +87,35 @@ export const UpcomingAppointmentsCard = () => {
<Info className="size-5 text-muted-foreground" />
</Tooltip>
</CardHeader>
{isLoading ?
<div className="flex-center py-8">
<Loader2 className="animate-spin text-muted-foreground" />
</div>
: <DataTable
data={upcomingAppointments}
columns={[
columnHelper.accessor('patient.name', {
header: 'Patient',
}),
columnHelper.accessor('date', {
header: 'Start',
cell: dateTimeColumn,
<DataTable
data={upcomingAppointments}
columns={[
columnHelper.accessor('patient.name', {
header: 'Patient',
}),
columnHelper.accessor('date', {
header: 'Start',
cell: dateTimeColumn,
}),
]}
minimal
bordered={false}
pageSize={6}
entityName="upcoming appointments"
tableView={{
onRowClick: (appointment, event) =>
void navigateOrOpen(event, {
to: routes.patients.patient(
appointment.patient.id,
appointment.patient.resourceType,
{
tab: PatientPageTab.appointments,
},
),
}),
]}
minimal
bordered={false}
pageSize={6}
entityName="upcoming appointments"
tableView={{
onRowClick: (appointment) =>
void navigate({
to: routes.patients.patient(
appointment.patient.id,
appointment.patient.resourceType,
{
tab: PatientPageTab.appointments,
},
),
}),
}}
/>
}
}}
{...queriesToAsyncProps([patientsQuery, appointmentsQuery])}
/>
</Card>
)
}
Loading

0 comments on commit 5f72377

Please sign in to comment.