Release govoplan-search v0.1.20: centralize filtering and refresh behavior
Module Package Release / publish-packages (push) Successful in 11s
Module Package Release / publish-packages (push) Successful in 11s
This commit is contained in:
@@ -5,13 +5,21 @@ const page = fs.readFileSync("src/features/search/SearchPage.tsx", "utf8");
|
||||
const overlay = fs.readFileSync("src/components/GlobalSearch.tsx", "utf8");
|
||||
const admin = fs.readFileSync("src/features/search/SearchAdminPanel.tsx", "utf8");
|
||||
const styles = fs.readFileSync("src/styles/search.css", "utf8");
|
||||
const filters = fs.readFileSync("src/components/SearchFilters.tsx", "utf8");
|
||||
const lifecycle = fs.readFileSync("src/components/useSearchResults.ts", "utf8");
|
||||
|
||||
for (const source of [page, overlay]) {
|
||||
assert.ok(source.includes("DocumentationHelpLink"), "Search surfaces expose configured-system help");
|
||||
assert.ok(source.includes("DismissibleAlert"), "Search failures use the shared alert contract");
|
||||
assert.ok(!source.includes("window.alert("), "Search must not use browser alerts");
|
||||
assert.ok(!/<(div|span|li|tr)\b[^>]*\bonClick\s*=/.test(source), "Search uses semantic interactive elements");
|
||||
assert.ok(source.includes("<SearchFilters"), "Search route and overlay share one filter composition");
|
||||
assert.ok(source.includes("useSearchResults"), "Search route and overlay share one request lifecycle");
|
||||
assert.ok(!source.includes("search-filter-popover"), "Search surfaces must not duplicate dropdown implementations");
|
||||
}
|
||||
assert.ok(filters.includes("MultiSelectFilter"), "Search facets reuse Core's multi-selection dropdown");
|
||||
assert.ok(lifecycle.includes("currentIdentity.current === identity"), "Late responses are gated by the current request identity");
|
||||
assert.ok(lifecycle.includes("controllerRef.current?.abort()"), "Cursor and initial reads are cancelled together");
|
||||
|
||||
assert.ok(page.includes("PageScrollViewport"), "The full search route owns bounded result scrolling");
|
||||
assert.ok(overlay.includes("<Dialog"), "The title-bar search uses the shared focus-contained dialog");
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import test from "node:test";
|
||||
|
||||
const requireCore = createRequire(new URL("../../../govoplan-core/webui/package.json", import.meta.url));
|
||||
const ts = requireCore("typescript");
|
||||
|
||||
function loadTypeScript(relativePath, dependencies = {}) {
|
||||
const source = readFileSync(new URL(relativePath, import.meta.url), "utf8");
|
||||
const { outputText } = ts.transpileModule(source, { compilerOptions: {
|
||||
target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS,
|
||||
} });
|
||||
const module = { exports: {} };
|
||||
new Function("require", "module", "exports", outputText)((id) => {
|
||||
if (id in dependencies) return dependencies[id];
|
||||
throw new Error(`Unexpected runtime dependency: ${id}`);
|
||||
}, module, module.exports);
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
const { readSearchFilter, writeSearchFilter, effectiveSearchFilters, searchFilterOptions } = loadTypeScript("../src/components/searchFilters.ts");
|
||||
|
||||
test("legacy missing filters remain all; explicit none wins over repeated values", () => {
|
||||
assert.equal(readSearchFilter(new URLSearchParams("q=permit"), "module"), null);
|
||||
assert.deepEqual(readSearchFilter(new URLSearchParams("module=files&module=cases&module=files"), "module"), ["cases", "files"]);
|
||||
assert.deepEqual(readSearchFilter(new URLSearchParams("module=cases&module_none=1"), "module"), []);
|
||||
assert.deepEqual(readSearchFilter(new URLSearchParams("resource_type=case&resource_type_none=1"), "resource_type"), []);
|
||||
});
|
||||
|
||||
test("filter URL roundtrips preserve query, context, language and the other filter", () => {
|
||||
const original = new URLSearchParams("q=permit&context=cases.current&language=german&resource_type=case&other=keep");
|
||||
const selected = writeSearchFilter(original, "module", ["files", "cases", "files"]);
|
||||
assert.deepEqual(selected.getAll("module"), ["cases", "files"]);
|
||||
assert.equal(original.has("module"), false);
|
||||
const none = writeSearchFilter(selected, "module", []);
|
||||
assert.equal(none.get("module_none"), "1");
|
||||
assert.equal(none.has("module"), false);
|
||||
assert.deepEqual(readSearchFilter(none, "module"), []);
|
||||
const all = writeSearchFilter(none, "module", null);
|
||||
assert.equal(all.toString(), original.toString());
|
||||
assert.equal(readSearchFilter(all, "module"), null);
|
||||
});
|
||||
|
||||
test("global all, none and subset selections keep distinct effective requests", () => {
|
||||
assert.deepEqual(effectiveSearchFilters(null, null), { modules: undefined, resourceTypes: undefined, matchNone: false });
|
||||
assert.equal(effectiveSearchFilters([], null).matchNone, true);
|
||||
assert.equal(effectiveSearchFilters(null, []).matchNone, true);
|
||||
assert.deepEqual(effectiveSearchFilters(["cases", "files"], ["case", "file"]), {
|
||||
modules: ["cases", "files"], resourceTypes: ["case", "file"], matchNone: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("context bounds never broaden an empty resource-type intersection", () => {
|
||||
const context = { id: "files.context", moduleId: "files", label: "Files", pathPrefixes: ["/files"], resourceTypes: ["file", "folder"] };
|
||||
assert.deepEqual(effectiveSearchFilters(null, null, context), { modules: ["files"], resourceTypes: ["file", "folder"], matchNone: false });
|
||||
assert.deepEqual(effectiveSearchFilters(["cases"], ["case", "file"], context), { modules: ["files"], resourceTypes: ["file"], matchNone: false });
|
||||
assert.deepEqual(effectiveSearchFilters(null, ["case"], context), { modules: ["files"], resourceTypes: [], matchNone: true });
|
||||
assert.equal(effectiveSearchFilters(null, [], context).matchNone, true);
|
||||
// The hidden global module selection must not override the current context.
|
||||
assert.equal(effectiveSearchFilters([], null, context).matchNone, false);
|
||||
assert.equal(effectiveSearchFilters(null, null, { ...context, resourceTypes: [] }).matchNone, false);
|
||||
});
|
||||
|
||||
const catalogue = [
|
||||
{ provider_id: "files.source", module_id: "files", resource_type: "file", label: "Files", order: 1 },
|
||||
{ provider_id: "files.source", module_id: "files", resource_type: "folder", label: "Folders", order: 2 },
|
||||
{ provider_id: "cases.source", module_id: "cases", resource_type: "case", label: "Cases", order: 3 },
|
||||
];
|
||||
const observed = [{ module_id: "mail", resource_type: "message" }];
|
||||
const labels = new Map([["files", "Dateien"], ["cases", "Fälle"]]);
|
||||
|
||||
test("filter choices combine catalogue, observed values and removable stale selections", () => {
|
||||
const all = searchFilterOptions(catalogue, observed, labels, ["files", "retired-source"], ["unknown-type"]);
|
||||
assert.deepEqual(new Set(all.modules.map((item) => item.value)), new Set(["files", "cases", "mail", "retired-source"]));
|
||||
assert.equal(all.modules.find((item) => item.value === "files").label, "Dateien");
|
||||
assert.deepEqual(new Set(all.resourceTypes.map((item) => item.value)), new Set(["file", "folder", "unknown-type"]));
|
||||
assert.deepEqual(searchFilterOptions(catalogue, observed, labels, [], null).resourceTypes, []);
|
||||
const context = { id: "files.context", moduleId: "files", label: "Files", pathPrefixes: ["/files"], resourceTypes: ["file"] };
|
||||
assert.deepEqual(searchFilterOptions(catalogue, observed, labels, null, null, context).resourceTypes, [{ value: "file", label: "Files" }]);
|
||||
});
|
||||
|
||||
test("explicit none is a frontend-only empty response; legacy API empty arrays still request all", async () => {
|
||||
const paths = [], calls = [];
|
||||
const response = { query: "permit", results: [], diagnostics: [], next_cursor: null, has_more: false };
|
||||
const { search } = loadTypeScript("../src/api/search.ts", { "@govoplan/core-webui": {
|
||||
apiPath: (path, parameters) => { paths.push({ path, parameters }); return path; },
|
||||
apiFetch: async (...args) => { calls.push(args); return response; },
|
||||
} });
|
||||
const settings = { apiBaseUrl: "", apiKey: "", accessToken: "" };
|
||||
assert.deepEqual(await search(settings, { query: "permit", matchNone: true, modules: [] }), response);
|
||||
assert.equal(calls.length, 0);
|
||||
assert.equal(paths.length, 0);
|
||||
await search(settings, { query: "permit", modules: [], resourceTypes: [] });
|
||||
assert.equal(calls.length, 1);
|
||||
assert.deepEqual(paths[0].parameters.module, []);
|
||||
assert.deepEqual(paths[0].parameters.resource_type, []);
|
||||
assert.equal("matchNone" in paths[0].parameters, false);
|
||||
await search(settings, { query: "permit", modules: ["cases", "files"], resourceTypes: ["case", "file"] });
|
||||
assert.deepEqual(paths[1].parameters.module, ["cases", "files"]);
|
||||
assert.deepEqual(paths[1].parameters.resource_type, ["case", "file"]);
|
||||
});
|
||||
@@ -8,6 +8,8 @@ function assert(condition, message) {
|
||||
const source = readFileSync("src/components/GlobalSearch.tsx", "utf8");
|
||||
const layoutSource = readFileSync("src/components/searchOverlayLayout.ts", "utf8");
|
||||
const styles = readFileSync("src/styles/search.css", "utf8");
|
||||
const requestSource = readFileSync("src/components/useSearchResults.ts", "utf8");
|
||||
const filterSource = readFileSync("src/components/SearchFilters.tsx", "utf8");
|
||||
|
||||
assert(source.includes("titlebar-icon-link titlebar-search-button"), "Search uses the shared titlebar icon-button appearance");
|
||||
assert(source.includes("onClick={openOverlay}"), "clicking the titlebar Search command opens Search");
|
||||
@@ -15,7 +17,8 @@ assert(!source.includes("sourceInputRef"), "the titlebar no longer reserves a pe
|
||||
assert(source.includes("<Dialog"), "Search uses the shared Dialog component");
|
||||
assert(source.includes("portal"), "the Search dialog portals above the complete shell");
|
||||
assert(source.includes("calculateSearchOverlayLayout"), "the overlay position is derived from the titlebar command");
|
||||
assert(source.includes("listSearchProviders"), "the overlay loads the complete filter catalogue");
|
||||
assert(source.includes("useSearchCatalogue") && requestSource.includes("listSearchProviders"), "the overlay loads the complete filter catalogue through the shared Search hook");
|
||||
assert(source.includes("<SearchFilters") && filterSource.includes("MultiSelectFilter"), "the overlay uses Search-owned facets with Core-owned dropdown behavior");
|
||||
assert(source.includes("limit: 50"), "the overlay requests full result windows rather than titlebar suggestions");
|
||||
assert(source.includes("response?.next_cursor"), "the overlay retains cursor pagination");
|
||||
assert(source.includes('usePlatformUiCapabilities<SearchContextsUiCapability>("search.contexts")'), "contextual Search contributions are consumed");
|
||||
|
||||
Reference in New Issue
Block a user