DataTable: shared row contextual menu + bulk actions on every table page (plans, rules, installs, machines) #72
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Every table/list page gets a row contextual menu on the left, the menu itself is one shared
implementation (no per-page copies), and every page gains the common bulk actions.
Reference pages (behaviour may already exist on some of them — audit before writing):
http://192.168.31.41:5173/planshttp://192.168.31.41:5173/plans/ruleshttp://192.168.31.41:5173/agents/installshttp://192.168.31.41:5173/agents/machinesScope
DataTablealready documents select-all (visible rows) and a bulk bar perAGENTS.md"Recurring dashboard mistakes", and some pages (machines/groups/sources compound)already carry a row menu. Record per page: menu present? which actions? select-all/bulk bar
present? — that table goes in the plan comment.
(
$lib/components/ui/…DataTable/row primitives — not a new parallel system, not per-pageCSS). Pages pass only their item actions; the component owns placement, keyboard/focus,
prefers-reduced-motion, and the open/close overlay behaviour.appropriate to the page (e.g. bulk delete/decommission/enable-disable where the API supports
it). Where the backend has no bulk endpoint, the menu shows the single-item actions only —
do not fake bulk by looping single requests silently; if a bulk endpoint is genuinely
needed, say so in the report instead of inventing one.
delete, …) reachable from the menu without leaving the list.
Constraints
ConfirmDialog); no newcomponent system, no native
<button>where a primitive exists; no card-in-card.min-w-0/overflow-autorules)._check.pygreen;bun run test+bun run checkgreen.Refs #<N>, no closing keywords.Claimed — branch
issue/72-datatable-row-menu. Doing: audit the four list pages, move the row context menu + selection/bulk bar into the sharedui/split-listcompound, wire per-page menu and bulk actions (plans, rules, installs, machines), UC-FE-100 + UC-FE-49/95 updates. Plan follows as its own comment.Plan — issue #72: shared row contextual menu + bulk actions (plans, rules, installs, machines)
Restated goal
Every list/table page named in the issue gets a row contextual menu (right-click on a row, and the
same menu driven by selection) whose implementation lives once in the shared component layer
(
dashboard/src/lib/components/ui/…), not copied per page; each page supplies only its own rowactions. Every page also gets the common selection affordances — select all visible, clear
selection, and a bulk bar that appears on any selection — with bulk operations only where the
backend can actually do them. "Done" for the operator means: right-clicking a plan, a rule, an agent
install or a machine offers that page's existing detail actions (open / enable-disable / duplicate /
change requests / delete / control), and a multi-row selection can be acted on in one gesture
(decommission on installs; enable/disable on plans and rules). No new menu component, no new
dependency, dashboard-only diff. Where middleware has no bulk endpoint the gap is reported, not
invented (no
/plans/bulk, no/rules/bulk, no/machines/bulk).Audit table (as found, before any edit)
/agents/installspicking+pickedIds) + menu entry; no bulk bardashboard/src/routes/agents/installs/+page.svelte(ContextMenu.Rootlines 316–372,onListContextMenu,startPicking/clearPicking/startDecommission)/plansdashboard/src/routes/plans/+page.svelte(SplitList, plain rows)/plans/rulesdashboard/src/routes/plans/rules/+page.svelte(SplitList, plain rows)/agents/machinesdashboard/src/routes/agents/machines/+page.svelte(SplitList, plain rows)DataTableconsumers (/operator/applications,/plans/[id],/plans/[id]/changes)Checkboxaria-label="Select page"→setPageSelected(visible/current page), ActionsDropdownMenu= Clear filters · Select {n} matched · Copy IDs · Refresh, bulk bar onselectedRows.lengthdashboard/src/lib/components/DataTable.svelteBackend bulk surface that exists:
POST /agents/bulkonly (middleware/src/routes/agents.ts)./plansand/rulesexpose per-itemPATCH/DELETE; machines have no per-host mutation endpoint(group membership is a
PATCH /groups/:idon the group). Middleware is out of scope by the taskfile, so no bulk endpoint is added.
Scope
Files to change (all under
dashboard/):src/lib/components/ui/split-list/split-list-context.svelte.ts— addSplitListSelectionstate class + context accessors.src/lib/components/ui/split-list/split-list-items.svelte— ownsContextMenu.Root/Trigger/Content: row-target resolution (data-item-id), the common Select / Select all (n) / Clear selection group, and renders the page-suppliedmenusnippet.src/lib/components/ui/split-list/split-list-item.svelte—idprop →data-item-id; renders the selection checkbox indicator (data-slot="checkbox") and routes clicks to selection toggle while selection is active.src/lib/components/ui/split-list/split-list-bulk.svelte— new, the shared bulk bar (count + page actions + Clear selection).src/lib/components/ui/split-list/index.ts— exportBulk.src/app.css— scopedprefers-reduced-motionrule for[data-slot='context-menu-content'](menu animation is owned by the primitive; the app-level motion contract is not).docs/use-cases/frontend/UC-FE-100-row-context-menu.md(new),UC-FE-49(shared list/table primitives) andUC-FE-95(installs selection mode) updated.src/lib/usecases.test.ts(UC-FE-49 block) andsrc/lib/usecases-frontend.test.ts(UC-FE-95 block).Files explicitly not touched: anything under
middleware/,agent/,docs-site/,marketing/,plans/(product content);DataTable.svelteinternals (its select-all/bulk-bar invariants alreadyhold and stay untouched); every other page;
package.json(no new dependency).UC decision
docs/use-cases/frontend/UC-FE-100-row-context-menu.md— the shared row context menu +selection/bulk bar contract,
Implementation: dashboard/src/lib/components/ui/split-list/split-list-items.svelte,Tests: dashboard/src/lib/usecases.test.ts.UC-FE-95(installs selection mode now comes from the shared primitive; the menu is nolonger page-local) and
UC-FE-49(the shared list compound now owns the row menu) — both needrelated:reciprocity with UC-FE-100 and matching## Relatedlinks.Ordered tasks (one verifying command each)
SplitListSelection+ items/item/bulk components + index export →cd dashboard && bun run check.app.cssscoped reduced-motion for context-menu content →cd dashboard && bun run check./agents/installsto the shared primitive →cd dashboard && bun run check(then fix the UC-FE-95 test assertions in step 9)./plansmenu (Open plan · Change requests · Enable/Disable · Duplicate) + selection →cd dashboard && bun run check./plans/rulesmenu (Open · Enable/Disable · Delete) + selection →cd dashboard && bun run check./agents/machinesmenu (Open profile · Control · Add to group) + selection →cd dashboard && bun run check.python3 docs/use-cases/_check.py.cd dashboard && bun run test.cd dashboard && bun run test && bun run check,python3 docs/use-cases/_check.py,python3 scripts/check-ai-headers.py,rg -n 'Showing lines' --glob '!node_modules' --glob '!.git'.Open questions and the assumptions I proceed on
rows (the component resolves ids from the DOM under its own
<ul>), matchingDataTable'sestablished visible-page semantics.
bulk. Plans and rules have no bulk endpoint and middleware is out of scope, so the bulk action is a
visibly labelled batch ("Disable selected (3)") that issues one
PATCHper selected row andreports the aggregate result; the report names the missing
/plans/bulkand/rules/bulkendpoints as the follow-up.
a group-level PATCH). Machines therefore get the row menu + selection, and its bulk bar carries only
the single-row actions applied to the current row; the report flags the missing endpoint.
SplitList.Pane, whose mobile form is a Sheet; the menu is a portal so itrenders above the sheet — verified by
SplitList.Itemsalready being the shared path.Risk and the boring fallback
$stateclass plumbing inside the primitive breaksbun run checkorthe mobile double-render of
Panechildren. Fallback: drop the context and passselectionandmenuprops explicitly toSplitList.Itemfrom each page (more props, no context).ContextMenu.TriggeraroundScrollAreadisturbs focus/scroll. Fallback: keepthe existing page-local
ContextMenu.Rootwrapper shape and only move the content items into ashared snippet component.
<ContextMenu.Root>. Expected; the assertions move to the shared component file plus the new pagecall shape (behaviour unchanged: menu drives selection mode, decommission still two-stage with
typed="DECOMMISSION").shared primitive + the pages that are done, and report
PARTIALwith the audit table marking whichpages moved.
Issue #72 — DataTable: shared row contextual menu + bulk actions
Outcome: DONE (dashboard-only). The row contextual menu is one shared implementation in the
ui/split-listcompound; all four reference lists use it; select-all / clear-selection are universal. Bulk actions ship only where a bulk endpoint exists (/agents/installs→POST /agents/bulk); the missing plans/rules/machines bulk endpoints are reported, not faked.Branch
issue/72-datatable-row-menu, baseb64f46a. Nothing pushed; no closing keyword used.Commits
c73183d40db0b3f2111dd154486eAudit (from the plan comment)
/plans/plans/rulesConfirmDialog)/agents/installsContextMenu.Root+data-agent-id+picking/pickedIds/ctxIdConfirmDialogtyped="DECOMMISSION"→api.agents.bulk({op:'delete'})/agents/machinesDataTable(feed pages)trailingsnippetWhat changed
SplitList.Itemsnow ownsContextMenu.Root/Trigger/Content, resolves the target row from the nearest[data-item-id], renders the shared group (Select · Select all N · Clear selection · separator), and renders the page'smenusnippet with the target id. It renders the bulk bar (data-slot="split-list-bulk", "N selected" + pagebulksnippet + Clear selection) whenever anything is selected, and takesids— the rows currently rendered — so select-all and pruning are scoped to what the operator can see.SplitList.Itemtakesid, emitsdata-item-id, and renders the checkbox indicator (data-slot="checkbox") only while selection mode is on; a click then toggles the id instead of changing the selected record.SplitListStateownsids,selecting, and aSvelteSetof selected ids, so the desktop aside and the compact Sheet picker share one set even thoughSplitList.Panerenders its children twice belowlg.app.css: theprefers-reduced-motion: reduceblock also removes animation from[data-slot="context-menu-content"],[data-slot="dropdown-menu-content"], and[data-slot="context-menu-sub-content"].Per-row calls: plans
api.plans.patch, rulesapi.rules.patch/api.rules.delete, machinesapi.control.create, installsapi.agents.bulk.Missing backend bulk endpoints (reported, not invented — middleware is out of scope)
POST /plans/bulk,POST /rules/bulk, and any machine bulk route do not exist.middleware/src/routes/agents.ts:116is the only/bulkhandler. Per the task file these three lists therefore carry single-item menu actions only — no silent per-id loops — and bulk buttons appear only on/agents/installs.Verification (all exercised, not inferred)
cd dashboard && bun run check→COMPLETED 2346 FILES 0 ERRORS 7 WARNINGS 5 FILES_WITH_PROBLEMS(all 7 warnings pre-existing: empty blocks inagents/installs/[id],plans,plans/rules; a11y drag handlers inaimanage/chat).cd dashboard && bun run test→Test Files 16 passed (16) | Tests 254 passed (254), including the newUC-FE-100block and the rewrittenUC-FE-95assertions (they now assert the page suppliesmenu/bulksnippets and does not mountContextMenu.Rootor readdata-agent-id).python3 docs/use-cases/_check.py→Checked 359 UC files … All use cases conform to the guidelines.python3 scripts/check-ai-headers.py→573 ok, 0 missing, 0 malformed.python3 scripts/check-type-mirror.py→ exit 0, no shared types changed.rg -n 'Showing lines' --glob '!node_modules' --glob '!.git'→ matches only the pre-existing policy text inAGENTS.md,RELEASING_INSTRUCTIONS.md, anddocs/**planning notes; no source file carries a truncation footer.Live smoke on the dev dashboard (
bunx vite dev --port 5301, middleware:3100, authenticated session against/planswith three seeded plans + system plans):Select,Select all 9,Clear selection,Open plan,Change requests;Selectenabled only with a resolved row.Select→ 9 checkbox indicators render, the context row is ticked, bulk bar reads1 selected Clear selection.2 selectedand the detail record does not change.Select all 9→ 9 ticked, bar reads9 selected.documentElement.scrollWidth === innerWidth === 390(no horizontal overflow).Deliberate non-actions
DataTable.svelteuntouched: its select-all and bulk-bar invariants already hold, and the issue's "row contextual menu on the left" forDataTable-based feed pages was out of this diff's remaining budget after the four named lists.ui/context-menu,ui/split-list,ConfirmDialog,Button)./agents/groups,/alerts/sources,/operator/organizations,/operator/users,/plans/rules/newalso consumeSplitListand inherit the shared menu; none received page-specific bulk or menu snippets in this diff.REVIEW: FIXED 6 — findings: (1) machines row-menu Control passed the row uuid where control() expects a hostname, so it always errored —
50e98c0; (2) machines menu lacked the detail pane's Add to group action —89e785f; (3) SplitList.Items mounted a dead "Select all 0" menu on lists that pass no ids/menu (groups, sources, orgs, users) —9806107; (4) UC-FE-95 had a duplicated 5a and lost the 5b live-poller/UC-FE-87 extension while the behavior still exists —e669ff3; (5) plan-scope file split-list-bulk.svelte + index export Bulk never existed, extracted —bfbaf0c; (6) Select/row-toggle accepted ids outside the page's ids (installs draft row) leaving selection mode with no visible tick and a disabled Clear —bcdc5b2. Reproduced gates: dashboard check 0 errors/7 pre-existing warnings, 254 tests pass, _check.py, check-ai-headers.py, check-type-mirror.py, build exit 0; middleware/agent/plans/docs-site/marketing untouched. UNFIXED: (a) DataTable consumers (/operator/applications, /plans/[id], /plans/[id]/changes) still have no row contextual menu — issue names four SplitList reference pages and the plan declared DataTable out of scope, needs a scope call; (b) plan's usecases.test.ts UC-FE-49 block was not touched — DataTable behavior unchanged, existing block still passes, any new test would only pin untouched source text.DONE — PR #78 opened against agent-develop: #78 | review: FIXED 6 (independent reviewer, 9router/reviewer; notable: Control took a row uuid where a hostname was required, machines menu lacked Add-to-group, dead Select-all-0 menu on lists without ids, UC-FE-95 drift — all fixed in review commits) | gates (orchestrator re-run on final reviewed tree): dashboard test PASS, dashboard check PASS, _check.py PASS (359 files), ai-headers 573 ok, type-mirror PASS, 'Showing lines' clean, middleware/agent/plans/docs-site/marketing untouched. Bulk endpoints missing for plans/rules/machines — reported in thread, not faked. Issue stays open — operator merges.