From 0dad7ca67d58d872ee031413ba51e60618fea93e Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 31 Jul 2026 13:29:10 +0200 Subject: [PATCH] fix(widgets): address table layout review --- .../docs/widgets/docker-containers/index.mdx | 4 ++ .../src/beszel-system-table/component.tsx | 16 +++---- .../src/common/use-persisted-table-layout.ts | 47 +++++++++++++++---- packages/widgets/src/docker/component.tsx | 16 +++---- packages/widgets/src/downloads/component.tsx | 16 +++---- 5 files changed, 67 insertions(+), 32 deletions(-) diff --git a/apps/docs/docs/widgets/docker-containers/index.mdx b/apps/docs/docs/widgets/docker-containers/index.mdx index 05c2208a7..3dfa40d7f 100644 --- a/apps/docs/docs/widgets/docker-containers/index.mdx +++ b/apps/docs/docs/widgets/docker-containers/index.mdx @@ -52,3 +52,7 @@ Each container row has an action menu, accessible via the three-dot icon or by r ### Adding the widget + +### Configuration + + diff --git a/packages/widgets/src/beszel-system-table/component.tsx b/packages/widgets/src/beszel-system-table/component.tsx index 8f66ded77..8286335a6 100644 --- a/packages/widgets/src/beszel-system-table/component.tsx +++ b/packages/widgets/src/beszel-system-table/component.tsx @@ -26,7 +26,7 @@ import { useScopedI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; import { HomarrDataTable } from "../common/homarr-data-table"; -import { usePersistedTableLayout } from "../common/use-persisted-table-layout"; +import { usePersistedTableLayout, useTableLayoutPersistence } from "../common/use-persisted-table-layout"; import type { BeszelSystemRow } from "../beszel/_shared/types"; import { loadAvgColor, statusColorMap, thresholdColor } from "../beszel/_shared/colors"; import { @@ -116,13 +116,13 @@ export default function BeszelSystemTableWidget({ message: t("error.layoutSaveMessage"), }), }); - const persistLayout = useCallback( - (newOptions: Partial>) => { - setOptions({ newOptions }); - if (hasChangeAccess && boardId && itemId) saveItemOptions({ boardId, itemId, newOptions }); - }, - [boardId, hasChangeAccess, itemId, saveItemOptions, setOptions], - ); + const persistLayout = useTableLayoutPersistence({ + boardId, + hasChangeAccess, + itemId, + saveItemOptions, + setOptions, + }); const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter); diff --git a/packages/widgets/src/common/use-persisted-table-layout.ts b/packages/widgets/src/common/use-persisted-table-layout.ts index bfee3e0c9..c9961bba5 100644 --- a/packages/widgets/src/common/use-persisted-table-layout.ts +++ b/packages/widgets/src/common/use-persisted-table-layout.ts @@ -1,14 +1,22 @@ "use client"; -import { useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import type { DataTableColumn } from "mantine-datatable"; import { useDataTableColumns } from "mantine-datatable"; -interface TableLayoutOptions { +interface TableLayoutOptions extends Record { columnOrder?: string; columnWidths?: string; } +interface UseTableLayoutPersistenceProps { + boardId: string | undefined; + hasChangeAccess: boolean; + itemId: string | undefined; + saveItemOptions: (input: { boardId: string; itemId: string; newOptions: TableLayoutOptions }) => void; + setOptions: (input: { newOptions: TableLayoutOptions }) => void; +} + interface UsePersistedTableLayoutProps { columns: DataTableColumn[]; columnAccessors: readonly string[]; @@ -21,6 +29,21 @@ interface UsePersistedTableLayoutProps { const layoutSaveDelay = 350; +export const useTableLayoutPersistence = ({ + boardId, + hasChangeAccess, + itemId, + saveItemOptions, + setOptions, +}: UseTableLayoutPersistenceProps) => + useCallback( + (newOptions: TableLayoutOptions) => { + setOptions({ newOptions }); + if (hasChangeAccess && boardId && itemId) saveItemOptions({ boardId, itemId, newOptions }); + }, + [boardId, hasChangeAccess, itemId, saveItemOptions, setOptions], + ); + const toPixelWidth = (width: string | number): number | undefined => { if (typeof width === "number" && Number.isFinite(width) && width > 0) return width; if (typeof width !== "string" || !width.endsWith("px")) return undefined; @@ -147,11 +170,22 @@ export const usePersistedTableLayout = ({ const pendingLayout = useRef({}); const saveTimeout = useRef(undefined); + const onLayoutChangeRef = useRef(onLayoutChange); + useEffect(() => { + onLayoutChangeRef.current = onLayoutChange; + }, [onLayoutChange]); + const flushPendingLayout = useCallback(() => { + const layout = pendingLayout.current; + pendingLayout.current = {}; + saveTimeout.current = undefined; + if (Object.keys(layout).length > 0) onLayoutChangeRef.current(layout); + }, []); useEffect( () => () => { window.clearTimeout(saveTimeout.current); + flushPendingLayout(); }, - [], + [flushPendingLayout], ); useEffect(() => { if (!hydrated.current) return; @@ -179,11 +213,8 @@ export const usePersistedTableLayout = ({ pendingLayout.current = { ...pendingLayout.current, ...newLayout }; window.clearTimeout(saveTimeout.current); - saveTimeout.current = window.setTimeout(() => { - onLayoutChange(pendingLayout.current); - pendingLayout.current = {}; - }, layoutSaveDelay); - }, [columnAccessors, columnsOrder, columnsWidth, onLayoutChange, savedOrder, savedWidths, visibleAccessorSet]); + saveTimeout.current = window.setTimeout(flushPendingLayout, layoutSaveDelay); + }, [columnAccessors, columnsOrder, columnsWidth, flushPendingLayout, savedOrder, savedWidths, visibleAccessorSet]); return { effectiveColumns, storeKey }; }; diff --git a/packages/widgets/src/docker/component.tsx b/packages/widgets/src/docker/component.tsx index 97cac81a3..576a80d9d 100644 --- a/packages/widgets/src/docker/component.tsx +++ b/packages/widgets/src/docker/component.tsx @@ -30,7 +30,7 @@ import { useScopedI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; import { HomarrDataTable } from "../common/homarr-data-table"; -import { usePersistedTableLayout } from "../common/use-persisted-table-layout"; +import { usePersistedTableLayout, useTableLayoutPersistence } from "../common/use-persisted-table-layout"; type DockerContainer = RouterOutputs["docker"]["getContainers"]["containers"][number]; type ContainerAction = "start" | "stop" | "restart" | "remove"; @@ -242,13 +242,13 @@ export default function DockerWidget({ message: tWidget("error.layoutSaveMessage"), }), }); - const persistLayout = useCallback( - (newOptions: Partial>) => { - setOptions({ newOptions }); - if (hasChangeAccess && boardId && itemId) saveItemOptions({ boardId, itemId, newOptions }); - }, - [boardId, hasChangeAccess, itemId, saveItemOptions, setOptions], - ); + const persistLayout = useTableLayoutPersistence({ + boardId, + hasChangeAccess, + itemId, + saveItemOptions, + setOptions, + }); const defaultDirection = options.descendingDefaultSort ? "desc" : "asc"; const [sortStatus, setSortStatus] = useState>({ diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index 7b38a5a8d..939293ed5 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -54,7 +54,7 @@ import { useScopedI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; import { HomarrDataTable } from "../common/homarr-data-table"; -import { usePersistedTableLayout } from "../common/use-persisted-table-layout"; +import { usePersistedTableLayout, useTableLayoutPersistence } from "../common/use-persisted-table-layout"; dayjs.extend(relativeTime); @@ -338,13 +338,13 @@ export default function DownloadClientsWidget({ const { mutate: saveItemOptions } = clientApi.widget.options.saveItemOptions.useMutation({ onError: () => showErrorNotification({ title: t("errors.actionFailed"), message: t("errors.actionFailedMessage") }), }); - const persistOption = useCallback( - (newOptions: Partial>) => { - setOptions({ newOptions }); - if (hasChangeAccess && boardId && itemId) saveItemOptions({ boardId, itemId, newOptions }); - }, - [setOptions, hasChangeAccess, boardId, itemId, saveItemOptions], - ); + const persistOption = useTableLayoutPersistence({ + boardId, + hasChangeAccess, + itemId, + saveItemOptions, + setOptions, + }); let defaultSortDirection: DataTableSortStatus["direction"] = "asc"; if (options.descendingDefaultSort) defaultSortDirection = "desc";