diff --git a/CHANGELOG.md b/CHANGELOG.md index d9a5b2b..eebd423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,35 @@ 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 +## 1.0.0 β€” 2026-07-28 + +### Card underline cleanup + +- Removed the inherited clickable underline from kanban and matrix cards for + both notes and to-dos. +- Completed cards retain their line-through styling; other clickable views and + card interactions are unchanged. + +### Independent project-note filters + +- Added `notes: all | none` to `gtd-kanban` and `gtd-matrix`, defaulting to + `all` for backward compatibility. +- `notes: all` continues to require explicit ordinary-note opt-in through a + `gtd` block; `notes: none` enables task-only boards and matrices. +- Project-note filtering is independent from `todos`, happens before warnings + and tag access, and preserves grouped-view totals and empty-group omission. + +### Notebook-grouped kanban and matrix views + +- Added explicit `group: notebook` support when paired with literal + `scope: children`, rendering separate exact-owner views for the root and each + non-empty descendant notebook. +- Cards appear once under their full notebook path; empty parents are omitted + without hiding non-empty descendants. +- Each generated column or quadrant retains independent pagination, while + warnings remain with their owning notebook and statistics remain block-wide. + +### 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. @@ -12,24 +40,25 @@ All notable changes to GTD Calendar are documented here. Versions follow the plu - 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 +### Opted-in note cards for kanban and matrix - Ordinary notes containing a `gtd` block now appear in kanban and matrix views. Empty blocks opt in; plain notes without the block remain excluded. -- Kanban places note cards in Backlog or In Progress from the configured tag. - Notes never enter Done and never display completion or recurrence state. +- Kanban places note cards in Backlog or In Progress from the configured tag; + the later `done-tag` addition places completed project notes in Done. + Ordinary notes never display recurrence state. - Skeleton and Eisenhower matrices apply their existing date/tag axes to note cards. Completed to-dos remain excluded. - Note cards reuse `gtd` title, date, colour, icon, and text overrides. They use πŸ“„ when no custom icon is provided and click through to the source note. - Malformed `gtd` blocks remain visible with fallback values and the existing warning. The `todos:` option continues to govern only to-dos. -- Kanban and matrix always inspect in-scope note bodies to discover explicit - note opt-in; tag requests remain limited to eligible items. +- Kanban and matrix inspect bodies whenever an admitted type can use a `gtd` + block; tag requests remain limited to eligible items. - Validation: 185 automated tests passed, the production JPL was built, and manual Joplin acceptance passed on 2026-07-27. -## Unreleased β€” Incremental kanban and matrix card limits +### Incremental kanban and matrix card limits - Added `page-size` to `gtd-kanban` and `gtd-matrix` (positive integer, default 10); invalid values warn and fall back to 10. - Each kanban column and matrix quadrant initially shows one batch. **List more** reveals the next batch independently without changing saved configuration; reload resets expansion. diff --git a/README.md b/README.md index 49f7a67..31e3b00 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ not filtered by `done-window` because they have no completion timestamp. The | `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. | | `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. | +| `notes` | `all`, `none` | `all` | Which project notes appear. `all` means every ordinary note in scope that explicitly opts in with a `gtd` block; it never includes plain notes without that block. | | `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` | `asc`, `desc` | `asc` | Sort direction. | @@ -141,6 +142,31 @@ not filtered by `done-window` because they have no completion timestamp. The | `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. | +Use the two independent type filters to build focused views: + +```yaml +# Projects only +notes: all +todos: none +``` + +```yaml +# Tasks only +notes: none +todos: all +``` + +```yaml +# Projects and tasks +notes: all +todos: all +``` + +Omitting `notes` preserves existing behavior (`notes: all`). Project notes still +require a `gtd` block and continue through the normal Backlog, In Progress, and +Done rules. `notes: none` filters them before warnings, tags, totals, grouping, +sorting, and pagination. + 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. ### Separate views for child notebooks @@ -245,6 +271,7 @@ Quadrants from the urgent/important tags: Do First (both), Schedule (important), | `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. | | `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. | +| `notes` | `all`, `none` | `all` | Which project notes appear. `all` includes only ordinary notes explicitly opted in with a `gtd` block; `none` produces a task-only matrix. | | `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` | `asc`, `desc` | `asc` | Sort direction. | @@ -338,7 +365,7 @@ Issues and ideas: [the repository](https://gitea.skeletonworks.online/vwiebe/jop ```bash npm install -npm test # 240 unit tests +npm test # 251 unit tests npm run dist # builds publish/*.jpl ``` diff --git a/SLICE10.md b/SLICE10.md index 0693184..ba22ce9 100644 --- a/SLICE10.md +++ b/SLICE10.md @@ -6,8 +6,9 @@ ## Status -**IN PROGRESS.** Phases 1–6 and Phase 8 manual acceptance are complete. Phase 7 -(documentation) is the only remaining phase. +**COMPLETE.** Phases 1–8 are implemented and validated. Automated checks and +manual Joplin acceptance passed; README, SPEC, and v1.0.0 CHANGELOG coverage are +complete. ## Goal @@ -370,10 +371,9 @@ one additional rendering decision. aggregated. - [x] Explain that every generated bucket has independent SLICE8 expansion state. - [x] Explain that SLICE9 notes group by their owning notebook. -- [ ] Update SPEC.md scope parsing, folder traversal, layout payloads, warning +- [x] Update SPEC.md scope parsing, folder traversal, layout payloads, warning ownership, statistics, and rendering flow. -- [ ] Add an unreleased SLICE10 entry to CHANGELOG.md without changing the - package version until release scope is decided. +- [x] Add the SLICE10 feature to the v1.0.0 CHANGELOG entry. ### Phase 7 README progress record @@ -386,8 +386,7 @@ one additional rendering decision. 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. +- `SPEC.md` and the v1.0.0 `CHANGELOG.md` now record the grouped layout contract. ### Phase 8 β€” Manual Joplin acceptance diff --git a/SLICE12.md b/SLICE12.md new file mode 100644 index 0000000..dbc2c1e --- /dev/null +++ b/SLICE12.md @@ -0,0 +1,276 @@ +# SLICE 12 β€” Filter project notes independently from task to-dos + +> **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 1–7 are implemented and validated. Focused and full +automation passed, the production JPL was built, and the user confirmed manual +Joplin acceptance on 2026-07-28. + +## Goal + +Allow `gtd-kanban` and `gtd-matrix` blocks to include or exclude opted-in +ordinary notes independently from native Joplin to-dos. The user treats +ordinary `gtd` notes as projects and native to-dos as tasks, and needs focused +project-only, task-only, and combined views. + +```yaml +# Projects only +notes: all +todos: none +``` + +```yaml +# Tasks only +notes: none +todos: all +``` + +```yaml +# Projects and tasks +notes: all +todos: all +``` + +## Confirmed configuration contract + +- The option is named `notes`, plural, matching the existing `todos` option and + the calendar configuration vocabulary. +- `notes` is valid only in `gtd-kanban` and `gtd-matrix` blocks in this slice. +- Accepted values are `all` and `none`. +- The default is `all`, preserving current behavior for existing blocks. +- `notes: all` means all **eligible ordinary notes containing a found `gtd` + block**. It never means every plain note in scope. +- `notes: none` excludes every ordinary note, including notes tagged + `in-progress`, `done`, `urgent`, or `important`. +- A tag alone never opts an ordinary note into a view. +- The existing `todos` contract remains unchanged: `gtd-only | all | none`, + defaulting to `gtd-only`. +- `notes` and `todos` are independent. Every valid combination is supported. +- Invalid or empty `notes` values warn and fall back to `all`. +- The singular key `note` remains unknown and produces the normal unknown-option + warning; no alias is introduced. + +## Inclusion table + +| Item | `gtd` block | `notes` | `todos` | Result | +|---|---:|---|---|---| +| Ordinary note | yes | `all` | any | Included, then normal view rules apply | +| Ordinary note | yes | `none` | any | Excluded | +| Ordinary note | no | `all` | any | Excluded | +| Ordinary note | no | `none` | any | Excluded | +| Native to-do | yes | any | `gtd-only` or `all` | Included, subject to completion rules | +| Native to-do | no | any | `all` | Included, subject to completion rules | +| Native to-do | no | any | `gtd-only` or `none` | Excluded | + +The host dashboard note remains excluded regardless of its type, block, or +configuration. + +## Interaction with existing behavior + +### Kanban + +- `notes: all` preserves SLICE9 and SLICE11 note behavior. +- An admitted unfinished note uses `in-progress-tag` to select In Progress or + Backlog. +- An admitted note carrying `done-tag` enters Done, even if it also carries + `in-progress-tag`. +- Completed ordinary notes remain exempt from the native to-do `done-window`. +- `notes: none` excludes ordinary notes before tag lookup, card construction, + completion checks, sorting, grouping, statistics, and pagination. +- Native to-do behavior is unaffected by `notes`. + +### Matrix + +- `notes: all` preserves both Skeleton and Eisenhower note behavior. +- Admitted notes carrying `done-tag` remain excluded before quadrant bucketing. +- Other admitted notes use the existing date and tag axes for the selected mode. +- `notes: none` excludes ordinary notes before tag lookup, completion checks, + quadrant bucketing, sorting, grouping, statistics, and pagination. +- Native completed to-dos remain excluded, and incomplete to-do behavior is + unaffected by `notes`. + +### Notebook grouping and statistics + +- SLICE10 exact-owner grouping remains unchanged. +- A filtered note must not create a notebook group, warning, card count, or + pagination state. +- A group containing only filtered notes is omitted. +- Non-empty descendant groups remain visible even when their ancestors become + empty after filtering. +- `scannedFolders` and `scannedNotes` continue to describe scan work, not admitted + cards; `cardCount`, bucket totals, headings, and pagination use admitted cards. +- Malformed-block warnings are emitted only for items admitted by their type + filter. A malformed ordinary note under `notes: none` produces no warning. + +### Rendering and payload + +- No new rendering branch is required. Collectors return the same card and + grouped-layout payloads with filtered arrays and totals. +- SLICE8 independent `page-size` expansion state remains per rendered bucket. +- Empty project-only or task-only results use the existing empty-state behavior. +- Card glyphs, styling, drilldown, recurrence, and read-only behavior do not + change. + +## Data-access and performance rules + +Filtering should occur as early as correctness permits: + +```text +scan note metadata/body + | + +-- host note -> exclude + +-- ordinary note + notes:none -> exclude + +-- to-do rejected by todos -> exclude + | + v +parse/admit gtd block -> warnings -> tag lookup -> card -> bucket +``` + +- Do not request tags for an item rejected by `notes` or `todos`. +- Ordinary notes require bodies to prove explicit `gtd` opt-in when + `notes: all`. +- `todos: gtd-only` requires bodies to determine to-do admission. +- `todos: all` still needs bodies because an optional `gtd` block can override + the to-do card date, title, colours, icon, and text. Do not optimize this away. +- Retain current body fetching whenever either admitted type can use a `gtd` + block. Any no-body optimization must be proven not to alter card overrides, + warnings, scan counts, or host-note handling. + +## Architecture + +Add a note-specific inclusion type rather than reusing `InclusionMode`, because +`gtd-only` would be redundant: all ordinary notes are already explicitly +`gtd`-only. + +```ts +type NoteInclusionMode = "all" | "none"; + +interface KanbanConfig { + notes: NoteInclusionMode; + // existing fields +} + +interface MatrixConfig { + notes: NoteInclusionMode; + // existing fields +} +``` + +Both collectors should apply the type filter before malformed-block warnings, +tag requests, and card construction. Keep parsing and filtering behavior shared +in intent, but avoid an abstraction that obscures the different kanban and +matrix completion paths. + +## Implementation plan + +### Phase 1 β€” Configuration and types + +- [x] Add `NoteInclusionMode = "all" | "none"`. +- [x] Add `notes` to `KanbanConfig` and `MatrixConfig`. +- [x] Recognize `notes` in both parsers. +- [x] Parse case-insensitive `all | none`, defaulting to `all`. +- [x] Warn on invalid, empty, null, array, or object values and use `all`. +- [x] Preserve the unknown-option warning for singular `note`. +- [x] Add parser tests for defaults, valid values, normalization, invalid values, + and coexistence with every `todos` mode. + +### Phase 2 β€” Kanban filtering + +- [x] Exclude ordinary notes immediately when `config.notes === "none"`. +- [x] Preserve explicit `gtd` opt-in when `config.notes === "all"`. +- [x] Ensure rejected notes trigger no malformed-block warning or tag request. +- [x] Preserve Backlog, In Progress, Done, done-tag precedence, done-window, + sorting, and native to-do behavior. +- [x] Cover project-only, task-only, combined, and fully empty configurations. +- [x] Cover empty, valid, and malformed `gtd` blocks. + +### Phase 3 β€” Matrix filtering + +- [x] Exclude ordinary notes immediately when `config.notes === "none"`. +- [x] Preserve explicit `gtd` opt-in when `config.notes === "all"`. +- [x] Ensure rejected notes trigger no malformed-block warning or tag request. +- [x] Preserve done-note exclusion and native completed-to-do exclusion. +- [x] Cover project-only, task-only, combined, and fully empty configurations in + both Skeleton and Eisenhower modes. +- [x] Cover empty, valid, and malformed `gtd` blocks. + +### Phase 4 β€” Grouping, totals, and performance + +- [x] Verify single-layout `cardCount` and all bucket totals after filtering. +- [x] Verify exact-owner notebook groups omit groups emptied by filtering. +- [x] Verify non-empty descendants survive filtered empty ancestors. +- [x] Verify filtered notes do not generate group-local warnings. +- [x] Verify each surviving bucket retains independent SLICE8 pagination. +- [x] Assert no tag request occurs for filtered notes. +- [x] Audit `getNotesInFolder` body-field hints without removing to-do `gtd` + overrides or malformed-block warnings. + +### Phase 5 β€” Documentation + +- [x] Add `notes: all | none` to README kanban and matrix option tables. +- [x] Document that `all` means all opted-in ordinary `gtd` notes. +- [x] Add project-only, task-only, and combined examples. +- [x] Document independence from `todos`, note completion behavior, grouped-view + effects, and the default-preserving migration story. +- [x] Update SPEC.md configuration, inclusion matrix, collector flow, statistics, + and performance notes. +- [x] Add an unreleased CHANGELOG entry. + +### Phase 6 β€” Automated validation and packaging + +- [x] Run focused parser, kanban, matrix, grouping, and field-hint tests + (117 passed). +- [x] Run the complete Jest suite (251 tests across 16 suites). +- [x] Run TypeScript validation and webview JavaScript syntax checking. +- [x] Run whitespace and prohibited-reference audits. +- [x] Build the production JPL. +- [x] Inspect archive contents; SHA-256: + final v1.0.0 artifact + `0b28fb0f5ffcb60d92e017fb0972ecaa456fcc3c24b144891dce7d72dc7d98ef`. + +### Phase 7 β€” Manual Joplin acceptance + +- [x] Existing blocks without `notes` still show opted-in project notes. +- [x] Kanban `notes: all` plus `todos: none` shows projects only across Backlog, + In Progress, and Done. +- [x] Kanban `notes: none` plus `todos: all` shows tasks only. +- [x] Kanban `notes: all` plus `todos: all` shows both without duplication. +- [x] Matrix project-only and task-only configurations work in Skeleton mode. +- [x] Matrix project-only and task-only configurations work in Eisenhower mode. +- [x] A plain note without `gtd` remains excluded under `notes: all`. +- [x] A `done` project appears in Kanban Done and remains excluded from matrices. +- [x] Invalid `notes` warns and falls back to `all`; singular `note` warns as + unknown. +- [x] Filtered malformed notes do not produce warnings. +- [x] Single and grouped views have correct headings, totals, empty states, + sorting, pagination, and navigation. +- [x] Record explicit user sign-off separately from automated checks. + +## Out of scope + +- Allowing ordinary notes without a `gtd` block into kanban or matrix views. +- Adding `gtd-only` as a distinct `notes` value. +- Renaming or changing the existing `todos` option. +- Inferring project/task identity from tags, titles, notebooks, or note content. +- Mutating note types, tags, or completion state from a rendered view. +- Changing calendar inclusion semantics. +- Changing Gantt item admission. +- Persisting filters or pagination state outside the fenced block. + +## Acceptance criteria + +- Existing kanban and matrix blocks behave identically when `notes` is omitted. +- `notes: all` includes only ordinary notes containing a `gtd` block. +- `notes: none` excludes ordinary notes without affecting native to-dos. +- All `notes` and `todos` combinations behave independently and predictably. +- Filtering happens before tag access, warning creation, bucketing, grouping, + totals, sorting, and pagination. +- SLICE8 batching, SLICE10 grouping, and SLICE11 completion semantics do not + regress. +- Documentation clearly supports project-only, task-only, and combined views. +- Automated validation, production packaging, and explicit manual acceptance + are completed and recorded separately. diff --git a/SLICE13.md b/SLICE13.md new file mode 100644 index 0000000..5822c4d --- /dev/null +++ b/SLICE13.md @@ -0,0 +1,174 @@ +# SLICE 13 β€” Remove underlines from kanban and matrix cards + +> **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 1–5 are implemented and validated. Automation and package +inspection passed, and the user confirmed manual Joplin acceptance on +2026-07-28. + +## Goal + +Remove the distracting underline from every kanban and matrix card title while +preserving the visual distinction for completed items and every existing card +interaction. + +The change applies equally to: + +- ordinary project notes; +- incomplete native to-dos; +- completed native to-dos in Kanban Done; +- completed ordinary notes in Kanban Done; +- cards in both Skeleton and Eisenhower matrices; +- single and notebook-grouped layouts. + +## Confirmed diagnosis + +Kanban and matrix cards are rendered by the shared `renderCard` function as: + +```html +
+
...
+
+``` + +The underline is real and comes from the generic clickable rule: + +```css +.gtd-calendar-clickable { + cursor: pointer; + text-decoration: underline; +} +``` + +Calendar items already suppress that decoration through a calendar-scoped +override. Kanban and matrix cards do not, so the parent decoration propagates +through their title text. + +Completed-card strikethrough is separate. `renderCard` applies an inline +`line-through` directly to `.gtd-kanban-card-title` when `card.completed` is +true. Removing the parent underline therefore must not remove Done styling. + +## Confirmed design + +Add a card-scoped override to the existing `.gtd-kanban-card` rule: + +```css +.gtd-kanban-card { + text-decoration: none; +} +``` + +This selector is deliberately narrow: + +- It affects both kanban and matrix because they share `renderCard`. +- It affects both notes and to-dos because both use the same card element. +- It does not change calendar events, unscheduled items, Gantt rows, debug + output, warnings, or other clickable elements. +- It does not change `cursor: pointer`, card borders, hover borders, custom + colours, glyphs, recurrence marks, details, pagination, or click navigation. +- It leaves the completed title's inline `line-through` authoritative. + +Do not remove `text-decoration: underline` from the generic +`.gtd-calendar-clickable` rule. Other renderers may rely on that default, and a +global change would make this small visual correction unnecessarily broad. + +## Visual state table + +| Card | Before | After | +|---|---|---| +| Backlog/In Progress note | Underline | No decoration | +| Backlog/In Progress to-do | Underline | No decoration | +| Done ordinary note | Underline + line-through | Line-through only | +| Done native to-do | Underline + line-through | Line-through only | +| Matrix note or to-do | Underline | No decoration | + +Custom foreground colours, including intentionally red text, remain unchanged; +only the underline is removed. + +## Accessibility and interaction boundary + +Cards remain visually identifiable as interactive controls through their card +container, border, hover-border change, glyph, and pointer cursor. Click-to-open +behavior is unchanged. + +This slice does not redesign keyboard semantics or convert cards from `div` +elements to buttons/links. That would be a separate accessibility task with a +larger markup, focus, key handling, and styling surface. + +## Implementation plan + +### Phase 1 β€” CSS correction + +- [x] Add `text-decoration: none` to `.gtd-kanban-card` in + `src/event-calendar.css`. +- [x] Keep the generic `.gtd-calendar-clickable` underline rule unchanged. +- [x] Keep the existing calendar-scoped decoration override unchanged. +- [x] Make no JavaScript or payload changes. + +### Phase 2 β€” Static and rendering regression audit + +- [x] Confirm kanban and matrix still use the shared `.gtd-kanban-card` element. +- [x] Confirm incomplete cards receive no inline text decoration. +- [x] Confirm completed cards still set `line-through` on the title row. +- [x] Confirm custom `fg-colour` and `bg-colour` remain inline card styles. +- [x] Confirm hover details, card-detail modes, click navigation, glyphs, and + recurrence marks use unchanged code paths. +- [x] Confirm single and notebook-grouped layouts use the same renderer. + +### Phase 3 β€” Documentation + +- [x] Add an unreleased CHANGELOG entry describing the visual cleanup. +- [x] Update README or SPEC only if existing text claims cards are underlined; + otherwise avoid unnecessary documentation churn. +- [x] Record the screenshot-derived acceptance intent in this file. + +### Phase 4 β€” Automated validation and packaging + +- [x] Run the complete Jest suite (251 tests across 16 suites). +- [x] Run TypeScript validation and webview JavaScript syntax checking. +- [x] Run CSS/build, whitespace, and prohibited-reference audits. +- [x] Build the production JPL. +- [x] Inspect the packaged `event-calendar.css` and confirm the scoped override + is present. +- [x] Inspect archive contents; SHA-256: + final v1.0.0 artifact + `0b28fb0f5ffcb60d92e017fb0972ecaa456fcc3c24b144891dce7d72dc7d98ef`. + +### Phase 5 β€” Manual Joplin acceptance + +- [x] Backlog project-note titles are not underlined. +- [x] In Progress project-note titles are not underlined. +- [x] Backlog and In Progress to-do titles are not underlined. +- [x] Done note titles show line-through without underline. +- [x] Done to-do titles show line-through without underline. +- [x] Skeleton matrix note and to-do titles are not underlined. +- [x] Eisenhower matrix note and to-do titles are not underlined. +- [x] Custom foreground/background colours still render correctly. +- [x] Hover borders and card-detail behavior still work. +- [x] Clicking every card type still opens its source note. +- [x] Single and notebook-grouped views render consistently. +- [x] Record explicit user sign-off separately from automated checks. + +## Out of scope + +- Changing the generic clickable style globally. +- Changing calendar, Unscheduled, Gantt, warning, or debug decorations. +- Changing completed-item semantics or removing line-through. +- Altering card colours, borders, spacing, typography, glyphs, or pagination. +- Changing card markup, focus behavior, keyboard behavior, or ARIA semantics. +- Changing kanban/matrix filtering, sorting, grouping, or data collection. +- Bumping the plugin version or performing the v1.0.0 release. + +## Acceptance criteria + +- No kanban or matrix card title is underlined solely because it is clickable. +- Completed kanban notes and to-dos retain line-through styling. +- The change applies identically to notes, to-dos, matrix modes, and grouped + layouts through the shared card renderer. +- No other clickable view element changes decoration. +- Automated validation, production packaging, and explicit manual acceptance + are completed and recorded separately. diff --git a/SPEC.md b/SPEC.md index ed16117..3848d5f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,6 +1,6 @@ # GTD Calendar β€” Project Specification -*Status: living document, current through v0.7.0 plus completed unreleased kanban/matrix card limits and opted-in note cards. Sections 1–5 describe the shipped design; Β§9 tracks what is still ahead.* +*Status: living document, current through v1.0.0. Sections 1–5 describe the shipped design; Β§9 tracks what is still ahead.* A fork of [WeMakeMachines/joplin-plugin-event-calendar](https://github.com/WeMakeMachines/joplin-plugin-event-calendar) (MIT) that inverts the plugin's data model: instead of events living as YAML inside the calendar note, the plugin's views are **populated by real Joplin notes and todos** in the surrounding folder tree, with clickable drilldown to the source note. What started as a single calendar view has grown into four composable viewsβ€”calendar, kanban, priorities matrix, and Ganttβ€”sharing one data layer. @@ -81,6 +81,7 @@ An **empty** ` ```gtd ` block is valid and meaningful: it is the opt-in signal w title: Editorial Board scope: this-folder # this-folder | children | N | all β€” same semantics as gtd-calendar notebook: Work/Ideas # same semantics as gtd-calendar (default: this note's folder) +notes: all # all | none (default: all; gtd notes only) todos: gtd-only # gtd-only | all | none (default: gtd-only) sort-type: due-date # due-date | title | modified-date (default: due-date) sort: asc # asc | desc (default: asc) @@ -94,7 +95,9 @@ done-window: 7 # days, or "all" (default: 7) Three columns are always shown: **Backlog**, **In Progress**, and **Done**. 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 -fallback card values. The `todos:` option applies only to to-dos. +fallback card values. `notes: all` admits those opted-in ordinary notes while +`notes: none` excludes them; it never admits a note without a `gtd` block. The +`notes` and `todos` options apply independently. Bucketing: completion wins regardless of progress tags. Native to-dos use `todo_completed`; opted-in ordinary notes use `done-tag`. Incomplete items @@ -118,6 +121,7 @@ mode: skeleton # skeleton | eisenhower (default: skeleton) title: Priorities scope: this-folder # this-folder | children | N | all β€” same semantics as gtd-calendar notebook: Work/Ideas # same semantics as gtd-calendar (default: this note's folder) +notes: all # all | none; gtd notes only (default: all) todos: gtd-only sort-type: due-date sort: asc @@ -131,7 +135,8 @@ 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 unfinished ordinary notes containing a `gtd` block. Completed +`todos:` plus unfinished ordinary notes admitted by `notes:`. `notes: all` +still requires an explicit `gtd` block; `notes: none` excludes all notes. 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 @@ -216,18 +221,24 @@ Kanban and matrix use a deliberately asymmetric inclusion contract: | Item | Has `gtd` block | No `gtd` block | |---|---|---| -| Ordinary note | Included, regardless of `todos:` | Excluded | +| Ordinary note with `notes: all` (default) | Included | Excluded | +| Ordinary note with `notes: none` | Excluded | Excluded | | To-do with `todos: gtd-only` | Included | Excluded | | To-do with `todos: all` | Included | Included | | To-do with `todos: none` | Excluded | Excluded | -An empty or malformed `gtd` block counts as found. Malformed blocks warn but do -not disappear. Each kanban/matrix host note is excluded before eligibility and -tag lookup. +An empty or malformed `gtd` block counts as found. Malformed admitted blocks +warn but do not disappear. The independent `notes` and `todos` filters run +before warnings and tag lookup. Each kanban/matrix host note is excluded before +eligibility and tag lookup. ### 3.3 Completed todos -**Calendar:** completed to-dos render with strikethrough and a checked indicator. **Kanban:** completed to-dos enter Done when inside the configured done window. **Matrix:** completed to-dos are excluded. Ordinary notes are always incomplete and never recurring in kanban/matrix cards. +**Calendar:** completed to-dos render with strikethrough and a checked indicator. +**Kanban:** completed to-dos enter Done when inside the configured done window; +opted-in ordinary notes carrying `done-tag` also enter Done without timestamp +filtering. **Matrix:** completed to-dos and done-tag notes are excluded. Ordinary +notes never become recurring. --- diff --git a/TASKS.md b/TASKS.md index 7261ef7..e956e2d 100644 --- a/TASKS.md +++ b/TASKS.md @@ -168,8 +168,8 @@ Implementation order: SLICE8, then SLICE9, then SLICE10. - [x] Document `scope: children` plus `group: notebook` multi-view behavior in README.md. - [x] Add a parent/child/grandchild notebook example to README.md or SPEC.md. -- [ ] Update architecture and payload behavior in SPEC.md. -- [ ] Add the feature to CHANGELOG.md. +- [x] Update architecture and payload behavior in SPEC.md. +- [x] Add the feature to CHANGELOG.md. - [x] Run the full automated test suite and record command/results. - [x] Run the production package build and record command/artifact. - [x] Manually verify a parent notebook containing multiple children and diff --git a/package-lock.json b/package-lock.json index 21c815a..1e8684e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "joplin-plugin-gtd-calendar", - "version": "0.8.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "joplin-plugin-gtd-calendar", - "version": "0.8.0", + "version": "1.0.0", "license": "MIT", "dependencies": { "date-fns": "^2.29.3", diff --git a/package.json b/package.json index 1dd7a0a..f29b94d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "joplin-plugin-gtd-calendar", - "version": "0.8.0", + "version": "1.0.0", "scripts": { "test": "jest", "predist": "bash scripts/archive-jpl.sh", diff --git a/src/Gtd/collectKanban.ts b/src/Gtd/collectKanban.ts index 7ee3588..06f46b4 100644 --- a/src/Gtd/collectKanban.ts +++ b/src/Gtd/collectKanban.ts @@ -22,12 +22,12 @@ export interface KanbanResult { } /** - * Build a three-column kanban board from to-dos in scope. + * Build a three-column kanban board from admitted to-dos and opted-in notes. * - * - Notes are excluded entirely (to-dos only). - * - Bucketing: Done wins (todo_completed > 0); else in-progress tag; + * - notes and todos filters apply independently. + * - Bucketing: completion wins; else in-progress tag; * else Backlog. - * - Done column filtered to the done-window (days). + * - done-window filters native completed to-dos only. * - Each column sorted per config. * * The kanban note itself is always excluded. @@ -71,16 +71,18 @@ export default async function collectKanban( ? -Infinity : now.getTime() - config.doneWindow * 24 * 60 * 60 * 1000; - // Ordinary notes opt in through their gtd block, independently of the - // to-do inclusion mode, so bodies are required for every scanned note. + // Bodies preserve explicit note opt-in and optional to-do card overrides. + // When both types are disabled, metadata alone is sufficient for scan totals. + const includeBody = config.notes !== "none" || config.todos !== "none"; for (const folderId of folderIds) { const target = grouped.get(folderId); const targetBoard = target?.board || { backlog, inProgress, done }; - const notes = await adapter.getNotesInFolder(folderId, true); + const notes = await adapter.getNotesInFolder(folderId, includeBody); for (const note of notes) { scannedNotes += 1; if (note.id === kanbanNoteId) continue; + if (!note.is_todo && config.notes === "none") continue; const result = extractGtdBlock(note.body); if (note.is_todo) { diff --git a/src/Gtd/collectMatrix.ts b/src/Gtd/collectMatrix.ts index d133031..1a1f38f 100644 --- a/src/Gtd/collectMatrix.ts +++ b/src/Gtd/collectMatrix.ts @@ -22,10 +22,10 @@ export interface MatrixResult { } /** - * Build a 2x2 Eisenhower matrix from to-dos in scope. + * Build a 2x2 matrix from admitted to-dos and opted-in notes. * - * - Notes are excluded (to-dos only). - * - Completed to-dos are excluded entirely: the matrix is a + * - notes and todos filters apply independently. + * - Completed to-dos and done-tag notes are excluded entirely: the matrix is a * prioritisation view; completions live in the kanban's Done column. * - Quadrants from the two axis tags: * urgent + important -> Do First @@ -73,8 +73,9 @@ export default async function collectMatrix( // Skeleton mode: a date on or before this ISO threshold is "due soon". const soonThreshold = isoDaysFromNow(now, config.urgentWindow); - // Ordinary notes opt in through their gtd block, independently of the - // to-do inclusion mode, so bodies are required for every scanned note. + // Bodies preserve explicit note opt-in and optional to-do card overrides. + // When both types are disabled, metadata alone is sufficient for scan totals. + const includeBody = config.notes !== "none" || config.todos !== "none"; for (const folderId of folderIds) { const target = grouped.get(folderId); @@ -84,10 +85,11 @@ export default async function collectMatrix( bottomLeft, bottomRight, }; - const notes = await adapter.getNotesInFolder(folderId, true); + const notes = await adapter.getNotesInFolder(folderId, includeBody); for (const note of notes) { scannedNotes += 1; if (note.id === matrixNoteId) continue; + if (!note.is_todo && config.notes === "none") continue; const result = extractGtdBlock(note.body); if (note.is_todo) { diff --git a/src/Gtd/parseKanbanConfig.ts b/src/Gtd/parseKanbanConfig.ts index 340a762..a3f76f1 100644 --- a/src/Gtd/parseKanbanConfig.ts +++ b/src/Gtd/parseKanbanConfig.ts @@ -4,6 +4,7 @@ import { KanbanConfig, InclusionMode, KanbanSortType, + NoteInclusionMode, } from "./types"; import { parseNotebookOption } from "./resolveNotebook"; import parsePageSize from "./pageSize"; @@ -25,6 +26,7 @@ export default function parseKanbanConfig(raw: any): KanbanConfig { "scope", "group", "notebook", + "notes", "todos", "sort-type", "sort", @@ -84,6 +86,18 @@ export default function parseKanbanConfig(raw: any): KanbanConfig { ); } + // ordinary-note inclusion; all still means explicit gtd opt-in only + let notes: NoteInclusionMode = "all"; + if (input.notes !== undefined) { + const candidate = + typeof input.notes === "string" ? input.notes.trim().toLowerCase() : ""; + if (candidate === "all" || candidate === "none") { + notes = candidate; + } else { + warnings.push("Invalid notes " + String(input.notes) + " (using all)"); + } + } + // todos inclusion let todos: InclusionMode = "gtd-only"; if (input.todos !== undefined) { @@ -177,6 +191,7 @@ export default function parseKanbanConfig(raw: any): KanbanConfig { scopeAll, groupByNotebook, notebook, + notes, todos, sortType, sort, diff --git a/src/Gtd/parseMatrixConfig.ts b/src/Gtd/parseMatrixConfig.ts index 10ac3c1..9f131d2 100644 --- a/src/Gtd/parseMatrixConfig.ts +++ b/src/Gtd/parseMatrixConfig.ts @@ -7,6 +7,7 @@ import { IMPORTANT_TAG, IN_PROGRESS_TAG, DONE_TAG, + NoteInclusionMode, } from "./types"; import { parseNotebookOption } from "./resolveNotebook"; import parsePageSize from "./pageSize"; @@ -29,6 +30,7 @@ export default function parseMatrixConfig(raw: any): MatrixConfig { "scope", "group", "notebook", + "notes", "todos", "sort-type", "sort", @@ -101,6 +103,18 @@ export default function parseMatrixConfig(raw: any): MatrixConfig { ); } + // ordinary-note inclusion; all still means explicit gtd opt-in only + let notes: NoteInclusionMode = "all"; + if (input.notes !== undefined) { + const candidate = + typeof input.notes === "string" ? input.notes.trim().toLowerCase() : ""; + if (candidate === "all" || candidate === "none") { + notes = candidate; + } else { + warnings.push("Invalid notes " + String(input.notes) + " (using all)"); + } + } + // todos inclusion let todos: InclusionMode = "gtd-only"; if (input.todos !== undefined) { @@ -187,6 +201,7 @@ export default function parseMatrixConfig(raw: any): MatrixConfig { scopeAll, groupByNotebook, notebook, + notes, todos, sortType, sort, diff --git a/src/Gtd/types.ts b/src/Gtd/types.ts index 4d2130b..9bcc678 100644 --- a/src/Gtd/types.ts +++ b/src/Gtd/types.ts @@ -122,6 +122,8 @@ export const IN_PROGRESS_TAG = "in-progress"; export const DONE_TAG = "done"; export type KanbanSortType = "due-date" | "title" | "modified-date"; +/** Ordinary-note inclusion for kanban/matrix; notes still require a gtd block. */ +export type NoteInclusionMode = "all" | "none"; /** Parsed and normalised ```gtd-kanban config. */ export interface KanbanConfig { @@ -133,6 +135,7 @@ export interface KanbanConfig { groupByNotebook: boolean; /** Notebook to root the scan at (id/title/path); null = host folder. */ notebook: string | null; + notes: NoteInclusionMode; todos: InclusionMode; sortType: KanbanSortType; sort: "asc" | "desc"; @@ -205,6 +208,7 @@ export interface MatrixConfig { groupByNotebook: boolean; /** Notebook to root the scan at (id/title/path); null = host folder. */ notebook: string | null; + notes: NoteInclusionMode; todos: InclusionMode; sortType: KanbanSortType; sort: "asc" | "desc"; diff --git a/src/event-calendar.css b/src/event-calendar.css index 542f084..c95be62 100644 --- a/src/event-calendar.css +++ b/src/event-calendar.css @@ -395,6 +395,7 @@ background-color: aliceblue; color: black; cursor: pointer; + text-decoration: none; font-size: 0.85em; } diff --git a/src/manifest.json b/src/manifest.json index 26dbe95..1cb96c6 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 1, "id": "com.victorwiebe.joplin.plugin.gtd-calendar", "app_min_version": "2.7", - "version": "0.8.0", + "version": "1.0.0", "name": "GTD Calendar", "description": "Day, week, and month calendars populated by your notes and to-dos, with click-through to the source note. Configure with simple YAML blocks. A GTD-friendly fork of Event Calendar by Franco Speziali.", "author": "Victor Wiebe", diff --git a/src/tests/Gtd/fieldsHint.test.ts b/src/tests/Gtd/fieldsHint.test.ts index f2d1ed7..07e1094 100644 --- a/src/tests/Gtd/fieldsHint.test.ts +++ b/src/tests/Gtd/fieldsHint.test.ts @@ -8,8 +8,8 @@ import { DataAdapter, RawFolder, RawNote } from "../../Gtd/types"; // --------------------------------------------------------------------------- // Body-fetch contract: the calendar may skip bodies when both item modes are -// "none". Kanban and matrix always require bodies because ordinary notes opt in -// through a gtd block independently of the to-do inclusion mode. These tests +// "none". Kanban and matrix require bodies whenever an admitted type can use a +// gtd block, including todos: all for optional card overrides. These tests // record the `includeBody` argument passed to getNotesInFolder. // --------------------------------------------------------------------------- @@ -63,9 +63,10 @@ describe("collectEvents β€” body fetch hint", () => { describe("collectKanban β€” body fetch hint", () => { test.each([ - ["gtd-only", { todos: "gtd-only" }, true], - ["all", { todos: "all" }, true], - ["none", { todos: "none" }, true], + ["default notes", { todos: "none" }, true], + ["gtd-only todos", { notes: "none", todos: "gtd-only" }, true], + ["all todos", { notes: "none", todos: "all" }, true], + ["both none", { notes: "none", todos: "none" }, false], ])("%s β†’ includeBody %s", async (_label, raw, expected) => { const { adapter, bodyFlags } = recordingAdapter([makeNote()]); const config = parseKanbanConfig(raw as any); @@ -76,9 +77,10 @@ describe("collectKanban β€” body fetch hint", () => { describe("collectMatrix β€” body fetch hint", () => { test.each([ - ["gtd-only", { todos: "gtd-only" }, true], - ["all", { todos: "all" }, true], - ["none", { todos: "none" }, true], + ["default notes", { todos: "none" }, true], + ["gtd-only todos", { notes: "none", todos: "gtd-only" }, true], + ["all todos", { notes: "none", todos: "all" }, true], + ["both none", { notes: "none", todos: "none" }, false], ])("%s β†’ includeBody %s", async (_label, raw, expected) => { const { adapter, bodyFlags } = recordingAdapter([makeNote()]); const config = parseMatrixConfig(raw as any); diff --git a/src/tests/Gtd/kanban.test.ts b/src/tests/Gtd/kanban.test.ts index df32639..4144193 100644 --- a/src/tests/Gtd/kanban.test.ts +++ b/src/tests/Gtd/kanban.test.ts @@ -12,6 +12,7 @@ describe("parseKanbanConfig", () => { expect(c).toMatchObject({ title: null, scopeDepth: 0, + notes: "all", todos: "gtd-only", sortType: "due-date", sort: "asc", @@ -24,6 +25,18 @@ describe("parseKanbanConfig", () => { expect(c.warnings).toHaveLength(0); }); + test("parses notes independently and rejects invalid or singular forms", () => { + expect(parseKanbanConfig({ notes: " NONE " }).notes).toBe("none"); + for (const value of ["", "gtd-only", null, ["all"], { mode: "all" }]) { + const config = parseKanbanConfig({ notes: value }); + expect(config.notes).toBe("all"); + expect(config.warnings[0]).toContain("Invalid notes"); + } + const singular = parseKanbanConfig({ note: "none" }); + expect(singular.notes).toBe("all"); + expect(singular.warnings[0]).toContain("Unknown option"); + }); + test("parses and normalizes done-tag with done-over-in-progress warning", () => { const custom = parseKanbanConfig({ "done-tag": " Finished " }); expect(custom.doneTag).toBe("finished"); @@ -460,6 +473,56 @@ describe("collectKanban β€” mixed note/to-do contracts", () => { ).toEqual(["backlog-todo", "active-note", "done-todo"]); }); }); +describe("collectKanban β€” notes inclusion", () => { + test("supports project-only, task-only, combined, and empty boards", async () => { + const notes = [ + makeNote({ id: "project", is_todo: 0 }), + makeNote({ id: "plain-note", is_todo: 0, body: "No block" }), + makeNote({ id: "task" }), + ]; + const cases: Array<[any, string[]]> = [ + [{ notes: "all", todos: "none" }, ["project"]], + [{ notes: "none", todos: "all" }, ["task"]], + [{ notes: "all", todos: "all" }, ["project", "task"]], + [{ notes: "none", todos: "none" }, []], + ]; + for (const [raw, expected] of cases) { + const result = await collectKanban( + makeAdapter(folders, notes), + "host", + "board", + parseKanbanConfig(raw), + NOW + ); + expect(result.board.backlog.map((card) => card.id).sort()).toEqual( + [...expected].sort() + ); + expect(result.cardCount).toBe(expected.length); + } + }); + + test("filtered notes do not request tags", async () => { + const tagCalls: string[] = []; + const adapter = makeAdapter(folders, [ + makeNote({ id: "project", is_todo: 0 }), + ]); + adapter.getNoteTagTitles = async (noteId: string) => { + tagCalls.push(noteId); + return []; + }; + const result = await collectKanban( + adapter, + "host", + "board", + parseKanbanConfig({ notes: "none", todos: "none" }), + NOW + ); + expect(tagCalls).toEqual([]); + expect(result.cardCount).toBe(0); + expect(result.scannedNotes).toBe(1); + }); +}); + describe("collectKanban β€” done window", () => { test("done notes win over in-progress and bypass the to-do done window", async () => { const notes = [ @@ -611,6 +674,42 @@ describe("collectKanban β€” recurrence flag", () => { }); describe("collectKanban β€” notebook groups", () => { + test("omits groups emptied by note filtering while retaining descendants", async () => { + const tree: RawFolder[] = [ + { id: "root", parent_id: "", title: "Root" }, + { id: "child", parent_id: "root", title: "Child" }, + ]; + const notes = [ + makeNote({ + id: "broken-project", + parent_id: "root", + is_todo: 0, + body: "```gtd\ntitle: [invalid\n```", + }), + makeNote({ id: "child-task", parent_id: "child" }), + ]; + const result = await collectKanban( + makeAdapter(tree, notes), + "host", + "root", + parseKanbanConfig({ + scope: "children", + group: "notebook", + notes: "none", + todos: "all", + }), + NOW + ); + expect(result.layout.kind).toBe("notebooks"); + if (result.layout.kind !== "notebooks") throw new Error("grouped expected"); + expect(result.layout.groups.map((group) => group.folderId)).toEqual([ + "child", + ]); + expect(result.layout.groups[0].warnings).toEqual([]); + expect(result.cardCount).toBe(1); + expect(result.scannedNotes).toBe(2); + }); + test("keeps completed notes in their exact-owner Done groups", async () => { const tree: RawFolder[] = [ { id: "root", parent_id: "", title: "Root" }, diff --git a/src/tests/Gtd/matrix.test.ts b/src/tests/Gtd/matrix.test.ts index 2687cb3..cf15ca8 100644 --- a/src/tests/Gtd/matrix.test.ts +++ b/src/tests/Gtd/matrix.test.ts @@ -13,6 +13,7 @@ describe("parseMatrixConfig", () => { mode: "skeleton", title: null, scopeDepth: 0, + notes: "all", todos: "gtd-only", sortType: "due-date", sort: "asc", @@ -27,6 +28,18 @@ describe("parseMatrixConfig", () => { expect(c.warnings).toHaveLength(0); }); + test("parses notes independently and rejects invalid or singular forms", () => { + expect(parseMatrixConfig({ notes: " NONE " }).notes).toBe("none"); + for (const value of ["", "gtd-only", null, ["all"], { mode: "all" }]) { + const config = parseMatrixConfig({ notes: value }); + expect(config.notes).toBe("all"); + expect(config.warnings[0]).toContain("Invalid notes"); + } + const singular = parseMatrixConfig({ note: "none" }); + expect(singular.notes).toBe("all"); + expect(singular.warnings[0]).toContain("Unknown option"); + }); + test("parses done-tag and warns when it matches in-progress-tag", () => { expect(parseMatrixConfig({ "done-tag": " Finished " }).doneTag).toBe( "finished" @@ -327,6 +340,58 @@ describe("collectMatrix β€” quadrant bucketing", () => { }); describe("collectMatrix β€” mixed note/to-do contracts", () => { + test.each(["skeleton", "eisenhower"] as const)( + "supports project-only and task-only %s matrices", + async (mode) => { + const notes = [ + makeNote({ id: "project", is_todo: 0 }), + makeNote({ id: "plain-note", is_todo: 0, body: "No block" }), + makeNote({ id: "task" }), + ]; + const cases: Array<[any, string[]]> = [ + [{ notes: "all", todos: "none", mode }, ["project"]], + [{ notes: "none", todos: "all", mode }, ["task"]], + [{ notes: "none", todos: "none", mode }, []], + ]; + for (const [raw, expected] of cases) { + const result = await collectMatrix( + makeAdapter(folders, notes), + "host", + "board", + parseMatrixConfig(raw) + ); + const ids = [ + ...result.board.topLeft, + ...result.board.topRight, + ...result.board.bottomLeft, + ...result.board.bottomRight, + ].map((card) => card.id); + expect(ids).toEqual(expected); + expect(result.cardCount).toBe(expected.length); + } + } + ); + + test("filtered notes do not request tags", async () => { + const tagCalls: string[] = []; + const adapter = makeAdapter(folders, [ + makeNote({ id: "project", is_todo: 0 }), + ]); + adapter.getNoteTagTitles = async (noteId: string) => { + tagCalls.push(noteId); + return []; + }; + const result = await collectMatrix( + adapter, + "host", + "board", + parseMatrixConfig({ notes: "none", todos: "none" }) + ); + expect(tagCalls).toEqual([]); + expect(result.cardCount).toBe(0); + expect(result.scannedNotes).toBe(1); + }); + test.each(["skeleton", "eisenhower"] as const)( "excludes done notes before %s bucketing while ignoring done on open to-dos", async (mode) => { @@ -567,6 +632,41 @@ describe("collectMatrix β€” Skeleton mode", () => { }); describe("collectMatrix β€” notebook groups", () => { + test("omits groups emptied by note filtering while retaining descendants", async () => { + const tree: RawFolder[] = [ + { id: "root", parent_id: "", title: "Root" }, + { id: "child", parent_id: "root", title: "Child" }, + ]; + const notes = [ + makeNote({ + id: "broken-project", + parent_id: "root", + is_todo: 0, + body: "```gtd\ntitle: [invalid\n```", + }), + makeNote({ id: "child-task", parent_id: "child" }), + ]; + const result = await collectMatrix( + makeAdapter(tree, notes), + "host", + "root", + parseMatrixConfig({ + scope: "children", + group: "notebook", + notes: "none", + todos: "all", + }) + ); + expect(result.layout.kind).toBe("notebooks"); + if (result.layout.kind !== "notebooks") throw new Error("grouped expected"); + expect(result.layout.groups.map((group) => group.folderId)).toEqual([ + "child", + ]); + expect(result.layout.groups[0].warnings).toEqual([]); + expect(result.cardCount).toBe(1); + expect(result.scannedNotes).toBe(2); + }); + test.each(["skeleton", "eisenhower"] as const)( "groups mixed cards exactly once in %s mode", async (mode) => {