From b064a6414222d41e0d378392254423a3268a3cdb Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 7 Sep 2026 20:22:29 -0400 Subject: [PATCH] fix: create missing period folders and explain failures Clicking a week/month/quarter/year in a vault whose configured folder does not exist yet failed with a generic 'Failed to create X note: ', because vault.create refuses to create parent folders. Create the folder (and any missing ancestors) first, then report what actually went wrong. - Every failure path now names the offending path and the reason, and points at the relevant settings section; diagnostic notices stay up for 10s. - A configured-but-missing template used to fall back to basic frontmatter silently; the creation notice now says the template was not found. - The unreachable !date.isValid() branch after detectPeriodType is gone; the 'not a periodic note' notice now lists the configured formats it expected. --- src/main.ts | 114 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 15 deletions(-) diff --git a/src/main.ts b/src/main.ts index c996b23..5c92096 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import { WorkspaceLeaf, Notice, TFile, + TFolder, TAbstractFile, getAllTags, moment, @@ -527,31 +528,105 @@ export default class WaypointPlugin extends Plugin { if (!file) { file = await this.createPeriodNote(fullPath, periodSettings, date, config.label); if (!file) return; - new Notice(`Created ${config.label.toLowerCase()} note: ${filename}`); } const target = leaf || this.app.workspace.getLeaf(false); await target.openFile(file); } - /** Create a period note from its template, or from minimal frontmatter. */ + /** + * Create a period note from its template, or from minimal frontmatter. + * + * Every failure path names the offending path and the reason: the usual + * cause is a configured folder that does not exist yet, which `vault.create` + * refuses outright rather than creating. + */ private async createPeriodNote( fullPath: string, periodSettings: PeriodNoteSettings, date: moment.Moment, label: string, ): Promise { + const noun = label.toLowerCase(); + const slash = fullPath.lastIndexOf('/'); + const folder = slash < 0 ? '' : fullPath.slice(0, slash); + try { - const templateFile = this.resolveTemplateFile(periodSettings.templateFile); - const content = templateFile - ? await this.app.vault.read(templateFile) - : `---\ntype: ${periodSettings.typeProperty}\ndate: ${date.format('YYYY-MM-DD')}\n---\n\n`; - return await this.app.vault.create(fullPath, content); + await this.ensureFolderExists(folder); } catch (err: unknown) { - const message = err instanceof Error ? err.message : String(err); - new Notice(`Failed to create ${label.toLowerCase()} note: ${message}`); + new Notice( + `Waypoint: could not create the folder "${folder}" for the ${noun} note.\n` + + `${describeError(err)}\n` + + 'Check Settings → Waypoint Sidebar → Periodic Notes.', + DIAGNOSTIC_NOTICE_MS, + ); return null; } + + const configuredTemplate = periodSettings.templateFile; + const templateFile = this.resolveTemplateFile(configuredTemplate); + + let content: string; + if (templateFile) { + try { + content = await this.app.vault.read(templateFile); + } catch (err: unknown) { + new Notice( + `Waypoint: could not read the template "${templateFile.path}" for the ${noun} note.\n` + + describeError(err), + DIAGNOSTIC_NOTICE_MS, + ); + return null; + } + } else { + content = `---\ntype: ${periodSettings.typeProperty}\ndate: ${date.format('YYYY-MM-DD')}\n---\n\n`; + } + + let file: TFile; + try { + file = await this.app.vault.create(fullPath, content); + } catch (err: unknown) { + new Notice( + `Waypoint: could not create the ${noun} note at "${fullPath}".\n${describeError(err)}`, + DIAGNOSTIC_NOTICE_MS, + ); + return null; + } + + // A configured-but-missing template otherwise fails silently: the note + // just appears with the fallback frontmatter and no explanation. + if (configuredTemplate && !templateFile) { + new Notice( + `Created ${noun} note: ${file.basename}\n` + + `Template "${configuredTemplate}" was not found, so a basic note was created instead.`, + DIAGNOSTIC_NOTICE_MS, + ); + } else { + new Notice(`Created ${noun} note: ${file.basename}`); + } + return file; + } + + /** + * Create `folder` and any missing ancestors. + * `vault.create` throws when the parent folder is absent, so this runs first. + */ + private async ensureFolderExists(folder: string): Promise { + if (!folder) return; + if (this.app.vault.getAbstractFileByPath(folder) instanceof TFolder) return; + + let path = ''; + for (const segment of folder.split('/')) { + if (!segment) continue; + path = path ? `${path}/${segment}` : segment; + if (this.app.vault.getAbstractFileByPath(path) instanceof TFolder) continue; + try { + await this.app.vault.createFolder(path); + } catch (err: unknown) { + // Someone else may have created it between the check and the call. + if (!(this.app.vault.getAbstractFileByPath(path) instanceof TFolder)) throw err; + } + } } /** The setting may or may not already carry the .md extension. */ @@ -591,17 +666,19 @@ export default class WaypointPlugin extends Plugin { const detected = this.detectPeriodType(file.basename); if (!detected) { - new Notice('Current file is not a periodic note (daily/weekly/monthly/quarterly/yearly)'); + const formats = PERIOD_DETECTION_ORDER + .map(p => `${PERIOD_CONFIGS[p].label.toLowerCase()} "${this.settings[PERIOD_CONFIGS[p].key].nameFormat}"`) + .join(', '); + new Notice( + `Waypoint: "${file.basename}" does not match any configured periodic note format.\n` + + `Expected one of: ${formats}.`, + DIAGNOSTIC_NOTICE_MS, + ); return; } const { period, date } = detected; - if (!date.isValid()) { - new Notice(`Could not parse date from filename: ${file.basename}`); - return; - } - const amount = direction === 'next' ? 1 : -1; // Map period to moment duration unit @@ -649,3 +726,10 @@ function basenameFromPath(path: string): string { const name = path.slice(path.lastIndexOf('/') + 1); return name.replace(/\.[^/.]+$/, ''); } + +/** Notices that carry a diagnosis need longer on screen than the default. */ +const DIAGNOSTIC_NOTICE_MS = 10000; + +function describeError(err: unknown): string { + return err instanceof Error ? err.message : String(err); +}