SLICE11-done-tag-completion
This commit is contained in:
parent
77d86d1beb
commit
6d645085a6
10
CHANGELOG.md
10
CHANGELOG.md
@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
All notable changes to GTD Calendar are documented here. Versions follow the plugin's `manifest.json` / `package.json` version, which also matches the published npm releases.
|
All notable changes to GTD Calendar are documented here. Versions follow the plugin's `manifest.json` / `package.json` version, which also matches the published npm releases.
|
||||||
|
|
||||||
|
## Unreleased — Done-tag completion for ordinary notes
|
||||||
|
|
||||||
|
- Added configurable `done-tag` support to kanban and matrix blocks for
|
||||||
|
ordinary notes that already opt in with a `gtd` block.
|
||||||
|
- Kanban completion takes priority over `in-progress-tag`; `done-window`
|
||||||
|
remains specific to native completed to-dos.
|
||||||
|
- Both matrix modes exclude completed notes before quadrant bucketing.
|
||||||
|
- Native to-dos still use Joplin completion, calendars are unchanged, and the
|
||||||
|
plugin remains read-only.
|
||||||
|
|
||||||
## Unreleased — Opted-in note cards for kanban and matrix
|
## Unreleased — Opted-in note cards for kanban and matrix
|
||||||
|
|
||||||
- Ordinary notes containing a `gtd` block now appear in kanban and matrix
|
- Ordinary notes containing a `gtd` block now appear in kanban and matrix
|
||||||
|
|||||||
75
README.md
75
README.md
@ -93,6 +93,7 @@ scope: this-folder
|
|||||||
todos: all
|
todos: all
|
||||||
sort-type: due-date
|
sort-type: due-date
|
||||||
in-progress-tag: in-progress
|
in-progress-tag: in-progress
|
||||||
|
done-tag: done
|
||||||
card-detail: hover
|
card-detail: hover
|
||||||
page-size: 10
|
page-size: 10
|
||||||
done-window: 7
|
done-window: 7
|
||||||
@ -116,29 +117,83 @@ The board has three columns:
|
|||||||
in-progress tag.
|
in-progress tag.
|
||||||
- **In Progress** — eligible notes and incomplete to-dos carrying the
|
- **In Progress** — eligible notes and incomplete to-dos carrying the
|
||||||
in-progress tag (default `in-progress`; configurable).
|
in-progress tag (default `in-progress`; configurable).
|
||||||
- **Done** — completed to-dos within `done-window` days (default 7; set `all`
|
- **Done** — opted-in ordinary notes carrying the configured done tag, plus
|
||||||
for full history). Ordinary notes never enter Done.
|
completed to-dos within `done-window` days (default 7; set `all` for full
|
||||||
|
to-do history).
|
||||||
|
|
||||||
A completed to-do always lands in **Done**, even if it still carries the
|
Completion always wins over the in-progress tag. Ordinary notes use `done`;
|
||||||
in-progress tag. The `todos:` option controls only to-dos; opted-in ordinary
|
native to-dos remain governed by native Joplin completion. Completed notes are
|
||||||
notes remain visible under `todos: none`.
|
not filtered by `done-window` because they have no completion timestamp. The
|
||||||
|
`todos:` option still controls only to-dos.
|
||||||
|
|
||||||
| Option | Values | Default | Description |
|
| Option | Values | Default | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `title` | text | — | Heading above the board. |
|
| `title` | text | — | Heading above the board. |
|
||||||
| `scope` | `this-folder`, `children`, integer, `all` | `this-folder` | Same folder-scanning rules as the calendar, including `all` (every notebook — see the calendar table's caution). |
|
| `scope` | `this-folder`, `children`, integer, `all` | `this-folder` | Same folder-scanning rules as the calendar, including `all` (every notebook — see the calendar table's caution). |
|
||||||
| `notebook` | notebook name, `Parent/Child` path, or folder id | this note's folder | Root the scan at a specific notebook (see the calendar table for the full resolution rules). `scope` applies relative to it. |
|
| `notebook` | notebook name, `Parent/Child` path, or folder id | this note's folder | Root the scan at a specific notebook (see the calendar table for the full resolution rules). `scope` applies relative to it. |
|
||||||
|
| `group` | `notebook` | — | With the literal `scope: children`, render a separate board for the root notebook and every non-empty descendant instead of one aggregated board. Other scope forms warn and remain aggregated. |
|
||||||
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Which to-dos appear. `gtd-only` requires a `gtd` block; `all` includes every to-do in scope; `none` excludes all to-dos. This option does not affect opted-in ordinary notes. |
|
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Which to-dos appear. `gtd-only` requires a `gtd` block; `all` includes every to-do in scope; `none` excludes all to-dos. This option does not affect opted-in ordinary notes. |
|
||||||
| `sort-type` | `due-date`, `title`, `modified-date` | `due-date` | Order of cards within each column. Under `due-date`, cards with no due date sort last. |
|
| `sort-type` | `due-date`, `title`, `modified-date` | `due-date` | Order of cards within each column. Under `due-date`, cards with no due date sort last. |
|
||||||
| `sort` | `asc`, `desc` | `asc` | Sort direction. |
|
| `sort` | `asc`, `desc` | `asc` | Sort direction. |
|
||||||
| `in-progress-tag` | text | `in-progress` | The tag that places an eligible note or incomplete to-do in the In Progress column. |
|
| `in-progress-tag` | text | `in-progress` | The tag that places an eligible note or incomplete to-do in the In Progress column. |
|
||||||
|
| `done-tag` | text | `done` | Marks an opted-in ordinary note complete. Done takes priority over `in-progress-tag`; native to-dos still use Joplin completion. |
|
||||||
| `card-detail` | `hover`, `always`, `none` | `hover` | Whether each card's due date / hover text shows on hover, always, or never. |
|
| `card-detail` | `hover`, `always`, `none` | `hover` | Whether each card's due date / hover text shows on hover, always, or never. |
|
||||||
| `page-size` | positive integer | `10` | Cards initially shown in each column and revealed by each **List more** click. Invalid values warn and fall back to 10. |
|
| `page-size` | positive integer | `10` | Cards initially shown in each column and revealed by each **List more** click. Invalid values warn and fall back to 10. |
|
||||||
| `done-window` | integer or `all` | `7` | How many days back the Done column reaches. |
|
| `done-window` | integer or `all` | `7` | How many days back the Done column reaches. |
|
||||||
|
|
||||||
Each column applies `page-size` independently after filtering and sorting. When a column has more cards, **List more** reveals the next batch; hovering over it or focusing it with the keyboard shows exactly how many entries remain. Expansion lasts only in the current rendered view and resets when the note is reloaded. Column headings and board statistics always show full totals.
|
Each column applies `page-size` independently after filtering and sorting. When a column has more cards, **List more** reveals the next batch; hovering over it or focusing it with the keyboard shows exactly how many entries remain. Expansion lasts only in the current rendered view and resets when the note is reloaded. Column headings and board statistics always show full totals.
|
||||||
|
|
||||||
Cards are compact and click through to their source note. To-dos use checkbox glyphs and may show recurrence ↻; ordinary notes use 📄 and are never completed or recurring. A custom `icon` overrides the fallback glyph. Note dates come only from the `gtd` block; to-do dates use the block first, then the native due date. Empty blocks opt in with defaults, while malformed blocks remain visible with fallback values and a warning. The board is **read-only** in this version—it reflects note and to-do state but does not change it. (Drag-and-drop to move cards between columns is planned.)
|
### Separate views for child notebooks
|
||||||
|
|
||||||
|
Kanban and matrix blocks can split a notebook tree into complete per-notebook
|
||||||
|
views. Use the literal `scope: children` together with `group: notebook`:
|
||||||
|
|
||||||
|
````
|
||||||
|
```gtd-kanban
|
||||||
|
title: Project boards
|
||||||
|
notebook: Projects
|
||||||
|
scope: children
|
||||||
|
group: notebook
|
||||||
|
todos: all
|
||||||
|
page-size: 10
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
For this tree:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Projects
|
||||||
|
├── Home
|
||||||
|
│ ├── Archive (empty)
|
||||||
|
│ └── Repairs
|
||||||
|
└── Work
|
||||||
|
└── Release
|
||||||
|
```
|
||||||
|
|
||||||
|
the block renders a separate three-column board for cards owned directly by
|
||||||
|
`Projects`, `Projects/Home`, `Projects/Home/Repairs`, `Projects/Work`, and
|
||||||
|
`Projects/Work/Release`. `Projects/Home/Archive` is omitted when it has no
|
||||||
|
eligible cards. An empty parent is also omitted without hiding a non-empty
|
||||||
|
grandchild.
|
||||||
|
|
||||||
|
Cards are never rolled up into ancestor views: every eligible note or to-do
|
||||||
|
appears exactly once under the full path of its owning notebook. Full paths also
|
||||||
|
distinguish duplicate notebook names. Views are ordered root-first, then
|
||||||
|
depth-first with siblings sorted by title. Each generated column or quadrant has
|
||||||
|
its own `page-size` and **List more** state, and reloading resets all of them.
|
||||||
|
Malformed-item warnings appear beside their owning notebook view; scan and card
|
||||||
|
statistics appear once for the overall block.
|
||||||
|
|
||||||
|
The same options and ownership rules apply to `gtd-matrix`; every non-empty
|
||||||
|
notebook receives a complete four-quadrant matrix in the selected mode. Opted-in
|
||||||
|
ordinary notes group by their owning notebook exactly like to-dos.
|
||||||
|
|
||||||
|
Grouping is deliberately limited to the exact combination above. Plain
|
||||||
|
`scope: children` remains one aggregated view. `this-folder`, numeric depths,
|
||||||
|
and `scope: all` also remain aggregated; combining any of them with
|
||||||
|
`group: notebook` shows a warning and disables grouping.
|
||||||
|
|
||||||
|
Cards are compact and click through to their source note. To-dos use checkbox glyphs and may show recurrence ↻; ordinary notes use 📄 and never become recurring. Completed notes keep 📄 and use completed title styling. A custom `icon` overrides the fallback glyph. Note dates come only from the `gtd` block; to-do dates use the block first, then the native due date. Empty blocks opt in with defaults, while malformed blocks remain visible with fallback values and a warning. The board is **read-only** and never changes tags or completion. (Drag-and-drop is planned.)
|
||||||
|
|
||||||
## The Matrix (Skeleton & Eisenhower)
|
## The Matrix (Skeleton & Eisenhower)
|
||||||
|
|
||||||
@ -150,6 +205,7 @@ title: Priorities
|
|||||||
todos: all
|
todos: all
|
||||||
mode: skeleton
|
mode: skeleton
|
||||||
page-size: 8
|
page-size: 8
|
||||||
|
done-tag: done
|
||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|
||||||
@ -174,7 +230,7 @@ Built to complement the kanban and calendar, using the tags and dates you alread
|
|||||||
| **Active** | Do Next | Scheduled |
|
| **Active** | Do Next | Scheduled |
|
||||||
| **Not active** | On Deck | Backlog |
|
| **Not active** | On Deck | Backlog |
|
||||||
|
|
||||||
**On Deck** is the quadrant to watch: due soon, not yet started. Nothing is ever labelled "Eliminate" — a Backlog item is simply low priority, not condemned. Dateless in-progress work sits in Scheduled (active, no clock). Eligible ordinary notes use their `gtd` date and the same tag rules; completed to-dos do not appear at all.
|
**On Deck** is the quadrant to watch: due soon, not yet started. Nothing is ever labelled "Eliminate" — a Backlog item is simply low priority, not condemned. Dateless in-progress work sits in Scheduled (active, no clock). Eligible ordinary notes use their `gtd` date and the same tag rules; completed to-dos and notes carrying `done-tag` do not appear at all.
|
||||||
|
|
||||||
### `mode: eisenhower` — the classic
|
### `mode: eisenhower` — the classic
|
||||||
|
|
||||||
@ -188,6 +244,7 @@ Quadrants from the urgent/important tags: Do First (both), Schedule (important),
|
|||||||
| `title` | text | — | Heading above the matrix. |
|
| `title` | text | — | Heading above the matrix. |
|
||||||
| `scope` | `this-folder`, `children`, integer, `all` | `this-folder` | Same folder-scanning rules as the calendar and kanban, including `all` (every notebook — see the calendar table's caution). |
|
| `scope` | `this-folder`, `children`, integer, `all` | `this-folder` | Same folder-scanning rules as the calendar and kanban, including `all` (every notebook — see the calendar table's caution). |
|
||||||
| `notebook` | notebook name, `Parent/Child` path, or folder id | this note's folder | Root the scan at a specific notebook (see the calendar table for the full resolution rules). `scope` applies relative to it. |
|
| `notebook` | notebook name, `Parent/Child` path, or folder id | this note's folder | Root the scan at a specific notebook (see the calendar table for the full resolution rules). `scope` applies relative to it. |
|
||||||
|
| `group` | `notebook` | — | With the literal `scope: children`, render a separate complete matrix for the root notebook and every non-empty descendant. Other scope forms warn and remain aggregated. |
|
||||||
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Which to-dos appear. This option does not affect ordinary notes, which appear only when they contain a `gtd` block. |
|
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Which to-dos appear. This option does not affect ordinary notes, which appear only when they contain a `gtd` block. |
|
||||||
| `sort-type` | `due-date`, `title`, `modified-date` | `due-date` | Order within each quadrant; dateless cards sort last under `due-date`. |
|
| `sort-type` | `due-date`, `title`, `modified-date` | `due-date` | Order within each quadrant; dateless cards sort last under `due-date`. |
|
||||||
| `sort` | `asc`, `desc` | `asc` | Sort direction. |
|
| `sort` | `asc`, `desc` | `asc` | Sort direction. |
|
||||||
@ -281,7 +338,7 @@ Issues and ideas: [the repository](https://gitea.skeletonworks.online/vwiebe/jop
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm test # 164 unit tests
|
npm test # 240 unit tests
|
||||||
npm run dist # builds publish/*.jpl
|
npm run dist # builds publish/*.jpl
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -289,7 +346,7 @@ Design notes and the full specification live in [SPEC.md](SPEC.md). Release hist
|
|||||||
|
|
||||||
### Cutting a release
|
### Cutting a release
|
||||||
|
|
||||||
1. Bump `version` in **both** `src/manifest.json` and `package.json` (they must match — the Joplin plugin repo reads the manifest, npm reads package.json).
|
1. Bump `version` in `src/manifest.json`, `package.json`, and `package-lock.json` (the manifest and package versions must match).
|
||||||
2. Add an entry to `CHANGELOG.md`.
|
2. Add an entry to `CHANGELOG.md`.
|
||||||
3. `npm publish` (the `prepare` script rebuilds the `.jpl` fresh from source, so you can't accidentally publish a stale build).
|
3. `npm publish` (the `prepare` script rebuilds the `.jpl` fresh from source, so you can't accidentally publish a stale build).
|
||||||
|
|
||||||
|
|||||||
24
SLICE10.md
24
SLICE10.md
@ -362,19 +362,33 @@ one additional rendering decision.
|
|||||||
|
|
||||||
### Phase 7 — Documentation
|
### Phase 7 — Documentation
|
||||||
|
|
||||||
- [ ] Update README.md to state that `scope: children` plus `group: notebook`
|
- [x] Update README.md to state that `scope: children` plus `group: notebook`
|
||||||
produces separate notebook views for kanban and matrix blocks.
|
produces separate notebook views for kanban and matrix blocks.
|
||||||
- [ ] Add a parent/child/grandchild example showing root ownership, omitted empty
|
- [x] Add a parent/child/grandchild example showing root ownership, omitted empty
|
||||||
notebooks, full-path headings, and no rollups.
|
notebooks, full-path headings, and no rollups.
|
||||||
- [ ] State that ungrouped `children`, numeric depth, and `scope: all` remain
|
- [x] State that ungrouped `children`, numeric depth, and `scope: all` remain
|
||||||
aggregated.
|
aggregated.
|
||||||
- [ ] Explain that every generated bucket has independent SLICE8 expansion state.
|
- [x] Explain that every generated bucket has independent SLICE8 expansion state.
|
||||||
- [ ] Explain that SLICE9 notes group by their owning notebook.
|
- [x] Explain that SLICE9 notes group by their owning notebook.
|
||||||
- [ ] Update SPEC.md scope parsing, folder traversal, layout payloads, warning
|
- [ ] Update SPEC.md scope parsing, folder traversal, layout payloads, warning
|
||||||
ownership, statistics, and rendering flow.
|
ownership, statistics, and rendering flow.
|
||||||
- [ ] Add an unreleased SLICE10 entry to CHANGELOG.md without changing the
|
- [ ] Add an unreleased SLICE10 entry to CHANGELOG.md without changing the
|
||||||
package version until release scope is decided.
|
package version until release scope is decided.
|
||||||
|
|
||||||
|
### Phase 7 README progress record
|
||||||
|
|
||||||
|
- README option tables now document `group: notebook` for kanban and matrix and
|
||||||
|
constrain it to the literal `scope: children` combination.
|
||||||
|
- Added a parent/child/grandchild example covering exact ownership, no ancestor
|
||||||
|
rollups, omitted empty notebooks, non-empty descendants, full-path headings,
|
||||||
|
deterministic order, warning/statistics placement, and independent batching.
|
||||||
|
- Documented that ordinary note cards group exactly like to-dos and that all
|
||||||
|
other scope forms remain aggregated, warning when grouping is requested.
|
||||||
|
- Updated the development test count to 230 and release-version instructions to
|
||||||
|
include the lockfile.
|
||||||
|
- `SPEC.md` and `CHANGELOG.md` remain pending, so Phase 7 and SLICE10 are not yet
|
||||||
|
complete.
|
||||||
|
|
||||||
### Phase 8 — Manual Joplin acceptance
|
### Phase 8 — Manual Joplin acceptance
|
||||||
|
|
||||||
Create a root notebook containing direct cards, multiple children, grandchildren,
|
Create a root notebook containing direct cards, multiple children, grandchildren,
|
||||||
|
|||||||
216
SLICE11.md
Normal file
216
SLICE11.md
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
# SLICE 11 — Complete ordinary notes with a `done` tag
|
||||||
|
|
||||||
|
> **State-saving rule:** update this file after every completed task and whenever
|
||||||
|
> work pauses. Keep automated validation and manual Joplin acceptance separate.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**COMPLETE.** Phases 1–6 are implemented and fully validated. The user confirmed
|
||||||
|
all Phase 7 manual Joplin acceptance tests pass on 2026-07-28. The production
|
||||||
|
JPL is built and inspected.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Give ordinary notes containing a `gtd` block a natural completion state without
|
||||||
|
turning them into native Joplin to-dos. Completion follows the same tag-driven
|
||||||
|
progression already used by kanban:
|
||||||
|
|
||||||
|
```text
|
||||||
|
no workflow tag -> Backlog
|
||||||
|
in-progress -> In Progress
|
||||||
|
done -> Done
|
||||||
|
```
|
||||||
|
|
||||||
|
If an ordinary note carries both `in-progress` and `done`, **Done wins**.
|
||||||
|
|
||||||
|
## Confirmed behavior
|
||||||
|
|
||||||
|
- Only ordinary notes that already opt in through a found `gtd` block are
|
||||||
|
affected. A `done` tag does not opt a plain note into a view.
|
||||||
|
- The default completion tag is `done`.
|
||||||
|
- For ordinary notes, `done` takes priority over `in-progress` during kanban
|
||||||
|
bucketing.
|
||||||
|
- Existing native to-do completion remains authoritative for Joplin to-dos.
|
||||||
|
- The feature must preserve SLICE8 batching, SLICE9 mixed cards, and SLICE10
|
||||||
|
exact-owner notebook grouping.
|
||||||
|
|
||||||
|
## Confirmed design decisions
|
||||||
|
|
||||||
|
### 1. Configuration
|
||||||
|
|
||||||
|
**Recommendation:** add `done-tag:` to both `gtd-kanban` and `gtd-matrix`,
|
||||||
|
defaulting to `done`, just as `in-progress-tag:` defaults to `in-progress`.
|
||||||
|
Normalize tag names case-insensitively.
|
||||||
|
|
||||||
|
Warn when `done-tag` and `in-progress-tag` are identical because the workflow
|
||||||
|
would collapse directly to Done. Do not silently choose a different tag.
|
||||||
|
|
||||||
|
### 2. Which item types the tag affects
|
||||||
|
|
||||||
|
**Recommendation:** `done-tag` changes completion only for ordinary notes.
|
||||||
|
Native to-dos continue to use `todo_completed`; an incomplete to-do tagged
|
||||||
|
`done` remains incomplete. This avoids two competing completion authorities for
|
||||||
|
Joplin to-dos.
|
||||||
|
|
||||||
|
### 3. Kanban `done-window`
|
||||||
|
|
||||||
|
Ordinary notes have no native completion timestamp. Their `updated_time` is not
|
||||||
|
a reliable substitute because any later edit would make an old completion look
|
||||||
|
recent.
|
||||||
|
|
||||||
|
**Recommendation:** completed ordinary notes are not filtered by `done-window`;
|
||||||
|
all opted-in notes carrying `done-tag` appear in Done. `done-window` continues to
|
||||||
|
apply only to native completed to-dos. Document this explicitly.
|
||||||
|
|
||||||
|
Alternative if an unbounded Done column is unacceptable: add a completion date
|
||||||
|
to the `gtd` block in a later slice. Do not infer it from `updated_time`.
|
||||||
|
|
||||||
|
### 4. Matrix behavior
|
||||||
|
|
||||||
|
**Recommendation:** exclude `done` ordinary notes from both Skeleton and
|
||||||
|
Eisenhower matrices, matching the existing exclusion of completed to-dos. A
|
||||||
|
prioritization view should contain only unfinished work.
|
||||||
|
|
||||||
|
The exclusion happens before quadrant bucketing. Thus `done` also wins over
|
||||||
|
`in-progress`, `urgent`, and `important` tags.
|
||||||
|
|
||||||
|
### 5. Calendar behavior
|
||||||
|
|
||||||
|
**Recommendation for this slice:** leave calendars unchanged. A completed
|
||||||
|
ordinary note remains a scheduled/unscheduled calendar item and does not gain
|
||||||
|
strikethrough solely from `done-tag`.
|
||||||
|
|
||||||
|
Calendar completion styling can be considered separately because calendars may
|
||||||
|
serve as historical records, while kanban and matrix are workflow views.
|
||||||
|
|
||||||
|
### 6. Card representation and styling
|
||||||
|
|
||||||
|
Completed ordinary-note cards keep the 📄 fallback glyph and use the existing
|
||||||
|
completed title styling in kanban Done. They do not use a checked-checkbox glyph
|
||||||
|
and never become recurring.
|
||||||
|
|
||||||
|
The shared card model may represent `completed: true` with `isTodo: false`.
|
||||||
|
`completedTime` remains `0` for notes because no completion timestamp exists.
|
||||||
|
|
||||||
|
### 7. Removing or changing tags
|
||||||
|
|
||||||
|
- Removing `done` from a note makes it unfinished again.
|
||||||
|
- If `in-progress` remains, the note returns to In Progress.
|
||||||
|
- If neither workflow tag remains, it returns to Backlog.
|
||||||
|
- No tag is automatically added or removed; the plugin remains read-only.
|
||||||
|
|
||||||
|
## Precedence table
|
||||||
|
|
||||||
|
| Item | Native completion | `done` tag | `in-progress` tag | Kanban result | Matrix result |
|
||||||
|
|---|---:|---:|---:|---|---|
|
||||||
|
| Ordinary opted-in note | n/a | yes | either | Done | Excluded |
|
||||||
|
| Ordinary opted-in note | n/a | no | yes | In Progress | Existing mode rules |
|
||||||
|
| Ordinary opted-in note | n/a | no | no | Backlog | Existing mode rules |
|
||||||
|
| Ordinary note without `gtd` | n/a | any | any | Excluded | Excluded |
|
||||||
|
| Completed to-do | yes | any | any | Done, subject to `done-window` | Excluded |
|
||||||
|
| Incomplete to-do | no | yes | yes/no | Existing in-progress rule | Existing mode rules |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```text
|
||||||
|
eligible note + normalized tag titles
|
||||||
|
|
|
||||||
|
v
|
||||||
|
buildKanbanCard
|
||||||
|
to-do -> completion from todo_completed
|
||||||
|
note -> completion from configured done-tag
|
||||||
|
|
|
||||||
|
+-- kanban: completed wins -> Done
|
||||||
|
| note: no done-window filtering
|
||||||
|
| to-do: existing done-window filtering
|
||||||
|
|
|
||||||
|
+-- matrix: completed -> exclude before quadrant bucketing
|
||||||
|
```
|
||||||
|
|
||||||
|
Because `done-tag` is configurable per view, card construction may need either a
|
||||||
|
completion-tag argument or a small view-specific completion step after the
|
||||||
|
shared builder. Prefer one explicit data path shared by kanban and matrix; do not
|
||||||
|
hide the configured tag in a global constant lookup.
|
||||||
|
|
||||||
|
## Implementation plan
|
||||||
|
|
||||||
|
### Phase 1 — Configuration and types
|
||||||
|
|
||||||
|
- [x] Add `DONE_TAG` with the value `done`.
|
||||||
|
- [x] Add normalized `doneTag` fields to `KanbanConfig` and `MatrixConfig`.
|
||||||
|
- [x] Parse `done-tag` in both view blocks with a default of `done`.
|
||||||
|
- [x] Warn when `done-tag` and `in-progress-tag` are identical.
|
||||||
|
- [x] Add parser tests for defaults, custom tags, normalization, empty values,
|
||||||
|
unknown-option handling, and identical-tag warnings.
|
||||||
|
|
||||||
|
### Phase 2 — Shared completion model
|
||||||
|
|
||||||
|
- [x] Allow an ordinary `KanbanCard` to be completed from the configured tag.
|
||||||
|
- [x] Keep `isTodo: false`, `completedTime: 0`, and `isRecurring: false`.
|
||||||
|
- [x] Preserve native to-do completion regardless of `done-tag`.
|
||||||
|
- [x] Test ordinary-note, native-to-do, custom-tag, and both-tags precedence.
|
||||||
|
|
||||||
|
### Phase 3 — Kanban behavior
|
||||||
|
|
||||||
|
- [x] Bucket completed ordinary notes into Done before checking in-progress.
|
||||||
|
- [x] Keep all completed note cards regardless of `done-window`.
|
||||||
|
- [x] Preserve the existing to-do done-window behavior.
|
||||||
|
- [x] Sort completed notes and to-dos together with the configured Done sorter.
|
||||||
|
- [x] Cover single and SLICE10 notebook-grouped layouts with exact card totals.
|
||||||
|
|
||||||
|
### Phase 4 — Matrix behavior
|
||||||
|
|
||||||
|
- [x] Exclude completed ordinary notes before Skeleton/Eisenhower bucketing.
|
||||||
|
- [x] Prove `done` wins over in-progress, urgent, and important tags.
|
||||||
|
- [x] Preserve native completed-to-do exclusion.
|
||||||
|
- [x] Cover single and SLICE10 notebook-grouped layouts without double-counting.
|
||||||
|
|
||||||
|
### Phase 5 — Rendering and payload regression
|
||||||
|
|
||||||
|
- [x] Confirm completed note cards retain the 📄 glyph and completed styling.
|
||||||
|
- [x] Confirm no new payload branch is needed beyond the normalized card fields.
|
||||||
|
- [x] Confirm SLICE8 independent batching and SLICE10 grouped rendering remain
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
### Phase 6 — Documentation and automated verification
|
||||||
|
|
||||||
|
- [x] Document `done-tag`, precedence, note-only semantics, matrix exclusion,
|
||||||
|
and the done-window exception in README.md and SPEC.md.
|
||||||
|
- [x] Add an unreleased CHANGELOG entry.
|
||||||
|
- [x] Run focused parser/builder/kanban/matrix tests (103 passed).
|
||||||
|
- [x] Run the complete Jest suite (240 tests), TypeScript, JavaScript syntax, whitespace,
|
||||||
|
and prohibited-reference checks.
|
||||||
|
- [x] Build the production JPL.
|
||||||
|
|
||||||
|
### Phase 7 — Manual Joplin acceptance
|
||||||
|
|
||||||
|
- [x] Note with `done` only appears in Kanban Done.
|
||||||
|
- [x] Note with both `done` and `in-progress` appears only in Done.
|
||||||
|
- [x] Removing `done` returns the note to In Progress or Backlog as appropriate.
|
||||||
|
- [x] Custom `done-tag` works independently in kanban and matrix blocks.
|
||||||
|
- [x] Completed notes are excluded from both matrix modes.
|
||||||
|
- [x] Incomplete to-dos tagged `done` remain governed by native completion.
|
||||||
|
- [x] `done-window` filters native to-dos but not completed ordinary notes.
|
||||||
|
- [x] Glyph, styling, sorting, navigation, warnings, statistics, batching, and
|
||||||
|
child-notebook grouping do not regress.
|
||||||
|
- [x] Record explicit user sign-off separately from automated checks.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Mutating tags from a rendered card.
|
||||||
|
- Automatically removing `in-progress` when `done` is added.
|
||||||
|
- Inferring a completion time from `updated_time`.
|
||||||
|
- Adding a completion date to the `gtd` block.
|
||||||
|
- Changing calendar completion styling.
|
||||||
|
- Treating `done` as an opt-in for ordinary notes without a `gtd` block.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- An opted-in ordinary note carrying the configured done tag is complete.
|
||||||
|
- Done wins over in-progress and every matrix axis tag.
|
||||||
|
- Native to-do completion behavior is unchanged.
|
||||||
|
- Done-window semantics are explicit and deterministic without fabricated dates.
|
||||||
|
- Single and grouped views retain correct ownership, totals, sorting, warnings,
|
||||||
|
batching, styling, and navigation.
|
||||||
|
- Automated validation, production packaging, and explicit manual acceptance
|
||||||
|
all pass and are recorded separately.
|
||||||
22
SPEC.md
22
SPEC.md
@ -85,6 +85,7 @@ todos: gtd-only # gtd-only | all | none (default: gtd-only)
|
|||||||
sort-type: due-date # due-date | title | modified-date (default: due-date)
|
sort-type: due-date # due-date | title | modified-date (default: due-date)
|
||||||
sort: asc # asc | desc (default: asc)
|
sort: asc # asc | desc (default: asc)
|
||||||
in-progress-tag: in-progress # default: "in-progress"
|
in-progress-tag: in-progress # default: "in-progress"
|
||||||
|
done-tag: done # ordinary-note completion; default: "done"
|
||||||
card-detail: hover # hover | always | none (default: hover)
|
card-detail: hover # hover | always | none (default: hover)
|
||||||
page-size: 10 # positive integer (default: 10)
|
page-size: 10 # positive integer (default: 10)
|
||||||
done-window: 7 # days, or "all" (default: 7)
|
done-window: 7 # days, or "all" (default: 7)
|
||||||
@ -95,12 +96,13 @@ Ordinary notes participate only when their body contains a `gtd` block; empty
|
|||||||
and malformed blocks both opt in, with malformed YAML producing a warning and
|
and malformed blocks both opt in, with malformed YAML producing a warning and
|
||||||
fallback card values. The `todos:` option applies only to to-dos.
|
fallback card values. The `todos:` option applies only to to-dos.
|
||||||
|
|
||||||
Bucketing: a completed to-do enters Done (and wins regardless of tags); otherwise
|
Bucketing: completion wins regardless of progress tags. Native to-dos use
|
||||||
an eligible note or incomplete to-do carrying `in-progress-tag` enters In
|
`todo_completed`; opted-in ordinary notes use `done-tag`. Incomplete items
|
||||||
Progress; everything else enters Backlog. Ordinary notes never enter Done and
|
carrying `in-progress-tag` enter In Progress; everything else enters Backlog.
|
||||||
never acquire completion or recurrence state. Done is filtered to to-dos
|
The done tag neither completes a native to-do nor opts in a plain note.
|
||||||
completed within `done-window` days (`all` for full history), preventing
|
`done-window` filters native completed to-dos only; completed notes remain in
|
||||||
unbounded growth. Sorting is per-column across the combined note/to-do array.
|
Done because no completion timestamp is inferred from `updated_time`. Sorting
|
||||||
|
is per-column across the combined note/to-do array.
|
||||||
Under `due-date`, dateless cards sort after dated cards regardless of direction.
|
Under `due-date`, dateless cards sort after dated cards regardless of direction.
|
||||||
`page-size` must be a positive integer; invalid values warn and fall back to
|
`page-size` must be a positive integer; invalid values warn and fall back to
|
||||||
10. Each column initially renders at most that many cards and reveals another
|
10. Each column initially renders at most that many cards and reveals another
|
||||||
@ -122,12 +124,18 @@ sort: asc
|
|||||||
urgent-tag: urgent # default: "urgent"
|
urgent-tag: urgent # default: "urgent"
|
||||||
important-tag: important # Eisenhower mode only; default: "important"
|
important-tag: important # Eisenhower mode only; default: "important"
|
||||||
in-progress-tag: in-progress # Skeleton mode only; default: "in-progress"
|
in-progress-tag: in-progress # Skeleton mode only; default: "in-progress"
|
||||||
|
done-tag: done # note completion in both modes; default: "done"
|
||||||
urgent-window: 3 # Skeleton mode only, days; default: 3
|
urgent-window: 3 # Skeleton mode only, days; default: 3
|
||||||
card-detail: hover
|
card-detail: hover
|
||||||
page-size: 10 # positive integer; per quadrant (default: 10)
|
page-size: 10 # positive integer; per quadrant (default: 10)
|
||||||
```
|
```
|
||||||
|
|
||||||
A 2×2 grid with labelled axes. It includes incomplete to-dos admitted by `todos:` plus ordinary notes containing a `gtd` block; `todos:` does not affect notes. **Completed to-dos are excluded entirely** in both modes (a prioritisation view, not a tracking view—the kanban Done column is where completions live). Ordinary notes have no completion or recurrence state. The board is internally positional (top-left / top-right / bottom-left / bottom-right); each mode supplies its own axis and quadrant labels.
|
A 2×2 grid with labelled axes. It includes incomplete to-dos admitted by
|
||||||
|
`todos:` plus unfinished ordinary notes containing a `gtd` block. Completed
|
||||||
|
to-dos and notes carrying `done-tag` are excluded before either mode buckets
|
||||||
|
them, so done wins over active, urgent, and important tags. Native to-dos ignore
|
||||||
|
`done-tag` and remain governed by `todo_completed`. The board is internally
|
||||||
|
positional; each mode supplies its own axis and quadrant labels.
|
||||||
|
|
||||||
**`mode: skeleton`** (default) — rows = active (`in-progress-tag`, the same tag the kanban uses); columns = due soon (resolved card date within `urgent-window` days, overdue included, or the `urgent-tag` as a manual override for dateless items). Quadrants: **Do Next** (active + due soon), **Scheduled** (active, not due soon — includes dateless in-progress work), **On Deck** (not active + due soon — the early-warning quadrant), **Backlog** (neither). Designed to complement the calendar and kanban directly: same in-progress tag, dates drive urgency instead of a second opinion-tag, and nothing is ever condemned as "Eliminate" — a Backlog item is simply low priority. In-progress + `urgent-tag` + no date resolves to Do Next (the urgent tag is an explicit override, so it wins over the absence of a date).
|
**`mode: skeleton`** (default) — rows = active (`in-progress-tag`, the same tag the kanban uses); columns = due soon (resolved card date within `urgent-window` days, overdue included, or the `urgent-tag` as a manual override for dateless items). Quadrants: **Do Next** (active + due soon), **Scheduled** (active, not due soon — includes dateless in-progress work), **On Deck** (not active + due soon — the early-warning quadrant), **Backlog** (neither). Designed to complement the calendar and kanban directly: same in-progress tag, dates drive urgency instead of a second opinion-tag, and nothing is ever condemned as "Eliminate" — a Backlog item is simply low priority. In-progress + `urgent-tag` + no date resolves to Do Next (the urgent tag is an explicit override, so it wins over the absence of a date).
|
||||||
|
|
||||||
|
|||||||
4
TASKS.md
4
TASKS.md
@ -165,9 +165,9 @@ Implementation order: SLICE8, then SLICE9, then SLICE10.
|
|||||||
|
|
||||||
### Documentation and acceptance
|
### Documentation and acceptance
|
||||||
|
|
||||||
- [ ] Document `scope: children` plus `group: notebook` multi-view behavior
|
- [x] Document `scope: children` plus `group: notebook` multi-view behavior
|
||||||
in README.md.
|
in README.md.
|
||||||
- [ ] Add a parent/child/grandchild notebook example to README.md or SPEC.md.
|
- [x] Add a parent/child/grandchild notebook example to README.md or SPEC.md.
|
||||||
- [ ] Update architecture and payload behavior in SPEC.md.
|
- [ ] Update architecture and payload behavior in SPEC.md.
|
||||||
- [ ] Add the feature to CHANGELOG.md.
|
- [ ] Add the feature to CHANGELOG.md.
|
||||||
- [x] Run the full automated test suite and record command/results.
|
- [x] Run the full automated test suite and record command/results.
|
||||||
|
|||||||
@ -1,26 +1,33 @@
|
|||||||
import { GtdBlock, KanbanCard, RawNote, RECURRING_TAG } from "./types";
|
import {
|
||||||
|
DONE_TAG,
|
||||||
|
GtdBlock,
|
||||||
|
KanbanCard,
|
||||||
|
RawNote,
|
||||||
|
RECURRING_TAG,
|
||||||
|
} from "./types";
|
||||||
import { resolveEventDate } from "./resolveDate";
|
import { resolveEventDate } from "./resolveDate";
|
||||||
|
|
||||||
/** Normalize a note or to-do into the shared kanban/matrix card shape. */
|
/** Normalize a note or to-do into the shared kanban/matrix card shape. */
|
||||||
export default function buildKanbanCard(
|
export default function buildKanbanCard(
|
||||||
note: RawNote,
|
note: RawNote,
|
||||||
block: GtdBlock | null,
|
block: GtdBlock | null,
|
||||||
tags: string[]
|
tags: string[],
|
||||||
|
doneTag: string = DONE_TAG
|
||||||
): KanbanCard {
|
): KanbanCard {
|
||||||
const isTodo = Boolean(note.is_todo);
|
const isTodo = Boolean(note.is_todo);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: note.id,
|
id: note.id,
|
||||||
title: block && block.title ? block.title : note.title,
|
title: block && block.title ? block.title : note.title,
|
||||||
date: resolveEventDate(note, block),
|
date: resolveEventDate(note, block),
|
||||||
isTodo,
|
isTodo,
|
||||||
completed: isTodo && note.todo_completed > 0,
|
completed: isTodo ? note.todo_completed > 0 : tags.includes(doneTag),
|
||||||
completedTime: isTodo ? note.todo_completed || 0 : 0,
|
completedTime: isTodo ? note.todo_completed || 0 : 0,
|
||||||
isRecurring: isTodo && tags.includes(RECURRING_TAG),
|
isRecurring: isTodo && tags.includes(RECURRING_TAG),
|
||||||
bgColour: block ? block.bgColour : null,
|
bgColour: block ? block.bgColour : null,
|
||||||
fgColour: block ? block.fgColour : null,
|
fgColour: block ? block.fgColour : null,
|
||||||
icon: block ? block.icon : null,
|
icon: block ? block.icon : null,
|
||||||
text: block ? block.text : null,
|
text: block ? block.text : null,
|
||||||
updatedTime: note.updated_time,
|
updatedTime: note.updated_time,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,12 +96,14 @@ export default async function collectKanban(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tags = await adapter.getNoteTagTitles(note.id);
|
const tags = await adapter.getNoteTagTitles(note.id);
|
||||||
const card = buildKanbanCard(note, result.block, tags);
|
const card = buildKanbanCard(note, result.block, tags, config.doneTag);
|
||||||
|
|
||||||
// Bucketing — Done wins.
|
// Bucketing — Done wins.
|
||||||
if (card.completed) {
|
if (card.completed) {
|
||||||
if (card.completedTime >= doneCutoff) targetBoard.done.push(card);
|
if (!card.isTodo || card.completedTime >= doneCutoff) {
|
||||||
// completed but outside the window: dropped from the board
|
targetBoard.done.push(card);
|
||||||
|
}
|
||||||
|
// Only native to-dos can be completed outside the timestamp window.
|
||||||
} else if (tags.includes(config.inProgressTag)) {
|
} else if (tags.includes(config.inProgressTag)) {
|
||||||
targetBoard.inProgress.push(card);
|
targetBoard.inProgress.push(card);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -104,7 +104,8 @@ export default async function collectMatrix(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tags = await adapter.getNoteTagTitles(note.id);
|
const tags = await adapter.getNoteTagTitles(note.id);
|
||||||
const card = buildKanbanCard(note, result.block, tags);
|
const card = buildKanbanCard(note, result.block, tags, config.doneTag);
|
||||||
|
if (card.completed) continue;
|
||||||
|
|
||||||
let topRow: boolean;
|
let topRow: boolean;
|
||||||
let leftColumn: boolean;
|
let leftColumn: boolean;
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import { KanbanConfig, InclusionMode, KanbanSortType } from "./types";
|
import {
|
||||||
|
DONE_TAG,
|
||||||
|
IN_PROGRESS_TAG,
|
||||||
|
KanbanConfig,
|
||||||
|
InclusionMode,
|
||||||
|
KanbanSortType,
|
||||||
|
} from "./types";
|
||||||
import { parseNotebookOption } from "./resolveNotebook";
|
import { parseNotebookOption } from "./resolveNotebook";
|
||||||
import parsePageSize from "./pageSize";
|
import parsePageSize from "./pageSize";
|
||||||
|
|
||||||
@ -23,6 +29,7 @@ export default function parseKanbanConfig(raw: any): KanbanConfig {
|
|||||||
"sort-type",
|
"sort-type",
|
||||||
"sort",
|
"sort",
|
||||||
"in-progress-tag",
|
"in-progress-tag",
|
||||||
|
"done-tag",
|
||||||
"card-detail",
|
"card-detail",
|
||||||
"page-size",
|
"page-size",
|
||||||
"done-window",
|
"done-window",
|
||||||
@ -60,9 +67,7 @@ export default function parseKanbanConfig(raw: any): KanbanConfig {
|
|||||||
if (input.group !== undefined) {
|
if (input.group !== undefined) {
|
||||||
const candidate = String(input.group).trim().toLowerCase();
|
const candidate = String(input.group).trim().toLowerCase();
|
||||||
if (candidate !== "notebook") {
|
if (candidate !== "notebook") {
|
||||||
warnings.push(
|
warnings.push(`Invalid group "${input.group}" (grouping disabled)`);
|
||||||
`Invalid group "${input.group}" (grouping disabled)`
|
|
||||||
);
|
|
||||||
} else if (!childrenScope) {
|
} else if (!childrenScope) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
'"group: notebook" requires scope: children (grouping disabled)'
|
'"group: notebook" requires scope: children (grouping disabled)'
|
||||||
@ -116,7 +121,7 @@ export default function parseKanbanConfig(raw: any): KanbanConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// in-progress tag (configurable; lowercased to match tag comparison)
|
// in-progress tag (configurable; lowercased to match tag comparison)
|
||||||
let inProgressTag = "in-progress";
|
let inProgressTag = IN_PROGRESS_TAG;
|
||||||
if (
|
if (
|
||||||
input["in-progress-tag"] !== undefined &&
|
input["in-progress-tag"] !== undefined &&
|
||||||
input["in-progress-tag"] !== null &&
|
input["in-progress-tag"] !== null &&
|
||||||
@ -124,6 +129,19 @@ export default function parseKanbanConfig(raw: any): KanbanConfig {
|
|||||||
) {
|
) {
|
||||||
inProgressTag = String(input["in-progress-tag"]).trim().toLowerCase();
|
inProgressTag = String(input["in-progress-tag"]).trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
let doneTag = DONE_TAG;
|
||||||
|
if (
|
||||||
|
input["done-tag"] !== undefined &&
|
||||||
|
input["done-tag"] !== null &&
|
||||||
|
String(input["done-tag"]).trim() !== ""
|
||||||
|
) {
|
||||||
|
doneTag = String(input["done-tag"]).trim().toLowerCase();
|
||||||
|
}
|
||||||
|
if (doneTag === inProgressTag) {
|
||||||
|
warnings.push(
|
||||||
|
`done-tag and in-progress-tag are both "${doneTag}" — done takes priority`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// card-detail
|
// card-detail
|
||||||
let cardDetail: KanbanConfig["cardDetail"] = "hover";
|
let cardDetail: KanbanConfig["cardDetail"] = "hover";
|
||||||
@ -163,6 +181,7 @@ export default function parseKanbanConfig(raw: any): KanbanConfig {
|
|||||||
sortType,
|
sortType,
|
||||||
sort,
|
sort,
|
||||||
inProgressTag,
|
inProgressTag,
|
||||||
|
doneTag,
|
||||||
cardDetail,
|
cardDetail,
|
||||||
pageSize,
|
pageSize,
|
||||||
doneWindow,
|
doneWindow,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
URGENT_TAG,
|
URGENT_TAG,
|
||||||
IMPORTANT_TAG,
|
IMPORTANT_TAG,
|
||||||
IN_PROGRESS_TAG,
|
IN_PROGRESS_TAG,
|
||||||
|
DONE_TAG,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { parseNotebookOption } from "./resolveNotebook";
|
import { parseNotebookOption } from "./resolveNotebook";
|
||||||
import parsePageSize from "./pageSize";
|
import parsePageSize from "./pageSize";
|
||||||
@ -34,6 +35,7 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
|||||||
"urgent-tag",
|
"urgent-tag",
|
||||||
"important-tag",
|
"important-tag",
|
||||||
"in-progress-tag",
|
"in-progress-tag",
|
||||||
|
"done-tag",
|
||||||
"urgent-window",
|
"urgent-window",
|
||||||
"card-detail",
|
"card-detail",
|
||||||
"page-size",
|
"page-size",
|
||||||
@ -82,9 +84,7 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
|||||||
if (input.group !== undefined) {
|
if (input.group !== undefined) {
|
||||||
const candidate = String(input.group).trim().toLowerCase();
|
const candidate = String(input.group).trim().toLowerCase();
|
||||||
if (candidate !== "notebook") {
|
if (candidate !== "notebook") {
|
||||||
warnings.push(
|
warnings.push(`Invalid group "${input.group}" (grouping disabled)`);
|
||||||
`Invalid group "${input.group}" (grouping disabled)`
|
|
||||||
);
|
|
||||||
} else if (!childrenScope) {
|
} else if (!childrenScope) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
'"group: notebook" requires scope: children (grouping disabled)'
|
'"group: notebook" requires scope: children (grouping disabled)'
|
||||||
@ -148,6 +148,12 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
|||||||
|
|
||||||
// skeleton mode: in-progress tag + urgent window
|
// skeleton mode: in-progress tag + urgent window
|
||||||
const inProgressTag = parseTag(input["in-progress-tag"], IN_PROGRESS_TAG);
|
const inProgressTag = parseTag(input["in-progress-tag"], IN_PROGRESS_TAG);
|
||||||
|
const doneTag = parseTag(input["done-tag"], DONE_TAG);
|
||||||
|
if (doneTag === inProgressTag) {
|
||||||
|
warnings.push(
|
||||||
|
`done-tag and in-progress-tag are both "${doneTag}" — done takes priority`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let urgentWindow = 3;
|
let urgentWindow = 3;
|
||||||
if (input["urgent-window"] !== undefined) {
|
if (input["urgent-window"] !== undefined) {
|
||||||
@ -187,6 +193,7 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
|||||||
urgentTag,
|
urgentTag,
|
||||||
importantTag,
|
importantTag,
|
||||||
inProgressTag,
|
inProgressTag,
|
||||||
|
doneTag,
|
||||||
urgentWindow,
|
urgentWindow,
|
||||||
cardDetail,
|
cardDetail,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
|||||||
@ -118,6 +118,9 @@ export const RECURRING_TAG = "recurring";
|
|||||||
*/
|
*/
|
||||||
export const IN_PROGRESS_TAG = "in-progress";
|
export const IN_PROGRESS_TAG = "in-progress";
|
||||||
|
|
||||||
|
/** Default tag that marks an opted-in ordinary note complete. */
|
||||||
|
export const DONE_TAG = "done";
|
||||||
|
|
||||||
export type KanbanSortType = "due-date" | "title" | "modified-date";
|
export type KanbanSortType = "due-date" | "title" | "modified-date";
|
||||||
|
|
||||||
/** Parsed and normalised ```gtd-kanban config. */
|
/** Parsed and normalised ```gtd-kanban config. */
|
||||||
@ -134,6 +137,8 @@ export interface KanbanConfig {
|
|||||||
sortType: KanbanSortType;
|
sortType: KanbanSortType;
|
||||||
sort: "asc" | "desc";
|
sort: "asc" | "desc";
|
||||||
inProgressTag: string;
|
inProgressTag: string;
|
||||||
|
/** Tag that marks an opted-in ordinary note complete. */
|
||||||
|
doneTag: string;
|
||||||
cardDetail: "hover" | "always" | "none";
|
cardDetail: "hover" | "always" | "none";
|
||||||
/** Cards initially shown, and revealed per "List more" action. */
|
/** Cards initially shown, and revealed per "List more" action. */
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
@ -207,6 +212,8 @@ export interface MatrixConfig {
|
|||||||
importantTag: string;
|
importantTag: string;
|
||||||
/** Skeleton mode: tag marking active work (shared with the kanban). */
|
/** Skeleton mode: tag marking active work (shared with the kanban). */
|
||||||
inProgressTag: string;
|
inProgressTag: string;
|
||||||
|
/** Tag that marks an opted-in ordinary note complete. */
|
||||||
|
doneTag: string;
|
||||||
/** Skeleton mode: days ahead within which a due date counts as "due soon". */
|
/** Skeleton mode: days ahead within which a due date counts as "due soon". */
|
||||||
urgentWindow: number;
|
urgentWindow: number;
|
||||||
cardDetail: "hover" | "always" | "none";
|
cardDetail: "hover" | "always" | "none";
|
||||||
|
|||||||
@ -2,90 +2,111 @@ import buildKanbanCard from "../../Gtd/buildKanbanCard";
|
|||||||
import { GtdBlock, RawNote } from "../../Gtd/types";
|
import { GtdBlock, RawNote } from "../../Gtd/types";
|
||||||
|
|
||||||
function makeNote(overrides: Partial<RawNote> = {}): RawNote {
|
function makeNote(overrides: Partial<RawNote> = {}): RawNote {
|
||||||
return {
|
return {
|
||||||
id: "note-id",
|
id: "note-id",
|
||||||
title: "Source title",
|
title: "Source title",
|
||||||
parent_id: "folder-id",
|
parent_id: "folder-id",
|
||||||
is_todo: 1,
|
is_todo: 1,
|
||||||
todo_due: new Date(2026, 6, 14).getTime(),
|
todo_due: new Date(2026, 6, 14).getTime(),
|
||||||
todo_completed: 0,
|
todo_completed: 0,
|
||||||
updated_time: 123,
|
updated_time: 123,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeBlock(overrides: Partial<GtdBlock> = {}): GtdBlock {
|
function makeBlock(overrides: Partial<GtdBlock> = {}): GtdBlock {
|
||||||
return {
|
return {
|
||||||
date: null,
|
date: null,
|
||||||
bgColour: null,
|
bgColour: null,
|
||||||
fgColour: null,
|
fgColour: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
title: null,
|
title: null,
|
||||||
text: null,
|
text: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("buildKanbanCard", () => {
|
describe("buildKanbanCard", () => {
|
||||||
test("preserves to-do completion, recurrence, and due-date behavior", () => {
|
test("preserves to-do completion, recurrence, and due-date behavior", () => {
|
||||||
const completedTime = new Date(2026, 6, 15).getTime();
|
const completedTime = new Date(2026, 6, 15).getTime();
|
||||||
const card = buildKanbanCard(
|
const card = buildKanbanCard(
|
||||||
makeNote({ todo_completed: completedTime }),
|
makeNote({ todo_completed: completedTime }),
|
||||||
null,
|
null,
|
||||||
["recurring"]
|
["recurring"]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(card).toMatchObject({
|
expect(card).toMatchObject({
|
||||||
isTodo: true,
|
isTodo: true,
|
||||||
completed: true,
|
completed: true,
|
||||||
completedTime,
|
completedTime,
|
||||||
isRecurring: true,
|
isRecurring: true,
|
||||||
date: "2026-07-14",
|
date: "2026-07-14",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("normalizes notes as incomplete and non-recurring", () => {
|
test("normalizes unfinished notes as incomplete and non-recurring", () => {
|
||||||
const card = buildKanbanCard(
|
const card = buildKanbanCard(
|
||||||
makeNote({ is_todo: 0, todo_completed: 999 }),
|
makeNote({ is_todo: 0, todo_completed: 999 }),
|
||||||
makeBlock({ date: "2026-08-02" }),
|
makeBlock({ date: "2026-08-02" }),
|
||||||
["recurring"]
|
["recurring"]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(card).toMatchObject({
|
expect(card).toMatchObject({
|
||||||
isTodo: false,
|
isTodo: false,
|
||||||
completed: false,
|
completed: false,
|
||||||
completedTime: 0,
|
completedTime: 0,
|
||||||
isRecurring: false,
|
isRecurring: false,
|
||||||
date: "2026-08-02",
|
date: "2026-08-02",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ignores todo_due for a normal note", () => {
|
test("marks ordinary notes complete from the configured done tag", () => {
|
||||||
const card = buildKanbanCard(makeNote({ is_todo: 0 }), null, []);
|
const card = buildKanbanCard(
|
||||||
expect(card.date).toBeNull();
|
makeNote({ is_todo: 0, todo_completed: 999 }),
|
||||||
});
|
null,
|
||||||
|
["finished", "recurring"],
|
||||||
|
"finished"
|
||||||
|
);
|
||||||
|
|
||||||
test("applies gtd card overrides without changing the source identity", () => {
|
expect(card).toMatchObject({
|
||||||
const card = buildKanbanCard(
|
isTodo: false,
|
||||||
makeNote(),
|
completed: true,
|
||||||
makeBlock({
|
completedTime: 0,
|
||||||
title: "Block title",
|
isRecurring: false,
|
||||||
bgColour: "#112233",
|
});
|
||||||
fgColour: "#ffffff",
|
});
|
||||||
icon: "!",
|
|
||||||
text: "Details",
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(card).toMatchObject({
|
test("does not let the done tag complete a native to-do", () => {
|
||||||
id: "note-id",
|
const card = buildKanbanCard(makeNote(), null, ["done"]);
|
||||||
title: "Block title",
|
expect(card).toMatchObject({ isTodo: true, completed: false });
|
||||||
bgColour: "#112233",
|
});
|
||||||
fgColour: "#ffffff",
|
|
||||||
icon: "!",
|
test("ignores todo_due for a normal note", () => {
|
||||||
text: "Details",
|
const card = buildKanbanCard(makeNote({ is_todo: 0 }), null, []);
|
||||||
updatedTime: 123,
|
expect(card.date).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
test("applies gtd card overrides without changing the source identity", () => {
|
||||||
|
const card = buildKanbanCard(
|
||||||
|
makeNote(),
|
||||||
|
makeBlock({
|
||||||
|
title: "Block title",
|
||||||
|
bgColour: "#112233",
|
||||||
|
fgColour: "#ffffff",
|
||||||
|
icon: "!",
|
||||||
|
text: "Details",
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(card).toMatchObject({
|
||||||
|
id: "note-id",
|
||||||
|
title: "Block title",
|
||||||
|
bgColour: "#112233",
|
||||||
|
fgColour: "#ffffff",
|
||||||
|
icon: "!",
|
||||||
|
text: "Details",
|
||||||
|
updatedTime: 123,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,6 +16,7 @@ describe("parseKanbanConfig", () => {
|
|||||||
sortType: "due-date",
|
sortType: "due-date",
|
||||||
sort: "asc",
|
sort: "asc",
|
||||||
inProgressTag: "in-progress",
|
inProgressTag: "in-progress",
|
||||||
|
doneTag: "done",
|
||||||
cardDetail: "hover",
|
cardDetail: "hover",
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
doneWindow: 7,
|
doneWindow: 7,
|
||||||
@ -23,6 +24,23 @@ describe("parseKanbanConfig", () => {
|
|||||||
expect(c.warnings).toHaveLength(0);
|
expect(c.warnings).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parses and normalizes done-tag with done-over-in-progress warning", () => {
|
||||||
|
const custom = parseKanbanConfig({ "done-tag": " Finished " });
|
||||||
|
expect(custom.doneTag).toBe("finished");
|
||||||
|
expect(custom.warnings).toHaveLength(0);
|
||||||
|
|
||||||
|
const empty = parseKanbanConfig({ "done-tag": " " });
|
||||||
|
expect(empty.doneTag).toBe("done");
|
||||||
|
|
||||||
|
const same = parseKanbanConfig({
|
||||||
|
"done-tag": "WIP",
|
||||||
|
"in-progress-tag": "wip",
|
||||||
|
});
|
||||||
|
expect(same.warnings).toEqual([
|
||||||
|
'done-tag and in-progress-tag are both "wip" — done takes priority',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("parses a full config", () => {
|
test("parses a full config", () => {
|
||||||
const c = parseKanbanConfig({
|
const c = parseKanbanConfig({
|
||||||
title: "Board",
|
title: "Board",
|
||||||
@ -443,6 +461,46 @@ describe("collectKanban — mixed note/to-do contracts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("collectKanban — done window", () => {
|
describe("collectKanban — done window", () => {
|
||||||
|
test("done notes win over in-progress and bypass the to-do done window", async () => {
|
||||||
|
const notes = [
|
||||||
|
makeNote({ id: "done-note", is_todo: 0, title: "Done note" }),
|
||||||
|
makeNote({ id: "tagged-todo", title: "Still open" }),
|
||||||
|
makeNote({ id: "old-todo", todo_completed: daysAgo(30) }),
|
||||||
|
];
|
||||||
|
const result = await collectKanban(
|
||||||
|
makeAdapter(folders, notes, {
|
||||||
|
"done-note": ["done", "in-progress"],
|
||||||
|
"tagged-todo": ["done"],
|
||||||
|
}),
|
||||||
|
"kanban-note",
|
||||||
|
"board",
|
||||||
|
parseKanbanConfig({ todos: "all", "done-window": 0 }),
|
||||||
|
NOW
|
||||||
|
);
|
||||||
|
expect(result.board.done.map((card) => card.id)).toEqual(["done-note"]);
|
||||||
|
expect(result.board.backlog.map((card) => card.id)).toEqual([
|
||||||
|
"tagged-todo",
|
||||||
|
]);
|
||||||
|
expect(result.board.inProgress).toHaveLength(0);
|
||||||
|
expect(result.board.done[0]).toMatchObject({
|
||||||
|
isTodo: false,
|
||||||
|
completed: true,
|
||||||
|
completedTime: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("honours a custom done tag for ordinary notes", async () => {
|
||||||
|
const result = await collectKanban(
|
||||||
|
makeAdapter(folders, [makeNote({ id: "note", is_todo: 0 })], {
|
||||||
|
note: ["finished"],
|
||||||
|
}),
|
||||||
|
"kanban-note",
|
||||||
|
"board",
|
||||||
|
parseKanbanConfig({ todos: "none", "done-tag": "finished" }),
|
||||||
|
NOW
|
||||||
|
);
|
||||||
|
expect(result.board.done.map((card) => card.id)).toEqual(["note"]);
|
||||||
|
});
|
||||||
test("only completions within the window appear in Done", async () => {
|
test("only completions within the window appear in Done", async () => {
|
||||||
const notes = [
|
const notes = [
|
||||||
makeNote({ id: "recent", todo_completed: daysAgo(3) }),
|
makeNote({ id: "recent", todo_completed: daysAgo(3) }),
|
||||||
@ -553,6 +611,32 @@ describe("collectKanban — recurrence flag", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("collectKanban — notebook groups", () => {
|
describe("collectKanban — notebook groups", () => {
|
||||||
|
test("keeps completed notes in their exact-owner Done groups", async () => {
|
||||||
|
const tree: RawFolder[] = [
|
||||||
|
{ id: "root", parent_id: "", title: "Root" },
|
||||||
|
{ id: "child", parent_id: "root", title: "Child" },
|
||||||
|
];
|
||||||
|
const result = await collectKanban(
|
||||||
|
makeAdapter(
|
||||||
|
tree,
|
||||||
|
[makeNote({ id: "done-note", parent_id: "child", is_todo: 0 })],
|
||||||
|
{ "done-note": ["done", "in-progress"] }
|
||||||
|
),
|
||||||
|
"host",
|
||||||
|
"root",
|
||||||
|
parseKanbanConfig({ scope: "children", group: "notebook" }),
|
||||||
|
NOW
|
||||||
|
);
|
||||||
|
expect(result.layout.kind).toBe("notebooks");
|
||||||
|
if (result.layout.kind !== "notebooks")
|
||||||
|
throw new Error("grouped layout expected");
|
||||||
|
expect(result.layout.groups).toHaveLength(1);
|
||||||
|
expect(result.layout.groups[0].folderId).toBe("child");
|
||||||
|
expect(result.layout.groups[0].board.done.map((card) => card.id)).toEqual([
|
||||||
|
"done-note",
|
||||||
|
]);
|
||||||
|
expect(result.cardCount).toBe(1);
|
||||||
|
});
|
||||||
test("builds sorted exact-owner groups and omits empty folders", async () => {
|
test("builds sorted exact-owner groups and omits empty folders", async () => {
|
||||||
const tree: RawFolder[] = [
|
const tree: RawFolder[] = [
|
||||||
{ id: "root-board", parent_id: "", title: "Projects" },
|
{ id: "root-board", parent_id: "", title: "Projects" },
|
||||||
|
|||||||
@ -19,6 +19,7 @@ describe("parseMatrixConfig", () => {
|
|||||||
urgentTag: "urgent",
|
urgentTag: "urgent",
|
||||||
importantTag: "important",
|
importantTag: "important",
|
||||||
inProgressTag: "in-progress",
|
inProgressTag: "in-progress",
|
||||||
|
doneTag: "done",
|
||||||
urgentWindow: 3,
|
urgentWindow: 3,
|
||||||
cardDetail: "hover",
|
cardDetail: "hover",
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@ -26,6 +27,20 @@ describe("parseMatrixConfig", () => {
|
|||||||
expect(c.warnings).toHaveLength(0);
|
expect(c.warnings).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parses done-tag and warns when it matches in-progress-tag", () => {
|
||||||
|
expect(parseMatrixConfig({ "done-tag": " Finished " }).doneTag).toBe(
|
||||||
|
"finished"
|
||||||
|
);
|
||||||
|
expect(parseMatrixConfig({ "done-tag": "" }).doneTag).toBe("done");
|
||||||
|
const same = parseMatrixConfig({
|
||||||
|
"done-tag": "Active",
|
||||||
|
"in-progress-tag": "active",
|
||||||
|
});
|
||||||
|
expect(same.warnings).toEqual([
|
||||||
|
'done-tag and in-progress-tag are both "active" — done takes priority',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
[1, 1],
|
[1, 1],
|
||||||
[10, 10],
|
[10, 10],
|
||||||
@ -312,6 +327,44 @@ describe("collectMatrix — quadrant bucketing", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("collectMatrix — mixed note/to-do contracts", () => {
|
describe("collectMatrix — mixed note/to-do contracts", () => {
|
||||||
|
test.each(["skeleton", "eisenhower"] as const)(
|
||||||
|
"excludes done notes before %s bucketing while ignoring done on open to-dos",
|
||||||
|
async (mode) => {
|
||||||
|
const notes = [
|
||||||
|
makeNote({ id: "done-note", is_todo: 0 }),
|
||||||
|
makeNote({ id: "open-todo" }),
|
||||||
|
];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes, {
|
||||||
|
"done-note": ["done", "in-progress", "urgent", "important"],
|
||||||
|
"open-todo": ["done"],
|
||||||
|
}),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all", mode })
|
||||||
|
);
|
||||||
|
expect(result.cardCount).toBe(1);
|
||||||
|
const ids = [
|
||||||
|
...result.board.topLeft,
|
||||||
|
...result.board.topRight,
|
||||||
|
...result.board.bottomLeft,
|
||||||
|
...result.board.bottomRight,
|
||||||
|
].map((card) => card.id);
|
||||||
|
expect(ids).toEqual(["open-todo"]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test("honours a custom done tag for note exclusion", async () => {
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, [makeNote({ id: "note", is_todo: 0 })], {
|
||||||
|
note: ["finished"],
|
||||||
|
}),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ "done-tag": "finished" })
|
||||||
|
);
|
||||||
|
expect(result.cardCount).toBe(0);
|
||||||
|
});
|
||||||
test("handles empty, malformed, absent, and host note blocks", async () => {
|
test("handles empty, malformed, absent, and host note blocks", async () => {
|
||||||
const notes = [
|
const notes = [
|
||||||
makeNote({ id: "empty-note", is_todo: 0 }),
|
makeNote({ id: "empty-note", is_todo: 0 }),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user