v0.5.0: the Skeleton Matrix (mode option on gtd-matrix)
- New default mode 'skeleton': rows = active (in-progress tag, shared with the kanban); columns = due soon (due date within urgent-window days, overdue included, or the urgent tag as manual override) - Quadrants: Do Next / Scheduled / On Deck / Backlog — no 'Eliminate'; a committed vault needs when-and-what's-moving, not triage - in-progress + urgent + dateless -> Do Next (urgent is the override) - Dateless active work -> Scheduled; completed to-dos still excluded - mode: eisenhower preserves the classic (urgent/important tags) - MatrixBoard now positional (topLeft..bottomRight); per-mode labels travel in the payload; rendering fully shared between modes - New options: mode, in-progress-tag, urgent-window (default 3) - Version 0.5.0; 8 new tests (88 total); README + SPEC updated
This commit is contained in:
parent
4cb6700a20
commit
6bb3227574
34
README.md
34
README.md
@ -118,37 +118,49 @@ A completed to-do always lands in **Done**, even if it still carries the in-prog
|
|||||||
|
|
||||||
Cards are compact (title, recurrence ↻ if applicable) and click through to the to-do. The board is **read-only** in this version — it reflects your to-dos' state but doesn't change it. (Drag-and-drop to move cards between columns is planned.)
|
Cards are compact (title, recurrence ↻ if applicable) and click through to the to-do. The board is **read-only** in this version — it reflects your to-dos' state but doesn't change it. (Drag-and-drop to move cards between columns is planned.)
|
||||||
|
|
||||||
## Eisenhower Matrix
|
## The Matrix (Skeleton & Eisenhower)
|
||||||
|
|
||||||
A third view: a 2×2 prioritisation matrix, driven by two tags. Add a `gtd-matrix` block anywhere:
|
A third view: a 2×2 prioritisation matrix. Add a `gtd-matrix` block anywhere:
|
||||||
|
|
||||||
````
|
````
|
||||||
```gtd-matrix
|
```gtd-matrix
|
||||||
title: Priorities
|
title: Priorities
|
||||||
todos: all
|
todos: all
|
||||||
urgent-tag: urgent
|
mode: skeleton
|
||||||
important-tag: important
|
|
||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|
||||||
To-dos are sorted into four quadrants by which of the two axis tags they carry:
|
Two modes:
|
||||||
|
|
||||||
| | **Urgent** | **Not urgent** |
|
### `mode: skeleton` (default) — the Skeleton Matrix
|
||||||
|
|
||||||
|
Built to complement the kanban and calendar, using the tags and dates you already maintain. Rows ask *am I on this?* (the in-progress tag); columns ask *is the clock running?* (due within `urgent-window` days — overdue counts — or carrying the urgent tag as a manual override):
|
||||||
|
|
||||||
|
| | **Due soon** | **Not due soon** |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Important** | Do First | Schedule |
|
| **Active** | Do Next | Scheduled |
|
||||||
| **Not important** | Delegate | Eliminate |
|
| **Not active** | On Deck | Backlog |
|
||||||
|
|
||||||
A to-do with neither tag lands in **Eliminate** — the matrix's pointed default. **Completed to-dos don't appear at all**: the matrix is for prioritising open work (the kanban's Done column is where completions live).
|
**On Deck** is the quadrant to watch: due soon, not yet started. Nothing is ever labelled "Eliminate" — a Backlog item is simply low priority, not condemned. Dateless in-progress work sits in Scheduled (active, no clock). Completed to-dos don't appear at all.
|
||||||
|
|
||||||
|
### `mode: eisenhower` — the classic
|
||||||
|
|
||||||
|
Quadrants from the urgent/important tags: Do First (both), Schedule (important), Delegate (urgent), Eliminate (neither).
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
| Option | Values | Default | Description |
|
| Option | Values | Default | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
| `mode` | `skeleton`, `eisenhower` | `skeleton` | Which matrix semantics to use. |
|
||||||
| `title` | text | — | Heading above the matrix. |
|
| `title` | text | — | Heading above the matrix. |
|
||||||
| `scope` | `this-folder`, `children`, integer | `this-folder` | Same folder-scanning rules as the calendar and kanban. |
|
| `scope` | `this-folder`, `children`, integer | `this-folder` | Same folder-scanning rules as the calendar and kanban. |
|
||||||
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Which to-dos appear. Plain notes never do. |
|
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Which to-dos appear. Plain notes never do. |
|
||||||
| `sort-type` | `due-date`, `title`, `modified-date` | `due-date` | Order within each quadrant; dateless cards sort last under `due-date`. |
|
| `sort-type` | `due-date`, `title`, `modified-date` | `due-date` | Order within each quadrant; dateless cards sort last under `due-date`. |
|
||||||
| `sort` | `asc`, `desc` | `asc` | Sort direction. |
|
| `sort` | `asc`, `desc` | `asc` | Sort direction. |
|
||||||
| `urgent-tag` | any tag name | `urgent` | The tag marking the urgent axis. |
|
| `urgent-tag` | any tag name | `urgent` | Eisenhower's urgent axis; the Skeleton mode's manual due-soon override. |
|
||||||
| `important-tag` | any tag name | `important` | The tag marking the important axis. |
|
| `important-tag` | any tag name | `important` | Eisenhower's important axis (unused in Skeleton mode). |
|
||||||
|
| `in-progress-tag` | any tag name | `in-progress` | Skeleton mode's active row — same tag as the kanban's In Progress column. |
|
||||||
|
| `urgent-window` | integer (days) | `3` | Skeleton mode: how close a due date must be to count as "due soon". |
|
||||||
| `card-detail` | `hover`, `always`, `none` | `hover` | Card detail line behaviour, as on the kanban. |
|
| `card-detail` | `hover`, `always`, `none` | `hover` | Card detail line behaviour, as on the kanban. |
|
||||||
|
|
||||||
Cards behave like kanban cards: compact, ↻ for recurring, click to open. Read-only.
|
Cards behave like kanban cards: compact, ↻ for recurring, click to open. Read-only.
|
||||||
|
|||||||
3
SPEC.md
3
SPEC.md
@ -237,6 +237,9 @@ done-window: 7 # days; integer or "all"; default 7
|
|||||||
**`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.)
|
**`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
|
### v0.4.0 — Eisenhower Matrix (`gtd-matrix` block) — SHIPPED
|
||||||
|
### v0.5.0 — Skeleton Matrix (`mode:` on gtd-matrix) — SHIPPED
|
||||||
|
|
||||||
|
Real-world feedback: Eisenhower's opinion axes (and "Eliminate" in particular) fit triage, not a vault of already-committed work. The **Skeleton Matrix** replaces them with two derived axes the system already maintains: rows = active (`in-progress-tag`, shared with the kanban); columns = due soon (`urgent-window` days, default 3, overdue included; `urgent` tag as manual override — in-progress + urgent + dateless is Do Next by design). Quadrants: Do Next / Scheduled / On Deck / Backlog; dateless active work → Scheduled; nothing is ever "Eliminate". Implemented as `mode: skeleton | eisenhower` on the same block — positional MatrixBoard (topLeft…bottomRight), labels supplied per mode via payload, shared rendering. Skeleton is the default (it matches the plugin's own tag/date vocabulary); classic Eisenhower preserved via `mode: eisenhower`.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "joplin-plugin-gtd-calendar",
|
"name": "joplin-plugin-gtd-calendar",
|
||||||
"version": "0.3.2",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "joplin-plugin-gtd-calendar",
|
"name": "joplin-plugin-gtd-calendar",
|
||||||
"version": "0.3.2",
|
"version": "0.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "joplin-plugin-gtd-calendar",
|
"name": "joplin-plugin-gtd-calendar",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
|
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
|
||||||
|
|||||||
@ -36,7 +36,8 @@ export default async function collectMatrix(
|
|||||||
adapter: DataAdapter,
|
adapter: DataAdapter,
|
||||||
matrixNoteId: string,
|
matrixNoteId: string,
|
||||||
matrixFolderId: string,
|
matrixFolderId: string,
|
||||||
config: MatrixConfig
|
config: MatrixConfig,
|
||||||
|
now: Date = new Date()
|
||||||
): Promise<MatrixResult> {
|
): Promise<MatrixResult> {
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
|
|
||||||
@ -48,10 +49,13 @@ export default async function collectMatrix(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let scannedNotes = 0;
|
let scannedNotes = 0;
|
||||||
const doFirst: KanbanCard[] = [];
|
const topLeft: KanbanCard[] = [];
|
||||||
const schedule: KanbanCard[] = [];
|
const topRight: KanbanCard[] = [];
|
||||||
const delegate: KanbanCard[] = [];
|
const bottomLeft: KanbanCard[] = [];
|
||||||
const eliminate: KanbanCard[] = [];
|
const bottomRight: KanbanCard[] = [];
|
||||||
|
|
||||||
|
// Skeleton mode: a date on or before this ISO threshold is "due soon".
|
||||||
|
const soonThreshold = isoDaysFromNow(now, config.urgentWindow);
|
||||||
|
|
||||||
for (const folderId of folderIds) {
|
for (const folderId of folderIds) {
|
||||||
const notes = await adapter.getNotesInFolder(folderId);
|
const notes = await adapter.getNotesInFolder(folderId);
|
||||||
@ -74,34 +78,60 @@ export default async function collectMatrix(
|
|||||||
const tags = await adapter.getNoteTagTitles(note.id);
|
const tags = await adapter.getNoteTagTitles(note.id);
|
||||||
const card = buildCard(note, result.block, tags);
|
const card = buildCard(note, result.block, tags);
|
||||||
|
|
||||||
const urgent = tags.includes(config.urgentTag);
|
let topRow: boolean;
|
||||||
const important = tags.includes(config.importantTag);
|
let leftColumn: boolean;
|
||||||
|
|
||||||
if (urgent && important) doFirst.push(card);
|
if (config.mode === "eisenhower") {
|
||||||
else if (important) schedule.push(card);
|
// Rows: important / not. Columns: urgent / not.
|
||||||
else if (urgent) delegate.push(card);
|
topRow = tags.includes(config.importantTag);
|
||||||
else eliminate.push(card);
|
leftColumn = tags.includes(config.urgentTag);
|
||||||
|
} else {
|
||||||
|
// Skeleton. Rows: active (in-progress) / not.
|
||||||
|
// Columns: due soon (urgent tag overrides; else date within
|
||||||
|
// the window, overdue included) / not.
|
||||||
|
topRow = tags.includes(config.inProgressTag);
|
||||||
|
leftColumn =
|
||||||
|
tags.includes(config.urgentTag) ||
|
||||||
|
(card.date !== null && card.date <= soonThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topRow && leftColumn) topLeft.push(card);
|
||||||
|
else if (topRow) topRight.push(card);
|
||||||
|
else if (leftColumn) bottomLeft.push(card);
|
||||||
|
else bottomRight.push(card);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sortColumn(doFirst, config);
|
sortColumn(topLeft, config);
|
||||||
sortColumn(schedule, config);
|
sortColumn(topRight, config);
|
||||||
sortColumn(delegate, config);
|
sortColumn(bottomLeft, config);
|
||||||
sortColumn(eliminate, config);
|
sortColumn(bottomRight, config);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
board: { doFirst, schedule, delegate, eliminate },
|
board: { topLeft, topRight, bottomLeft, bottomRight },
|
||||||
warnings,
|
warnings,
|
||||||
scannedFolders: folderIds.length,
|
scannedFolders: folderIds.length,
|
||||||
scannedNotes,
|
scannedNotes,
|
||||||
cardCount:
|
cardCount:
|
||||||
doFirst.length +
|
topLeft.length +
|
||||||
schedule.length +
|
topRight.length +
|
||||||
delegate.length +
|
bottomLeft.length +
|
||||||
eliminate.length,
|
bottomRight.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ISO yyyy-mm-dd for local `now` plus `days`. */
|
||||||
|
function isoDaysFromNow(now: Date, days: number): string {
|
||||||
|
const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() + days);
|
||||||
|
return (
|
||||||
|
String(d.getFullYear()).padStart(4, "0") +
|
||||||
|
"-" +
|
||||||
|
String(d.getMonth() + 1).padStart(2, "0") +
|
||||||
|
"-" +
|
||||||
|
String(d.getDate()).padStart(2, "0")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function buildCard(
|
function buildCard(
|
||||||
note: RawNote,
|
note: RawNote,
|
||||||
block: ReturnType<typeof extractGtdBlock>["block"],
|
block: ReturnType<typeof extractGtdBlock>["block"],
|
||||||
|
|||||||
28
src/Gtd/matrixLabels.ts
Normal file
28
src/Gtd/matrixLabels.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { MatrixLabels, MatrixMode } from "./types";
|
||||||
|
|
||||||
|
/** Axis and quadrant display labels per matrix mode. */
|
||||||
|
export default function matrixLabels(mode: MatrixMode): MatrixLabels {
|
||||||
|
if (mode === "eisenhower") {
|
||||||
|
return {
|
||||||
|
columns: ["Urgent", "Not urgent"],
|
||||||
|
rows: ["Important", "Not important"],
|
||||||
|
quadrants: {
|
||||||
|
topLeft: "Do First",
|
||||||
|
topRight: "Schedule",
|
||||||
|
bottomLeft: "Delegate",
|
||||||
|
bottomRight: "Eliminate",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// skeleton
|
||||||
|
return {
|
||||||
|
columns: ["Due soon", "Not due soon"],
|
||||||
|
rows: ["Active", "Not active"],
|
||||||
|
quadrants: {
|
||||||
|
topLeft: "Do Next",
|
||||||
|
topRight: "Scheduled",
|
||||||
|
bottomLeft: "On Deck",
|
||||||
|
bottomRight: "Backlog",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -2,8 +2,10 @@ import {
|
|||||||
InclusionMode,
|
InclusionMode,
|
||||||
KanbanSortType,
|
KanbanSortType,
|
||||||
MatrixConfig,
|
MatrixConfig,
|
||||||
|
MatrixMode,
|
||||||
URGENT_TAG,
|
URGENT_TAG,
|
||||||
IMPORTANT_TAG,
|
IMPORTANT_TAG,
|
||||||
|
IN_PROGRESS_TAG,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const INCLUSION_MODES: InclusionMode[] = ["gtd-only", "all", "none"];
|
const INCLUSION_MODES: InclusionMode[] = ["gtd-only", "all", "none"];
|
||||||
@ -19,6 +21,7 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
|||||||
const input = raw && typeof raw === "object" ? raw : {};
|
const input = raw && typeof raw === "object" ? raw : {};
|
||||||
|
|
||||||
const knownKeys = [
|
const knownKeys = [
|
||||||
|
"mode",
|
||||||
"title",
|
"title",
|
||||||
"scope",
|
"scope",
|
||||||
"todos",
|
"todos",
|
||||||
@ -26,12 +29,25 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
|||||||
"sort",
|
"sort",
|
||||||
"urgent-tag",
|
"urgent-tag",
|
||||||
"important-tag",
|
"important-tag",
|
||||||
|
"in-progress-tag",
|
||||||
|
"urgent-window",
|
||||||
"card-detail",
|
"card-detail",
|
||||||
];
|
];
|
||||||
for (const key of Object.keys(input)) {
|
for (const key of Object.keys(input)) {
|
||||||
if (!knownKeys.includes(key)) warnings.push(`Unknown option "${key}"`);
|
if (!knownKeys.includes(key)) warnings.push(`Unknown option "${key}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mode
|
||||||
|
let mode: MatrixMode = "skeleton";
|
||||||
|
if (input.mode !== undefined) {
|
||||||
|
const candidate = String(input.mode).toLowerCase();
|
||||||
|
if (candidate === "skeleton" || candidate === "eisenhower") {
|
||||||
|
mode = candidate as MatrixMode;
|
||||||
|
} else {
|
||||||
|
warnings.push(`Invalid mode "${input.mode}" (using "skeleton")`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const title =
|
const title =
|
||||||
input.title !== undefined && input.title !== null
|
input.title !== undefined && input.title !== null
|
||||||
? String(input.title)
|
? String(input.title)
|
||||||
@ -97,6 +113,19 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skeleton mode: in-progress tag + urgent window
|
||||||
|
const inProgressTag = parseTag(input["in-progress-tag"], IN_PROGRESS_TAG);
|
||||||
|
|
||||||
|
let urgentWindow = 3;
|
||||||
|
if (input["urgent-window"] !== undefined) {
|
||||||
|
const value = input["urgent-window"];
|
||||||
|
if (Number.isInteger(Number(value)) && Number(value) >= 0) {
|
||||||
|
urgentWindow = Number(value);
|
||||||
|
} else {
|
||||||
|
warnings.push(`Invalid urgent-window "${value}" (using 3)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// card-detail
|
// card-detail
|
||||||
let cardDetail: MatrixConfig["cardDetail"] = "hover";
|
let cardDetail: MatrixConfig["cardDetail"] = "hover";
|
||||||
if (input["card-detail"] !== undefined) {
|
if (input["card-detail"] !== undefined) {
|
||||||
@ -111,6 +140,7 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
mode,
|
||||||
title,
|
title,
|
||||||
scopeDepth,
|
scopeDepth,
|
||||||
todos,
|
todos,
|
||||||
@ -118,6 +148,8 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
|||||||
sort,
|
sort,
|
||||||
urgentTag,
|
urgentTag,
|
||||||
importantTag,
|
importantTag,
|
||||||
|
inProgressTag,
|
||||||
|
urgentWindow,
|
||||||
cardDetail,
|
cardDetail,
|
||||||
warnings,
|
warnings,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -134,8 +134,11 @@ export interface KanbanBoard {
|
|||||||
export const URGENT_TAG = "urgent";
|
export const URGENT_TAG = "urgent";
|
||||||
export const IMPORTANT_TAG = "important";
|
export const IMPORTANT_TAG = "important";
|
||||||
|
|
||||||
|
export type MatrixMode = "skeleton" | "eisenhower";
|
||||||
|
|
||||||
/** Parsed and normalised ```gtd-matrix config. */
|
/** Parsed and normalised ```gtd-matrix config. */
|
||||||
export interface MatrixConfig {
|
export interface MatrixConfig {
|
||||||
|
mode: MatrixMode;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
scopeDepth: number;
|
scopeDepth: number;
|
||||||
todos: InclusionMode;
|
todos: InclusionMode;
|
||||||
@ -143,14 +146,37 @@ export interface MatrixConfig {
|
|||||||
sort: "asc" | "desc";
|
sort: "asc" | "desc";
|
||||||
urgentTag: string;
|
urgentTag: string;
|
||||||
importantTag: string;
|
importantTag: string;
|
||||||
|
/** Skeleton mode: tag marking active work (shared with the kanban). */
|
||||||
|
inProgressTag: string;
|
||||||
|
/** Skeleton mode: days ahead within which a due date counts as "due soon". */
|
||||||
|
urgentWindow: number;
|
||||||
cardDetail: "hover" | "always" | "none";
|
cardDetail: "hover" | "always" | "none";
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The four Eisenhower quadrants. Cards are KanbanCards (same shape). */
|
/**
|
||||||
|
* The four matrix quadrants, positional (row x column):
|
||||||
|
* topLeft topRight
|
||||||
|
* bottomLeft bottomRight
|
||||||
|
*
|
||||||
|
* Eisenhower: Do First / Schedule / Delegate / Eliminate.
|
||||||
|
* Skeleton: Do Next / Scheduled / On Deck / Backlog.
|
||||||
|
*/
|
||||||
export interface MatrixBoard {
|
export interface MatrixBoard {
|
||||||
doFirst: KanbanCard[]; // urgent + important
|
topLeft: KanbanCard[];
|
||||||
schedule: KanbanCard[]; // important only
|
topRight: KanbanCard[];
|
||||||
delegate: KanbanCard[]; // urgent only
|
bottomLeft: KanbanCard[];
|
||||||
eliminate: KanbanCard[]; // neither
|
bottomRight: KanbanCard[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Display labels for a matrix mode, sent to the webview. */
|
||||||
|
export interface MatrixLabels {
|
||||||
|
columns: [string, string];
|
||||||
|
rows: [string, string];
|
||||||
|
quadrants: {
|
||||||
|
topLeft: string;
|
||||||
|
topRight: string;
|
||||||
|
bottomLeft: string;
|
||||||
|
bottomRight: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -628,26 +628,36 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const board = payload.board || {
|
const board = payload.board || {
|
||||||
doFirst: [],
|
topLeft: [],
|
||||||
schedule: [],
|
topRight: [],
|
||||||
delegate: [],
|
bottomLeft: [],
|
||||||
eliminate: [],
|
bottomRight: [],
|
||||||
|
};
|
||||||
|
const labels = payload.labels || {
|
||||||
|
columns: ["Urgent", "Not urgent"],
|
||||||
|
rows: ["Important", "Not important"],
|
||||||
|
quadrants: {
|
||||||
|
topLeft: "Do First",
|
||||||
|
topRight: "Schedule",
|
||||||
|
bottomLeft: "Delegate",
|
||||||
|
bottomRight: "Eliminate",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const detail = payload.cardDetail || "hover";
|
const detail = payload.cardDetail || "hover";
|
||||||
|
|
||||||
// Axis header row: blank corner + Urgent / Not urgent
|
// Axis header row: blank corner + column labels
|
||||||
const grid = document.createElement("div");
|
const grid = document.createElement("div");
|
||||||
grid.className = "gtd-matrix-grid";
|
grid.className = "gtd-matrix-grid";
|
||||||
|
|
||||||
grid.appendChild(matrixAxisCell(""));
|
grid.appendChild(matrixAxisCell(""));
|
||||||
grid.appendChild(matrixAxisCell("Urgent"));
|
grid.appendChild(matrixAxisCell(labels.columns[0]));
|
||||||
grid.appendChild(matrixAxisCell("Not urgent"));
|
grid.appendChild(matrixAxisCell(labels.columns[1]));
|
||||||
|
|
||||||
grid.appendChild(matrixAxisCell("Important"));
|
grid.appendChild(matrixAxisCell(labels.rows[0]));
|
||||||
grid.appendChild(
|
grid.appendChild(
|
||||||
matrixQuadrant(
|
matrixQuadrant(
|
||||||
"Do First",
|
labels.quadrants.topLeft,
|
||||||
board.doFirst,
|
board.topLeft,
|
||||||
"do-first",
|
"do-first",
|
||||||
detail,
|
detail,
|
||||||
contentScriptId
|
contentScriptId
|
||||||
@ -655,19 +665,19 @@
|
|||||||
);
|
);
|
||||||
grid.appendChild(
|
grid.appendChild(
|
||||||
matrixQuadrant(
|
matrixQuadrant(
|
||||||
"Schedule",
|
labels.quadrants.topRight,
|
||||||
board.schedule,
|
board.topRight,
|
||||||
"schedule",
|
"schedule",
|
||||||
detail,
|
detail,
|
||||||
contentScriptId
|
contentScriptId
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
grid.appendChild(matrixAxisCell("Not important"));
|
grid.appendChild(matrixAxisCell(labels.rows[1]));
|
||||||
grid.appendChild(
|
grid.appendChild(
|
||||||
matrixQuadrant(
|
matrixQuadrant(
|
||||||
"Delegate",
|
labels.quadrants.bottomLeft,
|
||||||
board.delegate,
|
board.bottomLeft,
|
||||||
"delegate",
|
"delegate",
|
||||||
detail,
|
detail,
|
||||||
contentScriptId
|
contentScriptId
|
||||||
@ -675,8 +685,8 @@
|
|||||||
);
|
);
|
||||||
grid.appendChild(
|
grid.appendChild(
|
||||||
matrixQuadrant(
|
matrixQuadrant(
|
||||||
"Eliminate",
|
labels.quadrants.bottomRight,
|
||||||
board.eliminate,
|
board.bottomRight,
|
||||||
"eliminate",
|
"eliminate",
|
||||||
detail,
|
detail,
|
||||||
contentScriptId
|
contentScriptId
|
||||||
|
|||||||
10
src/index.ts
10
src/index.ts
@ -10,6 +10,7 @@ import parseKanbanConfig from "./Gtd/parseKanbanConfig";
|
|||||||
import collectKanban from "./Gtd/collectKanban";
|
import collectKanban from "./Gtd/collectKanban";
|
||||||
import parseMatrixConfig from "./Gtd/parseMatrixConfig";
|
import parseMatrixConfig from "./Gtd/parseMatrixConfig";
|
||||||
import collectMatrix from "./Gtd/collectMatrix";
|
import collectMatrix from "./Gtd/collectMatrix";
|
||||||
|
import matrixLabels from "./Gtd/matrixLabels";
|
||||||
import { DataAdapter, RawFolder, RawNote } from "./Gtd/types";
|
import { DataAdapter, RawFolder, RawNote } from "./Gtd/types";
|
||||||
|
|
||||||
const CONTENT_SCRIPT_ID = "gtd-calendar-renderer";
|
const CONTENT_SCRIPT_ID = "gtd-calendar-renderer";
|
||||||
@ -250,7 +251,13 @@ async function handleGetMatrix(message: { rawConfig?: string }) {
|
|||||||
configError ||
|
configError ||
|
||||||
"Could not determine which note contains this matrix.",
|
"Could not determine which note contains this matrix.",
|
||||||
warnings: config.warnings,
|
warnings: config.warnings,
|
||||||
board: { doFirst: [], schedule: [], delegate: [], eliminate: [] },
|
labels: matrixLabels(config.mode),
|
||||||
|
board: {
|
||||||
|
topLeft: [],
|
||||||
|
topRight: [],
|
||||||
|
bottomLeft: [],
|
||||||
|
bottomRight: [],
|
||||||
|
},
|
||||||
stats: null,
|
stats: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -267,6 +274,7 @@ async function handleGetMatrix(message: { rawConfig?: string }) {
|
|||||||
cardDetail: config.cardDetail,
|
cardDetail: config.cardDetail,
|
||||||
configError,
|
configError,
|
||||||
warnings: [...config.warnings, ...result.warnings],
|
warnings: [...config.warnings, ...result.warnings],
|
||||||
|
labels: matrixLabels(config.mode),
|
||||||
board: result.board,
|
board: result.board,
|
||||||
stats: {
|
stats: {
|
||||||
scannedFolders: result.scannedFolders,
|
scannedFolders: result.scannedFolders,
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 1,
|
"manifest_version": 1,
|
||||||
"id": "com.victorwiebe.joplin.plugin.gtd-calendar",
|
"id": "com.victorwiebe.joplin.plugin.gtd-calendar",
|
||||||
"app_min_version": "2.7",
|
"app_min_version": "2.7",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"name": "GTD Calendar",
|
"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.",
|
"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",
|
"author": "Victor Wiebe",
|
||||||
|
|||||||
@ -10,6 +10,7 @@ describe("parseMatrixConfig", () => {
|
|||||||
test("applies SPEC defaults", () => {
|
test("applies SPEC defaults", () => {
|
||||||
const c = parseMatrixConfig({});
|
const c = parseMatrixConfig({});
|
||||||
expect(c).toMatchObject({
|
expect(c).toMatchObject({
|
||||||
|
mode: "skeleton",
|
||||||
title: null,
|
title: null,
|
||||||
scopeDepth: 0,
|
scopeDepth: 0,
|
||||||
todos: "gtd-only",
|
todos: "gtd-only",
|
||||||
@ -17,11 +18,22 @@ describe("parseMatrixConfig", () => {
|
|||||||
sort: "asc",
|
sort: "asc",
|
||||||
urgentTag: "urgent",
|
urgentTag: "urgent",
|
||||||
importantTag: "important",
|
importantTag: "important",
|
||||||
|
inProgressTag: "in-progress",
|
||||||
|
urgentWindow: 3,
|
||||||
cardDetail: "hover",
|
cardDetail: "hover",
|
||||||
});
|
});
|
||||||
expect(c.warnings).toHaveLength(0);
|
expect(c.warnings).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("mode parses and warns on invalid", () => {
|
||||||
|
expect(parseMatrixConfig({ mode: "eisenhower" }).mode).toBe(
|
||||||
|
"eisenhower"
|
||||||
|
);
|
||||||
|
const bad = parseMatrixConfig({ mode: "wiebe" });
|
||||||
|
expect(bad.mode).toBe("skeleton");
|
||||||
|
expect(bad.warnings.some((w) => w.includes("mode"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("parses custom axis tags, lowercased", () => {
|
test("parses custom axis tags, lowercased", () => {
|
||||||
const c = parseMatrixConfig({
|
const c = parseMatrixConfig({
|
||||||
"urgent-tag": "FIRE",
|
"urgent-tag": "FIRE",
|
||||||
@ -104,12 +116,12 @@ describe("collectMatrix — quadrant bucketing", () => {
|
|||||||
}),
|
}),
|
||||||
"matrix-note",
|
"matrix-note",
|
||||||
"board",
|
"board",
|
||||||
parseMatrixConfig({ todos: "all" })
|
parseMatrixConfig({ todos: "all", mode: "eisenhower" })
|
||||||
);
|
);
|
||||||
expect(result.board.doFirst.map((c) => c.id)).toEqual(["both"]);
|
expect(result.board.topLeft.map((c) => c.id)).toEqual(["both"]);
|
||||||
expect(result.board.schedule.map((c) => c.id)).toEqual(["imp"]);
|
expect(result.board.topRight.map((c) => c.id)).toEqual(["imp"]);
|
||||||
expect(result.board.delegate.map((c) => c.id)).toEqual(["urg"]);
|
expect(result.board.bottomLeft.map((c) => c.id)).toEqual(["urg"]);
|
||||||
expect(result.board.eliminate.map((c) => c.id)).toEqual(["none"]);
|
expect(result.board.bottomRight.map((c) => c.id)).toEqual(["none"]);
|
||||||
expect(result.cardCount).toBe(4);
|
expect(result.cardCount).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -122,13 +134,13 @@ describe("collectMatrix — quadrant bucketing", () => {
|
|||||||
makeAdapter(folders, notes, { done: ["urgent", "important"] }),
|
makeAdapter(folders, notes, { done: ["urgent", "important"] }),
|
||||||
"matrix-note",
|
"matrix-note",
|
||||||
"board",
|
"board",
|
||||||
parseMatrixConfig({ todos: "all" })
|
parseMatrixConfig({ todos: "all", mode: "eisenhower" })
|
||||||
);
|
);
|
||||||
const allIds = [
|
const allIds = [
|
||||||
...result.board.doFirst,
|
...result.board.topLeft,
|
||||||
...result.board.schedule,
|
...result.board.topRight,
|
||||||
...result.board.delegate,
|
...result.board.bottomLeft,
|
||||||
...result.board.eliminate,
|
...result.board.bottomRight,
|
||||||
].map((c) => c.id);
|
].map((c) => c.id);
|
||||||
expect(allIds).toEqual(["open"]);
|
expect(allIds).toEqual(["open"]);
|
||||||
});
|
});
|
||||||
@ -141,11 +153,12 @@ describe("collectMatrix — quadrant bucketing", () => {
|
|||||||
"board",
|
"board",
|
||||||
parseMatrixConfig({
|
parseMatrixConfig({
|
||||||
todos: "all",
|
todos: "all",
|
||||||
|
mode: "eisenhower",
|
||||||
"urgent-tag": "fire",
|
"urgent-tag": "fire",
|
||||||
"important-tag": "matters",
|
"important-tag": "matters",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(result.board.doFirst.map((c) => c.id)).toEqual(["x"]);
|
expect(result.board.topLeft.map((c) => c.id)).toEqual(["x"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("plain notes and the matrix note itself are excluded", async () => {
|
test("plain notes and the matrix note itself are excluded", async () => {
|
||||||
@ -158,10 +171,10 @@ describe("collectMatrix — quadrant bucketing", () => {
|
|||||||
makeAdapter(folders, notes),
|
makeAdapter(folders, notes),
|
||||||
"matrix-note",
|
"matrix-note",
|
||||||
"board",
|
"board",
|
||||||
parseMatrixConfig({ todos: "all" })
|
parseMatrixConfig({ todos: "all", mode: "eisenhower" })
|
||||||
);
|
);
|
||||||
expect(result.cardCount).toBe(1);
|
expect(result.cardCount).toBe(1);
|
||||||
expect(result.board.eliminate.map((c) => c.id)).toEqual(["t"]);
|
expect(result.board.bottomRight.map((c) => c.id)).toEqual(["t"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("gtd-only requires a gtd block", async () => {
|
test("gtd-only requires a gtd block", async () => {
|
||||||
@ -173,10 +186,10 @@ describe("collectMatrix — quadrant bucketing", () => {
|
|||||||
makeAdapter(folders, notes),
|
makeAdapter(folders, notes),
|
||||||
"matrix-note",
|
"matrix-note",
|
||||||
"board",
|
"board",
|
||||||
parseMatrixConfig({}) // todos: gtd-only default
|
parseMatrixConfig({ mode: "eisenhower" }) // todos: gtd-only default
|
||||||
);
|
);
|
||||||
expect(result.cardCount).toBe(1);
|
expect(result.cardCount).toBe(1);
|
||||||
expect(result.board.eliminate.map((c) => c.id)).toEqual(["in"]);
|
expect(result.board.bottomRight.map((c) => c.id)).toEqual(["in"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("due-date sort within a quadrant, dateless last", async () => {
|
test("due-date sort within a quadrant, dateless last", async () => {
|
||||||
@ -189,9 +202,9 @@ describe("collectMatrix — quadrant bucketing", () => {
|
|||||||
makeAdapter(folders, notes),
|
makeAdapter(folders, notes),
|
||||||
"matrix-note",
|
"matrix-note",
|
||||||
"board",
|
"board",
|
||||||
parseMatrixConfig({ todos: "all" })
|
parseMatrixConfig({ todos: "all", mode: "eisenhower" })
|
||||||
);
|
);
|
||||||
expect(result.board.eliminate.map((c) => c.id)).toEqual([
|
expect(result.board.bottomRight.map((c) => c.id)).toEqual([
|
||||||
"soon",
|
"soon",
|
||||||
"later",
|
"later",
|
||||||
"nodate",
|
"nodate",
|
||||||
@ -204,8 +217,122 @@ describe("collectMatrix — quadrant bucketing", () => {
|
|||||||
makeAdapter(folders, notes, { r: ["recurring", "urgent"] }),
|
makeAdapter(folders, notes, { r: ["recurring", "urgent"] }),
|
||||||
"matrix-note",
|
"matrix-note",
|
||||||
"board",
|
"board",
|
||||||
parseMatrixConfig({ todos: "all" })
|
parseMatrixConfig({ todos: "all", mode: "eisenhower" })
|
||||||
);
|
);
|
||||||
expect(result.board.delegate[0].isRecurring).toBe(true);
|
expect(result.board.bottomLeft[0].isRecurring).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("collectMatrix — Skeleton mode", () => {
|
||||||
|
const NOW = new Date(2026, 5, 15); // Jun 15 2026
|
||||||
|
|
||||||
|
function iso(y: number, m: number, d: number): number {
|
||||||
|
return new Date(y, m - 1, d, 12).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
test("the four quadrants: Do Next / Scheduled / On Deck / Backlog", async () => {
|
||||||
|
const notes = [
|
||||||
|
// active + due within window -> Do Next
|
||||||
|
makeNote({ id: "donext", todo_due: iso(2026, 6, 17) }),
|
||||||
|
// active + due later -> Scheduled
|
||||||
|
makeNote({ id: "sched", todo_due: iso(2026, 6, 25) }),
|
||||||
|
// not active + due soon -> On Deck
|
||||||
|
makeNote({ id: "ondeck", todo_due: iso(2026, 6, 16) }),
|
||||||
|
// not active, no date -> Backlog
|
||||||
|
makeNote({ id: "backlog" }),
|
||||||
|
];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes, {
|
||||||
|
donext: ["in-progress"],
|
||||||
|
sched: ["in-progress"],
|
||||||
|
}),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all" }),
|
||||||
|
NOW
|
||||||
|
);
|
||||||
|
expect(result.board.topLeft.map((c) => c.id)).toEqual(["donext"]);
|
||||||
|
expect(result.board.topRight.map((c) => c.id)).toEqual(["sched"]);
|
||||||
|
expect(result.board.bottomLeft.map((c) => c.id)).toEqual(["ondeck"]);
|
||||||
|
expect(result.board.bottomRight.map((c) => c.id)).toEqual(["backlog"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("urgent tag overrides: in-progress + urgent + no date -> Do Next", async () => {
|
||||||
|
const notes = [makeNote({ id: "hot" })];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes, { hot: ["in-progress", "urgent"] }),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all" }),
|
||||||
|
NOW
|
||||||
|
);
|
||||||
|
expect(result.board.topLeft.map((c) => c.id)).toEqual(["hot"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("urgent tag alone (no date, not active) -> On Deck", async () => {
|
||||||
|
const notes = [makeNote({ id: "flag" })];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes, { flag: ["urgent"] }),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all" }),
|
||||||
|
NOW
|
||||||
|
);
|
||||||
|
expect(result.board.bottomLeft.map((c) => c.id)).toEqual(["flag"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("overdue counts as due soon", async () => {
|
||||||
|
const notes = [makeNote({ id: "late", todo_due: iso(2026, 6, 1) })];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all" }),
|
||||||
|
NOW
|
||||||
|
);
|
||||||
|
expect(result.board.bottomLeft.map((c) => c.id)).toEqual(["late"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dateless in-progress work -> Scheduled (active, no clock)", async () => {
|
||||||
|
const notes = [makeNote({ id: "wip" })];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes, { wip: ["in-progress"] }),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all" }),
|
||||||
|
NOW
|
||||||
|
);
|
||||||
|
expect(result.board.topRight.map((c) => c.id)).toEqual(["wip"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("urgent-window boundary: exactly N days out is due soon; N+1 is not", async () => {
|
||||||
|
const notes = [
|
||||||
|
makeNote({ id: "edge", todo_due: iso(2026, 6, 18) }), // +3 days
|
||||||
|
makeNote({ id: "past-edge", todo_due: iso(2026, 6, 19) }), // +4
|
||||||
|
];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all", "urgent-window": 3 }),
|
||||||
|
NOW
|
||||||
|
);
|
||||||
|
expect(result.board.bottomLeft.map((c) => c.id)).toEqual(["edge"]);
|
||||||
|
expect(result.board.bottomRight.map((c) => c.id)).toEqual([
|
||||||
|
"past-edge",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("gtd date override affects due-soon just like a due date", async () => {
|
||||||
|
const body = "```gtd\ndate: 2026-06-16\n```";
|
||||||
|
const notes = [makeNote({ id: "g", body: body })];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({}),
|
||||||
|
NOW
|
||||||
|
);
|
||||||
|
expect(result.board.bottomLeft.map((c) => c.id)).toEqual(["g"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user