#91b — Priorisierte Liste — 2D-Karten-Ansicht mit Drag-to-Reposition #91

Closed
opened 2026-08-18 13:13:51 +02:00 by lena · 1 comment
lena commented 2026-08-18 13:13:51 +02:00 (Migrated from git.butzei.de)

Story: Priorisierte Liste — 2D-Karten-Ansicht mit Drag-to-Reposition

Folge-Story zu #91. #91's V1 lieferte die klassische Listenansicht mit Formel-Sortierung + einem
disabled-"Karte (kommt bald)"-Toggle-Placeholder. Diese Story tauscht den Placeholder gegen die
tatsächliche interaktive 2D-Ansicht aus.

As a Nutzer einer Priorisierten Liste,
I want to meine Todos in einer echten 2D-Karte (Achsen Dringend x / Wichtig y) sehen und einen
bestehenden Punkt direkt auf der Karte ziehen, um seine Urgency/Importance nachträglich zu ändern,
so that ich die Priorisierung visuell und mit einer einzigen Geste anpassen kann, statt jeden
Todo einzeln über das Ändern-Modal aufzurufen.

Voraussetzungen (bereits durch #91 V1 vorhanden — keine Backend-Arbeit nötig):

  • TodoListType.Priority-Diskriminator + PriorityMatrixFactor an TodoListEntity.
  • TodoEntity.Urgency/Importance (nullable, 1–100).
  • SetTodoUrgencyImportanceCommand-Handler mit Type=Priority-Gate.
  • PriorityMatrixPage mit Ansichts-Toggle "Liste / Karte (kommt bald)" — der Karten-Button ist
    bereits im DOM und muss nur enabled + der Panel-Inhalt gerendert werden.

Acceptance criteria:

  • "Karte"-Toggle-Button in PriorityMatrixPage ist enabled; der Klick tauscht den Listen-Panel
    gegen einen 2D-Scatter-Panel (Achsen Dringend x=1–100 / Wichtig y=1–100).
  • Jedes aktive (nicht-erledigte) Todo wird als Punkt/Kärtchen an seiner
    Urgency/Importance-Position gerendert, mit sichtbarem Titel bei Hover/Focus.
  • Ein bestehender Punkt lässt sich per Drag (Maus + Touch) auf der Karte verschieben; beim
    Loslassen wird SetTodoUrgencyImportanceCommand mit den neuen Achsen-Werten aufgerufen und
    die Kartenposition bestätigt sich anhand der DTO-Antwort.
  • Achsen-Clamping: eine Drag-Bewegung außerhalb 1..100 wird auf den nächsten gültigen Rand
    geklemmt (kein 400 vom Vogen-Validator, kein Punkt außerhalb der Karte).
  • Label-Filter bleibt in der Karten-Ansicht aktiv (dieselbe Chip-Auswahl wie in der Listen-
    Ansicht filtert die sichtbaren Punkte).
  • Erledigte Todos werden in der Karte visuell abgesetzt (z. B. ausgegraut) oder komplett
    ausgeblendet — analog zur Sortierung-ans-Ende in der Listen-Ansicht.
  • Screen-Reader-Alternative: die "Ändern"-Buttons in der Listen-Ansicht bleiben unverändert,
    sodass ein Nutzer der die Karte per Zeigegerät nicht bedienen kann, weiterhin über die
    Listen-Ansicht + das bestehende Modal (aus #91 V1) editieren kann.

Out of scope for this story:

  • Änderungen am Datenmodell, Command-Handler oder Sortier-Formel.
  • Multi-Select-Drag (mehrere Todos gleichzeitig verschieben).
  • Undo für einen versehentlichen Drag (jede einzelne Drag-Bewegung ist eine reguläre
    Command-Ausführung wie jede andere; Undo ist ein eigenständiges Cross-cutting-Thema).

Hinweis zur Priorität: Could — wie #91 selbst. Sinnvoll aufzunehmen, sobald V1 in echter
Nutzung ist und der genaue Bedarf klar wird (z. B. "wird die Karte auf Mobile überhaupt praktisch
genutzt?"). #91's Design-Doc hält die Rationale für den Deferral fest.

# Story: Priorisierte Liste — 2D-Karten-Ansicht mit Drag-to-Reposition **Folge-Story zu `#91`.** `#91`'s V1 lieferte die klassische Listenansicht mit Formel-Sortierung + einem disabled-"Karte (kommt bald)"-Toggle-Placeholder. Diese Story tauscht den Placeholder gegen die tatsächliche interaktive 2D-Ansicht aus. **As a** Nutzer einer Priorisierten Liste, **I want to** meine Todos in einer echten 2D-Karte (Achsen Dringend x / Wichtig y) sehen und einen bestehenden Punkt direkt auf der Karte ziehen, um seine Urgency/Importance nachträglich zu ändern, **so that** ich die Priorisierung visuell und mit einer einzigen Geste anpassen kann, statt jeden Todo einzeln über das Ändern-Modal aufzurufen. **Voraussetzungen (bereits durch `#91` V1 vorhanden — keine Backend-Arbeit nötig):** - `TodoListType.Priority`-Diskriminator + `PriorityMatrixFactor` an `TodoListEntity`. - `TodoEntity.Urgency`/`Importance` (nullable, 1–100). - `SetTodoUrgencyImportanceCommand`-Handler mit Type=Priority-Gate. - `PriorityMatrixPage` mit Ansichts-Toggle "Liste / Karte (kommt bald)" — der Karten-Button ist bereits im DOM und muss nur enabled + der Panel-Inhalt gerendert werden. **Acceptance criteria:** - [x] "Karte"-Toggle-Button in `PriorityMatrixPage` ist enabled; der Klick tauscht den Listen-Panel gegen einen 2D-Scatter-Panel (Achsen Dringend x=1–100 / Wichtig y=1–100). - [x] Jedes aktive (nicht-erledigte) Todo wird als Punkt/Kärtchen an seiner Urgency/Importance-Position gerendert, mit sichtbarem Titel bei Hover/Focus. - [x] Ein bestehender Punkt lässt sich per Drag (Maus + Touch) auf der Karte verschieben; beim Loslassen wird `SetTodoUrgencyImportanceCommand` mit den neuen Achsen-Werten aufgerufen und die Kartenposition bestätigt sich anhand der DTO-Antwort. - [x] Achsen-Clamping: eine Drag-Bewegung außerhalb 1..100 wird auf den nächsten gültigen Rand geklemmt (kein 400 vom Vogen-Validator, kein Punkt außerhalb der Karte). - [x] Label-Filter bleibt in der Karten-Ansicht aktiv (dieselbe Chip-Auswahl wie in der Listen- Ansicht filtert die sichtbaren Punkte). - [x] Erledigte Todos werden in der Karte visuell abgesetzt (z. B. ausgegraut) oder komplett ausgeblendet — analog zur Sortierung-ans-Ende in der Listen-Ansicht. - [x] Screen-Reader-Alternative: die "Ändern"-Buttons in der Listen-Ansicht bleiben unverändert, sodass ein Nutzer der die Karte per Zeigegerät nicht bedienen kann, weiterhin über die Listen-Ansicht + das bestehende Modal (aus `#91` V1) editieren kann. **Out of scope for this story:** - Änderungen am Datenmodell, Command-Handler oder Sortier-Formel. - Multi-Select-Drag (mehrere Todos gleichzeitig verschieben). - Undo für einen versehentlichen Drag (jede einzelne Drag-Bewegung ist eine reguläre Command-Ausführung wie jede andere; Undo ist ein eigenständiges Cross-cutting-Thema). **Hinweis zur Priorität:** Could — wie `#91` selbst. Sinnvoll aufzunehmen, sobald V1 in echter Nutzung ist und der genaue Bedarf klar wird (z. B. "wird die Karte auf Mobile überhaupt praktisch genutzt?"). `#91`'s Design-Doc hält die Rationale für den Deferral fest.
lena commented 2026-08-18 13:13:51 +02:00 (Migrated from git.butzei.de)

design (91b_priority_matrix_2d_view_design.md)

Design: #91b — Priorisierte Liste, 2D-Karten-Ansicht mit Drag-to-Reposition

Context

#91 V1 shipped the Priority Matrix list type with a formula-sorted classic list view and a
disabled "Karte (kommt bald)" toggle, deliberately deferring the full 2D scatter/drag view to this
follow-up story. No backend, data-model, or command-handler changes are needed — TodoListType,
TodoUrgency/TodoImportance, and SetTodoUrgencyImportanceCommand (with its Type=Priority gate)
already exist and are already tested. This is a frontend-only story.

Approach

New PriorityMatrixCanvas.tsx: a plain SVG scatter plot, viewBox="0 0 100 100", mapping directly
onto the 1–100 Urgency (x) / Importance (y) range from the AC. Importance is flipped
(cy = 100 - importance) so "more important" renders higher, matching the existing mini-card's
bottom-origin convention in CreatePriorityTodoDialog.tsx (kept consistent rather than
reinventing the axis direction for the full-size view).

PriorityMatrixPage.tsx gained a view: 'list' | 'map' state; the previously-disabled "Karte"
button now toggles it, and the map panel renders PriorityMatrixCanvas with one point per todo
currently visible in filteredSorted (so the label filter chip stays live in both views, per AC).
Done todos render dimmed (opacity: 0.35 vs 0.9) rather than being unconditionally excluded —
in practice they are usually already absent, since PriorityMatrixPage reuses the same
getTodosOfSelectedTodoList store selector the Standard list page uses, which defaults
filterStatus to 'Open'. The dimming only becomes visible if a user has widened that shared
filter to 'All'/'Done' from the Standard list's own filter bar before navigating here — the AC
allows either "dimmed" or "fully hidden," and this satisfies both readings without adding a
second, Priority-page-local filter control.

Dragging: onPointerDown on a circle calls setPointerCapture and records the dragged id;
onPointerMove/onPointerUp on the <svg> compute the new axis values from the pointer's
position relative to the SVG's bounding rect, clamped to 1..100 client-side before the drop is
reported. This clamp is not cosmetic: TodoUrgency/TodoImportance's Vogen validators reject
out-of-range values with a 400 rather than clamping them, and the AC explicitly requires "kein 400
vom Vogen-Validator, kein Punkt außerhalb der Karte" — so an out-of-bounds drag must never reach
the wire as an out-of-bounds value. On release, PriorityMatrixPage.handlePointDrag calls
SetTodoUrgencyImportanceCommand (the same command the "Ändern" modal already uses) and applies
the returned TodoDto to the store directly via updateTodo, rather than waiting on the WS
broadcast round-trip for the dragged point to visually settle.

Screen-reader alternative (AC's last bullet): unchanged — the classic list view's "Ändern" button
and modal from V1 remain the only editing path required, so a user who can't perform a pointer
drag still has full access to the same Urgency/Importance/label editing via that route.

Concurrent-session conflict note

This story exists because of a real merge conflict: an earlier session in parallel had already
implemented Feature #91 as TodoListType-based (this design) while a second, independent local
session simultaneously implemented an incompatible schema (TodoListEntity.PriorityMatrixFactor-
as-sole-switch, no Type column, SetTodoPriorityMatrixPositionCommand) and built the full 2D
drag view directly into #91 rather than deferring it. Both were pushed/committed around the same
time. Rather than force-overwrite either side's tested work, the TodoListType-based V1 (already
on origin/master, already covered by its own backend+frontend+E2E tests) was kept as the single
source of truth, and the other session's already-built 2D-drag UI was re-derived against this
schema as this story — the deferred #91b the V1 author had already drafted for exactly this
scope. See the git history around 2026-08-09 for the two divergent commits if reconstructing this
is ever needed; the abandoned local attempt is preserved on the backup-91-standalone-attempt
branch, not merged.

Testing

  • PriorityMatrixCanvas.test.tsx (new): point rendering/positioning, dimmed-opacity rendering,
    drag-to-report with correct axis math, clamping on out-of-bounds drag, no-op when no
    onPointDrag handler is given.
  • PriorityMatrixPage.test.tsx: replaced the now-obsolete "disabled Karte button" test with
    coverage for switching to the map view, dimmed rendering of a done todo (with the shared
    filterStatus widened to 'All' first, matching the real precondition), the label filter
    applying to the map, and a full drag round-trip asserting the exact
    SetTodoUrgencyImportanceCommand payload.

Verification

  • dotnet build/dotnet test (Release): 748 tests green, no backend files touched by this story.
  • npx tsc -b, npm run build, npx vitest run: clean; 845 frontend tests green (9 new — 6 in
    PriorityMatrixCanvas.test.tsx, net +3 replacing the removed placeholder test in
    PriorityMatrixPage.test.tsx).
  • Live-verified end-to-end against the dev Postgres/Redis stack: created a real Priority list and
    two todos via direct API calls (UI drawer clicks were unreliable in this sandbox's headless
    browser pane — see the Team Coach memory note this cycle), switched to the map view, dragged a
    point via a timed pointer-event sequence (see the Learned Pattern on synthetic-event timing),
    confirmed the position updated live and persisted across a full page reload, and confirmed the
    classic list view re-sorts correctly after the drag. Cleaned up the verification list afterward.
**design** (`91b_priority_matrix_2d_view_design.md`) # Design: `#91b` — Priorisierte Liste, 2D-Karten-Ansicht mit Drag-to-Reposition ## Context `#91` V1 shipped the Priority Matrix list type with a formula-sorted classic list view and a disabled "Karte (kommt bald)" toggle, deliberately deferring the full 2D scatter/drag view to this follow-up story. No backend, data-model, or command-handler changes are needed — `TodoListType`, `TodoUrgency`/`TodoImportance`, and `SetTodoUrgencyImportanceCommand` (with its Type=Priority gate) already exist and are already tested. This is a frontend-only story. ## Approach New `PriorityMatrixCanvas.tsx`: a plain SVG scatter plot, `viewBox="0 0 100 100"`, mapping directly onto the 1–100 Urgency (x) / Importance (y) range from the AC. Importance is flipped (`cy = 100 - importance`) so "more important" renders higher, matching the existing mini-card's bottom-origin convention in `CreatePriorityTodoDialog.tsx` (kept consistent rather than reinventing the axis direction for the full-size view). `PriorityMatrixPage.tsx` gained a `view: 'list' | 'map'` state; the previously-disabled "Karte" button now toggles it, and the map panel renders `PriorityMatrixCanvas` with one point per todo currently visible in `filteredSorted` (so the label filter chip stays live in both views, per AC). Done todos render dimmed (`opacity: 0.35` vs `0.9`) rather than being unconditionally excluded — in practice they are usually already absent, since `PriorityMatrixPage` reuses the same `getTodosOfSelectedTodoList` store selector the Standard list page uses, which defaults `filterStatus` to `'Open'`. The dimming only becomes visible if a user has widened that shared filter to `'All'`/`'Done'` from the Standard list's own filter bar before navigating here — the AC allows either "dimmed" or "fully hidden," and this satisfies both readings without adding a second, Priority-page-local filter control. Dragging: `onPointerDown` on a circle calls `setPointerCapture` and records the dragged id; `onPointerMove`/`onPointerUp` on the `<svg>` compute the new axis values from the pointer's position relative to the SVG's bounding rect, **clamped to 1..100 client-side** before the drop is reported. This clamp is not cosmetic: `TodoUrgency`/`TodoImportance`'s Vogen validators *reject* out-of-range values with a 400 rather than clamping them, and the AC explicitly requires "kein 400 vom Vogen-Validator, kein Punkt außerhalb der Karte" — so an out-of-bounds drag must never reach the wire as an out-of-bounds value. On release, `PriorityMatrixPage.handlePointDrag` calls `SetTodoUrgencyImportanceCommand` (the same command the "Ändern" modal already uses) and applies the returned `TodoDto` to the store directly via `updateTodo`, rather than waiting on the WS broadcast round-trip for the dragged point to visually settle. Screen-reader alternative (AC's last bullet): unchanged — the classic list view's "Ändern" button and modal from V1 remain the only editing path required, so a user who can't perform a pointer drag still has full access to the same Urgency/Importance/label editing via that route. ## Concurrent-session conflict note This story exists because of a real merge conflict: an earlier session in parallel had already implemented Feature `#91` as `TodoListType`-based (this design) while a second, independent local session simultaneously implemented an incompatible schema (`TodoListEntity.PriorityMatrixFactor`- as-sole-switch, no `Type` column, `SetTodoPriorityMatrixPositionCommand`) *and* built the full 2D drag view directly into `#91` rather than deferring it. Both were pushed/committed around the same time. Rather than force-overwrite either side's tested work, the `TodoListType`-based V1 (already on `origin/master`, already covered by its own backend+frontend+E2E tests) was kept as the single source of truth, and the other session's already-built 2D-drag UI was re-derived against this schema as this story — the deferred `#91b` the V1 author had already drafted for exactly this scope. See the git history around 2026-08-09 for the two divergent commits if reconstructing this is ever needed; the abandoned local attempt is preserved on the `backup-91-standalone-attempt` branch, not merged. ## Testing - `PriorityMatrixCanvas.test.tsx` (new): point rendering/positioning, dimmed-opacity rendering, drag-to-report with correct axis math, clamping on out-of-bounds drag, no-op when no `onPointDrag` handler is given. - `PriorityMatrixPage.test.tsx`: replaced the now-obsolete "disabled Karte button" test with coverage for switching to the map view, dimmed rendering of a done todo (with the shared `filterStatus` widened to `'All'` first, matching the real precondition), the label filter applying to the map, and a full drag round-trip asserting the exact `SetTodoUrgencyImportanceCommand` payload. ## Verification - `dotnet build`/`dotnet test` (Release): 748 tests green, no backend files touched by this story. - `npx tsc -b`, `npm run build`, `npx vitest run`: clean; 845 frontend tests green (9 new — 6 in `PriorityMatrixCanvas.test.tsx`, net +3 replacing the removed placeholder test in `PriorityMatrixPage.test.tsx`). - Live-verified end-to-end against the dev Postgres/Redis stack: created a real Priority list and two todos via direct API calls (UI drawer clicks were unreliable in this sandbox's headless browser pane — see the Team Coach memory note this cycle), switched to the map view, dragged a point via a timed pointer-event sequence (see the Learned Pattern on synthetic-event timing), confirmed the position updated live and persisted across a full page reload, and confirmed the classic list view re-sorts correctly after the drag. Cleaned up the verification list afterward.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
robert/todo#91
No description provided.