diff --git a/README.md b/README.md index f434458..851f1aa 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ The experience with this plugin is a lot like the excellent plugins for todoist: - [Ultimate Todoist Sync for Obsidian Plugin](https://github.com/HeroBlackInk/ultimate-todoist-sync-for-obsidian) - [Obsidian Todoist Plugin](https://github.com/jamiebrynes7/obsidian-todoist-plugin) +Supported Vikunja API: v0.24.1 + ## Features Every feature prioritizes the Obsidian side. If a task is updated in both systems, the Obsidian task will be the one who diff --git a/src/commands/index.ts b/src/commands/index.ts index c372526..449b888 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -48,6 +48,12 @@ export default class Commands { foundProblem = true; } + if (this.plugin.settings.availableViews.length === 0) { + new Notice("Vikunja Plugin: No views found. Please configure the plugin before using it."); + if (this.plugin.settings.debugging) console.log("Vikunja Plugin: No views found. Please configure the plugin before using it."); + foundProblem = true; + } + if (foundProblem) { new Notice( "Vikunja Plugin: Found problems. Please fix them in settings before using the plugin." diff --git a/src/settings/mainSetting.ts b/src/settings/mainSetting.ts index 8e0b209..6984255 100644 --- a/src/settings/mainSetting.ts +++ b/src/settings/mainSetting.ts @@ -2,7 +2,7 @@ import {App, Notice, PluginSettingTab, Setting} from "obsidian"; import VikunjaPlugin from "../../main"; import {Projects} from "../vikunja/projects"; import {backendToFindTasks, chooseOutputFile, supportedTasksPluginsFormat} from "../enums"; -import {ModelsProject} from "../../vikunja_sdk"; +import {ModelsProject, ModelsProjectView} from "../../vikunja_sdk"; import {appHasDailyNotesPluginLoaded} from "obsidian-daily-notes-interface"; export interface VikunjaPluginSettings { @@ -26,7 +26,10 @@ export interface VikunjaPluginSettings { cronInterval: number, updateOnStartup: boolean, updateOnCursorMovement: boolean, - pullTasksOnlyFromDefaultProject: boolean + pullTasksOnlyFromDefaultProject: boolean, + availableViews: ModelsProjectView[], + selectedView: number, + selectBucketForDoneTasks: number, } export const DEFAULT_SETTINGS: VikunjaPluginSettings = { @@ -50,7 +53,10 @@ export const DEFAULT_SETTINGS: VikunjaPluginSettings = { cronInterval: 500, updateOnStartup: false, updateOnCursorMovement: false, - pullTasksOnlyFromDefaultProject: false + pullTasksOnlyFromDefaultProject: false, + availableViews: [], + selectedView: 0, + selectBucketForDoneTasks: 0, } export class MainSetting extends PluginSettingTab { @@ -468,11 +474,49 @@ export class MainSetting extends PluginSettingTab { dropdown.onChange(async (value: string) => { this.plugin.settings.defaultVikunjaProject = parseInt(value); if (this.plugin.settings.debugging) console.log(`SettingsTab: Selected Vikunja project:`, this.plugin.settings.defaultVikunjaProject); + + this.plugin.settings.availableViews = await this.projectsApi.getViewsByProjectId(this.plugin.settings.defaultVikunjaProject); + if (this.plugin.settings.debugging) console.log(`SettingsTab: Available views:`, this.plugin.settings.availableViews); + + if (this.plugin.settings.availableViews.length === 1) { + const id = this.plugin.settings.availableViews[0].id; + if (id === undefined) throw new Error("View id is undefined"); + this.plugin.settings.selectedView = id; + this.plugin.settings.selectBucketForDoneTasks = await this.projectsApi.getDoneBucketIdFromKanbanView(this.plugin.settings.defaultVikunjaProject); + if (this.plugin.settings.debugging) console.log(`SettingsTab: Done bucket set to:`, this.plugin.settings.selectBucketForDoneTasks); + } await this.plugin.saveSettings(); + this.display(); }); } ) + if (this.plugin.settings.availableViews.length > 1) { + new Setting(containerEl) + .setName("Select bucket") + .setDesc("Because vikunja does not move done tasks to the correct bucket, you have to select the bucket where the done tasks are placed, so this plugin can do it for you.") + .addDropdown(dropdown => { + let i = 0; + for (const view of this.plugin.settings.availableViews) { + if (view.id === undefined || view.title === undefined) { + throw new Error("View id or title is undefined"); + } + dropdown.addOption((i++).toString(), view.title); + } + + dropdown.setValue(this.plugin.settings.selectedView.toString()); + + dropdown.onChange(async (value: string) => { + this.plugin.settings.selectedView = parseInt(value); + if (this.plugin.settings.debugging) console.log(`SettingsTab: Selected Vikunja bucket:`, this.plugin.settings.selectedView); + + this.plugin.settings.selectBucketForDoneTasks = await this.projectsApi.getDoneBucketIdFromKanbanView(this.plugin.settings.defaultVikunjaProject); + if (this.plugin.settings.debugging) console.log(`SettingsTab: Done bucket set to:`, this.plugin.settings.selectBucketForDoneTasks); + await this.plugin.saveSettings(); + }); + }); + } + new Setting(containerEl) .setName("Pull tasks only from default project") .setDesc("If enabled, only tasks from the default project will be pulled from Vikunja. Useful, if you use Vikunja with several apps or different projects and Obsidian is only one of them. Beware: If you select that labels should be deleted in vikunja, if not found in vault, this will sync all labels regardless of projects.") diff --git a/src/vikunja/projects.ts b/src/vikunja/projects.ts index a66bb02..0fcbb7e 100644 --- a/src/vikunja/projects.ts +++ b/src/vikunja/projects.ts @@ -1,6 +1,16 @@ import {App} from "obsidian"; import VikunjaPlugin from "../../main"; -import {Configuration, ModelsTask, ProjectApi} from "../../vikunja_sdk"; +import { + Configuration, + ModelsBucket, + ModelsProjectView, + ModelsProjectViewKind, + ModelsTask, + ProjectApi, + ProjectsIdViewsViewBucketsPutRequest, + ProjectsProjectViewsGetRequest, + ProjectsProjectViewsIdPostRequest +} from "../../vikunja_sdk"; class Projects { plugin: VikunjaPlugin; @@ -26,6 +36,65 @@ class Projects { async getProjectById(id: number): Promise { return this.projectsApi.projectsIdGet({id: id}); } + + async getViewsByProjectId(id: number): Promise { + const params: ProjectsProjectViewsGetRequest = { + project: id + }; + const views = await this.projectsApi.projectsProjectViewsGet(params); + if(this.plugin.settings.debugging) console.log("Found views", views); + + // @ts-ignore + const kanbanViews = views.filter(view => view.viewKind === "kanban"); + if (this.plugin.settings.debugging) console.log("Found kanban views", kanbanViews); + + return kanbanViews; + } + + // Get the done bucket id from the Kanban view + // If no done bucket is found, create a new one, call it Done + async getDoneBucketIdFromKanbanView(id: number): Promise { + const params: ProjectsProjectViewsGetRequest = { + project: id + }; + const views = await this.projectsApi.projectsProjectViewsGet(params); + if (this.plugin.settings.debugging) console.log("Found views", views); + + const kanbanView = views.find(view => view.id === this.plugin.settings.selectedView); + if (!kanbanView) throw new Error("No Kanban view found"); + + let done_bucket = kanbanView.doneBucketId; + if (done_bucket === undefined) { + throw new Error("No done bucket id found"); + } + + if (done_bucket === 0) { + if (kanbanView.id === undefined) throw new Error("No Kanban view id found"); + + // create new bucket, so we can set this as done bucket + const bucket_done: ModelsBucket = { + title: "Done", + }; + const params: ProjectsIdViewsViewBucketsPutRequest = { + id: id, + view: kanbanView.id, + bucket: bucket_done, + + } + const bucket = await this.projectsApi.projectsIdViewsViewBucketsPut(params); + kanbanView.doneBucketId = bucket.id; + const paramsView: ProjectsProjectViewsIdPostRequest = { + project: id, + id: kanbanView.id, + view: kanbanView + } + const kanbanViewUpdated = await this.projectsApi.projectsProjectViewsIdPost(paramsView); + if (this.plugin.settings.debugging) console.log("Updated Kanban view", kanbanViewUpdated); + if (kanbanViewUpdated.doneBucketId === undefined) throw new Error("No done bucket id found"); + done_bucket = kanbanViewUpdated.doneBucketId; + } + return done_bucket; + } } export {Projects}; diff --git a/src/vikunja/tasks.ts b/src/vikunja/tasks.ts index 6b59231..6840e28 100644 --- a/src/vikunja/tasks.ts +++ b/src/vikunja/tasks.ts @@ -5,7 +5,6 @@ import { ModelsTask, ProjectsIdTasksPutRequest, TaskApi, - TasksAllGetRequest, TasksIdDeleteRequest, TasksIdGetRequest, TasksIdPostRequest, @@ -37,6 +36,9 @@ class Tasks { async updateTask(task: ModelsTask): Promise { if (!task.id) throw new Error("TasksApi: Task id is not defined"); if (this.plugin.settings.debugging) console.log("TasksApi: Updating task", task.id, task); + if (task.done) { + task.bucketId = this.plugin.settings.selectBucketForDoneTasks; + } const param: TasksIdPostRequest = {id: task.id, task: task}; return this.tasksApi.tasksIdPost(param); } @@ -55,6 +57,10 @@ class Tasks { // filepath could be an issue, because this information is dropped right before calling this method right now // Another problem is, that it cannot track moved tasks in the vault + if (task.done) { + task.bucketId = this.plugin.settings.selectBucketForDoneTasks; + } + const param: ProjectsIdTasksPutRequest = { id: task.projectId, task: task @@ -141,6 +147,7 @@ class Tasks { async updateProjectsIdInVikunja(tasks: ModelsTask[], projectId: number) { if (this.plugin.settings.debugging) console.log("TasksApi: Updating project id in tasks", projectId); + // FIXME there is a bulkPost in tasksApi, use it instead of update any task separately return await Promise.all(tasks.map(task => this.updateProjectIdInVikunja(task, projectId))); } @@ -152,6 +159,12 @@ class Tasks { await this.updateTask(task); } + async updateBucketInVikunja(task: ModelsTask, bucketId: number) { + if (!task.id) throw new Error("TasksApi: Task id is not defined"); + if (this.plugin.settings.debugging) console.log("TasksApi: Updating bucket in task", task.id, bucketId); + + } + private async updateLabelsInVikunja(task: ModelsTask) { try { await this.addLabelToTask(task);