From 83d4d831fbd86825bff56114b7ba08e6c6de9c5c Mon Sep 17 00:00:00 2001 From: Peter Heiss Date: Sat, 20 Jul 2024 15:42:39 +0200 Subject: [PATCH] complete rewrite of labeling and caching of labels to fix issue #7 --- main.ts | 3 + src/commands/index.ts | 2 +- src/processing/automaton.ts | 2 +- src/processing/createTasks.ts | 11 +-- src/processing/syncLabels.ts | 40 ++++------- src/settings/mainSetting.ts | 13 ++-- src/vikunja/labels.ts | 123 +++++++++++++++++----------------- src/vikunja/tasks.ts | 25 ++----- 8 files changed, 91 insertions(+), 128 deletions(-) diff --git a/main.ts b/main.ts index fa6f11a..0c3a7e0 100644 --- a/main.ts +++ b/main.ts @@ -5,6 +5,7 @@ import {Processor} from "./src/processing/processor"; import {UserUser} from "./vikunja_sdk"; import {Label} from "./src/vikunja/labels"; import Commands from "./src/commands"; +import {Projects} from "./src/vikunja/projects"; // Remember to rename these classes and interfaces! @@ -15,6 +16,7 @@ export default class VikunjaPlugin extends Plugin { labelsApi: Label; processor: Processor; commands: Commands; + projectsApi: Projects; async onload() { await this.loadSettings(); @@ -24,6 +26,7 @@ export default class VikunjaPlugin extends Plugin { this.tasksApi = new Tasks(this.app, this); this.userObject = undefined; this.labelsApi = new Label(this.app, this); + this.projectsApi = new Projects(this.app, this); this.setupObsidian(); await this.processor.updateTasksOnStartup(); diff --git a/src/commands/index.ts b/src/commands/index.ts index 449b888..cb27410 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -108,7 +108,7 @@ export default class Commands { while (true) { const tasks = await this.plugin.tasksApi.getAllTasks(); - const labels = await this.plugin.labelsApi.getLabels() + const labels = this.plugin.labelsApi.getLabels() if (tasks.length === 0 && labels.length === 0) { if (this.plugin.settings.debugging) console.log("No tasks and labels found in Vikunja"); diff --git a/src/processing/automaton.ts b/src/processing/automaton.ts index dcfa214..05d7c38 100644 --- a/src/processing/automaton.ts +++ b/src/processing/automaton.ts @@ -41,10 +41,10 @@ class Automaton { this.steps = [ new GetTasks(app, plugin, processor), + new SyncLabels(app, plugin), new RemoveTasks(app, plugin), new CreateTasks(app, plugin, processor), new UpdateTasks(app, plugin, processor), - new SyncLabels(app, plugin), ]; this.status = AutomatonStatus.READY; diff --git a/src/processing/createTasks.ts b/src/processing/createTasks.ts index ff2317e..f2e1beb 100644 --- a/src/processing/createTasks.ts +++ b/src/processing/createTasks.ts @@ -1,14 +1,7 @@ import {IAutomatonSteps, StepsOutput} from "./automaton"; import {PluginTask} from "../vaultSearcher/vaultSearcher"; import {ModelsTask} from "../../vikunja_sdk"; -import {App, moment, Notice, TFile} from "obsidian"; -import { - appHasDailyNotesPluginLoaded, - createDailyNote, - getAllDailyNotes, - getDailyNote -} from "obsidian-daily-notes-interface"; -import {chooseOutputFile} from "../enums"; +import {App} from "obsidian"; import VikunjaPlugin from "../../main"; import {Processor} from "./processor"; @@ -25,12 +18,10 @@ class CreateTasks implements IAutomatonSteps { async step(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise { await this.createTasks(localTasks, vikunjaTasks); - return {localTasks, vikunjaTasks}; } private async createTasks(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]) { - if (this.plugin.settings.debugging) console.log("Step CreateTask: Creating tasks in Vikunja and vault", localTasks, vikunjaTasks); await this.processor.pullTasksFromVikunjaToVault(localTasks, vikunjaTasks); await this.processor.pushTasksFromVaultToVikunja(localTasks, vikunjaTasks); diff --git a/src/processing/syncLabels.ts b/src/processing/syncLabels.ts index ce9f475..7cce00b 100644 --- a/src/processing/syncLabels.ts +++ b/src/processing/syncLabels.ts @@ -14,49 +14,35 @@ class SyncLabels implements IAutomatonSteps { } async step(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise { - await this.removeLabelsInVikunjaIfNotInVault(localTasks, vikunjaTasks); + await this.removeLabelsInVikunjaIfNotInVault(localTasks); const localTasksWithLabels: PluginTask[] = await this.createLabels(localTasks); - for (const task of localTasksWithLabels) { - if (!task.task) throw new Error("Task is not defined"); - if (!task.task.id) throw new Error("Task id is not defined"); - const taskId = task.task.id; - if (!task.task.labels || task.task.labels.length === 0) continue; - - for (const label of task.task.labels) { - if (this.plugin.settings.debugging) console.log("Step SyncLabels: Adding label to task ", taskId, " in Vikunja", label); - await this.plugin.labelsApi.addLabelToTask(taskId, label); - } - } - return {localTasks: localTasksWithLabels, vikunjaTasks: vikunjaTasks}; } - private async removeLabelsInVikunjaIfNotInVault(localTasks: PluginTask[], _vikunjaTasks: ModelsTask[]) { + private async removeLabelsInVikunjaIfNotInVault(localTasks: PluginTask[]) { if (this.plugin.settings.debugging) console.log("Step SyncLabels: Deleting labels in Vikunja if not in vault."); + + const allLabels = this.plugin.labelsApi.getLabels(); + const dedupAllLabels = allLabels.filter((label, index, self) => self.findIndex(l => l.title === label.title) === index); + // remove all duplicated labels + await Promise.all(allLabels.filter(label => dedupAllLabels.find(l => l.id === label.id) === undefined).map(label => label.id && this.plugin.labelsApi.deleteLabel(label.id))); + if (!this.plugin.settings.removeLabelsIfInVaultNotUsed) { if (this.plugin.settings.debugging) console.log("Step SyncLabels: Not deleting labels in vikunja if ID not found in vault"); return; } - // FIXME This call will be placed everytime for every task. It should be cached or optimized away. - const allLabels = await this.plugin.labelsApi.getLabels(); const usedLabels = localTasks.flatMap(task => task.task.labels ?? []); - let foundLabels = new Map(); - - for (const label of allLabels) { - if (!label.id) throw new Error("Label id is not defined"); - if (usedLabels.find(usedLabel => usedLabel.title === label.title) && foundLabels.get(label.id) === undefined) { - foundLabels.set(label.id, label); - continue; - } + const labelsToDelete = dedupAllLabels.filter(label => usedLabels.find(l => l.title === label.title) === undefined); - if (this.plugin.settings.debugging) console.log("Step SyncLabels: Deleting label in vikunja", label); - await this.plugin.labelsApi.deleteLabel(label.id); - } + if (this.plugin.settings.debugging) console.log("Step SyncLabels: Deleting labels in Vikunja", labelsToDelete); + // remove all labels not used in vault + await Promise.all(labelsToDelete.map(label => label.id && this.plugin.labelsApi.deleteLabel(label.id))); } private async createLabels(localTasks: PluginTask[]): Promise { if (this.plugin.settings.debugging) console.log("Step SyncLabels: Creating labels in Vikunja if not existing."); + return await Promise.all(localTasks .map(async task => { if (!task.task) throw new Error("Task is not defined"); diff --git a/src/settings/mainSetting.ts b/src/settings/mainSetting.ts index 6984255..3a0e602 100644 --- a/src/settings/mainSetting.ts +++ b/src/settings/mainSetting.ts @@ -1,6 +1,5 @@ import {App, Notice, PluginSettingTab, Setting} from "obsidian"; import VikunjaPlugin from "../../main"; -import {Projects} from "../vikunja/projects"; import {backendToFindTasks, chooseOutputFile, supportedTasksPluginsFormat} from "../enums"; import {ModelsProject, ModelsProjectView} from "../../vikunja_sdk"; import {appHasDailyNotesPluginLoaded} from "obsidian-daily-notes-interface"; @@ -61,13 +60,11 @@ export const DEFAULT_SETTINGS: VikunjaPluginSettings = { export class MainSetting extends PluginSettingTab { plugin: VikunjaPlugin; - projectsApi: Projects; projects: ModelsProject[] = []; constructor(app: App, plugin: VikunjaPlugin) { super(app, plugin); this.plugin = plugin; - this.projectsApi = new Projects(app, plugin); } display(): void { @@ -475,14 +472,14 @@ export class MainSetting extends PluginSettingTab { 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); + this.plugin.settings.availableViews = await this.plugin.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); + this.plugin.settings.selectBucketForDoneTasks = await this.plugin.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(); @@ -510,7 +507,7 @@ export class MainSetting extends PluginSettingTab { 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); + this.plugin.settings.selectBucketForDoneTasks = await this.plugin.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(); }); @@ -540,7 +537,7 @@ export class MainSetting extends PluginSettingTab { } async loadApi() { - this.projects = await this.projectsApi.getAllProjects(); + this.projects = await this.plugin.projectsApi.getAllProjects(); // Set default project if not set if (this.projects.length > 0 && this.plugin.settings.defaultVikunjaProject === null && this.projects[0] !== undefined && this.projects[0].id !== undefined) { @@ -554,7 +551,7 @@ export class MainSetting extends PluginSettingTab { private resetApis() { // TODO: Implement an event to reload API configurations this.plugin.tasksApi.init(); - this.projectsApi.init(); + this.plugin.projectsApi.init(); this.plugin.labelsApi.init(); } } diff --git a/src/vikunja/labels.ts b/src/vikunja/labels.ts index 28ab3f0..9bfcde9 100644 --- a/src/vikunja/labels.ts +++ b/src/vikunja/labels.ts @@ -7,80 +7,80 @@ import { LabelsIdPutRequest, LabelsPutRequest, ModelsLabel, - ModelsLabelTask, - TasksTaskIDLabelsBulkPostRequest, - TasksTaskLabelsPutRequest, } from "../../vikunja_sdk"; class Label { plugin: VikunjaPlugin; labelsApi: LabelsApi; + labelsMap: Map; constructor(app: App, plugin: VikunjaPlugin) { this.plugin = plugin; this.init(); } - async addLabelsToTask(id: number, labels: ModelsLabel[]) { - - const params: TasksTaskIDLabelsBulkPostRequest = { - taskID: id, - label: {labels} - }; - await this.labelsApi.tasksTaskIDLabelsBulkPost(params); - } - - async addLabelToTask(id: number, label: ModelsLabel) { - const modelLabel: ModelsLabelTask = { - labelId: label.id, - }; - const params: TasksTaskLabelsPutRequest = { - task: id, - label: modelLabel - }; - try { - await this.labelsApi.tasksTaskLabelsPut(params); - } catch (e) { - console.error("Error adding label to task", e); - } - } - init() { const configuration = new Configuration({ basePath: this.plugin.settings.vikunjaHost + "/api/v1", apiKey: "Bearer " + this.plugin.settings.vikunjaAccessToken, }); this.labelsApi = new LabelsApi(configuration); + this.labelsMap = new Map(); + this.loadLabels().then(); } - async getLabels(): Promise { - let allLabels: ModelsLabel[] = []; - try { - allLabels = await this.labelsApi.labelsGet(); - } catch (e) { - // There is a bug in Vikunja API that returns null instead of an empty array - console.error("LabelsAPI: Could not get labels", e); - } - return allLabels; + getLabels(): ModelsLabel[] { + return Array.from(this.labelsMap.values()); } - async findLabelByTitle(title: string): Promise { - const labels = await this.getLabels(); - return labels.find(label => label.title === title); + async findLabelByTitle(title: string): Promise { + const labels = this.labelsMap.get(title); + if (labels === undefined) throw new Error("Label not found"); + return labels; } async createLabel(label: ModelsLabel): Promise { if (this.plugin.settings.debugging) console.log("LabelsAPI: Creating label", label); + if (!label.title) throw new Error("Label title is required to create label"); + if (this.labelsMap.has(label.title)) throw new Error("Label already exists"); + const param: LabelsPutRequest = { label: label, }; - return await this.labelsApi.labelsPut(param); + const createdLabel = await this.labelsApi.labelsPut(param); + + if (createdLabel.title === undefined) throw new Error("Label title is not defined"); + this.labelsMap.set(createdLabel.title, createdLabel); + if (this.plugin.settings.debugging) console.log("LabelsAPI: Created label", createdLabel); + + return createdLabel; } async createLabels(labels: ModelsLabel[]): Promise { return Promise.all(labels.map(label => this.createLabel(label))); } + async getOrCreateLabels(labels: ModelsLabel[]): Promise { + const labelsInVikunjaExisting = labels.filter(label => label.title && this.labelsMap.has(label.title)); + const labelsInVikunjaMissing = labels.filter(label => label.title && !this.labelsMap.has(label.title)); + + const createdLabel = await Promise.all(labelsInVikunjaMissing.map(label => this.createLabel(label))); + if (this.plugin.settings.debugging) console.log("LabelsAPI: Created labels", createdLabel); + const concatLabels = labelsInVikunjaExisting.concat(createdLabel); + if (this.plugin.settings.debugging) console.log("LabelsAPI: Returning labels", concatLabels); + // @ts-ignore + const labelsWithId: ModelsLabel[] = concatLabels.map(label => this.labelsMap.get(label.title)).filter(label => label !== undefined); + if (this.plugin.settings.debugging) console.log("LabelsAPI: Returning labels with id", labelsWithId); + return labelsWithId; + } + + async getOrCreateLabel(label: ModelsLabel): Promise { + if (!label.title) throw new Error("Label title is required to get or create label"); + const existingLabel = this.labelsMap.get(label.title); + if (existingLabel) return existingLabel; + return await this.createLabel(label); + } + async deleteLabel(labelId: number) { if (this.plugin.settings.debugging) console.log("LabelsAPI: Deleting label", labelId); const param: LabelsIdDeleteRequest = { @@ -92,34 +92,15 @@ class Label { async updateLabel(label: ModelsLabel): Promise { if (!label.id) throw new Error("Label id is required to update label"); + if (!label.title) throw new Error("Label title is required to update label"); + const param: LabelsIdPutRequest = { id: label.id, label: label, }; - return this.labelsApi.labelsIdPut(param); - } - async getOrCreateLabels(labels: ModelsLabel[]): Promise { - if (this.plugin.settings.debugging) console.log("LabelsAPI: Get or create labels", labels); - // FIXME This call will be placed everytime for every task. It should be cached or optimized away. - const allLabels = await this.getLabels(); - let createdLabels = new Map(); - - for (const label of labels) { - const vikunjaLabel = allLabels.find(l => l.title === label.title); - if (vikunjaLabel !== undefined && vikunjaLabel.id !== undefined && createdLabels.get(vikunjaLabel.id) === undefined) { - createdLabels.set(vikunjaLabel.id, vikunjaLabel); - continue; - } - - if (this.plugin.settings.debugging) console.log("LabelsAPI: Create label in vikunja", label); - const createdLabel = await this.createLabel(label); - if (!createdLabel.id) throw new Error("Label id for freshly created Label is not defined"); - createdLabels.set(createdLabel.id, createdLabel); - } - - if (this.plugin.settings.debugging) console.log("LabelsAPI: Created labels", createdLabels); - return Array.from(createdLabels.values()); + this.labelsMap.set(label.title, label); + return this.labelsApi.labelsIdPut(param); } async deleteLabels(labels: ModelsLabel[]) { @@ -128,6 +109,22 @@ class Label { await this.deleteLabel(label.id); } } + + private async loadLabels(): Promise { + this.labelsMap.clear(); + let allLabels: ModelsLabel[] = []; + try { + allLabels = await this.labelsApi.labelsGet(); + } catch (e) { + // There is a bug in Vikunja API that returns null instead of an empty array + console.error("LabelsAPI: Could not get labels", e); + } + allLabels.forEach(label => { + if (label.title === undefined) throw new Error("Label title is not defined"); + this.labelsMap.set(label.title, label); + }); + return allLabels; + } } export {Label}; diff --git a/src/vikunja/tasks.ts b/src/vikunja/tasks.ts index 6840e28..eb67d99 100644 --- a/src/vikunja/tasks.ts +++ b/src/vikunja/tasks.ts @@ -39,6 +39,9 @@ class Tasks { if (task.done) { task.bucketId = this.plugin.settings.selectBucketForDoneTasks; } + + await this.addLabelToTask(task); + const param: TasksIdPostRequest = {id: task.id, task: task}; return this.tasksApi.tasksIdPost(param); } @@ -97,24 +100,19 @@ class Tasks { if (!task.labels) return; if (this.plugin.settings.debugging) console.log("TasksApi: Adding labels to task", task.id, task.labels); - for (const label of task.labels) { + for (const label of await this.plugin.labelsApi.getOrCreateLabels(task.labels)) { if (!label.title) continue; - let existingLabel = await this.plugin.labelsApi.findLabelByTitle(label.title); - if (!existingLabel) { - existingLabel = await this.plugin.labelsApi.createLabel(label); - } - if (!existingLabel.id) throw new Error("TasksApi: Label id cannot be defined"); - const param: TasksTaskLabelsPutRequest = { - label: {labelId: existingLabel.id}, + label: {labelId: label.id}, task: task.id } try { + if (this.plugin.settings.debugging) console.log("TasksApi: Adding label to task", param); await this.plugin.labelsApi.labelsApi.tasksTaskLabelsPut(param); } catch (error) { - console.error("Error adding label to task. Mostly it has already this label assigned.", error); + if (this.plugin.settings.debugging) console.error("Error adding label to task, mostly because it is already there", error); } } } @@ -164,15 +162,6 @@ class Tasks { 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); - await this.deleteLabelFromTask(task); - } catch (error) { - console.error("Error updating labels in Vikunja", error); - } - } } export {Tasks};