link update on rename and tooltips

This commit is contained in:
poanse 2026-01-24 09:33:11 +03:00
parent 6d2a1739d9
commit cd5a6f5d5e
14 changed files with 433 additions and 202 deletions

View file

@ -2,7 +2,7 @@
export const TOOLBAR_GAP = 2;
export const TOOLBAR_PADDING = { x: 4, y: 4 };
export const TOOLBAR_SHIFT = 4;
export const SUBTOOLBAR_SHIFT = 2;
export const SUBTOOLBAR_SHIFT = 4;
export const TOOLBAR_SIZE = {
width: 98 * 2,

View file

@ -17,12 +17,15 @@ import {
V2,
} from "./NodePositionsCalculator";
import { DraggingManager } from "./DraggingManager.svelte";
import { delink, LinkManager, tasknameFromFilePath } from "./LinkManager";
export class Context {
app: App;
view: TaskmapView;
nodePositionsCalculator: NodePositionsCalculator;
linkManager: LinkManager;
pressedButtonCode = $state(-1);
editingTaskId = $state(NoTaskId);
draggedTaskId = $state(NoTaskId);
selectedTaskId = $state(NoTaskId);
focusedTaskId = $state(RootTaskId);
@ -53,6 +56,7 @@ export class Context {
this.nodePositionsCalculator = nodePositionsCalculator;
this.app = app;
this.projectData = projectData;
this.linkManager = new LinkManager(app);
this.taskPositions = projectData.tasks
.filter((t) => !t.deleted)
@ -273,11 +277,9 @@ export class Context {
}
public save() {
const x = TaskmapPlugin.getActiveView();
if (x != null) {
x.debouncedSave();
console.log("saved");
}
const x = this.view;
x.debouncedSave();
console.log("saved");
}
public addTask(parentId: TaskId): void {
@ -352,7 +354,7 @@ export class Context {
`Created by Taskmap.`,
);
const task = this.projectData.getTask(taskId);
task.name = this.tasknameFromFilePath(filepath);
task.name = tasknameFromFilePath(filepath);
this.save();
await this.openOrFocusNote(tfile);
} catch (error) {
@ -368,17 +370,10 @@ export class Context {
const taskName = task.name;
// Sanitize the name for Obsidian filenames
let sanitizedName = taskName.replace(/[\\/:*?"<>|]/g, "-");
sanitizedName = this.delink(sanitizedName);
sanitizedName = delink(sanitizedName);
return `${sanitizedName}.md`;
}
public tasknameFromFilePath(path: string) {
if (path.endsWith(".md")) {
path = path.slice(0, path.length - 3);
}
return `[[${path}]]`;
}
/**
* Opens a file with "Smart Focus":
* 1. If file is already open anywhere -> Focus it.
@ -437,14 +432,6 @@ export class Context {
}
}
public isLink(s: string): boolean {
return s.startsWith("[[") && s.endsWith("]]");
}
public delink(s: string) {
return this.isLink(s) ? s.slice(2, s.length - 2) : s;
}
public serializeForDebugging() {
return JSON.stringify({
pressedButtonIndex: this.pressedButtonCode,

48
src/LinkManager.ts Normal file
View file

@ -0,0 +1,48 @@
import type { App, TFile } from "obsidian";
export function tasknameFromFilePath(path: string) {
if (path.endsWith(".md")) {
path = path.slice(0, path.length - 3);
}
return `[[${path}]]`;
}
export function isLink(s: string): boolean {
return s.startsWith("[[") && s.endsWith("]]");
}
export function delink(s: string) {
return isLink(s) ? s.slice(2, s.length - 2) : s;
}
// relativePath to TFile
export function getFromRelativePath(app: App, path: string) {
return app.vault.getFileByPath(path);
}
// TFile to wikilink
export function generateMarkdownLink(app: App, file: TFile) {
return app.fileManager.generateMarkdownLink(file, "");
}
export class LinkManager {
private app: App;
public constructor(app: App) {
this.app = app;
}
// wikilink to TFile
public getFromLink = (linkText: string) => {
return this.app.metadataCache.getFirstLinkpathDest(
delink(linkText),
"",
);
};
public extractTextLink = (text: string) => {
if (!isLink(text)) {
throw new Error("Text is not a link");
}
return delink(text);
};
}

View file

@ -1,5 +1,6 @@
import { StatusCode, type TaskData, type TaskId } from "./types";
import { NoTaskId, RootTaskId } from "./NodePositionsCalculator";
import { serializeProjectData } from "./SaveManager";
export class ProjectData {
tasks = $state(new Array<TaskData>());
@ -12,14 +13,6 @@ export class ProjectData {
});
}
public serialize() {
return JSON.stringify(
{ tasks: this.tasks, curTaskId: this.curTaskId },
null,
2,
);
}
constructor(obj: { tasks: TaskData[]; curTaskId: number }) {
this.tasks = obj.tasks;
this.curTaskId = obj.curTaskId;
@ -229,4 +222,4 @@ export class ProjectData {
}
}
export const DEFAULT_DATA = ProjectData.getDefault().serialize();
export const DEFAULT_DATA = serializeProjectData(ProjectData.getDefault());

25
src/SaveManager.ts Normal file
View file

@ -0,0 +1,25 @@
import { ProjectData } from "./ProjectData.svelte";
import type { App, TFile } from "obsidian";
export function serializeProjectData(projectData: ProjectData) {
return JSON.stringify(
{
tasks: projectData.tasks,
curTaskId: projectData.curTaskId,
},
null,
2,
);
}
export function deserializeProjectData(app: App, data: string) {
return new ProjectData(JSON.parse(data));
}
export async function updateFile(
app: App,
file: TFile,
projectData: ProjectData,
) {
await app.vault.modify(file!, serializeProjectData(projectData));
}

View file

@ -4,8 +4,9 @@ import { DEFAULT_DATA, ProjectData } from "./ProjectData.svelte";
import { Context } from "./Context.svelte.js";
import { NodePositionsCalculator } from "./NodePositionsCalculator";
import TaskmapContainer from "./components/TaskmapContainer.svelte";
import { deserializeProjectData, updateFile } from "./SaveManager";
export const VIEW_TYPE = "taskmap-view";
export const TASKMAP_VIEW_TYPE = "taskmap-view";
export class TaskmapView extends TextFileView {
taskmapContainer: ReturnType<typeof TaskmapContainer> | undefined;
@ -18,24 +19,33 @@ export class TaskmapView extends TextFileView {
data: string = DEFAULT_DATA;
projectData: ProjectData;
context: Context;
getViewType() {
return VIEW_TYPE;
return TASKMAP_VIEW_TYPE;
}
async refreshUi() {
const projectFile = this.file!;
this.clear();
await this.onLoadFile(projectFile);
}
async onLoadFile(file: TFile): Promise<void> {
this.file = file;
this.setViewData(await this.app.vault.read(file));
this.projectData = new ProjectData(JSON.parse(this.data));
const data = await this.app.vault.read(file);
this.setViewData(data);
this.projectData = deserializeProjectData(this.app, this.data);
this.context = new Context(
this,
this.projectData,
this.app,
new NodePositionsCalculator(),
);
this.taskmapContainer = mount(TaskmapContainer, {
target: this.contentEl,
props: {
context: new Context(
this,
this.projectData,
this.app,
new NodePositionsCalculator(),
),
context: this.context,
},
});
}
@ -70,10 +80,7 @@ export class TaskmapView extends TextFileView {
async save() {
try {
await this.app.vault.modify(
this.file!,
this.projectData.serialize(),
);
await updateFile(this.app, this.file!, this.projectData);
} catch (err) {
console.error("Save failed:", err);
throw new Error(`Save failed. ${err.message}`);

56
src/Tooltip.ts Normal file
View file

@ -0,0 +1,56 @@
import { setTooltip } from "obsidian";
import { IconCode } from "./types";
/**
* Svelte Action to add an Obsidian-native tooltip to an element.
*
* @param node - The DOM element to attach the tooltip to.
* @param text - The text to display in the tooltip.
*/
export function tooltip(node: HTMLElement, text: string) {
setTooltip(node, text, {
placement: "top",
});
return {
update(newText: string) {
// Update the tooltip if the text prop changes
setTooltip(node, newText, {
placement: "top",
});
},
};
}
export function getTooltipText(code: IconCode) {
switch (code) {
case IconCode.FOCUS:
return "Focus";
case IconCode.CREATE_LINKED_NOTE:
return "Add link";
case IconCode.REPARENT:
return "Reparent";
case IconCode.KEY:
return "Block another task";
case IconCode.LOCK:
return "Add blocker task";
case IconCode.REMOVE:
return "Remove";
case IconCode.REMOVE_SINGLE:
return "Remove single task";
case IconCode.REMOVE_MULTIPLE:
return "Remove task branch";
case IconCode.STATUS:
return "Status";
case IconCode.STATUS_DRAFT:
return "Draft";
case IconCode.STATUS_READY:
return "Ready";
case IconCode.STATUS_IN_PROGRESS:
return "In progress";
case IconCode.STATUS_DONE:
return "Done";
default:
return "placeholder tooltip";
}
}

View file

@ -11,6 +11,7 @@
Trash2,
Unplug
} from 'lucide-svelte';
import {getTooltipText, tooltip} from "../Tooltip";
let {
iconCode,
@ -73,6 +74,7 @@
</script>
<div class="button"
use:tooltip={getTooltipText(iconCode)}
class:no-pan={true}
class:is-pressed-up={isPressed}
class:is-pressed-down={isPressedDown}

View file

@ -42,6 +42,7 @@
if (context.isReparentingOn() && context.isValidReparentingTarget(taskId)) {
context.finishReparenting(taskId);
} else {
context.pressedButtonCode = -1;
context.setSelectedTaskId(taskData.taskId);
// const input = (document.getElementById('titleInput') as HTMLInputElement);
// input.disabled = false;
@ -75,7 +76,9 @@
onmouseenter={() => isHovered = true}
onmouseleave={() => isHovered = false}
onpointerdown={(e: PointerEvent) => {
context.startTaskDragging(e, taskId);
if (context.editingTaskId === NoTaskId) {
context.startTaskDragging(e, taskId);
}
e.stopPropagation();
}}
onpointerup={onPointerUp}
@ -87,12 +90,6 @@
{isUnselected}
{context}
app={context.app}
content={taskData.name}
onSave={(newContent)=> {
taskData.name = newContent;
context.save();
}}
file={context.view.getFile()}
/>
</div>
{#if !context.taskDraggingManager.isDragging}

View file

@ -1,8 +1,11 @@
<script lang="ts">
import { onMount, tick } from "svelte";
import {MarkdownRenderer, Component, App, type TFile} from "obsidian";
import {onMount, tick} from "svelte";
import {App, 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
let {
@ -10,29 +13,22 @@
isUnselected,
context,
app,
content,
file,
onSave
}: {
taskId: number,
isUnselected: boolean,
context: Context,
app: App;
content: string;
file: TFile;
onSave: (newContent: string) => void
} = $props();
// STATE
let taskData = context.projectData.getTask(taskId);
let isSelected = $derived(context.isSelected(taskId));
let isEditing = $state(false);
let isEditing = $derived(context.editingTaskId === taskId);
let suggest: LinkSuggest | null = null;
let textPreviewEl: HTMLElement;
let textEditEl: HTMLTextAreaElement;
let component = new Component(); // Required by Obsidian to manage render lifecycle
let isDragging = $derived(context.taskDraggingManager.isDragging);
onMount(() => {
if(!isEditing && textPreviewEl) {
renderMarkdown();
@ -43,11 +39,9 @@
};
});
$effect(() => {
if (!isEditing && textPreviewEl) {
if (textPreviewEl) {
renderMarkdown();
}
});
$effect(() => {
if (context.taskDraggingManager.isDragging) {
document.body.classList.add('is-dragging-task');
} else {
@ -64,40 +58,21 @@
}
context.finishTaskDragging(e);
console.log('task text clicked');
const target = e.target as HTMLElement;
// Check if the clicked element (or its parent) is an internal link
const internalLink = target.closest(".internal-link");
if (internalLink) {
// Prevent the default "Swap to Edit" behavior
e.preventDefault(); // TODO: is this line needed?
e.stopPropagation();
const href = internalLink.getAttribute("data-href");
if (href === null) {
throw new Error('href is null');
}
// Open the link using Obsidian's API
// Whether to open in a new tab or in current one
// const openInNewLeaf = e.ctrlKey || e.metaKey;
// app.workspace.openLinkText(href, sourcePath, openInNewLeaf);
const filepath = context.filePathFromTask(taskId);
const file = context.app.vault.getAbstractFileByPath(filepath) as TFile;
context.openOrFocusNote(file);
return;
}
const externalLink = target.closest("a.external-link");
if (externalLink){
e.stopPropagation();
return;
}
if (context.isSelected(taskId)) {
toggleEdit();
e.stopPropagation();
toggleEdit();
return;
}
const target = e.target as HTMLElement;
const link = target.closest(".internal-link");
if (link && taskData.path) {
const file = getFromRelativePath(context.app, taskData.path);
if (file) {
e.preventDefault(); // TODO: is this line needed?
e.stopPropagation();
context.openOrFocusNote(file);
}
}
}
function handlePreviewMouseOver(e: MouseEvent) {
@ -119,26 +94,17 @@
});
}
}
async function renderMarkdown() {
textPreviewEl.empty(); // Clear previous render
if (file !== undefined) {
await MarkdownRenderer.render(
app,
`[[${file.path}]]`,
textPreviewEl,
"",
component,
);
} else {
await MarkdownRenderer.render(
app,
content,
textPreviewEl,
"",
component,
);
}
const content = taskData.name;
await MarkdownRenderer.render(
app,
content,
textPreviewEl,
"",
component,
);
// After rendering, find all links and disable their native dragging
const links = textPreviewEl.querySelectorAll('a, .internal-link, .external-link');
links.forEach(link => {
@ -149,7 +115,7 @@
}
async function toggleEdit() {
isEditing = true;
context.editingTaskId = taskId;
// Wait for DOM update so textarea exists, then focus
await tick();
if (textEditEl) {
@ -158,11 +124,6 @@
}
}
export function handleBlur() {
isEditing = false;
onSave(content); // Persist changes
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === "Enter") {
e.preventDefault();
@ -173,7 +134,31 @@
textEditEl.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
}
}
async function handleBlur() {
// TODO: throws error because text preview doesn't exit when blur on textedit happens
context.editingTaskId = NoTaskId;
handleInput();
await tick(); // Wait for DOM to render preview element
await renderMarkdown();
}
function handleInput() {
if (textEditEl === null) {
return;
}
taskData.name = textEditEl.value;
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;
} else {
taskData.path = undefined;
}
context.save();
}
</script>
@ -189,10 +174,10 @@
class:unselect={isUnselected}
maxlength="28"
bind:this={textEditEl}
bind:value={content}
onblur={handleBlur}
onkeydown={handleKeydown}
></textarea>
oninput={handleInput}
>{taskData.name}</textarea>
{:else}
<div
class="text-preview tasktext"

View file

@ -159,7 +159,11 @@
{/each}
</div>
<Toolbar context={context}/>
{#if context.selectedTaskId !== -1}
{#key context.selectedTaskId}
<Toolbar context={context} taskId={context.selectedTaskId}/>
{/key}
{/if}
</div>
</div>

View file

@ -11,14 +11,16 @@
import {quintOut} from 'svelte/easing';
import {slideCustom} from '../Custom';
import type {Context} from "../Context.svelte.js";
import {IconCode, toIconCode} from "../types";
import {IconCode, StatusCode, type TaskId, toIconCode} from "../types";
import {RootTaskId} from "../NodePositionsCalculator";
let {
taskId,
context
}: {context: Context} = $props();
}: {taskId: TaskId,
context: Context} = $props();
let position = $derived(context.getCurrentTaskPosition(context.selectedTaskId));
let position = $derived(context.getCurrentTaskPosition(taskId));
function getTop() {
return position.y - TOOLBAR_SIZE.height - TOOLBAR_SHIFT;
@ -26,76 +28,97 @@
function getLeft() {
return position.x + TASK_SIZE.width_hovered / 2;
}
let isLeafTask = $derived(context.projectData.getChildren(taskId).length === 0);
let isLeafTask = $derived(context.selectedTaskId != -1 && context.projectData.getChildren(context.selectedTaskId).length === 0);
let toolbarButtons = $derived(taskId == RootTaskId ? [
IconCode.CREATE_LINKED_NOTE,
IconCode.FOCUS,
IconCode.STATUS
] : [
IconCode.CREATE_LINKED_NOTE,
IconCode.REMOVE,
IconCode.REPARENT,
IconCode.KEY,
IconCode.LOCK,
IconCode.FOCUS,
IconCode.STATUS
]);
let removeButtons = [
IconCode.REMOVE_SINGLE,
IconCode.REMOVE_MULTIPLE
];
let statusButtons = $derived((isLeafTask ? [
StatusCode.DRAFT,
StatusCode.READY,
StatusCode.IN_PROGRESS,
StatusCode.DONE,
]: [
StatusCode.DRAFT,
context.projectData.calculateStatus(taskId)
]).map(x => toIconCode(x)));
let subtoolbarTopShift = (buttons: IconCode[]) => {
return - (buttons.length * BUTTON_SIZE + (buttons.length -1)*TOOLBAR_GAP + 2*TOOLBAR_PADDING.y + SUBTOOLBAR_SHIFT);
};
</script>
{#if context.selectedTaskId !== -1 && !context.taskDraggingManager.isDragging}
<div
class="toolbar"
class:no-pan={true}
in:fade={{ duration: 500 }}
out:fade={{ duration: 300 }}
onclick={(e) => e.stopPropagation()}
onpointerdown={(e) => e.stopPropagation()}
onpointerup={(e) => e.stopPropagation()}
style="
top: {getTop()}px;
left: {getLeft()}px;
"
>
{#key context.updateOnZoomCounter}
<Button iconCode={IconCode.CREATE_LINKED_NOTE} {context} />
{#if context.selectedTaskId != RootTaskId}
<Button iconCode={IconCode.REMOVE} {context}/>
<Button iconCode={IconCode.REPARENT} {context} />
<Button iconCode={IconCode.KEY} {context} />
<Button iconCode={IconCode.LOCK} {context} />
{#if !context.taskDraggingManager.isDragging}
<div
class="toolbar"
class:no-pan={true}
in:fade|global={{ duration: 500 }}
out:fade|global={{ duration: 300 }}
onclick={(e) => e.stopPropagation()}
onpointerdown={(e) => e.stopPropagation()}
onpointerup={(e) => e.stopPropagation()}
style="
top: {getTop()}px;
left: {getLeft()}px;
"
>
{#key context.updateOnZoomCounter}
{#each toolbarButtons as button}
<Button iconCode={button} {context} />
{/each}
{/key}
{#if context.pressedButtonCode === IconCode.REMOVE}
<div
class="subtoolbar"
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
style="
top: {subtoolbarTopShift(removeButtons)}px;
left: {toolbarButtons.indexOf(IconCode.REMOVE) * (BUTTON_SIZE + TOOLBAR_GAP) - 2}px;
"
>
{#key context.updateOnZoomCounter}
<Button iconCode={IconCode.REMOVE_SINGLE} {context} />
<Button iconCode={IconCode.REMOVE_MULTIPLE} {context} />
{/key}
</div>
{/if}
<Button iconCode={IconCode.FOCUS} {context} />
<Button iconCode={IconCode.STATUS} {context} />
{/key}
{#if context.pressedButtonCode === IconCode.REMOVE && context.selectedTaskId !== -1}
<div
class="subtoolbar"
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
style="
top: {- 2 * BUTTON_SIZE - TOOLBAR_GAP - 2 * TOOLBAR_PADDING.y - SUBTOOLBAR_SHIFT - 2}px;
left: {1 * (BUTTON_SIZE + TOOLBAR_GAP) - 2}px;
"
>
{#key context.updateOnZoomCounter}
<Button iconCode={IconCode.REMOVE_SINGLE} {context} />
<Button iconCode={IconCode.REMOVE_MULTIPLE} {context} />
{/key}
</div>
{/if}
{#if context.pressedButtonCode === IconCode.STATUS && context.selectedTaskId !== -1}
<div
class="subtoolbar"
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
style="
top: {- (isLeafTask ? 2 : 1) * 2 * BUTTON_SIZE - ((isLeafTask ? 2 : 0) + 1)*TOOLBAR_GAP - 2*TOOLBAR_PADDING.y - SUBTOOLBAR_SHIFT - 2}px;
left: {6 * (BUTTON_SIZE + TOOLBAR_GAP) - 2}px;
"
>
{#key context.updateOnZoomCounter}
{#if isLeafTask}
<Button iconCode={IconCode.STATUS_DRAFT} {context} />
<Button iconCode={IconCode.STATUS_READY} {context} />
<Button iconCode={IconCode.STATUS_IN_PROGRESS} {context} />
<Button iconCode={IconCode.STATUS_DONE} {context} />
{:else}
<Button iconCode={IconCode.STATUS_DRAFT} {context} />
<Button iconCode={toIconCode(context.projectData.calculateStatus(context.selectedTaskId))} {context} />
{/if}
{/key}
</div>
{/if}
</div>
{#if context.pressedButtonCode === IconCode.STATUS}
<div
class="subtoolbar"
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
style="
top: {subtoolbarTopShift(statusButtons)}px;
left: {toolbarButtons.indexOf(IconCode.STATUS) * (BUTTON_SIZE + TOOLBAR_GAP) - 2}px;
"
>
{#key context.updateOnZoomCounter}
{#each statusButtons as button}
<Button iconCode={button} {context} />
{/each}
{/key}
</div>
{/if}
</div>
{/if}

View file

@ -1,9 +1,19 @@
import { addIcon, Plugin } from "obsidian";
import { TaskmapView, VIEW_TYPE } from "./TaskmapView";
import { type PluginSettings, DEFAULT_SETTINGS } from "./PluginSettings";
import {
addIcon,
type App,
Plugin,
TAbstractFile,
TFile,
TFolder,
} from "obsidian";
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
import { DEFAULT_SETTINGS, type PluginSettings } from "./PluginSettings";
import { TaskmapSettingTab } from "./TaskmapSettingTab";
import { DEFAULT_DATA } from "./ProjectData.svelte";
import { LOGO_CONTENT, LOGO_NAME } from "./IconService";
import type { TaskData } from "./types";
import { deserializeProjectData, updateFile } from "./SaveManager";
import { generateMarkdownLink } from "./LinkManager";
export const FILE_EXTENSION = "taskmap";
@ -14,8 +24,8 @@ export default class TaskmapPlugin extends Plugin {
async onload() {
TaskmapPlugin.instance = this;
await this.loadSettings();
this.registerView(VIEW_TYPE, (leaf) => new TaskmapView(leaf));
this.registerExtensions([FILE_EXTENSION], VIEW_TYPE);
this.registerView(TASKMAP_VIEW_TYPE, (leaf) => new TaskmapView(leaf));
this.registerExtensions([FILE_EXTENSION], TASKMAP_VIEW_TYPE);
// This creates an icon in the left ribbon.
addIcon(LOGO_NAME, LOGO_CONTENT);
@ -34,6 +44,99 @@ export default class TaskmapPlugin extends Plugin {
// // When registering intervals, this function will automatically clear the interval when the plugin is disabled.
// this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
this.registerEvent(
this.app.vault.on(
"rename",
async (file: TAbstractFile, oldPath: string) => {
console.log(`${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,
);
}
},
),
);
}
async handleRenameFiles(
app: App,
changedMdFiles: TFile[],
oldPath: string,
newPath: string,
) {
const taskmapFiles = this.app.vault
.getFiles()
.filter((file) => file.path.endsWith(".taskmap"));
let updatedTaskMapFilePaths: Set<string> = new Set<string>();
// update paths in taskmap files
for (const taskmapFile of taskmapFiles) {
console.log(`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.log("mapping " + JSON.stringify(mapping.keys()));
let changed = false;
changedMdFiles.forEach((mdFile) => {
if (mdFile.path.contains("testNote")) {
console.log(`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.log(`${taskmapFile.path} changed`);
// resave file on the file system
await updateFile(app, taskmapFile, projectData);
updatedTaskMapFilePaths.add(taskmapFile.path);
} else {
console.log(`${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 static getActiveView() {
@ -43,7 +146,7 @@ export default class TaskmapPlugin extends Plugin {
}
public async createAndOpenDrawing(): Promise<string> {
this.app.workspace.detachLeavesOfType(VIEW_TYPE);
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}`,
@ -54,13 +157,13 @@ export default class TaskmapPlugin extends Plugin {
await leaf.openFile(file, { active: true });
leaf.setViewState({
type: VIEW_TYPE,
await leaf.setViewState({
type: TASKMAP_VIEW_TYPE,
state: leaf.view.getState(),
});
this.app.workspace.revealLeaf(
this.app.workspace.getLeavesOfType(VIEW_TYPE)[0],
await this.app.workspace.revealLeaf(
this.app.workspace.getLeavesOfType(TASKMAP_VIEW_TYPE)[0],
);
return file.path;

View file

@ -58,6 +58,7 @@ export type TaskId = number;
export type TaskData = {
name: string;
path?: string | undefined | null;
taskId: TaskId;
status: StatusCode;
deleted: boolean;