From aea63cfc0df92e22ae572447a6cf79f9f9daced2 Mon Sep 17 00:00:00 2001 From: Poluboiarinov Anton <50020771+poanse@users.noreply.github.com> Date: Sat, 23 May 2026 19:01:06 +0300 Subject: [PATCH] Fixed task priorities after reparent (#3) * fixed priorities bug when parent is changed. also added a fixing migration to existing projects * added auto test for priorities after reparent --- eslint.config.mjs | 2 +- manifest.json | 2 +- package-lock.json | 2 +- package.json | 3 ++- scripts/perf-tasks.ts | 1 + src/data/Action.ts | 13 ++++++++++++- src/data/ProjectData.svelte.ts | 11 ++++++++++- src/data/ProjectDataSchema.ts | 9 +++++++-- 8 files changed, 35 insertions(+), 8 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 481bc26..91bba3a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -65,7 +65,7 @@ export default defineConfig([ }, }, { - files: ["scripts/**/*.ts", "*.mjs"], + files: ["scripts/**/*.ts", "tests/**/*.ts", "*.mjs"], languageOptions: { globals: { ...globals.node, diff --git a/manifest.json b/manifest.json index 48e979d..f0508b0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "taskmap", "name": "Taskmap", - "version": "0.1.6", + "version": "0.1.7", "minAppVersion": "1.12.4", "description": "Plan projects via interactive GUI task trees with automatic layout.", "author": "poanse", diff --git a/package-lock.json b/package-lock.json index fc1e8d3..7e4fc7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "obsidian-taskmap", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index e871f8f..0a9d2ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-taskmap", - "version": "0.1.6", + "version": "0.1.7", "description": "Taskmap plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { @@ -9,6 +9,7 @@ "version": "node version-bump.mjs && git add manifest.json versions.json", "svelte-check": "svelte-check --tsconfig tsconfig.json", "perf:tasks": "tsx scripts/perf-tasks.ts", + "test": "tsx --test tests/reparent-priorities.test.ts", "lint": "eslint .", "lint:fix": "eslint . --fix" }, diff --git a/scripts/perf-tasks.ts b/scripts/perf-tasks.ts index 741bad8..2b63867 100644 --- a/scripts/perf-tasks.ts +++ b/scripts/perf-tasks.ts @@ -87,6 +87,7 @@ function randomTaskIds(count: number, picks: number): TaskId[] { function createProjectData(tasks: TaskData[]): ProjectData { return new ProjectData({ + schemaVersion: undefined, tasks: tasks.map((t) => ({ ...t })), blockerPairs: [], folderPath: undefined, diff --git a/src/data/Action.ts b/src/data/Action.ts index d75accf..f80ad73 100644 --- a/src/data/Action.ts +++ b/src/data/Action.ts @@ -224,6 +224,7 @@ export class ChangeParentAction implements Action { private taskId: TaskId; private newParentId: TaskId; private oldParentId?: TaskId; + private oldPriority?: number; constructor(taskId: TaskId, newParentId: number) { this.taskId = taskId; @@ -232,15 +233,25 @@ export class ChangeParentAction implements Action { do(data: ProjectData) { this.oldParentId = data.getTask(this.taskId).parentId; + this.oldPriority = data.getTask(this.taskId).priority; + data.getTask(this.taskId).priority = data.getChildren(this.newParentId).length; data.changeParent(this.taskId, this.newParentId); + data.recalcPriorities(this.newParentId); + data.recalcPriorities(this.oldParentId); } undo(data: ProjectData) { - if (this.oldParentId === undefined) { + if (this.oldParentId === undefined || this.oldPriority === undefined) { throw new Error(); } + // Use oldPriority - 0.5 so recalcPriorities places task exactly at oldPriority + // (sibling priorities are always whole numbers, so this slots in unambiguously) + data.getTask(this.taskId).priority = this.oldPriority - 0.5; data.changeParent(this.taskId, this.oldParentId); + data.recalcPriorities(this.oldParentId); + data.recalcPriorities(this.newParentId); this.oldParentId = undefined; + this.oldPriority = undefined; } } diff --git a/src/data/ProjectData.svelte.ts b/src/data/ProjectData.svelte.ts index a4d7e70..0def736 100644 --- a/src/data/ProjectData.svelte.ts +++ b/src/data/ProjectData.svelte.ts @@ -6,7 +6,10 @@ } from "../types"; import { SvelteMap } from "svelte/reactivity"; import { NoTaskId, RootTaskId } from "../NodePositionsCalculator"; -import type { ProjectFileParsed } from "./ProjectDataSchema"; +import { + TASKMAP_FILE_SCHEMA_VERSION, + type ProjectFileParsed, +} from "./ProjectDataSchema"; export class ProjectData { // cannot use just SvelteMap because it breaks reactivity @@ -22,6 +25,7 @@ export class ProjectData { public static getDefault(): ProjectData { return new ProjectData({ + schemaVersion: TASKMAP_FILE_SCHEMA_VERSION, tasks: new Array(), blockerPairs: new Array(), folderPath: undefined, @@ -39,6 +43,11 @@ export class ProjectData { this.addRootTask(); } this.rebuildCaches(); + if ((obj.schemaVersion ?? 0) < TASKMAP_FILE_SCHEMA_VERSION) { + for (const parentId of this.childrenCache.keys()) { + this.recalcPriorities(parentId); + } + } } public markTasksUpdated() { diff --git a/src/data/ProjectDataSchema.ts b/src/data/ProjectDataSchema.ts index e62f8cd..6b3361b 100644 --- a/src/data/ProjectDataSchema.ts +++ b/src/data/ProjectDataSchema.ts @@ -2,8 +2,11 @@ import * as v from "valibot"; import type { FlatErrors } from "valibot"; import { StatusCode, type BlockerPair, type TaskData } from "../types"; -/** Bump when the on-disk JSON shape changes (migrations can branch on this). */ -export const TASKMAP_FILE_SCHEMA_VERSION = 1 as const; +/** + * Bump when the on-disk JSON shape changes (migrations can branch on this). + * v2: normalize sibling priorities after reparent + */ +export const TASKMAP_FILE_SCHEMA_VERSION = 2 as const; const statusCodeSchema = v.picklist([ StatusCode.DRAFT, @@ -45,6 +48,7 @@ export const projectFileSchema = v.object({ }); export type ProjectFileParsed = { + schemaVersion: number | undefined; tasks: TaskData[]; blockerPairs: BlockerPair[]; folderPath: string | undefined; @@ -89,6 +93,7 @@ export function parseProjectFileJson(parsed: unknown): ProjectFileParsed { } const o = result.output; return { + schemaVersion: o.schemaVersion, tasks: o.tasks, blockerPairs: o.blockerPairs ?? [], folderPath: o.folderPath,