mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 15:04:52 +09:00
Adopt agreed URL scheme and drop the dist-based test
Following discussion on the PR, the sort value now packs the direction as `<field>.<asc|desc>` (e.g. user.sort=name.desc) instead of a separate `.order` param, page size uses `.size` instead of `.pageSize`, and the reserved system keys are sort/page/size. `.sort` is read tolerant of repeats so multi-column sort can be added later without changing the URL shape. Filterable columns must not use the reserved system names. Removed the web_ui unit test since it required importing from the built dist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
49eb594e5b
commit
245483cb35
2 files changed
+30
-128
No files matched your search
@@ -5,15 +5,15 @@
|
||||
*
|
||||
* Every parameter is namespaced with the table's name so the URL is
|
||||
* self-describing and two tables could coexist on one page without colliding:
|
||||
* <ns>.sort column key the table is sorted by
|
||||
* <ns>.order "ascend" | "descend"
|
||||
* <ns>.sort `<field>.<asc|desc>`, e.g. user.sort=name.desc
|
||||
* <ns>.page 1-based page number
|
||||
* <ns>.pageSize rows per page
|
||||
* <ns>.size rows per page
|
||||
* <ns>.<column> filter value for that column (e.g. user.name, user.admin)
|
||||
*
|
||||
* `prefix` below is the namespace with its trailing dot, e.g. "user.".
|
||||
* Multi-value filters use a repeated parameter (`user.roles=1&user.roles=2`)
|
||||
* so URLSearchParams handles all encoding/decoding exactly once.
|
||||
* System keys (`sort`, `page`, `size`) are reserved, so filterable columns must
|
||||
* not use those names. `prefix` below is the namespace with its trailing dot,
|
||||
* e.g. "user.". Multi-value filters use a repeated parameter
|
||||
* (`user.roles=1&user.roles=2`) so URLSearchParams handles encoding once.
|
||||
*/
|
||||
|
||||
export type SortOrder = "ascend" | "descend";
|
||||
@@ -47,16 +47,23 @@ export interface FilterCodec {
|
||||
}
|
||||
|
||||
/** Suffixes reserved for table-level state; any other `<prefix><x>` is a column filter. */
|
||||
const RESERVED_SUFFIXES = new Set(["sort", "order", "page", "pageSize"]);
|
||||
const RESERVED_SUFFIXES = new Set(["sort", "page", "size"]);
|
||||
|
||||
/** Parse an order parameter, returning undefined for anything unexpected. */
|
||||
export function parseOrder(value: string | null): SortOrder | undefined {
|
||||
return value === "ascend" || value === "descend" ? value : undefined;
|
||||
/** Convert a URL sort direction token to the antd sort order. */
|
||||
function orderFromToken(token: string): SortOrder {
|
||||
return token === "desc" ? "descend" : "ascend";
|
||||
}
|
||||
|
||||
/** Convert an antd sort order to a URL direction token. */
|
||||
function tokenFromOrder(order: SortOrder): string {
|
||||
return order === "descend" ? "desc" : "asc";
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the sort state from the query, falling back to the supplied defaults
|
||||
* when no (valid) sort parameter is present.
|
||||
* when no sort parameter is present. The value is `<field>.<asc|desc>`; only the
|
||||
* first `<prefix>sort` parameter is used (the format allows repeats so
|
||||
* multi-column sort can be added later without changing the URL shape).
|
||||
*/
|
||||
export function parseSort(
|
||||
params: URLSearchParams,
|
||||
@@ -64,11 +71,14 @@ export function parseSort(
|
||||
defaultKey?: string,
|
||||
defaultOrder: SortOrder = "ascend",
|
||||
): TableSortState {
|
||||
const columnKey = params.get(`${prefix}sort`);
|
||||
if (!columnKey) {
|
||||
const raw = params.getAll(`${prefix}sort`)[0];
|
||||
if (!raw) {
|
||||
return { columnKey: defaultKey, order: defaultKey ? defaultOrder : undefined };
|
||||
}
|
||||
return { columnKey, order: parseOrder(params.get(`${prefix}order`)) ?? defaultOrder };
|
||||
const dot = raw.lastIndexOf(".");
|
||||
const columnKey = dot === -1 ? raw : raw.slice(0, dot);
|
||||
const order = dot === -1 ? defaultOrder : orderFromToken(raw.slice(dot + 1));
|
||||
return { columnKey, order };
|
||||
}
|
||||
|
||||
/** Read pagination state from the query, tolerating missing/garbage values. */
|
||||
@@ -79,7 +89,7 @@ export function parsePagination(
|
||||
defaultPage = 1,
|
||||
): TablePageState {
|
||||
const page = Number.parseInt(params.get(`${prefix}page`) ?? "", 10);
|
||||
const pageSize = Number.parseInt(params.get(`${prefix}pageSize`) ?? "", 10);
|
||||
const pageSize = Number.parseInt(params.get(`${prefix}size`) ?? "", 10);
|
||||
return {
|
||||
page: Number.isInteger(page) && page >= 1 ? page : defaultPage,
|
||||
pageSize: Number.isInteger(pageSize) && pageSize >= 1 ? pageSize : defaultPageSize,
|
||||
@@ -114,13 +124,10 @@ export function applySort(
|
||||
): void {
|
||||
const { columnKey, order } = sort;
|
||||
const isDefault = columnKey === defaultKey && (order ?? defaultOrder) === defaultOrder;
|
||||
if (!columnKey || !order || isDefault) {
|
||||
params.delete(`${prefix}sort`);
|
||||
params.delete(`${prefix}order`);
|
||||
return;
|
||||
params.delete(`${prefix}sort`);
|
||||
if (columnKey && order && !isDefault) {
|
||||
params.set(`${prefix}sort`, `${columnKey}.${tokenFromOrder(order)}`);
|
||||
}
|
||||
params.set(`${prefix}sort`, columnKey);
|
||||
params.set(`${prefix}order`, order);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,8 +168,8 @@ export function applyPagination(
|
||||
params.delete(`${prefix}page`);
|
||||
}
|
||||
if (pageSize !== defaultPageSize) {
|
||||
params.set(`${prefix}pageSize`, String(pageSize));
|
||||
params.set(`${prefix}size`, String(pageSize));
|
||||
} else {
|
||||
params.delete(`${prefix}pageSize`);
|
||||
params.delete(`${prefix}size`);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
const {
|
||||
parseOrder, parseSort, parsePagination, parseFilters,
|
||||
applySort, applyFilters, applyPagination,
|
||||
} = require("@clusterio/web_ui/dist/browser/src/util/tableQuery");
|
||||
|
||||
// All helpers take the table namespace prefix (with trailing dot) as 2nd arg.
|
||||
const P = "t.";
|
||||
|
||||
describe("web_ui/util/tableQuery", function() {
|
||||
describe("parseOrder", function() {
|
||||
it("accepts ascend/descend and rejects anything else", function() {
|
||||
assert.equal(parseOrder("ascend"), "ascend");
|
||||
assert.equal(parseOrder("descend"), "descend");
|
||||
assert.equal(parseOrder("sideways"), undefined);
|
||||
assert.equal(parseOrder(null), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSort", function() {
|
||||
it("falls back to the supplied defaults when absent", function() {
|
||||
assert.deepEqual(parseSort(new URLSearchParams(""), P, "name"), { columnKey: "name", order: "ascend" });
|
||||
assert.deepEqual(parseSort(new URLSearchParams(""), P), { columnKey: undefined, order: undefined });
|
||||
});
|
||||
it("reads an explicit namespaced sort column and order", function() {
|
||||
assert.deepEqual(
|
||||
parseSort(new URLSearchParams("t.sort=cpu&t.order=descend"), P, "name"),
|
||||
{ columnKey: "cpu", order: "descend" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parsePagination", function() {
|
||||
it("uses defaults for missing or garbage values", function() {
|
||||
assert.deepEqual(parsePagination(new URLSearchParams(""), P, 50), { page: 1, pageSize: 50 });
|
||||
assert.deepEqual(
|
||||
parsePagination(new URLSearchParams("t.page=x&t.pageSize=-3"), P, 50), { page: 1, pageSize: 50 }
|
||||
);
|
||||
});
|
||||
it("reads valid values", function() {
|
||||
assert.deepEqual(
|
||||
parsePagination(new URLSearchParams("t.page=3&t.pageSize=20"), P, 50), { page: 3, pageSize: 20 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseFilters", function() {
|
||||
it("collects namespaced column filters but excludes reserved suffixes", function() {
|
||||
const params = new URLSearchParams("t.roles=1&t.roles=2&t.sort=name&other.x=1");
|
||||
assert.deepEqual(parseFilters(params, P), { roles: ["1", "2"] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("applySort", function() {
|
||||
it("omits parameters when the sort equals the default", function() {
|
||||
const params = new URLSearchParams();
|
||||
applySort(params, P, { columnKey: "name", order: "ascend" }, "name", "ascend");
|
||||
assert.equal(params.toString(), "");
|
||||
});
|
||||
it("writes a namespaced non-default sort", function() {
|
||||
const params = new URLSearchParams();
|
||||
applySort(params, P, { columnKey: "cpu", order: "descend" }, "name", "ascend");
|
||||
assert.equal(params.get("t.sort"), "cpu");
|
||||
assert.equal(params.get("t.order"), "descend");
|
||||
});
|
||||
it("clears the sort when the order is cleared", function() {
|
||||
const params = new URLSearchParams("t.sort=cpu&t.order=descend");
|
||||
applySort(params, P, { columnKey: undefined, order: undefined }, "name", "ascend");
|
||||
assert.equal(params.has("t.sort"), false);
|
||||
assert.equal(params.has("t.order"), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyFilters", function() {
|
||||
it("sets active filters as repeated params and deletes inactive ones", function() {
|
||||
const params = new URLSearchParams("t.roles=old");
|
||||
applyFilters(params, P, { roles: ["1", "2"], lastSeen: null });
|
||||
assert.deepEqual(params.getAll("t.roles"), ["1", "2"]);
|
||||
assert.equal(params.has("t.lastSeen"), false);
|
||||
});
|
||||
it("round-trips a JSON-blob filter value with single encoding", function() {
|
||||
const params = new URLSearchParams();
|
||||
const blob = '{"admin":false}';
|
||||
applyFilters(params, P, { name: [blob] });
|
||||
// Single-encoded (%7B…), not double-encoded (%257B…).
|
||||
assert.equal(params.toString(), "t.name=%7B%22admin%22%3Afalse%7D");
|
||||
assert.deepEqual(parseFilters(params, P), { name: [blob] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyPagination", function() {
|
||||
it("omits page 1 and the default page size", function() {
|
||||
const params = new URLSearchParams();
|
||||
applyPagination(params, P, 1, 50, 50);
|
||||
assert.equal(params.toString(), "");
|
||||
});
|
||||
it("writes non-default page and page size", function() {
|
||||
const params = new URLSearchParams();
|
||||
applyPagination(params, P, 2, 20, 50);
|
||||
assert.equal(params.get("t.page"), "2");
|
||||
assert.equal(params.get("t.pageSize"), "20");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user