v0.4.0: Eisenhower Matrix (gtd-matrix block)
- 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
This commit is contained in:
parent
1cc6f184f0
commit
4cb6700a20
35
README.md
35
README.md
@ -118,6 +118,41 @@ 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
|
||||||
|
|
||||||
|
A third view: a 2×2 prioritisation matrix, driven by two tags. Add a `gtd-matrix` block anywhere:
|
||||||
|
|
||||||
|
````
|
||||||
|
```gtd-matrix
|
||||||
|
title: Priorities
|
||||||
|
todos: all
|
||||||
|
urgent-tag: urgent
|
||||||
|
important-tag: important
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
To-dos are sorted into four quadrants by which of the two axis tags they carry:
|
||||||
|
|
||||||
|
| | **Urgent** | **Not urgent** |
|
||||||
|
|---|---|---|
|
||||||
|
| **Important** | Do First | Schedule |
|
||||||
|
| **Not important** | Delegate | Eliminate |
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
| Option | Values | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `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. |
|
||||||
|
| `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.
|
||||||
|
|
||||||
## Drilldown
|
## Drilldown
|
||||||
|
|
||||||
Everything is clickable: single-event tiles, every row of a multi-event hover card, every chip in the month grid, every Unscheduled chip. Clicking opens the source note.
|
Everything is clickable: single-event tiles, every row of a multi-event hover card, every chip in the month grid, every Unscheduled chip. Clicking opens the source note.
|
||||||
|
|||||||
24
SPEC.md
24
SPEC.md
@ -236,9 +236,24 @@ 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 — Drag-and-drop kanban (write-back)
|
### v0.4.0 — Eisenhower Matrix (`gtd-matrix` block) — SHIPPED
|
||||||
|
|
||||||
The first feature that makes the plugin a **controller** of data rather than a viewer — warrants its own design pass.
|
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:
|
- 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).
|
- → *In Progress*: add the `in-progress` tag (create the tag if absent; resolve its id; associate).
|
||||||
@ -246,3 +261,8 @@ The first feature that makes the plugin a **controller** of data rather than a v
|
|||||||
- → *Backlog*: remove the tag / clear completion.
|
- → *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.
|
- **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.
|
- 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.
|
||||||
|
|||||||
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.1",
|
"version": "0.3.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "joplin-plugin-gtd-calendar",
|
"name": "joplin-plugin-gtd-calendar",
|
||||||
"version": "0.3.1",
|
"version": "0.3.2",
|
||||||
"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.3.2",
|
"version": "0.4.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",
|
||||||
|
|||||||
151
src/Gtd/collectMatrix.ts
Normal file
151
src/Gtd/collectMatrix.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import {
|
||||||
|
DataAdapter,
|
||||||
|
KanbanCard,
|
||||||
|
MatrixBoard,
|
||||||
|
MatrixConfig,
|
||||||
|
RawNote,
|
||||||
|
RECURRING_TAG,
|
||||||
|
} from "./types";
|
||||||
|
import resolveScopedFolderIds from "./folderScope";
|
||||||
|
import extractGtdBlock from "./gtdBlock";
|
||||||
|
import { resolveEventDate } from "./resolveDate";
|
||||||
|
|
||||||
|
export interface MatrixResult {
|
||||||
|
board: MatrixBoard;
|
||||||
|
warnings: string[];
|
||||||
|
scannedFolders: number;
|
||||||
|
scannedNotes: number;
|
||||||
|
cardCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a 2x2 Eisenhower matrix from to-dos in scope.
|
||||||
|
*
|
||||||
|
* - Notes are excluded (to-dos only).
|
||||||
|
* - Completed to-dos are excluded entirely: the matrix is a
|
||||||
|
* prioritisation view; completions live in the kanban's Done column.
|
||||||
|
* - Quadrants from the two axis tags:
|
||||||
|
* urgent + important -> Do First
|
||||||
|
* important only -> Schedule
|
||||||
|
* urgent only -> Delegate
|
||||||
|
* neither -> Eliminate (default bucket for untagged)
|
||||||
|
*
|
||||||
|
* The matrix note itself is always excluded.
|
||||||
|
*/
|
||||||
|
export default async function collectMatrix(
|
||||||
|
adapter: DataAdapter,
|
||||||
|
matrixNoteId: string,
|
||||||
|
matrixFolderId: string,
|
||||||
|
config: MatrixConfig
|
||||||
|
): Promise<MatrixResult> {
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
const folders = await adapter.getFolders();
|
||||||
|
const folderIds = resolveScopedFolderIds(
|
||||||
|
folders,
|
||||||
|
matrixFolderId,
|
||||||
|
config.scopeDepth
|
||||||
|
);
|
||||||
|
|
||||||
|
let scannedNotes = 0;
|
||||||
|
const doFirst: KanbanCard[] = [];
|
||||||
|
const schedule: KanbanCard[] = [];
|
||||||
|
const delegate: KanbanCard[] = [];
|
||||||
|
const eliminate: KanbanCard[] = [];
|
||||||
|
|
||||||
|
for (const folderId of folderIds) {
|
||||||
|
const notes = await adapter.getNotesInFolder(folderId);
|
||||||
|
for (const note of notes) {
|
||||||
|
scannedNotes += 1;
|
||||||
|
if (note.id === matrixNoteId) continue;
|
||||||
|
if (!note.is_todo) continue; // to-dos only
|
||||||
|
if (note.todo_completed > 0) continue; // prioritisation view
|
||||||
|
if (config.todos === "none") continue;
|
||||||
|
|
||||||
|
const result = extractGtdBlock(note.body);
|
||||||
|
if (config.todos === "gtd-only" && !result.found) continue;
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
warnings.push(
|
||||||
|
`"${note.title}": gtd block problem — ${result.error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (urgent && important) doFirst.push(card);
|
||||||
|
else if (important) schedule.push(card);
|
||||||
|
else if (urgent) delegate.push(card);
|
||||||
|
else eliminate.push(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortColumn(doFirst, config);
|
||||||
|
sortColumn(schedule, config);
|
||||||
|
sortColumn(delegate, config);
|
||||||
|
sortColumn(eliminate, config);
|
||||||
|
|
||||||
|
return {
|
||||||
|
board: { doFirst, schedule, delegate, eliminate },
|
||||||
|
warnings,
|
||||||
|
scannedFolders: folderIds.length,
|
||||||
|
scannedNotes,
|
||||||
|
cardCount:
|
||||||
|
doFirst.length +
|
||||||
|
schedule.length +
|
||||||
|
delegate.length +
|
||||||
|
eliminate.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCard(
|
||||||
|
note: RawNote,
|
||||||
|
block: ReturnType<typeof extractGtdBlock>["block"],
|
||||||
|
tags: string[]
|
||||||
|
): KanbanCard {
|
||||||
|
return {
|
||||||
|
id: note.id,
|
||||||
|
title: block && block.title ? block.title : note.title,
|
||||||
|
date: resolveEventDate(note, block),
|
||||||
|
completed: false,
|
||||||
|
completedTime: 0,
|
||||||
|
isRecurring: tags.includes(RECURRING_TAG),
|
||||||
|
bgColour: block ? block.bgColour : null,
|
||||||
|
fgColour: block ? block.fgColour : null,
|
||||||
|
icon: block ? block.icon : null,
|
||||||
|
text: block ? block.text : null,
|
||||||
|
updatedTime: note.updated_time,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortColumn(cards: KanbanCard[], config: MatrixConfig): void {
|
||||||
|
const direction = config.sort === "desc" ? -1 : 1;
|
||||||
|
|
||||||
|
cards.sort((a, b) => {
|
||||||
|
let comparison: number;
|
||||||
|
|
||||||
|
if (config.sortType === "due-date") {
|
||||||
|
const aHas = a.date !== null;
|
||||||
|
const bHas = b.date !== null;
|
||||||
|
if (aHas && !bHas) return -1;
|
||||||
|
if (!aHas && bHas) return 1;
|
||||||
|
if (!aHas && !bHas) {
|
||||||
|
comparison = 0;
|
||||||
|
} else {
|
||||||
|
comparison = (a.date as string).localeCompare(b.date as string);
|
||||||
|
}
|
||||||
|
} else if (config.sortType === "modified-date") {
|
||||||
|
comparison = a.updatedTime - b.updatedTime;
|
||||||
|
} else {
|
||||||
|
comparison = a.title.localeCompare(b.title, undefined, {
|
||||||
|
sensitivity: "base",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return comparison * direction;
|
||||||
|
});
|
||||||
|
}
|
||||||
130
src/Gtd/parseMatrixConfig.ts
Normal file
130
src/Gtd/parseMatrixConfig.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
InclusionMode,
|
||||||
|
KanbanSortType,
|
||||||
|
MatrixConfig,
|
||||||
|
URGENT_TAG,
|
||||||
|
IMPORTANT_TAG,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
const INCLUSION_MODES: InclusionMode[] = ["gtd-only", "all", "none"];
|
||||||
|
const SORT_TYPES: KanbanSortType[] = ["due-date", "title", "modified-date"];
|
||||||
|
const CARD_DETAILS = ["hover", "always", "none"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise a parsed gtd-matrix YAML object into a MatrixConfig with
|
||||||
|
* SPEC defaults. Invalid values produce warnings, not failures.
|
||||||
|
*/
|
||||||
|
export default function parseMatrixConfig(raw: any): MatrixConfig {
|
||||||
|
const warnings: string[] = [];
|
||||||
|
const input = raw && typeof raw === "object" ? raw : {};
|
||||||
|
|
||||||
|
const knownKeys = [
|
||||||
|
"title",
|
||||||
|
"scope",
|
||||||
|
"todos",
|
||||||
|
"sort-type",
|
||||||
|
"sort",
|
||||||
|
"urgent-tag",
|
||||||
|
"important-tag",
|
||||||
|
"card-detail",
|
||||||
|
];
|
||||||
|
for (const key of Object.keys(input)) {
|
||||||
|
if (!knownKeys.includes(key)) warnings.push(`Unknown option "${key}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const title =
|
||||||
|
input.title !== undefined && input.title !== null
|
||||||
|
? String(input.title)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// scope (shared semantics with gtd-calendar / gtd-kanban)
|
||||||
|
let scopeDepth = 0;
|
||||||
|
if (input.scope !== undefined) {
|
||||||
|
const value = input.scope;
|
||||||
|
if (value === "this-folder") {
|
||||||
|
scopeDepth = 0;
|
||||||
|
} else if (value === "children") {
|
||||||
|
scopeDepth = Infinity;
|
||||||
|
} else if (Number.isInteger(Number(value)) && Number(value) >= 0) {
|
||||||
|
scopeDepth = Number(value);
|
||||||
|
} else {
|
||||||
|
warnings.push(`Invalid scope "${value}" (using "this-folder")`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todos inclusion
|
||||||
|
let todos: InclusionMode = "gtd-only";
|
||||||
|
if (input.todos !== undefined) {
|
||||||
|
const candidate = String(input.todos).toLowerCase() as InclusionMode;
|
||||||
|
if (INCLUSION_MODES.includes(candidate)) {
|
||||||
|
todos = candidate;
|
||||||
|
} else {
|
||||||
|
warnings.push(`Invalid todos "${input.todos}" (using "gtd-only")`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort-type
|
||||||
|
let sortType: KanbanSortType = "due-date";
|
||||||
|
if (input["sort-type"] !== undefined) {
|
||||||
|
let candidate = String(input["sort-type"]).toLowerCase();
|
||||||
|
if (candidate === "modified_date") candidate = "modified-date"; // alias
|
||||||
|
if (SORT_TYPES.includes(candidate as KanbanSortType)) {
|
||||||
|
sortType = candidate as KanbanSortType;
|
||||||
|
} else {
|
||||||
|
warnings.push(
|
||||||
|
`Invalid sort-type "${input["sort-type"]}" (using "due-date")`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort direction
|
||||||
|
let sort: MatrixConfig["sort"] = "asc";
|
||||||
|
if (input.sort !== undefined) {
|
||||||
|
const candidate = String(input.sort).toLowerCase();
|
||||||
|
if (candidate === "asc" || candidate === "desc") {
|
||||||
|
sort = candidate;
|
||||||
|
} else {
|
||||||
|
warnings.push(`Invalid sort "${input.sort}" (using "asc")`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// axis tags (configurable; lowercased for tag comparison)
|
||||||
|
const urgentTag = parseTag(input["urgent-tag"], URGENT_TAG);
|
||||||
|
const importantTag = parseTag(input["important-tag"], IMPORTANT_TAG);
|
||||||
|
if (urgentTag === importantTag) {
|
||||||
|
warnings.push(
|
||||||
|
`urgent-tag and important-tag are both "${urgentTag}" — quadrants will not separate`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// card-detail
|
||||||
|
let cardDetail: MatrixConfig["cardDetail"] = "hover";
|
||||||
|
if (input["card-detail"] !== undefined) {
|
||||||
|
const candidate = String(input["card-detail"]).toLowerCase();
|
||||||
|
if (CARD_DETAILS.includes(candidate)) {
|
||||||
|
cardDetail = candidate as MatrixConfig["cardDetail"];
|
||||||
|
} else {
|
||||||
|
warnings.push(
|
||||||
|
`Invalid card-detail "${input["card-detail"]}" (using "hover")`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
scopeDepth,
|
||||||
|
todos,
|
||||||
|
sortType,
|
||||||
|
sort,
|
||||||
|
urgentTag,
|
||||||
|
importantTag,
|
||||||
|
cardDetail,
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTag(value: any, fallback: string): string {
|
||||||
|
if (value === undefined || value === null) return fallback;
|
||||||
|
const candidate = String(value).trim().toLowerCase();
|
||||||
|
return candidate === "" ? fallback : candidate;
|
||||||
|
}
|
||||||
@ -126,3 +126,31 @@ export interface KanbanBoard {
|
|||||||
inProgress: KanbanCard[];
|
inProgress: KanbanCard[];
|
||||||
done: KanbanCard[];
|
done: KanbanCard[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default tags for the Eisenhower matrix axes. Overridable via the
|
||||||
|
* gtd-matrix `urgent-tag:` / `important-tag:` options.
|
||||||
|
*/
|
||||||
|
export const URGENT_TAG = "urgent";
|
||||||
|
export const IMPORTANT_TAG = "important";
|
||||||
|
|
||||||
|
/** Parsed and normalised ```gtd-matrix config. */
|
||||||
|
export interface MatrixConfig {
|
||||||
|
title: string | null;
|
||||||
|
scopeDepth: number;
|
||||||
|
todos: InclusionMode;
|
||||||
|
sortType: KanbanSortType;
|
||||||
|
sort: "asc" | "desc";
|
||||||
|
urgentTag: string;
|
||||||
|
importantTag: string;
|
||||||
|
cardDetail: "hover" | "always" | "none";
|
||||||
|
warnings: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The four Eisenhower quadrants. Cards are KanbanCards (same shape). */
|
||||||
|
export interface MatrixBoard {
|
||||||
|
doFirst: KanbanCard[]; // urgent + important
|
||||||
|
schedule: KanbanCard[]; // important only
|
||||||
|
delegate: KanbanCard[]; // urgent only
|
||||||
|
eliminate: KanbanCard[]; // neither
|
||||||
|
}
|
||||||
|
|||||||
@ -360,3 +360,50 @@
|
|||||||
.gtd-unscheduled-group + .gtd-unscheduled-group {
|
.gtd-unscheduled-group + .gtd-unscheduled-group {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* gtd-matrix (Eisenhower) */
|
||||||
|
|
||||||
|
.gtd-matrix-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gtd-matrix-axis {
|
||||||
|
font-size: 0.78em;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.75;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.2em 0.3em;
|
||||||
|
writing-mode: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gtd-matrix-quadrant {
|
||||||
|
border: 1px solid rgba(128, 128, 128, 0.35);
|
||||||
|
border-radius: 6px;
|
||||||
|
min-height: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gtd-matrix-do-first {
|
||||||
|
background-color: rgba(220, 60, 60, 0.08);
|
||||||
|
border-color: rgba(220, 60, 60, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gtd-matrix-schedule {
|
||||||
|
background-color: rgba(60, 140, 220, 0.08);
|
||||||
|
border-color: rgba(60, 140, 220, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gtd-matrix-delegate {
|
||||||
|
background-color: rgba(230, 160, 40, 0.08);
|
||||||
|
border-color: rgba(230, 160, 40, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gtd-matrix-eliminate {
|
||||||
|
background-color: rgba(128, 128, 128, 0.06);
|
||||||
|
border-color: rgba(128, 128, 128, 0.4);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
const CALENDAR_FENCE = "gtd-calendar";
|
const CALENDAR_FENCE = "gtd-calendar";
|
||||||
const KANBAN_FENCE = "gtd-kanban";
|
const KANBAN_FENCE = "gtd-kanban";
|
||||||
|
const MATRIX_FENCE = "gtd-matrix";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Markdown-it content script.
|
* Markdown-it content script.
|
||||||
@ -38,14 +39,21 @@ export default function (context: { contentScriptId: string }) {
|
|||||||
const token = tokens[idx];
|
const token = tokens[idx];
|
||||||
const isCalendar = token.info === CALENDAR_FENCE;
|
const isCalendar = token.info === CALENDAR_FENCE;
|
||||||
const isKanban = token.info === KANBAN_FENCE;
|
const isKanban = token.info === KANBAN_FENCE;
|
||||||
if (!isCalendar && !isKanban)
|
const isMatrix = token.info === MATRIX_FENCE;
|
||||||
|
if (!isCalendar && !isKanban && !isMatrix)
|
||||||
return defaultRender(tokens, idx, options, env, self);
|
return defaultRender(tokens, idx, options, env, self);
|
||||||
|
|
||||||
const encodedConfig = encodeURIComponent(token.content);
|
const encodedConfig = encodeURIComponent(token.content);
|
||||||
const blockType = isCalendar ? "calendar" : "kanban";
|
const blockType = isCalendar
|
||||||
|
? "calendar"
|
||||||
|
: isKanban
|
||||||
|
? "kanban"
|
||||||
|
: "matrix";
|
||||||
const label = isCalendar
|
const label = isCalendar
|
||||||
? "Loading GTD Calendar…"
|
? "Loading GTD Calendar…"
|
||||||
: "Loading GTD Kanban…";
|
: isKanban
|
||||||
|
? "Loading GTD Kanban…"
|
||||||
|
: "Loading GTD Matrix…";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`<div class="gtd-calendar-placeholder"` +
|
`<div class="gtd-calendar-placeholder"` +
|
||||||
|
|||||||
@ -40,7 +40,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messageType =
|
const messageType =
|
||||||
blockType === "kanban" ? "getKanban" : "getEvents";
|
blockType === "kanban"
|
||||||
|
? "getKanban"
|
||||||
|
: blockType === "matrix"
|
||||||
|
? "getMatrix"
|
||||||
|
: "getEvents";
|
||||||
|
|
||||||
webviewApi
|
webviewApi
|
||||||
.postMessage(contentScriptId, {
|
.postMessage(contentScriptId, {
|
||||||
@ -54,6 +58,8 @@
|
|||||||
}
|
}
|
||||||
if (blockType === "kanban") {
|
if (blockType === "kanban") {
|
||||||
renderKanban(el, payload, contentScriptId);
|
renderKanban(el, payload, contentScriptId);
|
||||||
|
} else if (blockType === "matrix") {
|
||||||
|
renderMatrix(el, payload, contentScriptId);
|
||||||
} else {
|
} else {
|
||||||
renderPayload(el, payload, contentScriptId);
|
renderPayload(el, payload, contentScriptId);
|
||||||
}
|
}
|
||||||
@ -590,6 +596,138 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Eisenhower matrix (gtd-matrix block)
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
function renderMatrix(el, payload, contentScriptId) {
|
||||||
|
el.innerHTML = "";
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "gtd-matrix";
|
||||||
|
|
||||||
|
if (payload.title) {
|
||||||
|
const heading = document.createElement("h3");
|
||||||
|
heading.className = "gtd-calendar-title";
|
||||||
|
heading.textContent = payload.title;
|
||||||
|
wrapper.appendChild(heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.configError) {
|
||||||
|
const error = document.createElement("p");
|
||||||
|
error.className = "gtd-calendar-error";
|
||||||
|
error.textContent = "Config problem: " + payload.configError;
|
||||||
|
wrapper.appendChild(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
(payload.warnings || []).forEach(function (warning) {
|
||||||
|
const warn = document.createElement("p");
|
||||||
|
warn.className = "gtd-calendar-warning";
|
||||||
|
warn.textContent = "\u26A0 " + warning;
|
||||||
|
wrapper.appendChild(warn);
|
||||||
|
});
|
||||||
|
|
||||||
|
const board = payload.board || {
|
||||||
|
doFirst: [],
|
||||||
|
schedule: [],
|
||||||
|
delegate: [],
|
||||||
|
eliminate: [],
|
||||||
|
};
|
||||||
|
const detail = payload.cardDetail || "hover";
|
||||||
|
|
||||||
|
// Axis header row: blank corner + Urgent / Not urgent
|
||||||
|
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("Important"));
|
||||||
|
grid.appendChild(
|
||||||
|
matrixQuadrant(
|
||||||
|
"Do First",
|
||||||
|
board.doFirst,
|
||||||
|
"do-first",
|
||||||
|
detail,
|
||||||
|
contentScriptId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
grid.appendChild(
|
||||||
|
matrixQuadrant(
|
||||||
|
"Schedule",
|
||||||
|
board.schedule,
|
||||||
|
"schedule",
|
||||||
|
detail,
|
||||||
|
contentScriptId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
grid.appendChild(matrixAxisCell("Not important"));
|
||||||
|
grid.appendChild(
|
||||||
|
matrixQuadrant(
|
||||||
|
"Delegate",
|
||||||
|
board.delegate,
|
||||||
|
"delegate",
|
||||||
|
detail,
|
||||||
|
contentScriptId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
grid.appendChild(
|
||||||
|
matrixQuadrant(
|
||||||
|
"Eliminate",
|
||||||
|
board.eliminate,
|
||||||
|
"eliminate",
|
||||||
|
detail,
|
||||||
|
contentScriptId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.appendChild(grid);
|
||||||
|
|
||||||
|
if (payload.stats) {
|
||||||
|
const meta = document.createElement("p");
|
||||||
|
meta.className = "gtd-calendar-debug-meta";
|
||||||
|
meta.textContent =
|
||||||
|
payload.stats.cardCount +
|
||||||
|
" card(s) from " +
|
||||||
|
payload.stats.scannedNotes +
|
||||||
|
" note(s) in " +
|
||||||
|
payload.stats.scannedFolders +
|
||||||
|
" folder(s)";
|
||||||
|
wrapper.appendChild(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
el.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matrixAxisCell(label) {
|
||||||
|
const cell = document.createElement("div");
|
||||||
|
cell.className = "gtd-matrix-axis";
|
||||||
|
cell.textContent = label;
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matrixQuadrant(title, cards, kind, detail, contentScriptId) {
|
||||||
|
const quadrant = document.createElement("div");
|
||||||
|
quadrant.className = "gtd-matrix-quadrant gtd-matrix-" + kind;
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.className = "gtd-kanban-column-header";
|
||||||
|
header.textContent = title + " (" + cards.length + ")";
|
||||||
|
quadrant.appendChild(header);
|
||||||
|
|
||||||
|
const list = document.createElement("div");
|
||||||
|
list.className = "gtd-kanban-cards";
|
||||||
|
|
||||||
|
cards.forEach(function (card) {
|
||||||
|
list.appendChild(renderCard(card, detail, contentScriptId));
|
||||||
|
});
|
||||||
|
|
||||||
|
quadrant.appendChild(list);
|
||||||
|
return quadrant;
|
||||||
|
}
|
||||||
|
|
||||||
processPlaceholders();
|
processPlaceholders();
|
||||||
|
|
||||||
// Joplin may replace rendered content without re-running asset scripts;
|
// Joplin may replace rendered content without re-running asset scripts;
|
||||||
|
|||||||
63
src/index.ts
63
src/index.ts
@ -8,6 +8,8 @@ import collectEvents from "./Gtd/collectEvents";
|
|||||||
import groupEvents from "./Gtd/groupEvents";
|
import groupEvents from "./Gtd/groupEvents";
|
||||||
import parseKanbanConfig from "./Gtd/parseKanbanConfig";
|
import parseKanbanConfig from "./Gtd/parseKanbanConfig";
|
||||||
import collectKanban from "./Gtd/collectKanban";
|
import collectKanban from "./Gtd/collectKanban";
|
||||||
|
import parseMatrixConfig from "./Gtd/parseMatrixConfig";
|
||||||
|
import collectMatrix from "./Gtd/collectMatrix";
|
||||||
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";
|
||||||
@ -76,6 +78,8 @@ joplin.plugins.register({
|
|||||||
return handleGetEvents(message);
|
return handleGetEvents(message);
|
||||||
case "getKanban":
|
case "getKanban":
|
||||||
return handleGetKanban(message);
|
return handleGetKanban(message);
|
||||||
|
case "getMatrix":
|
||||||
|
return handleGetMatrix(message);
|
||||||
case "openNote":
|
case "openNote":
|
||||||
return handleOpenNote(message);
|
return handleOpenNote(message);
|
||||||
default:
|
default:
|
||||||
@ -213,6 +217,65 @@ async function handleGetKanban(message: { rawConfig?: string }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleGetMatrix(message: { rawConfig?: string }) {
|
||||||
|
let rawParsed: any = {};
|
||||||
|
let configError: string | null = null;
|
||||||
|
try {
|
||||||
|
rawParsed = YAML.parse(message.rawConfig || "") || {};
|
||||||
|
} catch (error) {
|
||||||
|
configError = String(error);
|
||||||
|
}
|
||||||
|
const config = parseMatrixConfig(rawParsed);
|
||||||
|
|
||||||
|
let sourceNote: { id: string; title: string; parentId: string } | null =
|
||||||
|
null;
|
||||||
|
try {
|
||||||
|
const note = await joplin.workspace.selectedNote();
|
||||||
|
if (note) {
|
||||||
|
sourceNote = {
|
||||||
|
id: note.id,
|
||||||
|
title: note.title,
|
||||||
|
parentId: note.parent_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("gtd-matrix: could not resolve selected note", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceNote) {
|
||||||
|
return {
|
||||||
|
title: config.title,
|
||||||
|
cardDetail: config.cardDetail,
|
||||||
|
configError:
|
||||||
|
configError ||
|
||||||
|
"Could not determine which note contains this matrix.",
|
||||||
|
warnings: config.warnings,
|
||||||
|
board: { doFirst: [], schedule: [], delegate: [], eliminate: [] },
|
||||||
|
stats: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await collectMatrix(
|
||||||
|
joplinAdapter,
|
||||||
|
sourceNote.id,
|
||||||
|
sourceNote.parentId,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: config.title,
|
||||||
|
cardDetail: config.cardDetail,
|
||||||
|
configError,
|
||||||
|
warnings: [...config.warnings, ...result.warnings],
|
||||||
|
board: result.board,
|
||||||
|
stats: {
|
||||||
|
scannedFolders: result.scannedFolders,
|
||||||
|
scannedNotes: result.scannedNotes,
|
||||||
|
cardCount: result.cardCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function handleOpenNote(message: { noteId?: string }) {
|
async function handleOpenNote(message: { noteId?: string }) {
|
||||||
if (!message.noteId) return { ok: false, error: "No noteId provided" };
|
if (!message.noteId) return { ok: false, error: "No noteId provided" };
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -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.3.2",
|
"version": "0.4.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",
|
||||||
|
|||||||
211
src/tests/Gtd/matrix.test.ts
Normal file
211
src/tests/Gtd/matrix.test.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import parseMatrixConfig from "../../Gtd/parseMatrixConfig";
|
||||||
|
import collectMatrix from "../../Gtd/collectMatrix";
|
||||||
|
import { DataAdapter, RawFolder, RawNote } from "../../Gtd/types";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// parseMatrixConfig
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("parseMatrixConfig", () => {
|
||||||
|
test("applies SPEC defaults", () => {
|
||||||
|
const c = parseMatrixConfig({});
|
||||||
|
expect(c).toMatchObject({
|
||||||
|
title: null,
|
||||||
|
scopeDepth: 0,
|
||||||
|
todos: "gtd-only",
|
||||||
|
sortType: "due-date",
|
||||||
|
sort: "asc",
|
||||||
|
urgentTag: "urgent",
|
||||||
|
importantTag: "important",
|
||||||
|
cardDetail: "hover",
|
||||||
|
});
|
||||||
|
expect(c.warnings).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses custom axis tags, lowercased", () => {
|
||||||
|
const c = parseMatrixConfig({
|
||||||
|
"urgent-tag": "FIRE",
|
||||||
|
"important-tag": "Matters",
|
||||||
|
});
|
||||||
|
expect(c.urgentTag).toBe("fire");
|
||||||
|
expect(c.importantTag).toBe("matters");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("warns when both axis tags are identical", () => {
|
||||||
|
const c = parseMatrixConfig({
|
||||||
|
"urgent-tag": "now",
|
||||||
|
"important-tag": "NOW",
|
||||||
|
});
|
||||||
|
expect(c.warnings.some((w) => w.includes("will not separate"))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid values warn and fall back", () => {
|
||||||
|
const c = parseMatrixConfig({
|
||||||
|
todos: "some",
|
||||||
|
"sort-type": "priority",
|
||||||
|
"card-detail": "popup",
|
||||||
|
mystery: 1,
|
||||||
|
});
|
||||||
|
expect(c.todos).toBe("gtd-only");
|
||||||
|
expect(c.sortType).toBe("due-date");
|
||||||
|
expect(c.cardDetail).toBe("hover");
|
||||||
|
expect(c.warnings.length).toBeGreaterThanOrEqual(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// collectMatrix
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function makeNote(overrides: Partial<RawNote>): RawNote {
|
||||||
|
return {
|
||||||
|
id: "id",
|
||||||
|
title: "Card",
|
||||||
|
parent_id: "board",
|
||||||
|
is_todo: 1,
|
||||||
|
todo_due: 0,
|
||||||
|
todo_completed: 0,
|
||||||
|
updated_time: 0,
|
||||||
|
body: "```gtd\n```",
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeAdapter(
|
||||||
|
folders: RawFolder[],
|
||||||
|
notes: RawNote[],
|
||||||
|
tagsByNote: Record<string, string[]> = {}
|
||||||
|
): DataAdapter {
|
||||||
|
return {
|
||||||
|
getFolders: async () => folders,
|
||||||
|
getNotesInFolder: async (folderId: string) =>
|
||||||
|
notes.filter((n) => n.parent_id === folderId),
|
||||||
|
getNoteTagTitles: async (noteId: string) => tagsByNote[noteId] || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const folders: RawFolder[] = [{ id: "board", parent_id: "root" }];
|
||||||
|
|
||||||
|
describe("collectMatrix — quadrant bucketing", () => {
|
||||||
|
test("all four quadrants route correctly; untagged lands in Eliminate", async () => {
|
||||||
|
const notes = [
|
||||||
|
makeNote({ id: "both", title: "Crisis" }),
|
||||||
|
makeNote({ id: "imp", title: "Strategy" }),
|
||||||
|
makeNote({ id: "urg", title: "Interruption" }),
|
||||||
|
makeNote({ id: "none", title: "Timewaster" }),
|
||||||
|
];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes, {
|
||||||
|
both: ["urgent", "important"],
|
||||||
|
imp: ["important"],
|
||||||
|
urg: ["urgent"],
|
||||||
|
}),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all" })
|
||||||
|
);
|
||||||
|
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.cardCount).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("completed to-dos are excluded entirely", async () => {
|
||||||
|
const notes = [
|
||||||
|
makeNote({ id: "open", title: "Open" }),
|
||||||
|
makeNote({ id: "done", title: "Done", todo_completed: 123456 }),
|
||||||
|
];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes, { done: ["urgent", "important"] }),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all" })
|
||||||
|
);
|
||||||
|
const allIds = [
|
||||||
|
...result.board.doFirst,
|
||||||
|
...result.board.schedule,
|
||||||
|
...result.board.delegate,
|
||||||
|
...result.board.eliminate,
|
||||||
|
].map((c) => c.id);
|
||||||
|
expect(allIds).toEqual(["open"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("custom axis tags are honoured", async () => {
|
||||||
|
const notes = [makeNote({ id: "x", title: "Hot" })];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes, { x: ["fire", "matters"] }),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({
|
||||||
|
todos: "all",
|
||||||
|
"urgent-tag": "fire",
|
||||||
|
"important-tag": "matters",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(result.board.doFirst.map((c) => c.id)).toEqual(["x"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plain notes and the matrix note itself are excluded", async () => {
|
||||||
|
const notes = [
|
||||||
|
makeNote({ id: "matrix-note", title: "The matrix note" }),
|
||||||
|
makeNote({ id: "n", is_todo: 0, title: "A note" }),
|
||||||
|
makeNote({ id: "t", title: "A todo" }),
|
||||||
|
];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all" })
|
||||||
|
);
|
||||||
|
expect(result.cardCount).toBe(1);
|
||||||
|
expect(result.board.eliminate.map((c) => c.id)).toEqual(["t"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("gtd-only requires a gtd block", async () => {
|
||||||
|
const notes = [
|
||||||
|
makeNote({ id: "in", body: "```gtd\n```" }),
|
||||||
|
makeNote({ id: "out", body: "no block here" }),
|
||||||
|
];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({}) // todos: gtd-only default
|
||||||
|
);
|
||||||
|
expect(result.cardCount).toBe(1);
|
||||||
|
expect(result.board.eliminate.map((c) => c.id)).toEqual(["in"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("due-date sort within a quadrant, dateless last", async () => {
|
||||||
|
const notes = [
|
||||||
|
makeNote({ id: "later", todo_due: new Date(2026, 6, 20).getTime() }),
|
||||||
|
makeNote({ id: "soon", todo_due: new Date(2026, 6, 1).getTime() }),
|
||||||
|
makeNote({ id: "nodate" }),
|
||||||
|
];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all" })
|
||||||
|
);
|
||||||
|
expect(result.board.eliminate.map((c) => c.id)).toEqual([
|
||||||
|
"soon",
|
||||||
|
"later",
|
||||||
|
"nodate",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("recurring tag flags cards", async () => {
|
||||||
|
const notes = [makeNote({ id: "r" })];
|
||||||
|
const result = await collectMatrix(
|
||||||
|
makeAdapter(folders, notes, { r: ["recurring", "urgent"] }),
|
||||||
|
"matrix-note",
|
||||||
|
"board",
|
||||||
|
parseMatrixConfig({ todos: "all" })
|
||||||
|
);
|
||||||
|
expect(result.board.delegate[0].isRecurring).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user