From 90f6b2a57cb009a45b559173e5921c549097f4c4 Mon Sep 17 00:00:00 2001 From: Mahathi-s154 <1bi22ri028@bit-bangalore.edu.in> Date: Tue, 31 Mar 2026 21:20:03 +0530 Subject: [PATCH 1/2] Refactor startup promise orchestration --- src/index.ts | 374 +++++++++++++++++------------------ src/utils/promises.ts | 42 ++-- tests/utils/promises.spec.ts | 26 +++ 3 files changed, 220 insertions(+), 222 deletions(-) create mode 100644 tests/utils/promises.spec.ts diff --git a/src/index.ts b/src/index.ts index 0148bfc..ff8fd6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,11 +9,12 @@ import { setDefaultTemplatesView, DefaultTemplatesDisplayData, NotebookDefaultTe import { TemplateAction, performAction } from "./actions"; import { loadLegacyTemplates } from "./legacyTemplates"; import { Logger } from "./logger"; -import { PromiseGroup } from "./utils/promises"; +import { resolveNamedPromises } from "./utils/promises"; import { PluginSettingsRegistry, DefaultNoteTemplateIdSetting, DefaultTodoTemplateIdSetting, DefaultTemplatesConfigSetting } from "./settings"; import { LocaleGlobalSetting, DateFormatGlobalSetting, TimeFormatGlobalSetting, ProfileDirGlobalSetting } from "./settings/global"; import { DefaultTemplatesConfig } from "./settings/defaultTemplatesConfig"; import { CommandsPanel } from "./views/commandsPanel"; +import { notEmpty } from "./utils/typescript"; const DOCUMENTATION_URL = "https://github.com/joplin/plugin-templates#readme"; const AI_ASSISTANT_URL = "https://joplin-templates-assistant.nishantwrp.com/"; @@ -24,22 +25,20 @@ joplin.plugins.register({ await PluginSettingsRegistry.registerSettings(); // Global variables - const joplinGlobalApis = new PromiseGroup(); - - joplinGlobalApis.set("dialogViewHandle", joplin.views.dialogs.create("dialog")); - joplinGlobalApis.set("templateSelectorHandle", joplin.views.dialogs.create("templateSelector")); - joplinGlobalApis.set("folderSelectorHandle", joplin.views.dialogs.create("folderSelector")); - joplinGlobalApis.set("templateTypeSelectorHandle", joplin.views.dialogs.create("templateTypeSelector")); - joplinGlobalApis.set("userLocale", LocaleGlobalSetting.get()); - joplinGlobalApis.set("userDateFormat", DateFormatGlobalSetting.get()); - joplinGlobalApis.set("userTimeFormat", TimeFormatGlobalSetting.get()); - joplinGlobalApis.set("profileDir", ProfileDirGlobalSetting.get()); - const { dialogViewHandle, templateSelectorHandle, folderSelectorHandle, templateTypeSelectorHandle, userLocale, userDateFormat, userTimeFormat, profileDir - } = await joplinGlobalApis.groupAll(); + } = await resolveNamedPromises({ + dialogViewHandle: joplin.views.dialogs.create("dialog"), + templateSelectorHandle: joplin.views.dialogs.create("templateSelector"), + folderSelectorHandle: joplin.views.dialogs.create("folderSelector"), + templateTypeSelectorHandle: joplin.views.dialogs.create("templateTypeSelector"), + userLocale: LocaleGlobalSetting.get(), + userDateFormat: DateFormatGlobalSetting.get(), + userTimeFormat: TimeFormatGlobalSetting.get(), + profileDir: ProfileDirGlobalSetting.get() + }); const dateAndTimeUtils = new DateAndTimeUtils(userLocale, userDateFormat, userTimeFormat); const logger = new Logger(profileDir); @@ -70,11 +69,11 @@ joplin.plugins.register({ const getNotebookDefaultTemplatesDisplayData = async (settings: DefaultTemplatesConfig): Promise => { const getDisplayDataForNotebook = async (notebookId: string, defaultTemplateNoteId: string | null, defaultTemplateTodoId: string | null): Promise => { - const promiseGroup = new PromiseGroup(); - promiseGroup.set("notebook", getFolderFromId(notebookId)); - promiseGroup.set("noteTemplate", getTemplateFromId(defaultTemplateNoteId)); - promiseGroup.set("todoTemplate", getTemplateFromId(defaultTemplateTodoId)); - const { notebook, noteTemplate, todoTemplate } = await promiseGroup.groupAll(); + const { notebook, noteTemplate, todoTemplate } = await resolveNamedPromises({ + notebook: getFolderFromId(notebookId), + noteTemplate: getTemplateFromId(defaultTemplateNoteId), + todoTemplate: getTemplateFromId(defaultTemplateTodoId) + }); if (notebook === null || (noteTemplate === null && todoTemplate === null)) { // Async remove of the obsolete config @@ -88,190 +87,183 @@ joplin.plugins.register({ }; } - const notebookDisplayDataPromiseGroup = new PromiseGroup(); - for (const [notebookId, defaultTemplates] of Object.entries(settings)) { - notebookDisplayDataPromiseGroup.add(getDisplayDataForNotebook( - notebookId, defaultTemplates.defaultNoteTemplateId, defaultTemplates.defaultTodoTemplateId)); - } - return (await notebookDisplayDataPromiseGroup.groupAll())[PromiseGroup.UNNAMED_KEY].filter(x => x !== null); + return (await Promise.all( + Object.entries(settings).map(([notebookId, defaultTemplates]) => { + return getDisplayDataForNotebook( + notebookId, + defaultTemplates.defaultNoteTemplateId, + defaultTemplates.defaultTodoTemplateId + ); + }) + )).filter(notEmpty); } // Register all commands - const joplinCommands = new PromiseGroup(); - - joplinCommands.add(joplin.commands.register({ - name: "createNoteFromTemplate", - label: "Create note from template", - iconName: "far fa-file-alt", - execute: async () => { - await getTemplateAndPerformAction(TemplateAction.NewNote); - } - })); - - joplin.views.toolbarButtons.create( - "createNoteFromTemplateEditorToolbar", - "createNoteFromTemplate", - ToolbarButtonLocation.EditorToolbar - ); - - joplinCommands.add(joplin.commands.register({ - name: "createTodoFromTemplate", - label: "Create to-do from template", - execute: async () => { - await getTemplateAndPerformAction(TemplateAction.NewTodo); - } - })); - - joplinCommands.add(joplin.commands.register({ - name: "insertTemplate", - label: "Insert template", - execute: async () => { - await getTemplateAndPerformAction(TemplateAction.InsertText); - } - })); - - joplinCommands.add(joplin.commands.register({ - name: "showDefaultTemplates", - label: "Show default templates", - execute: async () => { - const noteTemplate = await getTemplateFromId(await DefaultNoteTemplateIdSetting.get()); - const todoTemplate = await getTemplateFromId(await DefaultTodoTemplateIdSetting.get()); - const defaultTemplatesConfig = await DefaultTemplatesConfigSetting.get(); - - const globalDefaultTemplates: DefaultTemplatesDisplayData = { - defaultNoteTemplateTitle: noteTemplate ? noteTemplate.title : null, - defaultTodoTemplateTitle: todoTemplate ? todoTemplate.title : null - }; - const notebookDisplayData = await getNotebookDefaultTemplatesDisplayData(defaultTemplatesConfig); - - await setDefaultTemplatesView(dialogViewHandle, globalDefaultTemplates, notebookDisplayData); - await joplin.views.dialogs.open(dialogViewHandle); - } - })); - - joplinCommands.add(joplin.commands.register({ - name: "setDefaultTemplate", - label: "Set default template", - execute: async () => { - const templateId = await getUserTemplateSelection(templateSelectorHandle, "id"); - if (templateId === null) return; - - const defaultType = await getUserDefaultTemplateTypeSelection(templateTypeSelectorHandle); - if (defaultType === null) return; - - await setDefaultTemplate(null, templateId, defaultType); - await joplin.views.dialogs.showMessageBox("Default template set successfully!"); - } - })); - - joplinCommands.add(joplin.commands.register({ - name: "setDefaultTemplateForNotebook", - label: "Set default template for notebook", - execute: async () => { - const folder: Folder | null = JSON.parse(await getUserFolderSelection(folderSelectorHandle) || "null"); - if (folder === null) return; - - const templateId = await getUserTemplateSelection(templateSelectorHandle, "id", `Default template for "${folder.title}":`); - if (templateId === null) return; - - const defaultType = await getUserDefaultTemplateTypeSelection(templateTypeSelectorHandle); - if (defaultType === null) return; - - await setDefaultTemplate(folder.id, templateId, defaultType); - await joplin.views.dialogs.showMessageBox(`Default template set for "${folder.title}" successfully!`); - } - })); - - joplinCommands.add(joplin.commands.register({ - name: "clearDefaultTemplatesForNotebook", - label: "Clear default templates for notebook", - execute: async () => { - const folder: Folder | null = JSON.parse(await getUserFolderSelection(folderSelectorHandle) || "null"); - if (folder === null) return; - - await DefaultTemplatesConfigSetting.clearDefaultTemplates(folder.id); - await joplin.views.dialogs.showMessageBox(`Default templates for "${folder.title}" cleared successfully!`); - } - })); - - joplinCommands.add(joplin.commands.register({ - name: "createNoteFromDefaultTemplate", - label: "Create note from default template", - execute: async () => { - let defaultTemplate: Note | null = null; - - const defaultTemplatesConfig = await DefaultTemplatesConfigSetting.get(); - const currentFolderId = await getSelectedFolder(); - - if (currentFolderId in defaultTemplatesConfig) { - defaultTemplate = await getTemplateFromId(defaultTemplatesConfig[currentFolderId].defaultNoteTemplateId); + const commandRegistrations = [ + joplin.commands.register({ + name: "createNoteFromTemplate", + label: "Create note from template", + iconName: "far fa-file-alt", + execute: async () => { + await getTemplateAndPerformAction(TemplateAction.NewNote); } - - if (defaultTemplate === null) { - defaultTemplate = await getTemplateFromId(await DefaultNoteTemplateIdSetting.get()); + }), + joplin.commands.register({ + name: "createTodoFromTemplate", + label: "Create to-do from template", + execute: async () => { + await getTemplateAndPerformAction(TemplateAction.NewTodo); } - - if (defaultTemplate) { - return await performActionWithParsedTemplate(TemplateAction.NewNote, defaultTemplate); + }), + joplin.commands.register({ + name: "insertTemplate", + label: "Insert template", + execute: async () => { + await getTemplateAndPerformAction(TemplateAction.InsertText); } - await joplin.views.dialogs.showMessageBox("No default note template is set."); - } - })); - - joplinCommands.add(joplin.commands.register({ - name: "createTodoFromDefaultTemplate", - label: "Create to-do from default template", - execute: async () => { - let defaultTemplate: Note | null = null; - - const defaultTemplatesConfig = await DefaultTemplatesConfigSetting.get(); - const currentFolderId = await getSelectedFolder(); - - if (currentFolderId in defaultTemplatesConfig) { - defaultTemplate = await getTemplateFromId(defaultTemplatesConfig[currentFolderId].defaultTodoTemplateId); + }), + joplin.commands.register({ + name: "showDefaultTemplates", + label: "Show default templates", + execute: async () => { + const noteTemplate = await getTemplateFromId(await DefaultNoteTemplateIdSetting.get()); + const todoTemplate = await getTemplateFromId(await DefaultTodoTemplateIdSetting.get()); + const defaultTemplatesConfig = await DefaultTemplatesConfigSetting.get(); + + const globalDefaultTemplates: DefaultTemplatesDisplayData = { + defaultNoteTemplateTitle: noteTemplate ? noteTemplate.title : null, + defaultTodoTemplateTitle: todoTemplate ? todoTemplate.title : null + }; + const notebookDisplayData = await getNotebookDefaultTemplatesDisplayData(defaultTemplatesConfig); + + await setDefaultTemplatesView(dialogViewHandle, globalDefaultTemplates, notebookDisplayData); + await joplin.views.dialogs.open(dialogViewHandle); } - - if (defaultTemplate === null) { - defaultTemplate = await getTemplateFromId(await DefaultTodoTemplateIdSetting.get()); + }), + joplin.commands.register({ + name: "setDefaultTemplate", + label: "Set default template", + execute: async () => { + const templateId = await getUserTemplateSelection(templateSelectorHandle, "id"); + if (templateId === null) return; + + const defaultType = await getUserDefaultTemplateTypeSelection(templateTypeSelectorHandle); + if (defaultType === null) return; + + await setDefaultTemplate(null, templateId, defaultType); + await joplin.views.dialogs.showMessageBox("Default template set successfully!"); } - - if (defaultTemplate) { - return await performActionWithParsedTemplate(TemplateAction.NewTodo, defaultTemplate); + }), + joplin.commands.register({ + name: "setDefaultTemplateForNotebook", + label: "Set default template for notebook", + execute: async () => { + const folder: Folder | null = JSON.parse(await getUserFolderSelection(folderSelectorHandle) || "null"); + if (folder === null) return; + + const templateId = await getUserTemplateSelection(templateSelectorHandle, "id", `Default template for "${folder.title}":`); + if (templateId === null) return; + + const defaultType = await getUserDefaultTemplateTypeSelection(templateTypeSelectorHandle); + if (defaultType === null) return; + + await setDefaultTemplate(folder.id, templateId, defaultType); + await joplin.views.dialogs.showMessageBox(`Default template set for "${folder.title}" successfully!`); } - await joplin.views.dialogs.showMessageBox("No default to-do template is set."); - } - })); - - joplinCommands.add(joplin.commands.register({ - name: "showAIAssistant", - label: "AI Assistant", - execute: async () => { - await joplin.commands.execute("openItem", AI_ASSISTANT_URL); - } - })); - - joplinCommands.add(joplin.commands.register({ - name: "showPluginDocumentation", - label: "Help", - execute: async () => { - await joplin.commands.execute("openItem", DOCUMENTATION_URL); - } - })); - - joplinCommands.add(joplin.commands.register({ - name: "copyFolderID", - label: "Copy notebook ID", - execute: async (folderId: string) => { - if (typeof folderId === "undefined") { - const selectedFolder = await joplin.workspace.selectedFolder(); - folderId = selectedFolder.id; + }), + joplin.commands.register({ + name: "clearDefaultTemplatesForNotebook", + label: "Clear default templates for notebook", + execute: async () => { + const folder: Folder | null = JSON.parse(await getUserFolderSelection(folderSelectorHandle) || "null"); + if (folder === null) return; + + await DefaultTemplatesConfigSetting.clearDefaultTemplates(folder.id); + await joplin.views.dialogs.showMessageBox(`Default templates for "${folder.title}" cleared successfully!`); + } + }), + joplin.commands.register({ + name: "createNoteFromDefaultTemplate", + label: "Create note from default template", + execute: async () => { + let defaultTemplate: Note | null = null; + + const defaultTemplatesConfig = await DefaultTemplatesConfigSetting.get(); + const currentFolderId = await getSelectedFolder(); + + if (currentFolderId in defaultTemplatesConfig) { + defaultTemplate = await getTemplateFromId(defaultTemplatesConfig[currentFolderId].defaultNoteTemplateId); + } + + if (defaultTemplate === null) { + defaultTemplate = await getTemplateFromId(await DefaultNoteTemplateIdSetting.get()); + } + + if (defaultTemplate) { + return await performActionWithParsedTemplate(TemplateAction.NewNote, defaultTemplate); + } + await joplin.views.dialogs.showMessageBox("No default note template is set."); + } + }), + joplin.commands.register({ + name: "createTodoFromDefaultTemplate", + label: "Create to-do from default template", + execute: async () => { + let defaultTemplate: Note | null = null; + + const defaultTemplatesConfig = await DefaultTemplatesConfigSetting.get(); + const currentFolderId = await getSelectedFolder(); + + if (currentFolderId in defaultTemplatesConfig) { + defaultTemplate = await getTemplateFromId(defaultTemplatesConfig[currentFolderId].defaultTodoTemplateId); + } + + if (defaultTemplate === null) { + defaultTemplate = await getTemplateFromId(await DefaultTodoTemplateIdSetting.get()); + } + + if (defaultTemplate) { + return await performActionWithParsedTemplate(TemplateAction.NewTodo, defaultTemplate); + } + await joplin.views.dialogs.showMessageBox("No default to-do template is set."); + } + }), + joplin.commands.register({ + name: "showAIAssistant", + label: "AI Assistant", + execute: async () => { + await joplin.commands.execute("openItem", AI_ASSISTANT_URL); + } + }), + joplin.commands.register({ + name: "showPluginDocumentation", + label: "Help", + execute: async () => { + await joplin.commands.execute("openItem", DOCUMENTATION_URL); } - await joplin.clipboard.writeText(folderId); + }), + joplin.commands.register({ + name: "copyFolderID", + label: "Copy notebook ID", + execute: async (folderId: string) => { + if (typeof folderId === "undefined") { + const selectedFolder = await joplin.workspace.selectedFolder(); + folderId = selectedFolder.id; + } + await joplin.clipboard.writeText(folderId); + + await joplin.commands.execute("editor.focus"); + } + }), + ]; - await joplin.commands.execute("editor.focus"); - } - })); + await Promise.all(commandRegistrations); + await joplin.views.toolbarButtons.create( + "createNoteFromTemplateEditorToolbar", + "createNoteFromTemplate", + ToolbarButtonLocation.EditorToolbar + ); // Create templates menu @@ -321,8 +313,6 @@ joplin.plugins.register({ } ]); - await joplinCommands.groupAll(); - // Folder context menu await joplin.views.menuItems.create("templates_folderid", "copyFolderID", MenuItemLocation.FolderContextMenu); diff --git a/src/utils/promises.ts b/src/utils/promises.ts index ca000b8..b7eefa8 100644 --- a/src/utils/promises.ts +++ b/src/utils/promises.ts @@ -1,35 +1,17 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -export class PromiseGroup { - private promises: { [key: string]: Promise } = {}; - private unnamedPromises: Promise[] = []; +type NamedPromises = Record>; - // TODO: This has become too hacky. Refactor it. - public static UNNAMED_KEY = "__unnamed__"; +type ResolvedNamedPromises = { + [K in keyof T]: Awaited; +}; - public add(promise: Promise): void { - this.unnamedPromises.push(promise); - } +export async function resolveNamedPromises(promises: T): Promise> { + const entries = Object.entries(promises) as [keyof T, T[keyof T]][]; + const resolvedValues = await Promise.all(entries.map(([, promise]) => promise)); - public set(key: string, promise: Promise): void { - if (key in this.promises || key === PromiseGroup.UNNAMED_KEY) { - throw new Error(`key: ${key} already in use`); - } + const resolvedPromises = {} as ResolvedNamedPromises; + entries.forEach(([key], index) => { + resolvedPromises[key] = resolvedValues[index] as ResolvedNamedPromises[typeof key]; + }); - this.promises[key] = promise; - } - - public async groupAll(): Promise<{[key: string]: any}> { - const namedPromises = Object.entries(this.promises); - const allPromises = [...this.unnamedPromises, ...namedPromises.map(np => np[1])]; - - const resolvedPromises = await Promise.all(allPromises); - - const res = {}; - for (let i = 0; i < namedPromises.length; i++) { - res[namedPromises[i][0]] = resolvedPromises[this.unnamedPromises.length + i]; - } - - res[PromiseGroup.UNNAMED_KEY] = resolvedPromises.slice(0, this.unnamedPromises.length); - return res; - } + return resolvedPromises; } diff --git a/tests/utils/promises.spec.ts b/tests/utils/promises.spec.ts new file mode 100644 index 0000000..26f3da2 --- /dev/null +++ b/tests/utils/promises.spec.ts @@ -0,0 +1,26 @@ +import { resolveNamedPromises } from "@templates/utils/promises"; + +describe("resolveNamedPromises", () => { + test("should resolve named promises while preserving keys", async () => { + const resolvedPromises = await resolveNamedPromises({ + title: Promise.resolve("Template"), + noteCount: Promise.resolve(3), + }); + + expect(resolvedPromises).toEqual({ + title: "Template", + noteCount: 3, + }); + }); + + test("should resolve an empty object", async () => { + await expect(resolveNamedPromises({})).resolves.toEqual({}); + }); + + test("should reject when one of the promises rejects", async () => { + await expect(resolveNamedPromises({ + success: Promise.resolve("ok"), + failure: Promise.reject(new Error("boom")), + })).rejects.toThrow("boom"); + }); +}); From 27d9da432dfa429994e6fab541b6357f1aa72c34 Mon Sep 17 00:00:00 2001 From: Mahathi-s154 <1bi22ri028@bit-bangalore.edu.in> Date: Thu, 9 Apr 2026 11:18:25 +0530 Subject: [PATCH 2/2] fix: handle optional folder ID in copy command --- src/index.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index ff8fd6f..55d5b18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -246,12 +246,9 @@ joplin.plugins.register({ joplin.commands.register({ name: "copyFolderID", label: "Copy notebook ID", - execute: async (folderId: string) => { - if (typeof folderId === "undefined") { - const selectedFolder = await joplin.workspace.selectedFolder(); - folderId = selectedFolder.id; - } - await joplin.clipboard.writeText(folderId); + execute: async (folderId?: string) => { + const effectiveId = folderId ?? (await joplin.workspace.selectedFolder()).id; + await joplin.clipboard.writeText(effectiveId); await joplin.commands.execute("editor.focus"); }