From a3f9d4e1e6eb648ecf49d5c3b73a3f3d605756df Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Tue, 17 Dec 2024 21:26:26 +0100 Subject: [PATCH 1/7] feat(core): template service to use any available template engine to post-process templates --- src/core/template/BasicTemplateService.ts | 26 ++++++++++++++ src/core/template/TemplateError.ts | 14 ++++++++ src/core/template/TemplateService.ts | 16 +++++++++ src/core/template/TemplaterService.ts | 41 +++++++++++++++++++++++ src/core/template/getTemplateService.ts | 16 +++++++++ src/core/template/index.ts | 5 +++ 6 files changed, 118 insertions(+) create mode 100644 src/core/template/BasicTemplateService.ts create mode 100644 src/core/template/TemplateError.ts create mode 100644 src/core/template/TemplateService.ts create mode 100644 src/core/template/TemplaterService.ts create mode 100644 src/core/template/getTemplateService.ts create mode 100644 src/core/template/index.ts diff --git a/src/core/template/BasicTemplateService.ts b/src/core/template/BasicTemplateService.ts new file mode 100644 index 0000000..f71ebb9 --- /dev/null +++ b/src/core/template/BasicTemplateService.ts @@ -0,0 +1,26 @@ +import { TE } from "@std"; +import { App, normalizePath } from "obsidian"; +import { Logger } from "src/utils/Logger"; +import { TemplateError } from "./TemplateError"; +import { TemplateService } from "./TemplateService"; + +/** + * Basic template service that creates notes with unchanged content + */ +export class BasicTemplateService implements TemplateService { + constructor( + private app: App, + private logger: Logger, + ) {} + + createNoteFromTemplate = ( + template: string, + targetPath: string, + ): TE.TaskEither => + TE.tryCatch( + async () => { + await this.app.vault.create(normalizePath(targetPath), template); + }, + TemplateError.of("Error creating note from template"), + ); +} diff --git a/src/core/template/TemplateError.ts b/src/core/template/TemplateError.ts new file mode 100644 index 0000000..0ee6ef8 --- /dev/null +++ b/src/core/template/TemplateError.ts @@ -0,0 +1,14 @@ +export class TemplateError extends Error { + public readonly _tag = "TemplateError"; + constructor( + message: string, + public readonly cause?: unknown, + ) { + super(message); + this.name = "TemplateError"; + } + + static of(message: string) { + return (cause: unknown) => new TemplateError(message, cause); + } +} diff --git a/src/core/template/TemplateService.ts b/src/core/template/TemplateService.ts new file mode 100644 index 0000000..d9f62d2 --- /dev/null +++ b/src/core/template/TemplateService.ts @@ -0,0 +1,16 @@ +import { TE } from "@std"; +import { TemplateError } from "./TemplateError"; + +export interface TemplateService { + /** + * Creates a note from a template content + * @param template The template content + * @param targetPath Path where the new note should be created + */ + createNoteFromTemplate( + templateContent: string, + targetFolder: string, + filename: string, + openNewNote: boolean, + ): TE.TaskEither; +} diff --git a/src/core/template/TemplaterService.ts b/src/core/template/TemplaterService.ts new file mode 100644 index 0000000..717fba3 --- /dev/null +++ b/src/core/template/TemplaterService.ts @@ -0,0 +1,41 @@ +import { TE } from "@std"; +import { App } from "obsidian"; +import { Logger } from "src/utils/Logger"; +import { TemplateError } from "./TemplateError"; +import { TemplateService } from "./TemplateService"; + +export interface TemplaterApi { + create_new_note_from_template: ( + content: string, + folder: string, + title: string, + openNewNote: boolean, + ) => Promise; +} + +/** + * Template service that uses the Templater plugin + */ +export class TemplaterService implements TemplateService { + constructor( + private app: App, + private logger: Logger, + private templaterApi: TemplaterApi, + ) {} + + createNoteFromTemplate = ( + templateContent: string, + targetFolder: string, + filename: string, + openNewNote: boolean, + ): TE.TaskEither => + TE.tryCatch(async () => { + const title = filename; + await this.templaterApi.create_new_note_from_template( + templateContent, + targetFolder, + title, + openNewNote, + ); + }, TemplateError.of("Error creating note from template")); +} diff --git a/src/core/template/getTemplateService.ts b/src/core/template/getTemplateService.ts new file mode 100644 index 0000000..eb098cd --- /dev/null +++ b/src/core/template/getTemplateService.ts @@ -0,0 +1,16 @@ +import { App } from "obsidian"; +import { Logger } from "src/utils/Logger"; +import { BasicTemplateService } from "./BasicTemplateService"; +import { TemplaterService } from "./TemplaterService"; +import { TemplateService } from "./TemplateService"; + +export function getTemplateService(app: App, logger: Logger): TemplateService { + const templaterApi = app.plugins.plugins["templater-obsidian"]?.templater; + if (templaterApi) { + logger.debug("Using Templater plugin for templates"); + return new TemplaterService(app, logger, templaterApi); + } + + logger.debug("Using basic template service"); + return new BasicTemplateService(app, logger); +} diff --git a/src/core/template/index.ts b/src/core/template/index.ts new file mode 100644 index 0000000..2d74315 --- /dev/null +++ b/src/core/template/index.ts @@ -0,0 +1,5 @@ +export * from "./TemplateService"; +export * from "./TemplateError"; +export * from "./BasicTemplateService"; +export * from "./TemplaterService"; +export * from "./getTemplateService"; From ea01cd93569771c4b21f7b70cc76928a957ac1aa Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Tue, 17 Dec 2024 21:27:12 +0100 Subject: [PATCH 2/7] feat(templates): post-process templates using templater fixes #309 [Feature request] Templater integration directly into "template" section of Modal forms --- src/main.ts | 37 ++++++++++++++++++++++++++++++------ src/typings/obsidian-ex.d.ts | 14 ++++++-------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/main.ts b/src/main.ts index 347c62f..93cd80a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { A, E, O, pipe } from "@std"; +import { A, E, O, pipe, TE } from "@std"; import { Platform, Plugin, WorkspaceLeaf } from "obsidian"; import { API } from "src/API"; import { ModalFormSettingTab } from "src/ModalFormSettingTab"; @@ -15,16 +15,19 @@ import { ModalFormError } from "src/utils/ModalFormError"; import { EDIT_FORM_VIEW, EditFormView } from "src/views/EditFormView"; import { MANAGE_FORMS_VIEW, ManageFormsView } from "src/views/ManageFormsView"; import { - InvalidData, - MigrationError, formNeedsMigration, + InvalidData, migrateToLatest, + MigrationError, } from "./core/formDefinitionSchema"; +import { TemplateService } from "./core/template/TemplateService"; +import { getTemplateService } from "./core/template/getTemplateService"; import { executeTemplate } from "./core/template/templateParser"; import { settingsStore } from "./store/store"; import { FormPickerModal } from "./suggesters/FormPickerModal"; import { NewNoteModal } from "./suggesters/NewNoteModal"; import { log_error, log_notice, notifyWarning } from "./utils/Log"; +import { logger } from "./utils/Logger"; import { file_exists } from "./utils/files"; import { FormImportModal } from "./views/FormImportView"; import { TemplateBuilderModal } from "./views/TemplateBuilderModal"; @@ -66,6 +69,7 @@ export default class ModalFormPlugin extends Plugin { private unsubscribeSettingsStore: () => void = () => {}; // This things will be setup in the onload function rather than constructor public api!: PublicAPI; + private templateService!: TemplateService; manageForms() { return this.activateView(MANAGE_FORMS_VIEW); @@ -230,6 +234,7 @@ export default class ModalFormPlugin extends Plugin { }); this.api = new API(this.app, this); this.attachShortcutToGlobalWindow(); + this.templateService = getTemplateService(this.app, logger); this.registerView(EDIT_FORM_VIEW, (leaf) => new EditFormView(leaf, this)); this.registerView(MANAGE_FORMS_VIEW, (leaf) => new ManageFormsView(leaf, this)); this.registerView(TEMPLATE_BUILDER_VIEW, (leaf) => new TemplateBuilderView(leaf, this)); @@ -360,11 +365,31 @@ export default class ModalFormPlugin extends Plugin { destinationFolder: string, ) => { const formData = await this.api.openForm(form); - const newNoteFullPath = this.getUniqueNoteName(noteName, destinationFolder); const noteContent = executeTemplate(form.template.parsedTemplate, formData.getData()); - console.log("new note content", noteContent); - this.app.vault.create(newNoteFullPath, noteContent); + + // Use template service instead of directly creating the file + await pipe( + this.templateService.createNoteFromTemplate( + noteContent, + destinationFolder, + noteName, + false, // don't open the new note + ), + TE.match( + (error) => { + log_error(error); + notifyWarning("Failed to create note from template"); + }, + () => { + log_notice( + "Note created successfully", + `Note ${noteName} created in ${destinationFolder}`, + ); + }, + ), + )(); }; + const picker = new NewNoteModal( this.app, formsWithTemplates, diff --git a/src/typings/obsidian-ex.d.ts b/src/typings/obsidian-ex.d.ts index ba9e2f3..1493914 100644 --- a/src/typings/obsidian-ex.d.ts +++ b/src/typings/obsidian-ex.d.ts @@ -2,6 +2,7 @@ import type { DataviewApi } from "api/plugin-api"; import type moment from "moment"; import "obsidian"; +import { TemplaterApi } from "src/core/template"; import { PublicAPI } from "src/main"; declare module "obsidian" { @@ -19,17 +20,14 @@ declare module "obsidian" { dataview?: { api: DataviewApi; }; + "templater-obsidian"?: { templater: TemplaterApi }; }; }; } interface Workspace { /** Sent to rendered dataview components to tell them to possibly refresh */ - on( - name: "dataview:refresh-views", - callback: () => void, - ctx?: unknown, - ): EventRef; + on(name: "dataview:refresh-views", callback: () => void, ctx?: unknown): EventRef; } } @@ -42,7 +40,7 @@ declare global { } declare global { - interface Window { - moment: typeof moment; - } + interface Window { + moment: typeof moment; + } } From e42809a2a0bcf6bb54ac3d0636096000a4a5adf5 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Tue, 17 Dec 2024 21:27:54 +0100 Subject: [PATCH 3/7] docs: templater example --- EXAMPLE_VAULT/.obsidian/app.json | 3 +- EXAMPLE_VAULT/.obsidian/core-plugins.json | 6 +- .../.obsidian/plugins/modal-form/data.json | 186 ++++++++++++++++++ 3 files changed, 191 insertions(+), 4 deletions(-) diff --git a/EXAMPLE_VAULT/.obsidian/app.json b/EXAMPLE_VAULT/.obsidian/app.json index e84d16d..bc18552 100644 --- a/EXAMPLE_VAULT/.obsidian/app.json +++ b/EXAMPLE_VAULT/.obsidian/app.json @@ -1,4 +1,5 @@ { "alwaysUpdateLinks": true, - "attachmentFolderPath": "attachments" + "attachmentFolderPath": "attachments", + "promptDelete": false } \ No newline at end of file diff --git a/EXAMPLE_VAULT/.obsidian/core-plugins.json b/EXAMPLE_VAULT/.obsidian/core-plugins.json index a1c0e0b..6e5c9e4 100644 --- a/EXAMPLE_VAULT/.obsidian/core-plugins.json +++ b/EXAMPLE_VAULT/.obsidian/core-plugins.json @@ -10,12 +10,12 @@ "properties": false, "page-preview": true, "daily-notes": false, - "templates": false, + "templates": true, "note-composer": true, "command-palette": true, "slash-command": false, "editor-status": true, - "bookmarks": true, + "bookmarks": false, "markdown-importer": false, "zk-prefixer": false, "random-note": false, @@ -24,7 +24,7 @@ "slides": false, "audio-recorder": false, "workspaces": false, - "file-recovery": true, + "file-recovery": false, "publish": false, "sync": false } \ No newline at end of file diff --git a/EXAMPLE_VAULT/.obsidian/plugins/modal-form/data.json b/EXAMPLE_VAULT/.obsidian/plugins/modal-form/data.json index 54a5fd8..1b4c3fb 100644 --- a/EXAMPLE_VAULT/.obsidian/plugins/modal-form/data.json +++ b/EXAMPLE_VAULT/.obsidian/plugins/modal-form/data.json @@ -382,6 +382,192 @@ } ], "version": "1" + }, + { + "title": "Templater example", + "name": "templater-example", + "fields": [ + { + "name": "name", + "label": "", + "description": "", + "isRequired": true, + "input": { + "type": "text", + "hidden": false + } + }, + { + "name": "age", + "label": "", + "description": "", + "isRequired": false, + "input": { + "type": "number", + "hidden": false + } + }, + { + "name": "favorite_book", + "label": "", + "description": "", + "isRequired": false, + "input": { + "type": "note", + "folder": "Books" + } + }, + { + "name": "isFamily", + "label": "", + "description": "", + "isRequired": false, + "input": { + "type": "toggle", + "hidden": false + } + }, + { + "name": "additional_information", + "label": "Additional info", + "description": "Provide any extra notes about this contact that you want", + "isRequired": false, + "input": { + "type": "textarea", + "hidden": false + } + } + ], + "version": "1", + "template": { + "parsedTemplate": [ + { + "_tag": "text", + "value": "---\ncreated: <% tp.date.now(\"YYYY-MM-DD HH:mm:ss\") %>\nmodified: <% tp.file.last_modified_date(\"YYYY-MM-DD HH:mm:ss\") %>\nname: " + }, + { + "_tag": "variable", + "value": "name" + }, + { + "_tag": "text", + "value": "\nage: " + }, + { + "_tag": "variable", + "value": "age" + }, + { + "_tag": "text", + "value": "\ndateOfBirth: " + }, + { + "_tag": "variable", + "value": "dateOfBirth" + }, + { + "_tag": "text", + "value": "\nisFamily: " + }, + { + "_tag": "variable", + "value": "isFamily" + }, + { + "_tag": "text", + "value": "\nfavoriteBook: " + }, + { + "_tag": "variable", + "value": "favorite_book" + }, + { + "_tag": "text", + "value": "\ntags: " + }, + { + "_tag": "variable", + "value": "Tags" + }, + { + "_tag": "text", + "value": "\n---\n\n<%*\n// Get current time in user's timezone\nconst now = moment();\nconst age = parseInt(" + }, + { + "_tag": "variable", + "value": "age" + }, + { + "_tag": "text", + "value": ");\nconst birthYear = now.year() - age;\n_%>\n\n# " + }, + { + "_tag": "variable", + "value": "name" + }, + { + "_tag": "text", + "value": "'s Profile\n> Created on <% tp.date.now(\"dddd, MMMM Do YYYY\") %> at <% tp.date.now(\"HH:mm\") %>\n\n## Basic Information\n- **Age**: " + }, + { + "_tag": "variable", + "value": "age" + }, + { + "_tag": "text", + "value": " years old *(born around <%* tR + birthYear %>)*\n- **Date of Birth**: " + }, + { + "_tag": "variable", + "value": "dateOfBirth" + }, + { + "_tag": "text", + "value": "\n- **Best Time to Contact**: " + }, + { + "_tag": "variable", + "value": "timeOfDay" + }, + { + "_tag": "text", + "value": "\n- **Family Member**: " + }, + { + "_tag": "variable", + "value": "is_family" + }, + { + "_tag": "text", + "value": "\n- **Days until next birthday**: <%* \nif (tp.frontmatter.dateOfBirth) {\n const birthday = moment(tp.frontmatter.dateOfBirth);\n const nextBirthday = moment(birthday).year(now.year());\n if (nextBirthday.isBefore(now)) {\n nextBirthday.add(1, 'year');\n }\n tR + nextBirthday.diff(now, 'days');\n} else {\n tR + \"Unknown\";\n}\n\nconsole.log({ age, birthYear, frontmatter: tp.frontmatter })\n_%> days\n\n## Preferences\n- **Favorite Book**: [[" + }, + { + "_tag": "variable", + "value": "favorite_book" + }, + { + "_tag": "text", + "value": "]]\n<%* if (tp.frontmatter.favorite_book) { %>\n> [!note] Related Books\n> \\`\\`\\`dataview\n> LIST\n> FROM [[" + }, + { + "_tag": "variable", + "value": "favorite_book" + }, + { + "_tag": "text", + "value": "]]-links\n> SORT file.name ASC\n> \\`\\`\\`\n<%* } %>\n\n## Additional Information\n" + }, + { + "_tag": "variable", + "value": "additional_information" + }, + { + "_tag": "text", + "value": "\n\n---\n> Last modified: <% tp.file.last_modified_date(\"dddd, MMMM Do YYYY HH:mm:ss\") %>" + } + ], + "createCommand": true + } } ] } \ No newline at end of file From 64dae864a6d36e54038857b958cd21bf9f1fc412 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Wed, 18 Dec 2024 00:00:06 +0100 Subject: [PATCH 4/7] feat(templates): offer retry on templater errors --- src/core/template/TemplaterService.ts | 27 ++++++---- src/core/template/retryForm.ts | 29 ++++++++++ src/main.ts | 77 +++++++++++++++++---------- 3 files changed, 95 insertions(+), 38 deletions(-) create mode 100644 src/core/template/retryForm.ts diff --git a/src/core/template/TemplaterService.ts b/src/core/template/TemplaterService.ts index 717fba3..bc58bdc 100644 --- a/src/core/template/TemplaterService.ts +++ b/src/core/template/TemplaterService.ts @@ -29,13 +29,22 @@ export class TemplaterService implements TemplateService { filename: string, openNewNote: boolean, ): TE.TaskEither => - TE.tryCatch(async () => { - const title = filename; - await this.templaterApi.create_new_note_from_template( - templateContent, - targetFolder, - title, - openNewNote, - ); - }, TemplateError.of("Error creating note from template")); + TE.tryCatch( + async () => { + const title = filename; + const result = await this.templaterApi.create_new_note_from_template( + templateContent, + targetFolder, + title, + openNewNote, + ); + if (result === undefined) { + throw new Error("Templater API returned undefined, probably a parsing error"); + } + }, + (e) => + e instanceof Error + ? TemplateError.of(e.message)(e) + : TemplateError.of("Unknown error")(e), + ); } diff --git a/src/core/template/retryForm.ts b/src/core/template/retryForm.ts new file mode 100644 index 0000000..125a049 --- /dev/null +++ b/src/core/template/retryForm.ts @@ -0,0 +1,29 @@ +import { FormDefinition } from "../formDefinition"; + +export const retryForm: FormDefinition = { + title: "Templater error", + name: "retry-temlate", + version: "1", + fields: [ + { + name: "title", + label: "", + description: "", + input: { + type: "markdown_block", + body: "return `\n==Templater reported an error==\nWe are not sure about what it is, but is very likely a parse error.\nPlease try to fix the templater code below and submit it to retry\n`", + }, + isRequired: false, + }, + { + name: "template", + label: "Code", + description: "Fix the template below and try to submit again", + input: { + type: "textarea", + hidden: false, + }, + isRequired: false, + }, + ], +}; diff --git a/src/main.ts b/src/main.ts index 93cd80a..fdca1b1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,6 @@ import { A, E, O, pipe, TE } from "@std"; import { Platform, Plugin, WorkspaceLeaf } from "obsidian"; import { API } from "src/API"; import { ModalFormSettingTab } from "src/ModalFormSettingTab"; -import FormResult from "src/core/FormResult"; import { FormWithTemplate, type FormDefinition } from "src/core/formDefinition"; import { getDefaultSettings, @@ -22,6 +21,7 @@ import { } from "./core/formDefinitionSchema"; import { TemplateService } from "./core/template/TemplateService"; import { getTemplateService } from "./core/template/getTemplateService"; +import { retryForm } from "./core/template/retryForm"; import { executeTemplate } from "./core/template/templateParser"; import { settingsStore } from "./store/store"; import { FormPickerModal } from "./suggesters/FormPickerModal"; @@ -36,12 +36,6 @@ import { makeModel } from "./views/components/TemplateBuilder"; type ViewType = typeof EDIT_FORM_VIEW | typeof MANAGE_FORMS_VIEW | typeof TEMPLATE_BUILDER_VIEW; -// Define functions and properties you want to make available to other plugins, or templater templates, etc -export interface PublicAPI { - exampleForm(): Promise; - openForm(formReference: string | FormDefinition): Promise; -} - function notifyParsingErrors(errors: InvalidData[]) { if (errors.length === 0) { return; @@ -68,7 +62,7 @@ export default class ModalFormPlugin extends Plugin { public settings: ModalFormSettings | undefined; private unsubscribeSettingsStore: () => void = () => {}; // This things will be setup in the onload function rather than constructor - public api!: PublicAPI; + public api!: API; private templateService!: TemplateService; manageForms() { @@ -352,6 +346,51 @@ export default class ModalFormPlugin extends Plugin { ); } + createNoteFromTemplate( + noteName: string, + noteContent: string, + destinationFolder: string, + ): TE.TaskEither { + // Use template service instead of directly creating the file + return pipe( + this.templateService.createNoteFromTemplate( + noteContent, + destinationFolder, + noteName, + false, // don't open the new note + ), + TE.orElse((error) => { + logger.error(error); + return pipe( + TE.tryCatch( + () => + this.api.openForm(retryForm, { + values: { + title: error.message, + template: noteContent, + }, + }), + E.toError, + ), + TE.map((result) => result.get("template")), + TE.chain((template) => { + if (typeof template !== "string") { + notifyWarning("Failed while retrying")("Template is not a string"); + return TE.left(new Error("Template is not a string")); + } + return this.createNoteFromTemplate(noteName, template, destinationFolder); + }), + ); + }), + TE.tapIO(() => () => { + log_notice( + "Note created successfully", + `Note "${noteName}" created in ${destinationFolder}`, + ); + }), + ); + } + /** * Checks if there are forms with templates, and presents a prompt * to select a form, then opens the forms, and creates a new note @@ -367,27 +406,7 @@ export default class ModalFormPlugin extends Plugin { const formData = await this.api.openForm(form); const noteContent = executeTemplate(form.template.parsedTemplate, formData.getData()); - // Use template service instead of directly creating the file - await pipe( - this.templateService.createNoteFromTemplate( - noteContent, - destinationFolder, - noteName, - false, // don't open the new note - ), - TE.match( - (error) => { - log_error(error); - notifyWarning("Failed to create note from template"); - }, - () => { - log_notice( - "Note created successfully", - `Note ${noteName} created in ${destinationFolder}`, - ); - }, - ), - )(); + await this.createNoteFromTemplate(noteName, noteContent, destinationFolder)(); }; const picker = new NewNoteModal( From 4509c653d47ee907a4e1e78e9fa5b8f0592ec1a8 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Wed, 18 Dec 2024 00:03:00 +0100 Subject: [PATCH 5/7] chore: some typings --- src/typings/obsidian-ex.d.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/typings/obsidian-ex.d.ts b/src/typings/obsidian-ex.d.ts index 1493914..5dd1dbe 100644 --- a/src/typings/obsidian-ex.d.ts +++ b/src/typings/obsidian-ex.d.ts @@ -3,7 +3,6 @@ import type { DataviewApi } from "api/plugin-api"; import type moment from "moment"; import "obsidian"; import { TemplaterApi } from "src/core/template"; -import { PublicAPI } from "src/main"; declare module "obsidian" { interface MetadataCache { @@ -34,13 +33,8 @@ declare module "obsidian" { declare global { interface Window { DataviewAPI?: DataviewApi; - MF?: PublicAPI; - ModalForm?: PublicAPI; - } -} - -declare global { - interface Window { + MF?: API; + ModalForm?: API; moment: typeof moment; } } From 49038ea5f8303d74210ac9b5e1aeecb658e11948 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Wed, 18 Dec 2024 00:11:03 +0100 Subject: [PATCH 6/7] chore: fix basic template service --- src/core/template/BasicTemplateService.ts | 28 +++++++++++++++-------- src/core/template/TemplateService.ts | 2 -- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/core/template/BasicTemplateService.ts b/src/core/template/BasicTemplateService.ts index f71ebb9..94a526a 100644 --- a/src/core/template/BasicTemplateService.ts +++ b/src/core/template/BasicTemplateService.ts @@ -1,5 +1,5 @@ import { TE } from "@std"; -import { App, normalizePath } from "obsidian"; +import { App, normalizePath, TFile } from "obsidian"; import { Logger } from "src/utils/Logger"; import { TemplateError } from "./TemplateError"; import { TemplateService } from "./TemplateService"; @@ -14,13 +14,23 @@ export class BasicTemplateService implements TemplateService { ) {} createNoteFromTemplate = ( - template: string, - targetPath: string, + templateContent: string, + targetFolder: string, + filename: string, + openNewNote: boolean, ): TE.TaskEither => - TE.tryCatch( - async () => { - await this.app.vault.create(normalizePath(targetPath), template); - }, - TemplateError.of("Error creating note from template"), - ); + TE.tryCatch(async () => { + const fullPath = normalizePath(`${targetFolder}/${filename}.md`); + await this.app.vault.create(fullPath, templateContent); + if (openNewNote) { + const file = this.app.vault.getAbstractFileByPath(fullPath); + if (!file) { + this.logger.error("File not found", fullPath); + return; + } + if (file instanceof TFile) { + await this.app.workspace.getLeaf("split").openFile(file); + } + } + }, TemplateError.of("Error creating note from template")); } diff --git a/src/core/template/TemplateService.ts b/src/core/template/TemplateService.ts index d9f62d2..0620a62 100644 --- a/src/core/template/TemplateService.ts +++ b/src/core/template/TemplateService.ts @@ -4,8 +4,6 @@ import { TemplateError } from "./TemplateError"; export interface TemplateService { /** * Creates a note from a template content - * @param template The template content - * @param targetPath Path where the new note should be created */ createNoteFromTemplate( templateContent: string, From 4b90dd208ff35c49397439038d33fe0eddd54e42 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Wed, 18 Dec 2024 14:57:30 +0100 Subject: [PATCH 7/7] chore: better retry and better example --- .../.obsidian/plugins/modal-form/data.json | 32 +++++---- src/main.ts | 65 ++++++++++--------- 2 files changed, 56 insertions(+), 41 deletions(-) diff --git a/EXAMPLE_VAULT/.obsidian/plugins/modal-form/data.json b/EXAMPLE_VAULT/.obsidian/plugins/modal-form/data.json index 1b4c3fb..558ab4c 100644 --- a/EXAMPLE_VAULT/.obsidian/plugins/modal-form/data.json +++ b/EXAMPLE_VAULT/.obsidian/plugins/modal-form/data.json @@ -436,6 +436,16 @@ "type": "textarea", "hidden": false } + }, + { + "name": "dateOfBirth", + "label": "", + "description": "", + "isRequired": false, + "input": { + "type": "date", + "hidden": false + } } ], "version": "1", @@ -499,35 +509,35 @@ }, { "_tag": "text", - "value": ");\nconst birthYear = now.year() - age;\n_%>\n\n# " + "value": ");\nconst birthYear = now.year() - age;\nconst dateOfBirth = moment(" }, { "_tag": "variable", - "value": "name" + "value": "dateOfBirth" }, { "_tag": "text", - "value": "'s Profile\n> Created on <% tp.date.now(\"dddd, MMMM Do YYYY\") %> at <% tp.date.now(\"HH:mm\") %>\n\n## Basic Information\n- **Age**: " + "value": ")\n_%>\n\n# " }, { "_tag": "variable", - "value": "age" + "value": "name" }, { "_tag": "text", - "value": " years old *(born around <%* tR + birthYear %>)*\n- **Date of Birth**: " + "value": "'s Profile\n> Created on <% tp.date.now(\"dddd, MMMM Do YYYY\") %> at <% tp.date.now(\"HH:mm\") %>\n\n## Basic Information\n- **Age**: " }, { "_tag": "variable", - "value": "dateOfBirth" + "value": "age" }, { "_tag": "text", - "value": "\n- **Best Time to Contact**: " + "value": " years old *(born around <%* tR += birthYear %>)*\n- **Date of Birth**: " }, { "_tag": "variable", - "value": "timeOfDay" + "value": "dateOfBirth" }, { "_tag": "text", @@ -539,7 +549,7 @@ }, { "_tag": "text", - "value": "\n- **Days until next birthday**: <%* \nif (tp.frontmatter.dateOfBirth) {\n const birthday = moment(tp.frontmatter.dateOfBirth);\n const nextBirthday = moment(birthday).year(now.year());\n if (nextBirthday.isBefore(now)) {\n nextBirthday.add(1, 'year');\n }\n tR + nextBirthday.diff(now, 'days');\n} else {\n tR + \"Unknown\";\n}\n\nconsole.log({ age, birthYear, frontmatter: tp.frontmatter })\n_%> days\n\n## Preferences\n- **Favorite Book**: [[" + "value": "\n- **Days until next birthday**: <%* \nif (dateOfBirth) {\n const nextBirthday = dateOfBirth.year(now.year());\n if (nextBirthday.isBefore(now)) {\n nextBirthday.add(1, 'year');\n }\n tR += nextBirthday.diff(now, 'days');\n} else {\n tR += \"Unknown\";\n}\n\nconsole.log({ age, birthYear, frontmatter: tp.frontmatter, dateOfBirth })\n_%> days\n\n## Preferences\n- **Favorite Book**: [[" }, { "_tag": "variable", @@ -547,7 +557,7 @@ }, { "_tag": "text", - "value": "]]\n<%* if (tp.frontmatter.favorite_book) { %>\n> [!note] Related Books\n> \\`\\`\\`dataview\n> LIST\n> FROM [[" + "value": "]]\n<%* if (tp.frontmatter.favorite_book) { %>\n> [!note] Related Books\n> ```dataview\n> LIST\n> FROM #book\n> WHERE contains(file.outlinks, [[" }, { "_tag": "variable", @@ -555,7 +565,7 @@ }, { "_tag": "text", - "value": "]]-links\n> SORT file.name ASC\n> \\`\\`\\`\n<%* } %>\n\n## Additional Information\n" + "value": "]])\n> SORT file.name ASC\n> ```\n<%* } %>\n\n## Additional Information\n" }, { "_tag": "variable", diff --git a/src/main.ts b/src/main.ts index fdca1b1..4cf9e65 100644 --- a/src/main.ts +++ b/src/main.ts @@ -351,37 +351,42 @@ export default class ModalFormPlugin extends Plugin { noteContent: string, destinationFolder: string, ): TE.TaskEither { - // Use template service instead of directly creating the file + const loop = (noteContent: string): TE.TaskEither => { + // Use template service instead of directly creating the file + return pipe( + this.templateService.createNoteFromTemplate( + noteContent, + destinationFolder, + noteName, + false, // don't open the new note + ), + TE.orElse((error) => { + logger.error(error); + return pipe( + TE.tryCatch( + () => + this.api.openForm(retryForm, { + values: { + title: error.message, + template: noteContent, + }, + }), + E.toError, + ), + TE.map((result) => result.get("template")), + TE.chain((template) => { + if (typeof template !== "string") { + notifyWarning("Failed while retrying")("Template is not a string"); + return TE.left(new Error("Template is not a string")); + } + return loop(template); + }), + ); + }), + ); + }; return pipe( - this.templateService.createNoteFromTemplate( - noteContent, - destinationFolder, - noteName, - false, // don't open the new note - ), - TE.orElse((error) => { - logger.error(error); - return pipe( - TE.tryCatch( - () => - this.api.openForm(retryForm, { - values: { - title: error.message, - template: noteContent, - }, - }), - E.toError, - ), - TE.map((result) => result.get("template")), - TE.chain((template) => { - if (typeof template !== "string") { - notifyWarning("Failed while retrying")("Template is not a string"); - return TE.left(new Error("Template is not a string")); - } - return this.createNoteFromTemplate(noteName, template, destinationFolder); - }), - ); - }), + loop(noteContent), TE.tapIO(() => () => { log_notice( "Note created successfully",