From 1a8ab95b2090700e4c08475d647b57c32e6075a6 Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Sun, 5 Jul 2026 10:42:26 +0200 Subject: [PATCH 1/9] 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 && ( - - -
- - - \ No newline at end of file From ea7bc49b6b50bd71e3d6dd55ed3dcee03ae4a283 Mon Sep 17 00:00:00 2001 From: Felix Thape <67233923+DasProffi@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:09:10 +0200 Subject: [PATCH 7/9] fix: fix font imports --- web/style/fonts.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/style/fonts.css b/web/style/fonts.css index 45f285e2..7f9f5949 100644 --- a/web/style/fonts.css +++ b/web/style/fonts.css @@ -1,6 +1,6 @@ @font-face { font-family: "Inter"; - src: url("/fonts/Inter-VariableFont_opsz,wght.ttf") format("truetype"); + src: url("/fonts/Inter/Inter-VariableFont_opsz,wght.ttf") format("truetype"); font-weight: 100 900; font-style: normal; font-display: swap; @@ -8,7 +8,7 @@ @font-face { font-family: "Space Grotesk"; - src: url("/fonts/SpaceGrotesk-VariableFont_wght.ttf") format("truetype"); + src: url("/fonts/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf") format("truetype"); font-weight: 300 700; font-style: normal; font-display: swap; From 304d042441583e50247ea3dc9af358ecb8241c5a Mon Sep 17 00:00:00 2001 From: Felix Thape <67233923+DasProffi@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:29:53 +0200 Subject: [PATCH 8/9] fix: fix lazy loading ReactQueryDevtools in _app to cause test errors --- tests/playwright.config.ts | 6 ++++++ web/pages/_app.tsx | 4 +++- web/utils/config.ts | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts index e85c13d9..641f2945 100644 --- a/tests/playwright.config.ts +++ b/tests/playwright.config.ts @@ -1,5 +1,7 @@ import { defineConfig, devices } from '@playwright/test' +process.env.TESTMODE = 'true' + const baseURL = process.env.E2E_BASE_URL || 'http://localhost:3000' if (!baseURL || baseURL.trim() === '') { @@ -31,5 +33,9 @@ export default defineConfig({ command: 'cd ../web && npm run dev', url: baseURL.trim(), reuseExistingServer: true, + env: { + ...process.env, + TESTMODE: 'true', + }, }, }) diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 7a63e545..6dbfd277 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -74,7 +74,9 @@ function MyApp({ - {config.env === 'development' && } + {config.env === 'development' && !config.testMode && ( + + )} ) diff --git a/web/utils/config.ts b/web/utils/config.ts index adea920b..edb1d68c 100644 --- a/web/utils/config.ts +++ b/web/utils/config.ts @@ -2,6 +2,7 @@ import { z } from 'zod' export const publicEnvSchema = z.object({ NODE_ENV: z.string().default('production'), + TESTMODE: z.literal('true').or(z.literal('false')).default('false'), RUNTIME_SHOW_STAGING_DISCLAIMER_MODAL: z.literal('true').or(z.literal('false')).optional(), RUNTIME_FEATURES_FEED_URL: z.string().url().default('https://cdn.helpwave.de/feed.json'), RUNTIME_IMPRINT_URL: z.string().url().default('https://cdn.helpwave.de/imprint.html'), @@ -18,6 +19,7 @@ export const publicEnvSchema = z.object({ const configSchema = publicEnvSchema.transform(obj => ({ env: obj.NODE_ENV, + testMode: obj.TESTMODE === 'true', showStagingDisclaimerModal: obj.RUNTIME_SHOW_STAGING_DISCLAIMER_MODAL === 'true', featuresFeedUrl: obj.RUNTIME_FEATURES_FEED_URL, imprintUrl: obj.RUNTIME_IMPRINT_URL, From f63ee3ada1bea7b804ec9aadee52e1c31478b2ee Mon Sep 17 00:00:00 2001 From: Felix Thape <67233923+DasProffi@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:51:44 +0200 Subject: [PATCH 9/9] chore: update to hightide 0.15 --- web/package-lock.json | 8 ++++---- web/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index a300dacf..0f53a2c6 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,7 +12,7 @@ "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", "@dnd-kit/sortable": "10.0.0", - "@helpwave/hightide": "0.14.5", + "@helpwave/hightide": "0.15.0", "@helpwave/internationalization": "0.4.0", "@tailwindcss/postcss": "4.3.1", "@tanstack/react-query": "5.101.0", @@ -1879,9 +1879,9 @@ } }, "node_modules/@helpwave/hightide": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/@helpwave/hightide/-/hightide-0.14.5.tgz", - "integrity": "sha512-WWMqR3T7ikU9Xf5GfdXMZiNSzJIDRA8vXdBiHBK9qiIuSgdQyZgHgC1yjuNjCnp/X8Q3sBUv+ugDwOijm3zing==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@helpwave/hightide/-/hightide-0.15.0.tgz", + "integrity": "sha512-1DtR3MpLj/E8IcN3ajBXdQ+tafC7wsUwKZwvWGlwcKw6eW7SC1uMi9dE8mA95NXZ9uCrZ8Pahn/8pfEr2LgXyg==", "license": "MPL-2.0", "dependencies": { "@helpwave/internationalization": "0.4.0", diff --git a/web/package.json b/web/package.json index 7ff008d8..70101ef5 100644 --- a/web/package.json +++ b/web/package.json @@ -30,7 +30,7 @@ "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", "@dnd-kit/sortable": "10.0.0", - "@helpwave/hightide": "0.14.5", + "@helpwave/hightide": "0.15.0", "@helpwave/internationalization": "0.4.0", "@tailwindcss/postcss": "4.3.1", "@tanstack/react-query": "5.101.0",