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";