commit 9920845af6b7dae89a68467ae252b86d1dda72f0
parent fe9a33301463c5ec9d1ff5396279b9677e07f962
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 24 Sep 2026 13:12:01 -0400
channels: column ids and presets move to a plain module (server-importable)
A server component that imports a VALUE from a "use client" module gets a
client reference, not the value: ChannelWorkTable's `WORK_COLUMNS.filter`
threw "filter is not a function" on every operation page (found by the e2e
run's first minute; tsc cannot see it). The ids, sort keys, PipelineColumn
and the three presets now live in `channelColumnPresets.ts` (no directive);
the client registry re-exports them for client importers, and the two server
importers (ChannelWorkTable, channels/page) take them from the plain module.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
4 files changed, 116 insertions(+), 95 deletions(-)
diff --git a/editor/app/channels/components/channelColumnPresets.ts b/editor/app/channels/components/channelColumnPresets.ts
@@ -0,0 +1,96 @@
+// THE CHANNEL COLUMN IDS AND PRESETS — plain data, deliberately NOT in the
+// "use client" registry (./channelColumns.tsx). A server component that imports
+// a value from a client module gets a client REFERENCE, not the value: the work
+// tables' server shell calling `WORK_COLUMNS.filter(...)` threw "filter is not
+// a function". So the ids, the sort keys, the pipeline-column shape and the
+// presets live here, importable from either side; the cells stay client-only.
+
+export type ChannelColumnId =
+ | "select"
+ | "slug"
+ | "name"
+ | "handling"
+ | "build"
+ | "tier"
+ | "playlist"
+ | "videos"
+ | "digestReachable"
+ | "count"
+ | "extra"
+ | "lastSync"
+ | "lastSyncRelative"
+ | "report"
+ | "location"
+ | "size"
+ | "pipelines"
+ | "actions";
+
+// Sorting a pipeline column sorts by `reachable` — "which channel has the most
+// diarizable audio left right now".
+export type ChannelSortKey =
+ | "slug"
+ | "name"
+ | "handling"
+ | "build"
+ | "tier"
+ | "playlist"
+ | "lastSync"
+ | "report"
+ | "location"
+ | "size"
+ | `op:${string}`;
+
+// A column heading for one pipeline. Comes off the operation registry on the
+// server (shortLabel, costBasis) rather than being abbreviated here — see
+// operations.ts.
+export type PipelineColumn = {
+ id: string;
+ shortLabel: string;
+ label: string;
+ costBasis: string;
+ // The two externally-dispatched pipelines keep their existing count column
+ // semantics: the cell's text node is the bare number the suite asserts on,
+ // under this accessible name. The slug is appended by the cell, so this is
+ // the prefix — `"downloads count"` becomes `"downloads count for foo"`, which
+ // is the name channels-counts.spec.ts has always addressed these cells by.
+ // Null for the derived operations, which have never had a count column and
+ // get the band's sentence as their text.
+ countLabel: string | null;
+};
+
+// THE PRESETS. The rack draws everything; the dashboard and a work section
+// each name the handful they have always drawn.
+export const RACK_COLUMNS: readonly ChannelColumnId[] = [
+ "select",
+ "slug",
+ "name",
+ "handling",
+ "build",
+ "tier",
+ "playlist",
+ "lastSync",
+ "report",
+ "location",
+ "size",
+ "pipelines",
+ "actions",
+];
+
+export const DASHBOARD_COLUMNS: readonly ChannelColumnId[] = [
+ "slug",
+ "handling",
+ "videos",
+ "digestReachable",
+ "lastSyncRelative",
+ "actions",
+];
+
+// `extra` is dropped by the caller when the section has no extra column.
+export const WORK_COLUMNS: readonly ChannelColumnId[] = [
+ "slug",
+ "count",
+ "extra",
+ "report",
+ "lastSync",
+ "actions",
+];
diff --git a/editor/app/channels/components/channelColumns.tsx b/editor/app/channels/components/channelColumns.tsx
@@ -11,6 +11,21 @@ import { ChannelAvailabilityButton } from "./ChannelAvailabilityButton";
import { ChannelBuildToggle } from "./ChannelBuildToggle";
import { ChannelSyncButton } from "./ChannelSyncButton";
import ChannelTierSelect from "./ChannelTierSelect";
+import type {
+ ChannelColumnId,
+ ChannelSortKey,
+} from "./channelColumnPresets";
+
+// Re-exported for the client importers; a SERVER importer must take these from
+// ./channelColumnPresets.ts directly (see there).
+export {
+ DASHBOARD_COLUMNS,
+ RACK_COLUMNS,
+ WORK_COLUMNS,
+ type ChannelColumnId,
+ type ChannelSortKey,
+ type PipelineColumn,
+} from "./channelColumnPresets";
// THE CHANNEL COLUMN REGISTRY.
//
@@ -27,59 +42,6 @@ import ChannelTierSelect from "./ChannelTierSelect";
// `playlist count for`, `report age for`, `media location for`, `media size
// for`, and — in the table's pipeline cell — `${countLabel} for`.
-export type ChannelColumnId =
- | "select"
- | "slug"
- | "name"
- | "handling"
- | "build"
- | "tier"
- | "playlist"
- | "videos"
- | "digestReachable"
- | "count"
- | "extra"
- | "lastSync"
- | "lastSyncRelative"
- | "report"
- | "location"
- | "size"
- | "pipelines"
- | "actions";
-
-// Sorting a pipeline column sorts by `reachable` — "which channel has the most
-// diarizable audio left right now".
-export type ChannelSortKey =
- | "slug"
- | "name"
- | "handling"
- | "build"
- | "tier"
- | "playlist"
- | "lastSync"
- | "report"
- | "location"
- | "size"
- | `op:${string}`;
-
-// A column heading for one pipeline. Comes off the operation registry on the
-// server (shortLabel, costBasis) rather than being abbreviated here — see
-// operations.ts.
-export type PipelineColumn = {
- id: string;
- shortLabel: string;
- label: string;
- costBasis: string;
- // The two externally-dispatched pipelines keep their existing count column
- // semantics: the cell's text node is the bare number the suite asserts on,
- // under this accessible name. The slug is appended by the cell, so this is
- // the prefix — `"downloads count"` becomes `"downloads count for foo"`, which
- // is the name channels-counts.spec.ts has always addressed these cells by.
- // Null for the derived operations, which have never had a count column and
- // get the band's sentence as their text.
- countLabel: string | null;
-};
-
// What a server caller adds to one row: the section's count and extra figure,
// and its actions cluster as an ELEMENT (elements cross the boundary; the
// functions that built them stay on the server).
@@ -377,40 +339,3 @@ export const CHANNEL_COLUMNS: Record<
}),
},
};
-
-// THE PRESETS. The rack draws everything; the dashboard and a work section
-// each name the handful they have always drawn.
-export const RACK_COLUMNS: readonly ChannelColumnId[] = [
- "select",
- "slug",
- "name",
- "handling",
- "build",
- "tier",
- "playlist",
- "lastSync",
- "report",
- "location",
- "size",
- "pipelines",
- "actions",
-];
-
-export const DASHBOARD_COLUMNS: readonly ChannelColumnId[] = [
- "slug",
- "handling",
- "videos",
- "digestReachable",
- "lastSyncRelative",
- "actions",
-];
-
-// `extra` is dropped by the caller when the section has no extra column.
-export const WORK_COLUMNS: readonly ChannelColumnId[] = [
- "slug",
- "count",
- "extra",
- "report",
- "lastSync",
- "actions",
-];
diff --git a/editor/app/channels/page.tsx b/editor/app/channels/page.tsx
@@ -39,7 +39,7 @@ import {
import { buildChannelBands } from "yt-dlp-transcript-common/views/pipeline/buildBands";
import { EXTERNAL_BAND_IDS } from "yt-dlp-transcript-common/views/pipeline/buildBands";
import { ChannelsRack } from "./components/ChannelsRack";
-import type { PipelineColumn } from "./components/channelColumns";
+import type { PipelineColumn } from "./components/channelColumnPresets";
import {
buildChannelRowView,
channelVolumeOf,
diff --git a/editor/app/components/channelWork/ChannelWorkTable.tsx b/editor/app/components/channelWork/ChannelWorkTable.tsx
@@ -9,10 +9,10 @@ import {
} from "yt-dlp-transcript-common/views/channelRow";
import { InlineActionButton } from "../actions/InlineActionButton";
import { ChannelsTable } from "../../channels/components/ChannelsTable";
-import {
- WORK_COLUMNS,
- type ChannelRowExtras,
-} from "../../channels/components/channelColumns";
+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