Victor Wiebe d3c0f0ee7f v0.3.0: read-only kanban board (gtd-kanban block)
- New gtd-kanban block renders a three-column board from to-dos:
  Backlog / In Progress / Done. Notes excluded; to-dos only.
- Bucketing: Done wins (todo_completed), else in-progress tag, else
  Backlog. in-progress tag name configurable (IN_PROGRESS_TAG constant,
  in-progress-tag: option).
- Backlog shows all uncompleted untagged to-dos; Done filtered to a
  configurable done-window (default 7 days; 'all' for full history).
- Per-column sort: due-date (default; dateless last) | title |
  modified-date, asc/desc.
- Compact cards with configurable detail panel (hover/always/none),
  recurrence ↻, click-to-open drilldown. Empty columns still shown.
- Reuses the calendar data layer: scope resolution, inclusion, the
  getNoteTagTitles tag fetch from v0.2.0, gtd block parsing.
- markdown-it renderer now handles both gtd-calendar and gtd-kanban
  fences (data-block-type); webview branches getEvents/getKanban.
- SPEC sort-type vocabulary noted for cross-block consistency
  (modified_date accepted as alias).
- Version 0.3.0; 32 new tests (65 total). Read-only — drag-and-drop
  write-back is v0.4.0.
2026-06-13 10:23:49 -04:00

18 KiB

GTD Calendar — Project Specification (v1 Draft)

A fork of 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 (0this-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

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 > 0Done, 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_datemodified-date for cross-block consistency; the old spelling stays accepted as an alias to avoid breaking existing calendars.)

v0.4.0 — Drag-and-drop kanban (write-back)

The first feature that makes 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.