From 55096ff07007f4e86294cd4fb4d98ed9e784c14e Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Thu, 5 Feb 2026 12:27:11 +0100 Subject: [PATCH 1/4] fix login stream --- web/api/auth/authService.ts | 5 +++ web/components/pages/login.tsx | 20 --------- web/hooks/useAuth.tsx | 46 ++++++++++++------- web/next-env.d.ts | 2 +- web/pages/auth/callback.tsx | 82 +++++++++++++++++++--------------- 5 files changed, 80 insertions(+), 75 deletions(-) delete mode 100644 web/components/pages/login.tsx diff --git a/web/api/auth/authService.ts b/web/api/auth/authService.ts index e6622a46..e83c649e 100644 --- a/web/api/auth/authService.ts +++ b/web/api/auth/authService.ts @@ -129,6 +129,11 @@ export const removeUser = async () => { return await userManager.removeUser() } +export const invalidateRestoreSessionCache = () => { + lastRestoreSessionResult = null + lastRestoreSessionTime = 0 +} + export const restoreSession = async (): Promise => { if (typeof window === 'undefined') return diff --git a/web/components/pages/login.tsx b/web/components/pages/login.tsx deleted file mode 100644 index 521928a0..00000000 --- a/web/components/pages/login.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Button } from '@helpwave/hightide' -import { useTasksTranslation } from '@/i18n/useTasksTranslation' - -type LoginPageProps = { - login: () => Promise, -} - -export const LoginPage = ({ login }: LoginPageProps) => { - const translation = useTasksTranslation() - - return ( -
-
-

{translation('loginRequired')}

- {translation('loginRequiredDescription')} - -
-
- ) -} diff --git a/web/hooks/useAuth.tsx b/web/hooks/useAuth.tsx index 5ce4e96e..196258fb 100644 --- a/web/hooks/useAuth.tsx +++ b/web/hooks/useAuth.tsx @@ -1,7 +1,7 @@ 'use client' import type { ComponentType, PropsWithChildren, ReactNode } from 'react' -import { createContext, useCallback, useContext, useEffect, useState } from 'react' +import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react' import { HelpwaveLogo } from '@helpwave/hightide' import { login, logout, onTokenExpiringCallback, removeUser, renewToken, restoreSession } from '@/api/auth/authService' import type { User } from 'oidc-client-ts' @@ -9,6 +9,7 @@ import { getConfig } from '@/utils/config' import { usePathname } from 'next/navigation' const config = getConfig() +const LOGIN_REDIRECT_COOLDOWN_MS = 5000 type AuthState = { identity?: User, @@ -42,6 +43,18 @@ export const AuthProvider = ({ pathname.startsWith(pattern)) const isIgnored = !!pathname && ignoredURLs.some(pattern => pathname.startsWith(pattern)) + const loginTimeoutRef = useRef | null>(null) + + const scheduleLoginRedirect = useCallback(() => { + if (loginTimeoutRef.current) return + loginTimeoutRef.current = setTimeout(() => { + loginTimeoutRef.current = null + login( + config.auth.redirect_uri + + `?redirect_uri=${encodeURIComponent(window.location.href)}` + ).catch(() => {}) + }, LOGIN_REDIRECT_COOLDOWN_MS) + }, []) useEffect(() => { if(isIgnored) { @@ -68,11 +81,7 @@ export const AuthProvider = ({ }) } else { if (!isUnprotected) { - login( - config.auth.redirect_uri + - `?redirect_uri=${encodeURIComponent(window.location.href)}` - ).catch(() => { - }) + scheduleLoginRedirect() } else { removeUser() .then(() => { @@ -95,11 +104,7 @@ export const AuthProvider = ({ if (isAuthenticationServerUnavailable) { setAuthState({ isLoading: false }) } else { - login( - config.auth.redirect_uri + - `?redirect_uri=${encodeURIComponent(window.location.href)}` - ).catch(() => { - }) + scheduleLoginRedirect() } } else { removeUser() @@ -114,8 +119,12 @@ export const AuthProvider = ({ return () => { isMounted = false + if (loginTimeoutRef.current) { + clearTimeout(loginTimeoutRef.current) + loginTimeoutRef.current = null + } } - }, [isIgnored, isUnprotected]) + }, [isIgnored, isUnprotected, scheduleLoginRedirect]) const logoutAndReset = useCallback(() => { logout() @@ -125,11 +134,14 @@ export const AuthProvider = ({ useEffect(() => { if (isIgnored || isUnprotected || identity || isLoading) return - login( - config.auth.redirect_uri + - `?redirect_uri=${encodeURIComponent(window.location.href)}` - ).catch(() => {}) - }, [identity, isLoading, isIgnored, isUnprotected]) + scheduleLoginRedirect() + return () => { + if (loginTimeoutRef.current) { + clearTimeout(loginTimeoutRef.current) + loginTimeoutRef.current = null + } + } + }, [identity, isLoading, isIgnored, isUnprotected, scheduleLoginRedirect]) const authLoadingContent = (
diff --git a/web/next-env.d.ts b/web/next-env.d.ts index 4503dcfd..4bc78492 100644 --- a/web/next-env.d.ts +++ b/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./build/types/routes.d.ts"; +import "./build/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/web/pages/auth/callback.tsx b/web/pages/auth/callback.tsx index 748a8331..89b7faab 100644 --- a/web/pages/auth/callback.tsx +++ b/web/pages/auth/callback.tsx @@ -1,57 +1,65 @@ 'use client' -import { useEffect, useState } from 'react' -import { handleCallback } from '@/api/auth/authService' +import { useEffect, useRef, useState } from 'react' +import { handleCallback, invalidateRestoreSessionCache } from '@/api/auth/authService' import { useRouter, useSearchParams } from 'next/navigation' import { useTasksTranslation } from '@/i18n/useTasksTranslation' -import { Button } from '@helpwave/hightide' +import { HelpwaveLogo } from '@helpwave/hightide' + +const REDIRECT_COOLDOWN_MS = 5000 export default function AuthCallback() { const translation = useTasksTranslation() - const router = useRouter() const searchParams = useSearchParams() const [hasError, setHasError] = useState(false) const [hasProcessed, setHasProcessed] = useState(false) + const redirectTarget = useRef(null) useEffect(() => { - if (hasProcessed) { - return - } - const checkAuthCallback = async () => { - if (searchParams.get('code') && searchParams.get('state')) { - setHasProcessed(true) - - try { - await handleCallback() - const redirect = searchParams.get('redirect_uri') - const isValidRedirect = redirect && new URL(redirect).host === window.location.host - const defaultRedirect = '/' - if (!isValidRedirect) { - - await router.push(defaultRedirect) - } else { - - await router.push(redirect ?? defaultRedirect) - } - } catch { - setHasError(true) - } + if (hasProcessed) return + const code = searchParams.get('code') + const state = searchParams.get('state') + if (!code || !state) return + + setHasProcessed(true) + const redirect = searchParams.get('redirect_uri') + const isValidRedirect = redirect && new URL(redirect).host === window.location.host + redirectTarget.current = isValidRedirect ? redirect : '/' + + const run = async () => { + try { + await handleCallback() + invalidateRestoreSessionCache() + } catch { + setHasError(true) + redirectTarget.current = '/' } } - checkAuthCallback().catch(() => {}) - }, [searchParams]) // eslint-disable-line react-hooks/exhaustive-deps + run().catch(() => {}) + }, [searchParams, hasProcessed]) + + useEffect(() => { + if (!hasProcessed || redirectTarget.current === null) return + const id = setTimeout(() => { + router.push(redirectTarget.current ?? '/') + }, REDIRECT_COOLDOWN_MS) + return () => clearTimeout(id) + }, [hasProcessed, router]) return ( -
-
- {hasError && ( - {translation('authenticationFailed')} - )} - -
+
+ + {hasError && ( + + {translation('authenticationFailed')} + + )}
) } From cb766e4b5d333cf378711a2a85d696480159ed03 Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Thu, 5 Feb 2026 12:36:29 +0100 Subject: [PATCH 2/4] remove unused pages --- web/components/layout/Page.tsx | 12 +- web/hooks/useTasksContext.tsx | 8 +- web/pages/teams/[id].tsx | 122 -------------- web/pages/teams/index.tsx | 20 --- web/pages/wards/[id].tsx | 94 ----------- web/pages/wards/index.tsx | 281 --------------------------------- 6 files changed, 8 insertions(+), 529 deletions(-) delete mode 100644 web/pages/teams/[id].tsx delete mode 100644 web/pages/teams/index.tsx delete mode 100644 web/pages/wards/[id].tsx delete mode 100644 web/pages/wards/index.tsx diff --git a/web/components/layout/Page.tsx b/web/components/layout/Page.tsx index e83d7d91..48e69be8 100644 --- a/web/components/layout/Page.tsx +++ b/web/components/layout/Page.tsx @@ -468,8 +468,7 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => { {translation('patients')} {context?.totalPatientsCount !== undefined && ({context.totalPatientsCount})} - {(context?.teams ?? []).length > 0 && ( - context?.update(prevState => ({ @@ -494,10 +493,8 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => { ))} - )} - {(context?.wards ?? []).length > 0 && ( - context?.update(prevState => ({ @@ -522,10 +519,8 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => { ))} - )} - {(context?.clinics ?? []).length > 0 && ( - context?.update(prevState => ({ @@ -550,7 +545,6 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => { ))} - )}
diff --git a/web/hooks/useTasksContext.tsx b/web/hooks/useTasksContext.tsx index f06a9597..9193fe53 100644 --- a/web/hooks/useTasksContext.tsx +++ b/web/hooks/useTasksContext.tsx @@ -17,12 +17,14 @@ function filterLocationsByRootSubtree( return locations.map(loc => ({ id: loc.id, title: loc.title })) } + if (!allLocations || allLocations.length === 0) { + return locations.map(loc => ({ id: loc.id, title: loc.title })) + } + const rootLocationSet = new Set(selectedRootLocationIds) const allLocationsMap = new Map() - if (allLocations) { - allLocations.forEach(loc => allLocationsMap.set(loc.id, loc)) - } + allLocations.forEach(loc => allLocationsMap.set(loc.id, loc)) locations.forEach(loc => allLocationsMap.set(loc.id, loc)) rootLocations.forEach(loc => allLocationsMap.set(loc.id, { id: loc.id, title: loc.title, parentId: null })) diff --git a/web/pages/teams/[id].tsx b/web/pages/teams/[id].tsx deleted file mode 100644 index af12a6b1..00000000 --- a/web/pages/teams/[id].tsx +++ /dev/null @@ -1,122 +0,0 @@ -import type { NextPage } from 'next' -import { Page } from '@/components/layout/Page' -import titleWrapper from '@/utils/titleWrapper' -import { useTasksTranslation } from '@/i18n/useTasksTranslation' -import { ContentPanel } from '@/components/layout/ContentPanel' -import { Button, LoadingContainer, TabPanel, TabSwitcher } from '@helpwave/hightide' -import { CenteredLoadingLogo } from '@/components/CenteredLoadingLogo' -import { PatientList } from '@/components/tables/PatientList' -import type { TaskViewModel } from '@/components/tables/TaskList' -import { TaskList } from '@/components/tables/TaskList' -import { useLocationNode, usePatients, useTasks } from '@/data' -import { useMemo, useState } from 'react' -import { useRouter } from 'next/router' - -const TeamPage: NextPage = () => { - const translation = useTasksTranslation() - const router = useRouter() - const id = Array.isArray(router.query['id']) ? router.query['id'][0] : router.query['id'] - const [showAllTasks, setShowAllTasks] = useState(false) - - const { data: locationNode, loading: isLoadingLocation, error: locationError } = useLocationNode( - id ?? '', - { skip: !id } - ) - const locationData = locationNode ? { locationNode } : undefined - const isLocationError = !!locationError - - const { refetch: refetchPatients, loading: isLoadingPatients } = usePatients( - { locationId: id }, - { skip: !id } - ) - - const { data: tasksData, refetch: refetchTasks, loading: isLoadingTasks } = useTasks( - { - assigneeTeamId: showAllTasks ? undefined : id, - rootLocationIds: undefined, - }, - { skip: !id } - ) - - const tasks: TaskViewModel[] = useMemo(() => { - if (!tasksData?.tasks) return [] - - return tasksData.tasks.map(task => ({ - id: task.id, - name: task.title, - description: task.description || undefined, - updateDate: task.updateDate ? new Date(task.updateDate) : new Date(task.creationDate), - dueDate: task.dueDate ? new Date(task.dueDate) : undefined, - priority: task.priority || null, - estimatedTime: task.estimatedTime ?? null, - done: task.done, - patient: task.patient - ? { - id: task.patient.id, - name: task.patient.name, - locations: task.patient.assignedLocations || [] - } - : undefined, - assignee: task.assignee - ? { id: task.assignee.id, name: task.assignee.name, avatarURL: task.assignee.avatarUrl, isOnline: task.assignee.isOnline ?? null } - : undefined, - assigneeTeam: task.assigneeTeam - ? { id: task.assigneeTeam.id, title: task.assigneeTeam.title } - : undefined, - })) - }, [tasksData]) - - const isLoading = isLoadingLocation || isLoadingPatients || (showAllTasks && isLoadingTasks) - const isError = isLocationError || !id - - const handleRefetch = () => { - refetchPatients() - if (showAllTasks) { - refetchTasks() - } - } - - return ( - - )} - > - {isLoading && ( - - )} - {!isLoading && isError && ( -
- {translation('errorOccurred')} -
- )} - {!isLoading && !isError && ( - - - - - - setShowAllTasks(!showAllTasks)} - color="neutral" - coloringStyle="outline" - className="w-full sm:w-auto flex-shrink-0" - > - {showAllTasks ? translation('showTeamTasks') ?? 'Show Team Tasks' : translation('showAllTasks') ?? 'Show All Tasks'} - - )} - /> - - - )} -
-
- ) -} - -export default TeamPage diff --git a/web/pages/teams/index.tsx b/web/pages/teams/index.tsx deleted file mode 100644 index 5c538636..00000000 --- a/web/pages/teams/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { NextPage } from 'next' -import { Page } from '@/components/layout/Page' -import titleWrapper from '@/utils/titleWrapper' -import { useTasksTranslation } from '@/i18n/useTasksTranslation' -import { ContentPanel } from '@/components/layout/ContentPanel' - -const TeamsOverviewPage: NextPage = () => { - const translation = useTasksTranslation() - - return ( - - - - - ) -} - -export default TeamsOverviewPage diff --git a/web/pages/wards/[id].tsx b/web/pages/wards/[id].tsx deleted file mode 100644 index 55812308..00000000 --- a/web/pages/wards/[id].tsx +++ /dev/null @@ -1,94 +0,0 @@ -import type { NextPage } from 'next' -import { Page } from '@/components/layout/Page' -import titleWrapper from '@/utils/titleWrapper' -import { useTasksTranslation } from '@/i18n/useTasksTranslation' -import { ContentPanel } from '@/components/layout/ContentPanel' -import { LoadingContainer, TabSwitcher, TabPanel } from '@helpwave/hightide' -import { CenteredLoadingLogo } from '@/components/CenteredLoadingLogo' -import { PatientList } from '@/components/tables/PatientList' -import type { TaskViewModel } from '@/components/tables/TaskList' -import { TaskList } from '@/components/tables/TaskList' -import { useMemo } from 'react' -import { useRouter } from 'next/router' -import { useLocationNode, usePatientsPaginated } from '@/data' - -const WardPage: NextPage = () => { - const translation = useTasksTranslation() - const router = useRouter() - const id = Array.isArray(router.query['id']) ? router.query['id'][0] : router.query['id'] - - const { data: locationNode, loading: isLoadingLocation, error: locationError } = useLocationNode( - id ?? '', - { skip: !id } - ) - const locationData = locationNode ? { locationNode } : undefined - const isLocationError = !!locationError - - const { data: patientsData, refetch: refetchPatients, loading: isLoadingPatients } = usePatientsPaginated( - { locationId: id }, - { pageSize: 10 } - ) - - const tasks: TaskViewModel[] = useMemo(() => { - if (!patientsData || patientsData.length === 0) return [] - - return patientsData.flatMap(patient => { - if (!patient.tasks) return [] - - return patient.tasks.map(task => ({ - id: task.id, - name: task.title, - description: task.description || undefined, - updateDate: task.updateDate ? new Date(task.updateDate) : new Date(task.creationDate), - dueDate: task.dueDate ? new Date(task.dueDate) : undefined, - priority: task.priority || null, - estimatedTime: task.estimatedTime ?? null, - done: task.done, - patient: { - id: patient.id, - name: patient.name, - locations: patient.assignedLocations || [] - }, - assignee: task.assignee - ? { id: task.assignee.id, name: task.assignee.name, avatarURL: task.assignee.avatarUrl } - : undefined, - })) - }) - }, [patientsData]) - - const isLoading = isLoadingLocation || isLoadingPatients - const isError = isLocationError || !id - - return ( - - )} - > - {isLoading && ( - - )} - {!isLoading && isError && ( -
- {translation('errorOccurred')} -
- )} - {!isLoading && !isError && ( - - - - - - - - - )} -
-
- ) -} - -export default WardPage diff --git a/web/pages/wards/index.tsx b/web/pages/wards/index.tsx deleted file mode 100644 index 217eca14..00000000 --- a/web/pages/wards/index.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import type { NextPage } from 'next' -import { Page } from '@/components/layout/Page' -import titleWrapper from '@/utils/titleWrapper' -import { useTasksTranslation } from '@/i18n/useTasksTranslation' -import { ContentPanel } from '@/components/layout/ContentPanel' -import { useMemo } from 'react' -import { useQuery } from '@tanstack/react-query' -import { ExpandableUncontrolled, LoadingAndErrorComponent } from '@helpwave/hightide' -import { Cross, Star } from 'lucide-react' -import clsx from 'clsx' -import { useRouter } from 'next/router' - -type Ward = { - id: string, - name: string, - patientCount: number, - freeBedCount: number, - totalBedCount: number, - isFavorite: boolean, -} - -type WardGroup = { - name: string, - wards: Ward[], -} - -const defaultData: WardGroup[] = [ - { - name: 'Emergency', - wards: [ - { - id: 'ER-1', - name: 'Trauma Unit', - patientCount: 18, - freeBedCount: 2, - totalBedCount: 20, - isFavorite: true, - }, - { - id: 'ER-2', - name: 'Acute Care', - patientCount: 22, - freeBedCount: 3, - totalBedCount: 25, - isFavorite: false, - }, - { - id: 'ER-3', - name: 'Observation', - patientCount: 10, - freeBedCount: 5, - totalBedCount: 15, - isFavorite: false, - }, - ], - }, - { - name: 'Surgery', - wards: [ - { - id: 'SUR-1', - name: 'General Surgery', - patientCount: 16, - freeBedCount: 4, - totalBedCount: 20, - isFavorite: true, - }, - { - id: 'SUR-2', - name: 'Orthopedics', - patientCount: 14, - freeBedCount: 6, - totalBedCount: 20, - isFavorite: false, - }, - { - id: 'SUR-3', - name: 'Neurosurgery', - patientCount: 9, - freeBedCount: 1, - totalBedCount: 10, - isFavorite: false, - }, - ], - }, - { - name: 'Internal Medicine', - wards: [ - { - id: 'IM-1', - name: 'Cardiology', - patientCount: 20, - freeBedCount: 2, - totalBedCount: 22, - isFavorite: true, - }, - { - id: 'IM-2', - name: 'Pulmonology', - patientCount: 15, - freeBedCount: 5, - totalBedCount: 20, - isFavorite: false, - }, - ], - }, - { - name: 'Pediatrics', - wards: [ - { - id: 'PED-1', - name: 'Neonatal', - patientCount: 8, - freeBedCount: 2, - totalBedCount: 10, - isFavorite: false, - }, - { - id: 'PED-2', - name: 'Child Care', - patientCount: 12, - freeBedCount: 6, - totalBedCount: 18, - isFavorite: true, - }, - ], - }, -] - -type WardCardProps = { - ward: Ward, -} - -const WardCard = ({ ward }: WardCardProps) => { - const translation = useTasksTranslation() - const { isFavorite, name, patientCount, totalBedCount, freeBedCount } = ward - const occupancyPercentage = Math.round((1 - (freeBedCount / Math.max(totalBedCount, 1))) * 100) - const router = useRouter() - - return ( -
{ - router.push(`/location/${ward.id}`).catch(() => {}) - }} - > -
-
-
- - {name} -
- -
- - {translation('nPatient', { count: patientCount })} - -
-
-
- - {freeBedCount} - - {/* TODO add typography-label-sm here once it exists */} - - {translation('freeBeds')} - -
-
- - {occupancyPercentage + '%'} - - {/* TODO add typography-label-sm here once it exists */} - - {translation('occupancy')} - -
-
-
- ) -} - -const WardsOverviewPage: NextPage = () => { - const translation = useTasksTranslation() - - const { data, isLoading, isError } = useQuery({ - queryKey: ['ward', 'overview'], - queryFn: async () => { - await new Promise(r => setTimeout(r, 1000)) - return defaultData - } - }) - const favorites = useMemo( - () => - data?.reduce((acc, group) => { - group.wards.forEach(ward => { - if (ward.isFavorite) acc.push(ward) - }) - return acc - }, []) ?? [], - [data] - ) - - return ( - - - - - - {translation('myFavorites')} -
- )} - headerClassName="typography-label-md font-bold !px-4 !py-4 rounded-xl" - contentExpandedClassName="!max-h-none !h-auto !overflow-visible pb-4" - className="rounded-xl" - isExpanded={true} - > -
    - {favorites.map(ward => ( -
  • - -
  • - ))} -
