mirror of
https://github.com/poanse/obsidian-taskmap.git
synced 2026-07-22 06:05:58 +00:00
undo/redo on key press
This commit is contained in:
parent
043e54f867
commit
7753caba8f
17 changed files with 662 additions and 225 deletions
|
|
@ -1,5 +1,4 @@
|
|||
import TaskmapPlugin from "./main";
|
||||
import type { ProjectData } from "./ProjectData.svelte.js";
|
||||
import {
|
||||
type BlockerPair,
|
||||
MouseDown,
|
||||
|
|
@ -24,6 +23,7 @@ import {
|
|||
} from "./NodePositionsCalculator";
|
||||
import { DraggingManager } from "./DraggingManager.svelte";
|
||||
import { delink, LinkManager, tasknameFromFilePath } from "./LinkManager";
|
||||
import { VersionedData } from "./data/VersionedData";
|
||||
|
||||
export class Context {
|
||||
app: App;
|
||||
|
|
@ -40,7 +40,7 @@ export class Context {
|
|||
chosenBlockedId = $state(NoTaskId);
|
||||
toolbarStatus = $state(StatusCode.DRAFT);
|
||||
reparentingTaskId = $state(NoTaskId);
|
||||
projectData: ProjectData;
|
||||
versionedData: VersionedData;
|
||||
positions: Map<TaskId, Vector2>;
|
||||
taskPositions: Array<{
|
||||
taskId: TaskId;
|
||||
|
|
@ -58,7 +58,7 @@ export class Context {
|
|||
constructor(
|
||||
plugin: TaskmapPlugin,
|
||||
view: TaskmapView,
|
||||
projectData: ProjectData,
|
||||
projectData: VersionedData,
|
||||
app: App,
|
||||
nodePositionsCalculator: NodePositionsCalculator,
|
||||
) {
|
||||
|
|
@ -66,54 +66,26 @@ export class Context {
|
|||
this.view = view;
|
||||
this.nodePositionsCalculator = nodePositionsCalculator;
|
||||
this.app = app;
|
||||
this.projectData = projectData;
|
||||
this.versionedData = projectData;
|
||||
this.linkManager = new LinkManager(app);
|
||||
|
||||
this.taskPositions = projectData.tasks
|
||||
.filter((t) => !t.deleted)
|
||||
.map((task) => {
|
||||
return {
|
||||
taskId: task.taskId,
|
||||
tween: null,
|
||||
};
|
||||
});
|
||||
this.taskPositions = projectData.getTasks().map((task) => {
|
||||
return {
|
||||
taskId: task.taskId,
|
||||
tween: null,
|
||||
};
|
||||
});
|
||||
this.updateTaskPositions();
|
||||
}
|
||||
|
||||
public isTaskBlocked(taskId: TaskId) {
|
||||
if (this.projectData.getTask(taskId).status === StatusCode.DONE) {
|
||||
return false;
|
||||
}
|
||||
return this.projectData.blockerPairs
|
||||
.filter((p) => p.blocked === taskId)
|
||||
.some(
|
||||
(p) =>
|
||||
this.projectData.getTask(p.blocker).status !==
|
||||
StatusCode.DONE,
|
||||
);
|
||||
}
|
||||
|
||||
public isTaskBlocking(taskId: TaskId) {
|
||||
if (this.projectData.getTask(taskId).status === StatusCode.DONE) {
|
||||
return false;
|
||||
}
|
||||
return this.projectData.blockerPairs
|
||||
.filter((p) => p.blocker === taskId)
|
||||
.some(
|
||||
(p) =>
|
||||
this.projectData.getTask(p.blocked).status !==
|
||||
StatusCode.DONE,
|
||||
);
|
||||
}
|
||||
|
||||
public isBlockerHighlighted = (taskId: TaskId) => {
|
||||
if (this.projectData.getTask(taskId).status === StatusCode.DONE) {
|
||||
if (this.versionedData.getTask(taskId).status === StatusCode.DONE) {
|
||||
return false;
|
||||
}
|
||||
if (this.chosenBlockedId !== NoTaskId) {
|
||||
return (
|
||||
this.chosenBlockedId === taskId ||
|
||||
this.projectData.containsBlockerPair({
|
||||
this.versionedData.containsBlockerPair({
|
||||
blocked: this.chosenBlockedId,
|
||||
blocker: taskId,
|
||||
})
|
||||
|
|
@ -121,7 +93,7 @@ export class Context {
|
|||
} else if (this.chosenBlockerId !== NoTaskId) {
|
||||
return (
|
||||
this.chosenBlockerId === taskId ||
|
||||
this.projectData.containsBlockerPair({
|
||||
this.versionedData.containsBlockerPair({
|
||||
blocked: taskId,
|
||||
blocker: this.chosenBlockerId,
|
||||
})
|
||||
|
|
@ -152,32 +124,11 @@ export class Context {
|
|||
this.updateOnZoomCounter += 1;
|
||||
}
|
||||
|
||||
public isTaskHidden(taskId: TaskId): boolean {
|
||||
const task = this.projectData.getTask(taskId);
|
||||
const ancestorTasks = this.projectData.getAncestors(taskId);
|
||||
|
||||
const focusedAncestorIds = this.projectData
|
||||
.getAncestors(this.focusedTaskId)
|
||||
.map((t) => t.taskId);
|
||||
const focusedDescendantIds = this.projectData.getDescendantIds(
|
||||
this.focusedTaskId,
|
||||
);
|
||||
return (
|
||||
task.deleted ||
|
||||
ancestorTasks.some((t) => t.hidden) ||
|
||||
!(
|
||||
task.taskId == this.focusedTaskId ||
|
||||
focusedAncestorIds.contains(task.taskId) ||
|
||||
focusedDescendantIds.contains(task.taskId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public isValidBlockerTarget(taskId: TaskId) {
|
||||
if (taskId === NoTaskId || this.chosenBlockedId === NoTaskId) {
|
||||
return false;
|
||||
}
|
||||
if (this.projectData.getTask(taskId).status == StatusCode.DONE) {
|
||||
if (this.versionedData.getTask(taskId).status == StatusCode.DONE) {
|
||||
return false;
|
||||
}
|
||||
return this.isValidBlockerPairTarget({
|
||||
|
|
@ -190,7 +141,7 @@ export class Context {
|
|||
if (taskId === NoTaskId || this.chosenBlockerId === NoTaskId) {
|
||||
return false;
|
||||
}
|
||||
if (this.projectData.getTask(taskId).status == StatusCode.DONE) {
|
||||
if (this.versionedData.getTask(taskId).status == StatusCode.DONE) {
|
||||
return false;
|
||||
}
|
||||
return this.isValidBlockerPairTarget({
|
||||
|
|
@ -202,11 +153,11 @@ export class Context {
|
|||
private isValidBlockerPairTarget(blockerPair: BlockerPair) {
|
||||
return !(
|
||||
blockerPair.blocked === blockerPair.blocker ||
|
||||
this.projectData.isAncestorOf(
|
||||
this.versionedData.isAncestorOf(
|
||||
blockerPair.blocked,
|
||||
blockerPair.blocker,
|
||||
) ||
|
||||
this.projectData.isDescendentOf(
|
||||
this.versionedData.isDescendentOf(
|
||||
blockerPair.blocked,
|
||||
blockerPair.blocker,
|
||||
)
|
||||
|
|
@ -225,16 +176,17 @@ export class Context {
|
|||
this.reparentingTaskId = NoTaskId;
|
||||
}
|
||||
|
||||
public isValidReparentingTarget(taskId: TaskId) {
|
||||
public isValidReparentingTarget(candidate: TaskId) {
|
||||
if (this.reparentingTaskId == NoTaskId) {
|
||||
throw new Error("Incorrect state: reparentingTaskId expected");
|
||||
}
|
||||
const task = this.versionedData.getTask(this.reparentingTaskId);
|
||||
return (
|
||||
taskId != this.reparentingTaskId &&
|
||||
!this.projectData
|
||||
candidate != this.reparentingTaskId &&
|
||||
candidate != task.parentId &&
|
||||
!this.versionedData
|
||||
.getDescendantIds(this.reparentingTaskId)
|
||||
.contains(taskId) &&
|
||||
this.projectData.getTask(this.reparentingTaskId).parentId != taskId
|
||||
.contains(candidate)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +194,7 @@ export class Context {
|
|||
if (this.reparentingTaskId == NoTaskId) {
|
||||
throw new Error("Incorrect state: reparentingTaskId expected");
|
||||
}
|
||||
this.projectData.changeParent(this.reparentingTaskId, newParentId);
|
||||
this.versionedData.changeParent(this.reparentingTaskId, newParentId);
|
||||
this.updateTaskPositions();
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +208,7 @@ export class Context {
|
|||
}
|
||||
|
||||
public isAncestorOfHidden(taskId: TaskId): boolean {
|
||||
return this.projectData
|
||||
return this.versionedData
|
||||
.getAncestors(this.focusedTaskId)
|
||||
.map((t) => t.taskId)
|
||||
.contains(taskId);
|
||||
|
|
@ -266,15 +218,15 @@ export class Context {
|
|||
if (!draggingOnly) {
|
||||
const newPositions =
|
||||
this.nodePositionsCalculator.CalculatePositionsInGlobalFrame(
|
||||
this.projectData.tasks.filter(
|
||||
(t) => !this.isTaskHidden(t.taskId),
|
||||
),
|
||||
this.versionedData
|
||||
.getTasks()
|
||||
.filter((t) => !this.isTaskHidden(t.taskId)),
|
||||
{ x: 0, y: 0 },
|
||||
);
|
||||
if (this.taskDraggingManager.isDragging) {
|
||||
[
|
||||
this.draggedTaskId,
|
||||
...this.projectData.getDescendantIds(this.draggedTaskId),
|
||||
...this.versionedData.getDescendantIds(this.draggedTaskId),
|
||||
].forEach((t) => {
|
||||
const v = this.positions.get(t);
|
||||
if (v !== undefined) {
|
||||
|
|
@ -283,8 +235,20 @@ export class Context {
|
|||
});
|
||||
}
|
||||
this.positions = newPositions;
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const taskIdSet = new Set<TaskId>(
|
||||
this.taskPositions.map((t) => t.taskId),
|
||||
);
|
||||
this.positions.forEach((v, k) => {
|
||||
if (k != NoTaskId && !taskIdSet.has(k)) {
|
||||
this.taskPositions.push({
|
||||
taskId: k,
|
||||
tween: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.taskPositions = this.taskPositions.filter(
|
||||
(t) => !this.projectData.getTask(t.taskId).deleted,
|
||||
(t) => !this.versionedData.getTaskOption(t.taskId)?.deleted,
|
||||
);
|
||||
this.taskPositions.forEach((taskPos) => {
|
||||
const newPos = this.positions.get(taskPos.taskId);
|
||||
|
|
@ -310,7 +274,7 @@ export class Context {
|
|||
},
|
||||
1 / this.scale,
|
||||
);
|
||||
const draggedTaskIds = this.projectData.getDescendantIds(
|
||||
const draggedTaskIds = this.versionedData.getDescendantIds(
|
||||
this.draggedTaskId,
|
||||
);
|
||||
this.taskPositions
|
||||
|
|
@ -341,12 +305,12 @@ export class Context {
|
|||
|
||||
private updateDraggedTaskPriority() {
|
||||
// Если порядок тасок по оси Y отличается от приоритетов, то меняем приоритеты и пересчитываем порядок
|
||||
const draggedParentId = this.projectData.getTask(
|
||||
const draggedParentId = this.versionedData.getTask(
|
||||
this.draggedTaskId,
|
||||
).parentId;
|
||||
const orderedSiblings = this.taskPositions
|
||||
.filter((t) =>
|
||||
this.projectData
|
||||
this.versionedData
|
||||
.getChildren(draggedParentId)
|
||||
.contains(t.taskId),
|
||||
)
|
||||
|
|
@ -355,11 +319,11 @@ export class Context {
|
|||
const newPriority = orderedSiblings.findIndex(
|
||||
(t) => t == this.draggedTaskId,
|
||||
);
|
||||
const oldPriority = this.projectData.getTask(
|
||||
const oldPriority = this.versionedData.getTask(
|
||||
this.draggedTaskId,
|
||||
).priority;
|
||||
if (newPriority != oldPriority) {
|
||||
this.projectData.setPriority(this.draggedTaskId, newPriority);
|
||||
this.versionedData.setPriority(this.draggedTaskId, newPriority);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -372,7 +336,7 @@ export class Context {
|
|||
public setSelectedTaskId(taskId: number) {
|
||||
this.selectedTaskId = taskId;
|
||||
if (taskId != -1) {
|
||||
this.toolbarStatus = this.projectData.getTaskStatus(taskId);
|
||||
this.toolbarStatus = this.versionedData.getTask(taskId).status;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -383,26 +347,21 @@ export class Context {
|
|||
}
|
||||
|
||||
public addTask(parentId: TaskId): void {
|
||||
const id = this.projectData.addTask(parentId);
|
||||
this.versionedData.addTask(parentId);
|
||||
this.save();
|
||||
this.taskPositions.push({
|
||||
taskId: id,
|
||||
tween: null,
|
||||
});
|
||||
this.updateTaskPositions();
|
||||
}
|
||||
|
||||
public removeTaskSingle(id: number) {
|
||||
this.setSelectedTaskId(-1);
|
||||
this.projectData.removeTaskSingle(id);
|
||||
// this.taskPositions = this.taskPositions.filter((t) => t.taskId !== id);
|
||||
this.versionedData.removeTaskSingle(id);
|
||||
this.updateTaskPositions();
|
||||
this.save();
|
||||
}
|
||||
|
||||
public removeTaskBranch(id: number) {
|
||||
this.setSelectedTaskId(-1);
|
||||
this.projectData.removeTaskBranch(id);
|
||||
this.versionedData.removeTaskBranch(id);
|
||||
// this.taskPositions = this.taskPositions.filter((t) => t.taskId !== id);
|
||||
this.updateTaskPositions();
|
||||
this.save();
|
||||
|
|
@ -412,24 +371,46 @@ export class Context {
|
|||
console.debug(
|
||||
`changeStatus from ${this.toolbarStatus} to ${status} for task ${this.selectedTaskId}`,
|
||||
);
|
||||
this.projectData.setTaskStatus(this.selectedTaskId, status);
|
||||
this.versionedData.setStatus(this.selectedTaskId, status);
|
||||
this.toolbarStatus = status;
|
||||
this.view.debouncedSave();
|
||||
}
|
||||
|
||||
public hideTaskBranch(id: number) {
|
||||
this.projectData.getTask(id).hidden = true;
|
||||
this.versionedData.setHidden(id, true);
|
||||
}
|
||||
|
||||
public unhideTaskBranch(id: number) {
|
||||
this.projectData.getTask(id).hidden = false;
|
||||
this.versionedData.setHidden(id, false);
|
||||
}
|
||||
|
||||
public getCurrentTaskPosition(taskId: number) {
|
||||
// TODO: ошибка при реверте удаления таски. Нет таски среди taskPositions. Вызов через Connection
|
||||
return this.taskPositions.find((t) => t.taskId === taskId)!.tween!
|
||||
.current;
|
||||
}
|
||||
|
||||
public isTaskHidden(taskId: TaskId): boolean {
|
||||
const task = this.versionedData.getTask(taskId);
|
||||
const ancestorTasks = this.versionedData.getAncestors(taskId);
|
||||
|
||||
const focusedAncestorIds = this.versionedData
|
||||
.getAncestors(this.focusedTaskId)
|
||||
.map((t) => t.taskId);
|
||||
const focusedDescendantIds = this.versionedData.getDescendantIds(
|
||||
this.focusedTaskId,
|
||||
);
|
||||
return (
|
||||
task.deleted ||
|
||||
ancestorTasks.some((t) => t.hidden) ||
|
||||
!(
|
||||
task.taskId == this.focusedTaskId ||
|
||||
focusedAncestorIds.contains(task.taskId) ||
|
||||
focusedDescendantIds.contains(task.taskId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates note named after task name if not exists already.
|
||||
* Changes task name to a link to the node.
|
||||
|
|
@ -448,8 +429,11 @@ export class Context {
|
|||
filepath,
|
||||
`Created by Taskmap.`,
|
||||
);
|
||||
const task = this.projectData.getTask(taskId);
|
||||
task.name = tasknameFromFilePath(filepath);
|
||||
this.versionedData.setName(
|
||||
taskId,
|
||||
tasknameFromFilePath(filepath),
|
||||
filepath,
|
||||
);
|
||||
this.save();
|
||||
await this.openOrFocusNote(tfile);
|
||||
} catch (error) {
|
||||
|
|
@ -461,7 +445,7 @@ export class Context {
|
|||
}
|
||||
|
||||
public filePathFromTask(taskId: TaskId) {
|
||||
const task = this.projectData.getTask(taskId);
|
||||
const task = this.versionedData.getTask(taskId);
|
||||
const taskName = task.name;
|
||||
// Sanitize the name for Obsidian filenames
|
||||
let sanitizedName = taskName.replace(/[\\/:*?"<>|]/g, "-");
|
||||
|
|
@ -532,7 +516,7 @@ export class Context {
|
|||
pressedButtonIndex: this.pressedButtonCode,
|
||||
selectedTaskId: this.selectedTaskId,
|
||||
toolbarStatus: this.toolbarStatus,
|
||||
projectData: this.projectData,
|
||||
projectData: this.versionedData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ProjectData } from "./ProjectData.svelte";
|
||||
import { ProjectData } from "./data/ProjectData.svelte.js";
|
||||
import type { App, TFile } from "obsidian";
|
||||
|
||||
export function serializeProjectData(projectData: ProjectData) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { debounce, TextFileView, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import { mount, unmount } from "svelte";
|
||||
import { DEFAULT_DATA, ProjectData } from "./ProjectData.svelte";
|
||||
import { DEFAULT_DATA, ProjectData } from "./data/ProjectData.svelte.js";
|
||||
import { Context } from "./Context.svelte.js";
|
||||
import { NodePositionsCalculator } from "./NodePositionsCalculator";
|
||||
import TaskmapContainer from "./components/TaskmapContainer.svelte";
|
||||
import { deserializeProjectData, updateFile } from "./SaveManager";
|
||||
import type TaskmapPlugin from "./main";
|
||||
import { VersionedData } from "./data/VersionedData";
|
||||
import { HistoryManager } from "./data/HistoryManager";
|
||||
|
||||
export const TASKMAP_VIEW_TYPE = "taskmap-view";
|
||||
|
||||
|
|
@ -41,7 +43,7 @@ export class TaskmapView extends TextFileView {
|
|||
this.context = new Context(
|
||||
this.plugin,
|
||||
this,
|
||||
this.projectData,
|
||||
new VersionedData(this.projectData, new HistoryManager()),
|
||||
this.app,
|
||||
new NodePositionsCalculator(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@
|
|||
import { TASK_SIZE } from "../Constants";
|
||||
import { StatusCode } from "../types";
|
||||
import { Context } from "../Context.svelte.js";
|
||||
import {NoTaskId} from "../NodePositionsCalculator";
|
||||
|
||||
const { taskId, context }: { taskId: number, context: Context } = $props();
|
||||
|
||||
let taskData = $derived(context.projectData.getTask(taskId));
|
||||
let taskData = $derived(context.versionedData.getTask(taskId));
|
||||
let entered = $state(false);
|
||||
|
||||
function addButtonPressed(event: PointerEvent) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
}: {
|
||||
iconCode: IconCode,
|
||||
context: Context,
|
||||
text: string
|
||||
text?: string
|
||||
} = $props();
|
||||
|
||||
let isPressedDown = $state(false);
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
(context.isReparentingOn() && [IconCode.LOCK, IconCode.KEY].contains(iconCode))
|
||||
|| (context.chosenBlockerId !== NoTaskId && [IconCode.REPARENT].contains(iconCode))
|
||||
|| (context.chosenBlockedId !== NoTaskId && [IconCode.REPARENT].contains(iconCode))
|
||||
|| (iconCode === IconCode.STATUS_DONE && context.isTaskBlocked(context.selectedTaskId))
|
||||
|| (iconCode === IconCode.STATUS_DONE && context.versionedData.isTaskBlocked(context.selectedTaskId))
|
||||
);
|
||||
|
||||
function onpointerup(event: MouseEvent) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
} = $props();
|
||||
|
||||
function isStartTaskDepthLE(start: TaskId, end: TaskId){
|
||||
return context.projectData.getTask(start).depth <= context.projectData.getTask(end).depth;
|
||||
return context.versionedData.getTask(start).depth <= context.versionedData.getTask(end).depth;
|
||||
}
|
||||
|
||||
function getConnectionPointShift(start: TaskId, end: TaskId) {
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
const { taskId, context }: { taskId: number, context: Context } = $props();
|
||||
|
||||
let taskData = $derived(context.projectData.getTask(taskId));
|
||||
let taskData = $derived(context.versionedData.getTask(taskId));
|
||||
let entered = $state(false);
|
||||
|
||||
function hidePressed(event: PointerEvent) {
|
||||
context.projectData.toggleHidden(taskId);
|
||||
context.versionedData.toggleHidden(taskId);
|
||||
event.stopPropagation();
|
||||
context.finishTaskDragging(event, true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
coords: {x: number, y: number}
|
||||
} = $props();
|
||||
|
||||
let taskData = $derived(context.projectData.getTask(taskId));
|
||||
|
||||
let taskData = $derived(context.versionedData.getTask(taskId));
|
||||
// let coords = $derived(context.getTaskPosition(taskId));
|
||||
let isHovered = $state(false);
|
||||
let isBlockerHighlighted = $derived(
|
||||
|
|
@ -58,10 +59,10 @@
|
|||
console.debug('Task click add blocker branch');
|
||||
if ( context.isValidBlockerTarget(taskId)){
|
||||
const blockerPair = {blocked: context.chosenBlockedId, blocker: taskId};
|
||||
if (context.projectData.containsBlockerPair(blockerPair)) {
|
||||
context.projectData.removeBlockerPair(blockerPair);
|
||||
if (context.versionedData.containsBlockerPair(blockerPair)) {
|
||||
context.versionedData.removeBlockerPair(blockerPair);
|
||||
} else {
|
||||
context.projectData.addBlockerPair(blockerPair);
|
||||
context.versionedData.addBlockerPair(blockerPair);
|
||||
}
|
||||
context.save();
|
||||
}
|
||||
|
|
@ -70,10 +71,10 @@
|
|||
console.debug('Task click add blocked branch');
|
||||
if (context.isValidBlockedTarget(taskId)) {
|
||||
const blockerPair = {blocked: taskId, blocker: context.chosenBlockerId};
|
||||
if (context.projectData.containsBlockerPair(blockerPair)) {
|
||||
context.projectData.removeBlockerPair(blockerPair);
|
||||
if (context.versionedData.containsBlockerPair(blockerPair)) {
|
||||
context.versionedData.removeBlockerPair(blockerPair);
|
||||
} else {
|
||||
context.projectData.addBlockerPair(blockerPair);
|
||||
context.versionedData.addBlockerPair(blockerPair);
|
||||
}
|
||||
context.save();
|
||||
}
|
||||
|
|
@ -104,7 +105,7 @@
|
|||
);
|
||||
</script>
|
||||
|
||||
{#if !context.isTaskHidden(taskId) && !context.projectData.isBranchHidden(taskId)}
|
||||
{#if !context.isTaskHidden(taskId) && !context.versionedData.isBranchHidden(taskId)}
|
||||
{#key context.updateOnZoomCounter}
|
||||
<div
|
||||
class="task-container"
|
||||
|
|
@ -141,7 +142,6 @@
|
|||
{taskId}
|
||||
{isUnselected}
|
||||
{context}
|
||||
app={context.app}
|
||||
/>
|
||||
</div>
|
||||
{#if !context.taskDraggingManager.isDragging
|
||||
|
|
@ -150,13 +150,13 @@
|
|||
&& !(context.chosenBlockerId !== NoTaskId)}
|
||||
<AddTaskButton {context} {taskId} />
|
||||
{/if}
|
||||
{#if (context.projectData.getChildren(taskId).length > 0)
|
||||
{#if (context.versionedData.getChildren(taskId).length > 0)
|
||||
&& !context.isReparentingOn()
|
||||
&& !(context.chosenBlockedId !== NoTaskId)
|
||||
&& !(context.chosenBlockerId !== NoTaskId)}
|
||||
<HideBranchButton {context} {taskId} />
|
||||
{/if}
|
||||
{#if context.isTaskBlocking(taskId)}
|
||||
{#if context.versionedData.isTaskBlocking(taskId)}
|
||||
<div
|
||||
class="icon-container"
|
||||
style="
|
||||
|
|
@ -173,7 +173,7 @@
|
|||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if context.isTaskBlocked(taskId)}
|
||||
{#if context.versionedData.isTaskBlocked(taskId)}
|
||||
<div
|
||||
class="icon-container"
|
||||
style="
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<script lang="ts">
|
||||
import {onMount, tick} from "svelte";
|
||||
import {App, Component, MarkdownRenderer} from "obsidian";
|
||||
import {Component, MarkdownRenderer} from "obsidian";
|
||||
import {LinkSuggest} from "../LinkSuggest";
|
||||
import type {Context} from "../Context.svelte.js";
|
||||
import {getFromRelativePath, isLink} from "../LinkManager";
|
||||
import type {TaskData} from "../types";
|
||||
import {NoTaskId} from "../NodePositionsCalculator";
|
||||
|
||||
// PROPS
|
||||
|
|
@ -12,16 +11,14 @@
|
|||
taskId,
|
||||
isUnselected,
|
||||
context,
|
||||
app,
|
||||
}: {
|
||||
taskId: number,
|
||||
isUnselected: boolean,
|
||||
context: Context,
|
||||
app: App;
|
||||
} = $props();
|
||||
|
||||
// STATE
|
||||
let taskData = context.projectData.getTask(taskId);
|
||||
let taskData = context.versionedData.getTask(taskId);
|
||||
let isSelected = $derived(context.isSelected(taskId));
|
||||
let isEditing = $derived(context.editingTaskId === taskId);
|
||||
let suggest: LinkSuggest | null = null;
|
||||
|
|
@ -84,7 +81,7 @@
|
|||
|
||||
if (link) {
|
||||
// Trigger the native hover preview
|
||||
app.workspace.trigger("hover-link", {
|
||||
context.app.workspace.trigger("hover-link", {
|
||||
event: e,
|
||||
source: "preview",
|
||||
hoverParent: textPreviewEl,
|
||||
|
|
@ -99,7 +96,7 @@
|
|||
textPreviewEl.empty(); // Clear previous render
|
||||
const content = taskData.name;
|
||||
await MarkdownRenderer.render(
|
||||
app,
|
||||
context.app,
|
||||
content,
|
||||
textPreviewEl,
|
||||
"",
|
||||
|
|
@ -120,7 +117,7 @@
|
|||
await tick();
|
||||
if (textEditEl) {
|
||||
textEditEl.focus();
|
||||
suggest = new LinkSuggest(app, textEditEl);
|
||||
suggest = new LinkSuggest(context.app, textEditEl);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,21 +144,21 @@
|
|||
if (textEditEl === null) {
|
||||
return;
|
||||
}
|
||||
taskData.name = textEditEl.value;
|
||||
let path;
|
||||
if (isLink(taskData.name)) {
|
||||
const file = context.linkManager.getFromLink(taskData.name);
|
||||
if (file === null) {
|
||||
throw new Error(`Link [${taskData.name}] points to a nonexistent file`);
|
||||
}
|
||||
taskData.path = file.path;
|
||||
path = file.path;
|
||||
} else {
|
||||
taskData.path = undefined;
|
||||
path = undefined;
|
||||
}
|
||||
context.versionedData.setName(taskId, textEditEl.value, path);
|
||||
context.save();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div
|
||||
class="task-text-container"
|
||||
class:selected={isSelected}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte";
|
||||
import {onMount, tick} from "svelte";
|
||||
import Toolbar from "./Toolbar.svelte";
|
||||
import Task from "./Task.svelte";
|
||||
import Panzoom, {type PanzoomObject} from '@panzoom/panzoom'
|
||||
|
|
@ -19,7 +19,6 @@
|
|||
let panstart: Vector2 = {x: 0, y: 0};
|
||||
let draggingManager = new DraggingManager([MouseDown.MIDDLE, MouseDown.LEFT]);
|
||||
|
||||
|
||||
onMount(async () => {
|
||||
if (!sceneEl) {
|
||||
throw new Error('No scene element');
|
||||
|
|
@ -42,15 +41,33 @@
|
|||
});
|
||||
});
|
||||
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
async function handleKey(e: KeyboardEvent) {
|
||||
console.debug('handleKey ', e.key);
|
||||
if (e.key === "Escape") {
|
||||
context.setSelectedTaskId(-1);
|
||||
context.setSelectedTaskId(NoTaskId);
|
||||
context.cancelReparenting();
|
||||
context.chosenBlockerId = NoTaskId;
|
||||
context.chosenBlockedId = NoTaskId;
|
||||
e.stopPropagation();
|
||||
return;
|
||||
} else if (e.key === "Delete") {
|
||||
if (context.selectedTaskId !== NoTaskId) {
|
||||
context.versionedData.removeTaskSingle(context.selectedTaskId);
|
||||
context.selectedTaskId = NoTaskId;
|
||||
}
|
||||
} else if (e.ctrlKey && e.key === "z") {
|
||||
context.versionedData.undo();
|
||||
} else if (e.ctrlKey && e.key === "r") {
|
||||
context.versionedData.redo();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
context.save();
|
||||
context.incrementUpdateOnZoomCounter();// required for TaskText update
|
||||
context.updateTaskPositions();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
await tick();
|
||||
}
|
||||
|
||||
function onwheel(e: WheelEvent) {
|
||||
|
|
@ -157,10 +174,10 @@
|
|||
</defs>
|
||||
</defs>
|
||||
<g class="svg-group" bind:this={svgGroupEl}>
|
||||
{#each (context.projectData.tasks
|
||||
{#each (context.versionedData.getTasks()
|
||||
.filter(t => !context.isTaskHidden(t.taskId))
|
||||
.filter(t => t.taskId !== RootTaskId)
|
||||
.filter(t => !context.projectData.isBranchHidden(t.taskId))
|
||||
.filter(t => !context.versionedData.isBranchHidden(t.taskId))
|
||||
) as task (task.taskId)}
|
||||
<Connection
|
||||
startTaskId={task.parentId}
|
||||
|
|
@ -177,7 +194,7 @@
|
|||
tabindex="-1"
|
||||
role="presentation"
|
||||
>
|
||||
{#each context.projectData.tasks.filter(t => !context.isTaskHidden(t.taskId)) as task (task.taskId)}
|
||||
{#each context.versionedData.getTasks().filter(t => !context.isTaskHidden(t.taskId)) as task (task.taskId)}
|
||||
<Task taskId={task.taskId} {context} coords={context.getCurrentTaskPosition(task.taskId)}/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -192,12 +209,12 @@
|
|||
</defs>
|
||||
<g class="svg-group" bind:this={svgGroupEl}>
|
||||
{#if context.chosenBlockerId !== NoTaskId || context.chosenBlockedId !== NoTaskId}
|
||||
{#each (context.projectData.blockerPairs.filter(
|
||||
{#each (context.versionedData.getBlockerPairs().filter(
|
||||
p => p.blocker === context.chosenBlockerId || p.blocked === context.chosenBlockedId
|
||||
).filter(
|
||||
p => {
|
||||
const blockedT = context.projectData.getTask(p.blocked);
|
||||
const blockerT = context.projectData.getTask(p.blocker);
|
||||
const blockedT = context.versionedData.getTask(p.blocked);
|
||||
const blockerT = context.versionedData.getTask(p.blocker);
|
||||
return blockedT.status !== StatusCode.DONE && blockerT.status !== StatusCode.DONE && !blockerT.deleted && !blockedT.deleted;
|
||||
}
|
||||
)) as pair}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
IconCode, StatusCode, type TaskId, toIconCode
|
||||
} from "../types";
|
||||
import {RootTaskId} from "../NodePositionsCalculator";
|
||||
import {onMount} from "svelte";
|
||||
|
||||
let {
|
||||
taskId,
|
||||
|
|
@ -22,6 +23,16 @@
|
|||
}: {taskId: TaskId,
|
||||
context: Context} = $props();
|
||||
|
||||
let self: HTMLElement;
|
||||
let viewport: HTMLElement;
|
||||
|
||||
onMount(() => {
|
||||
viewport = self?.closest('.viewport') as HTMLElement;
|
||||
return () => {
|
||||
viewport.focus();
|
||||
}
|
||||
});
|
||||
|
||||
let position = $derived(context.getCurrentTaskPosition(taskId));
|
||||
|
||||
function getTop() {
|
||||
|
|
@ -30,7 +41,7 @@
|
|||
function getLeft() {
|
||||
return position.x + TASK_SIZE.width_hovered / 2;
|
||||
}
|
||||
let isLeafTask = $derived(context.projectData.getChildren(taskId).length === 0);
|
||||
let isLeafTask = $derived(context.versionedData.getChildren(taskId).length === 0);
|
||||
|
||||
let toolbarButtons = $derived(taskId == RootTaskId ? [
|
||||
IconCode.CREATE_LINKED_NOTE,
|
||||
|
|
@ -59,7 +70,7 @@
|
|||
StatusCode.DONE,
|
||||
]: [
|
||||
StatusCode.DRAFT,
|
||||
context.projectData.calculateStatus(taskId)
|
||||
context.versionedData.calculateStatus(taskId)
|
||||
]).map(x => toIconCode(x)));
|
||||
|
||||
let subtoolbarTopShift = (buttons: IconCode[]) => {
|
||||
|
|
@ -71,6 +82,7 @@
|
|||
<div
|
||||
class="toolbar"
|
||||
class:no-pan={true}
|
||||
bind:this={self}
|
||||
in:fade|global={{ duration: 500 }}
|
||||
out:fade|global={{ duration: 300 }}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
|
|
|
|||
287
src/data/Action.ts
Normal file
287
src/data/Action.ts
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
import { type BlockerPair, StatusCode, type TaskId } from "../types";
|
||||
import { ProjectData } from "./ProjectData.svelte";
|
||||
|
||||
export interface Action {
|
||||
do(data: ProjectData): void;
|
||||
undo(data: ProjectData): void;
|
||||
}
|
||||
|
||||
export class AddTaskAction implements Action {
|
||||
private parentId: TaskId;
|
||||
private addedTaskId?: TaskId;
|
||||
|
||||
constructor(parentId: TaskId) {
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
do(data: ProjectData): void {
|
||||
const childrenCount = data.getChildren(this.parentId).length;
|
||||
this.addedTaskId = data.curTaskId;
|
||||
data.tasks.push({
|
||||
taskId: this.addedTaskId,
|
||||
parentId: this.parentId,
|
||||
status: StatusCode.READY,
|
||||
name: "default",
|
||||
deleted: false,
|
||||
hidden: false,
|
||||
priority: childrenCount,
|
||||
depth: data.getTask(this.parentId).depth + 1,
|
||||
});
|
||||
data.curTaskId++;
|
||||
data.recalcStatusRecursive(this.parentId);
|
||||
}
|
||||
|
||||
undo(data: ProjectData): void {
|
||||
if (this.addedTaskId === undefined) {
|
||||
throw new Error();
|
||||
}
|
||||
if (data.curTaskId != this.addedTaskId + 1) {
|
||||
throw new Error();
|
||||
}
|
||||
data.tasks = data.tasks.filter((t) => t.taskId !== this.addedTaskId);
|
||||
data.curTaskId--;
|
||||
this.addedTaskId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveTaskSingleAction implements Action {
|
||||
private taskId: TaskId;
|
||||
private children?: TaskId[] = undefined;
|
||||
|
||||
constructor(taskId: TaskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
do(data: ProjectData): void {
|
||||
const task = data.getTask(this.taskId);
|
||||
const parentTask = data.getTask(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;
|
||||
});
|
||||
// TODO: Priorities suck
|
||||
data.recalcPriorities(task.parentId);
|
||||
data.recalcStatusRecursive(task.parentId);
|
||||
}
|
||||
|
||||
undo(data: ProjectData) {
|
||||
if (this.children === undefined) {
|
||||
throw new Error();
|
||||
}
|
||||
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;
|
||||
});
|
||||
// TODO: Priorities suck
|
||||
data.recalcPriorities(task.parentId);
|
||||
data.recalcPriorities(task.taskId);
|
||||
data.recalcStatusRecursive(task.taskId);
|
||||
this.children = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveTaskBranchAction implements Action {
|
||||
private taskId: TaskId;
|
||||
private descendants?: TaskId[] = undefined;
|
||||
|
||||
constructor(taskId: TaskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
do(data: ProjectData): void {
|
||||
// Memorize descendents that are being removed on this call, because other descendants that were removed earlier can exist
|
||||
this.descendants = data.getDescendantIds(this.taskId);
|
||||
this.toggleDeleted(this.descendants, true, data);
|
||||
}
|
||||
|
||||
undo(data: ProjectData) {
|
||||
if (this.descendants === undefined) {
|
||||
throw new Error();
|
||||
}
|
||||
this.toggleDeleted(this.descendants, false, data);
|
||||
this.descendants = undefined;
|
||||
}
|
||||
|
||||
private toggleDeleted(
|
||||
descendants: TaskId[],
|
||||
value: boolean,
|
||||
data: ProjectData,
|
||||
) {
|
||||
descendants.forEach((taskId) => (data.getTask(taskId).deleted = value));
|
||||
const parentId = data.getTask(this.taskId).parentId;
|
||||
data.recalcPriorities(parentId);
|
||||
data.recalcStatusRecursive(parentId);
|
||||
}
|
||||
}
|
||||
|
||||
export class SetTaskStatusAction implements Action {
|
||||
private taskId: TaskId;
|
||||
private newStatus: StatusCode;
|
||||
private oldStatus?: StatusCode;
|
||||
|
||||
constructor(taskId: TaskId, newStatus: StatusCode) {
|
||||
this.taskId = taskId;
|
||||
this.newStatus = newStatus;
|
||||
}
|
||||
|
||||
do(data: ProjectData) {
|
||||
this.oldStatus = data.getTask(this.taskId).status;
|
||||
const task = data.getTask(this.taskId);
|
||||
task.status = this.newStatus;
|
||||
data.recalcStatusRecursive(task.parentId);
|
||||
}
|
||||
|
||||
undo(data: ProjectData) {
|
||||
if (this.oldStatus === undefined) {
|
||||
throw new Error();
|
||||
}
|
||||
const task = data.getTask(this.taskId);
|
||||
task.status = this.oldStatus;
|
||||
data.recalcStatusRecursive(task.parentId);
|
||||
this.oldStatus = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class SetTaskNameAction implements Action {
|
||||
private taskId: TaskId;
|
||||
private newName: string;
|
||||
private newPath?: string;
|
||||
private oldName?: string;
|
||||
private oldPath?: string;
|
||||
|
||||
constructor(taskId: TaskId, newName: string, path?: string) {
|
||||
this.taskId = taskId;
|
||||
this.newName = newName;
|
||||
this.newPath = path;
|
||||
}
|
||||
|
||||
do(data: ProjectData): void {
|
||||
this.oldName = data.getTask(this.taskId).name;
|
||||
this.oldPath = data.getTask(this.taskId).path;
|
||||
const task = data.getTask(this.taskId);
|
||||
task.name = this.newName;
|
||||
task.path = this.newPath;
|
||||
}
|
||||
|
||||
undo(data: ProjectData): void {
|
||||
if (this.oldName === undefined) {
|
||||
throw new Error();
|
||||
}
|
||||
const task = data.getTask(this.taskId);
|
||||
task.name = this.oldName;
|
||||
task.path = this.oldPath;
|
||||
this.oldName = undefined;
|
||||
this.oldPath = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class SetTaskPriorityAction implements Action {
|
||||
private taskId: TaskId;
|
||||
private newPriority: number;
|
||||
private oldPriority?: number;
|
||||
|
||||
constructor(taskId: TaskId, newPriority: number) {
|
||||
this.taskId = taskId;
|
||||
this.newPriority = newPriority;
|
||||
}
|
||||
|
||||
do(data: ProjectData) {
|
||||
this.oldPriority = data.getTask(this.taskId).priority;
|
||||
data.setPriority(this.taskId, this.newPriority);
|
||||
}
|
||||
|
||||
undo(data: ProjectData) {
|
||||
if (this.oldPriority === undefined) {
|
||||
throw new Error();
|
||||
}
|
||||
data.setPriority(this.taskId, this.oldPriority);
|
||||
this.oldPriority = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChangeParentAction implements Action {
|
||||
private taskId: TaskId;
|
||||
private newParentId: TaskId;
|
||||
private oldParentId?: TaskId;
|
||||
|
||||
constructor(taskId: TaskId, newParentId: number) {
|
||||
this.taskId = taskId;
|
||||
this.newParentId = newParentId;
|
||||
}
|
||||
|
||||
do(data: ProjectData) {
|
||||
this.oldParentId = data.getTask(this.taskId).parentId;
|
||||
data.changeParent(this.taskId, this.newParentId);
|
||||
}
|
||||
|
||||
undo(data: ProjectData) {
|
||||
if (this.oldParentId === undefined) {
|
||||
throw new Error();
|
||||
}
|
||||
data.changeParent(this.taskId, this.oldParentId);
|
||||
this.oldParentId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class SetTaskHiddenAction implements Action {
|
||||
private taskId: TaskId;
|
||||
private value: boolean;
|
||||
|
||||
constructor(taskId: TaskId, value: boolean) {
|
||||
this.taskId = taskId;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
do(data: ProjectData): void {
|
||||
data.getTask(this.taskId).hidden = this.value;
|
||||
}
|
||||
|
||||
undo(data: ProjectData): void {
|
||||
data.getTask(this.taskId).hidden = !this.value;
|
||||
}
|
||||
}
|
||||
|
||||
export class AddBlockerPairAction implements Action {
|
||||
private blockerPair: BlockerPair;
|
||||
|
||||
constructor(blockerPair: BlockerPair) {
|
||||
this.blockerPair = blockerPair;
|
||||
}
|
||||
|
||||
do(data: ProjectData): void {
|
||||
data.addBlockerPair(this.blockerPair);
|
||||
}
|
||||
|
||||
undo(data: ProjectData): void {
|
||||
data.removeBlockerPair(this.blockerPair);
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveBlockerPairAction implements Action {
|
||||
private blockerPair: BlockerPair;
|
||||
|
||||
constructor(blockerPair: BlockerPair) {
|
||||
this.blockerPair = blockerPair;
|
||||
}
|
||||
|
||||
do(data: ProjectData): void {
|
||||
data.removeBlockerPair(this.blockerPair);
|
||||
}
|
||||
|
||||
undo(data: ProjectData): void {
|
||||
data.addBlockerPair(this.blockerPair);
|
||||
}
|
||||
}
|
||||
39
src/data/HistoryManager.ts
Normal file
39
src/data/HistoryManager.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { ProjectData } from "./ProjectData.svelte";
|
||||
import type { Action } from "./Action";
|
||||
|
||||
export class HistoryManager {
|
||||
private undoStack: Action[] = [];
|
||||
private redoStack: Action[] = [];
|
||||
|
||||
execute(action: Action, data: ProjectData): void {
|
||||
action.do(data);
|
||||
this.undoStack.push(action);
|
||||
this.redoStack = []; // Clear redo stack on new action
|
||||
}
|
||||
|
||||
undo(data: ProjectData): boolean {
|
||||
const action = this.undoStack.pop();
|
||||
if (!action) return false;
|
||||
|
||||
action.undo(data);
|
||||
this.redoStack.push(action);
|
||||
return true;
|
||||
}
|
||||
|
||||
redo(data: ProjectData): boolean {
|
||||
const action = this.redoStack.pop();
|
||||
if (!action) return false;
|
||||
|
||||
action.do(data);
|
||||
this.undoStack.push(action);
|
||||
return true;
|
||||
}
|
||||
|
||||
canUndo(): boolean {
|
||||
return this.undoStack.length > 0;
|
||||
}
|
||||
|
||||
canRedo(): boolean {
|
||||
return this.redoStack.length > 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,9 @@
|
|||
StatusCode,
|
||||
type TaskData,
|
||||
type TaskId,
|
||||
} from "./types";
|
||||
import { NoTaskId, RootTaskId } from "./NodePositionsCalculator";
|
||||
import { serializeProjectData } from "./SaveManager";
|
||||
} from "../types";
|
||||
import { NoTaskId, RootTaskId } from "../NodePositionsCalculator";
|
||||
import { serializeProjectData } from "../SaveManager";
|
||||
|
||||
export class ProjectData {
|
||||
tasks = $state(new Array<TaskData>());
|
||||
|
|
@ -47,46 +47,6 @@ export class ProjectData {
|
|||
this.curTaskId++;
|
||||
}
|
||||
|
||||
public addTask(parentId: TaskId) {
|
||||
const childrenCount = this.getChildren(parentId).length;
|
||||
const id = this.curTaskId;
|
||||
this.tasks.push({
|
||||
taskId: id,
|
||||
parentId: parentId,
|
||||
status: StatusCode.READY,
|
||||
name: "default",
|
||||
deleted: false,
|
||||
hidden: false,
|
||||
priority: childrenCount,
|
||||
depth: this.getTask(parentId).depth + 1,
|
||||
});
|
||||
this.curTaskId++;
|
||||
this.recalculateStatusRecursive(parentId);
|
||||
return id;
|
||||
}
|
||||
|
||||
public removeTaskSingle(id: number) {
|
||||
const task = this.getTask(id);
|
||||
const parentTask = this.getTask(task.parentId);
|
||||
task.deleted = true;
|
||||
this.getChildren(id).forEach((taskId) => {
|
||||
const t = this.getTask(taskId);
|
||||
t.parentId = parentTask.taskId;
|
||||
t.depth = parentTask.depth + 1;
|
||||
});
|
||||
this.recalcPriorities(task.parentId);
|
||||
this.recalculateStatusRecursive(task.parentId);
|
||||
}
|
||||
|
||||
public removeTaskBranch(id: number) {
|
||||
this.getDescendantIds(id).forEach(
|
||||
(taskId) => (this.getTask(taskId).deleted = true),
|
||||
);
|
||||
const parentId = this.getTask(id).parentId;
|
||||
this.recalcPriorities(parentId);
|
||||
this.recalculateStatusRecursive(parentId);
|
||||
}
|
||||
|
||||
public getDescendantIds(taskId: number, includeDeleted: boolean = false) {
|
||||
const tasks = [taskId];
|
||||
const result: TaskId[] = [];
|
||||
|
|
@ -142,42 +102,19 @@ export class ProjectData {
|
|||
return this.getTask(taskId).deleted;
|
||||
}
|
||||
|
||||
public isBranchHidden(id: number) {
|
||||
return this.getAncestors(id).some((t) => t.hidden);
|
||||
}
|
||||
|
||||
public toggleHidden(id: number) {
|
||||
const task = this.getTask(id);
|
||||
task.hidden = !task.hidden;
|
||||
}
|
||||
|
||||
public getTaskStatus(taskId: number) {
|
||||
return this.getTask(taskId).status;
|
||||
}
|
||||
|
||||
public getTaskName(taskId: number) {
|
||||
return this.getTask(taskId).name;
|
||||
}
|
||||
|
||||
public setTaskStatus(taskId: number, status: StatusCode) {
|
||||
const task = this.getTask(taskId);
|
||||
task.status = status;
|
||||
this.recalculateStatusRecursive(task.parentId);
|
||||
}
|
||||
|
||||
public changeParent(taskId: TaskId, newParentId: TaskId) {
|
||||
const taskData = this.getTask(taskId);
|
||||
const oldParentId = taskData.parentId;
|
||||
taskData.parentId = newParentId;
|
||||
this.recalculateStatusRecursive(oldParentId);
|
||||
this.recalculateStatusRecursive(newParentId);
|
||||
this.recalcStatusRecursive(oldParentId);
|
||||
this.recalcStatusRecursive(newParentId);
|
||||
[taskId, ...this.getDescendantIds(taskId)].forEach((taskId) => {
|
||||
const task = this.getTask(taskId);
|
||||
task.depth = this.getTask(task.parentId).depth + 1;
|
||||
});
|
||||
}
|
||||
|
||||
public recalculateStatusRecursive(taskId: TaskId) {
|
||||
public recalcStatusRecursive(taskId: TaskId) {
|
||||
if (taskId == RootTaskId) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -186,7 +123,7 @@ export class ProjectData {
|
|||
return;
|
||||
}
|
||||
task.status = this.calculateStatus(taskId);
|
||||
this.recalculateStatusRecursive(task.parentId);
|
||||
this.recalcStatusRecursive(task.parentId);
|
||||
}
|
||||
|
||||
public calculateStatus(taskId: TaskId) {
|
||||
|
|
@ -211,13 +148,6 @@ export class ProjectData {
|
|||
}
|
||||
}
|
||||
|
||||
public setTaskName(taskId: number, name: string) {
|
||||
const task = this.getTask(taskId);
|
||||
if (task) {
|
||||
task.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public setPriority(taskId: TaskId, newPriority: number) {
|
||||
const task = this.getTask(taskId);
|
||||
const oldPriority = task.priority;
|
||||
170
src/data/VersionedData.ts
Normal file
170
src/data/VersionedData.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import { HistoryManager } from "./HistoryManager";
|
||||
import { type BlockerPair, StatusCode, type TaskId } from "../types";
|
||||
import { ProjectData } from "./ProjectData.svelte";
|
||||
import {
|
||||
AddTaskAction,
|
||||
SetTaskHiddenAction,
|
||||
RemoveTaskBranchAction,
|
||||
RemoveTaskSingleAction,
|
||||
SetTaskPriorityAction,
|
||||
SetTaskStatusAction,
|
||||
AddBlockerPairAction,
|
||||
RemoveBlockerPairAction,
|
||||
ChangeParentAction,
|
||||
SetTaskNameAction,
|
||||
} from "./Action";
|
||||
|
||||
export class VersionedData {
|
||||
private data: ProjectData;
|
||||
private history: HistoryManager;
|
||||
|
||||
constructor(data: ProjectData, history: HistoryManager) {
|
||||
this.data = data;
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
public undo = () => {
|
||||
if (this.history.canUndo()) {
|
||||
this.history.undo(this.data);
|
||||
}
|
||||
};
|
||||
|
||||
public redo = () => {
|
||||
if (this.history.canRedo()) {
|
||||
this.history.redo(this.data);
|
||||
}
|
||||
};
|
||||
|
||||
public addBlockerPair = (blockerPair: BlockerPair) => {
|
||||
this.history.execute(new AddBlockerPairAction(blockerPair), this.data);
|
||||
};
|
||||
|
||||
public removeBlockerPair = (blockerPair: BlockerPair) => {
|
||||
this.history.execute(
|
||||
new RemoveBlockerPairAction(blockerPair),
|
||||
this.data,
|
||||
);
|
||||
};
|
||||
|
||||
public getBlockerPairs = () => {
|
||||
return this.data.blockerPairs;
|
||||
};
|
||||
|
||||
public addTask = (parentId: TaskId) => {
|
||||
this.history.execute(new AddTaskAction(parentId), this.data);
|
||||
};
|
||||
|
||||
public removeTaskSingle = (taskId: TaskId) => {
|
||||
this.history.execute(new RemoveTaskSingleAction(taskId), this.data);
|
||||
};
|
||||
|
||||
public removeTaskBranch = (taskId: TaskId) => {
|
||||
this.history.execute(new RemoveTaskBranchAction(taskId), this.data);
|
||||
};
|
||||
|
||||
public setStatus = (taskId: TaskId, status: StatusCode) => {
|
||||
this.history.execute(
|
||||
new SetTaskStatusAction(taskId, status),
|
||||
this.data,
|
||||
);
|
||||
};
|
||||
|
||||
public setPriority = (taskId: TaskId, priority: number) => {
|
||||
this.history.execute(
|
||||
new SetTaskPriorityAction(taskId, priority),
|
||||
this.data,
|
||||
);
|
||||
};
|
||||
|
||||
public setHidden = (taskId: TaskId, value: boolean) => {
|
||||
this.history.execute(new SetTaskHiddenAction(taskId, value), this.data);
|
||||
};
|
||||
|
||||
public toggleHidden = (taskId: TaskId) => {
|
||||
const task = this.getTask(taskId);
|
||||
this.history.execute(
|
||||
new SetTaskHiddenAction(taskId, !task.hidden),
|
||||
this.data,
|
||||
);
|
||||
};
|
||||
|
||||
public setName = (
|
||||
taskId: TaskId,
|
||||
value: string,
|
||||
path: string | undefined,
|
||||
) => {
|
||||
this.history.execute(
|
||||
new SetTaskNameAction(taskId, value, path),
|
||||
this.data,
|
||||
);
|
||||
};
|
||||
|
||||
public changeParent = (taskId: TaskId, newParentId: TaskId) => {
|
||||
this.history.execute(
|
||||
new ChangeParentAction(taskId, newParentId),
|
||||
this.data,
|
||||
);
|
||||
};
|
||||
|
||||
public getTasks = (includeDeleted = false) => {
|
||||
return this.data.tasks.filter((t) => includeDeleted || !t.deleted);
|
||||
};
|
||||
|
||||
public getTask = (taskId: TaskId) => {
|
||||
return this.data.getTask(taskId);
|
||||
};
|
||||
|
||||
public getTaskOption = (taskId: TaskId) => {
|
||||
return this.data.tasks.find((t) => t.taskId == taskId) ?? null;
|
||||
};
|
||||
|
||||
public getChildren = (taskId: TaskId, includeDeleted?: boolean) => {
|
||||
return this.data.getChildren(taskId, includeDeleted);
|
||||
};
|
||||
|
||||
public getAncestors = (taskId: TaskId) => {
|
||||
return this.data.getAncestors(taskId);
|
||||
};
|
||||
|
||||
public getDescendantIds = (taskId: TaskId, includeDeleted?: boolean) => {
|
||||
return this.data.getDescendantIds(taskId, includeDeleted);
|
||||
};
|
||||
|
||||
public isAncestorOf = (taskId: TaskId, candidate: TaskId) => {
|
||||
return this.data.isAncestorOf(taskId, candidate);
|
||||
};
|
||||
|
||||
public isDescendentOf = (taskId: TaskId, candidate: TaskId) => {
|
||||
return this.data.isDescendentOf(taskId, candidate);
|
||||
};
|
||||
|
||||
public containsBlockerPair = (blockerPair: BlockerPair) => {
|
||||
return this.data.containsBlockerPair(blockerPair);
|
||||
};
|
||||
|
||||
public isTaskBlocked = (taskId: TaskId) => {
|
||||
if (this.getTask(taskId).status === StatusCode.DONE) {
|
||||
return false;
|
||||
}
|
||||
return this.data.blockerPairs
|
||||
.filter((p) => p.blocked === taskId)
|
||||
.some((p) => this.getTask(p.blocker).status !== StatusCode.DONE);
|
||||
};
|
||||
|
||||
public isTaskBlocking = (taskId: TaskId) => {
|
||||
if (this.getTask(taskId).status === StatusCode.DONE) {
|
||||
return false;
|
||||
}
|
||||
return this.data.blockerPairs
|
||||
.filter((p) => p.blocker === taskId)
|
||||
.some((p) => this.getTask(p.blocked).status !== StatusCode.DONE);
|
||||
};
|
||||
|
||||
public isBranchHidden(id: number) {
|
||||
return this.getAncestors(id).some((t) => t.hidden);
|
||||
}
|
||||
|
||||
public calculateStatus = (taskId: TaskId) => {
|
||||
return this.data.calculateStatus(taskId);
|
||||
};
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import {
|
|||
type TaskmapPluginSettings,
|
||||
} from "./TaskmapPluginSettings";
|
||||
import { TaskmapSettingTab } from "./TaskmapSettingTab";
|
||||
import { DEFAULT_DATA } from "./ProjectData.svelte";
|
||||
import { DEFAULT_DATA } from "./data/ProjectData.svelte.js";
|
||||
import { LOGO_CONTENT, LOGO_NAME } from "./IconService";
|
||||
import type { TaskData } from "./types";
|
||||
import { deserializeProjectData, updateFile } from "./SaveManager";
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const buttonTextFromIconCode = (code: IconCode) => {
|
|||
};
|
||||
|
||||
export const toIconCode = (s: StatusCode) => {
|
||||
return (s + IconCode.STATUS_DRAFT) as number as IconCode;
|
||||
return (s + IconCode.STATUS_DRAFT) as IconCode;
|
||||
};
|
||||
|
||||
export const toStatusCode = (s: IconCode) => {
|
||||
|
|
@ -92,7 +92,7 @@ export type TaskId = number;
|
|||
|
||||
export type TaskData = {
|
||||
name: string;
|
||||
path?: string | undefined | null;
|
||||
path?: string | undefined;
|
||||
taskId: TaskId;
|
||||
status: StatusCode;
|
||||
deleted: boolean;
|
||||
|
|
|
|||
Loading…
Reference in a new issue