- Introduced a new command to build semantic mapping blocks by section, including user prompts for overwriting existing mappings. - Enhanced settings interface with options for mapping wrapper type, title, folded state, default edge type, unassigned handling, and overwrite permissions. - Implemented virtualization in the EntityPickerModal for improved performance when displaying large sets of entries, including scroll handling and item rendering optimizations. - Updated UI components to reflect new settings and functionalities, ensuring a cohesive user experience.
184 lines
5.2 KiB
TypeScript
184 lines
5.2 KiB
TypeScript
/**
|
|
* Modal for prompting edge type assignment for a single link.
|
|
*/
|
|
|
|
import { Modal } from "obsidian";
|
|
import type { LinkWorkItem } from "../mapping/worklistBuilder";
|
|
import type { EdgeVocabulary } from "../vocab/types";
|
|
import type { GraphSchema } from "../mapping/graphSchema";
|
|
import { EdgeTypeChooserModal, type EdgeTypeChoice } from "./EdgeTypeChooserModal";
|
|
import { computeEdgeSuggestions } from "../mapping/schemaHelper";
|
|
|
|
export type LinkPromptDecision =
|
|
| { action: "keep"; edgeType: string }
|
|
| { action: "change"; edgeType: string; alias?: string }
|
|
| { action: "skip" };
|
|
|
|
export class LinkPromptModal extends Modal {
|
|
private item: LinkWorkItem;
|
|
private vocabulary: EdgeVocabulary;
|
|
private sourceType: string | null;
|
|
private graphSchema: GraphSchema | null;
|
|
private result: LinkPromptDecision | null = null;
|
|
private resolve: ((result: LinkPromptDecision) => void) | null = null;
|
|
|
|
constructor(
|
|
app: any,
|
|
item: LinkWorkItem,
|
|
vocabulary: EdgeVocabulary,
|
|
sourceType: string | null,
|
|
graphSchema: GraphSchema | null = null
|
|
) {
|
|
super(app);
|
|
this.item = item;
|
|
this.vocabulary = vocabulary;
|
|
this.sourceType = sourceType;
|
|
this.graphSchema = graphSchema;
|
|
}
|
|
|
|
onOpen(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
contentEl.addClass("link-prompt-modal");
|
|
|
|
// Link info
|
|
const linkInfo = contentEl.createEl("div", { cls: "link-info" });
|
|
linkInfo.createEl("h2", { text: `Link: [[${this.item.link}]]` });
|
|
|
|
if (this.item.targetType) {
|
|
linkInfo.createEl("p", { text: `Target type: ${this.item.targetType}` });
|
|
}
|
|
|
|
// Current mapping (if exists)
|
|
if (this.item.currentType) {
|
|
const currentInfo = linkInfo.createEl("p", { cls: "current-mapping" });
|
|
currentInfo.textContent = `Current edge type: ${this.item.currentType}`;
|
|
|
|
// Check if current type is prohibited
|
|
if (this.graphSchema) {
|
|
const suggestions = computeEdgeSuggestions(
|
|
this.vocabulary,
|
|
this.sourceType,
|
|
this.item.targetType,
|
|
this.graphSchema
|
|
);
|
|
if (suggestions.prohibited.includes(this.item.currentType)) {
|
|
const warning = linkInfo.createEl("span", {
|
|
text: " ⚠️ Prohibited",
|
|
cls: "prohibited-warning",
|
|
});
|
|
warning.style.color = "var(--text-error)";
|
|
warning.style.fontWeight = "bold";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Action buttons
|
|
const buttonContainer = contentEl.createEl("div", { cls: "action-buttons" });
|
|
buttonContainer.style.display = "flex";
|
|
buttonContainer.style.flexDirection = "column";
|
|
buttonContainer.style.gap = "0.5em";
|
|
buttonContainer.style.marginTop = "1em";
|
|
|
|
if (this.item.currentType) {
|
|
// Keep current
|
|
const keepBtn = buttonContainer.createEl("button", {
|
|
text: `✅ Keep (${this.item.currentType})`,
|
|
cls: "mod-cta",
|
|
});
|
|
keepBtn.onclick = () => {
|
|
this.result = { action: "keep", edgeType: this.item.currentType! };
|
|
this.close();
|
|
};
|
|
|
|
// Change type
|
|
const changeBtn = buttonContainer.createEl("button", {
|
|
text: "Change type...",
|
|
});
|
|
changeBtn.onclick = async () => {
|
|
const chooser = new EdgeTypeChooserModal(
|
|
this.app,
|
|
this.vocabulary,
|
|
this.sourceType,
|
|
this.item.targetType,
|
|
this.graphSchema
|
|
);
|
|
const choice = await chooser.show();
|
|
if (choice) {
|
|
this.result = {
|
|
action: "change",
|
|
edgeType: choice.edgeType,
|
|
alias: choice.alias,
|
|
};
|
|
this.close();
|
|
}
|
|
};
|
|
|
|
// Skip
|
|
const skipBtn = buttonContainer.createEl("button", {
|
|
text: "Skip link",
|
|
});
|
|
skipBtn.onclick = () => {
|
|
this.result = { action: "skip" };
|
|
this.close();
|
|
};
|
|
} else {
|
|
// No current mapping
|
|
// Skip
|
|
const skipBtn = buttonContainer.createEl("button", {
|
|
text: "⏩ Skip link",
|
|
});
|
|
skipBtn.onclick = () => {
|
|
this.result = { action: "skip" };
|
|
this.close();
|
|
};
|
|
|
|
// Choose type
|
|
const chooseBtn = buttonContainer.createEl("button", {
|
|
text: "Choose type...",
|
|
cls: "mod-cta",
|
|
});
|
|
chooseBtn.onclick = async () => {
|
|
const chooser = new EdgeTypeChooserModal(
|
|
this.app,
|
|
this.vocabulary,
|
|
this.sourceType,
|
|
this.item.targetType,
|
|
this.graphSchema
|
|
);
|
|
const choice = await chooser.show();
|
|
if (choice) {
|
|
this.result = {
|
|
action: "change",
|
|
edgeType: choice.edgeType,
|
|
alias: choice.alias,
|
|
};
|
|
this.close();
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
onClose(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
|
|
if (this.resolve && this.result) {
|
|
this.resolve(this.result);
|
|
} else if (this.resolve) {
|
|
// User closed without decision, treat as skip
|
|
this.resolve({ action: "skip" });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show modal and return promise that resolves with user's decision.
|
|
*/
|
|
async show(): Promise<LinkPromptDecision> {
|
|
return new Promise((resolve) => {
|
|
this.resolve = resolve;
|
|
this.open();
|
|
});
|
|
}
|
|
}
|