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: <raw error>',
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.
This commit is contained in:
2026-09-07 20:22:29 -04:00
parent ec3ad7d384
commit b064a64142
+99 -15
View File
@@ -5,6 +5,7 @@ import {
WorkspaceLeaf, WorkspaceLeaf,
Notice, Notice,
TFile, TFile,
TFolder,
TAbstractFile, TAbstractFile,
getAllTags, getAllTags,
moment, moment,
@@ -527,31 +528,105 @@ export default class WaypointPlugin extends Plugin {
if (!file) { if (!file) {
file = await this.createPeriodNote(fullPath, periodSettings, date, config.label); file = await this.createPeriodNote(fullPath, periodSettings, date, config.label);
if (!file) return; if (!file) return;
new Notice(`Created ${config.label.toLowerCase()} note: ${filename}`);
} }
const target = leaf || this.app.workspace.getLeaf(false); const target = leaf || this.app.workspace.getLeaf(false);
await target.openFile(file); 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( private async createPeriodNote(
fullPath: string, fullPath: string,
periodSettings: PeriodNoteSettings, periodSettings: PeriodNoteSettings,
date: moment.Moment, date: moment.Moment,
label: string, label: string,
): Promise<TFile | null> { ): Promise<TFile | null> {
const noun = label.toLowerCase();
const slash = fullPath.lastIndexOf('/');
const folder = slash < 0 ? '' : fullPath.slice(0, slash);
try { try {
const templateFile = this.resolveTemplateFile(periodSettings.templateFile); await this.ensureFolderExists(folder);
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);
} catch (err: unknown) { } catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err); new Notice(
new Notice(`Failed to create ${label.toLowerCase()} note: ${message}`); `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; 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<void> {
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. */ /** 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); const detected = this.detectPeriodType(file.basename);
if (!detected) { 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; return;
} }
const { period, date } = detected; const { period, date } = detected;
if (!date.isValid()) {
new Notice(`Could not parse date from filename: ${file.basename}`);
return;
}
const amount = direction === 'next' ? 1 : -1; const amount = direction === 'next' ? 1 : -1;
// Map period to moment duration unit // Map period to moment duration unit
@@ -649,3 +726,10 @@ function basenameFromPath(path: string): string {
const name = path.slice(path.lastIndexOf('/') + 1); const name = path.slice(path.lastIndexOf('/') + 1);
return name.replace(/\.[^/.]+$/, ''); 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);
}