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.
This commit is contained in:
+1
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"id": "waypoint-sidebar",
|
"id": "waypoint-sidebar",
|
||||||
"name": "Waypoint Sidebar",
|
"name": "Waypoint Sidebar",
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"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
|
||||||
|
|||||||
+34
-3
@@ -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
@@ -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
|
||||||
// ═══════════════════════════════
|
// ═══════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user