From 7a3eb0b5cbf4fcdda38b6ad05fd218f03f28848b Mon Sep 17 00:00:00 2001 From: Muness Castle <931+muness@users.noreply.github.com> Date: Mon, 25 Dec 2023 19:38:37 -0500 Subject: [PATCH 1/3] Task addition dialog, displaying it as drop down and text area --- src/addTaskModal.ts | 106 ++++++++++++++++++++++++++++++++++++++++++++ src/main.ts | 37 +++++++++++++++- 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/addTaskModal.ts diff --git a/src/addTaskModal.ts b/src/addTaskModal.ts new file mode 100644 index 0000000..d14e894 --- /dev/null +++ b/src/addTaskModal.ts @@ -0,0 +1,106 @@ +import { App, Modal, Setting, DropdownComponent, TextAreaComponent } from "obsidian"; +import { Category } from "./interfaces"; + +export class AddTaskModal extends Modal { + result: {catId: string, task: string}; + onSubmit: (result: {catId: string, task: string}) => void; + categories: Category[]; + + constructor(app: App, categories: Category[], onSubmit: (result: { catId: string; task: string; }) => void) { + super(app); + this.onSubmit = onSubmit; + this.categories = categories.sort((a, b) => { + return this.getFullPathToCategoryTitle(a, categories).localeCompare(this.getFullPathToCategoryTitle(b, categories)); + }); + this.result = { catId: '', task: '' }; // initialize result + } + + onOpen() { + const { contentEl } = this; + + contentEl.createEl("h1", { text: "New Amazing Marvin Task" }); + + + new Setting(contentEl) + .setName("Category") + .addDropdown((dropdown: DropdownComponent) => { + // Add "Inbox" at the top + dropdown.addOption("###root###", "Inbox"); + + // Display categories hierarchically + this.categories.forEach(category => { + const title = this.getTitleWithParent(category); + dropdown.addOption(category._id, title); + }); + dropdown.onChange((value: string) => { + this.result.catId = value; + }); + }); + + new Setting(contentEl) + .setHeading().setName("Task"); + + new Setting(contentEl) + .addTextArea((textArea: TextAreaComponent) => { + textArea.inputEl.style.minHeight = "5em"; // Increase the size of the text area + textArea.onChange((value: string) => { + this.result.task = value; + }); + }).settingEl.addClass("task-textarea-setting"); + + + // Submit Button + new Setting(contentEl) + .addButton((btn) => + btn + .setButtonText("Submit") + .setCta() + .onClick(() => { + this.close(); + if (this.onSubmit && this.result.catId && this.result.task) { + this.onSubmit(this.result); + } + })); + } + + private getTitleWithParent(category: Category): string { + let parent = category.parentId; + + let parentTitle = []; + while (parent && parent !== "root") { + const parentCategory = this.categories.find(c => c._id === parent); + if (parentCategory) { + parentTitle.push(parentCategory.title); + parent = parentCategory.parentId; + } else { + break; + } + } + if (parentTitle.length > 0) { + return category.title + ` in ${parentTitle.reverse().join("/")}`; + } + return category.title; + } + + private getFullPathToCategoryTitle(category: Category, categories: Category[]): string { + let parent = category.parentId; + + let parentTitle = []; + parentTitle.push('/'); + while (parent && parent !== "root") { + const parentCategory = categories.find(c => c._id === parent); + if (parentCategory) { + parentTitle.push(parentCategory.title); + parent = parentCategory.parentId; + } else { + break; + } + } + return `${parentTitle.reverse().join("/")}${category.title}`; + } + + onClose() { + let { contentEl } = this; + contentEl.empty(); + } +} diff --git a/src/main.ts b/src/main.ts index db56d1b..a89859d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,6 @@ import { requestUrl, } from "obsidian"; - import { Category, Task @@ -21,6 +20,7 @@ import { getDateFromFile } from "obsidian-daily-notes-interface"; import { amTaskWatcher } from "./amTaskWatcher"; +import { AddTaskModal } from "./addTaskModal"; let noticeTimeout: NodeJS.Timeout; @@ -48,6 +48,7 @@ const CONSTANTS = { dueOnDayEndpoint: '/api/dueItems' } + export default class AmazingMarvinPlugin extends Plugin { settings: AmazingMarvinPluginSettings; @@ -76,6 +77,40 @@ export default class AmazingMarvinPlugin extends Plugin { this.registerEditorExtension(amTaskWatcher(this.app, this)); } + this.addCommand({ + id: "create-marvin-task", + name: "Create Marvin Task", + editorCallback: async (editor, view) => { + // Fetch categories first and make sure they are loaded + try { + const categories = await this.fetchTasksAndCategories(CONSTANTS.categoriesEndpoint); + console.log('Categories:', categories); // For debug purposes + // Ensure categories are fetched before initializing the modal + if (categories.length > 0) { + new AddTaskModal(this.app, categories, async (taskDetails: { catId: string, task: string }) => { + console.log('Task details:', taskDetails); + + // Now you can add the logic to create a Marvin task here using the API... + // For example: + // this.createMarvinTask(taskDetails.catId, taskDetails.task) + // .then(task => { + // editor.replaceRange(`[Marvin Task](${task.deepLink})`, editor.getCursor()); + // }) + // .catch(error => { + // new Notice('Could not create Marvin task: ' + error.message); + // }); + }).open(); + } else { + // Handle the case where categories could not be loaded + new Notice('Failed to load categories from Amazing Marvin.'); + } + } catch (error) { + console.error('Error fetching categories:', error); + new Notice('Failed to load categories from Amazing Marvin.'); + } + } + }); + this.addCommand({ id: 'am-import', name: 'Import Categories and Tasks', From 48308fc9da6153b844d3f48424def0dde63d4c72 Mon Sep 17 00:00:00 2001 From: Muness Castle <931+muness@users.noreply.github.com> Date: Mon, 25 Dec 2023 20:07:03 -0500 Subject: [PATCH 2/3] Switch from a dropdown to a fuzzy matcher dialog for category --- src/addTaskModal.ts | 121 ++++++++++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/src/addTaskModal.ts b/src/addTaskModal.ts index d14e894..8774749 100644 --- a/src/addTaskModal.ts +++ b/src/addTaskModal.ts @@ -1,9 +1,72 @@ -import { App, Modal, Setting, DropdownComponent, TextAreaComponent } from "obsidian"; +import { App, Modal, Setting, DropdownComponent, TextAreaComponent, FuzzySuggestModal, FuzzyMatch } from "obsidian"; import { Category } from "./interfaces"; +function getTitleWithParent(category: Category, categories: Category[]): string { + let parent = category.parentId; + + let parentTitle = []; + while (parent && parent !== "root") { + const parentCategory = categories.find(category => category._id === parent); + if (parentCategory) { + parentTitle.push(parentCategory.title); + parent = parentCategory.parentId; + } else { + break; + } + } + if (parentTitle.length > 0) { + return category.title + ` in ${parentTitle.reverse().join("/")}`; + } + return category.title; +} + +// Suggester Modal Class for Category Selection +class CategorySuggesterModal extends FuzzySuggestModal { + getItems(): Category[] { + // Prepend a faux 'Inbox' category for matching purposes + const inboxCategory: Category = { + _id: "__inbox-faux__", // Arbitrary unique ID for the Inbox faux category + title: "Inbox", + type: "faux", + updatedAt: 0, + parentId: "root", + startDate: "", + endDate: "", + note: "", + isRecurring: false, + priority: "", + deepLink: "", + dueDate: "", + done: false, + }; + + // Include the Inbox at the beginning of the categories list + return [inboxCategory, ...this.categories]; + } + getItemText(category: Category): string { + if (category.type === "faux") { + return "Inbox"; + } + return getTitleWithParent(category, this.categories); + } + categories: Category[]; + onChooseItem: (item: Category, _evt: MouseEvent | KeyboardEvent) => void; + + constructor(app: App, categories: Category[], onChooseItem: (item: Category, _evt: MouseEvent | KeyboardEvent) => void) { + super(app); + this.categories = categories; + this.onChooseItem = onChooseItem; + this.setPlaceholder("Type to search for a Category"); + } + + onChooseSuggestion(item: FuzzyMatch, _evt: MouseEvent | KeyboardEvent): void { + this.onChooseItem(item.item, _evt); + } +} + export class AddTaskModal extends Modal { - result: {catId: string, task: string}; - onSubmit: (result: {catId: string, task: string}) => void; + result: { catId: string, task: string }; + onSubmit: (result: { catId: string, task: string }) => void; categories: Category[]; constructor(app: App, categories: Category[], onSubmit: (result: { catId: string; task: string; }) => void) { @@ -17,36 +80,35 @@ export class AddTaskModal extends Modal { onOpen() { const { contentEl } = this; + let categoryInput: HTMLInputElement; contentEl.createEl("h1", { text: "New Amazing Marvin Task" }); - new Setting(contentEl) .setName("Category") - .addDropdown((dropdown: DropdownComponent) => { - // Add "Inbox" at the top - dropdown.addOption("###root###", "Inbox"); - - // Display categories hierarchically - this.categories.forEach(category => { - const title = this.getTitleWithParent(category); - dropdown.addOption(category._id, title); - }); - dropdown.onChange((value: string) => { - this.result.catId = value; + .addText(text => { + categoryInput = text.inputEl; + text.onChange(value => { + // Simulate a dropdown by creating a suggester modal + const suggester = new CategorySuggesterModal(this.app, this.categories, (item: Category) => { + categoryInput.value = item.title; + this.result.catId = item._id; + suggester.close(); + }); + suggester.open(); }); }); - new Setting(contentEl) - .setHeading().setName("Task"); + new Setting(contentEl) + .setHeading().setName("Task"); - new Setting(contentEl) + new Setting(contentEl) .addTextArea((textArea: TextAreaComponent) => { textArea.inputEl.style.minHeight = "5em"; // Increase the size of the text area textArea.onChange((value: string) => { this.result.task = value; }); - }).settingEl.addClass("task-textarea-setting"); + }).settingEl.addClass("am-task-textarea-setting"); // Submit Button @@ -63,25 +125,6 @@ export class AddTaskModal extends Modal { })); } - private getTitleWithParent(category: Category): string { - let parent = category.parentId; - - let parentTitle = []; - while (parent && parent !== "root") { - const parentCategory = this.categories.find(c => c._id === parent); - if (parentCategory) { - parentTitle.push(parentCategory.title); - parent = parentCategory.parentId; - } else { - break; - } - } - if (parentTitle.length > 0) { - return category.title + ` in ${parentTitle.reverse().join("/")}`; - } - return category.title; - } - private getFullPathToCategoryTitle(category: Category, categories: Category[]): string { let parent = category.parentId; @@ -96,7 +139,7 @@ export class AddTaskModal extends Modal { break; } } - return `${parentTitle.reverse().join("/")}${category.title}`; + return `${parentTitle.reverse().join("/")}${category.title}`; } onClose() { From 4122aa7abe88090cf672c8522d2ca273e9f27fda Mon Sep 17 00:00:00 2001 From: Muness Castle <931+muness@users.noreply.github.com> Date: Mon, 25 Dec 2023 20:49:22 -0500 Subject: [PATCH 3/3] Implement task addition API --- src/addTaskModal.ts | 15 +++++++++++ src/main.ts | 63 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/addTaskModal.ts b/src/addTaskModal.ts index 8774749..74f0955 100644 --- a/src/addTaskModal.ts +++ b/src/addTaskModal.ts @@ -110,6 +110,13 @@ export class AddTaskModal extends Modal { }); }).settingEl.addClass("am-task-textarea-setting"); + const shortcutsDesc = document.createDocumentFragment(); + shortcutsDesc.appendText('The Task field accepts labels (@), time estimates (~), and scheduled dates (+). See '); + shortcutsDesc.appendChild(this.getShortcutsLink()); + shortcutsDesc.appendText('.'); + + new Setting(contentEl) + .setDesc(shortcutsDesc); // Submit Button new Setting(contentEl) @@ -125,6 +132,14 @@ export class AddTaskModal extends Modal { })); } + private getShortcutsLink(): HTMLAnchorElement { + const a = document.createElement('a'); + a.href = 'https://help.amazingmarvin.com/en/articles/1949399-using-shortcuts-while-creating-a-task'; + a.text = 'Using shortcuts while creating a task'; + a.target = '_blank'; + return a; + } + private getFullPathToCategoryTitle(category: Category, categories: Category[]): string { let parent = category.parentId; diff --git a/src/main.ts b/src/main.ts index a89859d..0315a49 100644 --- a/src/main.ts +++ b/src/main.ts @@ -45,7 +45,8 @@ const CONSTANTS = { categoriesEndpoint: '/api/categories', childrenEndpoint: '/api/children', scheduledOnDayEndpoint: '/api/todayItems', - dueOnDayEndpoint: '/api/dueItems' + dueOnDayEndpoint: '/api/dueItems', + addTaskEndpoint: '/api/addTask', } @@ -90,15 +91,13 @@ export default class AmazingMarvinPlugin extends Plugin { new AddTaskModal(this.app, categories, async (taskDetails: { catId: string, task: string }) => { console.log('Task details:', taskDetails); - // Now you can add the logic to create a Marvin task here using the API... - // For example: - // this.createMarvinTask(taskDetails.catId, taskDetails.task) - // .then(task => { - // editor.replaceRange(`[Marvin Task](${task.deepLink})`, editor.getCursor()); - // }) - // .catch(error => { - // new Notice('Could not create Marvin task: ' + error.message); - // }); + this.addMarvinTask(taskDetails.catId, taskDetails.task) + .then(task => { + editor.replaceRange(`- [${task.done ? 'x' : ' '}] [⚓](${task.deepLink}) ${this.formatTaskDetails(task as Task, '')} ${task.title}`, editor.getCursor()); + }) + .catch(error => { + new Notice('Could not create Marvin task: ' + error.message); + }); }).open(); } else { // Handle the case where categories could not be loaded @@ -152,6 +151,50 @@ export default class AmazingMarvinPlugin extends Plugin { }); } + async addMarvinTask(catId: string, taskTitle: string): Promise { + const opt = this.settings; + + let requestBody = (catId === '' || catId === undefined || catId === "root" || catId === "__inbox-faux__") ? + { + title : taskTitle, + timeZoneOffset: new Date().getTimezoneOffset(), + } + : + { + title: taskTitle, + timeZoneOffset: new Date().getTimezoneOffset(), + parentId: catId, + }; + + try { + const remoteResponse = await requestUrl({ + url: `https://serv.amazingmarvin.com/api/addTask`, + method: 'POST', + headers: { + 'X-API-Token': opt.apiKey, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }); + + if (remoteResponse.status === 200) { + new Notice("Task added in Amazing Marvin."); + return this.decorateWithDeepLink(remoteResponse.json) as Task; + } + } catch (error) { + const errorNote = document.createDocumentFragment(); + errorNote.appendText('Error creating task in Amazing Marvin. Try again or do it'); + const a = document.createElement('a'); + a.href = 'https://app.amazingmarvin.com/'; + a.text = 'manually'; + a.target = '_blank'; + errorNote.appendChild(a); + + new Notice(errorNote, 0); + console.error('Error creating task:', error); + } + return Promise.reject(new Error('Error creating task')); + } onunload() { }