168 lines
4.6 KiB
TypeScript
168 lines
4.6 KiB
TypeScript
import {
|
|
DataAdapter,
|
|
KanbanCard,
|
|
MatrixBoard,
|
|
MatrixConfig,
|
|
} from "./types";
|
|
import resolveScopedFolderIds from "./folderScope";
|
|
import extractGtdBlock from "./gtdBlock";
|
|
import buildKanbanCard from "./buildKanbanCard";
|
|
|
|
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,
|
|
now: Date = new Date()
|
|
): Promise<MatrixResult> {
|
|
const warnings: string[] = [];
|
|
|
|
const folders = await adapter.getFolders();
|
|
const folderIds = resolveScopedFolderIds(
|
|
folders,
|
|
matrixFolderId,
|
|
config.scopeDepth,
|
|
config.scopeAll
|
|
);
|
|
|
|
let scannedNotes = 0;
|
|
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);
|
|
|
|
// Ordinary notes opt in through their gtd block, independently of the
|
|
// to-do inclusion mode, so bodies are required for every scanned note.
|
|
|
|
for (const folderId of folderIds) {
|
|
const notes = await adapter.getNotesInFolder(folderId, true);
|
|
for (const note of notes) {
|
|
scannedNotes += 1;
|
|
if (note.id === matrixNoteId) continue;
|
|
|
|
const result = extractGtdBlock(note.body);
|
|
if (note.is_todo) {
|
|
if (note.todo_completed > 0) continue; // prioritisation view
|
|
if (config.todos === "none") continue;
|
|
if (config.todos === "gtd-only" && !result.found) continue;
|
|
} else if (!result.found) {
|
|
continue;
|
|
}
|
|
|
|
if (result.error) {
|
|
warnings.push(
|
|
`"${note.title}": gtd block problem — ${result.error}`
|
|
);
|
|
}
|
|
|
|
const tags = await adapter.getNoteTagTitles(note.id);
|
|
const card = buildKanbanCard(note, result.block, tags);
|
|
|
|
let topRow: boolean;
|
|
let leftColumn: boolean;
|
|
|
|
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(topLeft, config);
|
|
sortColumn(topRight, config);
|
|
sortColumn(bottomLeft, config);
|
|
sortColumn(bottomRight, config);
|
|
|
|
return {
|
|
board: { topLeft, topRight, bottomLeft, bottomRight },
|
|
warnings,
|
|
scannedFolders: folderIds.length,
|
|
scannedNotes,
|
|
cardCount:
|
|
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 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;
|
|
});
|
|
}
|