Skip to content

Commit

Permalink
complete rewrite of labeling and caching of labels to fix issue #7
Browse files Browse the repository at this point in the history
  • Loading branch information
Heiss committed Jul 20, 2024
1 parent 82046ae commit 83d4d83
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 128 deletions.
3 changes: 3 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand All @@ -15,6 +16,7 @@ export default class VikunjaPlugin extends Plugin {
labelsApi: Label;
processor: Processor;
commands: Commands;
projectsApi: Projects;

async onload() {
await this.loadSettings();
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion src/processing/automaton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 1 addition & 10 deletions src/processing/createTasks.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -25,12 +18,10 @@ class CreateTasks implements IAutomatonSteps {

async step(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise<StepsOutput> {
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);
Expand Down
40 changes: 13 additions & 27 deletions src/processing/syncLabels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,35 @@ class SyncLabels implements IAutomatonSteps {
}

async step(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise<StepsOutput> {
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<number, ModelsTask>();

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<PluginTask[]> {
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");
Expand Down
13 changes: 5 additions & 8 deletions src/settings/mainSetting.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
}
}
123 changes: 60 additions & 63 deletions src/vikunja/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,80 +7,80 @@ import {
LabelsIdPutRequest,
LabelsPutRequest,
ModelsLabel,
ModelsLabelTask,
TasksTaskIDLabelsBulkPostRequest,
TasksTaskLabelsPutRequest,
} from "../../vikunja_sdk";

class Label {
plugin: VikunjaPlugin;
labelsApi: LabelsApi;
labelsMap: Map<string, ModelsLabel>;

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<string, ModelsLabel>();
this.loadLabels().then();
}

async getLabels(): Promise<ModelsLabel[]> {
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<ModelsLabel | undefined> {
const labels = await this.getLabels();
return labels.find(label => label.title === title);
async findLabelByTitle(title: string): Promise<ModelsLabel> {
const labels = this.labelsMap.get(title);
if (labels === undefined) throw new Error("Label not found");
return labels;
}

async createLabel(label: ModelsLabel): Promise<ModelsLabel> {
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<ModelsLabel[]> {
return Promise.all(labels.map(label => this.createLabel(label)));
}

async getOrCreateLabels(labels: ModelsLabel[]): Promise<ModelsLabel[]> {
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<ModelsLabel> {
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 = {
Expand All @@ -92,34 +92,15 @@ class Label {

async updateLabel(label: ModelsLabel): Promise<ModelsLabel> {
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<ModelsLabel[]> {
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<number, ModelsLabel>();

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[]) {
Expand All @@ -128,6 +109,22 @@ class Label {
await this.deleteLabel(label.id);
}
}

private async loadLabels(): Promise<ModelsLabel[]> {
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};
Loading

0 comments on commit 83d4d83

Please sign in to comment.