- Introduced new workflows for Chain Workbench and Vault Triage, enhancing user capabilities for managing template matches and identifying chain gaps. - Added commands for opening the Chain Workbench and scanning the vault for chain gaps, improving the overall functionality of the plugin. - Updated documentation to include detailed instructions for the new workflows, ensuring users can effectively utilize the features. - Enhanced the UI for both the Chain Workbench and Vault Triage Scan, providing a more intuitive user experience. - Implemented tests for the new functionalities to ensure reliability and accuracy in various scenarios.
363 lines
12 KiB
TypeScript
363 lines
12 KiB
TypeScript
/**
|
|
* Vault Triage Scan Modal - UI for vault triage scan backlog.
|
|
*/
|
|
|
|
import { Modal, Setting, Notice } from "obsidian";
|
|
import type { App } from "obsidian";
|
|
import type { ChainRolesConfig, ChainTemplatesConfig } from "../dictionary/types";
|
|
import type { MindnetSettings } from "../settings";
|
|
import type { EdgeVocabulary } from "../vocab/types";
|
|
import { scanVaultForChainGaps, saveScanState, type ScanState, type ScanItem } from "../workbench/vaultTriageScan";
|
|
|
|
export class VaultTriageScanModal extends Modal {
|
|
private chainRoles: ChainRolesConfig | null;
|
|
private chainTemplates: ChainTemplatesConfig | null;
|
|
private edgeVocabulary: EdgeVocabulary | null;
|
|
private settings: MindnetSettings;
|
|
private pluginInstance: any;
|
|
private existingState: ScanState | null;
|
|
|
|
private items: ScanItem[] = [];
|
|
private filterStatus: string | null = null;
|
|
private filterHasMissingSlot: boolean = false;
|
|
private filterHasMissingLink: boolean = false;
|
|
private filterWeak: boolean = false;
|
|
private filterCandidateCleanup: boolean = false;
|
|
private searchQuery: string = "";
|
|
private isScanning: boolean = false;
|
|
private shouldCancel: boolean = false;
|
|
|
|
constructor(
|
|
app: App,
|
|
chainRoles: ChainRolesConfig | null,
|
|
chainTemplates: ChainTemplatesConfig | null,
|
|
edgeVocabulary: EdgeVocabulary | null,
|
|
settings: MindnetSettings,
|
|
pluginInstance: any,
|
|
existingState: ScanState | null
|
|
) {
|
|
super(app);
|
|
this.chainRoles = chainRoles;
|
|
this.chainTemplates = chainTemplates;
|
|
this.edgeVocabulary = edgeVocabulary;
|
|
this.settings = settings;
|
|
this.pluginInstance = pluginInstance;
|
|
this.existingState = existingState;
|
|
|
|
if (existingState) {
|
|
this.items = existingState.items;
|
|
}
|
|
}
|
|
|
|
onOpen(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
|
|
// Header
|
|
contentEl.createEl("h2", { text: "Vault Triage Scan" });
|
|
|
|
if (this.existingState && !this.existingState.completed) {
|
|
contentEl.createEl("p", {
|
|
text: `Resuming scan: ${this.existingState.progress.current}/${this.existingState.progress.total}`,
|
|
cls: "scan-progress-info",
|
|
});
|
|
}
|
|
|
|
// Controls
|
|
const controlsContainer = contentEl.createDiv({ cls: "scan-controls" });
|
|
|
|
// Start/Resume scan button
|
|
if (!this.isScanning && (this.items.length === 0 || !this.existingState?.completed)) {
|
|
const scanBtn = controlsContainer.createEl("button", {
|
|
text: this.items.length === 0 ? "Start Scan" : "Resume Scan",
|
|
cls: "mod-cta",
|
|
});
|
|
scanBtn.addEventListener("click", () => {
|
|
this.startScan();
|
|
});
|
|
}
|
|
|
|
// Filters
|
|
const filtersContainer = contentEl.createDiv({ cls: "scan-filters" });
|
|
filtersContainer.createEl("h3", { text: "Filters" });
|
|
|
|
// Status filter
|
|
const statusSelect = filtersContainer.createEl("select");
|
|
statusSelect.createEl("option", { text: "All", value: "" });
|
|
statusSelect.createEl("option", { text: "Near Complete", value: "near_complete" });
|
|
statusSelect.createEl("option", { text: "Partial", value: "partial" });
|
|
statusSelect.createEl("option", { text: "Weak", value: "weak" });
|
|
statusSelect.createEl("option", { text: "Deprioritized", value: "deprioritized" });
|
|
statusSelect.value = this.filterStatus || "";
|
|
statusSelect.addEventListener("change", (e) => {
|
|
const target = e.target as HTMLSelectElement;
|
|
this.filterStatus = target.value || null;
|
|
this.render();
|
|
});
|
|
|
|
// Gap filters
|
|
new Setting(filtersContainer)
|
|
.setName("Has Missing Slot")
|
|
.addToggle((toggle) => {
|
|
toggle.setValue(this.filterHasMissingSlot);
|
|
toggle.onChange((value) => {
|
|
this.filterHasMissingSlot = value;
|
|
this.render();
|
|
});
|
|
});
|
|
|
|
new Setting(filtersContainer)
|
|
.setName("Has Missing Link")
|
|
.addToggle((toggle) => {
|
|
toggle.setValue(this.filterHasMissingLink);
|
|
toggle.onChange((value) => {
|
|
this.filterHasMissingLink = value;
|
|
this.render();
|
|
});
|
|
});
|
|
|
|
new Setting(filtersContainer)
|
|
.setName("Weak")
|
|
.addToggle((toggle) => {
|
|
toggle.setValue(this.filterWeak);
|
|
toggle.onChange((value) => {
|
|
this.filterWeak = value;
|
|
this.render();
|
|
});
|
|
});
|
|
|
|
new Setting(filtersContainer)
|
|
.setName("Candidate Cleanup")
|
|
.addToggle((toggle) => {
|
|
toggle.setValue(this.filterCandidateCleanup);
|
|
toggle.onChange((value) => {
|
|
this.filterCandidateCleanup = value;
|
|
this.render();
|
|
});
|
|
});
|
|
|
|
// Search
|
|
filtersContainer.createEl("label", { text: "Search:" });
|
|
const searchInput = filtersContainer.createEl("input", {
|
|
type: "text",
|
|
placeholder: "File name, heading, template...",
|
|
});
|
|
searchInput.value = this.searchQuery;
|
|
searchInput.addEventListener("input", (e) => {
|
|
const target = e.target as HTMLInputElement;
|
|
this.searchQuery = target.value.toLowerCase();
|
|
this.render();
|
|
});
|
|
|
|
// Results list
|
|
const resultsContainer = contentEl.createDiv({ cls: "scan-results" });
|
|
resultsContainer.createEl("h3", { text: "Results" });
|
|
|
|
this.render();
|
|
}
|
|
|
|
private async startScan(): Promise<void> {
|
|
this.isScanning = true;
|
|
this.shouldCancel = false;
|
|
this.render();
|
|
|
|
try {
|
|
const items = await scanVaultForChainGaps(
|
|
this.app,
|
|
this.chainRoles,
|
|
this.chainTemplates,
|
|
this.edgeVocabulary,
|
|
this.settings,
|
|
(progress) => {
|
|
// Update progress
|
|
const state: ScanState = {
|
|
items: this.items,
|
|
timestamp: Date.now(),
|
|
progress,
|
|
completed: false,
|
|
};
|
|
saveScanState(this.app, state, this.pluginInstance);
|
|
this.render();
|
|
},
|
|
() => this.shouldCancel
|
|
);
|
|
|
|
this.items = items;
|
|
|
|
// Save completed state
|
|
const state: ScanState = {
|
|
items,
|
|
timestamp: Date.now(),
|
|
progress: {
|
|
current: items.length,
|
|
total: items.length,
|
|
currentFile: null,
|
|
},
|
|
completed: true,
|
|
};
|
|
await saveScanState(this.app, state, this.pluginInstance);
|
|
|
|
this.isScanning = false;
|
|
this.render();
|
|
} catch (e) {
|
|
this.isScanning = false;
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
console.error("[Vault Triage Scan] Error:", e);
|
|
new Notice(`Scan failed: ${msg}`);
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
private render(): void {
|
|
const resultsContainer = this.contentEl.querySelector(".scan-results");
|
|
if (!resultsContainer) return;
|
|
|
|
// Clear previous results
|
|
const existingList = resultsContainer.querySelector(".scan-items-list");
|
|
if (existingList) {
|
|
existingList.remove();
|
|
}
|
|
|
|
// Filter items
|
|
let filteredItems = this.items;
|
|
|
|
if (this.filterStatus) {
|
|
filteredItems = filteredItems.filter((item) => item.status === this.filterStatus);
|
|
}
|
|
|
|
if (this.filterHasMissingSlot) {
|
|
filteredItems = filteredItems.filter((item) => item.gapCounts.missingSlots > 0);
|
|
}
|
|
|
|
if (this.filterHasMissingLink) {
|
|
filteredItems = filteredItems.filter((item) => item.gapCounts.missingLinks > 0);
|
|
}
|
|
|
|
if (this.filterWeak) {
|
|
filteredItems = filteredItems.filter((item) => item.status === "weak");
|
|
}
|
|
|
|
if (this.filterCandidateCleanup) {
|
|
filteredItems = filteredItems.filter((item) => item.gapCounts.candidateCleanup > 0);
|
|
}
|
|
|
|
if (this.searchQuery) {
|
|
filteredItems = filteredItems.filter((item) => {
|
|
const fileMatch = item.file.toLowerCase().includes(this.searchQuery);
|
|
const headingMatch = item.heading?.toLowerCase().includes(this.searchQuery);
|
|
const templateMatch = item.matches.some((m) =>
|
|
m.templateName.toLowerCase().includes(this.searchQuery)
|
|
);
|
|
return fileMatch || headingMatch || templateMatch;
|
|
});
|
|
}
|
|
|
|
// Sort: near_complete first, then by score descending
|
|
filteredItems.sort((a, b) => {
|
|
if (a.status === "near_complete" && b.status !== "near_complete") return -1;
|
|
if (a.status !== "near_complete" && b.status === "near_complete") return 1;
|
|
|
|
const aMaxScore = Math.max(...a.matches.map((m) => m.score));
|
|
const bMaxScore = Math.max(...b.matches.map((m) => m.score));
|
|
return bMaxScore - aMaxScore;
|
|
});
|
|
|
|
// Render list
|
|
const listContainer = resultsContainer.createDiv({ cls: "scan-items-list" });
|
|
listContainer.createEl("p", {
|
|
text: `${filteredItems.length} item(s)${this.isScanning ? " (scanning...)" : ""}`,
|
|
});
|
|
|
|
for (const item of filteredItems) {
|
|
const itemEl = listContainer.createDiv({ cls: "scan-item" });
|
|
|
|
const headerEl = itemEl.createDiv({ cls: "scan-item-header" });
|
|
headerEl.createEl("div", {
|
|
cls: "scan-item-title",
|
|
text: `${item.file}${item.heading ? `#${item.heading}` : ""}`,
|
|
});
|
|
|
|
const statusBadge = headerEl.createEl("span", {
|
|
cls: `scan-item-status status-${item.status}`,
|
|
text: item.status,
|
|
});
|
|
|
|
// Gap counts
|
|
const gapCountsEl = itemEl.createDiv({ cls: "scan-item-gaps" });
|
|
if (item.gapCounts.missingSlots > 0) {
|
|
gapCountsEl.createEl("span", {
|
|
text: `Missing Slots: ${item.gapCounts.missingSlots}`,
|
|
cls: "gap-badge",
|
|
});
|
|
}
|
|
if (item.gapCounts.missingLinks > 0) {
|
|
gapCountsEl.createEl("span", {
|
|
text: `Missing Links: ${item.gapCounts.missingLinks}`,
|
|
cls: "gap-badge",
|
|
});
|
|
}
|
|
if (item.gapCounts.weakRoles > 0) {
|
|
gapCountsEl.createEl("span", {
|
|
text: `Weak Roles: ${item.gapCounts.weakRoles}`,
|
|
cls: "gap-badge",
|
|
});
|
|
}
|
|
if (item.gapCounts.candidateCleanup > 0) {
|
|
gapCountsEl.createEl("span", {
|
|
text: `Candidate Cleanup: ${item.gapCounts.candidateCleanup}`,
|
|
cls: "gap-badge",
|
|
});
|
|
}
|
|
|
|
// Matches summary
|
|
const matchesEl = itemEl.createDiv({ cls: "scan-item-matches" });
|
|
matchesEl.createEl("div", {
|
|
text: `${item.matches.length} template match(es)`,
|
|
});
|
|
|
|
// Actions
|
|
const actionsEl = itemEl.createDiv({ cls: "scan-item-actions" });
|
|
const openBtn = actionsEl.createEl("button", {
|
|
text: "Open Workbench",
|
|
});
|
|
openBtn.addEventListener("click", async () => {
|
|
// Open file and section, then open workbench
|
|
await this.app.workspace.openLinkText(item.file, "", true);
|
|
|
|
// Wait a bit for file to open, then trigger workbench
|
|
setTimeout(async () => {
|
|
const { executeChainWorkbench } = await import("../commands/chainWorkbenchCommand");
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const activeEditor = this.app.workspace.activeEditor?.editor;
|
|
|
|
if (activeFile && activeEditor) {
|
|
await executeChainWorkbench(
|
|
this.app,
|
|
activeEditor,
|
|
activeFile.path,
|
|
this.chainRoles,
|
|
this.chainTemplates,
|
|
undefined,
|
|
this.settings,
|
|
this.pluginInstance
|
|
);
|
|
}
|
|
}, 500);
|
|
});
|
|
|
|
const deprioritizeBtn = actionsEl.createEl("button", {
|
|
text: item.status === "deprioritized" ? "Prioritize" : "Deprioritize",
|
|
});
|
|
deprioritizeBtn.addEventListener("click", () => {
|
|
item.status = item.status === "deprioritized" ? "near_complete" : "deprioritized";
|
|
this.render();
|
|
});
|
|
}
|
|
}
|
|
|
|
onClose(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
}
|