Build compact TargetSelector component

Task 5607 · Done · Coder · created · updated

## Original requirement (verbatim, epic body) > When creating a cron, allo user to pick a different type of target. Currently the target is a project. Other types: node, none (just run a task without a project - we have that impelemented), what else? > > When node is a target it runs a task on all selected nodes. > > Implement a nice target selector, that allows to quickly add all, remove all, do all except specific, etc. It has to be a very compact and robust control. It must work for both projects, nodes and other targets. > > Make sure this is naturally working and improves previous work - triggers, crons, etc. All of these things (triggers, webhooks, crons, etc.) are combined into a unified coherent system that is state of the art and uses best practices (do a web research). Settled upstream (BA Task 5599 PRD decisions D1-D10; Architect Task 5600 design O1-O5). Do not reopen them: D1 no "None" target; D2 node target = one task per resolved node, pinned, in a node-level non-repo workspace; D3 selections are LIVE rules resolved at each firing; D4 crons + triggers merge into one Automation (When -> Target -> Action); D5 system crons merge too, operator-only with a System badge; D6 selector modes exactly All / None selected / Specific / All except; D7 Task action REUSES the task-create UI; D8 action registry (Task + System function), System function takes no target; D9 migrate in place; D10 overlap per target. O1 system "Nodes" project visible to org admin/owner (and operators) only; O2 no fan-out cap; O3 Task action adds Attachments + optional Assignee, excludes Dependencies and From Jira; O4 node tasks unprivileged; O5 fix dev docs reach at the root cause. Epic chain (this breakdown): C2 Node targets API -> C1 Automation core API -> C3 Fire engine API; C4 TargetSelector web; C5 TaskSpecFields web; then C6 Automations UI (needs C1, C3, C4, C5) alongside C8 dev docs reach infra; then C7 reader docs (needs C6, C8). ## This child: C4 TargetSelector (web) One adapter-driven selector for Projects, Nodes and any future target type (architecture sections 6.5, 7.1). Owns AC3.2, AC3.4, AC3.5, AC3.7, AC3.8 (component/screenshot level); contributes AC3.1. Built against pinned contracts with a mocked roster - no dependency on the API children. Acceptance carried verbatim from the PRD: - **AC3.1 One component.** A single selector component serves Projects, Nodes and any future target type through a data adapter, and both the schedule and event editors use it. The trigger and cron editors have no per-surface copies. - **AC3.2 Modes.** It offers exactly All / Specific / All except, plus the empty state (D6), and gives one-action shortcuts: **Select all** (switches to All), **Clear** (empty), and **Except…** (switches to All except, then pick exclusions). *Verify:* on dev, reach each mode in ≤ 2 clicks from any other and observe it. A component test covers every transition. - **AC3.4 Compact.** Collapsed, the control is **one line** that summarises the selection in words a person reads: "All projects (12)", "All nodes except node-a, node-b", "3 projects: api, web, docs", "+N more" past what fits. It expands to a searchable list with checkboxes. *Verify:* screenshot at 1280 px and 360 px. Each summary is one line, readable, with no horizontal page scroll. - **AC3.5 Legibility (quality bar).** Item names are never shredded. Every name visible in the summary or the list is shown whole, or truncated with an ellipsis that keeps at least about 12 characters and shows the full name on hover/focus. A name cut to 1–4 characters fails. *Verify:* on dev, add a project with a 40-character name, then select it alongside 5 others at 360 px. The name stays identifiable and its tooltip shows it in full. - **AC3.7 Robust.** Keyboard operable: type to filter, arrows, Space to toggle, Enter to confirm, Esc to close. The control has accessible names. A stored ID that this org can no longer reach, or that no longer exists, renders as an error-coloured, named chip and is never silently dropped (current `ProjectsField` behaviour, kept). Loading, failed and empty rosters each state which one they are. *Verify:* component tests for each of these states, plus a keyboard-only pass on dev. - **AC3.8 Same control, any target.** The control behaves identically for Projects and Nodes: same modes, summary style and states. *Verify:* run AC3.2–AC3.7 for both types. - **AC1.2** Switching the type clears the previous selection, and Save stays blocked until the new type has a valid selection. The editor states this inline and does not silently keep the old IDs. (this component: switching type resets to {kind:new, mode:'specific', ids:[]} with an inline reason.) (The "on dev" halves of these are run by C6 once the selector is wired into the editor.) ### How (settled design, implement exactly) - New `apps/web/src/ui/TargetSelector.tsx` exporting exactly: `type TargetKind = 'projects'|'nodes'`, `type TargetMode = 'all'|'specific'|'all_except'`, `type TargetRule = {kind, mode, ids: Array<number|string>}` (ids [] for all); `TargetItem {id, name, state:'ok'|'disabled'|'offline', detail?}`; `TargetAdapter {kind, noun:{one,many}, useRoster(orgId) -> {state:'loading'|'error'|'ready', items, error?, retry()}}`; `TargetPreview {resolved, resolvedCount, universeCount, dangling:[{id,label}]}`; `TargetSelectorProps {adapter, orgId, value, onChange, preview?, disabled?, label?}`; `projectsAdapter`, `nodesAdapter` (roster from `GET /api/automations/targets/{kind}?org_id=` -> `{items, total}`; mock it in tests); `summarizeTarget(rule, items, noun)`. - **Collapsed:** one row, field width, one line. Summary grammar as AC3.4. At least one name shown WHOLE whenever >=1 name is in the summary; add further names only while each still fits with >=12 visible characters, else fold into "+N more"; the count is never truncated; full names in a tooltip on hover/focus. **Never shrink the font, never clip without an ellipsis.** Right side: resolved chip ("11 of 12") and a chevron. - **Expanded:** MUI `Popover` (full-screen `Dialog` below 600 px): mode segmented control All · Specific · All except; search (autofocus); shortcuts Select all (-> All), Clear (-> Specific []), Except… (-> All except [], list focused); checkbox list `role="listbox" aria-multiselectable` (arrows, Space toggles, Enter confirms, Esc closes), virtualised beyond 200 items; footer "Will run on N of M projects" + "Show list" disclosure rendering `preview.resolved`. - Mode semantics: in All, unticking an item -> All except [item]; in All except, re-ticking the last exclusion -> All; in Specific, ticking every item stays Specific with hint "Use All to include new ones"; every mode <=2 clicks from every other. - States: loading / failed (Retry) / empty ("No nodes in this org") each say which they are. Dangling ids (`preview.dangling[].label`, "(removed)") render as error-coloured named chips at the top of the list and in the summary, removed only explicitly. Nodes show disabled/offline as secondary labels, stay selectable; summary notes "1 disabled (skipped)". - Follow `docs/standards/ui-copy-standards.md` for every label. Credentials & reach: none - component tests and screenshots run in the worktree (architecture section 12: verified). No dev oracle in this child; C6 runs the dev-browser oracles. Out of scope: Do NOT wire the selector into any page, and do NOT delete `ProjectsField` or the trigger `<select>` (C6 does both). Do NOT build API endpoints (C1). Do NOT add group / tag / node-group / online-only rules or any mode beyond All / Specific / All except + empty. Epic out of scope (PRD section 5, binds every child): No "None" target type for tasks and no projectless-task feature exposed to users (D1). No group, tag, node-group, cloud/self-hosted or online-only selector rules (D6). No new action types beyond Task and System function: no Slack, outbound webhook, email or playbook-start actions (D8). Do not change the webhook ingress URL scheme, existing secrets, signature verification or the dedup rules (D9, AC7.3). Do not change the task-create form's own behaviour to fit automations; any extension must leave regular task creation unchanged (D7). Do not change task claim/placement for non-automation tasks (D2). Do not alter what any system activity does when it runs (D5). Do not change Automation access roles (AC4.7). Do not rename or remove the agent verbs (AC5.5). Playbooks are untouched apart from their tab position. No feature flag and no "ships disabled". Deferred, NOT built here (architecture section 9): F-contract; node_projects hygiene; elevated (root) node tasks; Dependencies and From Jira in automation Task actions; pause-on-failure; a catch-up window. Done means: - AC3.2: a component test drives every mode transition (each mode reached in <=2 clicks from every other; Select all / Clear / Except… shortcuts; All untick -> All except [item]; last exclusion re-ticked -> All; Specific all-ticked stays Specific with the hint) - oracle: vitest component tests. - AC3.4 + AC3.5 (quality): screenshots at 1280 px and 360 px of each summary form show ONE line, readable, no horizontal scroll; at 360 px with "customer-portal-production-eu-1" and "customer-portal-production-eu-2" selected (plus a 40-character name alongside 5 others) the summary shows at least one whole name and "+N more", never two identical stubs and never a name cut below ~12 characters; tooltip shows the full names - oracle: Playwright/component screenshots attached to your report plus a test asserting the summary text. A summary that is one line but unreadable FAILS. - AC3.7: keyboard-only test (type to filter, arrows, Space, Enter, Esc), accessible names present, loading / failed+Retry / empty states each named, dangling id renders as a named error chip and is never dropped from `value` - oracle: component tests. - AC3.8: the same test suite runs for `projectsAdapter` and `nodesAdapter` - oracle: parametrised tests. - AC1.2 (component): switching kind resets to specific [] with an inline reason - oracle: component test. - `./dc check full` green; PR merged to develop; Jaah Deploy for the merge commit succeeds (fix and re-merge if not).

Outcome

Built TargetSelector (48 tests, both adapters; 360/1280 visual check OK). PR #4921 merged, Jaah Deploy green. Also fixed develop's ambiguous Dark-radio e2e. Full gate green apart from an unrelated smoke flake that passes when run alone.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)