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.)
|
||||
|
||||
## 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
|
||||
title: Priorities
|
||||
todos: all
|
||||
urgent-tag: urgent
|
||||
important-tag: important
|
||||
mode: skeleton
|
||||
```
|
||||
````
|
||||
|
||||
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 |
|
||||
| **Not important** | Delegate | Eliminate |
|
||||
| **Active** | Do Next | Scheduled |
|
||||
| **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 |
|
||||
|---|---|---|---|
|
||||
| `mode` | `skeleton`, `eisenhower` | `skeleton` | Which matrix semantics to use. |
|
||||
| `title` | text | — | Heading above the matrix. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `urgent-tag` | any tag name | `urgent` | The tag marking the urgent axis. |
|
||||
| `important-tag` | any tag name | `important` | The tag marking the important 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` | 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. |
|
||||
|
||||
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.)
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "joplin-plugin-gtd-calendar",
|
||||
"version": "0.3.2",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "joplin-plugin-gtd-calendar",
|
||||
"version": "0.3.2",
|
||||
"version": "0.4.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"date-fns": "^2.29.3",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin-plugin-gtd-calendar",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"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,
|
||||
matrixNoteId: string,
|
||||
matrixFolderId: string,
|
||||
config: MatrixConfig
|
||||
config: MatrixConfig,
|
||||
now: Date = new Date()
|
||||
): Promise<MatrixResult> {
|
||||
const warnings: string[] = [];
|
||||
|
||||
@ -48,10 +49,13 @@ export default async function collectMatrix(
|
||||
);
|
||||
|
||||
let scannedNotes = 0;
|
||||
const doFirst: KanbanCard[] = [];
|
||||
const schedule: KanbanCard[] = [];
|
||||
const delegate: KanbanCard[] = [];
|
||||
const eliminate: KanbanCard[] = [];
|
||||
const topLeft: KanbanCard[] = [];
|
||||
const topRight: KanbanCard[] = [];
|
||||
const bottomLeft: 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) {
|
||||
const notes = await adapter.getNotesInFolder(folderId);
|
||||
@ -74,34 +78,60 @@ export default async function collectMatrix(
|
||||
const tags = await adapter.getNoteTagTitles(note.id);
|
||||
const card = buildCard(note, result.block, tags);
|
||||
|
||||
const urgent = tags.includes(config.urgentTag);
|
||||
const important = tags.includes(config.importantTag);
|
||||
let topRow: boolean;
|
||||
let leftColumn: boolean;
|
||||
|
||||
if (urgent && important) doFirst.push(card);
|
||||
else if (important) schedule.push(card);
|
||||
else if (urgent) delegate.push(card);
|
||||
else eliminate.push(card);
|
||||
if (config.mode === "eisenhower") {
|
||||
// Rows: important / not. Columns: urgent / not.
|
||||
topRow = tags.includes(config.importantTag);
|
||||
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(schedule, config);
|
||||
sortColumn(delegate, config);
|
||||
sortColumn(eliminate, config);
|
||||
sortColumn(topLeft, config);
|
||||
sortColumn(topRight, config);
|
||||
sortColumn(bottomLeft, config);
|
||||
sortColumn(bottomRight, config);
|
||||
|
||||
return {
|
||||
board: { doFirst, schedule, delegate, eliminate },
|
||||
board: { topLeft, topRight, bottomLeft, bottomRight },
|
||||
warnings,
|
||||
scannedFolders: folderIds.length,
|
||||
scannedNotes,
|
||||
cardCount:
|
||||
doFirst.length +
|
||||
schedule.length +
|
||||
delegate.length +
|
||||
eliminate.length,
|
||||
topLeft.length +
|
||||
topRight.length +
|
||||
bottomLeft.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(
|
||||
note: RawNote,
|
||||
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,
|
||||
KanbanSortType,
|
||||
MatrixConfig,
|
||||
MatrixMode,
|
||||
URGENT_TAG,
|
||||
IMPORTANT_TAG,
|
||||
IN_PROGRESS_TAG,
|
||||
} from "./types";
|
||||
|
||||
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 knownKeys = [
|
||||
"mode",
|
||||
"title",
|
||||
"scope",
|
||||
"todos",
|
||||
@ -26,12 +29,25 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
||||
"sort",
|
||||
"urgent-tag",
|
||||
"important-tag",
|
||||
"in-progress-tag",
|
||||
"urgent-window",
|
||||
"card-detail",
|
||||
];
|
||||
for (const key of Object.keys(input)) {
|
||||
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 =
|
||||
input.title !== undefined && input.title !== null
|
||||
? 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
|
||||
let cardDetail: MatrixConfig["cardDetail"] = "hover";
|
||||
if (input["card-detail"] !== undefined) {
|
||||
@ -111,6 +140,7 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
||||
}
|
||||
|
||||
return {
|
||||
mode,
|
||||
title,
|
||||
scopeDepth,
|
||||
todos,
|
||||
@ -118,6 +148,8 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
|
||||
sort,
|
||||
urgentTag,
|
||||
importantTag,
|
||||
inProgressTag,
|
||||
urgentWindow,
|
||||
cardDetail,
|
||||
warnings,
|
||||
};
|
||||
|
||||
@ -134,8 +134,11 @@ export interface KanbanBoard {
|
||||
export const URGENT_TAG = "urgent";
|
||||
export const IMPORTANT_TAG = "important";
|
||||
|
||||
export type MatrixMode = "skeleton" | "eisenhower";
|
||||
|
||||
/** Parsed and normalised ```gtd-matrix config. */
|
||||
export interface MatrixConfig {
|
||||
mode: MatrixMode;
|
||||
title: string | null;
|
||||
scopeDepth: number;
|
||||
todos: InclusionMode;
|
||||
@ -143,14 +146,37 @@ export interface MatrixConfig {
|
||||
sort: "asc" | "desc";
|
||||
urgentTag: 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";
|
||||
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 {
|
||||
doFirst: KanbanCard[]; // urgent + important
|
||||
schedule: KanbanCard[]; // important only
|
||||
delegate: KanbanCard[]; // urgent only
|
||||
eliminate: KanbanCard[]; // neither
|
||||
topLeft: KanbanCard[];
|
||||
topRight: KanbanCard[];
|
||||
bottomLeft: KanbanCard[];
|
||||
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 || {
|
||||
doFirst: [],
|
||||
schedule: [],
|
||||
delegate: [],
|
||||
eliminate: [],
|
||||
topLeft: [],
|
||||
topRight: [],
|
||||
bottomLeft: [],
|
||||
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";
|
||||
|
||||
// Axis header row: blank corner + Urgent / Not urgent
|
||||
// Axis header row: blank corner + column labels
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "gtd-matrix-grid";
|
||||
|
||||
grid.appendChild(matrixAxisCell(""));
|
||||
grid.appendChild(matrixAxisCell("Urgent"));
|
||||
grid.appendChild(matrixAxisCell("Not urgent"));
|
||||
grid.appendChild(matrixAxisCell(labels.columns[0]));
|
||||
grid.appendChild(matrixAxisCell(labels.columns[1]));
|
||||
|
||||
grid.appendChild(matrixAxisCell("Important"));
|
||||
grid.appendChild(matrixAxisCell(labels.rows[0]));
|
||||
grid.appendChild(
|
||||
matrixQuadrant(
|
||||
"Do First",
|
||||
board.doFirst,
|
||||
labels.quadrants.topLeft,
|
||||
board.topLeft,
|
||||
"do-first",
|
||||
detail,
|
||||
contentScriptId
|
||||
@ -655,19 +665,19 @@
|
||||
);
|
||||
grid.appendChild(
|
||||
matrixQuadrant(
|
||||
"Schedule",
|
||||
board.schedule,
|
||||
labels.quadrants.topRight,
|
||||
board.topRight,
|
||||
"schedule",
|
||||
detail,
|
||||
contentScriptId
|
||||
)
|
||||
);
|
||||
|
||||
grid.appendChild(matrixAxisCell("Not important"));
|
||||
grid.appendChild(matrixAxisCell(labels.rows[1]));
|
||||
grid.appendChild(
|
||||
matrixQuadrant(
|
||||
"Delegate",
|
||||
board.delegate,
|
||||
labels.quadrants.bottomLeft,
|
||||
board.bottomLeft,
|
||||
"delegate",
|
||||
detail,
|
||||
contentScriptId
|
||||
@ -675,8 +685,8 @@
|
||||
);
|
||||
grid.appendChild(
|
||||
matrixQuadrant(
|
||||
"Eliminate",
|
||||
board.eliminate,
|
||||
labels.quadrants.bottomRight,
|
||||
board.bottomRight,
|
||||
"eliminate",
|
||||
detail,
|
||||
contentScriptId
|
||||
|
||||
10
src/index.ts
10
src/index.ts
@ -10,6 +10,7 @@ import parseKanbanConfig from "./Gtd/parseKanbanConfig";
|
||||
import collectKanban from "./Gtd/collectKanban";
|
||||
import parseMatrixConfig from "./Gtd/parseMatrixConfig";
|
||||
import collectMatrix from "./Gtd/collectMatrix";
|
||||
import matrixLabels from "./Gtd/matrixLabels";
|
||||
import { DataAdapter, RawFolder, RawNote } from "./Gtd/types";
|
||||
|
||||
const CONTENT_SCRIPT_ID = "gtd-calendar-renderer";
|
||||
@ -250,7 +251,13 @@ async function handleGetMatrix(message: { rawConfig?: string }) {
|
||||
configError ||
|
||||
"Could not determine which note contains this matrix.",
|
||||
warnings: config.warnings,
|
||||
board: { doFirst: [], schedule: [], delegate: [], eliminate: [] },
|
||||
labels: matrixLabels(config.mode),
|
||||
board: {
|
||||
topLeft: [],
|
||||
topRight: [],
|
||||
bottomLeft: [],
|
||||
bottomRight: [],
|
||||
},
|
||||
stats: null,
|
||||
};
|
||||
}
|
||||
@ -267,6 +274,7 @@ async function handleGetMatrix(message: { rawConfig?: string }) {
|
||||
cardDetail: config.cardDetail,
|
||||
configError,
|
||||
warnings: [...config.warnings, ...result.warnings],
|
||||
labels: matrixLabels(config.mode),
|
||||
board: result.board,
|
||||
stats: {
|
||||
scannedFolders: result.scannedFolders,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"manifest_version": 1,
|
||||
"id": "com.victorwiebe.joplin.plugin.gtd-calendar",
|
||||
"app_min_version": "2.7",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.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",
|
||||
|
||||
@ -10,6 +10,7 @@ describe("parseMatrixConfig", () => {
|
||||
test("applies SPEC defaults", () => {
|
||||
const c = parseMatrixConfig({});
|
||||
expect(c).toMatchObject({
|
||||
mode: "skeleton",
|
||||
title: null,
|
||||
scopeDepth: 0,
|
||||
todos: "gtd-only",
|
||||
@ -17,11 +18,22 @@ describe("parseMatrixConfig", () => {
|
||||
sort: "asc",
|
||||
urgentTag: "urgent",
|
||||
importantTag: "important",
|
||||
inProgressTag: "in-progress",
|
||||
urgentWindow: 3,
|
||||
cardDetail: "hover",
|
||||
});
|
||||
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", () => {
|
||||
const c = parseMatrixConfig({
|
||||
"urgent-tag": "FIRE",
|
||||
@ -104,12 +116,12 @@ describe("collectMatrix — quadrant bucketing", () => {
|
||||
}),
|
||||
"matrix-note",
|
||||
"board",
|
||||
parseMatrixConfig({ todos: "all" })
|
||||
parseMatrixConfig({ todos: "all", mode: "eisenhower" })
|
||||
);
|
||||
expect(result.board.doFirst.map((c) => c.id)).toEqual(["both"]);
|
||||
expect(result.board.schedule.map((c) => c.id)).toEqual(["imp"]);
|
||||
expect(result.board.delegate.map((c) => c.id)).toEqual(["urg"]);
|
||||
expect(result.board.eliminate.map((c) => c.id)).toEqual(["none"]);
|
||||
expect(result.board.topLeft.map((c) => c.id)).toEqual(["both"]);
|
||||
expect(result.board.topRight.map((c) => c.id)).toEqual(["imp"]);
|
||||
expect(result.board.bottomLeft.map((c) => c.id)).toEqual(["urg"]);
|
||||
expect(result.board.bottomRight.map((c) => c.id)).toEqual(["none"]);
|
||||
expect(result.cardCount).toBe(4);
|
||||
});
|
||||
|
||||
@ -122,13 +134,13 @@ describe("collectMatrix — quadrant bucketing", () => {
|
||||
makeAdapter(folders, notes, { done: ["urgent", "important"] }),
|
||||
"matrix-note",
|
||||
"board",
|
||||
parseMatrixConfig({ todos: "all" })
|
||||
parseMatrixConfig({ todos: "all", mode: "eisenhower" })
|
||||
);
|
||||
const allIds = [
|
||||
...result.board.doFirst,
|
||||
...result.board.schedule,
|
||||
...result.board.delegate,
|
||||
...result.board.eliminate,
|
||||
...result.board.topLeft,
|
||||
...result.board.topRight,
|
||||
...result.board.bottomLeft,
|
||||
...result.board.bottomRight,
|
||||
].map((c) => c.id);
|
||||
expect(allIds).toEqual(["open"]);
|
||||
});
|
||||
@ -141,11 +153,12 @@ describe("collectMatrix — quadrant bucketing", () => {
|
||||
"board",
|
||||
parseMatrixConfig({
|
||||
todos: "all",
|
||||
mode: "eisenhower",
|
||||
"urgent-tag": "fire",
|
||||
"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 () => {
|
||||
@ -158,10 +171,10 @@ describe("collectMatrix — quadrant bucketing", () => {
|
||||
makeAdapter(folders, notes),
|
||||
"matrix-note",
|
||||
"board",
|
||||
parseMatrixConfig({ todos: "all" })
|
||||
parseMatrixConfig({ todos: "all", mode: "eisenhower" })
|
||||
);
|
||||
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 () => {
|
||||
@ -173,10 +186,10 @@ describe("collectMatrix — quadrant bucketing", () => {
|
||||
makeAdapter(folders, notes),
|
||||
"matrix-note",
|
||||
"board",
|
||||
parseMatrixConfig({}) // todos: gtd-only default
|
||||
parseMatrixConfig({ mode: "eisenhower" }) // todos: gtd-only default
|
||||
);
|
||||
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 () => {
|
||||
@ -189,9 +202,9 @@ describe("collectMatrix — quadrant bucketing", () => {
|
||||
makeAdapter(folders, notes),
|
||||
"matrix-note",
|
||||
"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",
|
||||
"later",
|
||||
"nodate",
|
||||
@ -204,8 +217,122 @@ describe("collectMatrix — quadrant bucketing", () => {
|
||||
makeAdapter(folders, notes, { r: ["recurring", "urgent"] }),
|
||||
"matrix-note",
|
||||
"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