- - {data?.map((wardGroup, index) => ( - -
    - {wardGroup.wards.map(ward => ( -
  • - -
  • - ))} -
-
- ))} - - - - ) -} - -export default WardsOverviewPage From 628963d6484932359bdcef80b1d283c7219bba82 Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Thu, 5 Feb 2026 12:45:27 +0100 Subject: [PATCH 3/4] add merge strategy --- web/data/cache/policies.ts | 1 + web/hooks/useTasksContext.tsx | 30 ++++++++++++++++++------------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/web/data/cache/policies.ts b/web/data/cache/policies.ts index 3302b2d8..c9129ffb 100644 --- a/web/data/cache/policies.ts +++ b/web/data/cache/policies.ts @@ -17,6 +17,7 @@ export function buildCacheConfig(): InMemoryCacheConfig { patient: { keyArgs: ['id'] }, patients: { keyArgs: ['locationId', 'rootLocationIds', 'states', 'filtering', 'sorting', 'search', 'pagination'], + merge: (_existing, incoming) => incoming, }, locationNode: { keyArgs: ['id'] }, locationNodes: { diff --git a/web/hooks/useTasksContext.tsx b/web/hooks/useTasksContext.tsx index 9193fe53..1d100979 100644 --- a/web/hooks/useTasksContext.tsx +++ b/web/hooks/useTasksContext.tsx @@ -1,6 +1,7 @@ import type { Dispatch, SetStateAction } from 'react' import { createContext, type PropsWithChildren, useContext, useEffect, useRef, useState } from 'react' import { usePathname } from 'next/navigation' +import { HelpwaveLogo } from '@helpwave/hightide' import { useGlobalData, useLocations } from '@/data' import { useQueryClient } from '@tanstack/react-query' import { useAuth } from './useAuth' @@ -92,6 +93,7 @@ export type TasksContextState = { sidebar: SidebarContextType, user?: User, rootLocations?: LocationNode[], + isRootLocationReinitializing?: boolean, } export type TasksContextType = TasksContextState & { @@ -125,6 +127,7 @@ export const TasksContextProvider = ({ children }: PropsWithChildren) => { isShowingClinics: false, }, selectedRootLocationIds: storedSelectedRootLocationIds.length > 0 ? storedSelectedRootLocationIds : undefined, + isRootLocationReinitializing: false, }) const { data: allLocationsData } = useLocations( @@ -152,19 +155,8 @@ export const TasksContextProvider = ({ children }: PropsWithChildren) => { const currentSelectedIds = (state.selectedRootLocationIds || []).sort().join(',') if (prevSelectedRootLocationIdsRef.current !== currentSelectedIds) { prevSelectedRootLocationIdsRef.current = currentSelectedIds - + setState(prev => ({ ...prev, isRootLocationReinitializing: true })) queryClient.invalidateQueries() - queryClient.removeQueries({ - predicate: (query) => { - const queryKey = query.queryKey as unknown[] - const queryKeyStr = JSON.stringify(queryKey) - return queryKeyStr.includes('GetPatients') || - queryKeyStr.includes('GetTasks') || - queryKeyStr.includes('GetLocations') || - queryKeyStr.includes('GetGlobalData') || - queryKeyStr.includes('GetOverviewData') - } - }) } }, [state.selectedRootLocationIds, queryClient]) @@ -281,6 +273,7 @@ export const TasksContextProvider = ({ children }: PropsWithChildren) => { ), rootLocations, selectedRootLocationIds, + isRootLocationReinitializing: false, } }) }, [effectInputKey, data, storedSelectedRootLocationIds, allLocationsData, setStoredSelectedRootLocationIds]) @@ -318,6 +311,19 @@ export const TasksContextProvider = ({ children }: PropsWithChildren) => { }} > {children} + {state.isRootLocationReinitializing && ( + + )} ) } From 93b9b6c6183ecc817b94266218c3b56d58601260 Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Thu, 5 Feb 2026 13:11:18 +0100 Subject: [PATCH 4/4] update caching policies --- web/components/layout/Page.tsx | 144 +++++++++--------- web/data/cache/policies.ts | 10 +- .../useApolloGlobalSubscriptions.ts | 17 +-- web/eslint.config.js | 1 + web/hooks/useTasksContext.tsx | 2 +- 5 files changed, 86 insertions(+), 88 deletions(-) diff --git a/web/components/layout/Page.tsx b/web/components/layout/Page.tsx index 48e69be8..1bb4d9bc 100644 --- a/web/components/layout/Page.tsx +++ b/web/components/layout/Page.tsx @@ -469,82 +469,82 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => { {context?.totalPatientsCount !== undefined && ({context.totalPatientsCount})} context?.update(prevState => ({ - ...prevState, - sidebar: { - ...prevState.sidebar, - isShowingTeams: isExpanded, - } - }))} - > - -
- - {translation('teams')} -
-
- - {(context?.teams ?? []).map(team => ( - - {team.title} - - ))} - -
+ className="shadow-none" + isExpanded={context?.sidebar?.isShowingTeams ?? false} + onExpandedChange={isExpanded => context?.update(prevState => ({ + ...prevState, + sidebar: { + ...prevState.sidebar, + isShowingTeams: isExpanded, + } + }))} + > + +
+ + {translation('teams')} +
+
+ + {(context?.teams ?? []).map(team => ( + + {team.title} + + ))} + + context?.update(prevState => ({ - ...prevState, - sidebar: { - ...prevState.sidebar, - isShowingWards: isExpanded, - } - }))} - > - -
- - {translation('wards')} -
-
- - {(context?.wards ?? []).map(ward => ( - - {ward.title} - - ))} - -
+ className="shadow-none" + isExpanded={context?.sidebar?.isShowingWards ?? false} + onExpandedChange={isExpanded => context?.update(prevState => ({ + ...prevState, + sidebar: { + ...prevState.sidebar, + isShowingWards: isExpanded, + } + }))} + > + +
+ + {translation('wards')} +
+
+ + {(context?.wards ?? []).map(ward => ( + + {ward.title} + + ))} + + context?.update(prevState => ({ - ...prevState, - sidebar: { - ...prevState.sidebar, - isShowingClinics: isExpanded, - } - }))} - > - -
- - {translation('clinics')} -
-
- - {(context?.clinics ?? []).map(clinic => ( - - {clinic.title} - - ))} - -
+ className="shadow-none" + isExpanded={context?.sidebar?.isShowingClinics ?? false} + onExpandedChange={isExpanded => context?.update(prevState => ({ + ...prevState, + sidebar: { + ...prevState.sidebar, + isShowingClinics: isExpanded, + } + }))} + > + +
+ + {translation('clinics')} +
+
+ + {(context?.clinics ?? []).map(clinic => ( + + {clinic.title} + + ))} + +
diff --git a/web/data/cache/policies.ts b/web/data/cache/policies.ts index c9129ffb..b0025b9c 100644 --- a/web/data/cache/policies.ts +++ b/web/data/cache/policies.ts @@ -31,7 +31,15 @@ export function buildCacheConfig(): InMemoryCacheConfig { Task: { keyFields: ['id'] }, Patient: { keyFields: ['id'] }, User: { keyFields: ['id'] }, - UserType: { keyFields: ['id'] }, + UserType: { + keyFields: ['id'], + fields: { + tasks: { + keyArgs: ['rootLocationIds'], + merge: (_existing, incoming) => incoming, + }, + }, + }, LocationNode: { keyFields: ['id'] }, LocationNodeType: { keyFields: ['id'] }, PropertyValue: { keyFields: propertyValueKeyFields }, diff --git a/web/data/subscriptions/useApolloGlobalSubscriptions.ts b/web/data/subscriptions/useApolloGlobalSubscriptions.ts index 5bd0f190..dbe22ba5 100644 --- a/web/data/subscriptions/useApolloGlobalSubscriptions.ts +++ b/web/data/subscriptions/useApolloGlobalSubscriptions.ts @@ -15,17 +15,6 @@ import { removeRefreshingPatient } from './refreshingEntities' import { getConnectionStatus, setConnectionStatus } from '@/data/connectionStatus' -import { - GetGlobalDataDocument, - GetTasksDocument, - GetPatientsDocument -} from '@/api/gql/generated' - -const QUERIES_TO_REFETCH_AFTER_MERGE = [ - GetGlobalDataDocument, - GetTasksDocument, - GetPatientsDocument, -] const TASK_UPDATED = ` subscription TaskUpdated($taskId: ID, $rootLocationIds: [ID!]) { @@ -112,7 +101,7 @@ export function useApolloGlobalSubscriptions( await mergeTaskUpdatedIntoCache(client, taskId, payloadObj, optionsRef.current).catch( () => {} ) - client.refetchQueries({ include: QUERIES_TO_REFETCH_AFTER_MERGE }) + client.refetchQueries({ include: 'active' }) } finally { removeRefreshingTask(taskId) } @@ -143,7 +132,7 @@ export function useApolloGlobalSubscriptions( await mergePatientUpdatedIntoCache(client, patientId, payloadObj, optionsRef.current).catch( () => {} ) - client.refetchQueries({ include: QUERIES_TO_REFETCH_AFTER_MERGE }) + client.refetchQueries({ include: 'active' }) } finally { removeRefreshingPatient(patientId) } @@ -174,7 +163,7 @@ export function useApolloGlobalSubscriptions( await mergePatientUpdatedIntoCache(client, patientId, payloadObj, optionsRef.current).catch( () => {} ) - client.refetchQueries({ include: QUERIES_TO_REFETCH_AFTER_MERGE }) + client.refetchQueries({ include: 'active' }) } finally { removeRefreshingPatient(patientId) } diff --git a/web/eslint.config.js b/web/eslint.config.js index 3a0c9e58..f3d2d3de 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -6,6 +6,7 @@ export default [ 'api/gql/*', 'i18n/*', 'next-env.d.ts', + 'build/*', ], }, { diff --git a/web/hooks/useTasksContext.tsx b/web/hooks/useTasksContext.tsx index 1d100979..86145e1e 100644 --- a/web/hooks/useTasksContext.tsx +++ b/web/hooks/useTasksContext.tsx @@ -246,7 +246,7 @@ export const TasksContextProvider = ({ children }: PropsWithChildren) => { avatarUrl: data.me.avatarUrl, organizations: data.me.organizations ?? null, isOnline: data.me.isOnline ?? null - } : undefined, + } : prevState.user, myTasksCount: data?.me?.tasks?.filter(t => !t.done).length ?? 0, totalPatientsCount, waitingPatientsCount,