406 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SLICE 14 — Opt-in kanban drag and drop (v2.0 foundation)
> **State-saving rule:** update this file after every completed task and whenever
> work pauses. Keep implementation, automated validation, production packaging,
> and manual Joplin acceptance as separate status boundaries.
## Status
**COMPLETE.** Phases 17 are complete. Focused and full automation passed, the
production JPL was built and inspected, and the user confirmed every Phase 7
manual Joplin acceptance test passed on 2026-07-31.
## Goal
Allow users to move kanban cards between Backlog, In Progress, and Done from the
rendered-note view. Moves persist to Joplin, so refreshes and other dashboards
derive the same workflow state.
````
```gtd-kanban
title: Editable projects and tasks
editable: yes
notes: all
todos: all
```
````
Existing blocks remain read-only when `editable` is omitted.
## Confirmed product contract
- Desktop rendered-note webview only; cards are DOM elements, not canvas pixels.
- `editable: yes` enables mutation; default `no` preserves every v1 block.
- Parse `yes | no` case-insensitively. Invalid, empty, or non-scalar values warn
and fall back to `no`.
- Editable boards provide pointer drag and a keyboard-operable **Move to…**
control. Dragging is never the only interaction.
- Successful moves persist through Joplin's data API, then refresh the affected
board from canonical data.
- Failed/stale moves show an inline error and converge to canonical state.
- Moves change workflow state, not free-form ordering; configured sorting remains
authoritative.
- Cards stay in their current notebook; cross-group moves are out of scope.
## State transitions
An intentional move canonicalizes configured workflow markers. Normal read-only
collection still treats Done as higher priority than In Progress.
### Ordinary opted-in notes
| Destination | Persisted change |
|---|---|
| Backlog | Remove `done-tag` and `in-progress-tag`. |
| In Progress | Add `in-progress-tag`; remove `done-tag`. |
| Done | Add `done-tag`; remove `in-progress-tag`. |
Notes remain ordinary notes. Their `gtd` block, content, dates, notebook, and
unrelated tags remain unchanged.
### Native Joplin to-dos
| Destination | Persisted change |
|---|---|
| Backlog | Set `todo_completed` to `0`; remove `in-progress-tag`. |
| In Progress | Set `todo_completed` to `0`; add `in-progress-tag`. |
| Done | Set `todo_completed` to current epoch milliseconds; remove `in-progress-tag`. |
`done-tag` remains note-only. Moving an already completed to-do to Done is a
no-op rather than rewriting its completion history.
### Required cases
| Item | Source | Destination | Result |
|---|---|---|---|
| Note | Backlog | In Progress | Add progress, remove done. |
| Note | Backlog/In Progress | Done | Add done, remove progress. |
| Note | Done | In Progress | Remove done, add progress. |
| Note | Done/In Progress | Backlog | Remove both. |
| To-do | Backlog | In Progress | Reopen if needed, add progress. |
| To-do | Backlog/In Progress | Done | Complete natively, remove progress. |
| To-do | Done | In Progress | Reopen, add progress. |
| To-do | Done/In Progress | Backlog | Reopen, remove progress. |
| Either | Any | Same column | No mutation or refresh. |
## Repeating to-dos
Native completion may trigger Repeating To-Dos to advance or replace an item.
Use Joplin's real `todo_completed` field; do not simulate recurrence. Always
refetch after completion and accept canonical state if the item changes during
the round trip. Explicitly test completing and reopening recurring to-dos.
## Architecture and trust boundary
```text
pointer drop or keyboard Move to…
-> constrained webview intent
-> main process reparses config and requires editable:yes
-> mutation service fetches current note and tag identities
-> validate eligibility, scope, group, type, and destination
-> apply minimal idempotent completion/tag operations
-> recollect and rerender the initiating board from canonical data
```
The webview sends intent only:
```ts
type KanbanDestination = "backlog" | "inProgress" | "done";
interface MoveKanbanCardIntent {
view: "kanban";
hostNoteId: string;
cardId: string;
destination: KanbanDestination;
rawConfig: string;
viewInstanceId: string;
}
```
Never accept arbitrary REST paths, patches, tag IDs, or tag-operation lists from
the webview. Reparse configuration and independently derive every write.
Add narrowly scoped, testable operations for:
- fetching one current note, including type, completion, parent, and body when
eligibility must be checked;
- listing tags with both ID and title;
- finding/creating a configured tag by normalized title;
- idempotently attaching/detaching a tag;
- patching only `todo_completed` on a native to-do.
Tag matching stays case-insensitive. Reuse an existing case variant. If duplicate
same-title tags exist, do not create another; select deterministically and test it.
### Fresh-state validation
Before writing, verify that the host note exists, the card is not the host, the
destination is fixed, the block still has `editable: yes`, the item remains
admitted by `notes`/`todos`, ordinary notes retain a found `gtd` block, and the
item remains in resolved scope. Grouped boards must retain exact notebook
ownership. Stale conditions make no write and trigger a canonical refresh.
### Multi-step safety
Joplin note/tag operations are separate and may not be transactional. Compute
changes first, skip already-correct writes, order changes sensibly, and refetch
after success or failure. Do not compensate with stale values that might
overwrite a concurrent user edit.
## Webview interaction
Prefer pointer events for consistent desktop behavior and click/drag separation.
- Show a drag affordance only on editable cards.
- Start after a movement threshold; ordinary clicks still open notes.
- Use pointer capture and visible dragged/valid-target/active-target states.
- Support long, scrolling, grouped, and paginated boards.
- Escape, cancellation, lost capture, outside drop, and same-column drop do not
write.
- Scope targets to the nearest board and `viewInstanceId`, never another block.
- Disable duplicate submissions and show pending/error feedback.
- Refresh canonically; sorting may place a moved card beyond the visible page.
Keyboard/accessibility requirements:
- provide a focusable **Move to…** control with fixed destinations;
- disable the current destination;
- preserve keyboard open-note behavior and restore focus after rerender;
- announce pending, success, failure, and destination through `aria-live`;
- give controls and targets useful accessible names.
## Rendering and refresh
- Carry stable `hostNoteId` and per-render `viewInstanceId` context.
- Multiple blocks retain independent request/pagination state.
- Refresh only the initiating instance and reuse collectors for sorting, totals,
warnings, grouping, and cards.
- Optimistic preview is allowed; canonical payload remains authoritative.
- Preserve expansion where practical or document/test consistent reset behavior.
- Reject stale async responses with a per-instance request sequence.
## Implementation plan
### Phase 1 — Configuration and protocol
- [x] Add `editable: boolean` to `KanbanConfig`, default `false`.
- [x] Parse/validate `editable: yes | no` and add complete parser tests.
- [x] Define destinations, constrained intents, success, stale, and error types.
- [x] Add stable host/view identity to payload/context.
- [x] Prove existing blocks retain identical read-only behavior.
### Phase 1 completion record
- Added strict scalar `editable: yes | no` parsing. Omitted and `no` remain
read-only; invalid, empty, non-string, array, and object values warn and use
`no`.
- Added fixed Kanban destinations, a constrained semantic move intent, typed
success/stale/error results, and a runtime destination guard. The protocol
contains no arbitrary paths, patches, tag IDs, or tag-operation lists.
- Every rendered placeholder now owns a stable `viewInstanceId` for its render.
Kanban payloads echo that identity and the canonically resolved host note ID,
including a null host identity on missing-source responses.
- Focused automation passed: 2 suites, 62 tests. TypeScript with
`--skipLibCheck`, webview JavaScript syntax, and `git diff --check` passed.
- Full regression automation passed: 17 suites, 261 tests. No production JPL
was built and no manual Joplin acceptance was performed in this phase.
### Phase 2 — Testable mutation service
- [x] Add fresh note/tag reads, find-or-create, membership changes, and native
completion patches behind a narrow adapter.
- [x] Implement every note and to-do transition above.
- [x] Make changes idempotent and preserve unrelated data.
- [x] Test transitions, conflicting tags, custom names/case, duplicate tags,
no-ops, partial failures, and stale/concurrent reads.
### Phase 2 completion record
- Added a writable adapter separate from the existing read-only collector
abstraction. Its surface is limited to fetching one note and tag identities,
listing/creating tags, attaching/detaching memberships, and patching only
`todo_completed`.
- Added canonical note and native-to-do transitions for all three destinations.
Operations are minimal and idempotent, tag matching is case-insensitive,
existing case variants are reused deterministically, duplicate attached tags
are removed deterministically, and unrelated fields/tags are untouched.
- Partial write failures propagate without stale compensating writes so the
later handler can refetch canonical state. Missing-card reads fail before any
write.
- Focused mutation automation passed: 1 suite, 13 tests. Full regression
automation passed: 18 suites, 274 tests. TypeScript with `--skipLibCheck` and
`git diff --check` passed. No package or manual test was performed.
### Phase 3 — Message validation
- [x] Register the kanban move message in the existing handler.
- [x] Reparse config and require `editable: yes` in the main process.
- [x] Reject malformed intent, arbitrary destinations, missing/host/filtered/
out-of-scope items, lost opt-in, and cross-group movement without writes.
- [x] Return typed user-safe results and keep diagnostic detail in logs.
- [x] Prove webview input cannot select arbitrary paths, fields, tags, or writes.
### Phase 3 completion record
- Registered `moveKanbanCard` and added a testable validation service between
the untrusted webview message and the Phase 2 mutation adapter.
- The handler requires an exact constrained message shape, fixed destination,
non-empty host/card/view identities, matching currently selected host, valid
reparsed YAML, and explicit `editable: yes`. Unknown fields are rejected, so
paths, patches, tag operations, and arbitrary write instructions cannot be
smuggled alongside a valid destination.
- Fresh folders and the freshly fetched card are used to revalidate notebook
targeting, scope, exact current parent ownership, host exclusion, item type
filters, and ordinary-note/to-do `gtd` eligibility immediately before writes.
- Expected stale conditions and failures return typed user-safe results.
Unexpected API detail is retained only in plugin-process diagnostic logging.
- Focused validation passed: 3 suites, 43 tests. Full regression validation
passed: 19 suites, 295 tests. TypeScript with `--skipLibCheck`, webview syntax,
and `git diff --check` passed. Packaging and manual acceptance remain pending.
### Phase 4 — Pointer drag and drop
- [x] Add editable-only affordances and complete pointer lifecycle handling.
- [x] Implement thresholds, capture, target detection, scrolling, cancellation,
Escape, lost capture, pending state, and failure UI.
- [x] Preserve clicks, details, styling, pagination, and navigation.
- [x] Prevent cross-block/group drops and duplicate submissions.
- [x] Refresh canonically and ignore stale responses.
### Phase 4 completion record
- Added an explicit editable-only drag handle to each Kanban card. Pointer drag
begins only after six pixels of movement; ordinary card clicks retain the
existing open-note path, while a completed/cancelled drag suppresses the
synthetic click that could otherwise open a note accidentally.
- Pointer capture, active-card styling, valid/active target styling, viewport
edge scrolling, Escape, pointer cancellation, lost capture, outside drops,
and same-column no-ops are implemented. Drop targets are restricted to the
originating board element, preventing movement across notebook groups or
stacked blocks.
- Per-view pending state prevents duplicate submissions and temporarily disables
handles. Inline pending/error feedback is shown without changing the card
arrays optimistically.
- Every accepted/rejected move triggers a canonical `getKanban` refresh for the
initiating `viewInstanceId`. Per-instance request sequencing and echoed
identity prevent older or foreign responses from replacing the current view.
- Full regression automation passed: 19 suites, 295 tests. TypeScript with
`--skipLibCheck`, webview syntax, and `git diff --check` passed. The repository
has no browser DOM harness, so pointer visuals and behavior remain explicit
Phase 7 manual acceptance items. No production JPL was built in this phase.
### Phase 5 — Keyboard and accessibility
- [x] Add keyboard-operable **Move to…** controls.
- [x] Preserve open-note keys and predictable focus.
- [x] Add accessible names, instructions, states, and live announcements.
- [x] Test keyboard-only movement among all columns.
### Phase 5 completion record
- Every editable card now exposes a native keyboard-operable **Move to…**
selector with fixed Backlog, In Progress, and Done options. The current
destination and placeholder are disabled; choosing another destination uses
the same constrained message and canonical refresh as pointer movement.
- Editable cards are keyboard-focusable links with accessible open-note names
and Enter/Space activation. The pointer-only handle is removed from the tab
and accessibility order so it does not create an inert keyboard stop.
- Move controls carry card-specific accessible names and visible focus styles.
Pending, success, stale, and error status messages use atomic live regions;
controls are disabled while a request is active.
- Canonical rerender restores focus to the moved card's selector when it remains
visible. If sorting or pagination removes the card from the rendered batch,
focus moves to the status region instead of being lost.
- Read-only boards retain their previous markup/focus behavior and expose no
drag handle, move selector, or mutation status UI.
- Full regression automation passed: 19 suites, 295 tests. TypeScript with
`--skipLibCheck`, webview syntax, and `git diff --check` passed. With no DOM
harness in this repository, end-to-end keyboard and screen-reader behavior
remains a Phase 7 manual acceptance boundary. No JPL was built in this phase.
### Phase 6 — Documentation, regression, and packaging
- [x] Document opt-in editing, transitions, sorting, recurrence caveats, keyboard
controls, and desktop scope in README.md and SPEC.md.
- [x] Revise text that says all cards are read-only or drag is merely planned.
- [x] Add an unreleased v2.0.0 CHANGELOG entry without releasing.
- [x] Verify calendars, matrices, Gantt, read-only boards, filters, groups,
warnings, totals, navigation, and styling do not regress.
- [x] Run focused tests, full Jest, TypeScript, webview syntax, whitespace,
prohibited-reference checks, and production JPL build/inspection.
### Phase 6 completion record
- README documents the exact `editable: yes` opt-in, pointer and keyboard
controls, note/to-do transitions, sorting/pagination authority, recurrence,
cancellation/failure behavior, preservation guarantees, and desktop scope.
- SPEC documents parsing, transition tables, constrained protocol and trust
boundary, mutation safety, request sequencing, canonical refresh, focus/live
announcements, and the remaining multi-dashboard refresh limitation. The old
planned-authoring roadmap is replaced with the implemented release-pending
contract. Historical v1 read-only text and current matrix/Gantt read-only text
remain intentionally scoped and accurate.
- CHANGELOG contains an unreleased 2.0.0 Kanban entry. Package and manifest
versions intentionally remain 1.0.0 until the consolidated v2 release phase;
nothing was published or pushed.
- Focused automation passed: 4 suites, 96 tests. Full automation passed: 19
suites, 295 tests. TypeScript with `--skipLibCheck`, webview JavaScript syntax,
`git diff --check`, stale-wording review, and packaged-output prohibited-path/
planning-reference audits passed.
- `npm run dist` passed. The fresh production artifact is
`publish/com.victorwiebe.joplin.plugin.gtd-calendar.jpl` (177,664 bytes,
embedded manifest version 1.0.0, SHA-256
`749db5d334457ba4818a075c9d0df4f47bda53a478f0dc9904cbab5f8d0d4e33`).
Its five expected runtime files and Kanban mutation, pointer, keyboard,
accessibility, and request-sequencing markers were inspected.
- No manual Joplin acceptance was performed; every Phase 7 item remains open.
### Phase 7 — Manual Joplin acceptance
- [x] Omitted/`no`/invalid `editable` states remain read-only as documented.
- [x] Pointer moves notes through all columns and persists exact configured tags.
- [x] Pointer moves to-dos through all columns and persists native completion and
progress state.
- [x] Conflicting note tags are canonicalized only by an intentional move.
- [x] Custom workflow tags preserve unrelated/similarly named tags.
- [x] Same-column, cancelled, outside, Escape, and interrupted drags do not write.
- [x] Click still opens; drag does not accidentally open.
- [x] Keyboard-only movement works, restores focus, and announces results.
- [x] Stale state/API failure cannot leave false UI or corrupt unrelated data.
- [x] Sorting, totals, pagination, colours, glyphs, details, and Done styling stay
correct after refresh.
- [x] Single, grouped, multiple-block, and long scrolling boards work.
- [x] Completing/reopening a recurring to-do works with Repeating To-Dos.
- [x] Record explicit user sign-off separately from automation.
### Phase 7 acceptance record
- **PASSED — USER SIGN-OFF RECEIVED 2026-07-31.** The user confirmed all Phase 7
manual tests passed against the inspected Phase 6 production JPL.
- Accepted coverage includes read-only opt-in boundaries, ordinary-note and
native-to-do transitions, conflicting/custom/unrelated tags, pointer
cancellation and click separation, keyboard movement and focus/live feedback,
stale/API-failure convergence, sorting/pagination/styling, grouped and stacked
boards, long scrolling, and Repeating To-Dos integration.
- This manual acceptance is recorded separately from Phase 6 automation and
packaging. Slice 15 has not begun.
## Out of scope
- Free-form ordering or persisted rank.
- Moving between notebooks/groups or changing `parent_id`.
- Editing card fields or arbitrary tags.
- Calendar, Gantt, or matrix drag (SLICE 15 covers Eisenhower).
- Skeleton drag, mobile support, or recurrence reimplementation.
- Publishing v2.0.0 before both slices and release acceptance are complete.
## Acceptance criteria
- Only an explicitly editable kanban can mutate data.
- Every destination produces the exact canonical state above.
- Main-process validation uses fresh Joplin data and constrained intent.
- Pointer and keyboard workflows are complete and accessible.
- Failures converge to canonical state without overwriting unrelated changes.
- All read-only and v1.0.0 behavior remains compatible.
- Automation, packaging, and manual acceptance are recorded separately.