v0.3.2: per-type Unscheduled visibility toggles

- New gtd-calendar options unscheduled-todos and unscheduled-notes
  (default yes), independently showing/hiding each Unscheduled
  sub-section. Accepts yes/no, true/false, show/hide.
- Solves duplicate Unscheduled lists when two calendars share a note:
  e.g. show unscheduled to-dos under the week strip, unscheduled notes
  under the month grid.
- Permissive parseBoolean helper; invalid values warn and default to yes.
- Threaded through payload; webview gates each sub-section on its flag
  (empty OR toggled-off both omit cleanly).
- Version 0.3.2; 4 new tests (69 total); README updated.
This commit is contained in:
Victor Wiebe 2026-06-13 14:41:30 +00:00 committed by vwiebe
parent a3d7acde7f
commit 1cc6f184f0
8 changed files with 108 additions and 11 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "joplin-plugin-gtd-calendar",
"version": "0.1.0",
"version": "0.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "joplin-plugin-gtd-calendar",
"version": "0.1.0",
"version": "0.3.1",
"license": "MIT",
"dependencies": {
"date-fns": "^2.29.3",

View File

@ -1,6 +1,6 @@
{
"name": "joplin-plugin-gtd-calendar",
"version": "0.3.1",
"version": "0.3.2",
"scripts": {
"test": "jest",
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",

View File

@ -30,6 +30,8 @@ export default function parseCalendarConfig(raw: any): CalendarConfig {
"todos",
"sort",
"sort-type",
"unscheduled-todos",
"unscheduled-notes",
];
for (const key of Object.keys(input)) {
if (!knownKeys.includes(key)) warnings.push(`Unknown option "${key}"`);
@ -95,7 +97,49 @@ export default function parseCalendarConfig(raw: any): CalendarConfig {
}
}
return { view, title, scopeDepth, notes, todos, sort, sortType, warnings };
// Unscheduled visibility toggles (default: shown).
const unscheduledTodos = parseBoolean(
input["unscheduled-todos"],
"unscheduled-todos",
warnings
);
const unscheduledNotes = parseBoolean(
input["unscheduled-notes"],
"unscheduled-notes",
warnings
);
return {
view,
title,
scopeDepth,
notes,
todos,
sort,
sortType,
unscheduledTodos,
unscheduledNotes,
warnings,
};
}
/**
* Parse a permissive boolean: yes/no, true/false, show/hide (case-
* insensitive). Absent true (sections shown by default). Invalid
* true, with a warning.
*/
function parseBoolean(
value: any,
optionName: string,
warnings: string[]
): boolean {
if (value === undefined || value === null) return true;
if (typeof value === "boolean") return value;
const candidate = String(value).trim().toLowerCase();
if (["yes", "true", "show", "on"].includes(candidate)) return true;
if (["no", "false", "hide", "off"].includes(candidate)) return false;
warnings.push(`Invalid ${optionName} "${value}" (using "yes")`);
return true;
}
function parseInclusion(

View File

@ -8,6 +8,9 @@ export interface CalendarConfig {
todos: InclusionMode;
sort: "asc" | "desc";
sortType: "title" | "modified_date";
/** Whether to show the Unscheduled to-dos / notes sub-sections. */
unscheduledTodos: boolean;
unscheduledNotes: boolean;
/** Non-fatal problems found while parsing, surfaced in the UI. */
warnings: string[];
}

View File

@ -148,9 +148,19 @@
wrapper.appendChild(empty);
}
if (calendar.unscheduled.length > 0) {
const showTodos = payload.unscheduledTodos !== false;
const showNotes = payload.unscheduledNotes !== false;
if (
calendar.unscheduled.length > 0 &&
(showTodos || showNotes)
) {
wrapper.appendChild(
renderUnscheduled(calendar.unscheduled, contentScriptId)
renderUnscheduled(
calendar.unscheduled,
contentScriptId,
showTodos,
showNotes
)
);
}
@ -370,7 +380,7 @@
// Unscheduled section (GTD bucket)
// ------------------------------------------------------------------
function renderUnscheduled(events, contentScriptId) {
function renderUnscheduled(events, contentScriptId, showTodos, showNotes) {
const section = document.createElement("div");
section.className = "gtd-unscheduled";
@ -382,8 +392,9 @@
});
// To-dos first (actionable), notes second (reference). Each
// sub-section is omitted entirely when it has no items.
if (todos.length > 0) {
// sub-section is omitted when empty OR when toggled off via the
// unscheduled-todos / unscheduled-notes config options.
if (showTodos && todos.length > 0) {
section.appendChild(
renderUnscheduledGroup(
"Unscheduled to-dos",
@ -392,7 +403,7 @@
)
);
}
if (notes.length > 0) {
if (showNotes && notes.length > 0) {
section.appendChild(
renderUnscheduledGroup(
"Unscheduled notes",

View File

@ -144,6 +144,8 @@ async function handleGetEvents(message: { rawConfig?: string }) {
warnings: [...config.warnings, ...result.warnings],
sourceNote,
calendar,
unscheduledTodos: config.unscheduledTodos,
unscheduledNotes: config.unscheduledNotes,
stats: {
scannedFolders: result.scannedFolders,
scannedNotes: result.scannedNotes,

View File

@ -2,7 +2,7 @@
"manifest_version": 1,
"id": "com.victorwiebe.joplin.plugin.gtd-calendar",
"app_min_version": "2.7",
"version": "0.3.1",
"version": "0.3.2",
"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.",
"author": "Victor Wiebe",

View File

@ -411,3 +411,40 @@ describe("collectEvents — recurrence detection", () => {
expect(note.isRecurring).toBe(false);
});
});
describe("parseCalendarConfig — unscheduled toggles", () => {
test("default to shown when absent", () => {
const c = parseCalendarConfig({});
expect(c.unscheduledTodos).toBe(true);
expect(c.unscheduledNotes).toBe(true);
});
test("yes/no are honoured", () => {
const c = parseCalendarConfig({
"unscheduled-todos": "yes",
"unscheduled-notes": "no",
});
expect(c.unscheduledTodos).toBe(true);
expect(c.unscheduledNotes).toBe(false);
});
test("true/false and show/hide are also accepted", () => {
expect(
parseCalendarConfig({ "unscheduled-todos": "false" }).unscheduledTodos
).toBe(false);
expect(
parseCalendarConfig({ "unscheduled-notes": "hide" }).unscheduledNotes
).toBe(false);
expect(
parseCalendarConfig({ "unscheduled-todos": true }).unscheduledTodos
).toBe(true);
});
test("invalid value warns and defaults to shown", () => {
const c = parseCalendarConfig({ "unscheduled-todos": "maybe" });
expect(c.unscheduledTodos).toBe(true);
expect(c.warnings.some((w) => w.includes("unscheduled-todos"))).toBe(
true
);
});
});