Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 41 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.");
}
}));

Expand All @@ -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.");
}
}));

Expand Down
44 changes: 38 additions & 6 deletions src/utils/templates.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -10,6 +10,7 @@ export interface Note {
id: string;
title: string;
body: string;
parent_id?: string;
}

type NoteProperty = "body" | "id" | "title";
Expand Down Expand Up @@ -58,7 +59,7 @@ const getAllTemplates = async () => {
export async function getUserTemplateSelection(dialogHandle: string, property?: NoteProperty, promptLabel = "Template:"): Promise<string | null> {
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;
Expand Down Expand Up @@ -97,30 +98,61 @@ 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);
return null;
}
}

/**
* 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<boolean> => {
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<Note | null> => {
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;
}
}

90 changes: 90 additions & 0 deletions tests/utils/templates.spec.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -37,7 +38,7 @@
});

const expectTemplatesDialog = (selectedTemplateValue: string | null) => {
jest.spyOn(joplin.views.dialogs, "open").mockImplementation(async (handle: string) => {

Check warning on line 41 in tests/utils/templates.spec.ts

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

'handle' is defined but never used
if (selectedTemplateValue === null) {
return { id: "cancel" };
}
Expand Down Expand Up @@ -299,3 +300,92 @@
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);
});
});
1 change: 0 additions & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,6 @@ function main(environ) {
return configs[configName];
}


module.exports = (env) => {
let exportedConfigs = [];

Expand Down
Loading