diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3840d02..ff4cf61 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,6 +6,8 @@ on:
permissions:
contents: write
+ id-token: write
+ attestations: write
jobs:
build-and-upload:
@@ -29,8 +31,16 @@ jobs:
- name: Verify Files Exist
run: ls -la main.js styles.css manifest.json
+ - name: Generate Artifact Attestations
+ uses: actions/attest-build-provenance@v2
+ with:
+ subject-path: |
+ main.js
+ styles.css
+ manifest.json
+
- name: Upload Release Assets (Original Names)
- uses: softprops/action-gh-release@v1
+ uses: softprops/action-gh-release@v2
with:
files: |
main.js
diff --git a/.gitignore b/.gitignore
index aca1122..05fcffd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# cursor
+.cursor
+
# vscode
.vscode
diff --git a/manifest.json b/manifest.json
index e19c9b3..e4d30c9 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"id": "taskmap",
"name": "Taskmap",
- "version": "0.1.8",
+ "version": "0.1.9",
"minAppVersion": "1.12.4",
"description": "Plan projects via interactive GUI task trees with automatic layout.",
"author": "poanse",
diff --git a/package.json b/package.json
index 9c582f3..9119c1a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "obsidian-taskmap",
- "version": "0.1.8",
+ "version": "0.1.9",
"description": "Taskmap plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
@@ -9,7 +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",
+ "test": "tsx --test tests/*.test.ts",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
diff --git a/src/DraggingManager.svelte.ts b/src/DraggingManager.svelte.ts
index b58ef7d..ff7b8db 100644
--- a/src/DraggingManager.svelte.ts
+++ b/src/DraggingManager.svelte.ts
@@ -16,7 +16,7 @@ export class DraggingManager {
public onPointerDown = (e: PointerEvent) => {
console.debug("DraggingManager pointerDown");
- this.mouseDown = e.button as MouseDown;
+ this.mouseDown = e.button;
if (this.mouseCodes.includes(this.mouseDown)) {
this.startX = e.clientX;
this.startY = e.clientY;
diff --git a/src/components/Task.svelte b/src/components/Task.svelte
index b243802..8b2a196 100644
--- a/src/components/Task.svelte
+++ b/src/components/Task.svelte
@@ -119,6 +119,7 @@
{#key context.updateOnZoomCounter}
(value: T) => value,
+ });
+}
+
+function task(
+ taskId: TaskId,
+ parentId: TaskId,
+ depth: number,
+ priority: number,
+ name: string,
+): TaskData {
+ return {
+ taskId,
+ parentId,
+ depth,
+ priority,
+ deleted: false,
+ hidden: false,
+ status: StatusCode.READY,
+ name,
+ };
+}
+
+// Tree:
+// root (0)
+// A (1)
+// B (2) prio 0
+// C (4) prio 0
+// D (5) prio 1
+// E (3) prio 1
+// F (6) prio 2
+function createProjectData(): ProjectData {
+ return new ProjectData({
+ schemaVersion: TASKMAP_FILE_SCHEMA_VERSION,
+ tasks: [
+ task(0, NoTaskId, 0, 0, "root"),
+ task(1, 0, 1, 0, "A"),
+ task(2, 1, 2, 0, "B"),
+ task(3, 1, 2, 1, "E"),
+ task(4, 2, 3, 0, "C"),
+ task(5, 2, 3, 1, "D"),
+ task(6, 1, 2, 2, "F"),
+ ],
+ blockerPairs: [],
+ folderPath: undefined,
+ curTaskId: 7,
+ taskSizeOverrides: [],
+ });
+}
+
+function assertChildrenByPriority(
+ data: ProjectData,
+ parentId: TaskId,
+ expectedTaskIds: TaskId[],
+) {
+ const children = data
+ .getChildren(parentId)
+ .map((id) => data.getTask(id))
+ .sort((a, b) => a.priority - b.priority);
+
+ assert.deepEqual(
+ children.map((t) => t.taskId),
+ expectedTaskIds,
+ );
+ const priorities = children.map((t) => t.priority);
+ assert.ok(priorities.every(Number.isInteger));
+ assert.deepEqual(
+ priorities,
+ Array.from({ length: children.length }, (_, idx) => idx),
+ );
+}
+
+void test("removing a task promotes its children into its slot with contiguous priorities", () => {
+ const data = createProjectData();
+
+ // Delete B (id 2). Its children C (4) and D (5) should be promoted into A,
+ // taking B's slot, giving order C, D, E, F with priorities 0, 1, 2, 3.
+ new RemoveTaskSingleAction(2).do(data);
+
+ assert.equal(data.isTaskDeleted(2), true);
+ assert.equal(data.getTask(4).parentId, 1);
+ assert.equal(data.getTask(5).parentId, 1);
+ assert.equal(data.getTask(4).depth, 2);
+ assert.equal(data.getTask(5).depth, 2);
+ assertChildrenByPriority(data, 1, [4, 5, 3, 6]);
+});
+
+void test("undoing a task removal restores the original tree structure and priorities", () => {
+ const data = createProjectData();
+ const action = new RemoveTaskSingleAction(2);
+
+ action.do(data);
+ action.undo(data);
+
+ assert.equal(data.isTaskDeleted(2), false);
+ assert.equal(data.getTask(4).parentId, 2);
+ assert.equal(data.getTask(5).parentId, 2);
+ assertChildrenByPriority(data, 1, [2, 3, 6]);
+ assertChildrenByPriority(data, 2, [4, 5]);
+});
diff --git a/tests/reparent-priorities.test.ts b/tests/reparent-priorities.test.ts
new file mode 100644
index 0000000..248f477
--- /dev/null
+++ b/tests/reparent-priorities.test.ts
@@ -0,0 +1,98 @@
+import * as assert from "node:assert/strict";
+import { test } from "node:test";
+import { NoTaskId } from "../src/NodePositionsCalculator";
+import { ChangeParentAction } from "../src/data/Action";
+import { ProjectData } from "../src/data/ProjectData.svelte";
+import { TASKMAP_FILE_SCHEMA_VERSION } from "../src/data/ProjectDataSchema";
+import { StatusCode, type TaskData, type TaskId } from "../src/types";
+
+if (!("$state" in globalThis)) {
+ Object.defineProperty(globalThis, "$state", {
+ value: (value: T) => value,
+ });
+}
+
+function task(
+ taskId: TaskId,
+ parentId: TaskId,
+ depth: number,
+ priority: number,
+ name: string,
+): TaskData {
+ return {
+ taskId,
+ parentId,
+ depth,
+ priority,
+ deleted: false,
+ hidden: false,
+ status: StatusCode.READY,
+ name,
+ };
+}
+
+function createProjectData(): ProjectData {
+ return new ProjectData({
+ schemaVersion: TASKMAP_FILE_SCHEMA_VERSION,
+ tasks: [
+ task(0, NoTaskId, 0, 0, "root"),
+ task(1, 0, 1, 0, "old parent"),
+ task(2, 0, 1, 1, "new parent"),
+ task(3, 1, 2, 0, "old child before moved"),
+ task(4, 1, 2, 1, "moved child"),
+ task(5, 1, 2, 2, "old child after moved"),
+ task(6, 2, 2, 0, "new child first"),
+ task(7, 2, 2, 1, "new child second"),
+ ],
+ blockerPairs: [],
+ folderPath: undefined,
+ curTaskId: 8,
+ taskSizeOverrides: [],
+ });
+}
+
+function assertChildrenByPriority(
+ data: ProjectData,
+ parentId: TaskId,
+ expectedTaskIds: TaskId[],
+) {
+ const children = data
+ .getChildren(parentId)
+ .map((id) => data.getTask(id))
+ .sort((a, b) => a.priority - b.priority);
+
+ assert.deepEqual(
+ children.map((t) => t.taskId),
+ expectedTaskIds,
+ );
+ const priorities = children.map((t) => t.priority);
+ assert.ok(priorities.every(Number.isInteger));
+ assert.deepEqual(
+ priorities,
+ Array.from({ length: children.length }, (_, idx) => idx),
+ );
+}
+
+void test("reparenting normalizes sibling priorities for both parents", () => {
+ const data = createProjectData();
+
+ assert.equal(data.getTask(4).parentId, 1);
+ new ChangeParentAction(4, 2).do(data);
+
+ assert.equal(data.getTask(4).parentId, 2);
+ assertChildrenByPriority(data, 1, [3, 5]);
+ // Reparented task is inserted at the beginning of the new parent's children.
+ assertChildrenByPriority(data, 2, [4, 6, 7]);
+});
+
+void test("undoing a reparent restores the original sibling priority", () => {
+ const data = createProjectData();
+ const action = new ChangeParentAction(4, 2);
+
+ action.do(data);
+ action.undo(data);
+
+ assert.equal(data.getTask(4).parentId, 1);
+ assertChildrenByPriority(data, 1, [3, 4, 5]);
+ assertChildrenByPriority(data, 2, [6, 7]);
+});
diff --git a/tsconfig.json b/tsconfig.json
index 7b5ba36..093e496 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -13,6 +13,7 @@
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
+ "types": ["node"],
"lib": ["DOM", "ES5", "ES6", "ES7"]
},
"include": ["**/*.ts", "**/*.svelte"]
diff --git a/versions.json b/versions.json
index 26382a1..332b516 100644
--- a/versions.json
+++ b/versions.json
@@ -1,3 +1,3 @@
{
- "1.0.0": "0.15.0"
+ "0.1.9": "1.12.4"
}