fix(channels): drop a stale column filter when sent to "without full history"

A focus-channel deep-link sets (and persists) the channel-name column filter
via the DataTable's externalFilter. That persisted value then survived into
unrelated visits: clicking the header's "N without full history" link landed
on the right tab with the right status chip, but a leftover name filter (e.g.
a channel focused in a past session) hid every row — "No channels".

Add a resetFiltersToken to DataTable that clears its column filters when it
advances, and bump it from the header intent. The token's ref starts at 0 so
the clear fires even when the table remounts on navigation, while a plain
reload (token still 0) keeps the user's persisted filters.
This commit is contained in:
npeter83 2026-06-19 03:55:55 +02:00
parent 39cd025e79
commit 52a5d1fef8
3 changed files with 26 additions and 0 deletions

View file

@ -148,6 +148,9 @@ export default function App() {
setChannelsViewState(v);
localStorage.setItem(CHANNELS_VIEW_KEY, v);
};
// Bumped to tell the channel manager to drop a stale column filter when we send the user
// there to see a specific set (the header's "without full history" link).
const [channelsFilterReset, setChannelsFilterReset] = useState(0);
// "Focus this channel in the manager": jump to the Channels page and seed its name filter
// so the channel is isolated. Cleared when leaving the page so it doesn't re-apply later.
const [focusChannelName, setFocusChannelName] = useState<string | null>(null);
@ -423,6 +426,7 @@ export default function App() {
onGoToFullHistory={() => {
setChannelFilter("needs_full");
setChannelsView("subscribed"); // the status filter applies to the subscriptions tab
setChannelsFilterReset((n) => n + 1); // drop any stale column filter hiding the rows
setPage("channels");
}}
/>
@ -449,6 +453,7 @@ export default function App() {
setStatusFilter={setChannelFilter}
view={channelsView}
setView={setChannelsView}
filtersResetToken={channelsFilterReset}
onOpenWizard={() => setWizardOpen(true)}
onViewChannel={(id, name) => {
setFilters({ ...filters, channelId: id, channelName: name, show: "all" });

View file

@ -52,6 +52,7 @@ export default function Channels({
setStatusFilter,
view,
setView,
filtersResetToken,
onOpenWizard,
}: {
canWrite: boolean;
@ -66,6 +67,8 @@ export default function Channels({
// (the header's "without full history" link, focus-channel) can force it back here.
view: ChannelsView;
setView: (v: ChannelsView) => void;
// Bumped by the header's "without full history" intent to clear a stale column filter.
filtersResetToken: number;
onOpenWizard: () => void;
}) {
const { t } = useTranslation();
@ -510,6 +513,7 @@ export default function Channels({
controlsPosition="top"
controlsLeading={statusChips}
externalFilter={focusChannelName ? { key: "channel", value: focusChannelName } : null}
resetFiltersToken={filtersResetToken}
rowClassName={(c) => (c.hidden ? "opacity-60" : "")}
emptyText={t("channels.empty")}
/>

View file

@ -67,6 +67,7 @@ export default function DataTable<T>({
controlsPosition = "bottom",
controlsLeading,
externalFilter,
resetFiltersToken,
}: {
rows: T[];
columns: Column<T>[];
@ -81,6 +82,10 @@ export default function DataTable<T>({
// Set a column's filter from outside (e.g. "focus this channel" deep-links here). Applied
// whenever its value changes; the user can still clear it from the header afterwards.
externalFilter?: { key: string; value: string } | null;
// Bump this to clear all column filters — for navigation intents that mean "show me this
// set" (e.g. the header's "without full history" link), so a stale persisted filter can't
// hide the rows the user was just sent to see.
resetFiltersToken?: number;
}) {
const { t } = useTranslation();
const initial = loadPersist(persistKey);
@ -104,6 +109,18 @@ export default function DataTable<T>({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [externalFilter?.key, externalFilter?.value]);
// Clear column filters when the reset token advances. The ref starts at 0 (not at the prop)
// so a navigation that bumped the token still clears even when it remounts this table fresh
// (e.g. arriving from another page); a plain reload keeps the token at 0 and the effect is a
// no-op, so the user's persisted filters survive.
const resetRef = useRef(0);
useEffect(() => {
if (resetFiltersToken === undefined || resetFiltersToken === resetRef.current) return;
resetRef.current = resetFiltersToken;
setFilters({});
setPage(0);
}, [resetFiltersToken]);
useEffect(() => {
if (!openFilter) return;
function onDown(e: MouseEvent) {