diff --git a/manifest.json b/manifest.json index 30fd851..6701c51 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "id": "metaedit", "name": "MetaEdit", "version": "1.10.3", - "minAppVersion": "1.12.7", + "minAppVersion": "1.13.4", "description": "MetaEdit helps you manage your metadata.", "author": "Christian B. B. Houmann", "authorUrl": "https://bagerbach.com", diff --git a/src/Modals/IgnoredPropertiesSettingModal/IgnoredPropertiesModal.ts b/src/Modals/IgnoredPropertiesSettingModal/IgnoredPropertiesModal.ts deleted file mode 100644 index 3a3da74..0000000 --- a/src/Modals/IgnoredPropertiesSettingModal/IgnoredPropertiesModal.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {type App, Modal} from "obsidian"; -import type MetaEdit from "../../main"; -import SingleValueTableEditorContent from "../shared/SingleValueTableEditorContent.svelte"; -import {type MountedSvelteComponent, mountSvelteComponent, unmountSvelteComponent} from "../../svelteMount"; - -export default class IgnoredPropertiesModal extends Modal{ - public waitForResolve: Promise; - private plugin: MetaEdit; - private content: MountedSvelteComponent; - private resolvePromise: (ignoredProperties: string[]) => void; - private ignoredProperties: string[]; - - constructor(app: App, plugin: MetaEdit, ignoredProperties: string[]) { - super(app); - this.plugin = plugin; - this.ignoredProperties = ignoredProperties; - - this.waitForResolve = new Promise( - (resolve) => (this.resolvePromise = resolve) - ); - - this.content = mountSvelteComponent( - SingleValueTableEditorContent, - this.contentEl, - { - properties: ignoredProperties, - save: (ignoredProperties: string[]) => { - this.ignoredProperties = ignoredProperties; - this.close(); - } - }, - ); - - this.open(); - } - - onClose() { - super.onClose(); - unmountSvelteComponent(this.content); - this.resolvePromise(this.ignoredProperties); - } -} diff --git a/src/Modals/KanbanHelperSetting/KanbanHelperSettingContent.svelte b/src/Modals/KanbanHelperSetting/KanbanHelperSettingContent.svelte deleted file mode 100644 index 549ef14..0000000 --- a/src/Modals/KanbanHelperSetting/KanbanHelperSettingContent.svelte +++ /dev/null @@ -1,126 +0,0 @@ - - -
- - - - - - - - - - - {#each kanbanProperties as kanbanProperty, i (kanbanProperty.boardName)} - - - - - - - {/each} - -
BoardProperty in linkPossible values
- removeProperty(i)}/> - - {kanbanProperty.boardName} - - - - {getHeadingsInBoard(kanbanProperty.boardName)} -
- - -
- -
-
- - diff --git a/src/Modals/KanbanHelperSetting/KanbanHelperSettingSuggester.ts b/src/Modals/KanbanHelperSetting/KanbanHelperSettingSuggester.ts deleted file mode 100644 index 43bb688..0000000 --- a/src/Modals/KanbanHelperSetting/KanbanHelperSettingSuggester.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {AbstractInputSuggest, type App} from "obsidian"; -import type {TFile} from "obsidian"; - -export class KanbanHelperSettingSuggester extends AbstractInputSuggest { - - constructor(app: App, private readonly inputEl: HTMLInputElement, private readonly boards: TFile[]) { - super(app, inputEl); - this.onSelect((item) => this.acceptSuggestion(item)); - } - - protected getSuggestions(inputStr: string): TFile[] { - const inputLowerCase: string = inputStr.toLowerCase(); - return this.boards.filter(board => board.basename.toLowerCase().includes(inputLowerCase)); - } - - private acceptSuggestion(item: TFile): void { - this.setValue(item.basename); - this.inputEl.trigger("input"); - this.close(); - } - - renderSuggestion(value: TFile, el: HTMLElement): void { - if (value) - el.setText(value.basename); - } -} diff --git a/src/Modals/ProgressPropertiesSettingModal/ProgressPropertiesModal.ts b/src/Modals/ProgressPropertiesSettingModal/ProgressPropertiesModal.ts deleted file mode 100644 index 0472cb1..0000000 --- a/src/Modals/ProgressPropertiesSettingModal/ProgressPropertiesModal.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {type App, Modal} from "obsidian"; -import type MetaEdit from "../../main"; -import ProgressPropertiesModalContent from "./ProgressPropertiesModalContent.svelte"; -import type {ProgressProperty} from "../../Types/progressProperty"; -import {type MountedSvelteComponent, mountSvelteComponent, unmountSvelteComponent} from "../../svelteMount"; - -export default class ProgressPropertiesModal extends Modal { - public waitForResolve: Promise; - private plugin: MetaEdit; - private content: MountedSvelteComponent; - private resolvePromise: (properties: ProgressProperty[]) => void; - private properties: ProgressProperty[]; - - constructor(app: App, plugin: MetaEdit, properties: ProgressProperty[]) { - super(app); - this.plugin = plugin; - if (properties.length > 0) - this.properties = properties; - else - this.properties = []; - - this.waitForResolve = new Promise( - (resolve) => (this.resolvePromise = resolve) - ); - - this.content = mountSvelteComponent( - ProgressPropertiesModalContent, - this.contentEl, - { - properties: this.properties, - save: (properties: ProgressProperty[]) => { - this.properties = properties; - this.close(); - } - }, - ); - - this.open(); - } - - onClose() { - super.onClose(); - unmountSvelteComponent(this.content); - this.resolvePromise(this.properties); - } -} diff --git a/src/Modals/ProgressPropertiesSettingModal/ProgressPropertiesModalContent.svelte b/src/Modals/ProgressPropertiesSettingModal/ProgressPropertiesModalContent.svelte deleted file mode 100644 index 33c0d00..0000000 --- a/src/Modals/ProgressPropertiesSettingModal/ProgressPropertiesModalContent.svelte +++ /dev/null @@ -1,91 +0,0 @@ - - -
- - - - - - - - - {#each properties as property (property)} - - - - - - {/each} - -
NameType
- - - - - removeProperty(property)} value="❌"/> -
- -
- -
-
- - diff --git a/src/Modals/shared/SingleValueTableEditorContent.svelte b/src/Modals/shared/SingleValueTableEditorContent.svelte deleted file mode 100644 index 90eb91d..0000000 --- a/src/Modals/shared/SingleValueTableEditorContent.svelte +++ /dev/null @@ -1,69 +0,0 @@ - - -
- - - - - - - - - {#each propertyIndexes as i (i)} - - - - - {/each} - -
Property
- removeProperty(i)}/> - - -
- -
- -
-
- - diff --git a/src/Settings/metaEditSettingsTab.ts b/src/Settings/metaEditSettingsTab.ts index d7c507f..fdb4749 100644 --- a/src/Settings/metaEditSettingsTab.ts +++ b/src/Settings/metaEditSettingsTab.ts @@ -1,116 +1,396 @@ -import {type App, Notice, PluginSettingTab, Setting} from "obsidian"; +import { + type App, + Notice, + PluginSettingTab, + type SettingDefinitionItem, + type SettingDefinitionList, + SettingPage, + type TFile, +} from "obsidian"; import type MetaEdit from "../main"; import {EditMode} from "../Types/editMode"; -import ProgressPropertiesModalContent - from "../Modals/ProgressPropertiesSettingModal/ProgressPropertiesModalContent.svelte"; +import {ProgressPropertyOptions} from "../Types/progressPropertyOptions"; +import type {MetaEditSettings} from "./metaEditSettings"; import AutoPropertiesModalContent from "../Modals/AutoPropertiesSettingModal/AutoPropertiesModalContent.svelte"; -import KanbanHelperSettingContent from "../Modals/KanbanHelperSetting/KanbanHelperSettingContent.svelte"; -import SingleValueTableEditorContent from "../Modals/shared/SingleValueTableEditorContent.svelte"; -import type {ProgressProperty} from "../Types/progressProperty"; -import type {KanbanProperty} from "../Types/kanbanProperty"; +import GenericPrompt from "../Modals/GenericPrompt/GenericPrompt"; +import GenericSuggester from "../Modals/GenericSuggester/GenericSuggester"; import {type MountedSvelteComponent, mountSvelteComponent, unmountSvelteComponent} from "../svelteMount"; import {applyAutoPropertySettingsOperation, type AutoPropertySettingsOperation} from "../autoProperties"; -function toggleHiddenEl(el: HTMLElement | undefined, hidden: boolean) { - if (!el) return hidden; +type Control = { + get: (settings: MetaEditSettings) => unknown; + set: (settings: MetaEditSettings, value: unknown) => void; + // Runtime state that has to follow the stored value. + apply?: (plugin: MetaEdit) => void; +}; + +const controls = { + "UIElements.enabled": { + get: s => s.UIElements.enabled, + set: (s, v) => s.UIElements.enabled = v === true, + apply: plugin => plugin.settings.UIElements.enabled + ? plugin.linkMenu.registerEvent() + : plugin.linkMenu.unregisterEvent(), + }, + "EditMode.mode": { + get: s => s.EditMode.mode, + set: (s, v) => s.EditMode.mode = v as EditMode, + }, + "IgnoredProperties.enabled": { + get: s => s.IgnoredProperties.enabled, + set: (s, v) => s.IgnoredProperties.enabled = v === true, + }, + "IgnoredProperties.hideFileTags": { + get: s => s.IgnoredProperties.hideFileTags, + set: (s, v) => s.IgnoredProperties.hideFileTags = v === true, + }, + "AutoProperties.enabled": { + get: s => s.AutoProperties.enabled, + set: (s, v) => s.AutoProperties.enabled = v === true, + }, + "ProgressProperties.enabled": { + get: s => s.ProgressProperties.enabled, + set: (s, v) => s.ProgressProperties.enabled = v === true, + apply: plugin => plugin.toggleAutomators(), + }, + "KanbanHelper.enabled": { + get: s => s.KanbanHelper.enabled, + set: (s, v) => s.KanbanHelper.enabled = v === true, + apply: plugin => plugin.toggleAutomators(), + }, +} satisfies Record; + +type ControlKey = keyof typeof controls; + +function isControlKey(key: string): key is ControlKey { + return Object.prototype.hasOwnProperty.call(controls, key); +} + +const progressTypeLabels: Record = { + [ProgressPropertyOptions.TaskTotal]: "Total tasks", + [ProgressPropertyOptions.TaskComplete]: "Completed tasks", + [ProgressPropertyOptions.TaskIncomplete]: "Incomplete tasks", +}; + +/** + * Rows are deleted by the entry they rendered, not by index: two quick deletes + * can both be queued before the first one saves and shifts the indexes. + */ +function removeEntry(entries: T[], entry: T | undefined): void { + const index = entry === undefined ? -1 : entries.indexOf(entry); + if (index !== -1) entries.splice(index, 1); +} + +/** A settings sub-page whose whole body is one Svelte editor. */ +class SvelteSettingPage extends SettingPage { + private mounted: MountedSvelteComponent | null = null; + + constructor(private readonly mountContent: (target: HTMLElement) => MountedSvelteComponent) { + super(); + } - el.classList.toggle("metaedit-hidden", !hidden); - return !hidden; + display(): void { + unmountSvelteComponent(this.mounted); + this.containerEl.empty(); + this.mounted = this.mountContent(this.containerEl); + } + + hide(): void { + unmountSvelteComponent(this.mounted); + this.mounted = null; + super.hide(); + } } export class MetaEditSettingsTab extends PluginSettingTab { plugin: MetaEdit; - private svelteElements: MountedSvelteComponent[] = []; + private signature: string; constructor(app: App, plugin: MetaEdit) { super(app, plugin); this.plugin = plugin; + this.signature = this.structureSignature(); } - display(): void { - const {containerEl} = this; + /** + * Obsidian renders from a snapshot of getSettingDefinitions() until update() + * rebuilds it. Rebuild after any settings write - from this tab, the API, or a + * prompt - that adds, removes, or retypes a list entry, but not on toggles or + * while typing, so a rebuild never steals focus. + */ + syncWithSettings(): void { + const next = this.structureSignature(); + if (next === this.signature) return; + this.signature = next; + this.update(); + } - this.destroySvelteElements(); - containerEl.empty(); + private structureSignature(): string { + const s = this.plugin.settings; + return JSON.stringify([ + s.EditMode.properties, + s.IgnoredProperties.properties, + s.ProgressProperties.properties, + s.AutoProperties.properties.length, + s.KanbanHelper.boards.map(board => board.boardName), + ]); + } - this.addProgressPropertiesSetting(containerEl); - this.addAutoPropertiesSetting(containerEl); - this.addEditMetaMenuSetting(containerEl); - this.addEditModeSetting(containerEl); - this.addKanbanHelperSetting(containerEl); - this.addUIElementsSetting(containerEl); + override getControlValue(key: string): unknown { + return isControlKey(key) ? controls[key].get(this.plugin.settings) : undefined; } - private addProgressPropertiesSetting(containerEl: HTMLElement) { - let hidden: boolean = true; - const setting = new Setting(containerEl) - .setName("Progress Properties") - .setDesc("Update properties automatically.") - .addToggle(toggle => { - toggle - .setTooltip("Toggle Progress Properties") - .setValue(this.plugin.settings.ProgressProperties.enabled) - .onChange(async value => { - if (value === this.plugin.settings.ProgressProperties.enabled) return; - - this.plugin.settings.ProgressProperties.enabled = value; - this.plugin.toggleAutomators(); - - await this.plugin.saveSettings(); - }); - }); + override async setControlValue(key: string, value: unknown): Promise { + if (!isControlKey(key)) return; + const control: Control = controls[key]; + + const saved = await this.persist(settings => { + const previous = control.get(settings); + control.set(settings, value); + return () => control.set(settings, previous); + }); + if (saved) control.apply?.(this.plugin); + this.refreshDomState(); + } - const div = setting.settingEl.createDiv({cls: "metaedit-hidden"}); - setting.settingEl.classList.add("metaedit-setting-with-details"); - setting.addExtraButton(button => button.onClick(() => hidden = toggleHiddenEl(div, hidden))); + override getSettingDefinitions(): SettingDefinitionItem[] { + const settings = this.plugin.settings; + const count = (items: () => unknown[]) => () => String(items().length); - const modal = mountSvelteComponent( - ProgressPropertiesModalContent, - div, + return [ { - properties: this.plugin.settings.ProgressProperties.properties, - save: async (progressProperties: ProgressProperty[]) => { - this.plugin.settings.ProgressProperties.properties = progressProperties; - await this.plugin.saveSettings(); - } + type: "group", + heading: "Editing", + items: [ + { + name: "Edit mode", + desc: "Whether property values are saved as one value or as a list.", + control: { + type: "dropdown", + key: "EditMode.mode", + options: { + [EditMode.AllSingle]: "One value", + [EditMode.AllMulti]: "List", + [EditMode.SomeMulti]: "List for chosen properties", + }, + }, + }, + { + type: "page", + name: "List properties", + visible: () => settings.EditMode.mode === EditMode.SomeMulti, + displayValue: count(() => settings.EditMode.properties), + items: [this.propertyNameList(s => s.EditMode.properties)], + }, + { + name: "Auto properties", + desc: "Pick from preset values when you edit these properties.", + control: {type: "toggle", key: "AutoProperties.enabled"}, + }, + { + type: "page", + name: "Preset values", + visible: () => settings.AutoProperties.enabled, + displayValue: count(() => settings.AutoProperties.properties), + page: () => new SvelteSettingPage(target => mountSvelteComponent( + AutoPropertiesModalContent, + target, + { + autoProperties: settings.AutoProperties.properties, + save: (operation: AutoPropertySettingsOperation) => this.saveAutoPropertiesOperation(operation), + }, + )), + }, + ], }, - ); - - this.svelteElements.push(modal); + { + type: "group", + heading: "Menu", + items: [ + { + name: "Right-click menu", + desc: "Show Edit Meta when you right-click a note or link.", + control: {type: "toggle", key: "UIElements.enabled"}, + }, + { + name: "Hide properties", + desc: "Leave chosen properties out of the property menu.", + control: {type: "toggle", key: "IgnoredProperties.enabled"}, + }, + { + name: "Hide file tags", + desc: "A frontmatter tags property stays in the menu.", + visible: () => settings.IgnoredProperties.enabled, + control: {type: "toggle", key: "IgnoredProperties.hideFileTags"}, + }, + { + type: "page", + name: "Hidden properties", + visible: () => settings.IgnoredProperties.enabled, + displayValue: count(() => settings.IgnoredProperties.properties), + items: [this.propertyNameList(s => s.IgnoredProperties.properties)], + }, + ], + }, + { + type: "group", + heading: "Automation", + items: [ + { + name: "Progress properties", + desc: "Keep task counts in a note's properties up to date.", + control: {type: "toggle", key: "ProgressProperties.enabled"}, + }, + { + type: "page", + name: "Tracked task counts", + visible: () => settings.ProgressProperties.enabled, + displayValue: count(() => settings.ProgressProperties.properties), + items: [this.progressPropertyList()], + }, + { + name: "Kanban board helper", + desc: "When a card moves to another lane, update a property in the note it links to.", + control: {type: "toggle", key: "KanbanHelper.enabled"}, + }, + { + type: "page", + name: "Boards", + visible: () => settings.KanbanHelper.enabled, + displayValue: count(() => settings.KanbanHelper.boards), + items: [this.kanbanBoardList()], + }, + ], + }, + ]; } - private addAutoPropertiesSetting(containerEl: HTMLElement) { - let hidden: boolean = true; - const setting = new Setting(containerEl) - .setName("Auto Properties") - .setDesc("Quick switch for values you know the value of.") - .addToggle(toggle => { - toggle - .setTooltip("Toggle Auto Properties") - .setValue(this.plugin.settings.AutoProperties.enabled) - .onChange(async value => { - if (value === this.plugin.settings.AutoProperties.enabled) return; - - this.plugin.settings.AutoProperties.enabled = value; - - await this.plugin.saveSettings(); - }); - }); + private propertyNameList(list: (settings: MetaEditSettings) => string[]): SettingDefinitionList { + const names = [...list(this.plugin.settings)]; + + return { + type: "list", + emptyState: "No properties yet.", + addItem: { + name: "Add property", + action: async () => { + const name = (await GenericPrompt.Prompt(this.app, "Property name"))?.trim(); + if (!name || names.includes(name)) return; + await this.editList(list, entries => entries.push(name)); + }, + }, + onDelete: index => void this.editList(list, entries => removeEntry(entries, names[index])), + items: names.map(name => ({name})), + }; + } - const div = setting.settingEl.createDiv({cls: "metaedit-hidden"}); - setting.settingEl.classList.add("metaedit-setting-with-details"); - setting.addExtraButton(b => b.onClick(() => hidden = toggleHiddenEl(div, hidden))); + private progressPropertyList(): SettingDefinitionList { + const list = (settings: MetaEditSettings) => settings.ProgressProperties.properties; + const properties = [...list(this.plugin.settings)]; + + return { + type: "list", + emptyState: "No task counts yet.", + addItem: { + name: "Add property", + action: async () => { + const name = (await GenericPrompt.Prompt(this.app, "Property name"))?.trim(); + if (!name || properties.some(property => property.name === name)) return; + await this.editList(list, entries => entries.push({name, type: ProgressPropertyOptions.TaskTotal})); + }, + }, + onDelete: index => void this.editList(list, entries => removeEntry(entries, properties[index])), + items: properties.map(property => ({ + name: property.name, + render: setting => { + setting.addDropdown(dropdown => dropdown + .addOptions(progressTypeLabels) + .setValue(property.type) + .onChange(value => void this.persist(() => { + const previous = property.type; + property.type = value as ProgressPropertyOptions; + return () => property.type = previous; + }))); + }, + })), + }; + } - const modal = mountSvelteComponent( - AutoPropertiesModalContent, - div, - { - autoProperties: this.plugin.settings.AutoProperties.properties, - save: (operation: AutoPropertySettingsOperation) => this.saveAutoPropertiesOperation(operation), + private kanbanBoardList(): SettingDefinitionList { + const list = (settings: MetaEditSettings) => settings.KanbanHelper.boards; + const boards = [...list(this.plugin.settings)]; + + return { + type: "list", + emptyState: "No boards yet.", + addItem: { + name: "Add board", + action: async () => { + const candidates = this.plugin.getFilesWithProperty("kanban-plugin") + .map(file => file.basename) + .filter(name => !boards.some(board => board.boardName === name)); + if (candidates.length === 0) { + new Notice("MetaEdit found no other Kanban boards in this vault."); + return; + } + const boardName = await GenericSuggester.Suggest(this.app, candidates, candidates); + if (!boardName) return; + await this.editList(list, entries => entries.push({boardName, property: ""})); + }, }, - ); + onDelete: index => void this.editList(list, entries => removeEntry(entries, boards[index])), + items: boards.map(board => ({ + name: board.boardName, + render: setting => { + // Read at render time: the metadata cache may not be ready when + // the definitions are built at startup. + setting.setDesc(this.kanbanLanes(board.boardName)); + setting.addText(text => text + .setPlaceholder("Property in linked notes") + .setValue(board.property) + .onChange(value => void this.persist(() => { + const previous = board.property; + board.property = value.trim(); + return () => board.property = previous; + }))); + }, + })), + }; + } - this.svelteElements.push(modal); + private kanbanLanes(boardName: string): string { + const file: TFile | undefined = this.plugin.getFilesWithProperty("kanban-plugin") + .find(board => board.basename === boardName); + if (!file) return "Board not found."; + + const lanes = this.app.metadataCache.getFileCache(file)?.headings?.map(heading => heading.heading) ?? []; + return lanes.length > 0 ? `Lanes: ${lanes.join(", ")}` : "No lanes yet."; + } + + /** + * Save a settings change through the serialized writer. `mutate` returns how to + * undo it; a failed save undoes it and re-renders, so memory and the tab match + * disk. Returns whether the change reached disk. + */ + private async persist(mutate: (settings: MetaEditSettings) => () => void): Promise { + try { + await this.plugin.updateSettings(() => mutate(this.plugin.settings)); + return true; + } catch (error) { + const reason = error instanceof Error ? error.message : String(error); + new Notice(`MetaEdit could not save the setting: ${reason}`); + this.update(); + return false; + } + } + + private editList(list: (settings: MetaEditSettings) => T[], change: (entries: T[]) => void): Promise { + return this.persist(settings => { + const entries = list(settings); + const previous = [...entries]; + change(entries); + return () => entries.splice(0, entries.length, ...previous); + }); } private async saveAutoPropertiesOperation(operation: AutoPropertySettingsOperation) { @@ -135,205 +415,4 @@ export class MetaEditSettingsTab extends PluginSettingTab { new Notice(`MetaEdit could not save the Auto Properties setting: ${reason}`); } } - - private addEditMetaMenuSetting(containerEl: HTMLElement) { - let hidden = true; - const setting = new Setting(containerEl) - .setName("Edit Meta menu") - .setDesc("Control what the 'Edit Meta' menu lists. Enable it, then use the gear to hide specific properties by name or all of a note's file tags.") - .addToggle(toggle => { - toggle - .setTooltip("Toggle menu filtering") - .setValue(this.plugin.settings.IgnoredProperties.enabled) - .onChange(async value => { - if (value === this.plugin.settings.IgnoredProperties.enabled) return; - - this.plugin.settings.IgnoredProperties.enabled = value; - - await this.plugin.saveSettings(); - this.display(); - }); - }); - - const div = this.plugin.settings.IgnoredProperties.enabled - ? setting.settingEl.createDiv({cls: "metaedit-hidden"}) - : undefined; - if (div) setting.settingEl.classList.add("metaedit-setting-with-details"); - setting.addExtraButton(b => b.onClick(() => hidden = toggleHiddenEl(div, hidden))); - - if (div) { - new Setting(div) - .setName("Hide file tags") - .setDesc("Hide the note's #tags from the menu, leaving only frontmatter and inline fields. A frontmatter 'tags' property stays editable.") - .addToggle(toggle => { - toggle - .setTooltip("Toggle hiding file tags") - .setValue(this.plugin.settings.IgnoredProperties.hideFileTags) - .onChange(async value => { - if (value === this.plugin.settings.IgnoredProperties.hideFileTags) return; - - this.plugin.settings.IgnoredProperties.hideFileTags = value; - await this.plugin.saveSettings(); - }); - }); - - div.createEl("p", {text: "Hide specific properties by name:", cls: "metaedit-table-label"}); - - const modal = mountSvelteComponent( - SingleValueTableEditorContent, - div, - { - properties: this.plugin.settings.IgnoredProperties.properties, - save: async (ignoredProperties: string[]) => { - this.plugin.settings.IgnoredProperties.properties = ignoredProperties; - await this.plugin.saveSettings(); - } - }, - ); - - this.svelteElements.push(modal); - } - } - - private addEditModeSetting(containerEl: HTMLElement) { - let bDivToggle: boolean = true; - let extraButtonEl: HTMLElement | undefined; - - // For linebreaks - const df = new DocumentFragment(); - df.createEl('p', {text: "Single: property values are just one value. "}); - df.createEl('p', {text: "Multi: properties are arrays. "}) - df.createEl('p', {text: "Some Multi: all options are single, except those specified in the settings (click button)."}); - - const setting = new Setting(containerEl) - .setName("Edit Mode") - .setDesc(df); - const div = setting.settingEl.createDiv({cls: "metaedit-hidden"}); - setting.settingEl.classList.add("metaedit-setting-with-details"); - - setting - .addDropdown(dropdown => { - dropdown - .addOption(EditMode.AllSingle, EditMode.AllSingle) - .addOption(EditMode.AllMulti, EditMode.AllMulti) - .addOption(EditMode.SomeMulti, EditMode.SomeMulti) - .setValue(this.plugin.settings.EditMode.mode) - .onChange(async value => { - switch (value) { - case EditMode.AllMulti: - this.plugin.settings.EditMode.mode = EditMode.AllMulti; - toggleHiddenEl(extraButtonEl, false); - bDivToggle = toggleHiddenEl(div, false); - break; - case EditMode.AllSingle: - this.plugin.settings.EditMode.mode = EditMode.AllSingle; - toggleHiddenEl(extraButtonEl, false); - bDivToggle = toggleHiddenEl(div, false); - break; - case EditMode.SomeMulti: - this.plugin.settings.EditMode.mode = EditMode.SomeMulti; - toggleHiddenEl(extraButtonEl, true); - break; - } - - await this.plugin.saveSettings(); - }) - }) - .addExtraButton(b => { - extraButtonEl = b.extraSettingsEl; - b.setTooltip("Configure which properties are Multi.") - return b.onClick(() => bDivToggle = toggleHiddenEl(div, bDivToggle)); - }); - - if (this.plugin.settings.EditMode.mode != EditMode.SomeMulti) { - toggleHiddenEl(extraButtonEl, false); - } - - const modal = mountSvelteComponent( - SingleValueTableEditorContent, - div, - { - properties: this.plugin.settings.EditMode.properties, - save: async (properties: string[]) => { - this.plugin.settings.EditMode.properties = properties; - await this.plugin.saveSettings(); - } - }, - ); - - this.svelteElements.push(modal); - } - - hide(): any { - this.destroySvelteElements(); - return super.hide(); - } - - private destroySvelteElements() { - this.svelteElements.forEach(unmountSvelteComponent); - this.svelteElements = []; - } - - private addKanbanHelperSetting(containerEl: HTMLElement) { - let hidden: boolean = true; - const setting = new Setting(containerEl) - .setName("Kanban Board Helper") - .setDesc("Update properties in links in kanban boards automatically when a card is moved to a new lane.") - .addToggle(toggle => { - toggle - .setTooltip("Toggle Kanban Helper") - .setValue(this.plugin.settings.KanbanHelper.enabled) - .onChange(async value => { - if (value === this.plugin.settings.KanbanHelper.enabled) return; - - this.plugin.settings.KanbanHelper.enabled = value; - this.plugin.toggleAutomators(); - - await this.plugin.saveSettings(); - }); - }); - - const div = setting.settingEl.createDiv({cls: "metaedit-hidden"}); - setting.settingEl.classList.add("metaedit-setting-with-details"); - setting.addExtraButton(button => button.onClick(() => hidden = toggleHiddenEl(div, hidden))); - - const modal = mountSvelteComponent( - KanbanHelperSettingContent, - div, - { - kanbanProperties: this.plugin.settings.KanbanHelper.boards, - boards: this.plugin.getFilesWithProperty("kanban-plugin"), - app: this.app, - save: async (kanbanProperties: KanbanProperty[]) => { - this.plugin.settings.KanbanHelper.boards = kanbanProperties; - await this.plugin.saveSettings(); - } - }, - ); - - this.svelteElements.push(modal); - } - - private addUIElementsSetting(containerEl: HTMLElement) { - new Setting(containerEl) - .setName("UI Elements") - .setDesc("Toggle UI elements: the 'Edit Meta' right-click menu option.") - .addToggle(toggle => { - toggle - .setTooltip("Toggle UI elements") - .setValue(this.plugin.settings.UIElements.enabled) - .onChange(async value => { - if (value === this.plugin.settings.UIElements.enabled) return; - - this.plugin.settings.UIElements.enabled = value; - if (value) { - this.plugin.linkMenu.registerEvent(); - } else { - this.plugin.linkMenu.unregisterEvent(); - } - - await this.plugin.saveSettings(); - }); - }) - } } diff --git a/src/main.ts b/src/main.ts index 1325f9f..ed7fb19 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,7 +31,11 @@ export default class MetaEdit extends Plugin { // Property choice persistence - runs through this one serialization point, so two // concurrent writers can't lost-update a shared in-memory snapshot and two disk // flushes can't complete out of order. - private readonly settingsWriter = new SettingsWriter(() => this.saveData(this.settings)); + private readonly settingsWriter = new SettingsWriter(async () => { + await this.saveData(this.settings); + this.settingsTab?.syncWithSettings(); + }); + private settingsTab: MetaEditSettingsTab | undefined; async onload() { this.controller = new MetaController(this.app, this); @@ -61,7 +65,8 @@ export default class MetaEdit extends Plugin { } }); - this.addSettingTab(new MetaEditSettingsTab(this.app, this)); + this.settingsTab = new MetaEditSettingsTab(this.app, this); + this.addSettingTab(this.settingsTab); this.linkMenu = new LinkMenu(this); if (this.settings.UIElements.enabled) { diff --git a/styles.css b/styles.css index 2df0a0d..3f9bb33 100644 --- a/styles.css +++ b/styles.css @@ -1,31 +1,3 @@ -.centerSettingContent { - display: grid; - align-items: center; - justify-content: center; -} - -.not-a-button { - background: none; - color: inherit; - border: none; - padding: 0; - font: inherit; - cursor: pointer; - outline: inherit; -} - -.metaedit-hidden { - display: none; -} - -.metaedit-setting-with-details { - display: block; -} - -.metaedit-table-label { - margin-bottom: 0; -} - .metaedit-suggester-command { font-weight: var(--font-semibold); } diff --git a/tests/e2e/audit-modals.test.ts b/tests/e2e/audit-modals.test.ts index c2e4940..be22d72 100644 --- a/tests/e2e/audit-modals.test.ts +++ b/tests/e2e/audit-modals.test.ts @@ -25,6 +25,11 @@ const HELPERS = ` }; const itemByName = (root, name) => Array.from(root.querySelectorAll(".setting-item")) .find((el) => el.querySelector(".setting-item-name")?.textContent.trim() === name); + const openPage = async (root, name) => { + itemByName(root, name).click(); + const title = await waitFor(".setting-page-title", (el) => el.textContent.trim() === name); + return title.closest(".setting-page").querySelector(".setting-page-content"); + }; ${CLOSE_ALL_MODALS_JS} `; @@ -113,38 +118,40 @@ describe("MetaEdit modal + kanban flows", () => { ${HELPERS} await closeAllModals(); const plugin = app.plugins.plugins.${PLUGIN_ID}; - const root = await openTab(); - const item = itemByName(root, "Progress Properties"); - item.querySelector(".extra-setting-button").click(); - await sleep(200); - const before = plugin.settings.ProgressProperties.properties.length; - const addBtn = Array.from(item.querySelectorAll("button")).find(b => b.textContent.trim() === "Add"); - addBtn.click(); - await sleep(200); - // Name the new row + pick a type. - const rows = item.querySelectorAll("tbody tr"); - const row = rows[rows.length - 1]; - const nameInput = row.querySelector("input[type=text]"); - nameInput.value = "auditProg"; - nameInput.dispatchEvent(new Event("input", { bubbles: true })); - nameInput.dispatchEvent(new Event("change", { bubbles: true })); - const select = row.querySelector("select"); - select.value = "Completed Tasks"; - select.dispatchEvent(new Event("change", { bubbles: true })); - await sleep(200); - const afterAdd = plugin.settings.ProgressProperties.properties.map(p => ({ name: p.name, type: p.type })); + const snapshot = JSON.parse(JSON.stringify(plugin.settings.ProgressProperties)); + plugin.settings.ProgressProperties = { enabled: true, properties: [] }; + await plugin.saveSettings(); + let afterAdd; + let afterRemove; + try { + const root = await openTab(); + const page = await openPage(root, "Tracked task counts"); + page.querySelector("[aria-label='Add property']").click(); + const input = await waitFor(".metaEditPromptInput"); + input.value = "auditProg"; + input.dispatchEvent(new Event("input", { bubbles: true })); + input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); + const row = await waitFor(".setting-page-content .setting-item", (el) => el.querySelector(".setting-item-name")?.textContent.trim() === "auditProg"); + const select = row.querySelector("select"); + select.value = "Completed Tasks"; + select.dispatchEvent(new Event("change", { bubbles: true })); + await sleep(300); + afterAdd = plugin.settings.ProgressProperties.properties.map(p => ({ name: p.name, type: p.type })); - // Remove it. - const delBtn = row.querySelector("input[type=button]"); - delBtn.click(); - await sleep(200); - const afterRemove = plugin.settings.ProgressProperties.properties.length; - app.setting.close(); + row.querySelector("[aria-label='Delete']").click(); + await sleep(300); + afterRemove = plugin.settings.ProgressProperties.properties.length; + app.setting.close(); + } finally { + plugin.settings.ProgressProperties = snapshot; + await plugin.saveSettings(); + } return { afterAdd, afterRemove }; })() `, ); - expect(result.afterAdd).toContainEqual({ name: "auditProg", type: "Completed Tasks" }); + expect(result.afterAdd).toEqual([{ name: "auditProg", type: "Completed Tasks" }]); + expect(result.afterRemove).toBe(0); expect(await obsidian.dev.runtimeErrors()).toEqual([]); }); @@ -207,7 +214,7 @@ describe("MetaEdit modal + kanban flows", () => { test("KAN-02/KAN-03: configure a board mapping and display its lane headings", async () => { const { obsidian, sandbox } = getContext(); const boardPath = sandbox.path("board.md"); - const result = await evalJsonAsync<{ boardsListed: boolean; lanes: string; mapped: boolean }>( + const result = await evalJsonAsync<{ boardsListed: boolean; lanes: string; mapped: boolean; property: string; persistedProperty: string }>( obsidian, ` (async () => { @@ -227,24 +234,26 @@ describe("MetaEdit modal + kanban flows", () => { const prevBoards = plugin.settings.KanbanHelper.boards; plugin.settings.KanbanHelper.enabled = true; try { + plugin.settings.KanbanHelper.boards = []; const root = await openTab(); - const item = itemByName(root, "Kanban Board Helper"); - item.querySelector(".extra-setting-button").click(); - await sleep(250); - // The board file should be offered (getFilesWithProperty kanban-plugin). - const boardsListed = plugin.getFilesWithProperty("kanban-plugin").some(b => b.path === path); - // Type the board name into the suggester input and click Add. - const input = item.querySelector("input[type=text]"); - input.value = "board"; + const page = await openPage(root, "Boards"); + page.querySelector("[aria-label='Add board']").click(); + // The board file is offered (getFilesWithProperty kanban-plugin). + const option = await waitFor(".suggestion-item", (el) => el.textContent.trim() === "board"); + const boardsListed = Boolean(option); + option.click(); + const row = await waitFor(".setting-page-content .setting-item", (el) => el.querySelector(".setting-item-name")?.textContent.trim() === "board"); + const mapped = plugin.settings.KanbanHelper.boards.some(b => b.boardName === "board"); + // The lane headings render as the row description. + const lanes = row.querySelector(".setting-item-description").textContent; + const input = row.querySelector("input[type=text]"); + input.value = "status"; input.dispatchEvent(new Event("input", { bubbles: true })); - const addBtn = Array.from(item.querySelectorAll("button")).find(b => b.textContent.trim() === "Add"); - addBtn.click(); await sleep(300); - const mapped = plugin.settings.KanbanHelper.boards.some(b => b.boardName === "board"); - // The lane headings render in the panel (getHeadingsInBoard, null-safe). - const lanes = item.textContent; + const property = plugin.settings.KanbanHelper.boards.find(b => b.boardName === "board")?.property; + const persistedProperty = (await plugin.loadData()).KanbanHelper.boards.find(b => b.boardName === "board")?.property; app.setting.close(); - return { boardsListed, lanes, mapped }; + return { boardsListed, lanes, mapped, property, persistedProperty }; } finally { plugin.settings.KanbanHelper.boards = prevBoards; plugin.settings.KanbanHelper.enabled = false; @@ -255,6 +264,8 @@ describe("MetaEdit modal + kanban flows", () => { ); expect(result.boardsListed).toBe(true); expect(result.mapped).toBe(true); + expect(result.property).toBe("status"); + expect(result.persistedProperty).toBe("status"); // KAN-03: lane headings are displayed (null-safe getFileCache). expect(result.lanes).toContain("To Do"); expect(result.lanes).toContain("Done"); diff --git a/tests/e2e/audit-settings.test.ts b/tests/e2e/audit-settings.test.ts index 2ed65d1..5ee4fa5 100644 --- a/tests/e2e/audit-settings.test.ts +++ b/tests/e2e/audit-settings.test.ts @@ -15,12 +15,31 @@ const HELPERS = ` }; const itemByName = (root, name) => Array.from(root.querySelectorAll(".setting-item")) .find((el) => el.querySelector(".setting-item-name")?.textContent.trim() === name); + const isShown = (el) => Boolean(el && el.offsetParent !== null); + const openPage = async (root, name) => { + itemByName(root, name).click(); + for (let i = 0; i < 50; i++) { + await sleep(80); + const title = document.querySelector(".setting-page-title"); + if (title?.textContent.trim() === name) return title.closest(".setting-page").querySelector(".setting-page-content"); + } + throw new Error("Settings page did not open: " + name); + }; + const answerPrompt = async (value) => { + let input = null; + for (let i = 0; i < 50 && !input; i++) { await sleep(80); input = document.querySelector(".metaEditPromptInput"); } + if (!input) throw new Error("Prompt did not open"); + input.value = value; + input.dispatchEvent(new Event("input", { bubbles: true })); + input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); + await sleep(400); + }; `; describe("MetaEdit settings tab", () => { test("SET-03/SET-05: feature toggles flip settings, persist to disk, and survive reopen", async () => { const { obsidian } = getContext(); - // Toggle Auto Properties and UI Elements through the rendered settings tab. + // Toggle Auto properties and the right-click menu through the rendered settings tab. const result = await evalJsonAsync<{ autoFlipped: boolean; autoPersisted: boolean; @@ -41,18 +60,18 @@ describe("MetaEdit settings tab", () => { }; const autoBefore = plugin.settings.AutoProperties.enabled; - await toggle("Auto Properties"); + await toggle("Auto properties"); const autoAfter = plugin.settings.AutoProperties.enabled; const autoPersisted = (await plugin.loadData()).AutoProperties.enabled === autoAfter; const uiBefore = plugin.settings.UIElements.enabled; - await toggle("UI Elements"); + await toggle("Right-click menu"); const uiAfter = plugin.settings.UIElements.enabled; const uiPersisted = (await plugin.loadData()).UIElements.enabled === uiAfter; // Restore both to their original state via the same UI path. - if (plugin.settings.AutoProperties.enabled !== autoBefore) await toggle("Auto Properties"); - if (plugin.settings.UIElements.enabled !== uiBefore) await toggle("UI Elements"); + if (plugin.settings.AutoProperties.enabled !== autoBefore) await toggle("Auto properties"); + if (plugin.settings.UIElements.enabled !== uiBefore) await toggle("Right-click menu"); app.setting.close(); return { @@ -71,31 +90,181 @@ describe("MetaEdit settings tab", () => { expect(await obsidian.dev.runtimeErrors()).toEqual([]); }); - test("SET-04: the gear button expands and collapses a sub-settings panel", async () => { + test("SET-04: a feature's sub-page shows only while the feature is on, and opens as a settings page", async () => { const { obsidian } = getContext(); - const result = await evalJsonAsync<{ expanded: boolean; collapsed: boolean }>( + const result = await evalJsonAsync<{ hiddenWhenOff: boolean; shownWhenOn: boolean; editorMounted: boolean }>( obsidian, ` (async () => { ${HELPERS} - const root = await openTab(); - const item = itemByName(root, "Auto Properties"); - const gear = item.querySelector(".extra-setting-button"); - // The collapsible panel is the div the Svelte content mounts into; it - // carries the 'metaedit-hidden' class which the gear toggles. - const detail = item.querySelector(".metaedit-auto-properties").parentElement; - const startsHidden = detail.classList.contains("metaedit-hidden"); - gear.click(); await sleep(200); - const expanded = !detail.classList.contains("metaedit-hidden"); - gear.click(); await sleep(200); - const collapsed = detail.classList.contains("metaedit-hidden"); - app.setting.close(); - return { expanded: startsHidden && expanded, collapsed }; + const plugin = app.plugins.plugins.${PLUGIN_ID}; + const before = plugin.settings.AutoProperties.enabled; + plugin.settings.AutoProperties.enabled = false; + await plugin.saveSettings(); + try { + const root = await openTab(); + const hiddenWhenOff = !isShown(itemByName(root, "Preset values")); + itemByName(root, "Auto properties").querySelector(".checkbox-container").click(); + await sleep(250); + const shownWhenOn = isShown(itemByName(root, "Preset values")); + const page = await openPage(root, "Preset values"); + const editorMounted = Boolean(page.querySelector(".metaedit-auto-properties")); + app.setting.close(); + return { hiddenWhenOff, shownWhenOn, editorMounted }; + } finally { + plugin.settings.AutoProperties.enabled = before; + await plugin.saveSettings(); + } + })() + `, + ); + expect(result.hiddenWhenOff).toBe(true); + expect(result.shownWhenOn).toBe(true); + expect(result.editorMounted).toBe(true); + expect(await obsidian.dev.runtimeErrors()).toEqual([]); + }); + + test("SET-06: List properties follows the edit mode and edits EditMode.properties", async () => { + const { obsidian } = getContext(); + const result = await evalJsonAsync<{ + hiddenInSingle: boolean; + shownInSomeMulti: boolean; + afterAdd: string[]; + persisted: string[]; + afterDelete: string[]; + }>( + obsidian, + ` + (async () => { + ${HELPERS} + const plugin = app.plugins.plugins.${PLUGIN_ID}; + const snapshot = JSON.parse(JSON.stringify(plugin.settings.EditMode)); + plugin.settings.EditMode = { mode: "All Single", properties: [] }; + await plugin.saveSettings(); + try { + const root = await openTab(); + const hiddenInSingle = !isShown(itemByName(root, "List properties")); + const select = itemByName(root, "Edit mode").querySelector("select"); + select.value = "Some Multi"; + select.dispatchEvent(new Event("change", { bubbles: true })); + await sleep(300); + const shownInSomeMulti = isShown(itemByName(root, "List properties")); + + let page = await openPage(root, "List properties"); + page.querySelector("[aria-label='Add property']").click(); + await answerPrompt("auditList"); + const afterAdd = plugin.settings.EditMode.properties.slice(); + const persisted = (await plugin.loadData()).EditMode.properties; + + page = document.querySelector(".setting-page-content"); + itemByName(page, "auditList").querySelector("[aria-label='Delete']").click(); + await sleep(400); + const afterDelete = plugin.settings.EditMode.properties.slice(); + app.setting.close(); + return { hiddenInSingle, shownInSomeMulti, afterAdd, persisted, afterDelete }; + } finally { + plugin.settings.EditMode = snapshot; + await plugin.saveSettings(); + } })() `, ); - expect(result.expanded).toBe(true); - expect(result.collapsed).toBe(true); + expect(result.hiddenInSingle).toBe(true); + expect(result.shownInSomeMulti).toBe(true); + expect(result.afterAdd).toEqual(["auditList"]); + expect(result.persisted).toEqual(["auditList"]); + expect(result.afterDelete).toEqual([]); + expect(await obsidian.dev.runtimeErrors()).toEqual([]); + }); + + test("SET-07: Hidden properties edits IgnoredProperties.properties, ignores duplicates, and deletes the clicked rows", async () => { + const { obsidian } = getContext(); + const result = await evalJsonAsync<{ afterAdds: string[]; rows: string[]; afterDeletes: string[]; persisted: string[] }>( + obsidian, + ` + (async () => { + ${HELPERS} + const plugin = app.plugins.plugins.${PLUGIN_ID}; + const snapshot = JSON.parse(JSON.stringify(plugin.settings.IgnoredProperties)); + plugin.settings.IgnoredProperties = { enabled: true, properties: [], hideFileTags: false }; + await plugin.saveSettings(); + try { + const root = await openTab(); + await openPage(root, "Hidden properties"); + for (const name of ["secret", " secret ", "draft", "keep"]) { + document.querySelector(".setting-page-content [aria-label='Add property']").click(); + await answerPrompt(name); + } + const rows = Array.from(document.querySelectorAll(".setting-page-content .setting-item-name")) + .map((el) => el.textContent.trim()) + .filter(Boolean); + const afterAdds = plugin.settings.IgnoredProperties.properties.slice(); + + // Both deletes are queued before the first one saves and re-renders. + const page = document.querySelector(".setting-page-content"); + const deleteButtons = ["secret", "draft"].map((name) => itemByName(page, name).querySelector("[aria-label='Delete']")); + deleteButtons.forEach((button) => button.click()); + await sleep(500); + const afterDeletes = plugin.settings.IgnoredProperties.properties.slice(); + const persisted = (await plugin.loadData()).IgnoredProperties.properties; + app.setting.close(); + return { afterAdds, rows, afterDeletes, persisted }; + } finally { + plugin.settings.IgnoredProperties = snapshot; + await plugin.saveSettings(); + } + })() + `, + ); + expect(result.afterAdds).toEqual(["secret", "draft", "keep"]); + expect(result.rows).toEqual(["secret", "draft", "keep"]); + expect(result.afterDeletes).toEqual(["keep"]); + expect(result.persisted).toEqual(["keep"]); + expect(await obsidian.dev.runtimeErrors()).toEqual([]); + }); + + test("SET-08: the Progress properties and Kanban board helper toggles attach and detach their automators", async () => { + const { obsidian } = getContext(); + const result = await evalJsonAsync<{ on: string[]; off: string[] }>( + obsidian, + ` + (async () => { + ${HELPERS} + const plugin = app.plugins.plugins.${PLUGIN_ID}; + const snapshot = { + progress: plugin.settings.ProgressProperties.enabled, + kanban: plugin.settings.KanbanHelper.enabled, + }; + plugin.settings.ProgressProperties.enabled = false; + plugin.settings.KanbanHelper.enabled = false; + plugin.toggleAutomators(); + await plugin.saveSettings(); + const attached = () => plugin.automatorManager.automators.map((a) => a.type).sort(); + try { + const root = await openTab(); + const toggle = async (name) => { + itemByName(root, name).querySelector(".checkbox-container").click(); + await sleep(250); + }; + await toggle("Progress properties"); + await toggle("Kanban board helper"); + const on = attached(); + await toggle("Progress properties"); + await toggle("Kanban board helper"); + const off = attached(); + app.setting.close(); + return { on, off }; + } finally { + plugin.settings.ProgressProperties.enabled = snapshot.progress; + plugin.settings.KanbanHelper.enabled = snapshot.kanban; + plugin.toggleAutomators(); + await plugin.saveSettings(); + } + })() + `, + ); + expect(result.on).toEqual(["KanbanHelper", "ProgressProperties"]); + expect(result.off).toEqual([]); expect(await obsidian.dev.runtimeErrors()).toEqual([]); }); @@ -107,11 +276,10 @@ describe("MetaEdit settings tab", () => { (async () => { ${HELPERS} const plugin = app.plugins.plugins.${PLUGIN_ID}; + const wasEnabled = plugin.settings.AutoProperties.enabled; + plugin.settings.AutoProperties.enabled = true; const root = await openTab(); - const item = itemByName(root, "Auto Properties"); - // Expand the panel. - item.querySelector(".extra-setting-button").click(); - await sleep(200); + const item = await openPage(root, "Preset values"); const before = plugin.settings.AutoProperties.properties.length; // Click "Add auto property". const addBtn = Array.from(item.querySelectorAll("button")).find(b => b.textContent.trim().includes("Add auto property")); @@ -133,6 +301,8 @@ describe("MetaEdit settings tab", () => { await sleep(200); const afterRemove = plugin.settings.AutoProperties.properties.map(a => a.name); app.setting.close(); + plugin.settings.AutoProperties.enabled = wasEnabled; + await plugin.saveSettings(); return { afterAdd, afterRemove }; })() `, @@ -163,9 +333,7 @@ describe("MetaEdit settings tab", () => { await plugin.saveSettings(); const root = await openTab(); - const item = itemByName(root, "Auto Properties"); - item.querySelector(".extra-setting-button").click(); - await sleep(250); + const item = await openPage(root, "Preset values"); await plugin.api.setAutoProperties([{ name: "status", choices: ["todo", "external"], type: "Single" }]); const afterExternal = plugin.settings.AutoProperties.properties[0].choices.slice(); @@ -196,10 +364,10 @@ describe("MetaEdit settings tab", () => { expect(await obsidian.dev.runtimeErrors()).toEqual([]); }); - test("MENU-04: UIElements toggle registers and unregisters the Edit Meta file menu", async () => { + test("MENU-04: the Right-click menu toggle registers and unregisters the Edit Meta file menu", async () => { const { obsidian, sandbox } = getContext(); const notePath = sandbox.path("menu-toggle.md"); - const result = await evalJsonAsync<{ whenOn: boolean; whenOff: boolean }>( + const result = await evalJsonAsync<{ whenOn: boolean; whenOff: boolean; storedOff: boolean; whenBackOn: boolean }>( obsidian, ` (async () => { @@ -226,26 +394,29 @@ describe("MetaEdit settings tab", () => { return sink.some((t) => /Edit Meta/.test(t)); }; - // Ensure enabled. - if (!plugin.settings.UIElements.enabled) { plugin.linkMenu.registerEvent(); plugin.settings.UIElements.enabled = true; } - await sleep(100); + const root = await openTab(); + const toggle = async () => { + itemByName(root, "Right-click menu").querySelector(".checkbox-container").click(); + await sleep(250); + }; + if (!plugin.settings.UIElements.enabled) await toggle(); const whenOn = menuHasEditMeta(); - // Disable via the real handler path. - plugin.linkMenu.unregisterEvent(); - plugin.settings.UIElements.enabled = false; - await sleep(100); + await toggle(); const whenOff = menuHasEditMeta(); + const storedOff = plugin.settings.UIElements.enabled; - // Restore enabled for other tests. - plugin.linkMenu.registerEvent(); - plugin.settings.UIElements.enabled = true; - return { whenOn, whenOff }; + await toggle(); + const whenBackOn = menuHasEditMeta(); + app.setting.close(); + return { whenOn, whenOff, storedOff, whenBackOn }; })() `, ); expect(result.whenOn).toBe(true); expect(result.whenOff).toBe(false); + expect(result.storedOff).toBe(false); + expect(result.whenBackOn).toBe(true); expect(await obsidian.dev.runtimeErrors()).toEqual([]); }); diff --git a/website/src/assets/media/autoprop-settings.png b/website/src/assets/media/autoprop-settings.png index 62efe69..b30a033 100644 Binary files a/website/src/assets/media/autoprop-settings.png and b/website/src/assets/media/autoprop-settings.png differ diff --git a/website/src/assets/media/edit-meta-menu-panel.png b/website/src/assets/media/edit-meta-menu-panel.png index cd6fc00..9ac66e6 100644 Binary files a/website/src/assets/media/edit-meta-menu-panel.png and b/website/src/assets/media/edit-meta-menu-panel.png differ diff --git a/website/src/assets/media/edit-mode-some-multi.png b/website/src/assets/media/edit-mode-some-multi.png index 8db3265..4b2c6d1 100644 Binary files a/website/src/assets/media/edit-mode-some-multi.png and b/website/src/assets/media/edit-mode-some-multi.png differ diff --git a/website/src/assets/media/kanban-helper-panel.png b/website/src/assets/media/kanban-helper-panel.png index 292d524..0d25534 100644 Binary files a/website/src/assets/media/kanban-helper-panel.png and b/website/src/assets/media/kanban-helper-panel.png differ diff --git a/website/src/assets/media/progress-properties-panel.png b/website/src/assets/media/progress-properties-panel.png index b5ef063..8178a7a 100644 Binary files a/website/src/assets/media/progress-properties-panel.png and b/website/src/assets/media/progress-properties-panel.png differ diff --git a/website/src/assets/media/settings-tab.png b/website/src/assets/media/settings-tab.png index 718d61e..83766ca 100644 Binary files a/website/src/assets/media/settings-tab.png and b/website/src/assets/media/settings-tab.png differ diff --git a/website/src/content/docs/api/auto-properties.md b/website/src/content/docs/api/auto-properties.md index d821749..403f7af 100644 --- a/website/src/content/docs/api/auto-properties.md +++ b/website/src/content/docs/api/auto-properties.md @@ -30,10 +30,10 @@ Looks up an enabled Auto Property whose name exactly equals `propertyName` (matc | `null` | Auto Properties are disabled, no Auto Property with that name exists, or the user cancels the prompt | :::caution[The master toggle gates this at runtime] -A perfectly configured Auto Property still returns `null` while the "Auto Properties" toggle is off in MetaEdit's settings. If your template suddenly stops prompting, check the toggle before debugging your code. +A perfectly configured Auto Property still returns `null` while the "Auto properties" toggle is off in MetaEdit's settings. If your template suddenly stops prompting, check the toggle before debugging your code. ::: -Whether the prompt is single- or multi-select follows the property's configured type: an explicit Single or Multi is authoritative. When the property has no type (configurations saved before types existed), it inherits the global [Edit Mode](/reference/settings/): All Multi makes it multi-select, and Some Multi makes it multi-select when the property is listed there. +Whether the prompt is single- or multi-select follows the property's configured type: an explicit Single or Multi is authoritative. When the property has no type (configurations saved before types existed), it inherits the global [Edit Mode](/reference/settings/#edit-mode): "List" makes it multi-select, and "List for chosen properties" makes it multi-select when the property is listed there. The prompt itself is interactive. If the user types a value that is not in the choice list, they can use it once or save it as a new choice, which persists into the Auto Property's configuration. diff --git a/website/src/content/docs/api/examples.md b/website/src/content/docs/api/examples.md index b7d924a..1bd6b8c 100644 --- a/website/src/content/docs/api/examples.md +++ b/website/src/content/docs/api/examples.md @@ -39,7 +39,7 @@ Incomplete:: 1 How it works: [`autoprop`](/api/auto-properties/) opens the Auto Property value prompt for `Status`, then for `Priority`, and returns whatever you pick; Templater interpolates the result into the inline Dataview fields. For the prompts to appear, Auto Properties named exactly `Status` and `Priority` must exist and be enabled in [MetaEdit's settings](/reference/settings/) - name matching is case-sensitive, and `autoprop` returns `null` (which renders literally) when the property is missing or the prompt is cancelled. -The `Total`, `Complete`, and `Incomplete` lines are there for [Progress Properties](/guides/progress-properties/): configure those three names in the Progress Properties panel and MetaEdit updates the counts whenever the note's tasks change. The template pre-creates the fields because the automator only updates properties that already exist. +The `Total`, `Complete`, and `Incomplete` lines are there for [Progress Properties](/guides/progress-properties/): add those three names on the "Tracked task counts" settings page and MetaEdit updates the counts whenever the note's tasks change. The template pre-creates the fields because the automator only updates properties that already exist. ## Complete a task from a Dataview table (Buttons version) diff --git a/website/src/content/docs/api/overview.md b/website/src/content/docs/api/overview.md index f175307..25f56b5 100644 --- a/website/src/content/docs/api/overview.md +++ b/website/src/content/docs/api/overview.md @@ -7,7 +7,7 @@ sidebar: MetaEdit exposes a 14-method JavaScript API that other plugins, Templater templates, and `dataviewjs` blocks can call to read and write note metadata. This page shows how to get the API object, the conventions every method shares, and where each method is documented. -The API surface described here is MetaEdit 1.9.0, which requires Obsidian 1.12.7+ and works on desktop and mobile. See the [changelog](/help/changelog/) for what each release added. +The API surface described here is MetaEdit 1.9.0 and later. MetaEdit requires Obsidian 1.13.4+ and works on desktop and mobile. See the [changelog](/help/changelog/) for what each release added. ## Get the API diff --git a/website/src/content/docs/api/properties.md b/website/src/content/docs/api/properties.md index aaa61be..f714f5b 100644 --- a/website/src/content/docs/api/properties.md +++ b/website/src/content/docs/api/properties.md @@ -151,7 +151,7 @@ Adds a new top-level YAML frontmatter property. A frontmatter block is created i The value is shaped before writing: - A `tags`/`tag` key is canonicalized into a `#`-free string list. -- [Edit Mode](/reference/settings/) wrapping applies, as on all legacy add paths: when Edit Mode is "All Multi", or "Some Multi" with this property listed, a non-array value is wrapped into a one-element list - unless an enabled Auto Property of the same name resolves to Single, which keeps it scalar. +- [Edit Mode](/reference/settings/#edit-mode) wrapping applies, as on all legacy add paths: when Edit Mode is "List", or "List for chosen properties" with this property listed, a non-array value is wrapped into a one-element list - unless an enabled Auto Property of the same name resolves to Single, which keeps it scalar. ```js // Templater: stamp a fresh note as not yet reviewed diff --git a/website/src/content/docs/concepts/what-metaedit-can-edit.md b/website/src/content/docs/concepts/what-metaedit-can-edit.md index dadf871..2447d5e 100644 --- a/website/src/content/docs/concepts/what-metaedit-can-edit.md +++ b/website/src/content/docs/concepts/what-metaedit-can-edit.md @@ -98,7 +98,7 @@ Not listed: `publish` and `sources` (parent containers), the individual `genres` - **Non-markdown files.** "MetaEdit: Run" silently does nothing when the active file is not a markdown note (a PDF or canvas, for example); the only trace is a developer-console message. - **Anything inside code fences**, whether it looks like a field or a tag. - **Parent YAML containers** as a single editable value, as described above. -- **Rows hidden by settings.** The "Edit Meta menu" settings can hide named properties and body tags from the picker. Filtering only hides rows - it never deletes data, turning the feature off means nothing is filtered, and hidden-but-present keys are still excluded from new-property name suggestions. Matching is exact and case-sensitive, and hiding body tags never affects the frontmatter `tags` property. See the [settings reference](/reference/settings/). +- **Rows hidden by settings.** The "Hide properties" settings can hide named properties and body tags from the picker. Filtering only hides rows - it never deletes data, turning the feature off means nothing is filtered, and hidden-but-present keys are still excluded from new-property name suggestions. Matching is exact and case-sensitive, and hiding body tags never affects the frontmatter `tags` property. See the [settings reference](/reference/settings/). :::note[Legacy and malformed frontmatter] Legacy frontmatter closed with `...` instead of `---` still parses. If the YAML itself is malformed, MetaEdit falls back to Obsidian's metadata cache, and failing that shows no YAML rows - tag and inline-field parsing keep working either way. diff --git a/website/src/content/docs/cookbook/bulk-cleanups.md b/website/src/content/docs/cookbook/bulk-cleanups.md index bb4087f..0bab697 100644 --- a/website/src/content/docs/cookbook/bulk-cleanups.md +++ b/website/src/content/docs/cookbook/bulk-cleanups.md @@ -7,7 +7,7 @@ sidebar: MetaEdit's bulk edit writes one YAML property across many notes in a single pass, with a conflict policy that decides what happens to notes that already have the key. This page turns the three policies into three migration recipes you can reuse: an additive stamp, a list merge, and a value standardization with a safety net. -If you have not used bulk edit before, the [bulk edit guide](/guides/bulk-edit/) covers the full flow. The short version: right-click a folder and choose "Bulk edit metadata in this folder (and subfolders)", or select several notes and choose "Bulk edit metadata in selected notes". There is no command palette entry - bulk edit is context-menu only, and both items live behind the "UI Elements" toggle in [settings](/reference/settings/) (on by default). +If you have not used bulk edit before, the [bulk edit guide](/guides/bulk-edit/) covers the full flow. The short version: right-click a folder and choose "Bulk edit metadata in this folder (and subfolders)", or select several notes and choose "Bulk edit metadata in selected notes". There is no command palette entry - bulk edit is context-menu only, and both items live behind the "Right-click menu" toggle in [settings](/reference/settings/#right-click-menu) (on by default). ![Right-clicking a folder, choosing the bulk edit menu item, entering a property name and value, then picking a conflict policy](../../../assets/media/bulk-edit-flow.gif) @@ -25,7 +25,7 @@ When some notes in scope already define the property, MetaEdit shows a chooser w If no note in scope has the key yet, the chooser is skipped and everything is an add. Pressing Escape in the chooser aborts the whole operation - nothing is written. -Two things bulk edit always does, regardless of policy: it writes YAML frontmatter only (never inline `key:: value` fields), and it honors your Edit Mode setting, so a key configured as multi-value is written as a one-element list just like a single-note add. It ignores Auto Properties and Edit Meta menu filtering entirely, and it is not undoable with Ctrl+Z. +Two things bulk edit always does, regardless of policy: it writes YAML frontmatter only (never inline `key:: value` fields), and it honors your Edit Mode setting, so a key configured as multi-value is written as a one-element list just like a single-note add. It ignores Auto Properties and "Hide properties" filtering entirely, and it is not undoable with Ctrl+Z. ## Recipe 1: stamp a type on a folder diff --git a/website/src/content/docs/cookbook/kanban-status-sync.md b/website/src/content/docs/cookbook/kanban-status-sync.md index 4be5689..ca0aef2 100644 --- a/website/src/content/docs/cookbook/kanban-status-sync.md +++ b/website/src/content/docs/cookbook/kanban-status-sync.md @@ -9,7 +9,7 @@ Build a project board where dragging a card is all it takes: about five seconds ## What you need -- MetaEdit 1.9.0 on Obsidian 1.12.7 or newer (desktop or mobile). +- MetaEdit 1.9.0 or later on Obsidian 1.13.4 or newer (desktop or mobile). - The community [Kanban plugin](https://github.com/mgmeyers/obsidian-kanban). MetaEdit's board picker only lists notes with a `kanban-plugin` frontmatter key, which is what the Kanban plugin writes. - The community [Dataview plugin](https://github.com/blacksmithgu/obsidian-dataview) for the query at the end (optional). @@ -63,16 +63,16 @@ Property names match exactly and case-sensitively: a board row configured for `s ## Step 3: register the board in MetaEdit 1. Open Settings, then MetaEdit. -2. Turn on the "Kanban Board Helper" toggle. Its description reads "Update properties in links in kanban boards automatically when a card is moved to a new lane." No restart is needed. -3. Click the gear button on the row to expand the configuration panel. -4. In the text input at the bottom, start typing `Projects`. The input suggests every note with a `kanban-plugin` frontmatter key. Pick the board, then click "Add". The typed text must exactly match a suggested board name, or "Add" does nothing. -5. In the new table row, type `status` into the "Property in link" column (placeholder "Property name"). +2. Turn on the "Kanban board helper" toggle. Its description reads "When a card moves to another lane, update a property in the note it links to." No restart is needed. +3. Click the "Boards" row to open its page. +4. Click the "+" button ("Add board"). The picker lists every note with a `kanban-plugin` frontmatter key that is not added yet. Start typing `Projects` and pick the board. +5. In the board's row, type `status` into the field with the placeholder "Property in linked notes". -The "Possible values" column shows the board's current lane headings, so for this board it reads `Backlog, In Progress, Done`. +The "Lanes:" line under the board name shows the board's current lane headings, so for this board it reads `Lanes: Backlog, In Progress, Done`. -![The Kanban Board Helper settings panel, shown with a registered board named Editorial calendar, "Property in link" set to status, and "Possible values" listing the board's lanes Ideas, Drafting, Published](../../../assets/media/kanban-helper-panel.png) +![The "Boards" settings page with a board named Editorial calendar, "Lanes: Ideas, Drafting, Published" under its name, and status in its property field](../../../assets/media/kanban-helper-panel.png) -The panel above shows a different example board; yours will list `Projects` with the three lanes from step 1. +The page above shows a different example board; yours will list `Projects` with the three lanes from step 1. ## Step 4: drag a card @@ -110,8 +110,8 @@ Drag a card, wait a few seconds, and the table regroups. The same property also | Notice: `MetaEdit: (WARNING) 'status' not found in "Website relaunch" (Kanban board 'Projects').` | The linked note lacks the property. The helper updates properties but never creates them - add `status` to the note first (step 2). | | Nothing happens after a drag | Wait for the 5-second debounce; it resets while the board keeps changing. The helper only reacts to modifications made through Obsidian. | | One specific card never syncs | Only the leading link of a top-level card line counts (`- [ ] [[Note]] ...`). Trailing links such as Kanban date links, links in the card text, and indented sub-items are ignored. A card sitting above the first lane heading has no lane and is skipped. If several notes share the card's bare name and Obsidian cannot resolve the link, the helper skips it rather than guess. | -| Board stopped syncing after you renamed it | Boards are matched by file basename, and the stored row keeps the old name. Remove the row and re-add the renamed board. If the board file is gone, its "Possible values" cell shows `FILE NOT FOUND`. | -| "Add" does nothing | The typed name must exactly match a suggested board, and boards already registered are ignored. | -| Property present but still not updating | Matching is exact and case-sensitive: `status` is not `Status`. Check the "Property in link" spelling. | +| Board stopped syncing after you renamed it | Boards are matched by file basename, and the stored row keeps the old name. Remove the row and re-add the renamed board. If the board file is gone, its row shows "Board not found." | +| "Add board" shows a notice instead of a picker | "MetaEdit found no other Kanban boards in this vault." means every note with a `kanban-plugin` frontmatter key is already added. Check that the board file has that key. | +| Property present but still not updating | Matching is exact and case-sensitive: `status` is not `Status`. Check the spelling in the board's "Property in linked notes" field. | The helper shares the automator pipeline's gates with [Progress Properties](/guides/progress-properties/): it processes markdown files only, the modified file needs YAML frontmatter (a Kanban board always has one), Excalidraw notes are skipped, and unchanged content is skipped. Every write goes through MetaEdit's per-file queue - see [how MetaEdit writes to your notes](/concepts/write-safety/). diff --git a/website/src/content/docs/cookbook/reading-tracker.md b/website/src/content/docs/cookbook/reading-tracker.md index 54f7c8f..c83ef83 100644 --- a/website/src/content/docs/cookbook/reading-tracker.md +++ b/website/src/content/docs/cookbook/reading-tracker.md @@ -9,7 +9,7 @@ Track your reading with a small set of typed frontmatter properties, two [Auto P ## Prerequisites -- MetaEdit 1.9.0 or later. The plugin requires Obsidian 1.12.7+ and everything here works on desktop and mobile. +- MetaEdit 1.9.0 or later. The plugin requires Obsidian 1.13.4+ and everything here works on desktop and mobile. - The [Dataview](https://github.com/blacksmithgu/obsidian-dataview) community plugin, for the bookshelf query in the last step. Every other step works without it. - A folder for book notes. This recipe uses `Library/`. @@ -33,12 +33,12 @@ Property types live in Obsidian's vault-wide type registry, not in MetaEdit. Onc `status` and `genres` are the values you will set over and over, so give each a fixed list of choices: 1. Open Settings, then MetaEdit. -2. Turn on the "Auto Properties" toggle (its settings description reads "Quick switch for values you know the value of." - in practice: named choice lists for properties). -3. Click the row's extra button to expand the inline management UI. +2. Turn on the "Auto properties" toggle (its settings description reads "Pick from preset values when you edit these properties."). +3. Click the "Preset values" row to open its page. 4. Click "Add auto property". Name it `status`, keep the type dropdown on "Single", set the description to `Where this book is in your reading flow.`, then use "Add value" to add `To Read`, `Reading`, `Finished`, and `Abandoned`. 5. Click "Add auto property" again. Name it `genres`, switch the type dropdown to "Multi", and add your genres as values. -![Auto Properties settings with a Single "status" property (values To Read, Reading, Finished, Abandoned and a description) and a Multi "genres" property](../../../assets/media/autoprop-settings.png) +![The "Preset values" settings page with a Single "status" property (values To Read, Reading, Finished, Abandoned and a description) and a Multi "genres" property](../../../assets/media/autoprop-settings.png) Every change saves immediately; there is no save button. diff --git a/website/src/content/docs/cookbook/task-dashboard.md b/website/src/content/docs/cookbook/task-dashboard.md index 94f9b2d..9d47d74 100644 --- a/website/src/content/docs/cookbook/task-dashboard.md +++ b/website/src/content/docs/cookbook/task-dashboard.md @@ -9,7 +9,7 @@ Build a dashboard note that lists every open task in your vault and completes an ## Prerequisites -- MetaEdit 1.9.0 or later. The plugin requires Obsidian 1.12.7+ and works on desktop and mobile. +- MetaEdit 1.9.0 or later. The plugin requires Obsidian 1.13.4+ and works on desktop and mobile. - The [Dataview](https://github.com/blacksmithgu/obsidian-dataview) community plugin with "Enable JavaScript queries" turned on in its settings (the dashboard is a `dataviewjs` block). - The [Buttons](https://github.com/shabegom/buttons) community plugin for the first dashboard variant. The second variant uses a plain HTML button and needs no extra plugin. - Optionally [Templater](https://github.com/SilentVoid13/Templater), if you want new task notes to prompt for their metadata. @@ -95,10 +95,10 @@ Same behavior, no extra plugin: a click awaits `update()` against the task's fil Each `Project::` field links to a project note. Give those notes live task counts with Progress Properties: -1. Open Settings, then MetaEdit, and turn on the "Progress Properties" toggle (description: "Update properties automatically."). -2. Click the row's extra button to expand the configuration table and click "Add" three times. Name the rows `Total`, `Complete`, and `Incomplete`, with the types "Total Tasks", "Completed Tasks", and "Incomplete Tasks" respectively. +1. Open Settings, then MetaEdit, and turn on the "Progress properties" toggle (description: "Keep task counts in a note's properties up to date."). +2. Click the "Tracked task counts" row to open its page. Use the "+" button ("Add property") three times to add `Total`, `Complete`, and `Incomplete`, then set their dropdowns to "Total tasks", "Completed tasks", and "Incomplete tasks" respectively. -![The Progress Properties panel with rows Total, Complete, and Incomplete mapped to the Total Tasks, Completed Tasks, and Incomplete Tasks types](../../../assets/media/progress-properties-panel.png) +![The "Tracked task counts" settings page with Total, Complete, and Incomplete set to Total tasks, Completed tasks, and Incomplete tasks](../../../assets/media/progress-properties-panel.png) 3. Seed the three properties in each project note yourself - Progress Properties updates existing properties but never creates them: diff --git a/website/src/content/docs/cookbook/templater-metadata-prompts.md b/website/src/content/docs/cookbook/templater-metadata-prompts.md index 6f50486..6855a7c 100644 --- a/website/src/content/docs/cookbook/templater-metadata-prompts.md +++ b/website/src/content/docs/cookbook/templater-metadata-prompts.md @@ -10,7 +10,7 @@ Templater templates can pause and ask for metadata using the same value prompts ## Prerequisites - The [Templater](https://github.com/SilentVoid13/Templater) community plugin. -- MetaEdit 1.9.0 or later (Obsidian 1.12.7+, desktop or mobile) with the "Auto Properties" toggle on and the properties you want to prompt for defined in settings. This recipe uses the `status` (Single) and `genres` (Multi) Auto Properties from [Build a reading tracker](/cookbook/reading-tracker/). +- MetaEdit 1.9.0 or later (Obsidian 1.13.4+, desktop or mobile) with the "Auto properties" toggle on and the properties you want to prompt for defined in settings. This recipe uses the `status` (Single) and `genres` (Multi) Auto Properties from [Build a reading tracker](/cookbook/reading-tracker/). ## How autoprop() behaves @@ -39,7 +39,7 @@ Variables declared in a `<%* %>` block stay in scope for the rest of the templat `autoprop()` resolves `null` in exactly three cases: -1. The "Auto Properties" toggle is off in MetaEdit's settings. +1. The "Auto properties" toggle is off in MetaEdit's settings. 2. No enabled Auto Property has that exact, case-sensitive name. 3. The user cancelled the prompt (Esc, or clicking outside). @@ -57,7 +57,7 @@ if (genres !== null) { ``` :::caution[Templates must degrade gracefully] -Because a switched-off "Auto Properties" toggle also yields `null`, a template that interpolates the raw result would print the text `null` into the note. Always default with `??` or guard with an `if`. +Because a switched-off "Auto properties" toggle also yields `null`, a template that interpolates the raw result would print the text `null` into the note. Always default with `??` or guard with an `if`. ::: ## Two ways to write the answer @@ -132,7 +132,7 @@ Recommended by:: <% (await tp.system.prompt("Recommended by")) ?? "" %> How it holds together: - The prompts fire in order as the note is created: the `status` picker, the `genres` multi-select, then two plain Templater text prompts. -- The `??` defaults mean a cancelled prompt, or the "Auto Properties" toggle being off, still produces a valid note instead of `null` text or a rejected write. +- The `??` defaults mean a cancelled prompt, or the "Auto properties" toggle being off, still produces a valid note instead of `null` text or a rejected write. - `genres` reaches `addOrUpdateProperty()` as an array, so it lands in frontmatter as a real YAML list. - `started` is only written when the book begins as `Reading`. `addOrUpdateProperty()` creates missing keys, so nothing needs seeding. - The frontmatter writes run inside `on_all_templates_executed`, after Templater's own write; the "Recommended by" line is ordinary template output, so it needs no hook. diff --git a/website/src/content/docs/cookbook/tracker-metrics.md b/website/src/content/docs/cookbook/tracker-metrics.md index 9a8eafc..28d3b32 100644 --- a/website/src/content/docs/cookbook/tracker-metrics.md +++ b/website/src/content/docs/cookbook/tracker-metrics.md @@ -9,7 +9,7 @@ The [obsidian-tracker](https://github.com/pyrochlore/obsidian-tracker) plugin ca ## What you need -- MetaEdit 1.9.0 on Obsidian 1.12.7 or newer (desktop or mobile). +- MetaEdit 1.9.0 or later on Obsidian 1.13.4 or newer (desktop or mobile). - The community obsidian-tracker plugin, installed and enabled. The "Tracker value (#tag:value)" action only exists when Tracker is installed. MetaEdit checks for it once, when the plugin loads - if you install Tracker mid-session, disable and re-enable MetaEdit (or restart Obsidian) so the action appears. diff --git a/website/src/content/docs/getting-started/installation.md b/website/src/content/docs/getting-started/installation.md index 5d14bea..d2f7257 100644 --- a/website/src/content/docs/getting-started/installation.md +++ b/website/src/content/docs/getting-started/installation.md @@ -12,11 +12,11 @@ MetaEdit installs like any other Obsidian community plugin and needs no configur | | | | --- | --- | | MetaEdit version | 1.9.0 (current) | -| Obsidian version | 1.12.7 or newer | +| Obsidian version | 1.13.4 or newer | | Platforms | Desktop and mobile, with no feature differences | :::note[On an older Obsidian?] -If your vault runs an Obsidian version older than 1.12.7, the community plugin updater automatically serves you MetaEdit 1.8.4, the newest release compatible with your app. You do not need to pick a version yourself. Update Obsidian to get 1.9.0 and its [native property editing](/guides/edit-properties/). +If your vault runs an Obsidian version older than 1.13.4, the community plugin updater automatically serves you the newest MetaEdit release compatible with your app. On anything older than 1.12.7 that is 1.8.4, which predates [native property editing](/guides/edit-properties/). You do not need to pick a version yourself. Update Obsidian to get the current release. ::: ## Install from the Community plugins browser @@ -42,7 +42,7 @@ MetaEdit deliberately adds no ribbon icon, so don't look for one. Instead: - Open the command palette (`⌘/Ctrl+P`) in any note and search for **MetaEdit: Run**. If the command is there, the plugin is loaded. - Right-click any `.md` file in the file explorer. You should see an **Edit Meta** item with a pencil icon. -If the **Edit Meta** item is missing, check that the "UI Elements" toggle is on in MetaEdit's settings - it is on by default. See the [settings reference](/reference/settings/) for details. +If the **Edit Meta** item is missing, check that the "Right-click menu" toggle is on in MetaEdit's settings - it is on by default. See the [settings reference](/reference/settings/#right-click-menu) for details. ## Optional companion plugins diff --git a/website/src/content/docs/getting-started/whats-new.md b/website/src/content/docs/getting-started/whats-new.md index 4f0540a..3e94cbd 100644 --- a/website/src/content/docs/getting-started/whats-new.md +++ b/website/src/content/docs/getting-started/whats-new.md @@ -60,7 +60,7 @@ Text prompts now autocomplete from your vault: property values are suggested ran ## A cleaner, more capable menu - Every YAML/Dataview row has native, tooltipped actions: "Delete property", or "Transform to YAML ⇄ Dataview", right from the list. See [Delete and transform properties](/guides/delete-and-transform/). -- **Hide file tags** from the picker (Settings -> "Edit Meta menu") when you only care about frontmatter and inline fields - a frontmatter `tags:` property stays editable. +- **Hide file tags** from the picker (Settings -> "Hide properties") when you only care about frontmatter and inline fields - a frontmatter `tags:` property stays editable. - Hide specific properties by name via the same [filtering settings](/reference/settings/). - Nested YAML shows up as individual child rows, so structured frontmatter is no longer a wall of text. diff --git a/website/src/content/docs/guides/auto-properties.md b/website/src/content/docs/guides/auto-properties.md index e121d3a..0d33bf2 100644 --- a/website/src/content/docs/guides/auto-properties.md +++ b/website/src/content/docs/guides/auto-properties.md @@ -9,12 +9,12 @@ An Auto Property is a named set of values you define once and reuse everywhere. ## Turn it on and define properties -Auto Properties are configured in [MetaEdit's settings](/reference/settings/) under the "Auto Properties" row. Its in-app description is the somewhat cryptic "Quick switch for values you know the value of." - in practice it means: for properties whose values come from a known set, switch values quickly instead of retyping them. The feature is off by default. +Auto Properties are configured in [MetaEdit's settings](/reference/settings/) in the "Editing" group, under the "Auto properties" toggle. Its in-app description reads "Pick from preset values when you edit these properties." The feature is off by default. -![Auto Properties settings panel showing two property cards: "status" with type Single, a description, and the values To Read, Reading, Finished, and Abandoned; and "genres" with type Multi.](../../../assets/media/autoprop-settings.png) +![The "Preset values" settings page showing two property cards: "status" with type Single, a description, and the values To Read, Reading, Finished, and Abandoned; and "genres" with type Multi.](../../../assets/media/autoprop-settings.png) -1. Enable the "Auto Properties" toggle. -2. Click the row's gear button to expand the management panel. +1. Enable the "Auto properties" toggle. +2. Click the "Preset values" row to open its page. 3. Click "Add auto property". 4. Name it exactly after the property key. Matching is exact and case-sensitive, so `Status` does not match `status`. 5. Pick "Single" or "Multi" from the type dropdown. @@ -42,7 +42,7 @@ A single pasted value that contains a comma and no line break, like `Doe, Jane` | "Single" | Pick exactly one value | A scalar | | "Multi" | Checkbox multi-select | A list | -The per-property type is authoritative in both directions and overrides the global "Edit Mode" setting: a Single Auto Property stays scalar even under "All Multi", and a Multi one is a list even under "All Single". Only Auto Properties saved before the type field existed (no type set) inherit Edit Mode. See [Lists and multi-values](/guides/lists-and-multi-values/). +The per-property type is authoritative in both directions and overrides the global "Edit mode" setting: a Single Auto Property stays scalar even under "List", and a Multi one is a list even under "One value". Only Auto Properties saved before the type field existed (no type set) inherit Edit Mode. See [Lists and multi-values](/guides/lists-and-multi-values/). ## Picking a value @@ -104,7 +104,7 @@ genres: --- ``` -The same Multi result on an inline field is written as `genres:: Sci-Fi, Classics`. A Single Auto Property keeps its value scalar even when "Edit Mode" is set to "All Multi". +The same Multi result on an inline field is written as `genres:: Sci-Fi, Classics`. A Single Auto Property keeps its value scalar even when "Edit mode" is set to "List". A Multi Auto Property on a nested tag is effectively single-select: checking more than one value produces a comma-joined result that fails tag validation, so MetaEdit refuses the write with the invalid-tag-name notice. Checking nothing cancels silently. See [edit tags](/guides/edit-tags/). diff --git a/website/src/content/docs/guides/bulk-edit.md b/website/src/content/docs/guides/bulk-edit.md index 078a059..2489021 100644 --- a/website/src/content/docs/guides/bulk-edit.md +++ b/website/src/content/docs/guides/bulk-edit.md @@ -22,7 +22,7 @@ A few things to know about the menu items: - They only appear when the folder or selection contains at least one Markdown file somewhere in scope. Non-Markdown files are ignored. - A note reached two ways (selected directly and inside a selected folder) is edited once. -- Both items are gated by the "UI Elements" toggle in [MetaEdit's settings](/reference/settings/) (on by default). Its description only mentions the "Edit Meta" option, but it gates the bulk items too. +- Both items are gated by the "Right-click menu" toggle in [MetaEdit's settings](/reference/settings/#right-click-menu) (on by default). Its description only mentions "Edit Meta", but it gates the bulk items too. ## Name the property and value @@ -93,8 +93,8 @@ If every count is zero, the detail reads "no changes". When any note fails, the ## Boundaries and guarantees - **YAML frontmatter only.** Bulk edit never touches inline Dataview fields (`key:: value`), and only `.md` files are edited. See [What MetaEdit can edit](/concepts/what-metaedit-can-edit/). -- **Ignores Auto Properties and "Edit Meta menu" filtering.** The key and value you type are written as-is; no pick-from-list prompt appears and hidden properties are not exempt. See [Auto Properties](/guides/auto-properties/). -- **Honors Edit Mode wrapping.** Under "All Multi", or "Some Multi" with the key listed, a freshly added or overwriting value is wrapped as a one-element list, matching what a single-note add would produce. Merge produces lists regardless. See [Lists and multi-values](/guides/lists-and-multi-values/). +- **Ignores Auto Properties and "Hide properties" filtering.** The key and value you type are written as-is; no pick-from-list prompt appears and hidden properties are not exempt. See [Auto Properties](/guides/auto-properties/). +- **Honors Edit Mode wrapping.** Under "List", or "List for chosen properties" with the key listed, a freshly added or overwriting value is wrapped as a one-element list, matching what a single-note add would produce. Merge produces lists regardless. See [Lists and multi-values](/guides/lists-and-multi-values/). - **Idempotent re-runs.** Running the same bulk edit again reports the already-correct notes as unchanged and rewrites nothing, so a re-run after adding new notes to a folder is safe and cheap. - **Serialized writes.** Each note is written through MetaEdit's per-file write queue, so a bulk write cannot be lost to another MetaEdit edit of the same note. See [Write safety](/concepts/write-safety/). - **Not undoable.** There is no Ctrl+Z for a bulk run, as the overwrite confirmation warns. diff --git a/website/src/content/docs/guides/create-properties.md b/website/src/content/docs/guides/create-properties.md index 0f19e63..2d8f980 100644 --- a/website/src/content/docs/guides/create-properties.md +++ b/website/src/content/docs/guides/create-properties.md @@ -23,7 +23,7 @@ The footer holds an "Add" button, a "Cancel" button, and an info icon whose tool ### Key autocomplete -As you type the name, a dropdown suggests property names already used anywhere in your vault plus your configured [Auto Property](/guides/auto-properties/) names. Keys the note already has are excluded - including ones hidden from the picker by the ["Edit Meta menu" filters](/reference/settings/) - so you are never offered a name that would be rejected as a duplicate. +As you type the name, a dropdown suggests property names already used anywhere in your vault plus your configured [Auto Property](/guides/auto-properties/) names. Keys the note already has are excluded - including ones hidden from the picker by the ["Hide properties" setting](/reference/settings/#hide-properties) - so you are never offered a name that would be rejected as a duplicate. ### Automatic type adoption diff --git a/website/src/content/docs/guides/delete-and-transform.md b/website/src/content/docs/guides/delete-and-transform.md index 1271003..a68d1e3 100644 --- a/website/src/content/docs/guides/delete-and-transform.md +++ b/website/src/content/docs/guides/delete-and-transform.md @@ -50,7 +50,7 @@ Click the transform icon ("Transform to YAML ⇄ Dataview"). The direction is de **Inline to YAML.** The `key::` line is removed and the key is added to frontmatter through MetaEdit's standard add path, which means: -- [Edit Mode](/reference/settings/) wrapping applies: under "All Multi", or "Some Multi" with the key listed, the value is stored as a one-element YAML list. +- [Edit Mode](/reference/settings/#edit-mode) wrapping applies: under "List", or "List for chosen properties" with the key listed, the value is stored as a one-element YAML list. - A field named `tags` or `tag` is canonicalized: the value is split on commas and whitespace, leading `#`s are stripped, and the result is stored as a proper YAML list. `tags:: #sci-fi, #book-club` becomes `tags: [sci-fi, book-club]`. :::caution[Lists do not round-trip] diff --git a/website/src/content/docs/guides/edit-properties.md b/website/src/content/docs/guides/edit-properties.md index 3392202..c4b3043 100644 --- a/website/src/content/docs/guides/edit-properties.md +++ b/website/src/content/docs/guides/edit-properties.md @@ -87,7 +87,7 @@ Every native edit is guarded so a race or a widget failure can never corrupt you - **Serialized writes.** All MetaEdit writes to one file run through a per-file queue, so concurrent operations apply in order instead of losing updates. See [how MetaEdit writes to your notes](/concepts/write-safety/). :::caution[Plain-text fallback] -Native widgets rely on Obsidian internals available from Obsidian 1.12.7, which is why MetaEdit requires that version. If a widget is unavailable anyway, the "Edit {key}" modal degrades to a plain text input with an explanatory note such as "Obsidian native property widgets are not available." You can still save, but values committed through the fallback are written as strings. +Native widgets rely on Obsidian internals available from Obsidian 1.12.7, and MetaEdit requires Obsidian 1.13.4 or newer. If a widget is unavailable anyway, the "Edit {key}" modal degrades to a plain text input with an explanatory note such as "Obsidian native property widgets are not available." You can still save, but values committed through the fallback are written as strings. ::: ## The text prompt diff --git a/website/src/content/docs/guides/edit-tags.md b/website/src/content/docs/guides/edit-tags.md index 34c2b65..27ec8ec 100644 --- a/website/src/content/docs/guides/edit-tags.md +++ b/website/src/content/docs/guides/edit-tags.md @@ -28,7 +28,7 @@ The picker footer states the scope: "#tag - rename in this note · vault-wide: T Body tag rows never show the "Delete property" or "Transform to YAML ⇄ Dataview" icons. Those actions operate on a `key:` line, which a body tag does not have, so 1.9.0 removed them from tag rows rather than let them silently no-op or mangle the tag. See [delete and transform properties](/guides/delete-and-transform/) for where they do apply. ::: -To hide body tag rows from the picker entirely, enable "Hide file tags" under the "Edit Meta menu" section in settings - the frontmatter `tags:` property stays editable. See the [settings reference](/reference/settings/). +To hide body tag rows from the picker entirely, turn on "Hide properties" in the "Menu" settings group, then enable "Hide file tags" - the frontmatter `tags:` property stays editable. See the [settings reference](/reference/settings/). ## Choose an action diff --git a/website/src/content/docs/guides/kanban-helper.md b/website/src/content/docs/guides/kanban-helper.md index ec16d77..b497cc2 100644 --- a/website/src/content/docs/guides/kanban-helper.md +++ b/website/src/content/docs/guides/kanban-helper.md @@ -17,16 +17,14 @@ For a full worked setup, see the [Kanban status sync recipe](/cookbook/kanban-st ## Set it up 1. Open Settings and go to the MetaEdit tab. -2. Turn on the "Kanban Board Helper" toggle. It takes effect immediately. -3. Click the row's gear button to expand the configuration. -4. In the text input at the bottom, type or pick a board. The autocomplete suggests every note with a `kanban-plugin` frontmatter key, filtering as you type. Click "Add". -5. In the new table row, type the property key to sync into the "Property in link" field (for example `status`). The read-only "Possible values" column previews the board's current lane headings. +2. Turn on the "Kanban board helper" toggle. It takes effect immediately. +3. Click the "Boards" row to open its page. +4. Click the "+" button ("Add board") and pick a board. The picker lists every note with a `kanban-plugin` frontmatter key that you have not added yet. +5. In the board's row, type the property key to sync into the "Property in linked notes" field (for example `status`). The "Lanes:" line under the board name previews the board's current lane headings. -![Kanban Board Helper settings panel with a configured row: board Editorial calendar, status in the Property in link column, and the lane names Ideas, Drafting, Published in the Possible values column, above the board-name input and Add button](../../../assets/media/kanban-helper-panel.png) +![The "Boards" settings page with a board named Editorial calendar, "Lanes: Ideas, Drafting, Published" under its name, and status in its property field](../../../assets/media/kanban-helper-panel.png) -:::tip[Pick from the suggestions] -"Add" only works when the typed text exactly matches a suggested board name - otherwise it silently does nothing. Selecting a suggestion fills the input with the exact name. Boards you have already added are also silently ignored. -::: +If every board is already added, or the vault has none, a notice reads "MetaEdit found no other Kanban boards in this vault." Changes save immediately. From now on, moving a card writes the new lane's heading text into the configured property of the note the card links to. @@ -76,8 +74,7 @@ This is a warning - nothing is broken. Fix it by adding the property to the note ## Caveats -- **Renaming a board file breaks its row.** Boards are matched by file basename, and the stored name is not updated on rename. Remove the stale row and re-add the board under its new name. A row whose board file no longer exists shows "FILE NOT FOUND" in its "Possible values" column. -- **The board list is computed when the settings tab renders.** A board created while the tab is open will not appear in the suggestions until you reopen the tab. +- **Renaming a board file breaks its row.** Boards are matched by file basename, and the stored name is not updated on rename. Remove the stale row and re-add the board under its new name. A row whose board file no longer exists shows "Board not found." under its name. - **Per-card failures never abort the board.** One unresolvable link or a linked note with malformed YAML is logged to the developer console, and the rest of the cards still sync. - **Property names match exactly and case-sensitively**, like everywhere else in MetaEdit. diff --git a/website/src/content/docs/guides/lists-and-multi-values.md b/website/src/content/docs/guides/lists-and-multi-values.md index b7cb5f3..c940916 100644 --- a/website/src/content/docs/guides/lists-and-multi-values.md +++ b/website/src/content/docs/guides/lists-and-multi-values.md @@ -5,7 +5,7 @@ sidebar: order: 3 --- -Multi-value data flows through three surfaces in MetaEdit: real YAML lists open Obsidian's native list widget, frontmatter tags open Obsidian's tags widget, and everything else - inline Dataview fields, plain scalars - can opt into an element-by-element list editor through the "Edit Mode" setting. This page walks through all three and the guarantees that keep your list elements intact. +Multi-value data flows through three surfaces in MetaEdit: real YAML lists open Obsidian's native list widget, frontmatter tags open Obsidian's tags widget, and everything else - inline Dataview fields, plain scalars - can opt into an element-by-element list editor through the "Edit mode" setting. This page walks through all three and the guarantees that keep your list elements intact. ## Real YAML lists: the native list widget @@ -34,23 +34,23 @@ Body `#tags` are a different feature entirely - see [edit tags](/guides/edit-tag ## Everything else: the list editor and Edit Mode -Values that are not native-editable YAML - inline Dataview fields and nested YAML scalars - are single values by default, edited through a plain text prompt. The "Edit Mode" setting lets you treat them as lists instead: when it applies, selecting the property opens MetaEdit's element-by-element list editor rather than the single prompt. +Values that are not native-editable YAML - inline Dataview fields and nested YAML scalars - are single values by default, edited through a plain text prompt. The "Edit mode" setting lets you treat them as lists instead: when it applies, selecting the property opens MetaEdit's element-by-element list editor rather than the single prompt. ### The Edit Mode setting -In the MetaEdit settings tab, "Edit Mode" is a dropdown with three values: +In the "Editing" group of the MetaEdit settings tab, "Edit mode" is a dropdown with three values: | Value | Meaning | | --- | --- | -| "All Single" | Every value is edited as one value (the default) | -| "All Multi" | Every value is edited as a list | -| "Some Multi" | Everything is single except the properties you list | +| "One value" | Every value is edited as one value (the default) | +| "List" | Every value is edited as a list | +| "List for chosen properties" | Everything is one value except the properties you list | -With "Some Multi" selected, an extra button appears (tooltip: "Configure which properties are Multi."). It opens a table where you add the property names to treat as lists: +With "List for chosen properties" selected, a "List properties" row appears below the dropdown. Click it to open its page, then use the "+" button ("Add property") to add each property name to treat as a list. The "x" on a row removes it. -![The Edit Mode section in settings with the dropdown set to Some Multi and the Property table with an Add button visible](../../../assets/media/edit-mode-some-multi.png) +![The Editing group with "Edit mode" set to "List for chosen properties" and the "List properties" row below it](../../../assets/media/edit-mode-some-multi.png) -Name matching is exact and case-sensitive: `genres` does not match `Genres`. Changes save immediately, and the list persists even while another mode is selected. See the [settings reference](/reference/settings/) for the full section. +Name matching is exact and case-sensitive: `genres` does not match `Genres`. Changes save immediately, and the list persists even while another mode is selected. See the [settings reference](/reference/settings/#edit-mode) for the full section. ### Using the list editor @@ -66,7 +66,7 @@ Picking an add option opens a prompt titled "Enter a new value"; picking an exis ### What Edit Mode also governs -Beyond routing edits, Edit Mode shapes new values on the legacy add paths. When the mode is "All Multi", or "Some Multi" with the property listed, a scalar value written through those paths is wrapped into a one-element YAML list: +Beyond routing edits, Edit Mode shapes new values on the legacy add paths. When the mode is "List", or "List for chosen properties" with the property listed, a scalar value written through those paths is wrapped into a one-element YAML list: - ["Transform to YAML ⇄ Dataview"](/guides/delete-and-transform/) when converting an inline field into frontmatter. - The API's [`createYamlProperty`](/api/properties/). diff --git a/website/src/content/docs/guides/progress-properties.md b/website/src/content/docs/guides/progress-properties.md index 4d18238..e7210c0 100644 --- a/website/src/content/docs/guides/progress-properties.md +++ b/website/src/content/docs/guides/progress-properties.md @@ -12,12 +12,12 @@ This works for YAML frontmatter properties and [inline Dataview fields](/concept ## Set it up 1. Open Settings and go to the MetaEdit tab. -2. Turn on the "Progress Properties" toggle. It takes effect immediately, no restart needed. -3. Click the row's gear button to expand the configuration table (columns "Name" and "Type"). -4. Click "Add", type your property key into the "Property name" field, and pick a type from the dropdown: "Total Tasks", "Completed Tasks", or "Incomplete Tasks" (new rows default to "Total Tasks"). -5. Remove a row with its red cross button. Every change saves immediately. +2. Turn on the "Progress properties" toggle. It takes effect immediately, no restart needed. +3. Click the "Tracked task counts" row to open its page. +4. Click the "+" button ("Add property") and enter your property key. Then pick a count from the row's dropdown: "Total tasks", "Completed tasks", or "Incomplete tasks" (new rows default to "Total tasks"). +5. Remove a row with its "x" button. Every change saves immediately. -![Progress Properties settings panel expanded, showing a Name and Type table with rows Total set to Total Tasks, Complete set to Completed Tasks, and Incomplete set to Incomplete Tasks, each with a red remove button, and an Add button below](../../../assets/media/progress-properties-panel.png) +![The "Tracked task counts" settings page listing Total set to Total tasks, Complete set to Completed tasks, and Incomplete set to Incomplete tasks, each with a delete button, and a "+" button at the top right](../../../assets/media/progress-properties-panel.png) The property names are yours to choose - `Total`, `done`, `tasks-left`, anything. What matters is the type you pair each name with. Name matching is exact and case-sensitive: a configured `Total` will not update a note's `total`. @@ -29,15 +29,15 @@ MetaEdit only updates properties that already exist. It never creates them. Befo | Type | What it counts | | --- | --- | -| "Total Tasks" | Every checklist item in the note, whatever its marker, including nested sub-tasks | -| "Completed Tasks" | Only items checked as `[x]` or `[X]` | -| "Incomplete Tasks" | Total minus completed | +| "Total tasks" | Every checklist item in the note, whatever its marker, including nested sub-tasks | +| "Completed tasks" | Only items checked as `[x]` or `[X]` | +| "Incomplete tasks" | Total minus completed | Custom markers such as `[/]`, `[-]`, `[>]`, and `[?]` are not complete, so they count as incomplete. Counts are written as text (strings), not numbers: a count of 4 lands in frontmatter as `"4"`. ## Example: before and after -Configure three rows: `Total` as "Total Tasks", `Complete` as "Completed Tasks", `Incomplete` as "Incomplete Tasks". Then take this note: +Configure three rows: `Total` as "Total tasks", `Complete` as "Completed tasks", `Incomplete` as "Incomplete tasks". Then take this note: ```md --- @@ -90,7 +90,7 @@ Counts never update? Check these in order: 2. The name does not match exactly. Matching is case-sensitive: `total` and `Total` are different properties. 3. The note has no YAML frontmatter. Even inline-field tracking requires a frontmatter block to exist. 4. You are still within the 5-second debounce, which resets while you type. Stop editing and wait a moment. -5. The "Progress Properties" toggle is off, or no rows are configured in its table. +5. The "Progress properties" toggle is off, or "Tracked task counts" has no entries. One subtlety: a note whose body contains no list items at all is left untouched, but a note with a bullet list and zero checkboxes gets counts of 0 written to its configured properties. If something fails during an update, MetaEdit shows a notice prefixed `MetaEdit: (ERROR)` - see the [notices reference](/reference/notices/). diff --git a/website/src/content/docs/help/faq.md b/website/src/content/docs/help/faq.md index 600e2ee..f54d001 100644 --- a/website/src/content/docs/help/faq.md +++ b/website/src/content/docs/help/faq.md @@ -9,7 +9,7 @@ Quick answers, with links to the full story. For symptom-driven help, see [troub ## Does MetaEdit work on mobile? -Yes, fully. There are no desktop-only features - the property picker, native widgets, Auto Properties, bulk edit, and both automators all work on phones and tablets. The plugin requires Obsidian 1.12.7 or newer on every platform. +Yes, fully. There are no desktop-only features - the property picker, native widgets, Auto Properties, bulk edit, and both automators all work on phones and tablets. The plugin requires Obsidian 1.13.4 or newer on every platform. ## Why is there no ribbon icon? @@ -33,7 +33,7 @@ Not with `Ctrl+Z` - bulk edits are real file writes across many notes, and the " ## Which Obsidian version do I need? -MetaEdit 1.9.0 requires Obsidian 1.12.7 or newer. On older Obsidian versions the community plugin browser automatically serves you MetaEdit 1.8.4, the newest compatible release. See the [changelog](/help/changelog/#how-older-obsidian-versions-are-served). +MetaEdit requires Obsidian 1.13.4 or newer. On older Obsidian versions the community plugin browser automatically serves you the newest MetaEdit release your app can run, down to 1.8.4 on anything older than 1.12.7. See the [changelog](/help/changelog/#how-older-obsidian-versions-are-served). ## What's the difference between "New YAML property" and "New Dataview field"? @@ -45,7 +45,7 @@ MetaEdit 1.9.0 requires Obsidian 1.12.7 or newer. On older Obsidian versions the ## Are property and Auto Property name matches case-sensitive? -Yes. Everywhere MetaEdit matches names - Auto Properties, "Edit Meta menu" hidden properties, "Some Multi" lists, Progress Properties, Kanban properties - the match is exact and case-sensitive. `Status` and `status` are different keys. +Yes. Everywhere MetaEdit matches names - Auto Properties, "Hidden properties", "List properties", Progress Properties, Kanban properties - the match is exact and case-sensitive. `Status` and `status` are different keys. ## Where are MetaEdit's settings stored? diff --git a/website/src/content/docs/help/troubleshooting.md b/website/src/content/docs/help/troubleshooting.md index 1c5ce40..488a8f5 100644 --- a/website/src/content/docs/help/troubleshooting.md +++ b/website/src/content/docs/help/troubleshooting.md @@ -19,25 +19,25 @@ See [commands and menus](/reference/commands-and-menus/) for every way to open t **Cause:** one of three things. -- The "UI Elements" setting is off. It gates all of MetaEdit's context-menu items, including the two bulk edit items. +- The "Right-click menu" setting is off. It gates all of MetaEdit's context-menu items, including the two bulk edit items. - The file is not a markdown note. "Edit Meta" only appears for `.md` files. - You right-clicked somewhere MetaEdit does not attach to. The item appears on file explorer entries, internal links in notes, and Calendar plugin days - not on tab headers or editor text selections. -**Fix:** open Settings, go to the MetaEdit tab, and turn on "UI Elements" (its description reads "Toggle UI elements: the 'Edit Meta' right-click menu option.", and it also controls the bulk items). The change applies immediately, no restart needed. +**Fix:** open Settings, go to the MetaEdit tab, and turn on "Right-click menu" in the "Menu" group (its description reads "Show Edit Meta when you right-click a note or link.", and it also controls the bulk items). The change applies immediately, no restart needed. -See the [settings reference](/reference/settings/) for details. +See the [settings reference](/reference/settings/#right-click-menu) for details. ## A property is missing from the picker **Cause:** the property is being filtered or is not individually editable. -- "Edit Meta menu" filtering is enabled and the property's key is on the hidden list. Matching is exact and case-sensitive. +- "Hide properties" is on and the property's key is on the "Hidden properties" list. Matching is exact and case-sensitive. - "Hide file tags" is on, which removes body `#tag` rows from the picker. A frontmatter `tags` property stays visible and editable. - The value is a parent container - an object, or a list containing objects or lists. These are always hidden by design because they cannot be edited as one value; their nested leaves appear as individual child rows instead. -**Fix:** open Settings, go to the MetaEdit tab, and expand "Edit Meta menu". Remove the key from the hidden list, or turn the section's master toggle off - when it is off, nothing is filtered. +**Fix:** open Settings, go to the MetaEdit tab, and open "Hidden properties" in the "Menu" group. Remove the key with its "x" button, or turn "Hide properties" off - when it is off, nothing is filtered. -![The Edit Meta menu settings panel with the Hide file tags toggle and the per-name property hiding table](../../../assets/media/edit-meta-menu-panel.png) +![The Menu group with "Hide properties" on, showing the "Hide file tags" toggle and the "Hidden properties" row below it](../../../assets/media/edit-meta-menu-panel.png) :::note Filtering only hides rows in the picker. It never deletes data, and hidden-but-present keys are still excluded from new-property name suggestions so you cannot accidentally create a duplicate. @@ -49,7 +49,7 @@ See [what MetaEdit can (and can't) edit](/concepts/what-metaedit-can-edit/) for Both automators share the same gates. Work through this checklist: -1. **The toggle is on.** "Progress Properties" and "Kanban Board Helper" both default to off in the MetaEdit settings tab. +1. **The toggle is on.** "Progress properties" and "Kanban board helper" both default to off in the "Automation" group of the MetaEdit settings tab. 2. **The note has YAML frontmatter.** A note with no frontmatter block is never processed, even if the property you track is an inline Dataview field. 3. **The property already exists in the note.** Neither automator ever creates a property - both only update existing ones. Add `total: 0` (or your key) yourself first. 4. **The note is not an Excalidraw note.** Notes whose frontmatter contains any key with "excalidraw" in it are skipped to avoid fighting Excalidraw's auto-save. @@ -66,7 +66,7 @@ See [Progress Properties](/guides/progress-properties/) and the [Kanban Board He - **Only the leading link counts.** A card must be a top-level task line like `- [ ] [[Note]] ...`; trailing links on the same line (Kanban date links such as `@[[2026-07-01]]`, `see [[ref]]` mentions) and indented sub-items are ignored. - **Heading-less cards have no lane.** A card above the first heading is skipped because there is no lane name to write. - **Ambiguous basenames are refused.** If several notes share the linked basename and Obsidian's cache cannot resolve which one is meant, the helper skips that card rather than write to the wrong note. -- **A renamed board file breaks the configuration.** Boards are matched by file basename and the stored name does not follow a rename; the settings row then shows "FILE NOT FOUND" in its "Possible values" column. Re-add the board under its new name. +- **A renamed board file breaks the configuration.** Boards are matched by file basename and the stored name does not follow a rename; the board's row on the "Boards" settings page then shows "Board not found." Re-add the board under its new name. - **The linked note must already have the property.** If it is missing you get a notice per affected card: `MetaEdit: (WARNING) '' not found in "" (Kanban board '').` **Fix:** structure cards as `- [ ] [[Note]]` under a heading, keep board file names stable, and add the configured property to each linked note. Details in the [Kanban Board Helper guide](/guides/kanban-helper/). @@ -91,7 +91,7 @@ See [delete and transform properties](/guides/delete-and-transform/). **Cause:** the edit ran in fallback mode. When Obsidian's native property widgets are unavailable, MetaEdit shows a plain text input instead, preceded by a note reading "Obsidian native property widgets are not available." or "Obsidian's native `` property widget is not available." Everything committed through that text input is stored as a string. -**Fix:** on Obsidian 1.12.7 or newer (which MetaEdit 1.9.0 requires) the native widgets are normally always present, so fallback mode should be rare. If you see those notes regularly, update Obsidian. To repair an already-stringified value, edit the property again with the native widget, or fix its type in Obsidian's Properties view. +**Fix:** on Obsidian 1.13.4 or newer (which MetaEdit requires) the native widgets are normally always present, so fallback mode should be rare. If you see those notes regularly, update Obsidian. To repair an already-stringified value, edit the property again with the native widget, or fix its type in Obsidian's Properties view. See [edit properties with native widgets](/guides/edit-properties/). @@ -103,11 +103,11 @@ See [edit properties with native widgets](/guides/edit-properties/). See [bulk edit metadata across notes](/guides/bulk-edit/). -## The "Edit Meta menu" toggle turned itself on after an update +## The "Hide properties" toggle turned itself on after an update **Cause:** a one-time settings migration. Older MetaEdit versions filtered your hidden-property list even while the feature toggle was off (a bug). Now that the toggle is honored, the upgrade turns the feature on once for users who had built up a hidden list, so their picker keeps looking the way it always did. -**Fix:** nothing is broken. If you want no filtering, turn the toggle off - the migration runs only once, so it will not flip back on. See the [settings reference](/reference/settings/). +**Fix:** nothing is broken. If you want no filtering, turn the toggle off - the migration runs only once, so it will not flip back on. See the [settings reference](/reference/settings/#hide-properties). ## Open the developer console @@ -122,7 +122,7 @@ Several MetaEdit messages go to the console only: If your problem is not covered here, open an issue on [GitHub](https://github.com/chhoumann/MetaEdit/issues) and include: - Your Obsidian version (Settings, then "About") and platform (desktop or mobile, OS). -- Your MetaEdit version (Settings, then "Community plugins"). The current release is 1.9.0 and requires Obsidian 1.12.7 or newer; the [changelog](/help/changelog/) explains how older Obsidian installs are served 1.8.4. +- Your MetaEdit version (Settings, then "Community plugins"). MetaEdit requires Obsidian 1.13.4 or newer; the [changelog](/help/changelog/#how-older-obsidian-versions-are-served) explains how older Obsidian installs are served an older release. - Exact steps to reproduce, including how you triggered MetaEdit ("MetaEdit: Run", "Edit Meta", or a bulk item) and the relevant note content (frontmatter, inline fields, tags). - The exact text of any notice, plus anything in the developer console. -- Relevant settings: Edit Mode, Auto Properties, "Edit Meta menu" filtering, and automator toggles. +- Relevant settings: "Edit mode", "Auto properties", "Hide properties", and the automator toggles. diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index ae93a29..0e92534 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -76,4 +76,4 @@ value widget switches to the right type on its own. ![Creating a rating property: the key autocompletes, and the widget switches to Number automatically](../../assets/media/fluid-property-create.gif) -

Requires Obsidian 1.12.7 or newer · Desktop and mobile · Free and open source

+

Requires Obsidian 1.13.4 or newer · Desktop and mobile · Free and open source

diff --git a/website/src/content/docs/reference/commands-and-menus.md b/website/src/content/docs/reference/commands-and-menus.md index a40c303..87b80e4 100644 --- a/website/src/content/docs/reference/commands-and-menus.md +++ b/website/src/content/docs/reference/commands-and-menus.md @@ -48,7 +48,7 @@ Collects every `.md` file in the folder, recursing into subfolders, and starts t Select multiple files or folders in the file explorer (Ctrl/Cmd-click or Shift-click), then right-click the selection. Selected folders are expanded recursively, non-markdown files are ignored, and duplicates are edited once. The scope label in the first prompt is the note's name when exactly one note resolves, otherwise ` selected items`. :::caution[All three items share one toggle] -The ["UI Elements" setting](/reference/settings/#ui-elements) gates all three context-menu items, even though its description only mentions the "Edit Meta" option: "Toggle UI elements: the 'Edit Meta' right-click menu option." Turning it off removes the bulk items too. The change takes effect immediately, no restart needed. +The ["Right-click menu" setting](/reference/settings/#right-click-menu) gates all three context-menu items, even though its description only mentions "Edit Meta": "Show Edit Meta when you right-click a note or link." Turning it off removes the bulk items too. The change takes effect immediately, no restart needed. ::: ## The property picker @@ -67,7 +67,7 @@ When the same body tag appears more than once, each occurrence gets its own row, ### Rows that are hidden -- Properties filtered by the ["Edit Meta menu" settings](/reference/settings/#edit-meta-menu): exact-name matches from the hide list, and all body-tag rows when "Hide file tags" is on. Nothing is filtered while that feature's master toggle is off, and filtering never deletes data. +- Properties filtered by the ["Hide properties" setting](/reference/settings/#hide-properties): exact-name matches from the "Hidden properties" list, and all body-tag rows when "Hide file tags" is on. Nothing is filtered while "Hide properties" is off, and filtering never deletes data. - YAML values that are parent containers (an object, or a list containing objects or lists) are always hidden - they cannot be edited as a single value. ### Row actions diff --git a/website/src/content/docs/reference/notices.md b/website/src/content/docs/reference/notices.md index cbdb756..2d2038e 100644 --- a/website/src/content/docs/reference/notices.md +++ b/website/src/content/docs/reference/notices.md @@ -38,7 +38,7 @@ See [create properties](/guides/create-properties/). The "New property" modal also shows two inline warnings under the key field (not popups); the "Add" button is disabled while either shows: - `"{key}" is a reserved property name and can't be used.` -- `This note already has a property named "{key}".` - includes keys hidden by the ["Edit Meta menu" filter](/reference/settings/#edit-meta-menu). +- `This note already has a property named "{key}".` - includes keys hidden by the ["Hide properties" setting](/reference/settings/#hide-properties). ## Delete and transform @@ -76,8 +76,17 @@ See [Auto Properties](/guides/auto-properties/). | Notice | When it appears | What to do | | --- | --- | --- | -| `MetaEdit could not save the Auto Property choice: {reason}` | Saving a newly typed value into the Auto Property's choice list failed. The value was still written to your note - only remembering it as a choice failed. | Add the value manually under [Settings, "Auto Properties"](/reference/settings/#auto-properties). | -| `MetaEdit could not save the Auto Properties setting: {reason}` | An edit in the Auto Properties settings panel failed to persist and was rolled back. | Retry the edit; check for disk or sync problems if it keeps failing. | +| `MetaEdit could not save the Auto Property choice: {reason}` | Saving a newly typed value into the Auto Property's choice list failed. The value was still written to your note - only remembering it as a choice failed. | Add the value manually on the ["Preset values"](/reference/settings/#preset-values) settings page. | +| `MetaEdit could not save the Auto Properties setting: {reason}` | An edit on the "Preset values" settings page failed to persist and was rolled back. | Retry the edit; check for disk or sync problems if it keeps failing. | + +## Settings + +See the [settings reference](/reference/settings/). + +| Notice | When it appears | What to do | +| --- | --- | --- | +| `MetaEdit could not save the setting: {reason}` | A settings change failed to write to `data.json`. | Retry the change; check for disk or sync problems if it keeps failing. | +| `MetaEdit found no other Kanban boards in this vault.` | You clicked "Add board" on the "Boards" page, but every note with a `kanban-plugin` frontmatter key is already added, or the vault has none. | Create the board with the Kanban plugin first, or check that its frontmatter has the `kanban-plugin` key. | ## Progress Properties and Kanban Board Helper @@ -86,7 +95,6 @@ See [Progress Properties](/guides/progress-properties/) and the [Kanban Board He | Notice | When it appears | What to do | | --- | --- | --- | | `MetaEdit: (WARNING) '{property}' not found in "{note}" (Kanban board '{board}').` | A card changed lanes, but the linked note does not contain the configured property. The helper only updates existing properties - it never creates them. | Add the property to the note (any value), then move the card again. | -| `MetaEdit: (WARNING) file {board} not found.` | A configured board file no longer exists - the settings panel also shows `FILE NOT FOUND` in its "Possible values" column. Boards are matched by file basename, so renaming the board file breaks the row. | Remove the row and re-add the board under its new name. | | `MetaEdit: (ERROR) {message}` | A Progress Properties update failed while writing task counts. | Check the note's frontmatter parses (Obsidian shows invalid frontmatter in the editor), then modify the note again to re-trigger the count. | ## Reserved property names diff --git a/website/src/content/docs/reference/settings.md b/website/src/content/docs/reference/settings.md index 05e9e8e..67cbbad 100644 --- a/website/src/content/docs/reference/settings.md +++ b/website/src/content/docs/reference/settings.md @@ -5,55 +5,68 @@ sidebar: order: 2 --- -This page documents every MetaEdit setting in the fixed order the tab shows them, with each setting's exact name, description, default, and what its configuration panel contains. Open them under Settings, in the "MetaEdit" tab, headed "MetaEdit Settings". +This page documents every MetaEdit setting in the order the tab shows them, with each setting's exact name, description, default, and what its sub-page contains. Open them under Settings, in the "MetaEdit" tab. The tab has three groups: "Editing", "Menu", and "Automation". -![The MetaEdit Settings tab with all six sections visible: Progress Properties, Auto Properties, Edit Meta menu, Edit Mode, Kanban Board Helper, UI Elements](../../../assets/media/settings-tab.png) +![The MetaEdit settings tab with its three groups: Editing, Menu, and Automation](../../../assets/media/settings-tab.png) -Two things apply everywhere: +Three things apply everywhere: -- **Changes save immediately.** There is no Save button anywhere in the tab, and writes are serialized. Edits inside the Auto Properties panel additionally roll back and show a notice if the save fails. -- **Panels open inline.** Every setting except "UI Elements" and "Edit Mode" has an extra icon button on its row (the "Edit Meta menu" description calls it "the gear") that expands a configuration panel below the toggle; "Edit Mode" shows its button only while "Some Multi" is selected. Panels start collapsed. +- **Changes save immediately.** There is no Save button anywhere in the tab, and writes are serialized. If a save fails, a notice appears: "MetaEdit could not save the setting: {reason}". Edits on the "Preset values" page also roll back when their save fails. +- **Sub-pages open in place.** A row with a count on the right is a sub-page. Click it to open its settings page, and use the back arrow to return. A feature's sub-page row shows only while that feature is on. +- **Settings search finds every MetaEdit setting.** -### Defaults at a glance +## Defaults at a glance | Setting | Default | Takes effect | | --- | --- | --- | -| "Progress Properties" | Off | Immediately (attaches the automator live) | -| "Auto Properties" | Off | Immediately | -| "Edit Meta menu" | Off | Next time the picker opens | -| "Edit Mode" | "All Single" | Immediately | -| "Kanban Board Helper" | Off | Immediately (attaches the automator live) | -| "UI Elements" | On | Immediately (registers or unregisters the menus live) | +| "Edit mode" | "One value" | Immediately | +| "Auto properties" | Off | Immediately | +| "Right-click menu" | On | Immediately (registers or unregisters the menus live) | +| "Hide properties" | Off | Next time the picker opens | +| "Hide file tags" | Off | Next time the picker opens | +| "Progress properties" | Off | Immediately (attaches the automator live) | +| "Kanban board helper" | Off | Immediately (attaches the automator live) | -"UI Elements" is the only setting that defaults to enabled. +"Right-click menu" is the only setting that defaults to on. -## Progress Properties +## Editing -- **Description:** "Update properties automatically." -- **Toggle tooltip:** "Toggle Progress Properties". Default off. +### Edit mode -Enables the [Progress Properties](/guides/progress-properties/) automator, which keeps task-count properties in your notes up to date. Toggling attaches or detaches the automator immediately - no reload needed. +- **Description:** "Whether property values are saved as one value or as a list." +- **Dropdown values:** "One value" (default), "List", "List for chosen properties". + +Controls whether property values are edited (and created, on the legacy paths) as single values or as lists. See [lists and multi-values](/guides/lists-and-multi-values/) for the full routing rules. + +![The Editing group with "Edit mode" set to "List for chosen properties" and the "List properties" row below it](../../../assets/media/edit-mode-some-multi.png) + +:::note[Where Edit mode applies] +Edit mode does not apply to YAML properties edited or created with Obsidian's native widgets - it governs inline Dataview fields, non-native YAML scalars, and the legacy add paths (transform to YAML, the API's `createYamlProperty`, and [bulk edit](/guides/bulk-edit/)). YAML lists are inherently multi-value whatever the mode says: a real top-level frontmatter list opens Obsidian's native list widget, and a non-native YAML list (for example a nested array) always uses the legacy list editor - see [work with lists and multi-value properties](/guides/lists-and-multi-values/). An [Auto Property](/guides/auto-properties/) with an explicit "Single" or "Multi" type also overrides Edit mode for its key. +::: -![The Progress Properties panel expanded, showing a Name/Type table with rows mapping Total, Complete, and Incomplete to property names, red X removers, and an Add button](../../../assets/media/progress-properties-panel.png) +### List properties -The panel is a table with columns "Name" and "Type". Each row pairs: +A sub-page row, shown only while "Edit mode" is "List for chosen properties". It lists the properties treated as lists. Everything else is one value. -- a text input (placeholder "Property name") for the property key in your notes, -- a dropdown with exactly three types: "Total Tasks", "Completed Tasks", "Incomplete Tasks", -- a "❌" button that removes the row. +- The "+" button ("Add property") asks for a property name. Blank names and names already in the list are ignored. +- The "x" on a row deletes it. -The "Add" button appends a new row with the type defaulting to "Total Tasks". New rows start with an empty name and nothing validates the name before saving, so fill it in right away. Property names match exactly and case-sensitively, and the automator only updates properties that already exist in a note - it never creates them. +Name matching is exact and case-sensitive. Choosing "One value" or "List" hides the row, but the stored list persists across mode switches - it comes back when you return to "List for chosen properties". -## Auto Properties +### Auto properties -- **Description:** "Quick switch for values you know the value of." -- **Toggle tooltip:** "Toggle Auto Properties". Default off. +- **Description:** "Pick from preset values when you edit these properties." +- **Default:** off. Enables [Auto Properties](/guides/auto-properties/): predefined value sets that replace free-text prompts when you edit or create a matching property. While the toggle is off, no Auto Property activates anywhere, including the `autoprop()` [API call](/api/auto-properties/). -![The Auto Properties panel with two property cards: "status" as Single with a description and four values, and "genres" as Multi](../../../assets/media/autoprop-settings.png) +### Preset values -With no properties defined, the panel shows: "No auto properties yet. Add one to define a reusable set of values for a property." Each auto property renders as a card containing: +A sub-page row, shown only while "Auto properties" is on. Its count is the number of auto properties defined. + +![The "Preset values" page with two property cards: "status" as Single with a description and four values, and "genres" as Multi](../../../assets/media/autoprop-settings.png) + +With no properties defined, the page shows: "No auto properties yet. Add one to define a reusable set of values for a property." Each auto property renders as a card containing: | Control | Details | | --- | --- | @@ -64,25 +77,29 @@ With no properties defined, the panel shows: "No auto properties yet. Add one to | "Values" list | One text input per choice, placeholder "Value (or paste a list)", each with an x "Remove value" button | | "Add value" | Appends an empty value box | -At the bottom of the panel, "Add auto property" appends a new card. There is no reorder control; new items always append at the end. +At the bottom of the page, "Add auto property" appends a new card. There is no reorder control; new items always append at the end. **Paste a whole list:** pasting into a value box splits the text into individual choices - on lines when the text contains any line break, otherwise on commas. Tokens are trimmed, blanks dropped, and duplicates skipped. A paste that yields fewer than two tokens behaves like a normal paste. If a save fails, the change is rolled back and a notice appears: "MetaEdit could not save the Auto Properties setting: {reason}". -## Edit Meta menu +## Menu + +### Right-click menu -- **Description:** "Control what the 'Edit Meta' menu lists. Enable it, then use the gear to hide specific properties by name or all of a note's file tags." -- **Toggle tooltip:** "Toggle menu filtering". Default off. +- **Description:** "Show Edit Meta when you right-click a note or link." +- **Default:** on. -Filters which rows the [property picker](/reference/commands-and-menus/#the-property-picker) lists. While the master toggle is off, nothing is filtered - the gear panel only exists while the toggle is on. +Gates all three context-menu items - "Edit Meta" plus both bulk edit items - even though the description mentions only "Edit Meta". Turning it off unregisters the menu handlers immediately; turning it on brings them back, no restart needed. See [commands and menus](/reference/commands-and-menus/#context-menu-items). -![The Edit Meta menu panel expanded, with the Hide file tags toggle and the "Hide specific properties by name:" table](../../../assets/media/edit-meta-menu-panel.png) +### Hide properties -The panel contains two controls: +- **Description:** "Leave chosen properties out of the property menu." +- **Default:** off. -- **"Hide file tags"** (tooltip "Toggle hiding file tags", default off). Description: "Hide the note's #tags from the menu, leaving only frontmatter and inline fields. A frontmatter 'tags' property stays editable." It removes only body `#tag` rows from the picker. -- **"Hide specific properties by name:"** - a table of property names (placeholder "Property name", "❌" removers, "Add" button). A property is hidden when its key exactly matches a listed name; matching is case-sensitive, with no wildcards. +Filters which rows the [property picker](/reference/commands-and-menus/#the-property-picker) lists. While the toggle is off, nothing is filtered, and the "Hide file tags" and "Hidden properties" rows are hidden. + +![The Menu group with "Hide properties" on, showing the "Hide file tags" toggle and the "Hidden properties" row below it](../../../assets/media/edit-meta-menu-panel.png) Filtering only hides picker rows - it never deletes anything from your notes. Hidden-but-present keys are also excluded from the "New YAML property" and "New Dataview field" name suggestions, so you cannot accidentally re-create a key the note already has. @@ -90,44 +107,60 @@ Filtering only hides picker rows - it never deletes anything from your notes. Hi This feature was previously named "Ignored Properties", and its storage key in `data.json` is still `IgnoredProperties`. Old versions applied the ignore list even while the feature toggle was off, so on first load after upgrading, a vault with the feature disabled but a non-empty ignore list gets the toggle switched on once, automatically. That preserves your previous filtering; turn it off afterwards if you no longer want it, and it will stay off. ::: -## Edit Mode +### Hide file tags -- **Description:** "Single: property values are just one value. Multi: properties are arrays. Some Multi: all options are single, except those specified in the settings (click button)." -- **Dropdown values:** "All Single" (default), "All Multi", "Some Multi". +- **Description:** "A frontmatter tags property stays in the menu." +- **Default:** off. Shown only while "Hide properties" is on. -Controls whether property values are edited (and created, on the legacy paths) as single values or as lists. See [lists and multi-values](/guides/lists-and-multi-values/) for the full routing rules. +Removes the note's body `#tag` rows from the picker, leaving only frontmatter and inline fields. A frontmatter `tags` property stays editable. -![The Edit Mode section with the dropdown set to Some Multi and the Property table with an Add button visible](../../../assets/media/edit-mode-some-multi.png) +### Hidden properties -While "Some Multi" is selected, an extra button appears (tooltip "Configure which properties are Multi."). It opens a table of property names (placeholder "Property name", "❌" removers, "Add") listing the properties treated as Multi. Choosing "All Single" or "All Multi" hides the button and the panel, but the stored list persists across mode switches - it comes back when you return to "Some Multi". +A sub-page row, shown only while "Hide properties" is on. It lists the property names to hide from the picker. -:::note[Where Edit Mode applies] -Edit Mode does not apply to YAML properties edited or created with Obsidian's native widgets - it governs inline Dataview fields, non-native YAML scalars, and the legacy add paths (transform to YAML, the API's `createYamlProperty`, and [bulk edit](/guides/bulk-edit/)). YAML lists are inherently multi-value whatever the mode says: a real top-level frontmatter list opens Obsidian's native list widget, and a non-native YAML list (for example a nested array) always uses the legacy list editor - see [work with lists and multi-value properties](/guides/lists-and-multi-values/). An [Auto Property](/guides/auto-properties/) with an explicit "Single" or "Multi" type also overrides Edit Mode for its key. -::: +- The "+" button ("Add property") asks for a property name. Blank names and names already in the list are ignored. +- The "x" on a row deletes it. -## Kanban Board Helper +A property is hidden when its key exactly matches a listed name. Matching is case-sensitive, with no wildcards. -- **Description:** "Update properties in links in kanban boards automatically when a card is moved to a new lane." -- **Toggle tooltip:** "Toggle Kanban Helper". Default off. +## Automation -Enables the [Kanban Board Helper](/guides/kanban-helper/) automator. Toggling attaches or detaches it immediately. +### Progress properties + +- **Description:** "Keep task counts in a note's properties up to date." +- **Default:** off. + +Enables the [Progress Properties](/guides/progress-properties/) automator, which keeps task-count properties in your notes up to date. Toggling attaches or detaches the automator immediately - no reload needed. + +### Tracked task counts -![The Kanban Board Helper panel with a configured board row: Board "Editorial calendar", Property in link "status", Possible values "Ideas, Drafting, Published", and the board-name input with Add button](../../../assets/media/kanban-helper-panel.png) +A sub-page row, shown only while "Progress properties" is on. Each entry pairs a property name with the count it holds. -The panel is a table with columns "Board", "Property in link", and "Possible values" (plus an unlabeled remove column): +![The "Tracked task counts" page listing Total, Complete, and Incomplete, each with its task-count dropdown](../../../assets/media/progress-properties-panel.png) -- **"Board"** - the board file's basename. -- **"Property in link"** - a text input (placeholder "Property name") for the property to update in the notes the board's cards link to. -- **"Possible values"** - read-only; the board's lane headings joined by ", ", or "FILE NOT FOUND" if the configured board file no longer exists (a "file {name} not found." warning also appears). +- The "+" button ("Add property") asks for the property name. Blank names and names already in the list are ignored. New entries default to "Total tasks". +- Each row has a dropdown with exactly three counts: "Total tasks", "Completed tasks", "Incomplete tasks". +- The "x" on a row deletes it. + +Property names match exactly and case-sensitively, and the automator only updates properties that already exist in a note - it never creates them. In `data.json` the counts are stored as `Total Tasks`, `Completed Tasks`, and `Incomplete Tasks`. + +### Kanban board helper + +- **Description:** "When a card moves to another lane, update a property in the note it links to." +- **Default:** off. + +Enables the [Kanban Board Helper](/guides/kanban-helper/) automator. Toggling attaches or detaches it immediately. -Below the table, a text input with autocomplete suggests Kanban boards - markdown files whose frontmatter contains a `kanban-plugin` key, in practice boards created by the community Kanban plugin. The "Add" button adds a row only when the typed text exactly matches a suggested board basename that is not already configured; otherwise it silently does nothing. The board list is computed when the settings tab renders, so boards created afterwards need the tab reopened. +### Boards -## UI Elements +A sub-page row, shown only while "Kanban board helper" is on. Each entry is one board. -- **Description:** "Toggle UI elements: the 'Edit Meta' right-click menu option." -- **Toggle tooltip:** "Toggle UI elements". Default on. +![The "Boards" page with a board named Editorial calendar, "Lanes: Ideas, Drafting, Published" under its name, and "status" in its property field](../../../assets/media/kanban-helper-panel.png) -The only setting without a panel. It gates all three context-menu items - "Edit Meta" plus both bulk edit items - even though the description mentions only "Edit Meta". Turning it off unregisters the menu handlers immediately; turning it on brings them back, no restart needed. See [commands and menus](/reference/commands-and-menus/#context-menu-items). +- The "+" button ("Add board") opens a picker of Kanban boards that are not added yet: markdown files whose frontmatter contains a `kanban-plugin` key, in practice boards created by the community Kanban plugin. The picker reads your vault when you click, so it includes boards created while the tab is open. If there are none, a notice appears: "MetaEdit found no other Kanban boards in this vault." +- Each row shows the board file's basename, with "Lanes: {headings}" under it listing the board's lane headings. A board without headings shows "No lanes yet.", and a board file that no longer exists shows "Board not found." +- Each row has a text field (placeholder "Property in linked notes") for the property to update in the notes the board's cards link to. +- The "x" on a row deletes it. ## Related pages