cleanup, rename, delete hooks

This commit is contained in:
poanse 2026-04-24 05:25:38 +03:00
parent 1f755a3bbb
commit f21e0cfd9a
8 changed files with 109 additions and 97 deletions

View file

@ -1,22 +1,18 @@
import {
Notice,
type Plugin,
TAbstractFile,
type App,
TFile,
TFolder,
} from "obsidian";
import type { ProjectData } from "./data/ProjectData.svelte";
import { type Plugin, TAbstractFile, type App, TFile, TFolder } from "obsidian";
import type { TaskData } from "./types";
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
import { deserializeProjectData, updateFile } from "./SaveManager";
import { loadProjectData, updateFile } from "./SaveManager";
import { delink, generateMarkdownLink, pathIsUnderFolder } from "./LinkManager";
function taskPathAffectedByRename(taskPath: string, oldPath: string): boolean {
return taskPath === oldPath || pathIsUnderFolder(taskPath, oldPath);
}
function remapTaskPathAfterRename(taskPath: string, oldPath: string, newPath: string): string {
function remapTaskPathAfterRename(
taskPath: string,
oldPath: string,
newPath: string,
): string {
if (taskPath === oldPath) {
return newPath;
}
@ -61,7 +57,7 @@ export class FileWatcherWithCache {
);
plugin.registerEvent(
app.vault.on("rename", (file, oldPath) => {
void this.onRenameLinkUpdate(file, oldPath, app);
void this.onRenameLinkUpdate(app, file, oldPath);
}),
);
@ -114,29 +110,19 @@ export class FileWatcherWithCache {
files = [...this.cachedTaskmapFiles].filter((f) =>
pathIsUnderFolder(f.path, file.path),
);
await this.handleNoteFolderDelete(app, file);
} else {
return;
}
await this.handleDeleteFiles(app, files);
}
async onRenameLinkUpdate(
file: TAbstractFile,
oldPath: string,
app: App,
) {
console.debug(`${file.path} renamed`);
let files: TFile[];
if (file instanceof TFile) {
files = [file];
} else if (file instanceof TFolder) {
files = [...this.cachedTaskmapFiles].filter((f) =>
pathIsUnderFolder(f.path, file.path),
);
} else {
return;
// refresh all active views
for (const view of app.workspace
.getLeavesOfType(TASKMAP_VIEW_TYPE)
.map((l) => l.view)
.filter((view) => view instanceof TaskmapView)) {
await view.refreshUi();
}
await this.handleRenameFiles(app, files, oldPath, file.path);
}
async handleDeleteFiles(app: App, files: TFile[]) {
@ -146,13 +132,8 @@ export class FileWatcherWithCache {
// Entire .taskmap was removed; link handler runs before cache — skip read/write.
continue;
}
let projectData: ProjectData;
try {
const projectDataRaw = await app.vault.read(taskmapFile);
projectData = deserializeProjectData(projectDataRaw);
} catch (e) {
console.error(`Taskmap delete hook: skipped invalid file ${taskmapFile.path}`);
new Notice(`Taskmap delete hook: skipped invalid file: ${String(e)}`);
const projectData = await loadProjectData(app, taskmapFile);
if (!projectData) {
continue;
}
let taskmapFileChanged = false;
@ -167,10 +148,25 @@ export class FileWatcherWithCache {
console.debug(`${taskmapFile.path} changed`);
// resave file on the file system
await updateFile(app, taskmapFile, projectData);
} else {
console.debug(`${taskmapFile.path} not changed`);
}
}
}
async onRenameLinkUpdate(app: App, file: TAbstractFile, oldPath: string) {
console.debug(`${file.path} renamed`);
let files: TFile[];
if (file instanceof TFile) {
files = [file];
} else if (file instanceof TFolder) {
files = [...this.cachedTaskmapFiles].filter((f) =>
pathIsUnderFolder(f.path, file.path),
);
await this.handleNoteFolderRename(app, file, oldPath);
} else {
return;
}
await this.handleRenameFiles(app, files, oldPath, file.path);
// refresh all active views
for (const view of app.workspace
.getLeavesOfType(TASKMAP_VIEW_TYPE)
@ -180,6 +176,29 @@ export class FileWatcherWithCache {
}
}
async handleNoteFolderRename(app: App, folder: TFolder, oldPath: string) {
for (const taskmapFile of this.cachedTaskmapFiles) {
const projectData = await loadProjectData(app, taskmapFile);
if (projectData && projectData.folderPath == oldPath) {
console.debug(
`Changed folderPath for ${taskmapFile.name} from ${oldPath} to ${projectData.folderPath}`,
);
projectData.setFolderPath(folder.path);
await updateFile(app, taskmapFile, projectData);
}
}
}
async handleNoteFolderDelete(app: App, folder: TFolder) {
for (const taskmapFile of this.cachedTaskmapFiles) {
const projectData = await loadProjectData(app, taskmapFile);
if (projectData && projectData.folderPath == folder.path) {
console.debug(`Removed folderPath for ${taskmapFile.name}`);
projectData.setFolderPath("");
await updateFile(app, taskmapFile, projectData);
}
}
}
async handleRenameFiles(
app: App,
changedMdFiles: TFile[],
@ -188,26 +207,23 @@ export class FileWatcherWithCache {
) {
// update paths in taskmap files
for (const taskmapFile of [...this.cachedTaskmapFiles]) {
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)}`);
const projectData = await loadProjectData(app, taskmapFile);
if (!projectData) {
continue;
}
const mapping = new Map<string, TaskData>();
projectData.tasks.forEach((t) => {
if (t.path) {
if (taskPathAffectedByRename(t.path, oldPath)) {
t.path = remapTaskPathAfterRename(t.path, oldPath, newPath);
t.path = remapTaskPathAfterRename(
t.path,
oldPath,
newPath,
);
}
mapping.set(t.path, t);
}
});
console.debug("mapping " + JSON.stringify(mapping.keys()));
let changed = false;
changedMdFiles
.filter((mdFile) => mapping.has(mdFile.path))
@ -220,17 +236,7 @@ export class FileWatcherWithCache {
console.debug(`${taskmapFile.path} changed`);
// resave file on the file system
await updateFile(app, taskmapFile, projectData);
} else {
console.debug(`${taskmapFile.path} not changed`);
}
}
// refresh all active views
for (const view of app.workspace
.getLeavesOfType(TASKMAP_VIEW_TYPE)
.map((l) => l.view)
.filter((view) => view instanceof TaskmapView)) {
await view.refreshUi();
}
}
}

View file

@ -2,7 +2,7 @@ import { Modal, Setting, TFolder } from "obsidian";
import type { Context } from "./Context.svelte.js";
import { FolderSuggest } from "./helpers/FolderSuggest";
export class TaskmapSettingsModal extends Modal {
export class ProjectSettingsModal extends Modal {
constructor(private readonly context: Context) {
super(context.app);
}
@ -10,7 +10,7 @@ export class TaskmapSettingsModal extends Modal {
onOpen() {
const { contentEl } = this;
contentEl.createEl("h2", { text: "Project Settings" });
contentEl.createEl("h2", { text: "Project settings" });
new Setting(contentEl)
.setName("Note folder")
@ -19,14 +19,14 @@ export class TaskmapSettingsModal extends Modal {
)
.addText((text) => {
new FolderSuggest(this.app, text.inputEl);
text
.setPlaceholder("")
text.setPlaceholder("")
.setValue(this.context.versionedData.getFolderPath() ?? "")
.onChange(async (value) => {
if (
value === "" ||
this.app.vault.getAbstractFileByPath(value) instanceof
TFolder
this.app.vault.getAbstractFileByPath(
value,
) instanceof TFolder
) {
this.context.versionedData.setFolderPath(value);
this.context.save();

View file

@ -22,6 +22,27 @@ export function serializeProjectData(projectData: ProjectData) {
);
}
export async function updateFile(
app: App,
file: TFile,
projectData: ProjectData,
) {
await app.vault.modify(file, serializeProjectData(projectData));
}
export async function loadProjectData(app: App, taskmapFile: TFile) {
let projectData: ProjectData | undefined = undefined;
try {
const projectDataRaw = await app.vault.read(taskmapFile);
projectData = deserializeProjectData(projectDataRaw);
} catch (e) {
console.error(
`Taskmap rename hook: skipped invalid file ${taskmapFile.path} ${e}`,
);
}
return projectData;
}
export function deserializeProjectData(data: string) {
let parsed: unknown;
try {
@ -35,11 +56,3 @@ export function deserializeProjectData(data: string) {
const input = parseProjectFileJson(parsed);
return new ProjectData(input);
}
export async function updateFile(
app: App,
file: TFile,
projectData: ProjectData,
) {
await app.vault.modify(file, serializeProjectData(projectData));
}

View file

@ -58,7 +58,7 @@ export function getTooltipText(code: IconCode) {
export function getTooltipTextSettings(code: SettingsIconCode) {
switch (code) {
case SettingsIconCode.SETTINGS_MENU:
return "WIP (Settings)";
return "Settings";
case SettingsIconCode.SETTINGS_REDO:
return "Redo";
case SettingsIconCode.SETTINGS_UNDO:

View file

@ -3,13 +3,12 @@
import {SettingsIconCode} from "../types";
import {getTooltipTextSettings, tooltip} from "../Tooltip";
import {Redo2, Settings, Undo2} from 'lucide-svelte';
import { TaskmapSettingsModal } from "src/TaskMapSettingsModal.js";
import { ProjectSettingsModal } from "src/ProjectSettingsModal.js";
let { iconCode, context }: { iconCode: SettingsIconCode, context: Context } = $props();
let isPressedDown = $state(false);
let modal: TaskmapSettingsModal | null = null;
let buttonEl: HTMLDivElement | null = null;
let modal: ProjectSettingsModal | null = null;
function onpointerdown(event: MouseEvent) {
isPressedDown = true;
@ -43,19 +42,7 @@
} else if (iconCode == SettingsIconCode.SETTINGS_REDO) {
context.versionedData.redo();
} else if (iconCode == SettingsIconCode.SETTINGS_MENU) {
modal = new TaskmapSettingsModal(context);
modal.containerEl.classList.add("taskmap-settings-modal");
modal.open();
if (buttonEl) {
const rect = buttonEl.getBoundingClientRect();
modal.containerEl.style.position = "absolute";
// modal.containerEl.style.top = `${rect.top + 20}px`;
// 👇 THIS is the key line (left-align with button)
// modal.containerEl.style.left = `${rect.left}px`;
modal.containerEl.style.transform = "none";
}
new ProjectSettingsModal(context).open();
}
}
@ -65,7 +52,6 @@
</script>
<div class="button"
bind:this={buttonEl}
use:tooltip={getTooltipTextSettings(iconCode)}
class:disabled={isButtonDisabled}
class:no-pan={true}

View file

@ -93,7 +93,6 @@
}
function onpointermove(e: PointerEvent) {
console.debug('handlePointerMove', e.pointerId, e.button);
if (context.draggedTaskId != NoTaskId) {
context.taskDraggingManager.onPointerMove(e);
if (context.taskDraggingManager.isDragging) {

View file

@ -203,6 +203,18 @@ export class ProjectData {
public addBlockerPair = (blockerPair: BlockerPair) => {
this.blockerPairs.push(blockerPair);
};
public getFolderPath = (): string | undefined => {
return this.folderPath;
};
public setFolderPath = (path: string | undefined) => {
const next = path === "" ? undefined : path;
if (next === this.folderPath) {
return;
}
this.folderPath = next;
};
}
export const DEFAULT_DATA = serializeProjectData(ProjectData.getDefault());

View file

@ -125,15 +125,11 @@ export class VersionedData {
};
public getFolderPath = (): string | undefined => {
return this.data.folderPath;
return this.data.getFolderPath();
};
public setFolderPath = (path: string | undefined) => {
const next = path === "" ? undefined : path;
if (next === this.data.folderPath) {
return;
}
this.data.folderPath = next;
this.data.setFolderPath(path);
};
public getTasks = (includeDeleted = false) => {