From fbf9728f8bddcf3a4d2b8284537441f8b8bdf989 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 20 Feb 2023 13:02:40 +0100 Subject: [PATCH 1/9] refactor: add feature migrations makes it easier to migrate user settings to support new features --- src/global.d.ts | 9 ++ src/main.ts | 35 +------- src/migrations/migrate.ts | 44 +++++++++ .../migrateToMacroIDFromEmbeddedMacro.ts | 41 +++++++++ src/migrations/useQuickAddTemplateFolder.ts | 46 ++++++++++ src/quickAddSettingsTab.ts | 90 ++++++++++++++----- 6 files changed, 212 insertions(+), 53 deletions(-) create mode 100644 src/migrations/migrate.ts create mode 100644 src/migrations/migrateToMacroIDFromEmbeddedMacro.ts create mode 100644 src/migrations/useQuickAddTemplateFolder.ts diff --git a/src/global.d.ts b/src/global.d.ts index 06cfe67..98b647c 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -11,5 +11,14 @@ declare module "obsidian" { enablePlugin: (id: string) => Promise; disablePlugin: (id: string) => Promise; }; + internalPlugins: { + plugins: { + [pluginId: string]: Plugin & { + [pluginImplementations: string]: any; + }; + }; + enablePlugin: (id: string) => Promise; + disablePlugin: (id: string) => Promise; + }; } } diff --git a/src/main.ts b/src/main.ts index fac37aa..8a274ef 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,9 +10,9 @@ import { ChoiceExecutor } from "./choiceExecutor"; import type IChoice from "./types/choices/IChoice"; import type IMultiChoice from "./types/choices/IMultiChoice"; import { deleteObsidianCommand } from "./utility"; -import type IMacroChoice from "./types/choices/IMacroChoice"; import ChoiceSuggester from "./gui/suggesters/choiceSuggester"; import { QuickAddApi } from "./quickAddApi"; +import migrate from "./migrations/migrate"; export default class QuickAdd extends Plugin { static instance: QuickAdd; @@ -90,7 +90,7 @@ export default class QuickAdd extends Plugin { ); this.addCommandsForChoices(this.settings.choices); - await this.convertMacroChoicesMacroToId(); + migrate(this); } onunload() { @@ -164,35 +164,4 @@ export default class QuickAdd extends Plugin { public removeCommandForChoice(choice: IChoice) { deleteObsidianCommand(this.app, `quickadd:choice:${choice.id}`); } - - // Did not make sense to have copies of macros in the choices when they are maintained for themselves. - // Instead we reference by id now. Have to port this over for all users. - private async convertMacroChoicesMacroToId() { - function convertMacroChoiceMacroToIdHelper(choice: IChoice): IChoice { - if (choice.type === ChoiceType.Multi) { - let multiChoice = choice as IMultiChoice; - const multiChoices = multiChoice.choices.map( - convertMacroChoiceMacroToIdHelper - ); - multiChoice = { ...multiChoice, choices: multiChoices }; - return multiChoice; - } - - if (choice.type !== ChoiceType.Macro) return choice; - const macroChoice = choice as IMacroChoice; - - if (macroChoice.macro) { - macroChoice.macroId = macroChoice.macro.id; - delete macroChoice.macro; - } - - return macroChoice; - } - - this.settings.choices = this.settings.choices.map( - convertMacroChoiceMacroToIdHelper - ); - - await this.saveSettings(); - } } diff --git a/src/migrations/migrate.ts b/src/migrations/migrate.ts new file mode 100644 index 0000000..099febb --- /dev/null +++ b/src/migrations/migrate.ts @@ -0,0 +1,44 @@ +import { log } from "src/logger/logManager"; +import QuickAdd from "src/main"; +import type { QuickAddSettings } from "src/quickAddSettingsTab"; +import migrateToMacroIDFromEmbeddedMacro from "./migrateToMacroIDFromEmbeddedMacro"; +import useQuickAddTemplateFolder from "./useQuickAddTemplateFolder"; + +type Migrations = { + [key in keyof QuickAddSettings["migrations"]]: { + description: string; + migrate: (plugin: QuickAdd) => Promise; + }; +}; + +const migrations = { + migrateToMacroIDFromEmbeddedMacro, + useQuickAddTemplateFolder, +}; + +async function migrate(plugin: QuickAdd): Promise { + const migrationsToRun = Object.keys(migrations).filter( + (migration: keyof Migrations) => !plugin.settings.migrations[migration] + ); + + // Could batch-run with Promise.all, but we want to log each migration as it runs. + for (const migration of migrationsToRun as (keyof Migrations)[]) { + log.logMessage( + `Running migration ${migration}: ${migrations[migration].description}` + ); + + const success = await migrations[migration].migrate(plugin); + + plugin.settings.migrations[migration] = success; + + if (success) { + log.logMessage(`Migration ${migration} successful.`); + } else { + log.logError(`Migration ${migration} failed.`); + } + } + + plugin.saveSettings(); +} + +export default migrate; diff --git a/src/migrations/migrateToMacroIDFromEmbeddedMacro.ts b/src/migrations/migrateToMacroIDFromEmbeddedMacro.ts new file mode 100644 index 0000000..ef8215e --- /dev/null +++ b/src/migrations/migrateToMacroIDFromEmbeddedMacro.ts @@ -0,0 +1,41 @@ +import QuickAdd from "src/main"; +import { ChoiceType } from "src/types/choices/choiceType"; +import IChoice from "src/types/choices/IChoice"; +import IMacroChoice from "src/types/choices/IMacroChoice"; +import IMultiChoice from "src/types/choices/IMultiChoice"; + +export default { + description: "Migrate to macro ID from embedded macro in macro choices.", + migrate: async (plugin: QuickAdd) => { + // Did not make sense to have copies of macros in the choices when they are maintained for themselves. + // Instead we reference by id now. Have to port this over for all users. + function convertMacroChoiceMacroToIdHelper(choice: IChoice): IChoice { + if (choice.type === ChoiceType.Multi) { + let multiChoice = choice as IMultiChoice; + const multiChoices = multiChoice.choices.map( + convertMacroChoiceMacroToIdHelper + ); + multiChoice = { ...multiChoice, choices: multiChoices }; + return multiChoice; + } + + if (choice.type !== ChoiceType.Macro) return choice; + const macroChoice = choice as IMacroChoice; + + if (macroChoice.macro) { + macroChoice.macroId = macroChoice.macro.id; + delete macroChoice.macro; + } + + return macroChoice; + } + + plugin.settings.choices = plugin.settings.choices.map( + convertMacroChoiceMacroToIdHelper + ); + + await plugin.saveSettings(); + + return true; + }, +}; diff --git a/src/migrations/useQuickAddTemplateFolder.ts b/src/migrations/useQuickAddTemplateFolder.ts new file mode 100644 index 0000000..4f4abd5 --- /dev/null +++ b/src/migrations/useQuickAddTemplateFolder.ts @@ -0,0 +1,46 @@ +import { log } from "src/logger/logManager"; +import QuickAdd from "src/main"; + +export default { + description: + "Use QuickAdd template folder instead of Obsidian templates plugin folder / Templater templates folder.", + migrate: async (plugin: QuickAdd): Promise => { + try { + const templaterPlugin = app.plugins.plugins["templater"]; + const obsidianTemplatesPlugin = + app.internalPlugins.plugins["templates"]; + + if (!templaterPlugin && !obsidianTemplatesPlugin) { + log.logMessage("No template plugin found. Skipping migration."); + return true; + } + + if (obsidianTemplatesPlugin) { + const obsidianTemplatesSettings = + obsidianTemplatesPlugin.instance.options; + if (obsidianTemplatesSettings["folder"]) { + plugin.settings.templateFolderPath = + obsidianTemplatesSettings["folder"]; + + log.logMessage("Migrated template folder path to Obsidian Templates' setting."); + } + } + + if (templaterPlugin) { + const templaterSettings = templaterPlugin.settings; + if (templaterSettings["template_folder"]) { + plugin.settings.templateFolderPath = + templaterSettings["template_folder"]; + + log.logMessage("Migrated template folder path to Templaters setting."); + } + } + + return true; + } catch (error) { + log.logError("Failed to migrate template folder path."); + + return false; + } + }, +}; diff --git a/src/quickAddSettingsTab.ts b/src/quickAddSettingsTab.ts index 9d4318b..f3d0df6 100644 --- a/src/quickAddSettingsTab.ts +++ b/src/quickAddSettingsTab.ts @@ -1,14 +1,25 @@ -import { App, PluginSettingTab, Setting } from "obsidian"; +import { + App, + PluginSettingTab, + Setting, + TFolder, +} from "obsidian"; import type QuickAdd from "./main"; import type IChoice from "./types/choices/IChoice"; import ChoiceView from "./gui/choiceList/ChoiceView.svelte"; import type { IMacro } from "./types/macros/IMacro"; +import { GenericTextSuggester } from "./gui/suggesters/genericTextSuggester"; export interface QuickAddSettings { choices: IChoice[]; macros: IMacro[]; inputPrompt: "multi-line" | "single-line"; devMode: boolean; + templateFolderPath: string; + migrations: { + migrateToMacroIDFromEmbeddedMacro: boolean, + useQuickAddTemplateFolder: boolean, + } } export const DEFAULT_SETTINGS: QuickAddSettings = { @@ -16,6 +27,11 @@ export const DEFAULT_SETTINGS: QuickAddSettings = { macros: [], inputPrompt: "single-line", devMode: false, + templateFolderPath: "", + migrations: { + migrateToMacroIDFromEmbeddedMacro: false, + useQuickAddTemplateFolder: false, + } }; export class QuickAddSettingsTab extends PluginSettingTab { @@ -33,25 +49,8 @@ export class QuickAddSettingsTab extends PluginSettingTab { containerEl.createEl("h2", { text: "QuickAdd Settings" }); this.addChoicesSetting(); - new Setting(this.containerEl) - .setName("Use Multi-line Input Prompt") - .setDesc( - "Use multi-line input prompt instead of single-line input prompt" - ) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.inputPrompt === "multi-line") - .setTooltip("Use multi-line input prompt") - .onChange((value) => { - if (value) { - this.plugin.settings.inputPrompt = "multi-line"; - } else { - this.plugin.settings.inputPrompt = "single-line"; - } - - this.plugin.saveSettings(); - }) - ); + this.addUseMultiLineInputPromptSetting(); + this.addTemplateFolderPathSetting(); } hide(): any { @@ -81,4 +80,55 @@ export class QuickAddSettingsTab extends PluginSettingTab { }, }); } + + private addUseMultiLineInputPromptSetting() { + new Setting(this.containerEl) + .setName("Use Multi-line Input Prompt") + .setDesc( + "Use multi-line input prompt instead of single-line input prompt" + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.inputPrompt === "multi-line") + .setTooltip("Use multi-line input prompt") + .onChange((value) => { + if (value) { + this.plugin.settings.inputPrompt = "multi-line"; + } else { + this.plugin.settings.inputPrompt = "single-line"; + } + + this.plugin.saveSettings(); + }) + ); + } + + private addTemplateFolderPathSetting() { + const setting = new Setting(this.containerEl); + + setting.setName("Template Folder Path"); + setting.setDesc( + "Path to the folder where templates are stored." + ); + + setting.addText((text) => { + text.setPlaceholder( + "Leave blank to use default Obsidian templates folder" + ) + .setValue(this.plugin.settings.templateFolderPath) + .onChange(async (value) => { + this.plugin.settings.templateFolderPath = value; + await this.plugin.saveSettings(); + }); + + new GenericTextSuggester( + app, + text.inputEl, + app.vault + .getAllLoadedFiles() + .filter((f) => f instanceof TFolder && f.path !== '/') + .map((f) => f.path) + ); + }); + } } From 7768b77e427ad76354c5c13fe8c4fac9ac2ff19b Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 20 Feb 2023 14:16:18 +0100 Subject: [PATCH 2/9] feat: user can now set template folder path so QA suggests templates when configuring choices --- src/gui/ChoiceBuilder/captureChoiceBuilder.ts | 6 +-- .../ChoiceBuilder/templateChoiceBuilder.ts | 21 ++++++---- src/gui/suggesters/formatSyntaxSuggester.ts | 4 +- src/main.ts | 10 ++++- src/quickAddSettingsTab.ts | 4 +- src/utility.ts | 41 +------------------ 6 files changed, 28 insertions(+), 58 deletions(-) diff --git a/src/gui/ChoiceBuilder/captureChoiceBuilder.ts b/src/gui/ChoiceBuilder/captureChoiceBuilder.ts index bb3cc90..9d35f07 100644 --- a/src/gui/ChoiceBuilder/captureChoiceBuilder.ts +++ b/src/gui/ChoiceBuilder/captureChoiceBuilder.ts @@ -11,12 +11,10 @@ import { CREATE_IF_NOT_FOUND_BOTTOM, CREATE_IF_NOT_FOUND_TOP, FILE_NAME_FORMAT_SYNTAX, - FORMAT_SYNTAX, } from "../../constants"; import { FormatDisplayFormatter } from "../../formatters/formatDisplayFormatter"; import type QuickAdd from "../../main"; import { FileNameDisplayFormatter } from "../../formatters/fileNameDisplayFormatter"; -import { getTemplatePaths } from "../../utility"; import { NewTabDirection } from "../../types/newTabDirection"; import type { FileViewMode } from "../../types/fileViewMode"; import { GenericTextSuggester } from "../suggesters/genericTextSuggester"; @@ -350,11 +348,11 @@ export class CaptureChoiceBuilder extends ChoiceBuilder { templateSelector.inputEl.style.width = "100%"; templateSelector.inputEl.style.marginBottom = "8px"; - const markdownFiles: string[] = getTemplatePaths(this.app); + const templateFilePaths: string[] = this.plugin.getTemplateFiles().map(f => f.path); new GenericTextSuggester( this.app, templateSelector.inputEl, - markdownFiles + templateFilePaths ); templateSelector.onChange((value) => { diff --git a/src/gui/ChoiceBuilder/templateChoiceBuilder.ts b/src/gui/ChoiceBuilder/templateChoiceBuilder.ts index 9c65d68..fce1844 100644 --- a/src/gui/ChoiceBuilder/templateChoiceBuilder.ts +++ b/src/gui/ChoiceBuilder/templateChoiceBuilder.ts @@ -11,7 +11,7 @@ import { NewTabDirection } from "../../types/newTabDirection"; import FolderList from "./FolderList.svelte"; import { FileNameDisplayFormatter } from "../../formatters/fileNameDisplayFormatter"; import { log } from "../../logger/logManager"; -import { getAllFolderPathsInVault, getTemplatePaths } from "../../utility"; +import { getAllFolderPathsInVault } from "../../utility"; import type QuickAdd from "../../main"; import type { FileViewMode } from "../../types/fileViewMode"; import { GenericTextSuggester } from "../suggesters/genericTextSuggester"; @@ -45,7 +45,7 @@ export class TemplateChoiceBuilder extends ChoiceBuilder { .setName("Template Path") .setDesc("Path to the Template.") .addSearch((search) => { - const templates: string[] = getTemplatePaths(this.app); + const templates: string[] = this.plugin.getTemplateFiles().map(f => f.path); search.setValue(this.choice.templatePath); search.setPlaceholder("Template path"); @@ -148,13 +148,16 @@ export class TemplateChoiceBuilder extends ChoiceBuilder { const stn = new Setting(chooseFolderFromSubfolderContainer); stn.setName("Include subfolders") - .setDesc("Get prompted to choose from both the selected folders and their subfolders when creating the note.") - .addToggle((toggle) => toggle - .setValue(this.choice.folder?.chooseFromSubfolders) - .onChange((value) => { - this.choice.folder.chooseFromSubfolders = value; - this.reload(); - }) + .setDesc( + "Get prompted to choose from both the selected folders and their subfolders when creating the note." + ) + .addToggle((toggle) => + toggle + .setValue(this.choice.folder?.chooseFromSubfolders) + .onChange((value) => { + this.choice.folder.chooseFromSubfolders = value; + this.reload(); + }) ); } diff --git a/src/gui/suggesters/formatSyntaxSuggester.ts b/src/gui/suggesters/formatSyntaxSuggester.ts index 644a27e..fb39310 100644 --- a/src/gui/suggesters/formatSyntaxSuggester.ts +++ b/src/gui/suggesters/formatSyntaxSuggester.ts @@ -17,7 +17,6 @@ import { VARIABLE_DATE_SYNTAX_SUGGEST_REGEX, VARIABLE_SYNTAX_SUGGEST_REGEX, } from "../../constants"; -import { getTemplatePaths } from "../../utility"; import type QuickAdd from "../../main"; enum FormatSyntaxToken { @@ -50,7 +49,8 @@ export class FormatSyntaxSuggester extends TextInputSuggest { this.macroNames = this.plugin.settings.macros.map( (macro) => macro.name ); - this.templatePaths = getTemplatePaths(this.app); + + this.templatePaths = this.plugin.getTemplateFiles().map((file) => file.path); } getSuggestions(inputStr: string): string[] { diff --git a/src/main.ts b/src/main.ts index 8a274ef..b73e57c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { MarkdownView, Plugin } from "obsidian"; +import { MarkdownView, Plugin, TFile } from "obsidian"; import { DEFAULT_SETTINGS, QuickAddSettingsTab } from "./quickAddSettingsTab"; import type { QuickAddSettings } from "./quickAddSettingsTab"; import { log } from "./logger/logManager"; @@ -164,4 +164,12 @@ export default class QuickAdd extends Plugin { public removeCommandForChoice(choice: IChoice) { deleteObsidianCommand(this.app, `quickadd:choice:${choice.id}`); } + + public getTemplateFiles(): TFile[] { + if (!String.isString(this.settings.templateFolderPath)) return []; + + return this.app.vault.getFiles().filter((file) => + file.path.startsWith(this.settings.templateFolderPath) + ); + } } diff --git a/src/quickAddSettingsTab.ts b/src/quickAddSettingsTab.ts index f3d0df6..22b5a25 100644 --- a/src/quickAddSettingsTab.ts +++ b/src/quickAddSettingsTab.ts @@ -108,12 +108,12 @@ export class QuickAddSettingsTab extends PluginSettingTab { setting.setName("Template Folder Path"); setting.setDesc( - "Path to the folder where templates are stored." + "Path to the folder where templates are stored. Used to suggest template files when configuring QuickAdd." ); setting.addText((text) => { text.setPlaceholder( - "Leave blank to use default Obsidian templates folder" + "templates/" ) .setValue(this.plugin.settings.templateFolderPath) .onChange(async (value) => { diff --git a/src/utility.ts b/src/utility.ts index c044d38..19fc40a 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -10,6 +10,7 @@ import { CaptureChoice } from "./types/choices/CaptureChoice"; import { MacroChoice } from "./types/choices/MacroChoice"; import IChoice from "./types/choices/IChoice"; import { ChoiceType } from "./types/choices/choiceType"; +import QuickAdd from "./main"; export function getTemplater(app: App) { return app.plugins.plugins["templater-obsidian"]; @@ -48,46 +49,6 @@ export async function templaterParseTemplate( ); } -function getCoreTemplatesPath(app: App) { - // @ts-ignore - const internalTemplatePlugin = app.internalPlugins.plugins.templates; - if (internalTemplatePlugin) { - const templateFolderPath = - internalTemplatePlugin.instance.options.folder; - if (templateFolderPath) return templateFolderPath; - } -} - -function getTemplaterTemplatesPath(app: App) { - const templater = getTemplater(app); - if (templater) { - const templateFolderPath = templater.settings["template_folder"]; - if (templateFolderPath) return templateFolderPath; - } -} - -export function getTemplateFiles(app: App): TFile[] { - let templateFiles: Set = new Set(); - const markdownFiles = app.vault.getMarkdownFiles(); - - const coreTemplatesPath = getCoreTemplatesPath(app); - const templaterTemplatesPath = getTemplaterTemplatesPath(app); - - markdownFiles.forEach((file) => { - if ( - file.path.contains(coreTemplatesPath) || - file.path.contains(templaterTemplatesPath) - ) - templateFiles.add(file); - }); - - return [...templateFiles]; -} - -export function getTemplatePaths(app: App): string[] { - return getTemplateFiles(app).map((file) => file.path); -} - export function getNaturalLanguageDates(app: App) { // @ts-ignore return app.plugins.plugins["nldates-obsidian"]; From 1b2e5d786630f75c51976a5c97aa0696a5b2cc34 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 20 Feb 2023 14:33:42 +0100 Subject: [PATCH 3/9] refactor: increase error notice display time --- src/logger/guiLogger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger/guiLogger.ts b/src/logger/guiLogger.ts index cd82730..5dd1757 100644 --- a/src/logger/guiLogger.ts +++ b/src/logger/guiLogger.ts @@ -10,7 +10,7 @@ export class GuiLogger extends QuickAddLogger { logError(msg: string): void { const error = this.getQuickAddError(msg, ErrorLevel.Error); - new Notice(this.formatOutputString(error)); + new Notice(this.formatOutputString(error), 15000); } logWarning(msg: string): void { From 63eed06654cdab59aab53c1b76e2ed7c882edea4 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 20 Feb 2023 14:34:16 +0100 Subject: [PATCH 4/9] fix: better error logging when using template choices --- src/engine/TemplateEngine.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/engine/TemplateEngine.ts b/src/engine/TemplateEngine.ts index afe808f..978b4e2 100644 --- a/src/engine/TemplateEngine.ts +++ b/src/engine/TemplateEngine.ts @@ -108,7 +108,7 @@ export abstract class TemplateEngine extends QuickAddEngine { return createdFile; } catch (e) { log.logError( - `Could not create file with template. Maybe '${templatePath}' is an invalid template path?` + `Could not create file with template: \n\n${e.message}` ); return null; } @@ -171,8 +171,11 @@ export abstract class TemplateEngine extends QuickAddEngine { const templateFile = this.app.vault.getAbstractFileByPath(correctTemplatePath); + if (!(templateFile instanceof TFile)) - throw new Error("Template file not found."); + throw new Error( + `Template file not found at path "${correctTemplatePath}".` + ); return await this.app.vault.cachedRead(templateFile); } From 2be9054f5e0e5aca42725321172d4f71657160bd Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 20 Feb 2023 14:35:36 +0100 Subject: [PATCH 5/9] refactor: remove console.log --- src/gui/MacroGUIs/MacroBuilder.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gui/MacroGUIs/MacroBuilder.ts b/src/gui/MacroGUIs/MacroBuilder.ts index 445e026..4640ebd 100644 --- a/src/gui/MacroGUIs/MacroBuilder.ts +++ b/src/gui/MacroGUIs/MacroBuilder.ts @@ -320,7 +320,6 @@ export class MacroBuilder extends Modal { private addCommandList() { const commandList = this.contentEl.createDiv("commandList"); - console.log(this.macro.commands); this.commandListEl = new CommandList({ target: commandList, From bf6d405c72ed8dc84bc9be56d7681301b819ae8d Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 20 Feb 2023 14:47:20 +0100 Subject: [PATCH 6/9] refactor: merge increment file name setting into setting for default if file exists increment file name setting has now been merged into the setting for default behavior when a file exists. --- src/constants.ts | 16 ++-- src/engine/TemplateChoiceEngine.ts | 16 ++-- .../ChoiceBuilder/templateChoiceBuilder.ts | 41 ++++----- src/migrations/Migrations.ts | 11 +++ ...entFileNameSettingMoveToDefaultBehavior.ts | 89 +++++++++++++++++++ src/migrations/migrate.ts | 13 +-- src/quickAddSettingsTab.ts | 2 + src/types/choices/ITemplateChoice.ts | 5 +- src/types/choices/TemplateChoice.ts | 8 +- 9 files changed, 151 insertions(+), 50 deletions(-) create mode 100644 src/migrations/Migrations.ts create mode 100644 src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts diff --git a/src/constants.ts b/src/constants.ts index 446abdd..d4042a8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -102,17 +102,19 @@ export const TITLE_SYNTAX_SUGGEST_REGEX: RegExp = new RegExp( ); // == File Exists (Template Choice) == // -export const fileExistsAppendToBottom: string = - "Append to the bottom of the file"; -export const fileExistsAppendToTop: string = "Append to the top of the file"; -export const fileExistsOverwriteFile: string = "Overwrite the file"; -export const fileExistsDoNothing: string = "Nothing"; -export const fileExistsChoices: string[] = [ +export const fileExistsIncrement = "Increment the file name" as const; +export const fileExistsAppendToBottom = + "Append to the bottom of the file" as const; +export const fileExistsAppendToTop = "Append to the top of the file" as const; +export const fileExistsOverwriteFile = "Overwrite the file" as const; +export const fileExistsDoNothing = "Nothing" as const; +export const fileExistsChoices = [ fileExistsAppendToBottom, fileExistsAppendToTop, fileExistsOverwriteFile, + fileExistsIncrement, fileExistsDoNothing, -]; +] as const; // == MISC == // export const WIKI_LINK_REGEX: RegExp = new RegExp(/\[\[([^\]]*)\]\]/); diff --git a/src/engine/TemplateChoiceEngine.ts b/src/engine/TemplateChoiceEngine.ts index c4f8deb..226281a 100644 --- a/src/engine/TemplateChoiceEngine.ts +++ b/src/engine/TemplateChoiceEngine.ts @@ -13,6 +13,7 @@ import { fileExistsChoices, fileExistsOverwriteFile, VALUE_SYNTAX, + fileExistsIncrement, } from "../constants"; import { log } from "../logger/logManager"; import type QuickAdd from "../main"; @@ -56,7 +57,7 @@ export class TemplateChoiceEngine extends TemplateEngine { ); } - if (this.choice.incrementFileName) + if (this.choice.fileExistsMode === fileExistsIncrement) filePath = await this.incrementFileName(filePath); let createdFile: TFile | null; @@ -71,14 +72,15 @@ export class TemplateChoiceEngine extends TemplateEngine { await this.app.workspace.getLeaf("tab").openFile(file); - let userChoice = this.choice.fileExistsMode; + let userChoice: typeof fileExistsChoices[number] = + this.choice.fileExistsMode; - if(!this.choice.setFileExistsBehavior) { - userChoice = await GenericSuggester.Suggest( + if (!this.choice.setFileExistsBehavior) { + userChoice = (await GenericSuggester.Suggest( this.app, - fileExistsChoices, - fileExistsChoices - ); + [...fileExistsChoices], + [...fileExistsChoices] + )) as typeof fileExistsChoices[number]; } switch (userChoice) { diff --git a/src/gui/ChoiceBuilder/templateChoiceBuilder.ts b/src/gui/ChoiceBuilder/templateChoiceBuilder.ts index e8a65d3..e226258 100644 --- a/src/gui/ChoiceBuilder/templateChoiceBuilder.ts +++ b/src/gui/ChoiceBuilder/templateChoiceBuilder.ts @@ -17,7 +17,14 @@ import type { FileViewMode } from "../../types/fileViewMode"; import { GenericTextSuggester } from "../suggesters/genericTextSuggester"; import { FormatSyntaxSuggester } from "../suggesters/formatSyntaxSuggester"; import { ExclusiveSuggester } from "../suggesters/exclusiveSuggester"; -import { fileExistsAppendToBottom, fileExistsAppendToTop, fileExistsDoNothing, fileExistsOverwriteFile } from "src/constants"; +import { + fileExistsAppendToBottom, + fileExistsAppendToTop, + fileExistsChoices, + fileExistsDoNothing, + fileExistsIncrement, + fileExistsOverwriteFile, +} from "src/constants"; export class TemplateChoiceBuilder extends ChoiceBuilder { choice: ITemplateChoice; @@ -36,7 +43,6 @@ export class TemplateChoiceBuilder extends ChoiceBuilder { this.addFileNameFormatSetting(); this.addFolderSetting(); this.addAppendLinkSetting(); - this.addIncrementFileNameSetting(); this.addFileAlreadyExistsSetting(); this.addOpenFileSetting(); if (this.choice.openFile) this.addOpenFileInNewTabSetting(); @@ -47,7 +53,9 @@ export class TemplateChoiceBuilder extends ChoiceBuilder { .setName("Template Path") .setDesc("Path to the Template.") .addSearch((search) => { - const templates: string[] = this.plugin.getTemplateFiles().map(f => f.path); + const templates: string[] = this.plugin + .getTemplateFiles() + .map((f) => f.path); search.setValue(this.choice.templatePath); search.setPlaceholder("Template path"); @@ -263,24 +271,13 @@ export class TemplateChoiceBuilder extends ChoiceBuilder { }); } - private addIncrementFileNameSetting(): void { - const incrementFileNameSetting: Setting = new Setting(this.contentEl); - incrementFileNameSetting - .setName("Increment file name") - .setDesc("If the file already exists, increment the file name.") - .addToggle((toggle) => { - toggle.setValue(this.choice.incrementFileName); - toggle.onChange( - (value) => (this.choice.incrementFileName = value) - ); - }); - } - private addFileAlreadyExistsSetting(): void { const fileAlreadyExistsSetting: Setting = new Setting(this.contentEl); fileAlreadyExistsSetting .setName("Set default behavior if file already exists") - .setDesc("Set default behavior rather then prompting what to do if a file already exists set the default behavior.") + .setDesc( + "Set default behavior rather then prompting what to do if a file already exists set the default behavior." + ) .addToggle((toggle) => { toggle.setValue(this.choice.setFileExistsBehavior); toggle.onChange((value) => { @@ -294,14 +291,18 @@ export class TemplateChoiceBuilder extends ChoiceBuilder { this.choice.fileExistsMode = fileExistsDoNothing; dropdown - .addOption(fileExistsAppendToBottom, fileExistsAppendToBottom) + .addOption( + fileExistsAppendToBottom, + fileExistsAppendToBottom + ) .addOption(fileExistsAppendToTop, fileExistsAppendToTop) .addOption(fileExistsOverwriteFile, fileExistsOverwriteFile) .addOption(fileExistsDoNothing, fileExistsDoNothing) + .addOption(fileExistsIncrement, fileExistsIncrement) .setValue(this.choice.fileExistsMode) .onChange( - (value) => - (this.choice.fileExistsMode = value as string) + (value: typeof fileExistsChoices[number]) => + (this.choice.fileExistsMode = value) ); }); } diff --git a/src/migrations/Migrations.ts b/src/migrations/Migrations.ts new file mode 100644 index 0000000..4674ef7 --- /dev/null +++ b/src/migrations/Migrations.ts @@ -0,0 +1,11 @@ +import QuickAdd from "src/main"; +import { QuickAddSettings } from "src/quickAddSettingsTab"; + +export type Migration = { + description: string; + migrate: (plugin: QuickAdd) => Promise; +}; + +export type Migrations = { + [key in keyof QuickAddSettings["migrations"]]: Migration; +}; diff --git a/src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts b/src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts new file mode 100644 index 0000000..48579f7 --- /dev/null +++ b/src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts @@ -0,0 +1,89 @@ +import QuickAdd from "src/main"; +import IChoice from "src/types/choices/IChoice"; +import { MultiChoice } from "src/types/choices/MultiChoice"; +import { TemplateChoice } from "src/types/choices/TemplateChoice"; +import { NestedChoiceCommand } from "src/types/macros/QuickCommands/NestedChoiceCommand"; +import { IMacro } from "src/types/macros/IMacro"; +import { Migration } from "./Migrations"; +import { ChoiceType } from "src/types/choices/choiceType"; + +type OldTemplateChoice = TemplateChoice & { incrementFileName?: boolean }; + +function isOldTemplateChoice( + choice: any +): choice is OldTemplateChoice { + for (const key in choice) { + if (key === "incrementFileName") { + return true; + } + } + + return false; +} + +function isMultiChoice( + choice: any +): choice is MultiChoice { + return choice.type === ChoiceType.Multi && choice.choices !== undefined; +} + +function recursiveRemoveIncrementFileName( + choices: IChoice[] +): IChoice[] { + for (const choice of choices) { + if (isMultiChoice(choice)) { + choice.choices = recursiveRemoveIncrementFileName(choice.choices); + } + + if (isOldTemplateChoice(choice)) { + choice.setFileExistsBehavior = true; + choice.fileExistsMode = "Increment the file name"; + + delete choice.incrementFileName; + } + } + + return choices; +} + +function isNestedChoiceCommand( + command: any +): command is NestedChoiceCommand { + return command.choice !== undefined; +} + +function removeIncrementFileName( + macros: IMacro[] +): IMacro[] { + for (const macro of macros) { + for (const command of macro.commands) { + if (isNestedChoiceCommand(command) && isOldTemplateChoice(command.choice)) { + command.choice.setFileExistsBehavior = true; + command.choice.fileExistsMode = "Increment the file name"; + + delete command.choice.incrementFileName; + } + } + } + + return macros; +} + +const incrementFileNameSettingMoveToDefaultBehavior: Migration = { + description: + "'Increment file name' setting moved to 'Set default behavior if file already exists' setting", + migrate: async (plugin: QuickAdd): Promise => { + const choicesCopy = structuredClone(plugin.settings.choices); + const choices = recursiveRemoveIncrementFileName(choicesCopy); + + const macrosCopy = structuredClone(plugin.settings.macros); + const macros = removeIncrementFileName(macrosCopy); + + plugin.settings.choices = structuredClone(choices); + plugin.settings.macros = structuredClone(macros); + + return true; + }, +}; + +export default incrementFileNameSettingMoveToDefaultBehavior; diff --git a/src/migrations/migrate.ts b/src/migrations/migrate.ts index 099febb..68db7f1 100644 --- a/src/migrations/migrate.ts +++ b/src/migrations/migrate.ts @@ -1,19 +1,14 @@ import { log } from "src/logger/logManager"; import QuickAdd from "src/main"; -import type { QuickAddSettings } from "src/quickAddSettingsTab"; +import { Migrations } from "./Migrations"; import migrateToMacroIDFromEmbeddedMacro from "./migrateToMacroIDFromEmbeddedMacro"; import useQuickAddTemplateFolder from "./useQuickAddTemplateFolder"; +import incrementFileNameSettingMoveToDefaultBehavior from "./incrementFileNameSettingMoveToDefaultBehavior"; -type Migrations = { - [key in keyof QuickAddSettings["migrations"]]: { - description: string; - migrate: (plugin: QuickAdd) => Promise; - }; -}; - -const migrations = { +const migrations: Migrations = { migrateToMacroIDFromEmbeddedMacro, useQuickAddTemplateFolder, + incrementFileNameSettingMoveToDefaultBehavior }; async function migrate(plugin: QuickAdd): Promise { diff --git a/src/quickAddSettingsTab.ts b/src/quickAddSettingsTab.ts index 22b5a25..b9391ab 100644 --- a/src/quickAddSettingsTab.ts +++ b/src/quickAddSettingsTab.ts @@ -19,6 +19,7 @@ export interface QuickAddSettings { migrations: { migrateToMacroIDFromEmbeddedMacro: boolean, useQuickAddTemplateFolder: boolean, + incrementFileNameSettingMoveToDefaultBehavior: boolean; } } @@ -31,6 +32,7 @@ export const DEFAULT_SETTINGS: QuickAddSettings = { migrations: { migrateToMacroIDFromEmbeddedMacro: false, useQuickAddTemplateFolder: false, + incrementFileNameSettingMoveToDefaultBehavior: false, } }; diff --git a/src/types/choices/ITemplateChoice.ts b/src/types/choices/ITemplateChoice.ts index 494bca9..fbe4d44 100644 --- a/src/types/choices/ITemplateChoice.ts +++ b/src/types/choices/ITemplateChoice.ts @@ -1,7 +1,7 @@ import type IChoice from "./IChoice"; import type { NewTabDirection } from "../newTabDirection"; import type { FileViewMode } from "../fileViewMode"; -import {fileExistsAppendToBottom} from "../../constants"; +import { fileExistsChoices } from "src/constants"; export default interface ITemplateChoice extends IChoice { templatePath: string; @@ -14,7 +14,6 @@ export default interface ITemplateChoice extends IChoice { }; fileNameFormat: { enabled: boolean; format: string }; appendLink: boolean; - incrementFileName: boolean; openFile: boolean; openFileInNewTab: { enabled: boolean; @@ -22,6 +21,6 @@ export default interface ITemplateChoice extends IChoice { focus: boolean; }; openFileInMode: FileViewMode; - fileExistsMode: string; + fileExistsMode: typeof fileExistsChoices[number]; setFileExistsBehavior: boolean; } diff --git a/src/types/choices/TemplateChoice.ts b/src/types/choices/TemplateChoice.ts index fc79076..9035150 100644 --- a/src/types/choices/TemplateChoice.ts +++ b/src/types/choices/TemplateChoice.ts @@ -3,6 +3,7 @@ import type ITemplateChoice from "./ITemplateChoice"; import { Choice } from "./Choice"; import { NewTabDirection } from "../newTabDirection"; import type { FileViewMode } from "../fileViewMode"; +import { fileExistsChoices } from "src/constants"; export class TemplateChoice extends Choice implements ITemplateChoice { appendLink: boolean; @@ -14,7 +15,6 @@ export class TemplateChoice extends Choice implements ITemplateChoice { createInSameFolderAsActiveFile: boolean; chooseFromSubfolders: boolean; }; - incrementFileName: boolean; openFileInNewTab: { enabled: boolean; direction: NewTabDirection; @@ -23,10 +23,9 @@ export class TemplateChoice extends Choice implements ITemplateChoice { openFile: boolean; openFileInMode: FileViewMode; templatePath: string; - fileExistsMode: string; + fileExistsMode: typeof fileExistsChoices[number]; setFileExistsBehavior: boolean; - constructor(name: string) { super(name, ChoiceType.Template); @@ -40,7 +39,6 @@ export class TemplateChoice extends Choice implements ITemplateChoice { chooseFromSubfolders: false, }; this.appendLink = false; - this.incrementFileName = false; this.openFileInNewTab = { enabled: false, direction: NewTabDirection.vertical, @@ -48,6 +46,8 @@ export class TemplateChoice extends Choice implements ITemplateChoice { }; this.openFile = false; this.openFileInMode = "default"; + this.fileExistsMode = "Increment the file name"; + this.setFileExistsBehavior = false; } public static Load(choice: ITemplateChoice): TemplateChoice { From 6629525829b77c2bfd4341616f1f8d3ddaa28f7f Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Tue, 21 Feb 2023 07:28:38 +0100 Subject: [PATCH 7/9] fix: change order of dropdown --- src/gui/ChoiceBuilder/templateChoiceBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/ChoiceBuilder/templateChoiceBuilder.ts b/src/gui/ChoiceBuilder/templateChoiceBuilder.ts index e226258..06de111 100644 --- a/src/gui/ChoiceBuilder/templateChoiceBuilder.ts +++ b/src/gui/ChoiceBuilder/templateChoiceBuilder.ts @@ -296,9 +296,9 @@ export class TemplateChoiceBuilder extends ChoiceBuilder { fileExistsAppendToBottom ) .addOption(fileExistsAppendToTop, fileExistsAppendToTop) + .addOption(fileExistsIncrement, fileExistsIncrement) .addOption(fileExistsOverwriteFile, fileExistsOverwriteFile) .addOption(fileExistsDoNothing, fileExistsDoNothing) - .addOption(fileExistsIncrement, fileExistsIncrement) .setValue(this.choice.fileExistsMode) .onChange( (value: typeof fileExistsChoices[number]) => From 9b4a1c212d2af739b51655ceae03d5eb0cec24d1 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Tue, 21 Feb 2023 08:33:52 +0100 Subject: [PATCH 8/9] refactor: do backups when running migrations to recover from in case of errors While the aim is for errors to be _very_ rare, they can occur. So we're making backups to prevent data loss. --- src/migrations/Migrations.ts | 2 +- ...entFileNameSettingMoveToDefaultBehavior.ts | 4 +- src/migrations/migrate.ts | 60 +++++++++++++++++-- .../migrateToMacroIDFromEmbeddedMacro.ts | 2 - src/migrations/useQuickAddTemplateFolder.ts | 9 ++- 5 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/migrations/Migrations.ts b/src/migrations/Migrations.ts index 4674ef7..55e3b6b 100644 --- a/src/migrations/Migrations.ts +++ b/src/migrations/Migrations.ts @@ -3,7 +3,7 @@ import { QuickAddSettings } from "src/quickAddSettingsTab"; export type Migration = { description: string; - migrate: (plugin: QuickAdd) => Promise; + migrate: (plugin: QuickAdd) => Promise; }; export type Migrations = { diff --git a/src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts b/src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts index 48579f7..69103ec 100644 --- a/src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts +++ b/src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts @@ -72,7 +72,7 @@ function removeIncrementFileName( const incrementFileNameSettingMoveToDefaultBehavior: Migration = { description: "'Increment file name' setting moved to 'Set default behavior if file already exists' setting", - migrate: async (plugin: QuickAdd): Promise => { + migrate: async (plugin: QuickAdd): Promise => { const choicesCopy = structuredClone(plugin.settings.choices); const choices = recursiveRemoveIncrementFileName(choicesCopy); @@ -81,8 +81,6 @@ const incrementFileNameSettingMoveToDefaultBehavior: Migration = { plugin.settings.choices = structuredClone(choices); plugin.settings.macros = structuredClone(macros); - - return true; }, }; diff --git a/src/migrations/migrate.ts b/src/migrations/migrate.ts index 68db7f1..9b68633 100644 --- a/src/migrations/migrate.ts +++ b/src/migrations/migrate.ts @@ -4,32 +4,80 @@ import { Migrations } from "./Migrations"; import migrateToMacroIDFromEmbeddedMacro from "./migrateToMacroIDFromEmbeddedMacro"; import useQuickAddTemplateFolder from "./useQuickAddTemplateFolder"; import incrementFileNameSettingMoveToDefaultBehavior from "./incrementFileNameSettingMoveToDefaultBehavior"; +import { moment } from "obsidian"; const migrations: Migrations = { migrateToMacroIDFromEmbeddedMacro, useQuickAddTemplateFolder, - incrementFileNameSettingMoveToDefaultBehavior + incrementFileNameSettingMoveToDefaultBehavior, }; +const backupFolderPath = ".obsidian/plugins/quickadd/backup"; + +const getBackupPath = (backupName: string): string => + `${backupFolderPath}/${moment().format( + "DD-MM-YY_HH-mm-ss" + )}_${backupName}.json`; + +// Unfortunately, we cannot use 'app.vault.getAbstractFileByPath' here, because it doesn't seem to index files in the .obsidian folder. +async function makeBackupFolderIfNotExists() { + try { + await app.vault.createFolder(backupFolderPath); + } catch (error) { + if (!error.message?.includes("Folder already exists")) { + throw error; + } + } +} + async function migrate(plugin: QuickAdd): Promise { const migrationsToRun = Object.keys(migrations).filter( (migration: keyof Migrations) => !plugin.settings.migrations[migration] ); + if (migrationsToRun.length === 0) { + log.logMessage("No migrations to run."); + + return; + } + + try { + await makeBackupFolderIfNotExists(); + + const backup = structuredClone(plugin.settings); + + await app.vault.create( + getBackupPath("preMigrationBackup"), + JSON.stringify(backup) + ); + } catch (error) { + log.logError( + `Unable to create backup before migrating to new version. Please create an issue with the following error message: \n\n${error}\n\nYour data is still safe! QuickAdd won't proceed without backup.` + ); + + return; + } + // Could batch-run with Promise.all, but we want to log each migration as it runs. for (const migration of migrationsToRun as (keyof Migrations)[]) { log.logMessage( `Running migration ${migration}: ${migrations[migration].description}` ); - const success = await migrations[migration].migrate(plugin); + const backup = structuredClone(plugin.settings); - plugin.settings.migrations[migration] = success; + try { + await migrations[migration].migrate(plugin); + + plugin.settings.migrations[migration] = true; - if (success) { log.logMessage(`Migration ${migration} successful.`); - } else { - log.logError(`Migration ${migration} failed.`); + } catch (error) { + log.logError( + `Migration '${migration}' was unsuccessful. Please create an issue with the following error message: \n\n${error}\n\nQuickAdd will now revert to backup. You can also find a backup in the QuickAdd backup folder: "${backupFolderPath}"` + ); + + plugin.settings = backup; } } diff --git a/src/migrations/migrateToMacroIDFromEmbeddedMacro.ts b/src/migrations/migrateToMacroIDFromEmbeddedMacro.ts index ef8215e..17e32c7 100644 --- a/src/migrations/migrateToMacroIDFromEmbeddedMacro.ts +++ b/src/migrations/migrateToMacroIDFromEmbeddedMacro.ts @@ -35,7 +35,5 @@ export default { ); await plugin.saveSettings(); - - return true; }, }; diff --git a/src/migrations/useQuickAddTemplateFolder.ts b/src/migrations/useQuickAddTemplateFolder.ts index 4f4abd5..0f06678 100644 --- a/src/migrations/useQuickAddTemplateFolder.ts +++ b/src/migrations/useQuickAddTemplateFolder.ts @@ -4,7 +4,7 @@ import QuickAdd from "src/main"; export default { description: "Use QuickAdd template folder instead of Obsidian templates plugin folder / Templater templates folder.", - migrate: async (plugin: QuickAdd): Promise => { + migrate: async (plugin: QuickAdd): Promise => { try { const templaterPlugin = app.plugins.plugins["templater"]; const obsidianTemplatesPlugin = @@ -12,7 +12,8 @@ export default { if (!templaterPlugin && !obsidianTemplatesPlugin) { log.logMessage("No template plugin found. Skipping migration."); - return true; + + return; } if (obsidianTemplatesPlugin) { @@ -35,12 +36,10 @@ export default { log.logMessage("Migrated template folder path to Templaters setting."); } } - - return true; } catch (error) { log.logError("Failed to migrate template folder path."); - return false; + throw error; } }, }; From fcbb8ee91e233a2a511934e6198bd67beef098f1 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Tue, 21 Feb 2023 09:13:38 +0100 Subject: [PATCH 9/9] fix: fix issue where choices with commands wouldn't update choices bound to Obsidian commands (in Command Palette) are now remade on update, so the command always has the latest choice settings fix #347 fix #62 --- src/gui/choiceList/ChoiceView.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gui/choiceList/ChoiceView.svelte b/src/gui/choiceList/ChoiceView.svelte index ff02da1..2f21e10 100644 --- a/src/gui/choiceList/ChoiceView.svelte +++ b/src/gui/choiceList/ChoiceView.svelte @@ -99,6 +99,8 @@ if (!updatedChoice) return; choices = choices.map(choice => updateChoiceHelper(choice, updatedChoice)); + plugin.removeCommandForChoice(oldChoice); + plugin.addCommandForChoice(updatedChoice); saveChoices(choices); }