- New gtd-kanban block renders a three-column board from to-dos: Backlog / In Progress / Done. Notes excluded; to-dos only. - Bucketing: Done wins (todo_completed), else in-progress tag, else Backlog. in-progress tag name configurable (IN_PROGRESS_TAG constant, in-progress-tag: option). - Backlog shows all uncompleted untagged to-dos; Done filtered to a configurable done-window (default 7 days; 'all' for full history). - Per-column sort: due-date (default; dateless last) | title | modified-date, asc/desc. - Compact cards with configurable detail panel (hover/always/none), recurrence ↻, click-to-open drilldown. Empty columns still shown. - Reuses the calendar data layer: scope resolution, inclusion, the getNoteTagTitles tag fetch from v0.2.0, gtd block parsing. - markdown-it renderer now handles both gtd-calendar and gtd-kanban fences (data-block-type); webview branches getEvents/getKanban. - SPEC sort-type vocabulary noted for cross-block consistency (modified_date accepted as alias). - Version 0.3.0; 32 new tests (65 total). Read-only — drag-and-drop write-back is v0.4.0.
133 lines
3.6 KiB
TypeScript
133 lines
3.6 KiB
TypeScript
import { KanbanConfig, InclusionMode, KanbanSortType } 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-kanban YAML object into a KanbanConfig with
|
|
* SPEC defaults. Invalid values produce warnings, not failures.
|
|
*/
|
|
export default function parseKanbanConfig(raw: any): KanbanConfig {
|
|
const warnings: string[] = [];
|
|
const input = raw && typeof raw === "object" ? raw : {};
|
|
|
|
const knownKeys = [
|
|
"title",
|
|
"scope",
|
|
"todos",
|
|
"sort-type",
|
|
"sort",
|
|
"in-progress-tag",
|
|
"card-detail",
|
|
"done-window",
|
|
];
|
|
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)
|
|
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: KanbanConfig["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")`);
|
|
}
|
|
}
|
|
|
|
// in-progress tag (configurable; lowercased to match tag comparison)
|
|
let inProgressTag = "in-progress";
|
|
if (
|
|
input["in-progress-tag"] !== undefined &&
|
|
input["in-progress-tag"] !== null &&
|
|
String(input["in-progress-tag"]).trim() !== ""
|
|
) {
|
|
inProgressTag = String(input["in-progress-tag"]).trim().toLowerCase();
|
|
}
|
|
|
|
// card-detail
|
|
let cardDetail: KanbanConfig["cardDetail"] = "hover";
|
|
if (input["card-detail"] !== undefined) {
|
|
const candidate = String(input["card-detail"]).toLowerCase();
|
|
if (CARD_DETAILS.includes(candidate)) {
|
|
cardDetail = candidate as KanbanConfig["cardDetail"];
|
|
} else {
|
|
warnings.push(
|
|
`Invalid card-detail "${input["card-detail"]}" (using "hover")`
|
|
);
|
|
}
|
|
}
|
|
|
|
// done-window (days; "all" => Infinity)
|
|
let doneWindow = 7;
|
|
if (input["done-window"] !== undefined) {
|
|
const value = input["done-window"];
|
|
if (String(value).toLowerCase() === "all") {
|
|
doneWindow = Infinity;
|
|
} else if (Number.isInteger(Number(value)) && Number(value) >= 0) {
|
|
doneWindow = Number(value);
|
|
} else {
|
|
warnings.push(`Invalid done-window "${value}" (using 7)`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
title,
|
|
scopeDepth,
|
|
todos,
|
|
sortType,
|
|
sort,
|
|
inProgressTag,
|
|
cardDetail,
|
|
doneWindow,
|
|
warnings,
|
|
};
|
|
}
|