fixes, schema and position stopped resetting

This commit is contained in:
poanse 2026-04-15 10:04:13 +03:00
parent e005c765ba
commit 594cf50b10
19 changed files with 684 additions and 614 deletions

View file

@ -63,6 +63,7 @@ const context = await esbuild.context({
treeShaking: true,
outfile: "main.js",
minify: prod,
pure: prod ? ["console.debug"] : [],
});
if (prod) {

View file

@ -11,8 +11,8 @@ export default defineConfig([
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs["flat/recommended"],
prettier,
...svelte.configs["flat/prettier"],
prettier,
{
files: ["**/*.ts", "**/*.svelte"],
languageOptions: {

741
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,6 @@
"devDependencies": {
"@eslint/css": "^0.14.1",
"@eslint/js": "^9.39.2",
"@types/lodash": "^4.17.21",
"@types/node": "latest",
"@typescript-eslint/eslint-plugin": "latest",
"@typescript-eslint/parser": "latest",
@ -23,10 +22,12 @@
"esbuild": "latest",
"esbuild-svelte": "^0.9.3",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-obsidianmd": "^0.1.9",
"eslint-plugin-svelte": "^3.14.0",
"globals": "^16.5.0",
"obsidian": "latest",
"prettier": "^3.8.2",
"rollup-plugin-svelte": "^7.2.3",
"svelte": "^5.45.5",
"svelte-check": "^4.3.4",
@ -38,16 +39,11 @@
"dependencies": {
"@napolab/alpha-blend": "^2.0.0",
"@panzoom/panzoom": "^4.6.1",
"@rollup/plugin-node-resolve": "^16.0.3",
"autobind-decorator": "^2.4.0",
"eslint-config-prettier": "^10.1.8",
"esm-env": "^1.2.2",
"lodash": "^4.17.21",
"lucide-svelte": "^0.562.0",
"pixi.js": "^8.14.3",
"prettier": "^3.7.4",
"rollup-plugin-css-only": "^4.5.5",
"svelte-canvas": "^2.1.0",
"ts-pattern": "^5.9.0"
"ts-pattern": "^5.9.0",
"valibot": "^1.3.1"
}
}

View file

@ -23,10 +23,14 @@ import {
} from "./NodePositionsCalculator";
import { DraggingManager } from "./DraggingManager.svelte";
import { delink, LinkManager, tasknameFromFilePath } from "./LinkManager";
import { HistoryManager } from "./data/HistoryManager.svelte";
import type { ProjectData } from "./data/ProjectData.svelte.js";
import { VersionedData } from "./data/VersionedData";
import { innerHeight } from "svelte/reactivity/window";
import { TASK_SIZE } from "./Constants";
const DEFAULT_TOOLBAR_STATUS = StatusCode.DRAFT;
export class Context {
app: App;
plugin: TaskmapPlugin;
@ -41,9 +45,9 @@ export class Context {
chosenBlockedId = $state(NoTaskId);
hoveredBlockerId = $state(NoTaskId);
hoveredBlockedId = $state(NoTaskId);
toolbarStatus = $state(StatusCode.DRAFT);
toolbarStatus = $state(DEFAULT_TOOLBAR_STATUS);
reparentingTaskId = $state(NoTaskId);
versionedData: VersionedData;
versionedData = $state(null! as VersionedData);
positions: Map<TaskId, Vector2>;
taskPositions: Array<{
taskId: TaskId;
@ -79,6 +83,47 @@ export class Context {
this.updateTaskPositions();
}
/**
* Replaces project data from disk (e.g. after an external update) without remounting the view,
* so pan/zoom and the scene stay intact.
*/
public reloadFromDisk(projectData: ProjectData): void {
this.versionedData = new VersionedData(projectData, new HistoryManager());
this.taskDraggingManager.reset();
this.draggedTaskId = NoTaskId;
this.reparentingTaskId = NoTaskId;
this.chosenBlockerId = NoTaskId;
this.chosenBlockedId = NoTaskId;
this.hoveredBlockerId = NoTaskId;
this.hoveredBlockedId = NoTaskId;
this.pressedButtonCode = -1;
const sel = this.versionedData.getTaskOption(this.selectedTaskId);
if (sel == null || sel.deleted) {
this.selectedTaskId = NoTaskId;
this.toolbarStatus = DEFAULT_TOOLBAR_STATUS;
} else {
this.toolbarStatus = sel.status;
}
const focused = this.versionedData.getTaskOption(this.focusedTaskId);
if (focused == null || focused.deleted) {
this.focusedTaskId = RootTaskId;
}
const editing = this.versionedData.getTaskOption(this.editingTaskId);
if (editing == null || editing.deleted) {
this.editingTaskId = NoTaskId;
}
this.taskPositions = this.versionedData.getTasks().map((task) => ({
taskId: task.taskId,
tween: null,
}));
this.updateTaskPositions();
this.incrementUpdateOnZoomCounter();
}
public isBlockerHighlighted = (taskId: TaskId) => {
if (this.versionedData.getTask(taskId).status === StatusCode.DONE) {
return false;
@ -174,7 +219,7 @@ export class Context {
blockerPair.blocked,
blockerPair.blocker,
) ||
this.versionedData.isDescendentOf(
this.versionedData.isDescendantOf(
blockerPair.blocked,
blockerPair.blocker,
)
@ -203,7 +248,7 @@ export class Context {
candidate != task.parentId &&
!this.versionedData
.getDescendantIds(this.reparentingTaskId)
.contains(candidate)
.includes(candidate)
);
}
@ -229,7 +274,7 @@ export class Context {
return this.versionedData
.getAncestors(this.focusedTaskId)
.map((t) => t.taskId)
.contains(taskId);
.includes(taskId);
}
public updateTaskPositions(draggingOnly = false) {
@ -300,7 +345,7 @@ export class Context {
this.draggedTaskId,
);
this.taskPositions
.filter((t) => draggedTaskIds.contains(t.taskId))
.filter((t) => draggedTaskIds.includes(t.taskId))
.forEach((t) => {
if (
t.tween !== null &&
@ -334,7 +379,7 @@ export class Context {
.filter((t) =>
this.versionedData
.getChildren(draggedParentId)
.contains(t.taskId),
.includes(t.taskId),
)
.sort((a, b) => a.tween?.target.y! - b.tween?.target.y!)
.map((t) => t.taskId);
@ -424,8 +469,8 @@ export class Context {
ancestorTasks.some((t) => t.hidden) ||
!(
task.taskId == this.focusedTaskId ||
focusedAncestorIds.contains(task.taskId) ||
focusedDescendantIds.contains(task.taskId)
focusedAncestorIds.includes(task.taskId) ||
focusedDescendantIds.includes(task.taskId)
)
);
}

View file

@ -17,7 +17,7 @@ export class DraggingManager {
public onPointerDown = (e: PointerEvent) => {
console.debug("DraggingManager pointerDown");
this.mouseDown = e.button as MouseDown;
if (this.mouseCodes.contains(this.mouseDown)) {
if (this.mouseCodes.includes(this.mouseDown)) {
this.startX = e.clientX;
this.startY = e.clientY;
const target = e.target as HTMLElement;
@ -29,14 +29,19 @@ export class DraggingManager {
public onPointerUp = (e: PointerEvent) => {
console.debug(`DraggingManager pointerUp: ${this.deltaX}`);
this.reset();
};
/** Clears drag state without requiring a pointer event (e.g. external file reload). */
reset(): void {
this.mouseDown = MouseDown.NONE;
this.isDragging = false;
this.deltaX = 0;
this.deltaY = 0;
};
}
public onPointerMove = (e: PointerEvent) => {
if (this.mouseCodes.contains(this.mouseDown)) {
if (this.mouseCodes.includes(this.mouseDown)) {
const deltaX = e.clientX - this.startX;
const deltaY = e.clientY - this.startY;
// Calculate distance moved

96
src/FileWatcher.ts Normal file
View file

@ -0,0 +1,96 @@
import { Notice, TAbstractFile, type App, TFile, TFolder } from "obsidian";
import type { ProjectData } from "./data/ProjectData.svelte";
import type { TaskData } from "./types";
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
import { deserializeProjectData, updateFile } from "./SaveManager";
import { generateMarkdownLink } from "./LinkManager";
export function getOnRename(app: App) {
return async (file: TAbstractFile, oldPath: string) => {
console.debug(`${file.path} renamed`);
let files: TFile[];
if (file instanceof TFile) {
files = [file];
} else if (file instanceof TFolder) {
files = app.vault
.getMarkdownFiles()
.filter((mdFile) =>
mdFile.path.startsWith(file.path + "/"),
);
} else {
return;
}
await handleRenameFiles(
app,
files,
oldPath,
file.path,
);
};
}
export async function handleRenameFiles(
app: App,
changedMdFiles: TFile[],
oldPath: string,
newPath: string,
) {
const taskmapFiles = app.vault
.getFiles()
.filter((file) => file.path.endsWith(".taskmap"));
const updatedTaskMapFilePaths: Set<string> = new Set<string>();
// update paths in taskmap files
for (const taskmapFile of taskmapFiles) {
console.debug(`handling ${taskmapFile.path}`);
let projectData: ProjectData;
try {
const projectDataRaw = await app.vault.read(taskmapFile);
projectData = deserializeProjectData(projectDataRaw);
} catch (e) {
console.error(`Taskmap rename hook: skipped invalid file ${taskmapFile.path}`);
new Notice(`Taskmap rename hook: skipped invalid file: ${String(e)}`);
continue;
}
const mapping = new Map<string, TaskData>();
projectData.tasks.forEach((t) => {
if (t.path) {
if (t.path.startsWith(oldPath)) {
t.path = newPath + t.path.slice(oldPath.length);
}
mapping.set(t.path, t);
}
});
console.debug("mapping " + JSON.stringify(mapping.keys()));
let changed = false;
changedMdFiles.forEach((mdFile) => {
if (mapping.has(mdFile.path)) {
const t = mapping.get(mdFile.path)!;
t.name = generateMarkdownLink(app, mdFile);
changed = true;
}
});
if (changed) {
console.debug(`${taskmapFile.path} changed`);
// resave file on the file system
await updateFile(app, taskmapFile, projectData);
updatedTaskMapFilePaths.add(taskmapFile.path);
} else {
console.debug(`${taskmapFile.path} not changed`);
}
}
// refresh active views
for (const view of app.workspace
.getLeavesOfType(TASKMAP_VIEW_TYPE)
.map((l) => l.view)
.filter((view) => view instanceof TaskmapView)
.filter(
(view) =>
view.file && updatedTaskMapFilePaths.has(view.file.path),
)) {
await view.refreshUi();
}
}

View file

@ -1,9 +1,17 @@
import { ProjectData } from "./data/ProjectData.svelte.js";
import type { App, TFile } from "obsidian";
import { ProjectData } from "./data/ProjectData.svelte";
import { type App, Notice, type TFile } from "obsidian";
import {
parseProjectFileJson,
TaskmapDataError,
TASKMAP_FILE_SCHEMA_VERSION,
} from "./data/ProjectDataSchema";
export { TaskmapDataError };
export function serializeProjectData(projectData: ProjectData) {
return JSON.stringify(
{
schemaVersion: TASKMAP_FILE_SCHEMA_VERSION,
tasks: projectData.tasks,
blockerPairs: projectData.blockerPairs,
curTaskId: projectData.curTaskId,
@ -13,8 +21,18 @@ export function serializeProjectData(projectData: ProjectData) {
);
}
export function deserializeProjectData(app: App, data: string) {
return new ProjectData(JSON.parse(data));
export function deserializeProjectData(data: string) {
let parsed: unknown;
try {
parsed = JSON.parse(data);
} catch (err) {
new Notice(`Taskmap file could not be read: ${String(err)}`);
throw new TaskmapDataError(
`Taskmap file is not valid JSON. ${String(err)}`,
);
}
const input = parseProjectFileJson(parsed);
return new ProjectData(input);
}
export async function updateFile(

View file

@ -1,6 +1,6 @@
import { debounce, TextFileView, TFile, WorkspaceLeaf } from "obsidian";
import { mount, unmount } from "svelte";
import { DEFAULT_DATA, ProjectData } from "./data/ProjectData.svelte.js";
import { DEFAULT_DATA, ProjectData } from "./data/ProjectData.svelte";
import { Context } from "./Context.svelte.js";
import { NodePositionsCalculator } from "./NodePositionsCalculator";
import TaskmapContainer from "./components/TaskmapContainer.svelte";
@ -32,15 +32,21 @@ export class TaskmapView extends TextFileView {
async refreshUi() {
const projectFile = this.file!;
await this.debouncedSave.run();
this.clear();
await this.onLoadFile(projectFile);
if (this.taskmapContainer === undefined) {
await this.onLoadFile(projectFile);
} else {
const data = await this.app.vault.read(projectFile);
this.setViewData(data);
this.projectData = deserializeProjectData(data);
this.context.reloadFromDisk(this.projectData);
}
}
async onLoadFile(file: TFile): Promise<void> {
this.file = file;
const data = await this.app.vault.read(file);
this.setViewData(data);
this.projectData = deserializeProjectData(this.app, this.data);
this.projectData = deserializeProjectData(data);
this.context = new Context(
this.app,
this.plugin,
@ -56,22 +62,6 @@ export class TaskmapView extends TextFileView {
});
}
getFile() {
if (this.file) {
return this.file;
} else {
throw new Error();
}
}
getFilePath(): string {
if (this.file) {
return this.file.path;
} else {
throw new Error();
}
}
getViewData(): string {
return this.data;
}
@ -88,17 +78,16 @@ export class TaskmapView extends TextFileView {
try {
await updateFile(this.app, this.file!, this.projectData);
} catch (err) {
console.error("Save failed:", err);
throw new Error(`Save failed. ${err.message}`);
console.error("Save failed: ", err);
throw new Error(`Save failed: ${String(err)}`);
}
}
clear(): void {
this.debouncedSave.cancel();
this.setViewData(DEFAULT_DATA);
// 3. Proper unmounting in the clear cycle
if (this.taskmapContainer) {
unmount(this.taskmapContainer);
void unmount(this.taskmapContainer);
this.taskmapContainer = undefined;
}
this.contentEl.empty();

View file

@ -27,6 +27,9 @@
>
{#if entered}
<svg
role="button"
aria-label="Add task"
tabindex="0"
class="button-add"
class:draft={taskData.status === StatusCode.DRAFT}
class:ready={taskData.status === StatusCode.READY}
@ -57,29 +60,16 @@
background: transparent;
pointer-events: auto;
cursor: pointer;
.button-add {
width: 41px;
height: 41px;
stroke-width: 1;
fill: none;
stroke: currentColor;
}
:global(svg.draft) {
stroke: #7E7E7E;
fill: #1E1E1E;
}
:global(svg.ready) {
stroke: #A1383D;
fill: #2E2122;
}
:global(svg.in-progress) {
stroke: #A6A45D;
fill: #2C2C24;
}
:global(svg.done) {
stroke: #3E9959;
fill: #212B24;
fill: var(--button-fill, none);
stroke: var(--button-stroke, currentColor);
}
.button-add.draft { --button-stroke: #7E7E7E; --button-fill: #1E1E1E; }
.button-add.ready { --button-stroke: #A1383D; --button-fill: #2E2122; }
.button-add.in-progress { --button-stroke: #A6A45D; --button-fill: #2C2C24; }
.button-add.done { --button-stroke: #3E9959; --button-fill: #212B24; }
}
</style>

View file

@ -31,7 +31,7 @@
IconCode.KEY,
IconCode.LOCK,
IconCode.REPARENT
].contains(iconCode);
].includes(iconCode);
function onpointerdown(event: MouseEvent) {
isPressedDown = true;
@ -44,8 +44,8 @@
}
let isButtonDisabled = $derived(
[IconCode.LOCK, IconCode.KEY].contains(iconCode) && context.isReparentingOn()
|| [IconCode.LOCK, IconCode.KEY].contains(iconCode) && context.versionedData.getTask(context.selectedTaskId).status == StatusCode.DONE
[IconCode.LOCK, IconCode.KEY].includes(iconCode) && context.isReparentingOn()
|| [IconCode.LOCK, IconCode.KEY].includes(iconCode) && context.versionedData.getTask(context.selectedTaskId).status == StatusCode.DONE
|| (iconCode == IconCode.REPARENT && context.chosenBlockerId !== NoTaskId)
|| (iconCode == IconCode.REPARENT && context.chosenBlockedId !== NoTaskId)
|| (iconCode === IconCode.STATUS_DONE && context.versionedData.isTaskBlocked(context.selectedTaskId))

View file

@ -59,7 +59,7 @@
width: 41px;
height: 41px;
stroke: #bbb;
fill: #181818;
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;

View file

@ -6,7 +6,6 @@
import {delink, getFromRelativePath, isLink} from "../LinkManager";
import {NoTaskId} from "../NodePositionsCalculator";
// PROPS
let {
taskId,
isUnselected,
@ -18,7 +17,7 @@
} = $props();
// STATE
let taskData = context.versionedData.getTask(taskId);
let taskData = $derived(context.versionedData.getTask(taskId));
let isSelected = $derived(context.isSelected(taskId));
let isEditing = $derived(context.editingTaskId === taskId);
let suggest: LinkSuggest | null = null;
@ -87,7 +86,7 @@
hoverParent: textPreviewEl,
targetEl: link,
linktext: link.getAttribute("data-href"),
sourcePath: ""
sourcePath: taskData.path ?? ""
});
}
}
@ -192,13 +191,11 @@
class="text-preview tasktext"
class:unselect={isUnselected}
bind:this={textPreviewEl}
onpointerup={handlePreviewClick}
onmouseover={handlePreviewMouseOver}
>
</div>
{/if}
</div>
<!--onkeydown={(e) => e.key === 'Enter' && toggleEdit()}-->
<style>
:root, .task-text-container {
@ -218,6 +215,12 @@
padding: 0;
gap: 0;
}
.task-text-container.selected .tasktext:hover {
cursor: text;
}
.task-text-container.not-selected .tasktext:hover {
/*cursor: default;*/
}
.tasktext {
margin: 0;
padding: 0;
@ -248,17 +251,11 @@
.tasktext.unselect {
color: color-mix(in srgb, #7E7E7E 100%, #000000 50%);
}
.task-text-container.selected .tasktext:hover {
cursor: text;
}
.task-text-container.not-selected .tasktext:hover {
/*cursor: default;*/
}
.text-edit {
display: flex;
field-sizing: content;
padding-top: 4px; /* fixes text jumping between text-edit and text-preview */
padding-top: 5px; /* fixes text jumping between text-edit and text-preview */
}
.text-edit:hover {
background-color: transparent;
@ -284,8 +281,6 @@
text-align: center;
justify-content: center;
align-items: center;
/*white-space: pre-wrap;*/
/*word-wrap: break-word;*/
overflow: visible;
}
}

View file

@ -15,7 +15,6 @@
let viewportEl: HTMLDivElement | null = null;
let sceneEl: HTMLDivElement | null = null;
let svgGroupEl: SVGGElement | null = null;
let panzoom: PanzoomObject | null = null;
let panstart: Vector2 = {x: 0, y: 0};
let draggingManager = new DraggingManager([MouseDown.MIDDLE, MouseDown.LEFT]);
@ -24,9 +23,6 @@
if (!sceneEl) {
throw new Error('No scene element');
}
if (!svgGroupEl) {
throw new Error('No svg group element');
}
if (!viewportEl) {
throw new Error('No viewport');
}
@ -168,13 +164,11 @@
>
<svg class="svg-layer" overflow="visible">
<defs>
<defs>
<marker id="arrow" markerWidth="5" markerHeight="10" refX="3" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M-1,0 L-1,6 L4.5,3 z" fill="context-stroke" />
</marker>
</defs>
<marker id="arrow" markerWidth="5" markerHeight="10" refX="3" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M-1,0 L-1,6 L4.5,3 z" fill="context-stroke" />
</marker>
</defs>
<g class="svg-group" bind:this={svgGroupEl}>
<g class="svg-group">
{#each (context.versionedData.getTasks()
.filter(t => !context.isTaskHidden(t.taskId))
.filter(t => t.taskId !== RootTaskId)
@ -201,13 +195,11 @@
<svg class="svg-layer" overflow="visible">
<defs>
<defs>
<marker id="arrow" markerWidth="5" markerHeight="10" refX="3" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M-1,0 L-1,6 L4.5,3 z" fill="context-stroke" />
</marker>
</defs>
<marker id="arrow" markerWidth="5" markerHeight="10" refX="3" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M-1,0 L-1,6 L4.5,3 z" fill="context-stroke" />
</marker>
</defs>
<g class="svg-group" bind:this={svgGroupEl}>
<g class="svg-group">
{#if context.chosenBlockerId !== NoTaskId || context.chosenBlockedId !== NoTaskId || context.hoveredBlockerId !== NoTaskId || context.hoveredBlockedId !== NoTaskId}
{#each (context.versionedData.getBlockerPairs().filter(
p => {

View file

@ -74,11 +74,11 @@ export class ProjectData {
public isAncestorOf(taskId: TaskId, candidate: TaskId) {
return this.getAncestors(taskId)
.map((t) => t.taskId)
.contains(candidate);
.includes(candidate);
}
public isDescendentOf(taskId: TaskId, candidate: TaskId) {
return this.getDescendantIds(taskId).contains(candidate);
public isDescendantOf(taskId: TaskId, candidate: TaskId) {
return this.getDescendantIds(taskId).includes(candidate);
}
public getChildren(taskId: number, includeDeleted: boolean = false) {

View file

@ -0,0 +1,94 @@
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;
const statusCodeSchema = v.picklist([
StatusCode.DRAFT,
StatusCode.READY,
StatusCode.IN_PROGRESS,
StatusCode.DONE,
] as const);
const taskDataSchema = v.object({
name: v.string(),
path: v.optional(v.string()),
taskId: v.pipe(v.number(), v.integer()),
status: statusCodeSchema,
deleted: v.boolean(),
parentId: v.pipe(v.number(), v.integer()),
depth: v.pipe(v.number(), v.integer()),
priority: v.pipe(v.number(), v.integer()),
hidden: v.boolean(),
});
const blockerPairSchema = v.object({
blocker: v.pipe(v.number(), v.integer()),
blocked: v.pipe(v.number(), v.integer()),
});
export const projectFileSchema = v.object({
schemaVersion: v.optional(
v.pipe(
v.number(),
v.integer(),
v.minValue(1),
v.maxValue(TASKMAP_FILE_SCHEMA_VERSION),
),
),
tasks: v.array(taskDataSchema),
blockerPairs: v.optional(v.array(blockerPairSchema)),
curTaskId: v.pipe(v.number(), v.integer()),
});
export type ProjectFileParsed = {
tasks: TaskData[];
blockerPairs: BlockerPair[];
curTaskId: number;
};
export class TaskmapDataError extends Error {
constructor(message: string) {
super(message);
this.name = "TaskmapDataError";
}
}
function formatFlattenedIssues(
flat: FlatErrors<typeof projectFileSchema>,
): string {
const parts: string[] = [];
if (flat.root?.length) {
parts.push(...flat.root);
}
if (flat.nested) {
for (const [path, msgs] of Object.entries(flat.nested)) {
if (msgs?.length) {
parts.push(`${path}: ${msgs.join(", ")}`);
}
}
}
if (flat.other?.length) {
parts.push(...flat.other);
}
return parts.join("; ");
}
export function parseProjectFileJson(parsed: unknown): ProjectFileParsed {
const result = v.safeParse(projectFileSchema, parsed);
if (!result.success) {
const flat = v.flatten(result.issues);
const detail = formatFlattenedIssues(flat);
throw new TaskmapDataError(
detail || "Taskmap file does not match the expected structure.",
);
}
const o = result.output;
return {
tasks: o.tasks,
blockerPairs: o.blockerPairs ?? [],
curTaskId: o.curTaskId,
};
}

View file

@ -152,8 +152,8 @@ export class VersionedData {
return this.data.isAncestorOf(taskId, candidate);
};
public isDescendentOf = (taskId: TaskId, candidate: TaskId) => {
return this.data.isDescendentOf(taskId, candidate);
public isDescendantOf = (taskId: TaskId, candidate: TaskId) => {
return this.data.isDescendantOf(taskId, candidate);
};
public containsBlockerPair = (blockerPair: BlockerPair) => {

View file

@ -1,10 +1,6 @@
import {
addIcon,
type App,
Plugin,
TAbstractFile,
TFile,
TFolder,
} from "obsidian";
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
import {
@ -12,11 +8,9 @@ import {
type TaskmapPluginSettings,
} from "./TaskmapPluginSettings";
import { TaskmapSettingTab } from "./TaskmapSettingTab";
import { DEFAULT_DATA } from "./data/ProjectData.svelte.js";
import { DEFAULT_DATA} from "./data/ProjectData.svelte";
import { LOGO_CONTENT, LOGO_NAME } from "./IconService";
import type { TaskData } from "./types";
import { deserializeProjectData, updateFile } from "./SaveManager";
import { generateMarkdownLink } from "./LinkManager";
import { getOnRename} from "./FileWatcher";
export const FILE_EXTENSION = "taskmap";
@ -31,121 +25,19 @@ export default class TaskmapPlugin extends Plugin {
);
this.registerExtensions([FILE_EXTENSION], TASKMAP_VIEW_TYPE);
// This creates an icon in the left ribbon.
addIcon(LOGO_NAME, LOGO_CONTENT);
this.addRibbonIcon(LOGO_NAME, "New taskmap", (_evt: MouseEvent) => {
this.createAndOpenDrawing();
void this.createAndOpenDrawing();
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new TaskmapSettingTab(this.app, this));
// // If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// // Using this function will automatically remove the event listener when this plugin is disabled.
// this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
// console.debug('click', evt);
// });
// // When registering intervals, this function will automatically clear the interval when the plugin is disabled.
// this.registerInterval(window.setInterval(() => console.debug('setInterval'), 5 * 60 * 1000));
this.registerEvent(
this.app.vault.on(
"rename",
async (file: TAbstractFile, oldPath: string) => {
console.debug(`${file.path} renamed`);
if (file instanceof TFile) {
await this.handleRenameFiles(
this.app,
[file],
oldPath,
file.path,
);
} else if (file instanceof TFolder) {
const files = this.app.vault
.getMarkdownFiles()
.filter((file) =>
file.path.startsWith(file.path + "/"),
);
await this.handleRenameFiles(
this.app,
files,
oldPath,
file.path,
);
}
},
),
this.app.vault.on("rename", getOnRename(this.app)),
);
}
async handleRenameFiles(
app: App,
changedMdFiles: TFile[],
oldPath: string,
newPath: string,
) {
const taskmapFiles = this.app.vault
.getFiles()
.filter((file) => file.path.endsWith(".taskmap"));
const updatedTaskMapFilePaths: Set<string> = new Set<string>();
// update paths in taskmap files
for (const taskmapFile of taskmapFiles) {
console.debug(`handling ${taskmapFile.path}`);
const projectDataRaw = await this.app.vault.read(taskmapFile);
const projectData = deserializeProjectData(
this.app,
projectDataRaw,
);
const mapping = new Map<string, TaskData>();
projectData.tasks.forEach((t) => {
if (t.path) {
if (t.path.startsWith(oldPath)) {
t.path = t.path.replace(oldPath, newPath);
}
mapping.set(t.path, t);
}
});
console.debug("mapping " + JSON.stringify(mapping.keys()));
let changed = false;
changedMdFiles.forEach((mdFile) => {
if (mdFile.path.contains("testNote")) {
console.debug(`testNode filepath ${mdFile.path}`);
}
if (mapping.has(mdFile.path)) {
const t = mapping.get(mdFile.path)!;
t.name = generateMarkdownLink(this.app, mdFile);
changed = true;
}
});
if (changed) {
console.debug(`${taskmapFile.path} changed`);
// resave file on the file system
await updateFile(app, taskmapFile, projectData);
updatedTaskMapFilePaths.add(taskmapFile.path);
} else {
console.debug(`${taskmapFile.path} not changed`);
}
}
// refresh active views
for (const view of app.workspace
.getLeavesOfType(TASKMAP_VIEW_TYPE)
.map((l) => l.view)
.filter((view) => view instanceof TaskmapView)
.filter(
(view) =>
view.file && updatedTaskMapFilePaths.has(view.file.path),
)) {
await view.refreshUi();
}
}
public async createAndOpenDrawing(): Promise<string> {
this.app.workspace.detachLeavesOfType(TASKMAP_VIEW_TYPE);
const file = await this.app.vault.create(
`Example ${window.moment().format("YY-MM-DD hh.mm.ss")}.${FILE_EXTENSION}`,
DEFAULT_DATA,
@ -160,9 +52,7 @@ export default class TaskmapPlugin extends Plugin {
state: leaf.view.getState(),
});
await this.app.workspace.revealLeaf(
this.app.workspace.getLeavesOfType(TASKMAP_VIEW_TYPE)[0],
);
await this.app.workspace.revealLeaf(leaf);
return file.path;
}

View file

@ -36,7 +36,7 @@ export const isStatusCode = (s: IconCode) => {
IconCode.STATUS_DRAFT,
IconCode.STATUS_DONE,
IconCode.STATUS_IN_PROGRESS,
].contains(s);
].includes(s);
};
export const classStringFromStatusCode = (code: StatusCode) => {