Enhance InterviewWizardModal and SectionEdgesOverviewModal for improved link handling
- Integrated LinkTargetPickerModal into InterviewWizardModal to allow users to select link targets dynamically, enhancing user experience during note linking. - Updated link handling logic to support full links and improved edge type resolution in SectionEdgesOverviewModal, ensuring accurate target type identification. - Refactored code to streamline the extraction of note links and their associated types, improving overall functionality and maintainability.
This commit is contained in:
parent
054cfcf82d
commit
c044d6e8db
136
src/interview/targetTypeResolver.ts
Normal file
136
src/interview/targetTypeResolver.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
/**
|
||||||
|
* Ermittelt Zieltyp für Note-Links: Note-Type (ganze Note) oder Sektionstyp (bei Note#Abschnitt).
|
||||||
|
* Wird in Kantenübersicht und Toolbar-Link-Auswahl genutzt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { App } from "obsidian";
|
||||||
|
import { TFile } from "obsidian";
|
||||||
|
|
||||||
|
/** Link ohne Alias: "Note" oder "Note#Abschnitt" */
|
||||||
|
export function parseNoteLink(linkTarget: string): { basename: string; heading: string | null } {
|
||||||
|
const withoutAlias = linkTarget.split("|")[0]?.trim() || linkTarget;
|
||||||
|
const sharp = withoutAlias.indexOf("#");
|
||||||
|
if (sharp === -1) {
|
||||||
|
return { basename: withoutAlias.trim(), heading: null };
|
||||||
|
}
|
||||||
|
const basename = withoutAlias.slice(0, sharp).trim();
|
||||||
|
const heading = withoutAlias.slice(sharp + 1).trim() || null;
|
||||||
|
return { basename, heading };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest aus Dateiinhalt den Sektionstyp für eine Überschrift.
|
||||||
|
* Erwartet Format: ## Überschrift (optional ^block-id), nächste Zeile > [!section] typ
|
||||||
|
*/
|
||||||
|
export function getSectionTypeForHeading(content: string, heading: string): string | null {
|
||||||
|
const lines = content.split("\n");
|
||||||
|
const headingNorm = heading.trim().toLowerCase();
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line === undefined) continue;
|
||||||
|
const match = line.match(/^(#{1,6})\s+(.+?)(?:\s+\^[\w-]+)?\s*$/);
|
||||||
|
if (match && match[2]) {
|
||||||
|
const lineHeading = match[2].trim();
|
||||||
|
if (lineHeading.toLowerCase() === headingNorm) {
|
||||||
|
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
||||||
|
const nextLine = lines[j];
|
||||||
|
if (nextLine === undefined) continue;
|
||||||
|
const sectionMatch = nextLine.match(/^\s*>\s*\[!section\]\s+(.+)\s*$/);
|
||||||
|
if (sectionMatch && sectionMatch[1]) {
|
||||||
|
return sectionMatch[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedTargetType {
|
||||||
|
targetType: string | null;
|
||||||
|
displayLabel: string;
|
||||||
|
basename: string;
|
||||||
|
heading: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt Zieltyp für einen Note-Link (ganze Note → Note-Type, mit #Abschnitt → Sektionstyp).
|
||||||
|
* @param app Obsidian App
|
||||||
|
* @param linkTarget z.B. "Note" oder "Note#Abschnitt" (ohne Alias)
|
||||||
|
* @param sourcePath Pfad der Quelldatei für getFirstLinkpathDest
|
||||||
|
*/
|
||||||
|
export async function resolveTargetTypeForNoteLink(
|
||||||
|
app: App,
|
||||||
|
linkTarget: string,
|
||||||
|
sourcePath: string
|
||||||
|
): Promise<ResolvedTargetType> {
|
||||||
|
const { basename, heading } = parseNoteLink(linkTarget);
|
||||||
|
const displayLabel = heading ? `${basename}#${heading}` : basename;
|
||||||
|
|
||||||
|
let targetType: string | null = null;
|
||||||
|
try {
|
||||||
|
const targetFile = app.metadataCache.getFirstLinkpathDest(basename, sourcePath);
|
||||||
|
if (!targetFile || !(targetFile instanceof TFile)) {
|
||||||
|
return { targetType: null, displayLabel, basename, heading };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heading) {
|
||||||
|
const content = await app.vault.read(targetFile);
|
||||||
|
targetType = getSectionTypeForHeading(content, heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType === null) {
|
||||||
|
const cache = app.metadataCache.getFileCache(targetFile);
|
||||||
|
if (cache?.frontmatter) {
|
||||||
|
const t = cache.frontmatter.type ?? cache.frontmatter.noteType;
|
||||||
|
targetType = t != null ? String(t) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return { targetType, displayLabel, basename, heading };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest aus einer Note alle Überschriften mit zugehörigem Sektionstyp (falls vorhanden).
|
||||||
|
* Für Toolbar-Auswahl: "Ganze Note" oder "## Überschrift (typ)".
|
||||||
|
*/
|
||||||
|
export interface HeadingWithType {
|
||||||
|
heading: string;
|
||||||
|
sectionType: string | null;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHeadingsWithSectionTypes(
|
||||||
|
app: App,
|
||||||
|
file: TFile
|
||||||
|
): Promise<HeadingWithType[]> {
|
||||||
|
const content = await app.vault.read(file);
|
||||||
|
const lines = content.split("\n");
|
||||||
|
const result: HeadingWithType[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line === undefined) continue;
|
||||||
|
const match = line.match(/^(#{1,6})\s+(.+?)(?:\s+\^[\w-]+)?\s*$/);
|
||||||
|
if (match && match[1] && match[2]) {
|
||||||
|
const level = match[1].length;
|
||||||
|
const heading = match[2].trim();
|
||||||
|
let sectionType: string | null = null;
|
||||||
|
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
||||||
|
const nextLine = lines[j];
|
||||||
|
if (nextLine === undefined) continue;
|
||||||
|
const sectionMatch = nextLine.match(/^\s*>\s*\[!section\]\s+(.+)\s*$/);
|
||||||
|
if (sectionMatch && sectionMatch[1]) {
|
||||||
|
sectionType = sectionMatch[1].trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push({ heading, sectionType, level });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ import { buildSemanticMappings, type BuildResult } from "../mapping/semanticMapp
|
||||||
import type { MindnetSettings } from "../settings";
|
import type { MindnetSettings } from "../settings";
|
||||||
import { InlineEdgeTypeModal, type InlineEdgeTypeResult } from "./InlineEdgeTypeModal";
|
import { InlineEdgeTypeModal, type InlineEdgeTypeResult } from "./InlineEdgeTypeModal";
|
||||||
import { SectionEdgesOverviewModal, type SectionEdgesOverviewResult } from "./SectionEdgesOverviewModal";
|
import { SectionEdgesOverviewModal, type SectionEdgesOverviewResult } from "./SectionEdgesOverviewModal";
|
||||||
|
import { LinkTargetPickerModal } from "./LinkTargetPickerModal";
|
||||||
import { getSectionKeyForWizardContext } from "../interview/sectionKeyResolver";
|
import { getSectionKeyForWizardContext } from "../interview/sectionKeyResolver";
|
||||||
import type { PendingEdgeAssignment } from "../interview/wizardState";
|
import type { PendingEdgeAssignment } from "../interview/wizardState";
|
||||||
import { VocabularyLoader } from "../vocab/VocabularyLoader";
|
import { VocabularyLoader } from "../vocab/VocabularyLoader";
|
||||||
|
|
@ -601,30 +602,35 @@ export class InterviewWizardModal extends Modal {
|
||||||
app,
|
app,
|
||||||
this.noteIndex,
|
this.noteIndex,
|
||||||
async (result: EntityPickerResult) => {
|
async (result: EntityPickerResult) => {
|
||||||
// Check if inline micro edging is enabled (also for toolbar)
|
|
||||||
// Support: inline_micro, both (inline_micro + post_run)
|
|
||||||
const edgingMode = this.profile.edging?.mode;
|
const edgingMode = this.profile.edging?.mode;
|
||||||
const shouldRunInlineMicro =
|
const shouldRunInlineMicro =
|
||||||
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
||||||
this.settings?.inlineMicroEnabled !== false;
|
this.settings?.inlineMicroEnabled !== false;
|
||||||
|
const currentStep = getCurrentStep(this.state);
|
||||||
|
|
||||||
let linkText = `[[${result.basename}]]`;
|
let linkTarget = result.basename;
|
||||||
|
if (shouldRunInlineMicro && currentStep) {
|
||||||
if (shouldRunInlineMicro) {
|
const file = this.app.vault.getAbstractFileByPath(result.path);
|
||||||
// Get current step for section key resolution
|
if (file && file instanceof TFile) {
|
||||||
const currentStep = getCurrentStep(this.state);
|
const picker = new LinkTargetPickerModal(
|
||||||
if (currentStep) {
|
this.app,
|
||||||
console.log("[Mindnet] Starting inline micro edging from toolbar");
|
file,
|
||||||
const edgeType = await this.handleInlineMicroEdging(currentStep, result.basename, result.path);
|
result.basename,
|
||||||
if (edgeType && typeof edgeType === "string") {
|
this.file?.path
|
||||||
// Use [[rel:type|link]] format
|
);
|
||||||
linkText = `[[rel:${edgeType}|${result.basename}]]`;
|
const pick = await picker.show();
|
||||||
}
|
if (pick) linkTarget = pick.linkTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let linkText = `[[${linkTarget}]]`;
|
||||||
|
if (shouldRunInlineMicro && currentStep) {
|
||||||
|
const edgeType = await this.handleInlineMicroEdging(currentStep, linkTarget, result.path);
|
||||||
|
if (edgeType && typeof edgeType === "string") {
|
||||||
|
linkText = `[[rel:${edgeType}|${linkTarget}]]`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert link with rel: prefix if edge type was selected
|
|
||||||
// Extract inner part (without [[ and ]])
|
|
||||||
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
||||||
insertWikilinkIntoTextarea(textarea, innerLink);
|
insertWikilinkIntoTextarea(textarea, innerLink);
|
||||||
}
|
}
|
||||||
|
|
@ -633,7 +639,6 @@ export class InterviewWizardModal extends Modal {
|
||||||
|
|
||||||
blockIdModal.open();
|
blockIdModal.open();
|
||||||
} else {
|
} else {
|
||||||
// Keine Block-IDs vorhanden, öffne direkt Entity-Picker
|
|
||||||
if (!this.noteIndex) {
|
if (!this.noteIndex) {
|
||||||
new Notice("Note index not available");
|
new Notice("Note index not available");
|
||||||
return;
|
return;
|
||||||
|
|
@ -642,30 +647,35 @@ export class InterviewWizardModal extends Modal {
|
||||||
app,
|
app,
|
||||||
this.noteIndex,
|
this.noteIndex,
|
||||||
async (result: EntityPickerResult) => {
|
async (result: EntityPickerResult) => {
|
||||||
// Check if inline micro edging is enabled (also for toolbar)
|
|
||||||
// Support: inline_micro, both (inline_micro + post_run)
|
|
||||||
const edgingMode = this.profile.edging?.mode;
|
const edgingMode = this.profile.edging?.mode;
|
||||||
const shouldRunInlineMicro =
|
const shouldRunInlineMicro =
|
||||||
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
||||||
this.settings?.inlineMicroEnabled !== false;
|
this.settings?.inlineMicroEnabled !== false;
|
||||||
|
const currentStep = getCurrentStep(this.state);
|
||||||
|
|
||||||
let linkText = `[[${result.basename}]]`;
|
let linkTarget = result.basename;
|
||||||
|
if (shouldRunInlineMicro && currentStep) {
|
||||||
if (shouldRunInlineMicro) {
|
const file = this.app.vault.getAbstractFileByPath(result.path);
|
||||||
// Get current step for section key resolution
|
if (file && file instanceof TFile) {
|
||||||
const currentStep = getCurrentStep(this.state);
|
const picker = new LinkTargetPickerModal(
|
||||||
if (currentStep) {
|
this.app,
|
||||||
console.log("[Mindnet] Starting inline micro edging from toolbar");
|
file,
|
||||||
const edgeType = await this.handleInlineMicroEdging(currentStep, result.basename, result.path);
|
result.basename,
|
||||||
if (edgeType && typeof edgeType === "string") {
|
this.file?.path
|
||||||
// Use [[rel:type|link]] format
|
);
|
||||||
linkText = `[[rel:${edgeType}|${result.basename}]]`;
|
const pick = await picker.show();
|
||||||
}
|
if (pick) linkTarget = pick.linkTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let linkText = `[[${linkTarget}]]`;
|
||||||
|
if (shouldRunInlineMicro && currentStep) {
|
||||||
|
const edgeType = await this.handleInlineMicroEdging(currentStep, linkTarget, result.path);
|
||||||
|
if (edgeType && typeof edgeType === "string") {
|
||||||
|
linkText = `[[rel:${edgeType}|${linkTarget}]]`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert link with rel: prefix if edge type was selected
|
|
||||||
// Extract inner part (without [[ and ]])
|
|
||||||
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
||||||
insertWikilinkIntoTextarea(textarea, innerLink);
|
insertWikilinkIntoTextarea(textarea, innerLink);
|
||||||
}
|
}
|
||||||
|
|
@ -2593,7 +2603,8 @@ export class InterviewWizardModal extends Modal {
|
||||||
this.state.sectionSequence,
|
this.state.sectionSequence,
|
||||||
this.vocabulary,
|
this.vocabulary,
|
||||||
graphSchema,
|
graphSchema,
|
||||||
this.state.collectedData // Übergebe gesammelte Daten für Note-Link-Extraktion
|
this.state.collectedData,
|
||||||
|
this.file?.path // Quelldatei für Link-Auflösung (Zieltyp ermitteln)
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await overviewModal.show();
|
const result = await overviewModal.show();
|
||||||
|
|
@ -2906,27 +2917,26 @@ export class InterviewWizardModal extends Modal {
|
||||||
sourceNoteId = extractFrontmatterId(sourceContent) || undefined;
|
sourceNoteId = extractFrontmatterId(sourceContent) || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get target note ID and type
|
// Zieltyp ermitteln: Note-Type (ganze Note) oder Sektionstyp (bei Note#Abschnitt)
|
||||||
try {
|
try {
|
||||||
|
const { resolveTargetTypeForNoteLink } = await import("../interview/targetTypeResolver");
|
||||||
|
const resolved = await resolveTargetTypeForNoteLink(
|
||||||
|
this.app,
|
||||||
|
linkBasename,
|
||||||
|
this.file?.path || ""
|
||||||
|
);
|
||||||
|
targetType = resolved.targetType ?? undefined;
|
||||||
const targetFile = this.app.vault.getAbstractFileByPath(linkPath);
|
const targetFile = this.app.vault.getAbstractFileByPath(linkPath);
|
||||||
if (targetFile && targetFile instanceof TFile) {
|
if (targetFile && targetFile instanceof TFile) {
|
||||||
const targetContent = await this.app.vault.read(targetFile);
|
const targetContent = await this.app.vault.read(targetFile);
|
||||||
targetNoteId = extractFrontmatterId(targetContent) || undefined;
|
targetNoteId = extractFrontmatterId(targetContent) || undefined;
|
||||||
const targetFrontmatter = targetContent.match(/^---\n([\s\S]*?)\n---/);
|
|
||||||
if (targetFrontmatter && targetFrontmatter[1]) {
|
|
||||||
const typeMatch = targetFrontmatter[1].match(/^type:\s*(.+)$/m);
|
|
||||||
if (typeMatch && typeMatch[1]) {
|
|
||||||
targetType = typeMatch[1].trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Target note might not exist yet, that's OK
|
console.debug("[Mindnet] Could not resolve target type for inline micro:", e);
|
||||||
console.debug("[Mindnet] Could not read target note for inline micro:", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show inline edge type modal
|
// Show inline edge type modal (linkBasename kann "Note" oder "Note#Abschnitt" sein)
|
||||||
const modal = new InlineEdgeTypeModal(
|
const modal = new InlineEdgeTypeModal(
|
||||||
this.app,
|
this.app,
|
||||||
linkBasename,
|
linkBasename,
|
||||||
|
|
|
||||||
139
src/ui/LinkTargetPickerModal.ts
Normal file
139
src/ui/LinkTargetPickerModal.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
/**
|
||||||
|
* Modal zur Auswahl des Link-Ziels: Ganze Note oder ein Abschnitt (Überschrift inkl. Sektionstyp).
|
||||||
|
* Wird in der Interview-Toolbar nach der Note-Auswahl angezeigt.
|
||||||
|
* Speichert Link als [[Note]] oder [[Note#Abschnitt]].
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { App, Modal, TFile } from "obsidian";
|
||||||
|
import {
|
||||||
|
getHeadingsWithSectionTypes,
|
||||||
|
resolveTargetTypeForNoteLink,
|
||||||
|
type HeadingWithType,
|
||||||
|
} from "../interview/targetTypeResolver";
|
||||||
|
|
||||||
|
export interface LinkTargetPick {
|
||||||
|
linkTarget: string; // "Note" oder "Note#Abschnitt"
|
||||||
|
targetType: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LinkTargetPickerModal extends Modal {
|
||||||
|
private file: TFile;
|
||||||
|
private basename: string;
|
||||||
|
private sourceFilePath: string;
|
||||||
|
private resolve: ((pick: LinkTargetPick | null) => void) | null = null;
|
||||||
|
|
||||||
|
constructor(app: App, file: TFile, basename: string, sourceFilePath?: string) {
|
||||||
|
super(app);
|
||||||
|
this.file = file;
|
||||||
|
this.basename = basename;
|
||||||
|
this.sourceFilePath = sourceFilePath || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async onOpen(): Promise<void> {
|
||||||
|
const { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
contentEl.addClass("link-target-picker-modal");
|
||||||
|
|
||||||
|
contentEl.createEl("h2", { text: "Link-Ziel wählen" });
|
||||||
|
contentEl.createEl("p", {
|
||||||
|
text: `Zielnote: ${this.basename}. Ganze Note oder einen Abschnitt auswählen.`,
|
||||||
|
cls: "link-target-picker-description",
|
||||||
|
});
|
||||||
|
|
||||||
|
let noteType: string | null = null;
|
||||||
|
try {
|
||||||
|
const resolved = await resolveTargetTypeForNoteLink(
|
||||||
|
this.app,
|
||||||
|
this.basename,
|
||||||
|
this.sourceFilePath
|
||||||
|
);
|
||||||
|
noteType = resolved.targetType;
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = contentEl.createEl("div", { cls: "link-target-picker-list" });
|
||||||
|
|
||||||
|
// Option: Ganze Note
|
||||||
|
const wholeNoteBtn = list.createEl("button", { cls: "link-target-option" });
|
||||||
|
wholeNoteBtn.style.display = "block";
|
||||||
|
wholeNoteBtn.style.width = "100%";
|
||||||
|
wholeNoteBtn.style.textAlign = "left";
|
||||||
|
wholeNoteBtn.style.padding = "0.5em 0.75em";
|
||||||
|
wholeNoteBtn.style.marginBottom = "0.25em";
|
||||||
|
wholeNoteBtn.style.cursor = "pointer";
|
||||||
|
wholeNoteBtn.style.border = "1px solid var(--background-modifier-border)";
|
||||||
|
wholeNoteBtn.style.borderRadius = "4px";
|
||||||
|
const wholeLabel = wholeNoteBtn.createEl("span", { text: "Ganze Note" });
|
||||||
|
wholeLabel.style.fontWeight = "bold";
|
||||||
|
if (noteType) {
|
||||||
|
wholeNoteBtn.createEl("span", { text: ` (Typ: ${noteType})`, cls: "target-type" }).style.color =
|
||||||
|
"var(--text-muted)";
|
||||||
|
}
|
||||||
|
wholeNoteBtn.onclick = () => {
|
||||||
|
if (this.resolve) {
|
||||||
|
this.resolve({ linkTarget: this.basename, targetType: noteType });
|
||||||
|
this.resolve = null;
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
let headings: HeadingWithType[] = [];
|
||||||
|
try {
|
||||||
|
headings = await getHeadingsWithSectionTypes(this.app, this.file);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const h of headings) {
|
||||||
|
const headingBtn = list.createEl("button", { cls: "link-target-option" });
|
||||||
|
headingBtn.style.display = "block";
|
||||||
|
headingBtn.style.width = "100%";
|
||||||
|
headingBtn.style.textAlign = "left";
|
||||||
|
headingBtn.style.padding = "0.5em 0.75em";
|
||||||
|
headingBtn.style.marginBottom = "0.25em";
|
||||||
|
headingBtn.style.cursor = "pointer";
|
||||||
|
headingBtn.style.border = "1px solid var(--background-modifier-border)";
|
||||||
|
headingBtn.style.borderRadius = "4px";
|
||||||
|
const prefix = "#".repeat(h.level) + " ";
|
||||||
|
headingBtn.createEl("span", { text: prefix + h.heading });
|
||||||
|
if (h.sectionType) {
|
||||||
|
headingBtn
|
||||||
|
.createEl("span", { text: ` (Sektion: ${h.sectionType})`, cls: "target-type" })
|
||||||
|
.style.color = "var(--text-muted)";
|
||||||
|
}
|
||||||
|
const linkTarget = `${this.basename}#${h.heading}`;
|
||||||
|
headingBtn.onclick = () => {
|
||||||
|
if (this.resolve) {
|
||||||
|
this.resolve({ linkTarget, targetType: h.sectionType });
|
||||||
|
this.resolve = null;
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelBtn = contentEl.createEl("button", { text: "Abbrechen", cls: "mod-secondary" });
|
||||||
|
cancelBtn.style.marginTop = "1em";
|
||||||
|
cancelBtn.onclick = () => {
|
||||||
|
if (this.resolve) {
|
||||||
|
this.resolve(null);
|
||||||
|
this.resolve = null;
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
show(): Promise<LinkTargetPick | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.resolve = resolve;
|
||||||
|
this.open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose(): void {
|
||||||
|
if (this.resolve) {
|
||||||
|
this.resolve(null);
|
||||||
|
this.resolve = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import type { EdgeVocabulary } from "../vocab/types";
|
||||||
import type { GraphSchema } from "../mapping/graphSchema";
|
import type { GraphSchema } from "../mapping/graphSchema";
|
||||||
import { EdgeTypeChooserModal, type EdgeTypeChoice } from "./EdgeTypeChooserModal";
|
import { EdgeTypeChooserModal, type EdgeTypeChoice } from "./EdgeTypeChooserModal";
|
||||||
import { getHints } from "../mapping/graphSchema";
|
import { getHints } from "../mapping/graphSchema";
|
||||||
|
import { resolveTargetTypeForNoteLink } from "../interview/targetTypeResolver";
|
||||||
|
|
||||||
export interface SectionEdge {
|
export interface SectionEdge {
|
||||||
fromSection: SectionInfo;
|
fromSection: SectionInfo;
|
||||||
|
|
@ -20,10 +21,10 @@ export interface SectionEdge {
|
||||||
|
|
||||||
export interface NoteEdge {
|
export interface NoteEdge {
|
||||||
fromSection: SectionInfo | null; // null = Note-Level
|
fromSection: SectionInfo | null; // null = Note-Level
|
||||||
toNote: string; // Note-Basename
|
toNote: string; // Vollständiger Link: Note oder Note#Abschnitt (für Anzeige und Kantenzuordnung)
|
||||||
edgeType: string; // Aktueller Edge-Type (kann geändert werden)
|
edgeType: string; // Aktueller Edge-Type (kann geändert werden)
|
||||||
suggestedType: string | null; // Vorgeschlagener Edge-Type aus graph_schema
|
suggestedType: string | null; // Vorgeschlagener Edge-Type aus graph_schema
|
||||||
targetType: string | null; // Target-Note-Type (aus Frontmatter)
|
targetType: string | null; // Zieltyp: Note-Type (ganze Note) oder Sektionstyp (bei Note#Abschnitt)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SectionEdgesOverviewResult {
|
export interface SectionEdgesOverviewResult {
|
||||||
|
|
@ -37,6 +38,7 @@ export class SectionEdgesOverviewModal extends Modal {
|
||||||
private vocabulary: EdgeVocabulary | null;
|
private vocabulary: EdgeVocabulary | null;
|
||||||
private graphSchema: GraphSchema | null;
|
private graphSchema: GraphSchema | null;
|
||||||
private collectedData: Map<string, unknown>; // Gesammelte Daten aus dem Interview
|
private collectedData: Map<string, unknown>; // Gesammelte Daten aus dem Interview
|
||||||
|
private activeFilePath: string; // Quelldatei für Link-Auflösung (getFirstLinkpathDest)
|
||||||
private sectionEdges: SectionEdge[] = [];
|
private sectionEdges: SectionEdge[] = [];
|
||||||
private noteEdges: NoteEdge[] = [];
|
private noteEdges: NoteEdge[] = [];
|
||||||
private result: SectionEdgesOverviewResult | null = null;
|
private result: SectionEdgesOverviewResult | null = null;
|
||||||
|
|
@ -47,13 +49,15 @@ export class SectionEdgesOverviewModal extends Modal {
|
||||||
sectionSequence: SectionInfo[],
|
sectionSequence: SectionInfo[],
|
||||||
vocabulary: EdgeVocabulary | null,
|
vocabulary: EdgeVocabulary | null,
|
||||||
graphSchema: GraphSchema | null,
|
graphSchema: GraphSchema | null,
|
||||||
collectedData?: Map<string, unknown>
|
collectedData?: Map<string, unknown>,
|
||||||
|
activeFilePath?: string
|
||||||
) {
|
) {
|
||||||
super(app);
|
super(app);
|
||||||
this.sectionSequence = sectionSequence;
|
this.sectionSequence = sectionSequence;
|
||||||
this.vocabulary = vocabulary;
|
this.vocabulary = vocabulary;
|
||||||
this.graphSchema = graphSchema;
|
this.graphSchema = graphSchema;
|
||||||
this.collectedData = collectedData || new Map();
|
this.collectedData = collectedData || new Map();
|
||||||
|
this.activeFilePath = activeFilePath || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async onOpen(): Promise<void> {
|
async onOpen(): Promise<void> {
|
||||||
|
|
@ -468,13 +472,12 @@ export class SectionEdgesOverviewModal extends Modal {
|
||||||
private async buildNoteEdgeList(): Promise<void> {
|
private async buildNoteEdgeList(): Promise<void> {
|
||||||
this.noteEdges = [];
|
this.noteEdges = [];
|
||||||
|
|
||||||
// Extrahiere alle Note-Links aus gesammelten Daten
|
// Extrahiere alle Note-Links aus gesammelten Daten (vollständiger Link: Note oder Note#Abschnitt)
|
||||||
const noteLinksBySection = new Map<string | null, Set<string>>(); // blockId -> Set<noteBasename>
|
const noteLinksBySection = new Map<string | null, Set<string>>(); // blockId -> Set<fullLink>
|
||||||
|
|
||||||
for (const [key, value] of this.collectedData.entries()) {
|
for (const [key, value] of this.collectedData.entries()) {
|
||||||
if (!value || typeof value !== "string") continue;
|
if (!value || typeof value !== "string") continue;
|
||||||
|
|
||||||
// Finde zugehörige Section für diesen Step
|
|
||||||
let sectionInfo: SectionInfo | null = null;
|
let sectionInfo: SectionInfo | null = null;
|
||||||
for (const section of this.sectionSequence) {
|
for (const section of this.sectionSequence) {
|
||||||
if (section.stepKey === key) {
|
if (section.stepKey === key) {
|
||||||
|
|
@ -483,63 +486,46 @@ export class SectionEdgesOverviewModal extends Modal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extrahiere Wikilinks aus dem Text
|
|
||||||
const wikilinkRegex = /\[\[([^\]]+?)\]\]/g;
|
const wikilinkRegex = /\[\[([^\]]+?)\]\]/g;
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
while ((match = wikilinkRegex.exec(value)) !== null) {
|
while ((match = wikilinkRegex.exec(value)) !== null) {
|
||||||
if (match[1]) {
|
if (match[1]) {
|
||||||
const linkTarget = match[1].trim();
|
const linkTarget = match[1].trim();
|
||||||
if (!linkTarget) continue;
|
if (!linkTarget) continue;
|
||||||
|
if (linkTarget.startsWith("#^")) continue; // Block-ID-Links sind Section-Edges
|
||||||
// Prüfe, ob es ein Block-ID-Link ist ([[#^...]])
|
// Ziel-Link: Bei [[rel:type|Note#Abschnitt]] den Teil nach dem ersten | nehmen, sonst ganzer Link
|
||||||
if (linkTarget.startsWith("#^")) {
|
const fullLink = linkTarget.includes("|")
|
||||||
continue; // Block-ID-Links sind Section-Edges, keine Note-Edges
|
? linkTarget.split("|").slice(1).join("|").trim() || linkTarget.trim()
|
||||||
}
|
: linkTarget.split("|")[0]?.trim() || linkTarget;
|
||||||
|
if (!fullLink || fullLink.startsWith("rel:")) continue; // rel: allein ist kein gültiger Ziel-Link
|
||||||
// Normalisiere Link (entferne Alias und Heading)
|
|
||||||
const normalized = linkTarget.split("|")[0]?.split("#")[0]?.trim() || linkTarget;
|
|
||||||
if (!normalized) continue;
|
|
||||||
|
|
||||||
const blockId = sectionInfo?.blockId || null;
|
const blockId = sectionInfo?.blockId || null;
|
||||||
if (!noteLinksBySection.has(blockId)) {
|
if (!noteLinksBySection.has(blockId)) {
|
||||||
noteLinksBySection.set(blockId, new Set());
|
noteLinksBySection.set(blockId, new Set());
|
||||||
}
|
}
|
||||||
noteLinksBySection.get(blockId)!.add(normalized);
|
noteLinksBySection.get(blockId)!.add(fullLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erstelle NoteEdge-Objekte
|
|
||||||
for (const [blockId, noteLinks] of noteLinksBySection.entries()) {
|
for (const [blockId, noteLinks] of noteLinksBySection.entries()) {
|
||||||
const sectionInfo = blockId
|
const sectionInfo = blockId
|
||||||
? this.sectionSequence.find(s => s.blockId === blockId) || null
|
? this.sectionSequence.find(s => s.blockId === blockId) || null
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// WP-26: Verwende effektiven Section-Type (mit Heading-Level-basierter Fallback-Logik)
|
|
||||||
const sectionIndex = blockId ? this.sectionSequence.findIndex(s => s.blockId === blockId) : -1;
|
const sectionIndex = blockId ? this.sectionSequence.findIndex(s => s.blockId === blockId) : -1;
|
||||||
const sourceType = sectionInfo && sectionIndex >= 0
|
const sourceType = sectionInfo && sectionIndex >= 0
|
||||||
? this.getEffectiveSectionType(sectionInfo, sectionIndex)
|
? this.getEffectiveSectionType(sectionInfo, sectionIndex)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
for (const noteBasename of noteLinks) {
|
for (const fullLink of noteLinks) {
|
||||||
// Ermittle Target-Note-Type
|
const resolved = await resolveTargetTypeForNoteLink(
|
||||||
let targetType: string | null = null;
|
this.app,
|
||||||
try {
|
fullLink,
|
||||||
const targetFile = this.app.metadataCache.getFirstLinkpathDest(noteBasename, "");
|
this.activeFilePath
|
||||||
if (targetFile) {
|
);
|
||||||
const cache = this.app.metadataCache.getFileCache(targetFile);
|
const targetType = resolved.targetType;
|
||||||
if (cache?.frontmatter) {
|
|
||||||
targetType = cache.frontmatter.type || cache.frontmatter.noteType || null;
|
|
||||||
if (targetType && typeof targetType !== "string") {
|
|
||||||
targetType = String(targetType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ermittle vorgeschlagenen Edge-Type aus graph_schema
|
|
||||||
let suggestedType: string | null = null;
|
let suggestedType: string | null = null;
|
||||||
if (this.graphSchema && sourceType && targetType) {
|
if (this.graphSchema && sourceType && targetType) {
|
||||||
const hints = getHints(this.graphSchema, sourceType, targetType);
|
const hints = getHints(this.graphSchema, sourceType, targetType);
|
||||||
|
|
@ -547,15 +533,13 @@ export class SectionEdgesOverviewModal extends Modal {
|
||||||
suggestedType = hints.typical[0] || null;
|
suggestedType = hints.typical[0] || null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: references
|
|
||||||
if (!suggestedType) {
|
if (!suggestedType) {
|
||||||
suggestedType = "references";
|
suggestedType = "references";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.noteEdges.push({
|
this.noteEdges.push({
|
||||||
fromSection: sectionInfo,
|
fromSection: sectionInfo,
|
||||||
toNote: noteBasename,
|
toNote: resolved.displayLabel,
|
||||||
edgeType: suggestedType,
|
edgeType: suggestedType,
|
||||||
suggestedType: suggestedType,
|
suggestedType: suggestedType,
|
||||||
targetType: targetType,
|
targetType: targetType,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user