poanse_obsidian-taskmap/src/data/ProjectData.svelte.ts
2026-06-09 03:47:23 +03:00

341 lines
8.9 KiB
TypeScript

import {
type BlockerPair,
StatusCode,
type TaskData,
type TaskId,
type Vector2,
} from "../types";
import { SvelteMap } from "svelte/reactivity";
import { NoTaskId, RootTaskId } from "../NodePositionsCalculator";
import {
TASKMAP_FILE_SCHEMA_VERSION,
type ProjectFileParsed,
} from "./ProjectDataSchema";
export class ProjectData {
// cannot use just SvelteMap<TaskId, Task> because it breaks reactivity
tasks: Array<TaskData>;
taskIndexCache = new SvelteMap<TaskId, number>();
childrenCache = new SvelteMap<TaskId, TaskId[]>();
ancestorsCache = new SvelteMap<TaskId, TaskId[]>();
descendantsCache = new SvelteMap<TaskId, TaskId[]>();
tasksViewUpdateCounter: number;
connectionsViewUpdateCounter: number;
blockerPairs: Array<BlockerPair>;
folderPath: string | undefined;
curTaskId = RootTaskId;
taskSizeOverrides: SvelteMap<TaskId, Vector2>;
public static getDefault(): ProjectData {
return new ProjectData({
schemaVersion: TASKMAP_FILE_SCHEMA_VERSION,
tasks: new Array<TaskData>(),
blockerPairs: new Array<BlockerPair>(),
folderPath: undefined,
curTaskId: 0,
taskSizeOverrides: [],
});
}
constructor(obj: ProjectFileParsed) {
// persistent data from disk
this.tasks = $state(obj.tasks);
this.blockerPairs = $state(obj.blockerPairs ?? []);
this.folderPath = obj.folderPath;
this.curTaskId = obj.curTaskId;
this.taskSizeOverrides = new SvelteMap(
(obj.taskSizeOverrides ?? []).map(({ taskId, x, y }) => [
taskId,
{ x, y },
]),
);
// temporary in-memory properties
this.tasksViewUpdateCounter = $state(0);
this.connectionsViewUpdateCounter = $state(0);
// root task must always be present
if (this.tasks.length == 0) {
this.addRootTask();
}
// initialize auxiliary data structures
this.rebuildCaches();
// fix broken priorities in old project versions
if ((obj.schemaVersion ?? 0) < TASKMAP_FILE_SCHEMA_VERSION) {
this.tasks.forEach((t) => this.recalcPriorities(t.taskId));
}
}
public updateTasksView() {
this.tasksViewUpdateCounter += 1;
this.updateConnectionsView();
}
public updateConnectionsView() {
this.connectionsViewUpdateCounter += 1;
}
private rebuildCaches() {
this.taskIndexCache.clear();
this.tasks.forEach((value, index) => {
this.taskIndexCache.set(value.taskId, index);
});
// Order matters: descendants rely on ancestors, and ancestors rely on children.
this.rebuildChildrenCache();
this.rebuildAncestorsCache();
this.rebuildDescendantsCache();
}
private rebuildChildrenCache() {
this.childrenCache = new SvelteMap<TaskId, TaskId[]>();
for (const task of this.getTasks()) {
const children = this.childrenCache.get(task.parentId) ?? [];
children.push(task.taskId);
this.childrenCache.set(task.parentId, children);
}
}
// Rebuild ancestors by traversing childrenCache top-down from root.
private rebuildAncestorsCache() {
this.ancestorsCache = new SvelteMap<TaskId, TaskId[]>();
const queue: TaskId[] = [RootTaskId];
while (queue.length > 0) {
const taskId = queue.shift();
if (taskId === undefined) {
continue;
}
const task = this.getTask(taskId);
if (task.depth === 0) {
this.ancestorsCache.set(taskId, []);
} else {
const parentAncestors =
this.ancestorsCache.get(task.parentId) ?? [];
this.ancestorsCache.set(taskId, [
task.parentId,
...parentAncestors,
]);
}
queue.push(...(this.childrenCache.get(taskId) ?? []));
}
}
private rebuildDescendantsCache() {
this.descendantsCache = new SvelteMap<TaskId, TaskId[]>();
for (const task of this.getTasks()) {
this.descendantsCache.set(task.taskId, [task.taskId]);
}
for (const taskId of this.tasks.keys()) {
for (const ancestorId of this.getAncestorIds(taskId)) {
const descendants = this.descendantsCache.get(ancestorId);
if (descendants !== undefined) {
descendants.push(taskId);
}
}
}
}
public addTask(task: TaskData) {
this.tasks.push(task);
this.rebuildCaches();
this.updateTasksView();
this.curTaskId++;
}
public removeTask() {
const task = this.tasks.pop();
if (task) {
this.rebuildCaches();
this.updateTasksView();
this.curTaskId--;
}
}
public addRootTask() {
const task = {
taskId: this.curTaskId,
parentId: NoTaskId,
status: StatusCode.IN_PROGRESS,
name: "root",
priority: 0,
depth: 0,
deleted: false,
hidden: false,
};
this.addTask(task);
}
public getDescendantIds(taskId: number) {
return this.descendantsCache.get(taskId) ?? [taskId];
}
public getAncestors(taskId: number) {
return this.getAncestorIds(taskId).map((id) => this.getTask(id));
}
public getAncestorIds(taskId: number) {
return [...(this.ancestorsCache.get(taskId) ?? [])];
}
public isAncestorOf(taskId: TaskId, candidate: TaskId) {
return this.getAncestorIds(taskId).includes(candidate);
}
public isDescendantOf(taskId: TaskId, candidate: TaskId) {
return this.getDescendantIds(taskId).includes(candidate);
}
public getChildren(taskId: number, includeDeleted: boolean = false) {
const childIds = this.childrenCache.get(taskId) ?? [];
if (includeDeleted) {
return [...childIds];
}
return childIds.filter((id) => !this.getTask(id).deleted);
}
public getTask(taskId: number) {
const res = this.tasks[this.taskIndexCache.get(taskId)!];
if (res) {
return res;
} else {
throw new Error(`No task found with id ${taskId}`);
}
}
public getTasks(includeDeleted: boolean = false) {
return [...this.tasks.values()].filter(
(t) => includeDeleted || !t.deleted,
);
}
public isTaskDeleted(taskId: number) {
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;
});
}
public recalcStatusRecursive(taskId: TaskId) {
if (taskId == RootTaskId) {
return;
}
const task = this.getTask(taskId);
if (task.status == StatusCode.DRAFT) {
return;
}
task.status = this.calculateStatus(taskId);
this.recalcStatusRecursive(task.parentId);
}
public calculateStatus(taskId: TaskId) {
const children = this.getChildren(taskId).map((x) => this.getTask(x));
if (children.length === 0) {
return this.getTask(taskId).status;
}
const counts = [0, 0, 0, 0];
children.forEach((t) => (counts[t.status] += 1));
if (counts[StatusCode.DONE] == children.length) {
return StatusCode.DONE;
} else if (counts[StatusCode.DONE] > 0) {
return StatusCode.IN_PROGRESS;
} else if (counts[StatusCode.IN_PROGRESS] > 0) {
return StatusCode.IN_PROGRESS;
} else if (counts[StatusCode.DRAFT] == children.length) {
return StatusCode.DRAFT;
} else if (counts[StatusCode.READY] == children.length) {
return StatusCode.READY;
} else {
return StatusCode.READY;
}
}
public setPriority(taskId: TaskId, newPriority: number) {
const task = this.getTask(taskId);
const oldPriority = task.priority;
task.priority = newPriority;
this.getChildren(task.parentId)
.filter((t) => t != taskId)
.forEach((child) => {
const childTask = this.getTask(child);
const childPriority = childTask.priority;
if (
newPriority > oldPriority &&
childPriority >= oldPriority &&
childPriority <= newPriority
) {
childTask.priority -= 1;
} else if (
newPriority < oldPriority &&
childPriority >= newPriority &&
childPriority <= oldPriority
) {
childTask.priority += 1;
}
});
}
public recalcPriorities(parentId: TaskId) {
this.getChildren(parentId)
.map((tId) => this.getTask(tId))
.sort((a, b) => a.priority - b.priority)
.forEach((t, idx) => {
t.priority = idx;
});
}
public containsBlockerPair = (blockerPair: BlockerPair) => {
return this.blockerPairs.some(
(p) =>
p.blocked === blockerPair.blocked &&
p.blocker === blockerPair.blocker,
);
};
public removeBlockerPair = (blockerPair: BlockerPair) => {
this.blockerPairs = this.blockerPairs.filter(
(p) =>
!(
p.blocker === blockerPair.blocker &&
p.blocked === blockerPair.blocked
),
);
};
public addBlockerPair = (blockerPair: BlockerPair) => {
this.blockerPairs.push(blockerPair);
};
public setTaskSizeOverride(taskId: TaskId, size: Vector2) {
this.taskSizeOverrides.set(taskId, size);
}
public deleteTaskSizeOverride(taskId: TaskId) {
this.taskSizeOverrides.delete(taskId);
}
public getTaskSizeOverride(taskId: TaskId): Vector2 | undefined {
return this.taskSizeOverrides.get(taskId);
}
public hasTaskSizeOverride(taskId: TaskId): boolean {
return this.taskSizeOverrides.has(taskId);
}
public getFolderPath = (): string | undefined => {
return this.folderPath;
};
public setFolderPath = (path: string | undefined) => {
this.folderPath = path === "" ? undefined : path;
};
}