From 1a8ab95b2090700e4c08475d647b57c32e6075a6 Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Sun, 5 Jul 2026 10:42:26 +0200 Subject: [PATCH 01/10] improve performance using hightide 0.13.5 --- tests/e2e/patient-table.spec.ts | 51 +++++++++++++++++- web/components/common/ScrollToTopButton.tsx | 17 ++++-- web/components/tables/PatientList.tsx | 8 +-- web/components/tables/TaskList.tsx | 8 +-- web/package-lock.json | 6 +-- web/package.json | 2 +- web/pages/properties/index.tsx | 4 +- web/pages/settings/views.tsx | 4 +- web/style/table-printing.css | 1 + web/utils/virtualGrid.test.ts | 57 --------------------- web/utils/virtualGrid.ts | 36 ------------- 11 files changed, 81 insertions(+), 113 deletions(-) delete mode 100644 web/utils/virtualGrid.test.ts delete mode 100644 web/utils/virtualGrid.ts diff --git a/tests/e2e/patient-table.spec.ts b/tests/e2e/patient-table.spec.ts index 3962df5a..4b09a11d 100644 --- a/tests/e2e/patient-table.spec.ts +++ b/tests/e2e/patient-table.spec.ts @@ -308,6 +308,55 @@ test.describe('patient table (patient list)', () => { const table = page.locator('table[data-name="table"]') await expect(table).toHaveAttribute('data-column-sizing', 'natural') expect(await table.evaluate((el) => (el as HTMLElement).style.width)).toBe('') - expect(await table.evaluate((el) => getComputedStyle(el).tableLayout)).toBe('auto') + await expect(table).toHaveAttribute('data-natural-locked', '') + expect(await table.evaluate((el) => getComputedStyle(el).tableLayout)).toBe('fixed') + }) + + test('column resize drags clamp to the minimum width but allow widening', async ({ page }) => { + await page.setViewportSize({ width: 1600, height: 900 }) + await seedAuth(page) + await seedStoredSelection(page, ['root-1']) + await mockBackend(page, { + patients: PATIENTS, + propertyDefinitions: [ALLERGY_DEF], + rootLocations: ROOT_LOCATIONS, + }) + + await page.goto(`${BASE}/patients`) + await expect(page.locator(ROW_SELECTOR).first()).toBeVisible({ timeout: 20000 }) + await page.waitForTimeout(500) + + const nameHeader = page.locator('th[data-name="table-header-cell"]').first() + const handle = nameHeader.locator('[data-name="table-resize-indicator"]') + const before = (await nameHeader.boundingBox())! + + await nameHeader.hover() + let handleBox = (await handle.boundingBox())! + await page.mouse.move(handleBox.x + handleBox.width / 2, handleBox.y + handleBox.height / 2) + await page.mouse.down() + for (let x = handleBox.x; x > handleBox.x - 400; x -= 40) { + await page.mouse.move(x, handleBox.y) + await page.waitForTimeout(20) + } + await page.mouse.up() + await page.waitForTimeout(300) + + const shrunk = (await nameHeader.boundingBox())! + expect(shrunk.width).toBeGreaterThanOrEqual(199) + expect(shrunk.width).toBeLessThanOrEqual(before.width + 1) + + await nameHeader.hover() + handleBox = (await handle.boundingBox())! + await page.mouse.move(handleBox.x + handleBox.width / 2, handleBox.y + handleBox.height / 2) + await page.mouse.down() + for (let x = handleBox.x; x < handleBox.x + 300; x += 40) { + await page.mouse.move(x, handleBox.y) + await page.waitForTimeout(20) + } + await page.mouse.up() + await page.waitForTimeout(300) + + const widened = (await nameHeader.boundingBox())! + expect(widened.width).toBeGreaterThan(shrunk.width + 200) }) }) diff --git a/web/components/common/ScrollToTopButton.tsx b/web/components/common/ScrollToTopButton.tsx index c55e4d61..2b835a61 100644 --- a/web/components/common/ScrollToTopButton.tsx +++ b/web/components/common/ScrollToTopButton.tsx @@ -27,10 +27,21 @@ export const ScrollToTopButton = () => { useEffect(() => { if (!scrollElement) return - const handleScroll = () => setIsVisible(scrollElement.scrollTop > SHOW_THRESHOLD_PX) - handleScroll() + let frame: number | null = null + const update = () => { + frame = null + setIsVisible(scrollElement.scrollTop > SHOW_THRESHOLD_PX) + } + const handleScroll = () => { + if (frame !== null) return + frame = window.requestAnimationFrame(update) + } + update() scrollElement.addEventListener('scroll', handleScroll, { passive: true }) - return () => scrollElement.removeEventListener('scroll', handleScroll) + return () => { + if (frame !== null) window.cancelAnimationFrame(frame) + scrollElement.removeEventListener('scroll', handleScroll) + } }, [scrollElement]) return ( diff --git a/web/components/tables/PatientList.tsx b/web/components/tables/PatientList.tsx index dbb3f7fd..96dadd21 100644 --- a/web/components/tables/PatientList.tsx +++ b/web/components/tables/PatientList.tsx @@ -1,7 +1,7 @@ import { useMemo, useState, forwardRef, useImperativeHandle, useEffect, useCallback, useRef, type ReactNode } from 'react' import { useMutation } from '@apollo/client/react' import type { IdentifierFilterValue, FilterListItem, FilterListPopUpBuilderProps } from '@helpwave/hightide' -import { Chip, DateUtils, FillerCell, SearchBar, ProgressIndicator, Tooltip, Drawer, TableProvider, TableDisplay, TableColumnSwitcher, IconButton, useLocale, FilterList, SortingList, Button, ExpansionIcon, Visibility, ConfirmDialog, VirtualizedCardGrid } from '@helpwave/hightide' +import { Chip, DateUtils, FillerCell, SearchBar, ProgressIndicator, Tooltip, Drawer, TableProvider, TableDisplay, TableColumnSwitcher, IconButton, useLocale, FilterList, SortingList, Button, ExpansionIcon, Visibility, ConfirmDialog, VirtualizedCardGrid, overscanRowsForBuffer } from '@helpwave/hightide' import clsx from 'clsx' import { LayoutGrid, PlusIcon, Table2 } from 'lucide-react' import type { LocationType } from '@/api/gql/generated' @@ -23,7 +23,7 @@ import { columnFiltersToQueryFilterClauses, sortingStateToQuerySortClauses } fro import { LIST_PAGE_SIZE } from '@/utils/listPaging' import { useAccumulatedPagination } from '@/hooks/useAccumulatedPagination' import { RowRefreshingGate } from '@/components/tables/RowRefreshingGate' -import { overscanRowsForBuffer } from '@/utils/virtualGrid' + import { ListLoadingHint } from '@/components/common/ListLoadingHint' import { useIsPrinting } from '@/hooks/useIsPrinting' import { ScrollToTopButton } from '@/components/common/ScrollToTopButton' @@ -1221,7 +1221,7 @@ export const PatientList = forwardRef(({ )} )} -
+
(({ containerProps={{ className: 'print:max-h-none print:overflow-visible', }} - className="print-content overflow-x-auto hw-touch-scroll" + className="print-content" />
{listLayout === 'card' && ( diff --git a/web/components/tables/TaskList.tsx b/web/components/tables/TaskList.tsx index 1911cc79..1837d9a8 100644 --- a/web/components/tables/TaskList.tsx +++ b/web/components/tables/TaskList.tsx @@ -1,7 +1,7 @@ import { useMemo, useState, forwardRef, useImperativeHandle, useEffect, useRef, useCallback, type ReactNode } from 'react' import { useQueryClient } from '@tanstack/react-query' import type { FilterListItem } from '@helpwave/hightide' -import { Button, Checkbox, ConfirmDialog, FilterList, FillerCell, IconButton, SearchBar, TableColumnSwitcher, TableDisplay, TableProvider, SortingList, ExpansionIcon, VirtualizedCardGrid } from '@helpwave/hightide' +import { Button, Checkbox, ConfirmDialog, FilterList, FillerCell, IconButton, SearchBar, TableColumnSwitcher, TableDisplay, TableProvider, SortingList, ExpansionIcon, VirtualizedCardGrid, overscanRowsForBuffer } from '@helpwave/hightide' import clsx from 'clsx' import { Edit2, ExternalLink, LayoutGrid, PlusIcon, Table2, UserCheck } from 'lucide-react' import type { IdentifierFilterValue } from '@helpwave/hightide' @@ -32,7 +32,7 @@ import { queryableFieldsToFilterListItems, queryableFieldsToSortingListItems, ty import { LIST_PAGE_SIZE } from '@/utils/listPaging' import { TaskCardView } from '@/components/tasks/TaskCardView' import { RefreshingTaskIdsContext, TaskRowRefreshingGate } from '@/components/tables/TaskRowRefreshingGate' -import { overscanRowsForBuffer } from '@/utils/virtualGrid' + import { ListLoadingHint } from '@/components/common/ListLoadingHint' import { useIsPrinting } from '@/hooks/useIsPrinting' import { ScrollToTopButton } from '@/components/common/ScrollToTopButton' @@ -1055,7 +1055,7 @@ export const TaskList = forwardRef(({ tasks: initial )}
)} -
+
{!embedded && (