diff --git a/packages/old-import/src/widgets/options.ts b/packages/old-import/src/widgets/options.ts index 8da3bb8a2..f06bc1499 100644 --- a/packages/old-import/src/widgets/options.ts +++ b/packages/old-import/src/widgets/options.ts @@ -82,6 +82,8 @@ const optionMapping: OptionMapping = { showCompletedUsenet: () => true, showCompletedHttp: () => true, limitPerIntegration: () => undefined, + columnOrder: () => undefined, + columnWidths: () => undefined, }, weather: { forecastDayCount: (oldOptions) => oldOptions.forecastDays, diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index 740e3c85f..d589e3eae 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -3,7 +3,7 @@ import "../widgets-common.css"; import "./styles.css"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActionIcon, Avatar, @@ -41,7 +41,7 @@ import { import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import type { DataTableColumn, DataTableSortStatus } from "mantine-datatable"; -import { DataTable } from "mantine-datatable"; +import { DataTable, useDataTableColumns } from "mantine-datatable"; import { clientApi } from "@homarr/api/client"; import { useIntegrationsWithInteractAccess } from "@homarr/auth/client"; @@ -116,6 +116,9 @@ export default function DownloadClientsWidget({ isEditMode, integrationIds, options, + setOptions, + boardId, + itemId, width, }: WidgetComponentProps<"downloads">) { const allInteractAccess = useIntegrationsWithInteractAccess(); @@ -147,6 +150,24 @@ export default function DownloadClientsWidget({ const { mutate: mutateResumeQueue } = clientApi.widget.downloads.resume.useMutation(mutationOptions); const { mutate: mutatePauseQueue } = clientApi.widget.downloads.pause.useMutation(mutationOptions); + const { mutate: saveItemOptions } = clientApi.widget.options.saveItemOptions.useMutation(); + const persistOption = useCallback( + (newOptions: Partial>) => { + setOptions({ newOptions }); + if (boardId && itemId) saveItemOptions({ boardId, itemId, newOptions }); + }, + [setOptions, boardId, itemId, saveItemOptions], + ); + + const savedOrder = useMemo( + () => (options.columnOrder ? JSON.parse(options.columnOrder) as string[] : []), + [options.columnOrder], + ); + const savedWidths = useMemo>( + () => (options.columnWidths ? JSON.parse(options.columnWidths) as Record : {}), + [options.columnWidths], + ); + const [sortStatus, setSortStatus] = useState>({ columnAccessor: options.defaultSort, direction: options.descendingDefaultSort ? "desc" : "asc", @@ -448,6 +469,51 @@ export default function DownloadClientsWidget({ return cols.filter(Boolean) as DataTableColumn[]; }, [shouldIncludeColumn, t, size, width]); + const storeKey = `downloads-${options.columns.join(",")}`; + const { + effectiveColumns, + columnsOrder, + columnsWidth, + } = useDataTableColumns({ + key: storeKey, + columns, + }); + + const initialSyncDone = useRef(false); + useEffect(() => { + if (initialSyncDone.current) return; + initialSyncDone.current = true; + if (savedOrder.length > 0) { + const stored = localStorage.getItem(`mantine-datatable-columns-order-${storeKey}`); + if (!stored) localStorage.setItem(`mantine-datatable-columns-order-${storeKey}`, JSON.stringify(savedOrder)); + } + if (Object.keys(savedWidths).length > 0) { + const stored = localStorage.getItem(`mantine-datatable-columns-width-${storeKey}`); + if (!stored) localStorage.setItem(`mantine-datatable-columns-width-${storeKey}`, JSON.stringify( + Object.entries(savedWidths).map(([accessor, w]) => ({ accessor, width: w })), + )); + } + }, [storeKey, savedOrder, savedWidths]); + + const prevOrder = useRef(columnsOrder); + const prevWidths = useRef(columnsWidth); + useEffect(() => { + const orderChanged = JSON.stringify(columnsOrder) !== JSON.stringify(prevOrder.current); + const widthsChanged = JSON.stringify(columnsWidth) !== JSON.stringify(prevWidths.current); + prevOrder.current = columnsOrder; + prevWidths.current = columnsWidth; + + if (!orderChanged && !widthsChanged) return; + if (orderChanged) persistOption({ columnOrder: JSON.stringify(columnsOrder) }); + if (widthsChanged) { + const widthMap: Record = {}; + for (const { accessor, width: w } of columnsWidth as { accessor: string; width: number }[]) { + if (typeof w === "number") widthMap[accessor] = w; + } + persistOption({ columnWidths: JSON.stringify(widthMap) }); + } + }, [columnsOrder, columnsWidth, persistOption]); + const handleContextMenu = useCallback( ({ record, event }: { record: ExtendedDownloadClientItem; event: React.MouseEvent }) => { event.preventDefault(); @@ -529,7 +595,8 @@ export default function DownloadClientsWidget({ loaderBackgroundBlur={2} fz={size.fontSize} records={sortedData} - columns={columns} + columns={effectiveColumns} + storeColumnsKey={storeKey} textSelectionDisabled sortStatus={sortStatus} onSortStatusChange={setSortStatus} @@ -539,6 +606,8 @@ export default function DownloadClientsWidget({ className="downloads-table" defaultColumnProps={{ noWrap: true, + draggable: true, + resizable: true, cellsStyle: () => ({ padding: `${size.cellPadding}px 8px` }), }} scrollAreaProps={{ type: "auto", scrollbarSize: 6 }} diff --git a/packages/widgets/src/downloads/index.ts b/packages/widgets/src/downloads/index.ts index 365cf858d..cc8f9a5b0 100644 --- a/packages/widgets/src/downloads/index.ts +++ b/packages/widgets/src/downloads/index.ts @@ -93,8 +93,12 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads validate: z.number().min(1), withDescription: true, }), + columnOrder: factory.text({ defaultValue: "" }), + columnWidths: factory.text({ defaultValue: "" }), }), { + columnOrder: { shouldHide: () => true }, + columnWidths: { shouldHide: () => true }, showCompletedUsenet: { shouldHide: (_, integrationKinds) => !getIntegrationKindsByCategory("usenet").some((kinds) => integrationKinds.includes(kinds)),