From 35104769f9e1cf1056b9e9100b152d67582166c6 Mon Sep 17 00:00:00 2001 From: bkennedy Date: Tue, 14 Jul 2026 19:27:53 +0000 Subject: [PATCH] feat(palette): PROG-123 order move/arc pickers by outline rank with parent hints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The move picker listed focuses alphabetically with a constant 'Focus' hint, ignoring the manual order the owner curates on the Outline and leaving same-named focuses under different workspaces ambiguous. Focuses now group under their workspace in structure order (sortContainers for workspaces, byRankThenName within), the hint names the parent workspace, and the filter query matches the workspace name since it's on screen. The arc picker likewise sorts byRankThenName and hints the parent focus (the level one up); 'current' still wins on the current arc and 'No arc' stays pinned first. Supersedes PROG-83's alphabetical rule for these two pickers only — docs/decisions/PROG-123.md; REFERENCE picker-order notes updated. --- docs/REFERENCE.md | 11 +++++--- docs/decisions/PROG-123.md | 24 +++++++++++++++++ src/client/commands/CommandPalette.tsx | 36 +++++++++++++++++++------- 3 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 docs/decisions/PROG-123.md diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 9973a9d..34fd1e7 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -464,8 +464,10 @@ so old bookmarks keep working. the shared `sortContainers` (active first, archived last, rank-then-name; PROG-83) — the child lists on container pages. **Every other container/tag list is deterministic too** (PROG-83): pickers and selects (create dialogs, - palette arc/move/tag pickers, palette container quick-jump, filter - dropdowns per PROG-66) list alphabetically by name, tag chips on action + palette tag picker, palette container quick-jump, filter + dropdowns per PROG-66) list alphabetically by name — except the palette + **move/arc pickers**, which follow this same outline order and hint each row + with its parent container (PROG-123) — tag chips on action pages and board/Agenda cards sort alphabetically (shared `tagsByAction`, `src/client/tags.ts`), and Archive groups sort by name. Board **actions** keep pure `rank` order — never alphabetized. The scope @@ -649,7 +651,10 @@ so old bookmarks keep working. actions by key (retired alias keys included) or title and containers by name, and lists commands (create action/workspace/focus/arc, pickers for the current action). Picker modes are filterable lists; tag - toggles keep the palette open for multi-edit. + toggles keep the palette open for multi-edit. The move picker lists focuses + grouped by workspace in outline rank order with the parent workspace as the + row hint (a typed query matches it too); the arc picker lists the focus's + arcs in outline rank order, hinting the parent focus (PROG-123). - **Create dialogs** — action and container creation; parents/containers default from the current view (open container page, viewed action's container, or active board filters). New actions default to **Todo** so diff --git a/docs/decisions/PROG-123.md b/docs/decisions/PROG-123.md new file mode 100644 index 0000000..7d800fd --- /dev/null +++ b/docs/decisions/PROG-123.md @@ -0,0 +1,24 @@ +### PROG-123 — move/arc pickers follow outline order and name the parent + +The palette's move (focus) and arc pickers listed alphabetically per PROG-83's +"pickers list alphabetically" rule, with a generic "Focus" hint. That rule +predates the owner curating a manual order on the Outline/Structure pages; a +picker that ignores it fights the order the owner set, and a bare focus name +is ambiguous once the same name exists under two workspaces (e.g. "Admin"). + +Superseding PROG-83 **for these two pickers only**: + +- **Move picker** — focuses group under their workspace in structure order + (workspaces by `sortContainers`, focuses by `byRankThenName` within each), + and the row hint is the parent **workspace name** instead of the constant + "Focus". Since the workspace name is on screen, the filter query matches it + too — typing a workspace name lists that workspace's focuses. +- **Arc picker** — arcs sort `byRankThenName` instead of alphabetically, and + the hint names the parent **focus** (the level one up, per the action). All + rows share it — an arc picker is scoped to the action's focus — but it + confirms where the arc lives; "current" still wins on the current arc, and + "No arc" stays pinned first. + +Every other picker (tags, create dialogs, filter dropdowns, quick-jump) keeps +PROG-83's alphabetical order — nothing else displays rank, so alpha remains +the scannable choice there. diff --git a/src/client/commands/CommandPalette.tsx b/src/client/commands/CommandPalette.tsx index 95bb747..0f9e672 100644 --- a/src/client/commands/CommandPalette.tsx +++ b/src/client/commands/CommandPalette.tsx @@ -9,6 +9,7 @@ import { useLocation } from "wouter"; import { ACTION_ESTIMATES, ACTION_PRIORITIES, ACTION_STATUSES } from "../../shared/constants"; import type { WireAction, SnapshotPayload } from "../../shared/types"; import { sortByName } from "../boardFilters"; +import { byRankThenName, sortContainers } from "../containerReorder"; import { addDays, formatDueDate, relativeDue, todayISO } from "../dates"; import { PRIORITY_LABELS, STATUS_LABELS } from "../labels"; import { @@ -216,10 +217,14 @@ function buildItems( return items; } case "arc": { - // Alphabetical like every picker (PROG-83); "No arc" stays pinned first. - const focusArcs = sortByName( - ws.arcs.filter((a) => a.focusId === action.focusId && !a.archivedAt), - ); + // Outline order, not alphabetical (PROG-123 supersedes PROG-83 here): + // arcs list in the manual rank set on the outline/structure pages, and + // each row's hint names the parent focus one level up. "No arc" stays + // pinned first. + const parentFocus = ws.focuses.find((p) => p.id === action.focusId); + const focusArcs = ws.arcs + .filter((a) => a.focusId === action.focusId && !a.archivedAt) + .sort(byRankThenName); // "No arc" filters like any option, so a typed query can't leave it // sitting first and steal the Enter. return [ @@ -232,7 +237,7 @@ function buildItems( ...focusArcs.map((a) => ({ id: a.id, label: a.name, - hint: a.id === action.arcId ? "current" : undefined, + hint: a.id === action.arcId ? "current" : parentFocus?.name, run: () => void updateAction(action.id, { arcId: a.id }), })), ].filter((item) => matches(item.label)); @@ -240,14 +245,25 @@ function buildItems( case "move": { // A move now only changes the focus (PROG-102). Archived focuses aren't // valid destinations (D26); the action's current focus is excluded. - // Alphabetical (PROG-83). - return sortByName(ws.focuses.filter((p) => !p.archivedAt)) - .filter((focus) => focus.id !== action.focusId) - .filter((focus) => matches(focus.name)) + // Structure order, not alphabetical (PROG-123 supersedes PROG-83 here): + // focuses group under their workspace in the outline's manual rank, and + // the hint names the parent workspace. A typed query also matches the + // workspace name, since it's on screen. + const workspaceOrder = new Map(sortContainers(ws.workspaces).map((w, i) => [w.id, i])); + const workspaceName = (focus: { workspaceId: string }) => + ws.workspaces.find((w) => w.id === focus.workspaceId)?.name; + return ws.focuses + .filter((focus) => !focus.archivedAt && focus.id !== action.focusId) + .sort( + (a, b) => + (workspaceOrder.get(a.workspaceId) ?? 0) - (workspaceOrder.get(b.workspaceId) ?? 0) || + byRankThenName(a, b), + ) + .filter((focus) => matches(focus.name) || matches(workspaceName(focus) ?? "")) .map((focus) => ({ id: focus.id, label: focus.name, - hint: "Focus", + hint: workspaceName(focus) ?? "Focus", run: () => moveAction(action.id, { focusId: focus.id }), })); }