Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 0cc979a56242db8adee6c17ed4e1f588641a5389
parent 74e549feccd0b263055e720cad092b981b3f3f3f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 25 May 2026 19:47:18 -0400

one-layer compact mode reverts don't show hits option

Diffstat:
Mcommon/components/QueryBuilder.tsx | 33++++++++++++++++-----------------
Mcommon/lib/searchQuery.ts | 22++++++++++++++++++++++
Mexport/CHANGELOG.md | 3+++
Mexport/e2e/query-tree.spec.ts | 45+++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 86 insertions(+), 17 deletions(-)

diff --git a/common/components/QueryBuilder.tsx b/common/components/QueryBuilder.tsx @@ -11,9 +11,10 @@ import { insertChild, - isLeaf, + isCompactRoot, newGroup, newLeaf, + normalizeRoot, removeNode, replaceNode, unwrapGroup, @@ -39,34 +40,32 @@ export default function QueryBuilder({ leafStates, groupStates, }: Props) { + // All mutations flow through `emit` so the result is normalized: when a + // change collapses the tree into compact mode, options the compact UI hides + // (e.g. "Show hits in results") revert to their leaf defaults rather than + // staying stuck with no control to toggle them. See `normalizeRoot`. + const emit = (next: GroupNode) => onChange(normalizeRoot(next)); + const onChangeNode = (id: string, next: QueryNode) => { if (id === root.id) { - if (next.kind === "group") onChange(next); + if (next.kind === "group") emit(next); return; } - onChange(replaceNode(root, id, next)); + emit(replaceNode(root, id, next)); }; - const onRemoveNode = (id: string) => onChange(removeNode(root, id)); + const onRemoveNode = (id: string) => emit(removeNode(root, id)); const onAddChild = (groupId: string, child: QueryNode) => - onChange(insertChild(root, groupId, child)); + emit(insertChild(root, groupId, child)); const onWrapLeaf = (leafId: string) => - onChange(wrapInGroup(root, leafId, "AND")); + emit(wrapInGroup(root, leafId, "AND")); const onUnwrapGroup = (groupId: string) => - onChange(unwrapGroup(root, groupId)); + emit(unwrapGroup(root, groupId)); // Compact mode: root AND group with one non-negated leaf, no negation // applied to the root either. Anything else falls back to the full // builder rendering. - const onlyChild = root.children.length === 1 ? root.children[0] : null; - const isCompactCandidate = - !root.negate && - root.op === "AND" && - onlyChild !== null && - isLeaf(onlyChild) && - !onlyChild.negate; - - if (isCompactCandidate && onlyChild && isLeaf(onlyChild)) { - const leaf = onlyChild as LeafNode; + if (isCompactRoot(root)) { + const leaf = root.children[0] as LeafNode; return ( <div className="flex flex-col gap-2" data-testid="query-builder"> <QueryLeafView diff --git a/common/lib/searchQuery.ts b/common/lib/searchQuery.ts @@ -292,6 +292,28 @@ export function removeNode(root: GroupNode, id: string): GroupNode { return out; } +// True when the tree renders in QueryBuilder's "compact" single-input mode: +// a non-negated AND root with exactly one non-negated leaf child. +export function isCompactRoot(root: GroupNode): boolean { + if (root.negate || root.op !== "AND" || root.children.length !== 1) { + return false; + } + const only = root.children[0]; + return isLeaf(only) && !only.negate; +} + +// Compact mode hides per-leaf options that have no other control to toggle +// them (currently "Show hits in results"). When a tree collapses to compact +// form those hidden options would otherwise stay stuck in whatever state they +// had with 2+ layers, so reset them to their leaf defaults. No-op for any +// non-compact tree. +export function normalizeRoot(root: GroupNode): GroupNode { + if (!isCompactRoot(root)) return root; + const only = root.children[0] as LeafNode; + if (only.contributeHits) return root; + return { ...root, children: [{ ...only, contributeHits: true }] }; +} + function removeFromNode(node: QueryNode, id: string): QueryNode | null { if (!isGroup(node)) return null; let changed = false; diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [Unreleased] +- **Collapsing the query builder to one layer no longer strips a hidden setting.** Deselecting "Show hits in results" on a layer and then deleting the other layers used to leave that lone layer permanently hits-off, with no control to fix it (the checkbox is hidden in the single-layer compact view). Collapsing back to compact now resets "Show hits" to on. + ## [0.2.1] - 2026-05-25 - **"Unlisted" availability filter.** The Availability filter group now has a dedicated **Unlisted** checkbox alongside Available and Deleted. Availability is now a three-way split — unlisted videos are their own bucket rather than counting as "Available", so you can isolate or hide them independently. Unlisted videos remain shown by default, and the filter is saved in profiles and shareable links like the other filters. diff --git a/export/e2e/query-tree.spec.ts b/export/e2e/query-tree.spec.ts @@ -282,6 +282,51 @@ test.describe("composite search — query tree", () => { ).toHaveCount(2, { timeout: 15_000 }); }); + test("collapse to one layer resets the hidden 'Show hits' option", async ({ + page, + }) => { + // Regression: "Show hits in results" (contributeHits) is hidden in the + // builder's compact single-layer view. Deselecting it on a layer and then + // deleting the other layers used to leave the lone layer stuck hits-off + // with no control to turn it back on. Collapsing to compact now resets it. + // Seed a two-leaf AND where the first (alpha) is hits-off, the second + // (gamma) plain — so only gamma contributes hits to start. + const tree: SGroup = { + k: "g", + o: "AND", + c: [ + { k: "l", q: "alpha", s: "transcripts", h: 0 }, + { k: "l", q: "gamma", s: "transcripts" }, + ], + }; + await page.goto(`/?qt=${qt(tree)}`); + await expectResultSlugs(page, [ + TRANSCRIPT_ONLY_SLUG, + CHAT_SMALL_SLUG, + CHAT_LARGE_SLUG, + ]); + // Only gamma contributes hits → one leaf-section per matched video. + await expect( + page.locator( + `[data-result-slug="${TRANSCRIPT_ONLY_SLUG}"] [data-leaf-section]`, + ), + ).toHaveCount(1); + + // Delete the second leaf (gamma); alpha is now the only layer, so the + // builder collapses into compact mode. + await page.locator('[data-testid^="leaf-delete-"]').nth(1).click(); + await expect(page.getByTestId("compact-add-layer")).toBeVisible(); + + // Commit. alpha had hits off, but collapsing reset contributeHits → it now + // contributes hits as the sole layer (one leaf-section per matched video). + await page.getByTestId("search-submit").click(); + await expect( + page.locator( + `[data-result-slug="${TRANSCRIPT_ONLY_SLUG}"] [data-leaf-section]`, + ), + ).toHaveCount(1, { timeout: 15_000 }); + }); + test("unwrap: wrapping then unwrapping a leaf restores the original tree", async ({ page, }) => {