3 Commits

Author SHA1 Message Date
olivier 695141383f release v1.7.1: native template autocomplete, durable indicator migration 2026-09-08 10:49:01 -04:00
olivier a0a3b50d94 docs: add QA coverage for indicator migration and native autocomplete 2026-09-08 10:33:10 -04:00
olivier 621bef0d29 fix: native template autocomplete and durable indicator migration
Template file fields used an unstyled <datalist>, not Obsidian's native suggest popup. Replace it with TemplateFileSuggest, an AbstractInputSuggest<TFile> matching Templater's own FileSuggest/FolderSuggest pattern exactly. Requires Obsidian >=1.4.10; minAppVersion corrected.

The coloured-indicator default never reached existing installs: every install that loaded before that default changed had already persisted the prior default (indicatorMode: 'any') to data.json as if it were a deliberate choice, and Object.assign-based settings merging always prefers a value already on disk over a new code default. Hand-editing data.json directly does not fix this durably either -- Obsidian holds settings in memory while a vault is open and overwrites external edits on its own save cycle.

Add a one-time, code-level migration: on load, an install with a persisted indicatorMode of 'any' and no indicatorModeMigrated flag is promoted to 'systems' and the flag is persisted immediately, so it survives even a session with no further settings changes. Any indicatorMode chosen after the flag exists, any (deliberately) included, is never touched again.
2026-09-08 10:32:54 -04:00
7 changed files with 99 additions and 63 deletions
+1 -1
View File
@@ -178,7 +178,7 @@ Configured systems live in `settings.dateSystems`, where array order is menu ord
| `journal` | Journal | `periodic/journal` | `YYYY-MM-DD - [Journal]` | green (`#22c55e`) | one | | `journal` | Journal | `periodic/journal` | `YYYY-MM-DD - [Journal]` | green (`#22c55e`) | one |
| `meetings` | Meeting | `periodic/meetings` | `YYYY-MM-DD - {title}` | violet (`#a855f7`) | many | | `meetings` | Meeting | `periodic/meetings` | `YYYY-MM-DD - {title}` | violet (`#a855f7`) | many |
The default calendar mode is `systems`, while `any` preserves the older one-neutral-dot behaviour. Template file fields in both Periodic Notes and Date systems use a native `datalist` populated from every Markdown path in the vault (without the optional `.md` extension), so templates stored outside a conventional `Templates/` folder remain discoverable. The default calendar mode is `systems`, while `any` preserves the older one-neutral-dot behaviour. Existing installs that silently persisted the pre-`systems` default of `any` are promoted to `systems` exactly once, on load, via a `data.json`-level `indicatorModeMigrated` flag; a value chosen after that flag is set (including `any`) is never touched again. Template file fields in both Periodic Notes and Date systems use `TemplateFileSuggest`, an `AbstractInputSuggest<TFile>` (the same base class Templater's own template pickers use — Obsidian's native fuzzy-suggest popup, not an unstyled `<datalist>`), searching every Markdown path in the vault. `AbstractInputSuggest` requires Obsidian ≥1.4.10, hence `manifest.json`'s `minAppVersion`.
### Templater folder triggers ### Templater folder triggers
+3 -2
View File
@@ -80,8 +80,9 @@
- [ ] Change Daily/Journal/Meeting indicator colours in Settings → the visible calendar refreshes immediately - [ ] Change Daily/Journal/Meeting indicator colours in Settings → the visible calendar refreshes immediately
- [ ] Add a custom date system with its own colour and a matching note → its dot appears in system order - [ ] Add a custom date system with its own colour and a matching note → its dot appears in system order
- [ ] Create, delete, or rename a date-system note → coloured dots refresh without changing months - [ ] Create, delete, or rename a date-system note → coloured dots refresh without changing months
- [ ] Start typing in any Template file field → matching vault Markdown paths appear as native autocomplete suggestions - [ ] An install that previously had **Single dot** as its silently-defaulted, never-touched setting shows **Colour by note system** after upgrading and reloading the plugin
- [ ] After that one-time change, manually selecting **Single dot** and reloading again keeps **Single dot** — the migration never re-fires
- [ ] Typing in any Template file field opens Obsidian's native fuzzy-suggest popup (matching the look of Templater's own template pickers), not a plain browser dropdown
## Calendar: Date systems ## Calendar: Date systems
+19 -19
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"id": "waypoint-sidebar", "id": "waypoint-sidebar",
"name": "Waypoint Sidebar", "name": "Waypoint Sidebar",
"version": "1.7.0", "version": "1.7.1",
"minAppVersion": "1.4.4", "minAppVersion": "1.4.10",
"description": "Calendar, recent files, and custom bookmarks sidebar.", "description": "Calendar, recent files, and custom bookmarks sidebar.",
"author": "Olivier", "author": "Olivier",
"isDesktopOnly": false "isDesktopOnly": false
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "waypoint", "name": "waypoint",
"version": "1.7.0", "version": "1.7.1",
"description": "Calendar, recent files, and custom bookmarks sidebar.", "description": "Calendar, recent files, and custom bookmarks sidebar.",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
+34 -3
View File
@@ -61,14 +61,26 @@ export default class WaypointPlugin extends Plugin {
/** Results for the displayed month; invalidated by markdown-file changes. */ /** Results for the displayed month; invalidated by markdown-file changes. */
private dateSystemIndicators: Map<string, DateSystemIndicator[]> | null = null; private dateSystemIndicators: Map<string, DateSystemIndicator[]> | null = null;
private dateSystemIndicatorKey = ''; private dateSystemIndicatorKey = '';
/**
* True once `data.json` carries the post-migration schema. Set on every
* load and included in every save, so a single migration run is durable
* even if this session never triggers another settings write.
*/
private indicatorModeMigrated = false;
async onload(): Promise<void> { async onload(): Promise<void> {
console.debug('Waypoint: loading plugin v' + this.manifest.version); console.debug('Waypoint: loading plugin v' + this.manifest.version);
// Load persisted data — data.json is read exactly once here. // Load persisted data — data.json is read exactly once here.
const saved = await this.loadData() as Record<string, unknown> | null; const saved = await this.loadData() as Record<string, unknown> | null;
this.applySettings(saved); const migrated = this.applySettings(saved);
this.applyWaypointData(saved); this.applyWaypointData(saved);
if (migrated) {
// Persist immediately: an install that only ever reads data.json
// (never changes a setting or bookmark this session) must still
// keep the migrated value instead of re-migrating every load.
void this.persistAll();
}
// Register the sidebar view // Register the sidebar view
this.registerView( this.registerView(
@@ -245,9 +257,11 @@ export default class WaypointPlugin extends Plugin {
/** /**
* Merge persisted settings over the defaults. Nested objects are merged * Merge persisted settings over the defaults. Nested objects are merged
* individually so existing configs keep their values while picking up * individually so existing configs keep their values while picking up
* fields added in newer versions. * fields added in newer versions. Returns true the one time a one-off
* migration (see `indicatorModeMigrated` below) actually changes a value,
* so the caller can persist that change immediately.
*/ */
private applySettings(saved: Record<string, unknown> | null): void { private applySettings(saved: Record<string, unknown> | null): boolean {
const s = (saved?.settings || {}) as Partial<WaypointSettings>; const s = (saved?.settings || {}) as Partial<WaypointSettings>;
this.settings = Object.assign({}, DEFAULT_SETTINGS, s); this.settings = Object.assign({}, DEFAULT_SETTINGS, s);
this.settings.recentFiles = Object.assign({}, DEFAULT_SETTINGS.recentFiles, s.recentFiles || {}); this.settings.recentFiles = Object.assign({}, DEFAULT_SETTINGS.recentFiles, s.recentFiles || {});
@@ -267,6 +281,22 @@ export default class WaypointPlugin extends Plugin {
return Object.assign({}, builtIn || DEFAULT_DATE_SYSTEM, sys); return Object.assign({}, builtIn || DEFAULT_DATE_SYSTEM, sys);
}) })
: DEFAULT_SETTINGS.dateSystems.map(sys => Object.assign({}, sys)); : DEFAULT_SETTINGS.dateSystems.map(sys => Object.assign({}, sys));
// `indicatorMode` shipped with a default of 'any' before per-system
// colours existed, so every install that loaded before that default
// changed silently persisted 'any' to disk as if it were a deliberate
// choice — changing DEFAULT_SETTINGS alone can never reach an install
// that already has an explicit value on disk. Promote that one-time
// default to the current default exactly once; any choice made after
// this flag is set is real and must never be touched again.
const alreadyMigrated = saved?.indicatorModeMigrated === true;
let didMigrateValue = false;
if (!alreadyMigrated && s.calendar?.indicatorMode === 'any') {
this.settings.calendar.indicatorMode = 'systems';
didMigrateValue = true;
}
this.indicatorModeMigrated = true;
return didMigrateValue;
} }
private applyWaypointData(saved: Record<string, unknown> | null): void { private applyWaypointData(saved: Record<string, unknown> | null): void {
@@ -301,6 +331,7 @@ export default class WaypointPlugin extends Plugin {
return this.saveData({ return this.saveData({
settings: this.settings, settings: this.settings,
waypointData: this.waypointData, waypointData: this.waypointData,
indicatorModeMigrated: this.indicatorModeMigrated,
}); });
}); });
// Keep the queue usable after a failed write without leaving an // Keep the queue usable after a failed write without leaving an
+39 -35
View File
@@ -1,8 +1,36 @@
import { Setting, PluginSettingTab, App, setIcon } from 'obsidian'; import { Setting, PluginSettingTab, App, setIcon, AbstractInputSuggest, TFile } from 'obsidian';
import type WaypointPlugin from 'src/main'; import type WaypointPlugin from 'src/main';
import { WaypointSettings, DEFAULT_SETTINGS, DEFAULT_DATE_SYSTEM, PeriodNoteSettings } from 'src/settings'; import { WaypointSettings, DEFAULT_SETTINGS, DEFAULT_DATE_SYSTEM, PeriodNoteSettings } from 'src/settings';
import { formatHasDateToken } from 'src/utils/date-systems'; import { formatHasDateToken } from 'src/utils/date-systems';
/**
* Vault-wide Markdown-file suggester for template-path fields, matching
* Obsidian's native suggest popup (same base class Templater's own template
* pickers use) rather than the browser's unstyled `<datalist>` dropdown.
*/
class TemplateFileSuggest extends AbstractInputSuggest<TFile> {
constructor(app: App, private inputEl: HTMLInputElement) {
super(app, inputEl);
}
getSuggestions(query: string): TFile[] {
const q = query.toLowerCase();
return this.app.vault.getMarkdownFiles()
.filter(file => file.path.toLowerCase().includes(q))
.slice(0, 200);
}
renderSuggestion(file: TFile, el: HTMLElement): void {
el.setText(file.path.replace(/\.md$/, ''));
}
selectSuggestion(file: TFile): void {
this.setValue(file.path.replace(/\.md$/, ''));
this.inputEl.trigger('input');
this.close();
}
}
export class WaypointSettingTab extends PluginSettingTab { export class WaypointSettingTab extends PluginSettingTab {
private plugin: WaypointPlugin; private plugin: WaypointPlugin;
private settings: WaypointSettings; private settings: WaypointSettings;
@@ -129,20 +157,14 @@ export class WaypointSettingTab extends PluginSettingTab {
// ═══════════════════════════════ // ═══════════════════════════════
private renderPeriodicTab(container: HTMLElement): void { private renderPeriodicTab(container: HTMLElement): void {
const templateSuggestionsId = this.createTemplateSuggestions(container); this.addPeriodNoteSettings(container, 'Daily', this.settings.daily);
this.addPeriodNoteSettings(container, 'Daily', this.settings.daily, templateSuggestionsId); this.addPeriodNoteSettings(container, 'Weekly', this.settings.weekly);
this.addPeriodNoteSettings(container, 'Weekly', this.settings.weekly, templateSuggestionsId); this.addPeriodNoteSettings(container, 'Monthly', this.settings.monthly);
this.addPeriodNoteSettings(container, 'Monthly', this.settings.monthly, templateSuggestionsId); this.addPeriodNoteSettings(container, 'Quarterly', this.settings.quarterly);
this.addPeriodNoteSettings(container, 'Quarterly', this.settings.quarterly, templateSuggestionsId); this.addPeriodNoteSettings(container, 'Yearly', this.settings.yearly);
this.addPeriodNoteSettings(container, 'Yearly', this.settings.yearly, templateSuggestionsId);
} }
private addPeriodNoteSettings( private addPeriodNoteSettings(container: HTMLElement, label: string, period: PeriodNoteSettings): void {
container: HTMLElement,
label: string,
period: PeriodNoteSettings,
templateSuggestionsId: string,
): void {
new Setting(container).setHeading().setName(label); new Setting(container).setHeading().setName(label);
new Setting(container) new Setting(container)
@@ -175,7 +197,7 @@ export class WaypointSettingTab extends PluginSettingTab {
.addText((text) => { .addText((text) => {
text.setPlaceholder('Templates/Daily note'); text.setPlaceholder('Templates/Daily note');
text.setValue(period.templateFile); text.setValue(period.templateFile);
text.inputEl.setAttr('list', templateSuggestionsId); new TemplateFileSuggest(this.app, text.inputEl);
text.onChange((value) => { text.onChange((value) => {
period.templateFile = value; period.templateFile = value;
this.saveAndRefresh(); this.saveAndRefresh();
@@ -211,8 +233,6 @@ export class WaypointSettingTab extends PluginSettingTab {
.setName('Date systems') .setName('Date systems')
.setDesc(intro); .setDesc(intro);
const templateSuggestionsId = this.createTemplateSuggestions(container);
this.settings.dateSystems.forEach((system, i, arr) => { this.settings.dateSystems.forEach((system, i, arr) => {
new Setting(container) new Setting(container)
.setHeading() .setHeading()
@@ -279,7 +299,7 @@ export class WaypointSettingTab extends PluginSettingTab {
this.addSystemTextSetting(container, this.addSystemTextSetting(container,
'Template file', 'Path to the template file. The .md extension is optional.', 'Template file', 'Path to the template file. The .md extension is optional.',
system, 'templateFile', 'resources/template/journal', templateSuggestionsId, system, 'templateFile', 'resources/template/journal', true,
); );
this.addSystemTextSetting(container, this.addSystemTextSetting(container,
'Type property', `Fallback value for the 'type' frontmatter property, used when no template is found.`, 'Type property', `Fallback value for the 'type' frontmatter property, used when no template is found.`,
@@ -327,7 +347,7 @@ export class WaypointSettingTab extends PluginSettingTab {
obj: Record<K, string>, obj: Record<K, string>,
key: K, key: K,
placeholder: string, placeholder: string,
templateSuggestionsId?: string, suggestTemplates?: boolean,
): Setting { ): Setting {
return new Setting(container) return new Setting(container)
.setName(name) .setName(name)
@@ -335,7 +355,7 @@ export class WaypointSettingTab extends PluginSettingTab {
.addText((text) => { .addText((text) => {
text.setPlaceholder(placeholder); text.setPlaceholder(placeholder);
text.setValue(obj[key]); text.setValue(obj[key]);
if (templateSuggestionsId) text.inputEl.setAttr('list', templateSuggestionsId); if (suggestTemplates) new TemplateFileSuggest(this.app, text.inputEl);
text.onChange((value) => { text.onChange((value) => {
obj[key] = value; obj[key] = value;
this.saveAndRefresh(); this.saveAndRefresh();
@@ -343,22 +363,6 @@ export class WaypointSettingTab extends PluginSettingTab {
}); });
} }
/**
* Native datalist keeps template paths searchable without another custom
* modal, and accepts templates stored anywhere in the vault.
*/
private createTemplateSuggestions(container: HTMLElement): string {
const id = 'waypoint-template-suggestions';
const list = container.createEl('datalist', { attr: { id } });
const paths = this.app.vault.getMarkdownFiles()
.map(file => file.path.replace(/\.md$/, ''))
.sort((a, b) => a.localeCompare(b));
for (const path of paths) {
list.createEl('option', { value: path });
}
return id;
}
// ═══════════════════════════════ // ═══════════════════════════════
// Recent Files tab // Recent Files tab
// ═══════════════════════════════ // ═══════════════════════════════