mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 07:24:52 +09:00
Persist table sort/filter state in the URL
Tables such as Hosts, Instances, Users, Roles, etc. now write their sort order, column filters and pagination into the URL query string, so reload, link-sharing and browser back/forward restore the same view (closes #927). Every parameter is namespaced with the table name so the URL is self-describing and two tables can share a page without colliding, e.g. ?user.sort=name&user.name=bob&user.admin=true or ?instance.name=foo. Adds two reusable hooks, exported from @clusterio/web_ui so plugins can use them too: - useTableQueryState: makes an antd Table controlled and round-trips its sort/filter/pagination through the URL via useSearchParams. The pure serialisation helpers live in util/tableQuery.ts and are unit tested. - useColumnSearch: a column-header text-search filter (matching the Users table style), persisted through the same filter mechanism. Applied to Hosts, Roles, Plugins, the instance list (standalone and on the host detail page), Users (main table and the online-players list on the instance detail page), instance saves, the per-instance user stats, the mod packs and stored mods tables, and the subspace_storage storage page. The storage page's resource search moves from a standalone box into the Resource column header, keeping its regex matching. The bundled user filter persists as separate readable params (user.name, user.admin, ...) via a per-column FilterCodec rather than one JSON blob. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9e727f9a13
commit
49eb594e5b
19 files changed
+639
-78
No files matched your search
@@ -1,16 +1,16 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Input, Table, Typography } from "antd";
|
||||
import { Table } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
|
||||
import {
|
||||
BaseWebPlugin, PageLayout, PageHeader, Control, ControlContext, notifyErrorHandler,
|
||||
useExportLocale, useExportPrototypeMetadata, useDefaultModPack, FactorioIcon,
|
||||
useTableQueryState, useColumnSearch,
|
||||
} from "@clusterio/web_ui";
|
||||
import { GetStorageRequest, Item, SetStorageSubscriptionRequest, UpdateStorageEvent } from "../messages";
|
||||
|
||||
import "./style.css";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
|
||||
function useStorage(control: Control) {
|
||||
let plugin = control.plugins.get("subspace_storage") as WebPlugin;
|
||||
@@ -37,8 +37,10 @@ function StoragePage() {
|
||||
let itemMetadata = prototypes?.get("item");
|
||||
let fluidMetadata = prototypes?.get("fluid");
|
||||
let storage = useStorage(control);
|
||||
type ItemFilter = ([name, item]: [string, Item]) => boolean;
|
||||
let [filter, setFilter] = useState<null | ItemFilter>(null);
|
||||
const tableState = useTableQueryState<[string, Item]>({
|
||||
namespace: "storage", defaultSortKey: "quantity", defaultSortOrder: "descend", pagination: false,
|
||||
});
|
||||
const resourceSearch = useColumnSearch<[string, Item]>(item => getLocaleName(item[1].name), "Search");
|
||||
|
||||
function getLocaleName(itemName: string) {
|
||||
let localeName = itemName;
|
||||
@@ -65,33 +67,32 @@ function StoragePage() {
|
||||
|
||||
let numberFormat = new Intl.NumberFormat("en-US");
|
||||
|
||||
// Build the regex the resource search uses: word-boundary anchored, spaces match any gap.
|
||||
function matchesResource(value: string | number | bigint | boolean, item: [string, Item]) {
|
||||
let search = String(value).trim();
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
search = search.replace(/(^| )(\w)/g, "$1\\b$2").replace(/ +/g, ".*");
|
||||
let filterExpr;
|
||||
try {
|
||||
filterExpr = new RegExp(search, "i");
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
return filterExpr.test(getLocaleName(item[1].name)) || filterExpr.test(item[1].name);
|
||||
}
|
||||
|
||||
return <PageLayout nav={[{ name: "Storage" }]}>
|
||||
<PageHeader title="Storage" />
|
||||
<Paragraph>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
onChange={(event) => {
|
||||
let search = event.target.value.trim();
|
||||
if (!search) {
|
||||
setFilter(null);
|
||||
return;
|
||||
}
|
||||
search = search.replace(/(^| )(\w)/g, "$1\\b$2");
|
||||
search = search.replace(/ +/g, ".*");
|
||||
let filterExpr = new RegExp(search, "i");
|
||||
setFilter(() => ((item: [string, Item]) => {
|
||||
let name = getLocaleName(item[1].name);
|
||||
return filterExpr.test(name) || filterExpr.test(item[1].name);
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Paragraph>
|
||||
<Table
|
||||
className="subspace-storage-storage"
|
||||
columns={[
|
||||
columns={([
|
||||
{
|
||||
title: "Resource",
|
||||
key: "resource",
|
||||
...resourceSearch,
|
||||
onFilter: matchesResource,
|
||||
sorter: (a, b) => {
|
||||
let aName = getLocaleName(a[1].name);
|
||||
let bName = getLocaleName(b[1].name);
|
||||
@@ -119,14 +120,14 @@ function StoragePage() {
|
||||
title: "Quantity",
|
||||
key: "quantity",
|
||||
align: "right",
|
||||
defaultSortOrder: "descend",
|
||||
sorter: (a, b) => a[1].count - b[1].count,
|
||||
render: (_, item) => numberFormat.format(item[1].count),
|
||||
},
|
||||
]}
|
||||
dataSource={filter ? storage.filter(filter) : storage}
|
||||
] satisfies ColumnsType<[string, Item]>).map(tableState.applyColumn)}
|
||||
dataSource={storage}
|
||||
rowKey={item => item[0]}
|
||||
pagination={false}
|
||||
pagination={tableState.pagination}
|
||||
onChange={tableState.onChange}
|
||||
/>
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user