diff --git a/src/addTaskModal.ts b/src/addTaskModal.ts new file mode 100644 index 0000000..74f0955 --- /dev/null +++ b/src/addTaskModal.ts @@ -0,0 +1,164 @@ +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; + 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; + let categoryInput: HTMLInputElement; + + contentEl.createEl("h1", { text: "New Amazing Marvin Task" }); + + new Setting(contentEl) + .setName("Category") + .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) + .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("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) + .addButton((btn) => + btn + .setButtonText("Submit") + .setCta() + .onClick(() => { + this.close(); + if (this.onSubmit && this.result.catId && this.result.task) { + this.onSubmit(this.result); + } + })); + } + + 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; + + 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..0315a49 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; @@ -45,9 +45,11 @@ const CONSTANTS = { categoriesEndpoint: '/api/categories', childrenEndpoint: '/api/children', scheduledOnDayEndpoint: '/api/todayItems', - dueOnDayEndpoint: '/api/dueItems' + dueOnDayEndpoint: '/api/dueItems', + addTaskEndpoint: '/api/addTask', } + export default class AmazingMarvinPlugin extends Plugin { settings: AmazingMarvinPluginSettings; @@ -76,6 +78,38 @@ 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); + + 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 + 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', @@ -117,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() { }