commit c3fccb430c6f4719a482adbb0c1332946b6ed6b7
parent 875418da67cb789dd55867a7dcf01529b2ac1094
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 11 Sep 2026 12:14:06 -0400
editor: /channels rows can be ticked
The table had one checkbox and it was "Group by section". This adds the select
column a bulk action needs: a header "select all channels" and a per-row
"select <slug>", a `ReadonlySet` of SLUGS rather than indices so a re-render
that reorders or drops a row cannot retarget the selection, and a
`selectedSlugs` that is always the intersection with what is on screen — a scope
change or a deletion must not leave a bulk edit acting on a row nobody can see.
COPIED LINE-FOR-LINE FROM `channel-priority/s3` (`ac39d91`), which adds the same
column to the same file for its own bulk bar. Both branches are unmerged and
both need this column; identical lines make that merge a no-op instead of a
hand-reconciliation of two selection models. Everything is byte-identical — the
state, the derived slugs, `toggleOne`, the colSpan arithmetic, both checkboxes
and both aria-labels — except one comment word, because "a bulk priority edit"
is not what this branch selects for.
No bar yet: the next commit is the action it drives.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
1 file changed, 64 insertions(+), 4 deletions(-)
diff --git a/editor/app/channels/components/ChannelsTable.tsx b/editor/app/channels/components/ChannelsTable.tsx
@@ -201,6 +201,10 @@ export function ChannelsTable({
siteId?: string;
}) {
const [sort, setSort] = useState<SortState>(null);
+ // Slugs ticked for a bulk edit. A Set of SLUGS, not indices, so a
+ // re-render that reorders or drops a row cannot retarget the selection —
+ // the same reason SyncConsole keys its selection this way.
+ const [selected, setSelected] = useState<ReadonlySet<string>>(new Set());
// Plain component state, deliberately NOT the URL: router.replace races the
// global AutoRefresh's router.refresh() and gets dropped.
const [grouped, setGrouped] = useState(true);
@@ -215,8 +219,24 @@ export function ChannelsTable({
[channels],
);
const showSections = grouped && !!sections && sections.length > 0 && !!siteId;
- // Eight fixed columns, one per pipeline, then Actions.
- const colSpan = 9 + columns.length;
+ // The select column, eight fixed columns, one per pipeline, then Actions.
+ const colSpan = 10 + columns.length;
+ // Always the intersection with what is on screen: a slug can leave the table
+ // between renders (a scope change, a deletion), and a bulk edit must not act
+ // on a row nobody can see.
+ const selectedSlugs = channels
+ .map((c) => c.slug)
+ .filter((s) => selected.has(s));
+ const allSelected =
+ channels.length > 0 && selectedSlugs.length === channels.length;
+
+ function toggleOne(slug: string) {
+ setSelected((prev) => {
+ const next = new Set(prev);
+ if (!next.delete(slug)) next.add(slug);
+ return next;
+ });
+ }
function onHeaderClick(key: SortKey) {
setSort((prev) => {
@@ -243,6 +263,21 @@ export function ChannelsTable({
<table className="text-sm border-y md:border border-border md:rounded-md md:overflow-hidden w-full">
<thead className="bg-muted">
<tr>
+ <th className="px-2 py-2">
+ <input
+ type="checkbox"
+ aria-label="select all channels"
+ checked={allSelected}
+ onChange={(e) =>
+ setSelected(
+ e.target.checked
+ ? new Set(channels.map((c) => c.slug))
+ : new Set(),
+ )
+ }
+ className="accent-primary"
+ />
+ </th>
<SortableTh
label="Slug"
sortKey="slug"
@@ -328,14 +363,26 @@ export function ChannelsTable({
section.channels.flatMap((c) => rowBySlug.get(c.slug) ?? []),
sort,
).map((c) => (
- <ChannelTableRow key={c.slug} channel={c} columns={columns} />
+ <ChannelTableRow
+ key={c.slug}
+ channel={c}
+ columns={columns}
+ selected={selected.has(c.slug)}
+ onToggle={toggleOne}
+ />
))}
</tbody>
))
) : (
<tbody>
{rows.map((c) => (
- <ChannelTableRow key={c.slug} channel={c} columns={columns} />
+ <ChannelTableRow
+ key={c.slug}
+ channel={c}
+ columns={columns}
+ selected={selected.has(c.slug)}
+ onToggle={toggleOne}
+ />
))}
</tbody>
)}
@@ -406,9 +453,13 @@ function PipelineCell({
function ChannelTableRow({
channel: c,
columns,
+ selected,
+ onToggle,
}: {
channel: ChannelRow;
columns: PipelineColumn[];
+ selected: boolean;
+ onToggle: (slug: string) => void;
}) {
return (
<tr
@@ -419,6 +470,15 @@ function ChannelTableRow({
: "")
}
>
+ <td className="px-2 py-2">
+ <input
+ type="checkbox"
+ aria-label={`select ${c.slug}`}
+ checked={selected}
+ onChange={() => onToggle(c.slug)}
+ className="accent-primary"
+ />
+ </td>
<Td className="font-mono">
<span className="inline-flex items-center gap-1.5">
<Link