import type { ComponentProps, ReactNode } from "react"; import type { TFunction } from "i18next"; import { NO_TAG_SENTINEL, type ManagedChannel } from "../lib/api"; import { formatCountOrDash, formatTotalHours } from "../lib/format"; import type { Column } from "./ui/DataTable"; import { PriorityStepper } from "./ui/PriorityStepper"; import ChannelLink from "./ChannelLink"; import AboutCell from "./AboutCell"; import { ChannelTagsControl, type SuggestedTag } from "./ChannelTags"; import { StoredCell } from "./channelCells"; import { isFullySynced } from "./channelSync"; import { RelativeTime } from "./ui/RelativeTime"; /** 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 * channel row shapes. */ export function subsColumn( t: TFunction, lang: string, ): Column { return { key: "subs", header: t("channels.cols.subs"), align: "right", nowrap: true, sortable: true, sortValue: (c) => c.subscriber_count ?? -1, render: (c) => ( {formatCountOrDash(c.subscriber_count, lang)} ), }; } /** 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). */ export function applySyncFilter(rows: ManagedChannel[], value: unknown): ManagedChannel[] { if (value === "fully") return rows.filter(isFullySynced); if (value === "needsFull") return rows.filter((c) => !c.backfill_done); return rows; } /** The Tags column filter predicate (item B): the "No tag" sentinel keeps channels the user hasn't * personally tagged (empty `tag_ids`); it's exclusive with real tag picks (the chip row enforces that), * so when present it wins. Otherwise a channel passes if it carries ANY selected tag id. */ /** @internal exported for channelColumns.dom.test.tsx */ export function tagsColumnTest(c: ManagedChannel, values: string[]): boolean { if (values.includes(NO_TAG_SENTINEL)) return c.tag_ids.length === 0; return values.some((v) => c.tag_ids.includes(Number(v))); } /** Everything the Channels-manager column set closes over. The manager owns the state + mutations; * this factory just wires them into `Column` render/filter/sort defs so the ~150-line * column block lives outside the page component (its cell components are in channelCells). */ export interface ChannelColumnDeps { t: TFunction; /** The UI language, for the numbers — the compiler re-builds the columns when it changes. */ lang: string; prioOptions: { value: string; label: string }[]; userTags: ComponentProps["userTags"]; bumpPriority: (id: string, delta: number) => void; setPriority: (id: string, value: number) => void; /** The row's "…" — the door to the channel's one action list (`useChannelActions`). */ more: (c: ManagedChannel) => ReactNode; onView: (c: ManagedChannel) => void; toggleTag: (channelId: string, tagId: number, hasTag: boolean) => void; createAndAttachTag: (channelId: string, name: string) => void; onFilterByTag: (tagId: number, name: string) => void; // E8·S4 — suggested user tags per channel (resolved to Tag + confidence), plus accept/dismiss. suggestionsFor: (channelId: string) => SuggestedTag[]; onAcceptSuggestion: (channelId: string, tagId: number) => void; onDismissSuggestion: (channelId: string, tagId: number) => void; } export function buildChannelColumns(deps: ChannelColumnDeps): Column[] { const { t, lang, prioOptions, userTags, bumpPriority, setPriority, more, onView, toggleTag, createAndAttachTag, onFilterByTag, suggestionsFor, onAcceptSuggestion, onDismissSuggestion, } = deps; return [ { key: "priority", header: t("channels.cols.priority"), align: "center", // Sized to the longer language's label + the funnel + a sort arrow ("Prioritás ▼ ⧩"): fixed layout // clips a header that outgrows its column instead of widening it. width: "7.5rem", // Off on a tablet-wide table, with About and Length: the row's "…" menu sets the priority too. tabletHidden: true, sortable: true, hideInCard: true, sortValue: (c) => c.priority, filter: { kind: "select", options: prioOptions, test: (c, v) => String(c.priority) === v }, render: (c) => ( bumpPriority(c.id, d)} onSet={(v) => setPriority(c.id, v)} hint={t("channels.row.priorityHint")} /> ), }, { key: "channel", header: t("channels.cols.channel"), sortable: true, sortValue: (c) => (c.title ?? c.id).toLowerCase(), filter: { kind: "text", get: (c) => `${c.title ?? ""} ${c.handle ?? ""}` }, cardPrimary: true, // A FLUID column (R14): no width cap. Under the manager table's fixed layout this cell takes a // share of the leftover space and ChannelLink truncates the name to fit (full name on hover via // title), so a long name can't push the table wider than its container at any zoom. render: (c) => (
onView(c)} />
), }, { key: "about", header: t("channels.cols.about"), hideInCard: true, tabletHidden: true, // The other FLUID column — AboutCell already truncates to its cell. render: (c) => , }, { key: "stored", header: t("channels.cols.stored"), align: "right", nowrap: true, width: "5rem", sortable: true, sortValue: (c) => c.stored_videos, // The stored count carries the "fully synced" ✓ charm (R14 folded the retired Sync column in). render: (c) => , }, { ...subsColumn(t, lang), width: "6rem" }, { key: "lastUpload", header: t("channels.cols.lastUpload"), nowrap: true, width: "8rem", // "Utolsó feltöltés" + a sort arrow, as the priority column above sortable: true, sortValue: (c) => (c.last_video_at ? new Date(c.last_video_at).getTime() : 0), render: (c) => , }, { key: "length", header: t("channels.cols.length"), align: "right", nowrap: true, width: "4.5rem", tabletHidden: true, sortable: true, sortValue: (c) => c.total_duration_seconds, render: (c) => ( {formatTotalHours(c.total_duration_seconds, lang)} ), }, { key: "tags", header: t("channels.cols.tags"), cardLabel: false, // Bounded width so the cell can't widen the column and reflow the whole table; the control // fills it and collapses overflow into "+N" (see ChannelTagsControl). width: "9rem", filter: { kind: "multi", options: userTags.map((tg) => ({ value: String(tg.id), label: tg.name })), test: tagsColumnTest, }, render: (c) => ( toggleTag(c.id, tagId, c.tag_ids.includes(tagId))} onCreate={(name) => createAndAttachTag(c.id, name)} onChipClick={onFilterByTag} suggestions={suggestionsFor(c.id)} onAcceptSuggestion={(tagId) => onAcceptSuggestion(c.id, tagId)} onDismissSuggestion={(tagId) => onDismissSuggestion(c.id, tagId)} /> ), }, { key: "actions", header: "", align: "right", // Room for the "…" at 44px on touch (`.touch-target`), not just its 28px under a mouse. width: "3.25rem", nowrap: true, cardLabel: false, // The row's full-history, hide and unsubscribe buttons went into the menu (E28 C1): the row // keeps what is read or tuned all the time — priority, tags, the ✓ — and ends in its "…". render: more, }, ]; }