refactor: component files export only components; lint fails on a warning
The 43 react-refresh/only-export-components warnings the gate let through meant an edit to a provider (or AnchoredPopover, Toaster, plexDetailUi, …) could not be hot-swapped: Fast Refresh reloads a module that also exports hooks or helpers. - Each provider's contexts, the types its hooks return and the hooks themselves live in a sibling context module (navigationContext.ts, channelsContext.ts, …); the provider file keeps the component. loadAccountFilters moved next to the feed filters' defaults (feedFiltersInit.ts), following navigationInit/playlistsInit. - Helpers that sat in component files have their own modules: welcomeMode, downloadTitle, plexPlayable, channelSync, ui/anchoredPopoverHooks, ui/pageToolbarSlot, ui/pagerContent, ui/toastLevels, ui/overlayOrder, ui/tableAlign, lib/plexDetail; the cells of the channel columns and the registry's page wrappers are components files of their own (channelCells, modulePages). - usePersistedTab, an alias of useAccountPersistedState, is gone; its callers use the original. npm run lint is now eslint --max-warnings 0, so a warning fails the gate like an error does.
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import type { TFunction } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Check, Eye, EyeOff, History, UserMinus } from "lucide-react";
|
||||
import { NO_TAG_SENTINEL, type ManagedChannel } from "../lib/api";
|
||||
import { formatCountOrDash, formatNumber, formatTotalHours, relativeTime } from "../lib/format";
|
||||
import { formatCountOrDash, formatTotalHours, relativeTime } from "../lib/format";
|
||||
import type { Column } from "./ui/DataTable";
|
||||
import Tooltip from "./ui/Tooltip";
|
||||
import { PriorityStepper } from "./ui/PriorityStepper";
|
||||
import ChannelLink from "./ChannelLink";
|
||||
import AboutCell from "./AboutCell";
|
||||
import { ChannelTagsControl, type SuggestedTag } from "./ChannelTags";
|
||||
import { ActionsCell, StoredCell } from "./channelCells";
|
||||
import { isFullySynced } from "./channelSync";
|
||||
|
||||
/** The shared "subscribers" DataTable column (right-aligned, sortable, em-dash for null) — identical
|
||||
* in the Channels manager and the ChannelDiscovery table, which show subscriber counts on different
|
||||
@@ -28,11 +27,6 @@ export function subsColumn<T extends { subscriber_count: number | null }>(t: TFu
|
||||
};
|
||||
}
|
||||
|
||||
// A channel is "fully synced" when its recent uploads AND full back-catalogue are both in — the
|
||||
// predicate the retired Sync column used, now surfaced as a ✓ charm on the Stored count and driven
|
||||
// by the top sync-filter chips.
|
||||
const isFullySynced = (c: ManagedChannel) => c.recent_synced && c.backfill_done;
|
||||
|
||||
/** Sync-status pre-filter for the subscribed table. The Sync COLUMN is gone (R14) — its status folded
|
||||
* into a charm on Stored — so the top chips' `filters.sync` value is applied here instead of via a
|
||||
* column predicate. Same vocab as the old column filter: "fully" | "needsFull" | "" (all). */
|
||||
@@ -50,106 +44,9 @@ export function tagsColumnTest(c: ManagedChannel, values: string[]): boolean {
|
||||
return values.some((v) => c.tag_ids.includes(Number(v)));
|
||||
}
|
||||
|
||||
// The Stored count, with a "fully synced" ✓ charm to the left of the number when recent uploads and
|
||||
// full history are both in — folding the retired Sync column's at-a-glance status into the value it
|
||||
// describes. Full detail stays in the hover tooltip.
|
||||
function StoredCell({ c }: { c: ManagedChannel }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<span className="inline-flex items-center justify-end gap-1 text-muted tabular-nums">
|
||||
{isFullySynced(c) && (
|
||||
<Tooltip hint={t("channels.row.fullySyncedHint")}>
|
||||
<Check
|
||||
role="img"
|
||||
className="w-3.5 h-3.5 text-accent shrink-0 cursor-help"
|
||||
aria-label={t("channels.row.fullySynced")}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{formatNumber(c.stored_videos)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionsCell({
|
||||
c,
|
||||
isAdmin,
|
||||
canWrite,
|
||||
onHide,
|
||||
onDeep,
|
||||
onReset,
|
||||
onUnsubscribe,
|
||||
}: {
|
||||
c: ManagedChannel;
|
||||
isAdmin: boolean;
|
||||
canWrite: boolean;
|
||||
onHide: () => void;
|
||||
onDeep: () => void;
|
||||
onReset: () => void;
|
||||
onUnsubscribe: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
// Admins get a "reset" trigger that re-fetches the channel from scratch regardless of
|
||||
// state — always actionable. Regular users get the per-user "request full history" opt-in,
|
||||
// which only applies until the back-catalog is in (or already coming for everyone).
|
||||
const done = c.backfill_done;
|
||||
const requested = !done && c.deep_requested;
|
||||
const optInActionable = !done && !c.deep_in_queue;
|
||||
const backfillHint = isAdmin
|
||||
? t("channels.row.resetHint")
|
||||
: c.deep_in_queue
|
||||
? t("channels.row.queuedByOtherHint")
|
||||
: requested
|
||||
? t("channels.row.queuedRequestedHint")
|
||||
: done
|
||||
? t("channels.row.fullHint")
|
||||
: t("channels.row.backfillThisHint");
|
||||
const enabled = isAdmin || optInActionable;
|
||||
return (
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Tooltip hint={backfillHint}>
|
||||
<button
|
||||
onClick={!enabled ? undefined : isAdmin ? onReset : onDeep}
|
||||
disabled={!enabled}
|
||||
className={`shrink-0 transition ${
|
||||
requested
|
||||
? "text-accent"
|
||||
: enabled
|
||||
? "text-muted hover:text-fg"
|
||||
: "text-muted/40 cursor-default"
|
||||
}`}
|
||||
aria-label={isAdmin ? t("channels.row.resetBackfill") : t("channels.row.backfillThis")}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip hint={c.hidden ? t("channels.row.hiddenHint") : t("channels.row.hideHint")}>
|
||||
<button
|
||||
onClick={onHide}
|
||||
className="text-muted hover:text-fg shrink-0"
|
||||
aria-label={c.hidden ? t("channels.row.unhide") : t("channels.row.hideFromFeed")}
|
||||
>
|
||||
{c.hidden ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
{canWrite && (
|
||||
<Tooltip hint={t("channels.row.unsubscribeHint")}>
|
||||
<button
|
||||
onClick={onUnsubscribe}
|
||||
className="text-muted hover:text-danger shrink-0"
|
||||
aria-label={t("channels.row.unsubscribeOnYoutube")}
|
||||
>
|
||||
<UserMinus className="w-4 h-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Everything the Channels-manager column set closes over. The manager owns the state + mutations;
|
||||
* this factory just wires them into `Column<ManagedChannel>` render/filter/sort defs so the ~150-line
|
||||
* column block lives next to its cells instead of inside the page component. */
|
||||
* column block lives outside the page component (its cell components are in channelCells). */
|
||||
export interface ChannelColumnDeps {
|
||||
t: TFunction;
|
||||
isAdmin: boolean;
|
||||
|
||||
Reference in New Issue
Block a user