import type { ActionableRow, ActionableSummary, } from "../../lib/actionable/loadActionable"; import { buildChannelRowView, neutralChannelPriority, type ChannelRowView, } from "yt-dlp-transcript-common/views/channelRow"; import { InlineActionButton } from "../actions/InlineActionButton"; import { ChannelsTable } from "../../channels/components/ChannelsTable"; import type { ChannelRowExtras } from "../../channels/components/channelColumns"; // From the plain-data module, never the "use client" registry: a server // component importing a client module's value gets a reference, not the array. import { WORK_COLUMNS } from "../../channels/components/channelColumnPresets"; import type { SectionConfig } from "./sections"; // A server component on purpose: `SectionConfig.primaryAction` is a function // that returns an element, and a function cannot cross the server/client // boundary as a prop. So this is the SHELL — section, heading, description, // empty paragraph — and the rows are the shared channel table // (channels/components/ChannelsTable.tsx) with the WORK_COLUMNS preset, fed // each row's count, extra figure and actions as data and elements. // // THE ARIA CONTRACT BELOW IS LOAD-BEARING. `channel-work.spec`, // `cleanup-actionable.spec`, `incomplete-transcript.spec` and // `download-format-guard.spec` all locate by these exact strings: //
,

{title}

, //

, , // columns Slug / countLabel / [extra] / Last report / Last sync / Actions // (the report cell is the rack's: `report age for `, "stale" / // "missing" in the warning colour), // and a `refresh report ` InlineActionButton on every row. // // NEVER add `role="status"` in here. This renders on the RUNNER operation // pages, where `auto-subs-replace.spec.ts` takes `getByRole("status").first()` // and the runner section reserves that role for its own console. // // On /cleanup the ChannelCleanupCard's own "Clean audio" control is a // StreamActionLog whose button carries no aria-label, so the table's // `clean audio ` button is still the only match for that name; the log's // `aria-label="Clean audio error"` differs from this table's only by // case, and neither is asserted anywhere. export function ChannelWorkTable({ sections, summary, }: { sections: SectionConfig[]; summary: ActionableSummary; }) { return ( <> {sections.map((config) => (

))} ); } function Section({ config, rows, }: { config: SectionConfig; rows: ActionableRow[]; }) { // The functions stay HERE, on the server: each row's count, extra figure and // actions are computed now and cross to the shared (client) table as a // number, a string and an element. const rowExtras: Record = {}; for (const row of rows) { rowExtras[row.channel.slug] = { count: config.getCount(row), extra: config.extraColumn?.getValue(row), actions: ( {config.primaryAction(row)} ), }; } const columns = config.extraColumn ? WORK_COLUMNS : WORK_COLUMNS.filter((id) => id !== "extra"); return (

{config.title}

{config.headerAction}

{config.description}

{rows.length === 0 ? (

{config.emptyLabel}

) : ( )}
); } // The shared row, for a table that draws no bands, no tier, no media badge and // no Location column: those inputs are neutral rather than read. The report // state is the builder's `reportStateOf` (common/views/channelRow.ts, moved // there from loadActionable), which this table used to carry a copy of. function workRowOf(row: ActionableRow): ChannelRowView { return buildChannelRowView({ slug: row.channel.slug, config: row.channel.config, snapshot: row.snapshot, playlistCount: null, bands: [], priority: neutralChannelPriority(), media: null, volume: { id: "", label: "" }, }); }