joplin-plugin-gtd-calendar/src/Gtd/parseKanbanConfig.ts
Victor Wiebe d3c0f0ee7f v0.3.0: read-only kanban board (gtd-kanban block)
- 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.
2026-06-13 10:23:49 -04:00

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,
};
}