Merge bug/channels-tab-focus: reliable channel-manager navigation (tab + stale filter)
This commit is contained in:
commit
b6404a5e55
3 changed files with 50 additions and 13 deletions
|
|
@ -26,7 +26,7 @@ import Header from "./components/Header";
|
|||
import NavSidebar from "./components/NavSidebar";
|
||||
import Sidebar from "./components/Sidebar";
|
||||
import Feed from "./components/Feed";
|
||||
import Channels, { type ChannelStatusFilter } from "./components/Channels";
|
||||
import Channels, { type ChannelStatusFilter, type ChannelsView } from "./components/Channels";
|
||||
import Playlists from "./components/Playlists";
|
||||
import Stats from "./components/Stats";
|
||||
import Scheduler from "./components/Scheduler";
|
||||
|
|
@ -136,11 +136,27 @@ export default function App() {
|
|||
const [aboutOpen, setAboutOpen] = useState(false);
|
||||
const [notesOpen, setNotesOpen] = useState(false);
|
||||
const [notesHighlight, setNotesHighlight] = useState<string | undefined>(undefined);
|
||||
// The Channel manager's active tab (subscriptions vs playlist discovery). Lifted here and
|
||||
// persisted so navigation intents that target the subscriptions table — the header's
|
||||
// "without full history" link, focus-channel — can force it back to "subscribed" instead
|
||||
// of dumping the user on whatever tab they last left open.
|
||||
const CHANNELS_VIEW_KEY = "siftlode.channelsView";
|
||||
const [channelsView, setChannelsViewState] = useState<ChannelsView>(() =>
|
||||
localStorage.getItem(CHANNELS_VIEW_KEY) === "discovery" ? "discovery" : "subscribed"
|
||||
);
|
||||
const setChannelsView = (v: ChannelsView) => {
|
||||
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);
|
||||
const focusChannel = (name: string) => {
|
||||
setFocusChannelName(name);
|
||||
setChannelsView("subscribed"); // the name filter applies to the subscriptions table
|
||||
setPage("channels");
|
||||
};
|
||||
useEffect(() => {
|
||||
|
|
@ -409,6 +425,8 @@ export default function App() {
|
|||
page={page}
|
||||
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");
|
||||
}}
|
||||
/>
|
||||
|
|
@ -433,6 +451,9 @@ export default function App() {
|
|||
onFocusChannel={focusChannel}
|
||||
statusFilter={channelFilter}
|
||||
setStatusFilter={setChannelFilter}
|
||||
view={channelsView}
|
||||
setView={setChannelsView}
|
||||
filtersResetToken={channelsFilterReset}
|
||||
onOpenWizard={() => setWizardOpen(true)}
|
||||
onViewChannel={(id, name) => {
|
||||
setFilters({ ...filters, channelId: id, channelName: name, show: "all" });
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import ChannelDiscovery from "./ChannelDiscovery";
|
|||
import TagManager from "./TagManager";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
|
||||
type ChannelsView = "subscribed" | "discovery";
|
||||
export type ChannelsView = "subscribed" | "discovery";
|
||||
|
||||
export type ChannelStatusFilter = "all" | "needs_full" | "fully_synced" | "hidden";
|
||||
|
||||
|
|
@ -50,6 +50,9 @@ export default function Channels({
|
|||
focusChannelName,
|
||||
statusFilter,
|
||||
setStatusFilter,
|
||||
view,
|
||||
setView,
|
||||
filtersResetToken,
|
||||
onOpenWizard,
|
||||
}: {
|
||||
canWrite: boolean;
|
||||
|
|
@ -60,23 +63,18 @@ export default function Channels({
|
|||
focusChannelName: string | null;
|
||||
statusFilter: ChannelStatusFilter;
|
||||
setStatusFilter: (f: ChannelStatusFilter) => void;
|
||||
// The active tab lives in App so navigation intents that target the subscriptions table
|
||||
// (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();
|
||||
const qc = useQueryClient();
|
||||
const confirm = useConfirm();
|
||||
|
||||
// Which tab is showing: the user's subscriptions, or channels discovered from their
|
||||
// playlists. Persisted so a reload keeps the active tab (see other siftlode.* keys).
|
||||
const [view, setView] = useState<ChannelsView>(() =>
|
||||
localStorage.getItem("siftlode.channelsView") === "discovery"
|
||||
? "discovery"
|
||||
: "subscribed"
|
||||
);
|
||||
useEffect(() => {
|
||||
localStorage.setItem("siftlode.channelsView", view);
|
||||
}, [view]);
|
||||
|
||||
// A YouTube-gated action (sync, backfill, unsubscribe) that 403s means the user hasn't
|
||||
// granted the needed scope — surface that with a "Connect" action instead of a vague fail.
|
||||
const notifyActionError = (err: unknown, fallbackKey: string) => {
|
||||
|
|
@ -515,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")}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue