mindnet_obsidian/src/commands/fixFindingsCommand.ts
Lars 725adb5302
Some checks are pending
Node.js build / build (20.x) (push) Waiting to run
Node.js build / build (22.x) (push) Waiting to run
Enhance UI and functionality for Chain Workbench and related features
- Introduced a wide two-column layout for the Chain Workbench modal, improving user experience and accessibility.
- Added new styles for workbench components, including headers, filters, and main containers, to enhance visual organization.
- Updated chain templates to allow for multiple distinct matches per template, improving flexibility in template matching.
- Enhanced documentation to clarify the new settings and commands related to the Chain Workbench and edge detection features.
- Implemented logging for better tracking of missing configurations, ensuring users are informed about any loading issues.
2026-02-05 11:41:15 +01:00

943 lines
28 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Command: Fix Findings (Current Section)
*/
import type { App, Editor } from "obsidian";
import { Notice, Modal, Setting } from "obsidian";
import { resolveSectionContext } from "../analysis/sectionContext";
import { inspectChains } from "../analysis/chainInspector";
import type { Finding, InspectorOptions } from "../analysis/chainInspector";
import type { ChainRolesConfig } from "../dictionary/types";
import type { MindnetSettings } from "../settings";
import { EntityPickerModal } from "../ui/EntityPickerModal";
import { ProfileSelectionModal } from "../ui/ProfileSelectionModal";
import type { InterviewConfig } from "../interview/types";
import { writeFrontmatter } from "../interview/writeFrontmatter";
import { joinFolderAndBasename, ensureUniqueFilePath, ensureFolderExists } from "../mapping/folderHelpers";
import { normalizeLinkTarget } from "../unresolvedLink/linkHelpers";
import { startWizardAfterCreate } from "../unresolvedLink/unresolvedLinkHandler";
/**
* Execute fix findings command.
*/
export async function executeFixFindings(
app: App,
editor: Editor,
filePath: string,
chainRoles: ChainRolesConfig | null,
interviewConfig: InterviewConfig | null,
settings: MindnetSettings,
pluginInstance?: any
): Promise<void> {
try {
// Resolve section context
const context = resolveSectionContext(editor, filePath);
// Inspect chains to get findings
const inspectorOptions: InspectorOptions = {
includeNoteLinks: true,
includeCandidates: false,
maxDepth: 3,
direction: "both",
maxMatchesPerTemplateDefault: settings.maxMatchesPerTemplateDefault,
debugLogging: settings.debugLogging,
};
const report = await inspectChains(
app,
context,
inspectorOptions,
chainRoles,
settings.edgeVocabularyPath
);
// Filter findings that have fix actions
const fixableFindings = report.findings.filter(
(f) =>
f.code === "dangling_target" ||
f.code === "dangling_target_heading" ||
f.code === "only_candidates"
);
console.log("[Fix Findings] Found", fixableFindings.length, "fixable findings:", fixableFindings.map(f => f.code));
if (fixableFindings.length === 0) {
new Notice("No fixable findings found in current section");
return;
}
// Let user select a finding
const selectedFinding = await selectFinding(app, fixableFindings);
if (!selectedFinding) {
console.log("[Fix Findings] No finding selected, aborting");
return; // User cancelled
}
// Let user select an action
const action = await selectAction(app, selectedFinding);
if (!action) {
console.log("[Fix Findings] No action selected, aborting");
return; // User cancelled
}
console.log("[Fix Findings] Applying action", action, "for finding", selectedFinding.code);
// Apply action
await applyFixAction(
app,
editor,
context,
selectedFinding,
action,
report,
interviewConfig,
settings,
pluginInstance
);
new Notice(`Fix action "${action}" applied successfully`);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
new Notice(`Failed to fix findings: ${msg}`);
console.error(e);
}
}
/**
* Let user select a finding.
*/
function selectFinding(
app: App,
findings: Finding[]
): Promise<Finding | null> {
return new Promise((resolve) => {
console.log("[Fix Findings] Showing finding selection modal with", findings.length, "findings");
let resolved = false;
const modal = new FindingSelectionModal(app, findings, (finding) => {
console.log("[Fix Findings] Finding selected:", finding.code);
if (!resolved) {
resolved = true;
modal.close();
resolve(finding);
}
});
modal.onClose = () => {
if (!resolved) {
console.log("[Fix Findings] Finding selection cancelled");
resolved = true;
resolve(null);
}
};
modal.open();
});
}
/**
* Let user select an action for a finding.
*/
function selectAction(
app: App,
finding: Finding
): Promise<string | null> {
return new Promise((resolve) => {
const actions = getAvailableActions(finding);
console.log("[Fix Findings] Showing action selection modal for", finding.code, "with actions:", actions);
let resolved = false;
const modal = new ActionSelectionModal(app, finding, actions, (action) => {
console.log("[Fix Findings] Action selected:", action);
if (!resolved) {
resolved = true;
modal.close();
resolve(action);
}
});
modal.onClose = () => {
if (!resolved) {
console.log("[Fix Findings] Action selection cancelled");
resolved = true;
resolve(null);
}
};
modal.open();
});
}
/**
* Get available actions for a finding.
*/
function getAvailableActions(finding: Finding): string[] {
switch (finding.code) {
case "dangling_target":
return ["create_missing_note", "retarget_link"];
case "dangling_target_heading":
return ["create_missing_heading", "retarget_to_heading"];
case "only_candidates":
return ["promote_candidate"];
default:
return [];
}
}
/**
* Apply fix action.
*/
async function applyFixAction(
app: App,
editor: Editor,
context: any,
finding: Finding,
action: string,
report: any,
interviewConfig: InterviewConfig | null,
settings: MindnetSettings,
pluginInstance?: any
): Promise<void> {
console.log(`[Fix Findings] Applying action ${action} for finding ${finding.code}`);
try {
switch (action) {
case "create_missing_note":
await createMissingNote(
app,
finding,
report,
interviewConfig,
settings,
pluginInstance
);
break;
case "retarget_link":
console.log(`[Fix Findings] Calling retargetLink...`);
await retargetLink(app, editor, finding, report);
console.log(`[Fix Findings] retargetLink completed`);
break;
case "create_missing_heading":
await createMissingHeading(app, finding, report, settings);
break;
case "retarget_to_heading":
await retargetToHeading(app, editor, finding, report);
break;
case "promote_candidate":
await promoteCandidate(app, editor, context, finding, report, settings);
break;
default:
throw new Error(`Unknown action: ${action}`);
}
} catch (e) {
console.error(`[Fix Findings] Error applying action ${action}:`, e);
throw e;
}
}
/**
* Create missing note.
*/
async function createMissingNote(
app: App,
finding: Finding,
report: any,
interviewConfig: InterviewConfig | null,
settings: MindnetSettings,
pluginInstance?: any
): Promise<void> {
// Extract target file from finding message
const message = finding.message;
const targetMatch = message.match(/Target file does not exist: (.+)/);
if (!targetMatch || !targetMatch[1]) {
throw new Error("Could not extract target file from finding");
}
const targetFile = targetMatch[1];
const mode = settings.fixActions.createMissingNote.mode;
const defaultTypeStrategy = settings.fixActions.createMissingNote.defaultTypeStrategy;
const includeZones = settings.fixActions.createMissingNote.includeZones;
if (mode === "skeleton_only") {
// Create skeleton note
const id = `note_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
const title = targetFile.replace(/\.md$/, "");
let noteType = "concept"; // Default fallback
if (defaultTypeStrategy === "default_concept_no_prompt") {
// Use default from types.yaml or "concept"
noteType = "concept"; // TODO: Load from types.yaml if available
} else if (defaultTypeStrategy === "profile_picker" || defaultTypeStrategy === "inference_then_picker") {
// Will be handled by profile picker
noteType = "concept"; // Placeholder, will be overwritten
}
// Build frontmatter
const frontmatterLines = [
"---",
`id: ${id}`,
`title: ${JSON.stringify(title)}`,
`type: ${noteType}`,
`status: draft`,
`created: ${new Date().toISOString().split("T")[0]}`,
"---",
];
// Build content with optional zones
let content = frontmatterLines.join("\n") + "\n\n";
if (includeZones === "note_links_only" || includeZones === "both") {
content += "## Note-Verbindungen\n\n";
}
if (includeZones === "candidates_only" || includeZones === "both") {
content += "## Kandidaten\n\n";
}
// Determine folder path
const folderPath = settings.defaultNotesFolder || "";
if (folderPath) {
await ensureFolderExists(app, folderPath);
}
// Build file path
const fileName = `${title}.md`;
const desiredPath = joinFolderAndBasename(folderPath, fileName);
const filePath = await ensureUniqueFilePath(app, desiredPath);
// Create file
await app.vault.create(filePath, content);
new Notice(`Created skeleton note: ${title}`);
} else if (mode === "create_and_open_profile_picker" || mode === "create_and_start_wizard") {
// Use profile picker
if (!interviewConfig) {
throw new Error("Interview config required for profile picker mode");
}
const title = targetFile.replace(/\.md$/, "");
const folderPath = settings.defaultNotesFolder || "";
await new Promise<void>((resolve, reject) => {
new ProfileSelectionModal(
app,
interviewConfig,
async (result) => {
try {
// Generate ID
const id = `note_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
// Write frontmatter
const frontmatter = writeFrontmatter({
id,
title: result.title,
noteType: result.profile.note_type,
interviewProfile: result.profile.key,
defaults: result.profile.defaults,
frontmatterWhitelist: interviewConfig.frontmatterWhitelist,
});
// Build content
let content = `${frontmatter}\n\n`;
if (includeZones === "note_links_only" || includeZones === "both") {
content += "## Note-Verbindungen\n\n";
}
if (includeZones === "candidates_only" || includeZones === "both") {
content += "## Kandidaten\n\n";
}
// Ensure folder exists
const finalFolderPath = result.folderPath || folderPath;
if (finalFolderPath) {
await ensureFolderExists(app, finalFolderPath);
}
// Build file path
const fileName = `${result.title}.md`;
const desiredPath = joinFolderAndBasename(finalFolderPath, fileName);
const filePath = await ensureUniqueFilePath(app, desiredPath);
// Create file
const file = await app.vault.create(filePath, content);
// Open file
await app.workspace.openLinkText(filePath, "", true);
// Start wizard if requested
if (mode === "create_and_start_wizard" && pluginInstance) {
await startWizardAfterCreate(
app,
settings,
file,
result.profile,
content,
false,
async () => {
new Notice("Wizard completed");
},
async () => {
new Notice("Wizard saved");
},
pluginInstance
);
}
resolve();
} catch (e) {
reject(e);
}
},
title,
folderPath
).open();
});
}
}
/**
* Retarget link to existing note.
*/
async function retargetLink(
app: App,
editor: Editor,
finding: Finding,
report: any
): Promise<void> {
console.log(`[Fix Findings] retargetLink called`);
// Extract target file from finding message
const message = finding.message;
console.log(`[Fix Findings] Finding message: "${message}"`);
const targetMatch = message.match(/Target file does not exist: (.+)/);
if (!targetMatch || !targetMatch[1]) {
throw new Error("Could not extract target file from finding");
}
const oldTarget = targetMatch[1];
console.log(`[Fix Findings] Extracted oldTarget: "${oldTarget}"`);
// Find the edge in the report that matches this finding
const edge = findEdgeForFinding(report, finding);
if (!edge) {
console.error(`[Fix Findings] Could not find edge for finding:`, finding);
console.error(`[Fix Findings] Report neighbors.outgoing:`, report.neighbors?.outgoing);
throw new Error("Could not find edge for finding");
}
console.log(`[Fix Findings] Found edge:`, edge);
// Show note picker
console.log(`[Fix Findings] Opening EntityPickerModal...`);
const { NoteIndex } = await import("../entityPicker/noteIndex");
const noteIndex = new NoteIndex(app);
let resolved = false;
const selectedNote = await new Promise<{ basename: string; path: string } | null>(
(resolve) => {
const modal = new EntityPickerModal(
app,
noteIndex,
(result) => {
console.log(`[Fix Findings] EntityPickerModal selected:`, result);
if (!resolved) {
resolved = true;
modal.close();
resolve(result);
}
}
);
modal.onClose = () => {
if (!resolved) {
console.log(`[Fix Findings] EntityPickerModal cancelled`);
resolved = true;
resolve(null);
}
};
modal.open();
}
);
console.log(`[Fix Findings] Selected note:`, selectedNote);
if (!selectedNote) {
return; // User cancelled
}
// Find and replace ALL occurrences of the link in the current section
const content = editor.getValue();
const lines = content.split(/\r?\n/);
const newTarget = selectedNote.basename.replace(/\.md$/, "");
const heading = edge.target.heading ? `#${edge.target.heading}` : "";
const newLink = `[[${newTarget}${heading}]]`;
// Escape oldTarget for regex (but keep it flexible for matching)
const escapedOldTarget = oldTarget.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
console.log(`[Fix Findings] Retargeting link:`);
console.log(` - oldTarget: "${oldTarget}"`);
console.log(` - newTarget: "${newTarget}"`);
console.log(` - newLink: "${newLink}"`);
console.log(` - report.context:`, report.context);
// Build regex pattern that matches:
// - [[oldTarget]]
// - [[oldTarget#heading]]
// - [[oldTarget|alias]]
// - [[oldTarget#heading|alias]]
const linkPattern = new RegExp(
`(\\[\\[)${escapedOldTarget}(#[^\\]|]+)?(\\|[^\\]]+)?(\\]\\])`,
"g"
);
// Find the current section boundaries
const { splitIntoSections } = await import("../mapping/sectionParser");
const sections = splitIntoSections(content);
const context = report.context;
// Find section that matches the context
let sectionStartLine = 0;
let sectionEndLine = lines.length;
if (context.heading !== null) {
// Find section with matching heading
for (const section of sections) {
if (section.heading === context.heading) {
sectionStartLine = section.startLine;
sectionEndLine = section.endLine;
console.log(`[Fix Findings] Found section "${context.heading}" at lines ${sectionStartLine}-${sectionEndLine}`);
break;
}
}
} else {
// Root section (before first heading)
if (sections.length > 0 && sections[0]) {
sectionEndLine = sections[0].startLine;
}
console.log(`[Fix Findings] Using root section (lines 0-${sectionEndLine})`);
}
// Replace ALL occurrences in the current section
let replacedCount = 0;
for (let i = sectionStartLine; i < sectionEndLine && i < lines.length; i++) {
const line = lines[i];
if (!line) continue;
// Check if line contains the old target
if (line.includes(oldTarget) || linkPattern.test(line)) {
// Reset regex lastIndex for global match
linkPattern.lastIndex = 0;
const newLine = line.replace(linkPattern, `$1${newTarget}$2$3$4`);
if (newLine !== line) {
lines[i] = newLine;
replacedCount++;
console.log(`[Fix Findings] ✓ Replaced link in line ${i}:`);
console.log(` Before: "${line.trim()}"`);
console.log(` After: "${newLine.trim()}"`);
}
}
}
if (replacedCount === 0) {
console.warn(`[Fix Findings] No links found in section, trying full content search`);
// Fallback: search entire content
linkPattern.lastIndex = 0;
const newContent = content.replace(linkPattern, `$1${newTarget}$2$3$4`);
if (newContent !== content) {
editor.setValue(newContent);
console.log(`[Fix Findings] ✓ Replaced link in full content`);
return;
} else {
console.error(`[Fix Findings] ✗ Link not found even in full content search`);
throw new Error(`Could not find link "${oldTarget}" in content to replace`);
}
}
// Write back modified lines
const newContent = lines.join("\n");
editor.setValue(newContent);
console.log(`[Fix Findings] ✓ Link retargeted successfully (${replacedCount} occurrence(s) replaced)`);
}
/**
* Create missing heading in target note.
*/
async function createMissingHeading(
app: App,
finding: Finding,
report: any,
settings: MindnetSettings
): Promise<void> {
// Extract target file and heading from finding message
const message = finding.message;
const match = message.match(/Target heading not found in (.+): (.+)/);
if (!match || !match[1] || !match[2]) {
throw new Error("Could not extract target file and heading from finding");
}
const targetFile = match[1];
const targetHeading = match[2];
// Find the edge
const edge = findEdgeForFinding(report, finding);
if (!edge) {
throw new Error("Could not find edge for finding");
}
// Resolve target file
const resolvedFile = app.metadataCache.getFirstLinkpathDest(
normalizeLinkTarget(targetFile),
report.context.file
);
if (!resolvedFile) {
throw new Error(`Target file not found: ${targetFile}`);
}
// Read file
const content = await app.vault.read(resolvedFile);
// Create heading with specified level
const level = settings.fixActions.createMissingHeading.level;
const headingPrefix = "#".repeat(level);
const newHeading = `\n\n${headingPrefix} ${targetHeading}\n`;
// Append heading at end of file
const newContent = content + newHeading;
// Write file
await app.vault.modify(resolvedFile, newContent);
}
/**
* Retarget to existing heading.
*/
async function retargetToHeading(
app: App,
editor: Editor,
finding: Finding,
report: any
): Promise<void> {
// Extract target file and heading from finding message
const message = finding.message;
const match = message.match(/Target heading not found in (.+): (.+)/);
if (!match || !match[1] || !match[2]) {
throw new Error("Could not extract target file and heading from finding");
}
const targetFile = match[1];
const oldHeading = match[2];
// Resolve target file
const resolvedFile = app.metadataCache.getFirstLinkpathDest(
normalizeLinkTarget(targetFile),
report.context.file
);
if (!resolvedFile) {
throw new Error(`Target file not found: ${targetFile}`);
}
// Get headings from file cache
const fileCache = app.metadataCache.getFileCache(resolvedFile);
if (!fileCache || !fileCache.headings || fileCache.headings.length === 0) {
throw new Error("No headings found in target file");
}
// Show heading picker
const headings = fileCache.headings.map((h) => h.heading);
const selectedHeading = await selectHeading(app, headings, targetFile);
if (!selectedHeading) {
return; // User cancelled
}
// Find and replace link in editor
const content = editor.getValue();
const oldLinkPattern = new RegExp(
`\\[\\[${targetFile.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}#${oldHeading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]\\]`,
"g"
);
const newLink = `[[${targetFile}#${selectedHeading}]]`;
const newContent = content.replace(oldLinkPattern, newLink);
editor.setValue(newContent);
}
/**
* Promote candidate edge to explicit edge.
*/
async function promoteCandidate(
app: App,
editor: Editor,
context: any,
finding: Finding,
report: any,
settings: MindnetSettings
): Promise<void> {
const content = editor.getValue();
// Find candidates zone
const candidatesMatch = content.match(/## Kandidaten\n([\s\S]*?)(?=\n## |$)/);
if (!candidatesMatch || !candidatesMatch[1]) {
throw new Error("Could not find candidates zone");
}
const candidatesContent = candidatesMatch[1];
// Parse first candidate edge
const edgeMatch = candidatesContent.match(
/>\s*\[!edge\]\s+(\S+)\s*\n>\s*\[\[([^\]]+)\]\]/
);
if (!edgeMatch || !edgeMatch[1] || !edgeMatch[2]) {
throw new Error("Could not find candidate edge to promote");
}
const edgeType = edgeMatch[1];
const target = edgeMatch[2];
const fullEdgeMatch = edgeMatch[0];
// Find current section (by heading or root)
const headingPattern = context.heading
? new RegExp(`^##\\s+${context.heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m")
: /^##\s+/m;
const sectionMatch = content.match(
new RegExp(
`(${context.heading ? `##\\s+${context.heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}` : "^[^#]"}[\\s\\S]*?)(?=\\n##|$)`,
"m"
)
);
if (!sectionMatch || !sectionMatch[1]) {
throw new Error("Could not find current section");
}
const currentSection = sectionMatch[1];
const sectionStart = sectionMatch.index || 0;
// Check if section already has semantic mapping block
const hasMappingBlock = currentSection.includes("🕸️ Semantic Mapping");
let newSectionContent: string;
if (hasMappingBlock) {
// Add edge to existing mapping block
const mappingBlockMatch = currentSection.match(
/(>\s*\[!abstract\]-?\s*🕸️ Semantic Mapping[\s\S]*?)(\n\n|$)/
);
if (mappingBlockMatch && mappingBlockMatch[1]) {
const mappingBlock = mappingBlockMatch[1];
const newEdge = `>> [!edge] ${edgeType}\n>> [[${target}]]\n`;
const updatedMappingBlock = mappingBlock.trimEnd() + "\n" + newEdge;
newSectionContent = currentSection.replace(
mappingBlockMatch[0],
updatedMappingBlock + "\n\n"
);
} else {
throw new Error("Could not parse existing mapping block");
}
} else {
// Create new mapping block at end of section
const mappingBlock = `\n\n> [!abstract]- 🕸️ Semantic Mapping\n>> [!edge] ${edgeType}\n>> [[${target}]]\n`;
newSectionContent = currentSection.trimEnd() + mappingBlock;
}
// Replace section in content
const beforeSection = content.substring(0, sectionStart);
const afterSection = content.substring(sectionStart + currentSection.length);
let newContent = beforeSection + newSectionContent + afterSection;
// Remove candidate edge if keepOriginal is false
if (!settings.fixActions.promoteCandidate.keepOriginal) {
const updatedCandidatesContent = candidatesContent.replace(
fullEdgeMatch + "\n",
""
).replace(fullEdgeMatch, "");
newContent = newContent.replace(
/## Kandidaten\n[\s\S]*?(?=\n## |$)/,
`## Kandidaten\n${updatedCandidatesContent}`
);
}
editor.setValue(newContent);
}
/**
* Find edge in report that matches finding.
*/
function findEdgeForFinding(report: any, finding: Finding): any {
if (finding.code === "dangling_target") {
const message = finding.message;
const targetMatch = message.match(/Target file does not exist: (.+)/);
if (targetMatch && targetMatch[1]) {
const targetFile = targetMatch[1];
// Match by exact file or basename
return report.neighbors.outgoing.find((e: any) => {
const edgeTarget = e.target.file;
return (
edgeTarget === targetFile ||
edgeTarget === targetFile.replace(/\.md$/, "") ||
edgeTarget.replace(/\.md$/, "") === targetFile ||
edgeTarget.replace(/\.md$/, "") === targetFile.replace(/\.md$/, "")
);
});
}
} else if (finding.code === "dangling_target_heading") {
const message = finding.message;
const match = message.match(/Target heading not found in (.+): (.+)/);
if (match && match[1] && match[2]) {
const targetFile = match[1];
const targetHeading = match[2];
return report.neighbors.outgoing.find((e: any) => {
const edgeTarget = e.target.file;
const edgeHeading = e.target.heading;
const fileMatches =
edgeTarget === targetFile ||
edgeTarget === targetFile.replace(/\.md$/, "") ||
edgeTarget.replace(/\.md$/, "") === targetFile;
return fileMatches && edgeHeading === targetHeading;
});
}
}
return null;
}
/**
* Simple modal for finding selection.
*/
class FindingSelectionModal extends Modal {
constructor(
app: App,
findings: Finding[],
onSelect: (finding: Finding) => void
) {
super(app);
this.findings = findings;
this.onSelect = onSelect;
}
private findings: Finding[];
private onSelect: (finding: Finding) => void;
onOpen(): void {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: "Select Finding to Fix" });
for (const finding of this.findings) {
const severityIcon =
finding.severity === "error"
? "❌"
: finding.severity === "warn"
? "⚠️"
: "";
const setting = new Setting(contentEl)
.setName(`${severityIcon} ${finding.code}`)
.setDesc(finding.message)
.addButton((btn) =>
btn.setButtonText("Fix").setCta().onClick(() => {
console.log("[Fix Findings] Fix button clicked for finding:", finding.code);
this.onSelect(finding);
})
);
}
}
}
/**
* Simple modal for action selection.
*/
class ActionSelectionModal extends Modal {
constructor(
app: App,
finding: Finding,
actions: string[],
onSelect: (action: string) => void
) {
super(app);
this.finding = finding;
this.actions = actions;
this.onSelect = onSelect;
}
private finding: Finding;
private actions: string[];
private onSelect: (action: string) => void;
onOpen(): void {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: "Select Action" });
contentEl.createEl("p", { text: `Finding: ${this.finding.message}` });
const actionLabels: Record<string, string> = {
create_missing_note: "Create Missing Note",
retarget_link: "Retarget Link to Existing Note",
create_missing_heading: "Create Missing Heading",
retarget_to_heading: "Retarget to Existing Heading",
promote_candidate: "Promote Candidate Edge",
};
for (const action of this.actions) {
const setting = new Setting(contentEl)
.setName(actionLabels[action] || action)
.addButton((btn) =>
btn.setButtonText("Apply").setCta().onClick(() => {
console.log("[Fix Findings] Apply button clicked for action:", action);
this.onSelect(action);
})
);
}
}
}
/**
* Select heading from list.
*/
function selectHeading(
app: App,
headings: string[],
targetFile: string
): Promise<string | null> {
return new Promise((resolve) => {
let resolved = false;
const modal = new HeadingSelectionModal(app, headings, targetFile, (heading) => {
if (!resolved) {
resolved = true;
modal.close();
resolve(heading);
}
});
modal.onClose = () => {
if (!resolved) {
resolved = true;
resolve(null);
}
};
modal.open();
});
}
/**
* Simple modal for heading selection.
*/
class HeadingSelectionModal extends Modal {
constructor(
app: App,
headings: string[],
targetFile: string,
onSelect: (heading: string) => void
) {
super(app);
this.headings = headings;
this.targetFile = targetFile;
this.onSelect = onSelect;
}
private headings: string[];
private targetFile: string;
private onSelect: (heading: string) => void;
onOpen(): void {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: "Select Heading" });
contentEl.createEl("p", { text: `File: ${this.targetFile}` });
for (const heading of this.headings) {
const setting = new Setting(contentEl)
.setName(heading)
.addButton((btn) =>
btn.setButtonText("Select").onClick(() => {
this.onSelect(heading);
})
);
}
}
}