mirror of
https://github.com/poanse/obsidian-taskmap.git
synced 2026-07-22 06:05:58 +00:00
task deletion bugfix and github attestation added
This commit is contained in:
parent
5b47fcd94a
commit
3adda62610
9 changed files with 228 additions and 50 deletions
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -58,23 +58,15 @@ export class RemoveTaskSingleAction implements Action {
|
|||
|
||||
do(data: ProjectData): void {
|
||||
const task = data.getTask(this.taskId);
|
||||
const parentTask = data.getTask(task.parentId);
|
||||
const parentId = task.parentId;
|
||||
task.deleted = true;
|
||||
this.children = data.getChildren(this.taskId);
|
||||
data.getChildren(task.parentId).forEach((taskId) => {
|
||||
const t = data.getTask(taskId);
|
||||
if (t.priority > task.priority) {
|
||||
t.priority += this.children!.length;
|
||||
}
|
||||
});
|
||||
this.children.forEach((taskId) => {
|
||||
const t = data.getTask(taskId);
|
||||
t.priority += task.priority;
|
||||
t.parentId = parentTask.taskId;
|
||||
t.depth = parentTask.depth + 1;
|
||||
});
|
||||
data.recalcPriorities(task.parentId);
|
||||
data.recalcStatusRecursive(task.parentId);
|
||||
// Promote the children into the deleted task's slot, or recalc the parent.
|
||||
data.reparentChildren(
|
||||
this.children,
|
||||
parentId,
|
||||
task.priority,
|
||||
);
|
||||
data.updateTasksView();
|
||||
}
|
||||
|
||||
|
|
@ -84,14 +76,7 @@ export class RemoveTaskSingleAction implements Action {
|
|||
}
|
||||
const task = data.getTask(this.taskId);
|
||||
task.deleted = false;
|
||||
this.children.forEach((taskId) => {
|
||||
const t = data.getTask(taskId);
|
||||
t.parentId = task.taskId;
|
||||
t.depth = task.depth + 1;
|
||||
});
|
||||
data.recalcPriorities(task.parentId);
|
||||
data.recalcPriorities(task.taskId);
|
||||
data.recalcStatusRecursive(task.taskId);
|
||||
data.reparentChildren(this.children, task.taskId);
|
||||
this.children = undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -250,22 +235,16 @@ 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 = -1;
|
||||
data.changeParent(this.taskId, this.newParentId);
|
||||
data.recalcPriorities(this.newParentId);
|
||||
data.recalcPriorities(this.oldParentId);
|
||||
// Insert at the beginning of the new parent's children.
|
||||
data.reparentChild(this.taskId, this.newParentId, 0);
|
||||
}
|
||||
|
||||
undo(data: ProjectData) {
|
||||
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);
|
||||
// Restore the task to its exact original slot among its old siblings.
|
||||
data.reparentChild(this.taskId, this.oldParentId, this.oldPriority);
|
||||
this.oldParentId = undefined;
|
||||
this.oldPriority = undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,17 +212,93 @@ export class ProjectData {
|
|||
return this.getTask(taskId).deleted;
|
||||
}
|
||||
|
||||
public changeParent(taskId: TaskId, newParentId: TaskId) {
|
||||
const taskData = this.getTask(taskId);
|
||||
const oldParentId = taskData.parentId;
|
||||
taskData.parentId = newParentId;
|
||||
this.rebuildCaches();
|
||||
this.recalcStatusRecursive(oldParentId);
|
||||
this.recalcStatusRecursive(newParentId);
|
||||
this.getDescendantIds(taskId).forEach((taskId) => {
|
||||
const task = this.getTask(taskId);
|
||||
task.depth = this.getTask(task.parentId).depth + 1;
|
||||
});
|
||||
// Normalize a parent's child priorities and refresh its status upward.
|
||||
public recalcParent(parentId: TaskId) {
|
||||
this.recalcPriorities(parentId);
|
||||
this.recalcStatusRecursive(parentId);
|
||||
}
|
||||
|
||||
public reparentChild(
|
||||
taskId: TaskId,
|
||||
newParentId: TaskId,
|
||||
insertPriority?: number,
|
||||
) {
|
||||
this.reparentChildren([taskId], newParentId, insertPriority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reparent tasks under newParentId with a single cache rebuild. Updates
|
||||
* subtree depths and inserts the batch into the new parent's priority
|
||||
* ordering, preserving the batch's own relative order while existing
|
||||
* siblings shift to make room. When insertPriority is omitted the batch is
|
||||
* appended after the current siblings.
|
||||
*
|
||||
* When taskIds is empty, recalcParent runs for newParentId and its parent.
|
||||
*/
|
||||
public reparentChildren(
|
||||
taskIds: TaskId[],
|
||||
newParentId: TaskId,
|
||||
insertPriority?: number,
|
||||
) {
|
||||
const oldParentIds: TaskId[] = [];
|
||||
for (const taskId of taskIds) {
|
||||
const parentId = this.getTask(taskId).parentId;
|
||||
if (!oldParentIds.includes(parentId)) {
|
||||
oldParentIds.push(parentId);
|
||||
}
|
||||
}
|
||||
if (taskIds.length > 0) {
|
||||
// Capture the batch's relative order before assigning new priorities.
|
||||
const ordered = [...taskIds].sort(
|
||||
(a, b) => this.getTask(a).priority - this.getTask(b).priority,
|
||||
);
|
||||
for (const taskId of taskIds) {
|
||||
this.getTask(taskId).parentId = newParentId;
|
||||
}
|
||||
this.rebuildCaches();
|
||||
for (const taskId of taskIds) {
|
||||
this.getDescendantIds(taskId).forEach((descendantId) => {
|
||||
const descendant = this.getTask(descendantId);
|
||||
descendant.depth =
|
||||
this.getTask(descendant.parentId).depth + 1;
|
||||
});
|
||||
}
|
||||
const siblings = this.getChildren(newParentId).filter(
|
||||
(id) => !taskIds.includes(id),
|
||||
);
|
||||
const insertAt =
|
||||
insertPriority ??
|
||||
siblings.reduce(
|
||||
(max, id) => Math.max(max, this.getTask(id).priority + 1),
|
||||
0,
|
||||
);
|
||||
siblings.forEach((id) => {
|
||||
const sibling = this.getTask(id);
|
||||
if (sibling.priority >= insertAt) {
|
||||
sibling.priority += taskIds.length;
|
||||
}
|
||||
});
|
||||
ordered.forEach((id, idx) => {
|
||||
this.getTask(id).priority = insertAt + idx;
|
||||
});
|
||||
}
|
||||
const parentsToRecalc: TaskId[] = [];
|
||||
const addParent = (parentId: TaskId) => {
|
||||
if (!parentsToRecalc.includes(parentId)) {
|
||||
parentsToRecalc.push(parentId);
|
||||
}
|
||||
};
|
||||
addParent(newParentId);
|
||||
oldParentIds.forEach(addParent);
|
||||
if (taskIds.length === 0) {
|
||||
const parentId = this.getTask(newParentId).parentId;
|
||||
if (parentId !== NoTaskId) {
|
||||
addParent(parentId);
|
||||
}
|
||||
}
|
||||
for (const parentId of parentsToRecalc) {
|
||||
this.recalcParent(parentId);
|
||||
}
|
||||
}
|
||||
|
||||
public recalcStatusRecursive(taskId: TaskId) {
|
||||
|
|
|
|||
110
tests/remove-task-priorities.test.ts
Normal file
110
tests/remove-task-priorities.test.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import * as assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { NoTaskId } from "../src/NodePositionsCalculator";
|
||||
import { RemoveTaskSingleAction } 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: <T>(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]);
|
||||
});
|
||||
|
|
@ -47,6 +47,7 @@ function createProjectData(): ProjectData {
|
|||
blockerPairs: [],
|
||||
folderPath: undefined,
|
||||
curTaskId: 8,
|
||||
taskSizeOverrides: [],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +81,8 @@ void test("reparenting normalizes sibling priorities for both parents", () => {
|
|||
|
||||
assert.equal(data.getTask(4).parentId, 2);
|
||||
assertChildrenByPriority(data, 1, [3, 5]);
|
||||
assertChildrenByPriority(data, 2, [6, 7, 4]);
|
||||
// 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", () => {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"types": ["node"],
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7"]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.svelte"]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
"0.1.9": "1.12.4"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue