diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 15cd070..dfda858 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 @@ -653,7 +655,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 }), })); }