Single release covering two feature groups (CHANGELOG 0.7.0 + 0.6.0): - gtd-gantt block: project-grouped timeline of bars (notes with begin/end dates) and milestones (to-dos on their due date); dual-role fence, swimlanes, month tick header, today line, read-only. - notebook: targeting across all four view blocks; scope: all for profile-wide scans with size guardrails; note body fetched only when the inclusion mode actually needs the gtd block. jest 140/140. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
177 lines
4.7 KiB
TypeScript
177 lines
4.7 KiB
TypeScript
import {
|
|
InclusionMode,
|
|
KanbanSortType,
|
|
MatrixConfig,
|
|
MatrixMode,
|
|
URGENT_TAG,
|
|
IMPORTANT_TAG,
|
|
IN_PROGRESS_TAG,
|
|
} from "./types";
|
|
import { parseNotebookOption } from "./resolveNotebook";
|
|
|
|
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 = [
|
|
"mode",
|
|
"title",
|
|
"scope",
|
|
"notebook",
|
|
"todos",
|
|
"sort-type",
|
|
"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)
|
|
: null;
|
|
|
|
// scope (shared semantics with gtd-calendar / gtd-kanban)
|
|
let scopeDepth = 0;
|
|
let scopeAll = false;
|
|
if (input.scope !== undefined) {
|
|
const value = input.scope;
|
|
if (value === "all") {
|
|
scopeAll = true;
|
|
} else 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")`);
|
|
}
|
|
}
|
|
|
|
const notebook = parseNotebookOption(input.notebook);
|
|
if (scopeAll && notebook) {
|
|
warnings.push(
|
|
`"notebook" is ignored when scope: all (scanning every notebook)`
|
|
);
|
|
}
|
|
|
|
// 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`
|
|
);
|
|
}
|
|
|
|
// 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) {
|
|
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 {
|
|
mode,
|
|
title,
|
|
scopeDepth,
|
|
scopeAll,
|
|
notebook,
|
|
todos,
|
|
sortType,
|
|
sort,
|
|
urgentTag,
|
|
importantTag,
|
|
inProgressTag,
|
|
urgentWindow,
|
|
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;
|
|
}
|