From 9888941817254241c501201932f17fbbef97260a Mon Sep 17 00:00:00 2001 From: xxxliqu1dxxx Date: Thu, 3 Sep 2026 14:40:13 -0400 Subject: [PATCH 1/4] feat(ui): make the Name column sortable in the directory listing --- src/components/DirectoryItems.jsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/DirectoryItems.jsx b/src/components/DirectoryItems.jsx index 02d39557..82f65e61 100644 --- a/src/components/DirectoryItems.jsx +++ b/src/components/DirectoryItems.jsx @@ -1,7 +1,7 @@ import React from "react"; import { Link } from "react-router"; import KopiaTable from "./KopiaTable"; -import { objectLink, rfc3339TimestampForDisplay } from "../utils/formatutils"; +import { compare, objectLink, rfc3339TimestampForDisplay } from "../utils/formatutils"; import { sizeWithFailures } from "../utils/uiutil"; import { UIPreferencesContext } from "../contexts/UIPreferencesContext"; import PropTypes from "prop-types"; @@ -47,6 +47,24 @@ export function DirectoryItems({ historyState, items }) { id: "name", header: "Name", width: "", + accessorFn: (x) => x.name, + sortDescFirst: true, // first click sorts descending, like the other columns + sortingFn: (rowA, rowB, columnId) => { + const aIsDir = rowA.original.type === "d"; + const bIsDir = rowB.original.type === "d"; + if (aIsDir !== bIsDir) { + return aIsDir ? -1 : 1; // directories always first, in both directions + } + + const aName = rowA.getValue(columnId); + const bName = rowB.getValue(columnId); + const v = compare(aName.toLowerCase(), bName.toLowerCase()); + if (v !== 0) { + return v; + } + + return compare(aName, bName); // exact-compare fallback for case-insensitive ties + }, cell: (x) => directoryLinkOrDownload(x.row.original, historyState), }, { From 1755519d5660f64c547e8b10fe34f61918e97ad1 Mon Sep 17 00:00:00 2001 From: xxxliqu1dxxx Date: Thu, 3 Sep 2026 16:42:15 -0400 Subject: [PATCH 2/4] feat(ui): persist user-applied table sorting across navigation --- src/App.jsx | 7 +++-- src/components/DirectoryItems.jsx | 2 +- src/components/KopiaTable.jsx | 6 ++-- src/contexts/TableSortContext.jsx | 48 +++++++++++++++++++++++++++++++ src/pages/Policies.jsx | 2 +- src/pages/SnapshotHistory.jsx | 2 +- src/pages/Snapshots.jsx | 2 +- src/pages/Tasks.jsx | 2 +- 8 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 src/contexts/TableSortContext.jsx diff --git a/src/App.jsx b/src/App.jsx index b717560b..80919534 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,6 +18,7 @@ import { SnapshotHistory } from "./pages/SnapshotHistory"; import { SnapshotRestore } from "./pages/SnapshotRestore"; import { AppContext } from "./contexts/AppContext"; import { UIPreferenceProvider } from "./contexts/UIPreferencesContext"; +import { TableSortProvider } from "./contexts/TableSortContext"; export default class App extends Component { constructor() { @@ -175,7 +176,8 @@ export default class App extends Component {
{this.state.repoDescription}
- + + } /> } /> } /> @@ -188,7 +190,8 @@ export default class App extends Component { } /> } /> } /> - + + diff --git a/src/components/DirectoryItems.jsx b/src/components/DirectoryItems.jsx index 82f65e61..5b8a90e9 100644 --- a/src/components/DirectoryItems.jsx +++ b/src/components/DirectoryItems.jsx @@ -95,7 +95,7 @@ export function DirectoryItems({ historyState, items }) { }, ]; - return ; + return ; } DirectoryItems.propTypes = { diff --git a/src/components/KopiaTable.jsx b/src/components/KopiaTable.jsx index 6b3906d7..1db9181c 100644 --- a/src/components/KopiaTable.jsx +++ b/src/components/KopiaTable.jsx @@ -10,6 +10,7 @@ import { flexRender, } from "@tanstack/react-table"; import { PAGE_SIZES, UIPreferencesContext } from "../contexts/UIPreferencesContext"; +import { useTableSort } from "../contexts/TableSortContext"; import PropTypes from "prop-types"; function paginationItems(count, active, gotoPage) { @@ -55,9 +56,9 @@ function paginationItems(count, active, gotoPage) { return items; } -export default function KopiaTable({ columns, data }) { +export default function KopiaTable({ columns, data, tableKey }) { const { pageSize, setPageSize } = use(UIPreferencesContext); - const [sorting, setSorting] = useState([]); + const [sorting, setSorting] = useTableSort(tableKey); const [pagination, setPagination] = useState({ pageIndex: 0, //default page index pageSize: pageSize, //default page size @@ -173,4 +174,5 @@ export default function KopiaTable({ columns, data }) { KopiaTable.propTypes = { columns: PropTypes.array.isRequired, data: PropTypes.array.isRequired, + tableKey: PropTypes.string, }; diff --git a/src/contexts/TableSortContext.jsx b/src/contexts/TableSortContext.jsx new file mode 100644 index 00000000..31f8a36a --- /dev/null +++ b/src/contexts/TableSortContext.jsx @@ -0,0 +1,48 @@ +import React, { use, useState, useCallback } from "react"; +import PropTypes from "prop-types"; + +// Table sort state, persisted across component (re)mounts, keyed by table +// identity. In-memory only: it does not survive a browser refresh. +const TableSortContext = React.createContext(null); + +export function TableSortProvider({ children }) { + const [sorts, setSorts] = useState({}); + + const getSort = useCallback((key) => sorts[key] ?? [], [sorts]); + // TanStack v8 onSortingChange may pass either the new SortingState or an + // updater function (state) => SortingState; resolve both to a plain array. + const setSort = useCallback((key, update) => { + setSorts((prev) => { + const current = prev[key] ?? []; + const next = typeof update === "function" ? update(current) : update; + return { ...prev, [key]: next }; + }); + }, []); + + return ( + + {children} + + ); +} + +TableSortProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +// Returns [sorting, setSorting] compatible with TanStack's onSortingChange. +// When tableKey is provided, the state is shared across (re)mounts via the +// provider; otherwise it falls back to component-local state (behavior +// unchanged for call sites without a key). +export function useTableSort(tableKey) { + const ctx = use(TableSortContext); + const [localSorting, setLocalSorting] = useState([]); + + if (!ctx || !tableKey) { + return [localSorting, setLocalSorting]; + } + + const sorting = ctx.getSort(tableKey); + const setSorting = (s) => ctx.setSort(tableKey, s); + return [sorting, setSorting]; +} diff --git a/src/pages/Policies.jsx b/src/pages/Policies.jsx index 54605f86..3362cb6b 100644 --- a/src/pages/Policies.jsx +++ b/src/pages/Policies.jsx @@ -352,7 +352,7 @@ export class PoliciesInternal extends Component { {policies.length > 0 ? (

Found {policies.length} policies matching criteria.

- +
) : this.state.selectedOwner === localPolicies && this.state.policyPath ? (

diff --git a/src/pages/SnapshotHistory.jsx b/src/pages/SnapshotHistory.jsx index b499831b..89cb25f5 100644 --- a/src/pages/SnapshotHistory.jsx +++ b/src/pages/SnapshotHistory.jsx @@ -528,7 +528,7 @@ class SnapshotHistoryInternal extends Component { )} - + diff --git a/src/pages/Snapshots.jsx b/src/pages/Snapshots.jsx index 9eaf6fd7..ab8776b2 100644 --- a/src/pages/Snapshots.jsx +++ b/src/pages/Snapshots.jsx @@ -415,7 +415,7 @@ export class Snapshots extends Component { - + ); diff --git a/src/pages/Tasks.jsx b/src/pages/Tasks.jsx index 70dcc10d..3cd81b11 100644 --- a/src/pages/Tasks.jsx +++ b/src/pages/Tasks.jsx @@ -187,7 +187,7 @@ export class Tasks extends Component { snapshots, restore, run maintenance, etc. ) : ( - + )} From dd2ae976afcf3c3539c8117090cec1978baa4c65 Mon Sep 17 00:00:00 2001 From: xxxliqu1dxxx Date: Thu, 3 Sep 2026 20:04:18 -0400 Subject: [PATCH 3/4] feat(ui): make the Start time column sortable in the snapshot history --- src/pages/SnapshotHistory.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/SnapshotHistory.jsx b/src/pages/SnapshotHistory.jsx index 89cb25f5..78cba0ba 100644 --- a/src/pages/SnapshotHistory.jsx +++ b/src/pages/SnapshotHistory.jsx @@ -378,6 +378,7 @@ class SnapshotHistoryInternal extends Component { id: "startTime", header: "Start time", width: 200, + accessorFn: (x) => x.startTime, // RFC3339 strings compare correctly as strings cell: (x) => { let timestamp = rfc3339TimestampForDisplay(x.row.original.startTime); return ( From c1401be6c924f96a834af1a7030de182379c0ce2 Mon Sep 17 00:00:00 2001 From: xxxliqu1dxxx Date: Tue, 29 Sep 2026 08:54:40 -0400 Subject: [PATCH 4/4] style: format App.jsx and TableSortContext.jsx to pass prettier check --- src/App.jsx | 24 ++++++++++++------------ src/contexts/TableSortContext.jsx | 6 +----- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 80919534..af518eb8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -178,18 +178,18 @@ export default class App extends Component { - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> diff --git a/src/contexts/TableSortContext.jsx b/src/contexts/TableSortContext.jsx index 31f8a36a..259b0295 100644 --- a/src/contexts/TableSortContext.jsx +++ b/src/contexts/TableSortContext.jsx @@ -19,11 +19,7 @@ export function TableSortProvider({ children }) { }); }, []); - return ( - - {children} - - ); + return {children}; } TableSortProvider.propTypes = {