Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Meditor/app/channels/components/ChannelsTable.tsx | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
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