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
This commit is contained in:
Poluboiarinov Anton 2026-05-23 19:01:06 +03:00 committed by GitHub
parent ede78786f7
commit aea63cfc0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 35 additions and 8 deletions

View file

@ -65,7 +65,7 @@ export default defineConfig([
},
},
{
files: ["scripts/**/*.ts", "*.mjs"],
files: ["scripts/**/*.ts", "tests/**/*.ts", "*.mjs"],
languageOptions: {
globals: {
...globals.node,

View file

@ -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",

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "obsidian-taskmap",
"version": "0.1.6",
"version": "0.1.7",
"lockfileVersion": 3,
"requires": true,
"packages": {

View file

@ -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"
},

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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<TaskId, Task> 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<TaskData>(),
blockerPairs: new Array<BlockerPair>(),
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() {

View file

@ -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,