fix(channels): picked tag chips lead the row, so the +N fold never hides an active filter

This commit is contained in:
2026-11-05 22:34:12 +01:00
parent e4b6d918e4
commit c34f6f3297
3 changed files with 47 additions and 13 deletions
@@ -210,4 +210,31 @@ describe("tagFilterChips", () => {
expect(chips.find((c) => c.key === "t99")?.count).toBeUndefined();
expect(chips[chips.length - 1]?.count).toBeUndefined();
});
// The row folds from its end into "+N", so a picked tag late in the list was folded away: at 1024 px
// "music" sat behind "+11" with nothing on the row saying a filter was on. The picks lead the row.
it("puts the picked tags first, keeping their order, then the rest in theirs", () => {
const tag = (id: number, name: string) => ({ id, name, system: false }) as Tag;
const row = (picked: string[], noTagOn = false) =>
tagFilterChips({
userTags: [tag(1, "ai"), tag(2, "docu"), tag(3, "music"), tag(4, "nature")],
tags: { isPending: false } as never,
picked,
noTagOn,
toggle: () => {},
toggleNoTag: () => {},
t: ((k: string) => k) as never,
}).map((c) => c.label + (c.active ? "*" : ""));
expect(row(["4", "3"])).toEqual(["music*", "nature*", "ai", "docu", "channels.tags.noTag"]);
expect(row(["99", "3"])).toEqual([
"99*",
"music*",
"ai",
"docu",
"nature",
"channels.tags.noTag",
]);
expect(row([], true)).toEqual(["channels.tags.noTag*", "ai", "docu", "music", "nature"]);
expect(row([])).toEqual(["ai", "docu", "music", "nature", "channels.tags.noTag"]);
});
});
@@ -51,11 +51,12 @@ export function tagsColumnTest(c: ManagedChannel, values: string[]): boolean {
}
/** The manager's tag-filter chip row (the Tags column filter's values): any picked id that is not one
* of the user's tags, then each user tag, then the "No tag" sentinel. A stored pick can outlive its
* of the user's tags, then the picked ones of the user's tags and "No tag", then the rest, each group
* in the tags' order with "No tag" last. The row folds from its end into "+N", so every pick leads it:
* a filter that is on stays in sight, never only inside the fold. A stored pick can outlive its
* tag (deleted here or on another device) and still narrows the table to nothing, so it stays a
* visible, removable chip — FIRST, so the row's "+N" overflow never folds the reason away —
* and so does every pick while the tag list loads or after it failed, named by lib/tagPicks' rule
* (a placeholder, then the name, "Deleted tag" or the id), not hidden. */
* visible, removable chip, and so does every pick while the tag list loads or after it failed, named
* by lib/tagPicks' rule (a placeholder, then the name, "Deleted tag" or the id), not hidden. */
export function tagFilterChips(o: {
userTags: Tag[];
tags: TagList;
@@ -67,14 +68,7 @@ export function tagFilterChips(o: {
}): FilterChip[] {
const mine = new Set(o.userTags.map((tg) => String(tg.id)));
const others = o.picked.filter((id) => id !== NO_TAG_SENTINEL && !mine.has(id));
return [
...others.map((id) => ({
key: `t${id}`,
label: tagPickLabel(Number(id), o.tags, o.t),
pending: o.tags.isPending,
active: true,
onToggle: () => o.toggle(Number(id)),
})),
const known: FilterChip[] = [
...o.userTags.map((tg) => ({
key: `t${tg.id}`,
label: tg.name,
@@ -89,6 +83,17 @@ export function tagFilterChips(o: {
onToggle: o.toggleNoTag,
},
];
return [
...others.map((id) => ({
key: `t${id}`,
label: tagPickLabel(Number(id), o.tags, o.t),
pending: o.tags.isPending,
active: true,
onToggle: () => o.toggle(Number(id)),
})),
...known.filter((c) => c.active),
...known.filter((c) => !c.active),
];
}
/** Everything the Channels-manager column set closes over. The manager owns the state + mutations;