- New gtd-matrix block: 2x2 prioritisation grid with axis headers (Urgent / Not urgent x Important / Not important) - Quadrants from two configurable tags (urgent-tag default 'urgent', important-tag default 'important'): both -> Do First; important -> Schedule; urgent -> Delegate; neither -> Eliminate (default bucket) - Completed to-dos excluded entirely (prioritisation view, not tracking; kanban Done is where completions live); to-dos only; matrix note excluded; warns when both axis tags are identical - Same scope/todos/sort-type/sort/card-detail options as kanban; cards reuse kanban rendering (compact, recurrence mark, drilldown) - Quadrant colour accents: red Do First, blue Schedule, amber Delegate, grey Eliminate - SPEC roadmap restructured: matrix shipped as v0.4.0 (views axis); drag-and-drop + create-from-view clustered as the v0.5.0+ authoring epic with multi-board refresh and trigger/target questions recorded - Version 0.4.0; 11 new tests (80 total); README documented
269 lines
20 KiB
Markdown
269 lines
20 KiB
Markdown
# GTD Calendar — Project Specification (v1 Draft)
|
||
|
||
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 calendar is **populated by real Joplin notes and todos** in the surrounding folder tree, with clickable drilldown to the source note.
|
||
|
||
**Working name:** `joplin-plugin-gtd-calendar`
|
||
**Proposed manifest id:** `com.victorwiebe.joplin.plugin.gtd-calendar` (must differ from upstream's id so both plugins can coexist during development)
|
||
|
||
---
|
||
|
||
## 1. Concept
|
||
|
||
A note anywhere in a folder contains a ` ```gtd-calendar ` fenced block. That block renders as a Day/Week/Month calendar populated by notes and todos in scope. Items opt in (by default) via a ` ```gtd ` fenced block in their body. Clicking a calendar event opens the underlying note.
|
||
|
||
Dateless items are not lost — they collect in an **Unscheduled** section below the calendar grid, functioning as a GTD next-actions / someday-maybe bucket.
|
||
|
||
---
|
||
|
||
## 2. Schema Reference
|
||
|
||
### 2.1 The `gtd-calendar` block (lives in the calendar note)
|
||
|
||
```
|
||
```gtd-calendar
|
||
view: month # day | week | month | month-grid (default: day)
|
||
title: Editorial Schedule
|
||
scope: this-folder # this-folder | children | <integer depth> (default: this-folder)
|
||
notes: gtd-only # gtd-only | all | none (default: gtd-only)
|
||
todos: gtd-only # gtd-only | all | none (default: gtd-only)
|
||
sort: asc # asc | desc (default: asc)
|
||
sort-type: title # title | modified_date (default: title)
|
||
```
|
||
|
||
| Key | Values | Default | Notes |
|
||
|---|---|---|---|
|
||
| `view` | `day`, `week`, `month` (also `d`/`w`/`m`), `month-grid` (also `grid`) | `day` | Grouping granularity. `day`/`week`/`month` render the compact tile strip spanning all events; `month-grid` renders a classic 7-column grid of the entire *current* calendar month with weekday headers, dimmed adjacent-month days, today highlighted, and clickable event chips in each day cell. Events outside the displayed month are counted in a footnote. |
|
||
| `title` | string | none | Rendered as a heading above the calendar. |
|
||
| `scope` | `this-folder`, `children`, or an integer | `this-folder` | `children` = unlimited recursion into subfolders. An integer *n* = this folder plus *n* levels of descendants (`0` ≡ `this-folder`). Documentation will caution that broad scopes scan every note body in the tree. |
|
||
| `notes` | `gtd-only`, `all`, `none` | `gtd-only` | Which plain notes participate. `all` includes notes without a `gtd` block (they receive default styling and are Unscheduled unless... see §3.2). |
|
||
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Same semantics for todos. |
|
||
| `sort` | `asc`, `desc` | `asc` | Order of events **within** each day/week/month grouping and within Unscheduled. Groupings themselves always run chronologically. |
|
||
| `sort-type` | `title`, `modified_date` | `title` | Field used by `sort`. `modified_date` maps to Joplin's `updated_time`. |
|
||
|
||
Unknown keys are ignored with a console warning. A malformed block renders an inline error box rather than failing silently (improvement over upstream, which swallows parse errors into the console).
|
||
|
||
### 2.2 The `gtd` block (lives in participating notes/todos)
|
||
|
||
```
|
||
```gtd
|
||
date: 2026-07-04 # required for notes to be scheduled; optional override for todos
|
||
bg-colour: teal # any CSS color value
|
||
fg-colour: white
|
||
icon: 🦴
|
||
title: Override title # optional; defaults to the note's own title
|
||
text: Hover text # optional; tooltip detail, as upstream
|
||
```
|
||
|
||
| Key | Applies to | Behavior |
|
||
|---|---|---|
|
||
| `date` | notes | Required for the item to appear **in the grid**; without it the note goes to Unscheduled. Accepts `yyyy-mm-dd` (and `mm-dd-yyyy` for upstream parity). |
|
||
| `date` | todos | Optional. If present, **overrides** `todo_due` (explicit beats implicit). |
|
||
| `bg-colour` / `bgColor` | both | Event tile background. Both spellings accepted; `bg-colour` is canonical for this fork. |
|
||
| `fg-colour` | both | Event tile text color (new vs. upstream). |
|
||
| `icon` | both | Emoji/short string, as upstream. Defaults: type indicator (see §4). |
|
||
| `title` | both | Overrides the note title on the tile. |
|
||
| `text` | both | Tooltip body. |
|
||
|
||
An **empty** ` ```gtd ` block is valid and meaningful: it is the opt-in signal with all defaults.
|
||
|
||
---
|
||
|
||
## 3. Data Model Rules
|
||
|
||
### 3.1 Date resolution
|
||
|
||
1. **Todos:** `todo_due` from the Joplin API. A `date:` in the `gtd` block overrides it.
|
||
2. **Notes:** `date:` from the `gtd` block only. No fallback to created/updated time — those are not event dates and would produce misleading calendars.
|
||
3. **No resolvable date** → item is rendered in the **Unscheduled** section below the grid.
|
||
|
||
### 3.2 Inclusion matrix
|
||
|
||
An item appears on the calendar iff its type's mode admits it:
|
||
|
||
| Mode | Has `gtd` block | No `gtd` block |
|
||
|---|---|---|
|
||
| `gtd-only` (default) | ✅ included | ❌ excluded |
|
||
| `all` | ✅ included | ✅ included (default styling) |
|
||
| `none` | ❌ excluded | ❌ excluded |
|
||
|
||
The calendar note itself is always excluded from results, as is any note whose only `gtd-calendar` block makes it a calendar (a note may, however, be both an event and contain a calendar if it has both block types — edge case, supported, documented).
|
||
|
||
### 3.3 Completed todos
|
||
|
||
**v1 decision:** completed todos (`todo_completed > 0`) render with strikethrough + checked indicator, and a future `completed: show | hide` calendar option is reserved. (Logged as decision D3 below; cheap to change.)
|
||
|
||
---
|
||
|
||
## 4. Rendering
|
||
|
||
- Reuses upstream's grouping/renderer architecture (`DayGrouping`/`WeekGrouping`/`MonthGrouping` + DOM renderers + CSS asset), including the empty-grouping placeholders and current-date highlight.
|
||
- **Fixes upstream bug:** `Calendar/index.ts` imports `WeekGrouping` from `Month/MonthGrouping`, so the week view silently groups by month. Corrected in the fork.
|
||
- **Type distinction:** todos render with a checkbox glyph (☐ / ☑ when completed); notes render with a document glyph. Glyphs are suppressed if the item supplies its own `icon`. A subtle border-style difference (solid vs. dashed, TBD in CSS) keeps the distinction visible under custom colours.
|
||
- **Unscheduled section:** pinned below the grid, with its own header, honoring `sort` / `sort-type`.
|
||
- **Drilldown:** each tile is clickable and opens the source note.
|
||
|
||
---
|
||
|
||
## 5. Architecture
|
||
|
||
Upstream is a single synchronous markdown-it content script with no data API access. The fork becomes a three-part plugin:
|
||
|
||
```
|
||
┌─────────────────────────────┐
|
||
│ 1. Markdown-it content │ Intercepts ```gtd-calendar fences.
|
||
│ script (renderer) │ Parses YAML config, emits a placeholder
|
||
│ │ <div> carrying the config as data-attrs.
|
||
└──────────────┬──────────────┘
|
||
│ (rendered HTML)
|
||
┌──────────────▼──────────────┐
|
||
│ 2. Webview asset script │ Finds placeholders, calls
|
||
│ (runs in rendered note) │ webviewApi.postMessage(config).
|
||
│ │ Receives event data, builds the
|
||
│ │ calendar DOM via the (reused)
|
||
│ │ grouping/renderer classes.
|
||
│ │ Click → postMessage({open: noteId}).
|
||
└──────────────┬──────────────┘
|
||
│ postMessage / onMessage
|
||
┌──────────────▼──────────────┐
|
||
│ 3. Main plugin process │ joplin.contentScripts.onMessage:
|
||
│ (index.ts) │ • resolve current note's folder
|
||
│ │ (joplin.workspace.selectedNote)
|
||
│ │ • build folder tree, apply scope
|
||
│ │ • fetch notes/todos (id, title,
|
||
│ │ is_todo, todo_due, todo_completed,
|
||
│ │ updated_time, body)
|
||
│ │ • parse ```gtd blocks from bodies
|
||
│ │ • apply inclusion matrix, return
|
||
│ │ structured event list
|
||
│ │ • handle open: joplin.commands
|
||
│ │ .execute('openNote', noteId)
|
||
└─────────────────────────────┘
|
||
```
|
||
|
||
### Performance notes
|
||
|
||
- Folder tree and note list fetched per render request; paginated API calls (`page`/`has_more`).
|
||
- Body scanning is limited to items in scope. For large scopes, pre-filter with Joplin's search API (`body:"```gtd"`-style query) before fetching full bodies. Cache by `updated_time` if needed; not a v1 requirement.
|
||
- Refresh model: re-render occurs on note switch (Joplin behavior). `joplin.workspace.onNoteChange` live refresh is a stretch goal, not v1.
|
||
|
||
### Platform caveats
|
||
|
||
- Desktop is the target. Mobile plugin support is partial; `openNote` behavior there is unverified. The "embed search" pairing remains a documented fallback for mobile users.
|
||
|
||
---
|
||
|
||
## 6. Implementation Phases
|
||
|
||
1. **Phase 0 — Scaffolding.** Fork, rename (manifest id, plugin name, file names), build pipeline verified (`npm install && npm run dist`), upstream week-view bug fixed, upstream tests green.
|
||
2. **Phase 1 — Plumbing.** Placeholder rendering for `gtd-calendar` fences; webview ⇄ plugin message round-trip proven with a hardcoded payload.
|
||
3. **Phase 2 — Data layer.** Folder tree + scope resolution; note/todo fetching; `gtd` block parsing; inclusion matrix; date resolution; unit tests for all of it (the upstream Jest setup carries over).
|
||
4. **Phase 3 — Rendering.** Wire real data into the reused renderers; Unscheduled section; type glyphs; fg-colour support; sort options.
|
||
5. **Phase 4 — Drilldown & polish.** Click-to-open; error boxes for malformed YAML; docs/README; scope-performance warnings.
|
||
|
||
---
|
||
|
||
## 7. Decision Log / Open Items
|
||
|
||
| # | Decision | Status |
|
||
|---|---|---|
|
||
| D1 | Defaults: `notes: gtd-only`, `todos: gtd-only` | ⚠️ **Confirm** — earlier discussion said todos are *always* included (i.e. `todos: all` default); the final message specified `gtd-only` as default for both. Spec follows the final message. |
|
||
| D2 | `gtd` `date:` overrides `todo_due` (vs. emitting a warning) | Adopted; revisit if confusing in practice. |
|
||
| D3 | Completed todos: shown struck-through in v1; `completed:` option reserved | Proposed, unconfirmed. |
|
||
| D4 | `sort`/`sort-type` act within groupings only; grid is always chronological | Confirmed. |
|
||
| D5 | Colour key spelling: `bg-colour`/`fg-colour` canonical, `bgColor` accepted for upstream parity | Proposed. |
|
||
| D6 | Future options parked: `completed:`, tag-based filtering, week-start day, live refresh | Backlog. |
|
||
|
||
---
|
||
|
||
## 8. Licensing & Attribution
|
||
|
||
Upstream is MIT (© Franco Speziali / WeMakeMachines). The fork retains the MIT license, preserves the original copyright notice, and credits upstream in the README per standard practice.
|
||
---
|
||
|
||
## 9. Roadmap (post-1.0 / post-launch)
|
||
|
||
Recorded direction, not commitments. Each gets a focused design pass before work begins; real-world use of the shipped plugin may reorder this.
|
||
|
||
### v0.2.0 — Repeating to-dos (visualisation) + configurable strip horizon
|
||
|
||
Recurrence itself is delegated to an existing community plugin (open-environment approach; we don't reimplement it). Candidates evaluated: BeatLink's original *Repeating To-Dos* (unmaintained), the *[Revived]* fork (maintained, GitHub issues only), and *Repeating To-dos v2* by TheScriptingGuy (`com.github.TheScriptingGuy.joplin-plugin-repeating-todos`, best-documented). All share the same mechanic: on completion the to-do's alarm date is reset to the next occurrence and the to-do is unmarked — so at any moment a recurring to-do is a **single** to-do at its next due date, with the recurrence rule stored in the note body.
|
||
|
||
- **Option A (shipped in 0.2.0):** detect recurrence and mark those events with a ↻ glyph. *Implementation note:* v2 of the plugin stores recurrence in Joplin's `userData` (not the note body) and maintains a `recurring` **tag** as its query index — so detection is by tag presence on to-dos (constant `RECURRING_TAG`), not body-scanning. Cleaner than the originally-planned body parse, and reuses the tag-fetching the kanban work will need. No series projection — the to-do shows at its next due date.
|
||
- **Option B (in pocket):** parse the recurrence rule and project all future occurrences across the visible window. Deferred because the body format has proven to be a moving target across forks; promote to a 0.2.x only if Option A's single-occurrence view proves insufficient, and target a specific fork's format at that time.
|
||
- **README:** recommend the maintained fork (v2 first, Revived as fallback); warn about the known "recurrence block injected into all notes" failure mode; note coexistence (their plugin moves the anchor, ours draws the marker).
|
||
|
||
**Configurable strip horizon.** `day`/`week`/`month` strip views currently span first-event to last-event (plus today). Add a horizon option to `gtd-calendar` (working name `weeks:` or `horizon:`, default ~8 weeks forward) controlling how far ahead the strip extends — relevant both generally and for how many future occurrences Option B would draw. **`month-grid` is explicitly excluded**: it always shows the entire current calendar month, by design, and that is not configurable.
|
||
|
||
*Note: multiple `gtd-calendar` blocks in one note already work as of 0.1.0 (each fence renders independently) — e.g. a "week ahead" strip plus a "month grid" in a single dashboard note. No work required; document as a recommended pattern.*
|
||
|
||
### v0.3.0 — Kanban board (read-only) — SHIPPED
|
||
|
||
Decision: a **second block type within this plugin**, not a separate plugin. Rationale: a kanban needs the entire existing data layer (`folderScope`, `collectEvents` inclusion matrix, `getNoteTagTitles`, config parsing, recurrence detection, webview messaging); a separate plugin would duplicate all of it or require a shared-library split whose overhead isn't justified for "three tag-driven columns." One plugin also serves the intended use — a calendar and a kanban in one dashboard note — with a single install and a unified config/tag vocabulary.
|
||
|
||
**The `gtd-kanban` block.** Parallel to `gtd-calendar`; lives in its own note or stacked beneath a calendar (multiple blocks already render independently).
|
||
|
||
```
|
||
```gtd-kanban
|
||
title: Editorial Board
|
||
scope: this-folder # this-folder | children | N — same semantics as gtd-calendar
|
||
todos: gtd-only # gtd-only (default) | all | none
|
||
sort-type: due-date # due-date (default) | title | modified-date
|
||
sort: asc # asc | desc
|
||
in-progress-tag: in-progress # configurable; default "in-progress"
|
||
card-detail: hover # hover (default) | always | none
|
||
done-window: 7 # days; integer or "all"; default 7
|
||
```
|
||
|
||
**Columns (always three, always shown even when empty):** Backlog · In Progress · Done.
|
||
|
||
**Bucketing rules:**
|
||
- **Done wins.** Any to-do with `todo_completed > 0` → *Done*, regardless of tags.
|
||
- Else, carrying the in-progress tag → *In Progress*.
|
||
- Else → *Backlog*.
|
||
- **Backlog shows everything** uncompleted and untagged — dated or not, near or far. (Acknowledged: can grow long; compact cards mitigate.)
|
||
|
||
**Sorting (within each column):**
|
||
- `sort-type: due-date | title | modified-date`, with `sort: asc | desc`.
|
||
- Under `due-date`, to-dos **without** a due date sort *after* all dated ones.
|
||
|
||
**Done column window:** shows only to-dos completed within `done-window` days (default 7); `all` shows the full history. Prevents unbounded growth.
|
||
|
||
**Cards:** compact by default — title, due date, ↻ if recurring, click-to-open. `card-detail` controls the hover panel: `hover` (default), `always` (details inline), `none` (no panel). Notes are excluded entirely (to-dos only).
|
||
|
||
**Read-only in 0.3.0:** reflects state, never mutates it. Proves columns/bucketing cheaply before write-back.
|
||
|
||
**Architecture:** reuses the calendar's pipeline. New `gtd-kanban` markdown-it fence → placeholder → webview requests data → plugin runs scope + inclusion + tag fetch (already built) + bucketing → returns three columns → webview renders. `IN_PROGRESS_TAG` constant mirrors `RECURRING_TAG`, exposed via `in-progress-tag:` config from the start.
|
||
|
||
**`sort-type` vocabulary reconciliation:** the calendar block's `sort-type` (currently `title | modified_date`) is unified toward `due-date | title | modified-date` across both blocks so they read consistently. For the calendar, `due-date` is accepted but falls back to chronological where less meaningful. (Note the spelling shift `modified_date` → `modified-date` for cross-block consistency; the old spelling stays accepted as an alias to avoid breaking existing calendars.)
|
||
|
||
### v0.4.0 — Eisenhower Matrix (`gtd-matrix` block) — SHIPPED
|
||
|
||
A fourth display block: a 2×2 prioritisation matrix, tag-driven like the kanban. Chosen ahead of drag-and-drop deliberately: it is on the **views axis** (another rendering of tagged to-dos, reusing the whole data layer, read-only) rather than the **authoring axis** (vault mutation), so it deepens what the plugin already is without committing to write-back.
|
||
|
||
- **Quadrants** from two configurable tags (`urgent-tag:` default `urgent`; `important-tag:` default `important`):
|
||
- urgent + important → **Do First**
|
||
- important only → **Schedule**
|
||
- urgent only → **Delegate**
|
||
- neither → **Eliminate** (the default bucket for untagged to-dos — a deliberately pointed default)
|
||
- **Completed to-dos are excluded entirely** — the matrix is a prioritisation view, not a tracking view; kanban's Done column is where completions live.
|
||
- To-dos only; same `scope` / `todos` / `sort-type` / `sort` / `card-detail` options as the kanban; same compact clickable cards with ↻.
|
||
- Own block (`gtd-matrix`), not a kanban mode — different layout, different tag semantics.
|
||
|
||
### v0.5.0+ — The authoring axis (drag-and-drop + create-from-view) — one deliberate epic
|
||
|
||
Clustered because they share write-back machinery, and adopting any of them commits the plugin to mutating the vault:
|
||
|
||
**Drag-and-drop kanban (write-back).** The first feature making the plugin a **controller** of data rather than a viewer — warrants its own design pass.
|
||
|
||
- Dragging a card writes back to Joplin via the existing webview→plugin channel:
|
||
- → *In Progress*: add the `in-progress` tag (create the tag if absent; resolve its id; associate).
|
||
- → *Done*: set `todo_completed`.
|
||
- → *Backlog*: remove the tag / clear completion.
|
||
- **Open design questions for that pass:** optimistic UI vs. wait-for-confirmation; behaviour on write failure; whether drops need undo; re-query/refresh of kanban and any affected calendars after a successful write; the trust implications of a rendered view mutating other notes.
|
||
- Feasibility: confirmed possible (we already execute `openNote` from the webview; data writes use the same channel with more responsibility). Complexity lives in tag management and the write/refresh cycle, not the dragging.
|
||
- **Multi-board refresh:** with Persistent Layout keeping several dashboards rendered, a write from one board leaves others stale until re-render — the refresh question is sharper in the fractal-dashboard architecture than for single-board use.
|
||
|
||
**Create-to-do from kanban.** A "+" affordance per column creates a to-do with that column's state (In Progress column → in-progress tag; Backlog → no tag). Easier than create-from-calendar: the column *is* the metadata, no date geometry. Open: target notebook (leaf dashboards obvious, cockpit murky), trigger reliability in the rendered viewer.
|
||
|
||
**Create-to-do from calendar.** Click/affordance on a day cell creates a to-do due that day. Open questions: trigger mechanism in the read-only viewer (double-click/right-click may be claimed by Joplin; per-cell "+" affordance likelier), target notebook rule, whether created to-dos get an empty gtd block so they appear under `todos: gtd-only`. Prototype the trigger before designing the rest.
|