diff --git a/src/index.ts b/src/index.ts index 0148bfc..9c3c37f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import { Parser } from "./parser"; import { DateAndTimeUtils } from "./utils/dateAndTime"; import { getFolderFromId, getSelectedFolder, getUserFolderSelection, Folder } from "./utils/folders"; import { getUserDefaultTemplateTypeSelection, setDefaultTemplate } from "./utils/defaultTemplates"; -import { getTemplateFromId, getUserTemplateSelection, Note } from "./utils/templates"; +import { getTemplateFromId, getUserTemplateSelection, isNoteATemplate, Note } from "./utils/templates"; import { setDefaultTemplatesView, DefaultTemplatesDisplayData, NotebookDefaultTemplatesDisplayData } from "./views/defaultTemplates"; import { TemplateAction, performAction } from "./actions"; import { loadLegacyTemplates } from "./legacyTemplates"; @@ -200,22 +200,38 @@ joplin.plugins.register({ label: "Create note from default template", execute: async () => { let defaultTemplate: Note | null = null; + let defaultTemplateIdWasSet = false; const defaultTemplatesConfig = await DefaultTemplatesConfigSetting.get(); const currentFolderId = await getSelectedFolder(); if (currentFolderId in defaultTemplatesConfig) { - defaultTemplate = await getTemplateFromId(defaultTemplatesConfig[currentFolderId].defaultNoteTemplateId); + const notebookDefaultId = defaultTemplatesConfig[currentFolderId].defaultNoteTemplateId; + if (notebookDefaultId) defaultTemplateIdWasSet = true; + defaultTemplate = await getTemplateFromId(notebookDefaultId); } if (defaultTemplate === null) { - defaultTemplate = await getTemplateFromId(await DefaultNoteTemplateIdSetting.get()); + const globalDefaultId = await DefaultNoteTemplateIdSetting.get(); + if (globalDefaultId) defaultTemplateIdWasSet = true; + defaultTemplate = await getTemplateFromId(globalDefaultId); } if (defaultTemplate) { - return await performActionWithParsedTemplate(TemplateAction.NewNote, defaultTemplate); + const validTemplate = await isNoteATemplate(defaultTemplate); + if (validTemplate) { + return await performActionWithParsedTemplate(TemplateAction.NewNote, defaultTemplate); + } else { + await joplin.views.dialogs.showMessageBox("The note set as the default template is no longer a template. Please set a new default template via Tools \u2192 Templates \u2192 Default templates."); + return; + } + } + + if (defaultTemplateIdWasSet) { + await joplin.views.dialogs.showMessageBox("The note set as the default template is no longer a template. Please set a new default template via Tools \u2192 Templates \u2192 Default templates."); + } else { + await joplin.views.dialogs.showMessageBox("No default note template is set."); } - await joplin.views.dialogs.showMessageBox("No default note template is set."); } })); @@ -224,22 +240,38 @@ joplin.plugins.register({ label: "Create to-do from default template", execute: async () => { let defaultTemplate: Note | null = null; + let defaultTemplateIdWasSet = false; const defaultTemplatesConfig = await DefaultTemplatesConfigSetting.get(); const currentFolderId = await getSelectedFolder(); if (currentFolderId in defaultTemplatesConfig) { - defaultTemplate = await getTemplateFromId(defaultTemplatesConfig[currentFolderId].defaultTodoTemplateId); + const notebookDefaultId = defaultTemplatesConfig[currentFolderId].defaultTodoTemplateId; + if (notebookDefaultId) defaultTemplateIdWasSet = true; + defaultTemplate = await getTemplateFromId(notebookDefaultId); } if (defaultTemplate === null) { - defaultTemplate = await getTemplateFromId(await DefaultTodoTemplateIdSetting.get()); + const globalDefaultId = await DefaultTodoTemplateIdSetting.get(); + if (globalDefaultId) defaultTemplateIdWasSet = true; + defaultTemplate = await getTemplateFromId(globalDefaultId); } if (defaultTemplate) { - return await performActionWithParsedTemplate(TemplateAction.NewTodo, defaultTemplate); + const validTemplate = await isNoteATemplate(defaultTemplate); + if (validTemplate) { + return await performActionWithParsedTemplate(TemplateAction.NewTodo, defaultTemplate); + } else { + await joplin.views.dialogs.showMessageBox("The note set as the default template is no longer a template. Please set a new default template via Tools \u2192 Templates \u2192 Default templates."); + return; + } + } + + if (defaultTemplateIdWasSet) { + await joplin.views.dialogs.showMessageBox("The note set as the default template is no longer a template. Please set a new default template via Tools \u2192 Templates \u2192 Default templates."); + } else { + await joplin.views.dialogs.showMessageBox("No default to-do template is set."); } - await joplin.views.dialogs.showMessageBox("No default to-do template is set."); } })); diff --git a/src/utils/templates.ts b/src/utils/templates.ts index 0586cce..b389a30 100644 --- a/src/utils/templates.ts +++ b/src/utils/templates.ts @@ -1,5 +1,5 @@ import joplin from "api"; -import { getAllNotesInFolder } from "./folders"; +import { getAllNotesInFolder, getFolderFromId } from "./folders"; import { getAllNotesWithTag, getAllTagsWithTitle } from "./tags"; import { TemplatesSourceSetting, TemplatesSource } from "../settings/templatesSource"; import { LocaleGlobalSetting } from "../settings/global"; @@ -10,6 +10,7 @@ export interface Note { id: string; title: string; body: string; + parent_id?: string; } type NoteProperty = "body" | "id" | "title"; @@ -58,7 +59,7 @@ const getAllTemplates = async () => { export async function getUserTemplateSelection(dialogHandle: string, property?: NoteProperty, promptLabel = "Template:"): Promise { try { const templates = await getAllTemplates(); - + if (templates.length === 0) { await joplin.views.dialogs.showMessageBox("No templates found! Please create a template and try again."); return null; @@ -97,14 +98,13 @@ export async function getUserTemplateSelection(dialogHandle: string, property?: await joplin.views.dialogs.setFitToContent(dialogHandle, true); const result = await joplin.views.dialogs.open(dialogHandle); - + if (result.id === "cancel") { return null; } - // Get the template value from the nested form data structure const templateValue = result.formData?.["templates-form"]?.template; - + return templateValue || null; } catch (error) { console.error("Error in getUserTemplateSelection:", error); @@ -112,15 +112,47 @@ export async function getUserTemplateSelection(dialogHandle: string, property?: } } +/** + * Checks whether a given note is still considered a template based on the user's TemplatesSource setting. + * - For Tag source: checks if the note has the 'template' tag. + * - For Notebook source: checks if the note's parent folder title is 'Templates'. + */ +export const isNoteATemplate = async (note: Note): Promise => { + try { + const templatesSource = await TemplatesSourceSetting.get(); + + if (templatesSource === TemplatesSource.Tag) { + const templateTags = await getAllTagsWithTitle("template"); + for (const tag of templateTags) { + const notesWithTag = await getAllNotesWithTag(tag.id); + if (notesWithTag.some(n => n.id === note.id)) { + return true; + } + } + return false; + } else { + if (!note.parent_id) { + return false; + } + const folder = await getFolderFromId(note.parent_id); + return folder !== null && folder.title === "Templates"; + } + } catch (error) { + console.error("Error checking if note is a template", error); + return false; + } +}; + export const getTemplateFromId = async (templateId: string | null): Promise => { if (!templateId) { return null; } try { - return await joplin.data.get([ "notes", templateId ], { fields: ["id", "title", "body"] }); + return await joplin.data.get(["notes", templateId], { fields: ["id", "title", "body", "parent_id"] }); } catch (error) { console.error("There was an error loading a template from id", error); return null; } } + diff --git a/tests/utils/templates.spec.ts b/tests/utils/templates.spec.ts index c83cd4f..5a75f6a 100644 --- a/tests/utils/templates.spec.ts +++ b/tests/utils/templates.spec.ts @@ -1,5 +1,6 @@ import joplin from "api"; import * as tagUtils from "@templates/utils/tags"; +import * as folderUtils from "@templates/utils/folders"; import { getUserTemplateSelection } from "@templates/utils/templates"; interface TagData { @@ -299,3 +300,92 @@ describe("Get user template selection", () => { expect(res).toEqual(null); }); }); + +describe("isNoteATemplate", () => { + const setTemplateTagsAndNotes = (data: TagData[]) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + jest.spyOn(tagUtils, "getAllTagsWithTitle").mockImplementation(async (title: string) => { + return data.map(tag => { + return { + id: tag.id, + title: tag.title + } + }); + }); + + jest.spyOn(tagUtils, "getAllNotesWithTag").mockImplementation(async (id: string) => { + for (const tag of data) { + if (tag.id === id) { + return tag.notes; + } + } + return []; + }); + } + + test("should return true for tag source if note has template tag", async () => { + jest.spyOn(joplin.settings, "value").mockImplementation(async (setting: string) => { + if (setting === "templatesSource") return "tag"; + return ""; + }); + setTemplateTagsAndNotes([ + { + id: "tag-1", + title: "template", + notes: [{ id: "note-1", title: "Test", body: "" }] + } + ]); + + const { isNoteATemplate } = await import("@templates/utils/templates"); + const res = await isNoteATemplate({ id: "note-1", title: "Test", body: "" }); + expect(res).toBe(true); + }); + + test("should return false for tag source if note lacks template tag", async () => { + jest.spyOn(joplin.settings, "value").mockImplementation(async (setting: string) => { + if (setting === "templatesSource") return "tag"; + return ""; + }); + setTemplateTagsAndNotes([ + { + id: "tag-1", + title: "template", + notes: [{ id: "note-2", title: "Other Note", body: "" }] + } + ]); + + const { isNoteATemplate } = await import("@templates/utils/templates"); + const res = await isNoteATemplate({ id: "note-1", title: "Test", body: "" }); + expect(res).toBe(false); + }); + + test("should return true for notebook source if parent folder is Templates", async () => { + jest.spyOn(joplin.settings, "value").mockImplementation(async (setting: string) => { + if (setting === "templatesSource") return "notebook"; + return ""; + }); + jest.spyOn(folderUtils, "getFolderFromId").mockImplementation(async (id: string) => { + if (id === "folder-1") return { id: "folder-1", title: "Templates" }; + return null; + }); + + const { isNoteATemplate } = await import("@templates/utils/templates"); + const res = await isNoteATemplate({ id: "note-1", title: "Test", body: "", parent_id: "folder-1" }); + expect(res).toBe(true); + }); + + test("should return false for notebook source if parent folder is not Templates", async () => { + jest.spyOn(joplin.settings, "value").mockImplementation(async (setting: string) => { + if (setting === "templatesSource") return "notebook"; + return ""; + }); + jest.spyOn(folderUtils, "getFolderFromId").mockImplementation(async (id: string) => { + if (id === "folder-2") return { id: "folder-2", title: "Personal" }; + return null; + }); + + const { isNoteATemplate } = await import("@templates/utils/templates"); + const res = await isNoteATemplate({ id: "note-1", title: "Test", body: "", parent_id: "folder-2" }); + expect(res).toBe(false); + }); +}); diff --git a/webpack.config.js b/webpack.config.js index b4b3e18..dcf60a8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -387,7 +387,6 @@ function main(environ) { return configs[configName]; } - module.exports = (env) => { let exportedConfigs = [];