Enhance edge type handling and categorization
- Added optional description and category fields to edge type entries, improving metadata for edge types. - Updated the `getAllEdgeTypes` and `groupEdgeTypesByCategory` functions to utilize new fields for better organization and display. - Enhanced UI components to show descriptions as tooltips and categorize edge types in the EdgeTypeChooserModal and InlineEdgeTypeModal. - Improved parsing logic in `parseEdgeVocabulary` to extract descriptions and categories from the vocabulary table, ensuring richer edge type data. - Adjusted the LinkPromptModal to clarify edge type actions and maintain alias information during selection.
This commit is contained in:
parent
7ea36fbed4
commit
2fcf333e56
|
|
@ -39,11 +39,15 @@ export function getAllEdgeTypes(vocabulary: EdgeVocabulary): Array<{
|
||||||
canonical: string;
|
canonical: string;
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
displayName: string; // First alias or canonical
|
displayName: string; // First alias or canonical
|
||||||
|
description?: string;
|
||||||
|
category?: string;
|
||||||
}> {
|
}> {
|
||||||
const result: Array<{
|
const result: Array<{
|
||||||
canonical: string;
|
canonical: string;
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
description?: string;
|
||||||
|
category?: string;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
for (const [canonical, entry] of vocabulary.byCanonical.entries()) {
|
for (const [canonical, entry] of vocabulary.byCanonical.entries()) {
|
||||||
|
|
@ -53,6 +57,8 @@ export function getAllEdgeTypes(vocabulary: EdgeVocabulary): Array<{
|
||||||
canonical,
|
canonical,
|
||||||
aliases: entry.aliases,
|
aliases: entry.aliases,
|
||||||
displayName,
|
displayName,
|
||||||
|
description: entry.description,
|
||||||
|
category: entry.category,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,14 +70,42 @@ export function getAllEdgeTypes(vocabulary: EdgeVocabulary): Array<{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group edge types by category (if available).
|
* Group edge types by category (if available).
|
||||||
* MVP: Returns single "All" category.
|
* Uses category from EdgeTypeEntry if available.
|
||||||
|
* If categories are found, groups by category. Otherwise returns single "All" group.
|
||||||
*/
|
*/
|
||||||
export function groupEdgeTypesByCategory(
|
export function groupEdgeTypesByCategory(
|
||||||
edgeTypes: Array<{ canonical: string; aliases: string[]; displayName: string }>
|
edgeTypes: Array<{ canonical: string; aliases: string[]; displayName: string; description?: string; category?: string }>
|
||||||
): Map<string, Array<{ canonical: string; aliases: string[]; displayName: string }>> {
|
): Map<string, Array<{ canonical: string; aliases: string[]; displayName: string; description?: string; category?: string }>> {
|
||||||
// TODO: Implement category grouping from schema
|
const grouped = new Map<string, Array<{ canonical: string; aliases: string[]; displayName: string; description?: string; category?: string }>>();
|
||||||
// For MVP, return single "All" category
|
|
||||||
const grouped = new Map<string, Array<{ canonical: string; aliases: string[]; displayName: string }>>();
|
// Check if any edge types have categories
|
||||||
grouped.set("All", edgeTypes);
|
const hasCategories = edgeTypes.some(e => e.category && e.category.trim() !== "");
|
||||||
return grouped;
|
|
||||||
|
if (hasCategories) {
|
||||||
|
// Group by category
|
||||||
|
for (const edgeType of edgeTypes) {
|
||||||
|
const category = edgeType.category && edgeType.category.trim() !== ""
|
||||||
|
? edgeType.category.trim()
|
||||||
|
: "Allgemein"; // Default category if not specified
|
||||||
|
if (!grouped.has(category)) {
|
||||||
|
grouped.set(category, []);
|
||||||
|
}
|
||||||
|
grouped.get(category)!.push(edgeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort categories alphabetically
|
||||||
|
const sortedCategories = Array.from(grouped.keys()).sort();
|
||||||
|
const sortedGrouped = new Map<string, Array<{ canonical: string; aliases: string[]; displayName: string; description?: string; category?: string }>>();
|
||||||
|
for (const category of sortedCategories) {
|
||||||
|
const types = grouped.get(category);
|
||||||
|
if (types) {
|
||||||
|
sortedGrouped.set(category, types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortedGrouped;
|
||||||
|
} else {
|
||||||
|
// No categories found, use "All"
|
||||||
|
grouped.set("All", edgeTypes);
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -227,13 +227,18 @@ export async function buildSemanticMappings(
|
||||||
|
|
||||||
// Apply decision
|
// Apply decision
|
||||||
if (decision.action === "keep") {
|
if (decision.action === "keep") {
|
||||||
|
// Use the current type as-is (preserves alias if it was an alias)
|
||||||
|
// item.currentType is already the actual type from existingMappings (could be alias or canonical)
|
||||||
mappingsToUse.set(item.link, decision.edgeType);
|
mappingsToUse.set(item.link, decision.edgeType);
|
||||||
if (mappingState.existingMappings.has(item.link)) {
|
if (mappingState.existingMappings.has(item.link)) {
|
||||||
result.existingMappingsKept++;
|
result.existingMappingsKept++;
|
||||||
}
|
}
|
||||||
} else if (decision.action === "change") {
|
} else if (decision.action === "change") {
|
||||||
// Use canonical type (alias is for display only)
|
// Use alias if provided, otherwise use canonical type
|
||||||
mappingsToUse.set(item.link, decision.edgeType);
|
// This ensures selected aliases are written to the file, not just canonical types
|
||||||
|
const finalEdgeType = decision.alias || decision.edgeType;
|
||||||
|
mappingsToUse.set(item.link, finalEdgeType);
|
||||||
|
console.log(`[Mindnet] Setting edge type for ${item.link}: ${finalEdgeType} (canonical: ${decision.edgeType}, alias: ${decision.alias || "none"})`);
|
||||||
if (mappingState.existingMappings.has(item.link)) {
|
if (mappingState.existingMappings.has(item.link)) {
|
||||||
// Changed existing
|
// Changed existing
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -248,9 +253,10 @@ export async function buildSemanticMappings(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Silent modes: none/defaultType/advisor
|
// Silent modes: none/defaultType/advisor
|
||||||
// Always include existing mappings
|
// Always include existing mappings (preserve aliases as they are)
|
||||||
for (const [link, edgeType] of mappingState.existingMappings.entries()) {
|
for (const [link, edgeType] of mappingState.existingMappings.entries()) {
|
||||||
if (section.links.includes(link)) {
|
if (section.links.includes(link)) {
|
||||||
|
// Use existing type as-is (could be alias or canonical)
|
||||||
mappingsToUse.set(link, edgeType);
|
mappingsToUse.set(link, edgeType);
|
||||||
result.existingMappingsKept++;
|
result.existingMappingsKept++;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,13 @@ export class EdgeTypeChooserModal extends Modal {
|
||||||
const allEdgeTypes = getAllEdgeTypes(this.vocabulary);
|
const allEdgeTypes = getAllEdgeTypes(this.vocabulary);
|
||||||
const grouped = groupEdgeTypesByCategory(allEdgeTypes);
|
const grouped = groupEdgeTypesByCategory(allEdgeTypes);
|
||||||
|
|
||||||
|
// Debug: Log grouping info
|
||||||
|
console.log("[Mindnet] EdgeTypeChooserModal:", {
|
||||||
|
totalEdgeTypes: allEdgeTypes.length,
|
||||||
|
categories: Array.from(grouped.keys()),
|
||||||
|
categorySizes: Array.from(grouped.entries()).map(([cat, types]) => ({ category: cat, count: types.length })),
|
||||||
|
});
|
||||||
|
|
||||||
// Recommended section (if any)
|
// Recommended section (if any)
|
||||||
if (suggestions.typical.length > 0) {
|
if (suggestions.typical.length > 0) {
|
||||||
contentEl.createEl("h3", { text: "⭐ Recommended (schema)" });
|
contentEl.createEl("h3", { text: "⭐ Recommended (schema)" });
|
||||||
|
|
@ -61,11 +68,18 @@ export class EdgeTypeChooserModal extends Modal {
|
||||||
const entry = this.vocabulary.byCanonical.get(canonical);
|
const entry = this.vocabulary.byCanonical.get(canonical);
|
||||||
if (!entry) continue;
|
if (!entry) continue;
|
||||||
|
|
||||||
const displayName = entry.aliases.length > 0 ? entry.aliases[0] : canonical;
|
// Display: canonical type (primary), aliases shown after selection
|
||||||
const btn = recommendedContainer.createEl("button", {
|
const btn = recommendedContainer.createEl("button", {
|
||||||
text: `⭐ ${displayName}`,
|
text: `⭐ ${canonical}`,
|
||||||
cls: "mod-cta",
|
cls: "mod-cta",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add description as tooltip/hover text
|
||||||
|
if (entry.description) {
|
||||||
|
btn.title = entry.description;
|
||||||
|
btn.setAttribute("data-description", entry.description);
|
||||||
|
}
|
||||||
|
|
||||||
btn.onclick = () => {
|
btn.onclick = () => {
|
||||||
this.selectEdgeType(canonical, entry.aliases);
|
this.selectEdgeType(canonical, entry.aliases);
|
||||||
};
|
};
|
||||||
|
|
@ -73,16 +87,24 @@ export class EdgeTypeChooserModal extends Modal {
|
||||||
}
|
}
|
||||||
|
|
||||||
// All categories
|
// All categories
|
||||||
contentEl.createEl("h3", { text: "📂 All categories" });
|
if (grouped.size > 1 || (grouped.size === 1 && !grouped.has("All"))) {
|
||||||
|
contentEl.createEl("h3", { text: "📂 All categories" });
|
||||||
|
}
|
||||||
|
|
||||||
for (const [category, types] of grouped.entries()) {
|
for (const [category, types] of grouped.entries()) {
|
||||||
if (category !== "All") {
|
// Show category heading if not "All" or if there are multiple categories
|
||||||
contentEl.createEl("h4", { text: category });
|
if (category !== "All" || grouped.size > 1) {
|
||||||
|
const categoryHeading = contentEl.createEl("h4", { text: category });
|
||||||
|
// Add description if available (from first entry in category)
|
||||||
|
const firstType = types[0];
|
||||||
|
if (firstType && firstType.description) {
|
||||||
|
categoryHeading.title = firstType.description;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = contentEl.createEl("div", { cls: "edge-type-list" });
|
const container = contentEl.createEl("div", { cls: "edge-type-list" });
|
||||||
|
|
||||||
for (const { canonical, aliases, displayName } of types) {
|
for (const { canonical, aliases, displayName, description } of types) {
|
||||||
const isProhibited = suggestions.prohibited.includes(canonical);
|
const isProhibited = suggestions.prohibited.includes(canonical);
|
||||||
const isTypical = suggestions.typical.includes(canonical);
|
const isTypical = suggestions.typical.includes(canonical);
|
||||||
|
|
||||||
|
|
@ -90,14 +112,21 @@ export class EdgeTypeChooserModal extends Modal {
|
||||||
if (isTypical) prefix = "⭐";
|
if (isTypical) prefix = "⭐";
|
||||||
if (isProhibited) prefix = "🚫";
|
if (isProhibited) prefix = "🚫";
|
||||||
|
|
||||||
|
// Display: canonical type (primary), aliases shown after selection
|
||||||
const btn = container.createEl("button", {
|
const btn = container.createEl("button", {
|
||||||
text: `${prefix} ${displayName}`,
|
text: `${prefix} ${canonical}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isProhibited) {
|
if (isProhibited) {
|
||||||
btn.addClass("prohibited");
|
btn.addClass("prohibited");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add description as tooltip/hover text
|
||||||
|
if (description) {
|
||||||
|
btn.title = description;
|
||||||
|
btn.setAttribute("data-description", description);
|
||||||
|
}
|
||||||
|
|
||||||
btn.onclick = () => {
|
btn.onclick = () => {
|
||||||
this.selectEdgeType(canonical, aliases);
|
this.selectEdgeType(canonical, aliases);
|
||||||
};
|
};
|
||||||
|
|
@ -122,12 +151,8 @@ export class EdgeTypeChooserModal extends Modal {
|
||||||
// No aliases, use canonical directly
|
// No aliases, use canonical directly
|
||||||
this.result = { edgeType: canonical };
|
this.result = { edgeType: canonical };
|
||||||
this.close();
|
this.close();
|
||||||
} else if (aliases.length === 1) {
|
|
||||||
// Single alias, use it
|
|
||||||
this.result = { edgeType: canonical, alias: aliases[0] };
|
|
||||||
this.close();
|
|
||||||
} else {
|
} else {
|
||||||
// Multiple aliases, show alias chooser
|
// Always show alias chooser if aliases exist (even for single alias)
|
||||||
this.selectedCanonical = canonical;
|
this.selectedCanonical = canonical;
|
||||||
this.showAliasChooser(aliases);
|
this.showAliasChooser(aliases);
|
||||||
}
|
}
|
||||||
|
|
@ -138,14 +163,26 @@ export class EdgeTypeChooserModal extends Modal {
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
|
|
||||||
contentEl.createEl("h2", { text: "Choose alias" });
|
contentEl.createEl("h2", { text: "Choose alias" });
|
||||||
contentEl.createEl("p", { text: `Multiple aliases available for ${this.selectedCanonical}` });
|
contentEl.createEl("p", { text: `Aliases available for ${this.selectedCanonical}` });
|
||||||
|
|
||||||
const container = contentEl.createEl("div", { cls: "alias-list" });
|
const container = contentEl.createEl("div", { cls: "alias-list" });
|
||||||
|
|
||||||
|
// Option: Use canonical (no alias)
|
||||||
|
const canonicalBtn = container.createEl("button", {
|
||||||
|
text: `${this.selectedCanonical} (canonical)`,
|
||||||
|
cls: "mod-cta",
|
||||||
|
});
|
||||||
|
canonicalBtn.onclick = () => {
|
||||||
|
if (this.selectedCanonical) {
|
||||||
|
this.result = { edgeType: this.selectedCanonical };
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// All aliases
|
||||||
for (const alias of aliases) {
|
for (const alias of aliases) {
|
||||||
const btn = container.createEl("button", {
|
const btn = container.createEl("button", {
|
||||||
text: alias,
|
text: alias,
|
||||||
cls: "mod-cta",
|
|
||||||
});
|
});
|
||||||
btn.onclick = () => {
|
btn.onclick = () => {
|
||||||
if (this.selectedCanonical) {
|
if (this.selectedCanonical) {
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,20 @@ export class InlineEdgeTypeModal extends Modal {
|
||||||
cls: "inline-edge-type-modal__link-info",
|
cls: "inline-edge-type-modal__link-info",
|
||||||
});
|
});
|
||||||
linkInfo.textContent = `Link: [[${this.linkBasename}]]`;
|
linkInfo.textContent = `Link: [[${this.linkBasename}]]`;
|
||||||
|
|
||||||
|
// Show selected type if one was chosen (e.g., from chooser)
|
||||||
|
if (this.selectedEdgeType) {
|
||||||
|
const selectedInfo = contentEl.createEl("div", {
|
||||||
|
cls: "inline-edge-type-modal__selected-info",
|
||||||
|
});
|
||||||
|
const displayType = this.selectedAlias || this.selectedEdgeType;
|
||||||
|
selectedInfo.textContent = `Ausgewählt: ${displayType}`;
|
||||||
|
selectedInfo.style.marginTop = "0.5em";
|
||||||
|
selectedInfo.style.padding = "0.5em";
|
||||||
|
selectedInfo.style.backgroundColor = "var(--background-modifier-hover)";
|
||||||
|
selectedInfo.style.borderRadius = "4px";
|
||||||
|
selectedInfo.style.fontWeight = "bold";
|
||||||
|
}
|
||||||
|
|
||||||
// Get recommendations
|
// Get recommendations
|
||||||
const recommendations = this.getRecommendations();
|
const recommendations = this.getRecommendations();
|
||||||
|
|
@ -80,10 +94,19 @@ export class InlineEdgeTypeModal extends Modal {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-select first typical if available
|
// Auto-select first typical if available (use canonical, alias selection comes later)
|
||||||
|
// Only auto-select if no type has been explicitly selected yet
|
||||||
if (typical.length > 0 && typical[0] && !this.selectedEdgeType) {
|
if (typical.length > 0 && typical[0] && !this.selectedEdgeType) {
|
||||||
this.selectedEdgeType = typical[0];
|
const firstTypical = typical[0];
|
||||||
this.result = { chosenRawType: typical[0], cancelled: false };
|
this.selectedEdgeType = firstTypical;
|
||||||
|
// Use canonical as default, alias will be selected in alias chooser if needed
|
||||||
|
this.result = { chosenRawType: firstTypical, cancelled: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a type was already selected (e.g., from chooser), ensure it's reflected in result
|
||||||
|
if (this.selectedEdgeType && !this.result) {
|
||||||
|
const rawType = this.selectedAlias || this.selectedEdgeType;
|
||||||
|
this.result = { chosenRawType: rawType, cancelled: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recommended chips (typical) - all are directly selectable, first one is preselected
|
// Recommended chips (typical) - all are directly selectable, first one is preselected
|
||||||
|
|
@ -100,21 +123,45 @@ export class InlineEdgeTypeModal extends Modal {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < typical.length; i++) {
|
for (let i = 0; i < typical.length; i++) {
|
||||||
const edgeType = typical[i];
|
const canonical = typical[i];
|
||||||
if (!edgeType) continue;
|
if (!canonical) continue;
|
||||||
|
|
||||||
|
// Get entry for description
|
||||||
|
let description: string | undefined = undefined;
|
||||||
|
let aliases: string[] = [];
|
||||||
|
if (this.vocabulary) {
|
||||||
|
const entry = this.vocabulary.byCanonical.get(canonical);
|
||||||
|
if (entry) {
|
||||||
|
description = entry.description;
|
||||||
|
aliases = entry.aliases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display: canonical type (primary)
|
||||||
const chip = chipsContainer.createEl("button", {
|
const chip = chipsContainer.createEl("button", {
|
||||||
cls: "inline-edge-type-modal__chip",
|
cls: "inline-edge-type-modal__chip",
|
||||||
text: edgeType,
|
text: canonical,
|
||||||
});
|
});
|
||||||
|
|
||||||
// First typical is preselected
|
// Add description as tooltip/hover text
|
||||||
if (i === 0) {
|
if (description) {
|
||||||
|
chip.title = description;
|
||||||
|
chip.setAttribute("data-description", description);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as selected if this is the currently selected type
|
||||||
|
const isSelected = this.selectedEdgeType === canonical;
|
||||||
|
if (isSelected || (i === 0 && !this.selectedEdgeType)) {
|
||||||
chip.addClass("mod-cta");
|
chip.addClass("mod-cta");
|
||||||
}
|
}
|
||||||
|
|
||||||
chip.onclick = () => {
|
chip.onclick = () => {
|
||||||
this.selectEdgeType(edgeType);
|
// If aliases exist, show alias chooser, otherwise use canonical
|
||||||
|
if (aliases.length > 0) {
|
||||||
|
this.showAliasChooser(canonical, aliases);
|
||||||
|
} else {
|
||||||
|
this.selectEdgeType(canonical);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,12 +180,38 @@ export class InlineEdgeTypeModal extends Modal {
|
||||||
cls: "inline-edge-type-modal__chips",
|
cls: "inline-edge-type-modal__chips",
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const edgeType of alternatives) {
|
for (const canonical of alternatives) {
|
||||||
|
// Get entry for description
|
||||||
|
let description: string | undefined = undefined;
|
||||||
|
let aliases: string[] = [];
|
||||||
|
if (this.vocabulary) {
|
||||||
|
const entry = this.vocabulary.byCanonical.get(canonical);
|
||||||
|
if (entry) {
|
||||||
|
description = entry.description;
|
||||||
|
aliases = entry.aliases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display: canonical type (primary)
|
||||||
const chip = chipsContainer.createEl("button", {
|
const chip = chipsContainer.createEl("button", {
|
||||||
cls: "inline-edge-type-modal__chip",
|
cls: "inline-edge-type-modal__chip",
|
||||||
text: edgeType,
|
text: canonical,
|
||||||
});
|
});
|
||||||
chip.onclick = () => this.selectEdgeType(edgeType);
|
|
||||||
|
// Add description as tooltip/hover text
|
||||||
|
if (description) {
|
||||||
|
chip.title = description;
|
||||||
|
chip.setAttribute("data-description", description);
|
||||||
|
}
|
||||||
|
|
||||||
|
chip.onclick = () => {
|
||||||
|
// If aliases exist, show alias chooser, otherwise use canonical
|
||||||
|
if (aliases.length > 0) {
|
||||||
|
this.showAliasChooser(canonical, aliases);
|
||||||
|
} else {
|
||||||
|
this.selectEdgeType(canonical);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,11 +228,28 @@ export class InlineEdgeTypeModal extends Modal {
|
||||||
cls: "inline-edge-type-modal__chips",
|
cls: "inline-edge-type-modal__chips",
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const edgeType of prohibited) {
|
for (const canonical of prohibited) {
|
||||||
|
// Get entry for description
|
||||||
|
let description: string | undefined = undefined;
|
||||||
|
if (this.vocabulary) {
|
||||||
|
const entry = this.vocabulary.byCanonical.get(canonical);
|
||||||
|
if (entry) {
|
||||||
|
description = entry.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display: canonical type (primary)
|
||||||
const chip = chipsContainer.createEl("button", {
|
const chip = chipsContainer.createEl("button", {
|
||||||
cls: "inline-edge-type-modal__chip",
|
cls: "inline-edge-type-modal__chip",
|
||||||
text: edgeType,
|
text: canonical,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add description as tooltip/hover text (even for disabled chips)
|
||||||
|
if (description) {
|
||||||
|
chip.title = description;
|
||||||
|
chip.setAttribute("data-description", description);
|
||||||
|
}
|
||||||
|
|
||||||
chip.disabled = true;
|
chip.disabled = true;
|
||||||
chip.addClass("mod-muted");
|
chip.addClass("mod-muted");
|
||||||
}
|
}
|
||||||
|
|
@ -183,10 +273,10 @@ export class InlineEdgeTypeModal extends Modal {
|
||||||
);
|
);
|
||||||
const choice: EdgeTypeChoice | null = await chooser.show();
|
const choice: EdgeTypeChoice | null = await chooser.show();
|
||||||
if (choice) {
|
if (choice) {
|
||||||
// Store the choice and close modal
|
// Store the choice and re-render to show selection
|
||||||
this.selectEdgeType(choice.edgeType, choice.alias);
|
this.selectEdgeType(choice.edgeType, choice.alias);
|
||||||
// Close this modal immediately after selection
|
// Re-render the modal to show the selected type
|
||||||
this.close();
|
this.onOpen();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -201,8 +291,18 @@ export class InlineEdgeTypeModal extends Modal {
|
||||||
cls: "mod-cta",
|
cls: "mod-cta",
|
||||||
});
|
});
|
||||||
okBtn.onclick = () => {
|
okBtn.onclick = () => {
|
||||||
// If no type selected yet, use preselected
|
// If no type selected yet, use preselected canonical
|
||||||
if (!this.result && this.selectedEdgeType) {
|
if (!this.result && this.selectedEdgeType) {
|
||||||
|
// Check if aliases exist - if so, show alias chooser first
|
||||||
|
if (this.vocabulary) {
|
||||||
|
const entry = this.vocabulary.byCanonical.get(this.selectedEdgeType);
|
||||||
|
if (entry && entry.aliases.length > 0) {
|
||||||
|
// Show alias chooser
|
||||||
|
this.showAliasChooser(this.selectedEdgeType, entry.aliases);
|
||||||
|
return; // Don't close yet, wait for alias selection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No aliases, use canonical directly
|
||||||
this.result = { chosenRawType: this.selectedEdgeType, cancelled: false };
|
this.result = { chosenRawType: this.selectedEdgeType, cancelled: false };
|
||||||
} else if (!this.result) {
|
} else if (!this.result) {
|
||||||
// No selection possible, treat as skip
|
// No selection possible, treat as skip
|
||||||
|
|
@ -249,27 +349,67 @@ export class InlineEdgeTypeModal extends Modal {
|
||||||
}
|
}
|
||||||
|
|
||||||
private selectEdgeType(edgeType: string, alias?: string | null): void {
|
private selectEdgeType(edgeType: string, alias?: string | null): void {
|
||||||
// Remove previous selection
|
// Store result - use alias if provided, otherwise use edgeType (canonical)
|
||||||
const chips = this.contentEl.querySelectorAll(".inline-edge-type-modal__chip");
|
|
||||||
const chipsArray = Array.from(chips);
|
|
||||||
for (const chip of chipsArray) {
|
|
||||||
if (chip instanceof HTMLElement) {
|
|
||||||
chip.removeClass("mod-cta");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark selected
|
|
||||||
const selectedChip = chipsArray.find(
|
|
||||||
(chip) => chip.textContent === edgeType || chip.textContent?.includes(edgeType)
|
|
||||||
);
|
|
||||||
if (selectedChip && selectedChip instanceof HTMLElement) {
|
|
||||||
selectedChip.addClass("mod-cta");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store result
|
|
||||||
this.selectedEdgeType = edgeType;
|
this.selectedEdgeType = edgeType;
|
||||||
this.selectedAlias = alias || null;
|
this.selectedAlias = alias || null;
|
||||||
this.result = { chosenRawType: edgeType, cancelled: false };
|
// Use alias if available, otherwise use canonical
|
||||||
|
const rawType = alias || edgeType;
|
||||||
|
this.result = { chosenRawType: rawType, cancelled: false };
|
||||||
|
|
||||||
|
// Note: Visual highlighting will be done in onOpen() when re-rendering
|
||||||
|
}
|
||||||
|
|
||||||
|
private showAliasChooser(canonical: string, aliases: string[]): void {
|
||||||
|
const { contentEl } = this;
|
||||||
|
|
||||||
|
// Store original content structure for restoration
|
||||||
|
const originalSections = Array.from(contentEl.children);
|
||||||
|
|
||||||
|
// Clear and show alias chooser
|
||||||
|
contentEl.empty();
|
||||||
|
contentEl.createEl("h2", { text: "Choose alias" });
|
||||||
|
contentEl.createEl("p", { text: `Aliases available for ${canonical}` });
|
||||||
|
|
||||||
|
const container = contentEl.createEl("div", { cls: "alias-list" });
|
||||||
|
container.style.display = "flex";
|
||||||
|
container.style.flexDirection = "column";
|
||||||
|
container.style.gap = "0.5em";
|
||||||
|
container.style.marginTop = "1em";
|
||||||
|
|
||||||
|
// Option: Use canonical (no alias)
|
||||||
|
const canonicalBtn = container.createEl("button", {
|
||||||
|
text: `${canonical} (canonical)`,
|
||||||
|
cls: "mod-cta",
|
||||||
|
});
|
||||||
|
canonicalBtn.style.width = "100%";
|
||||||
|
canonicalBtn.style.padding = "0.75em";
|
||||||
|
canonicalBtn.onclick = () => {
|
||||||
|
this.selectEdgeType(canonical);
|
||||||
|
// Restore original content by re-rendering
|
||||||
|
this.onOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
// All aliases
|
||||||
|
for (const alias of aliases) {
|
||||||
|
const btn = container.createEl("button", {
|
||||||
|
text: alias,
|
||||||
|
});
|
||||||
|
btn.style.width = "100%";
|
||||||
|
btn.style.padding = "0.75em";
|
||||||
|
btn.onclick = () => {
|
||||||
|
this.selectEdgeType(canonical, alias);
|
||||||
|
// Restore original content by re-rendering
|
||||||
|
this.onOpen();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const backBtn = container.createEl("button", { text: "← Back" });
|
||||||
|
backBtn.style.width = "100%";
|
||||||
|
backBtn.style.marginTop = "1em";
|
||||||
|
backBtn.onclick = () => {
|
||||||
|
// Restore original content by re-rendering
|
||||||
|
this.onOpen();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async openFullChooser(): Promise<void> {
|
private async openFullChooser(): Promise<void> {
|
||||||
|
|
@ -290,8 +430,10 @@ export class InlineEdgeTypeModal extends Modal {
|
||||||
const choice: EdgeTypeChoice | null = await chooser.show();
|
const choice: EdgeTypeChoice | null = await chooser.show();
|
||||||
|
|
||||||
if (choice) {
|
if (choice) {
|
||||||
|
// Store the choice and re-render to show selection
|
||||||
this.selectEdgeType(choice.edgeType, choice.alias);
|
this.selectEdgeType(choice.edgeType, choice.alias);
|
||||||
this.close();
|
// Re-render the modal to show the selected type
|
||||||
|
this.onOpen();
|
||||||
} else {
|
} else {
|
||||||
// User cancelled chooser, treat as skip
|
// User cancelled chooser, treat as skip
|
||||||
this.result = { chosenRawType: null, cancelled: false };
|
this.result = { chosenRawType: null, cancelled: false };
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import { EdgeTypeChooserModal, type EdgeTypeChoice } from "./EdgeTypeChooserModa
|
||||||
import { computeEdgeSuggestions } from "../mapping/schemaHelper";
|
import { computeEdgeSuggestions } from "../mapping/schemaHelper";
|
||||||
|
|
||||||
export type LinkPromptDecision =
|
export type LinkPromptDecision =
|
||||||
| { action: "keep"; edgeType: string }
|
| { action: "keep"; edgeType: string } // edgeType is the actual type (alias or canonical) to keep
|
||||||
| { action: "change"; edgeType: string; alias?: string }
|
| { action: "change"; edgeType: string; alias?: string } // edgeType is canonical, alias is the selected alias (if any)
|
||||||
| { action: "skip" };
|
| { action: "skip" };
|
||||||
|
|
||||||
export class LinkPromptModal extends Modal {
|
export class LinkPromptModal extends Modal {
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,20 @@ const BACKTICK_RE = /`([^`]+)`/g;
|
||||||
*/
|
*/
|
||||||
export function parseEdgeVocabulary(md: string): EdgeVocabulary {
|
export function parseEdgeVocabulary(md: string): EdgeVocabulary {
|
||||||
const lines = md.split(/\r?\n/);
|
const lines = md.split(/\r?\n/);
|
||||||
const byCanonical = new Map<string, { canonical: string; inverse?: string; aliases: string[] }>();
|
const byCanonical = new Map<string, { canonical: string; inverse?: string; aliases: string[]; description?: string; category?: string }>();
|
||||||
const aliasToCanonical = new Map<string, string>();
|
const aliasToCanonical = new Map<string, string>();
|
||||||
|
|
||||||
let skippedRows = 0;
|
let skippedRows = 0;
|
||||||
|
let currentCategory: string | null = null; // Track current H3 category
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
// Detect H3 headings (###) as category separators
|
||||||
|
const h3Match = line.match(/^###\s+(.+)$/);
|
||||||
|
if (h3Match && h3Match[1]) {
|
||||||
|
currentCategory = h3Match[1].trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip header separator rows (e.g., "| :--- | :--- |")
|
// Skip header separator rows (e.g., "| :--- | :--- |")
|
||||||
if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) {
|
if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -35,6 +43,12 @@ export function parseEdgeVocabulary(md: string): EdgeVocabulary {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip header rows (contains "Canonical", "System-Typ", "Beschreibung", "Kategorie", etc.)
|
||||||
|
// Check for common header keywords
|
||||||
|
if (/canonical|system-typ|beschreibung|kategorie|category|description|inverser|aliasse/i.test(line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract all backticked tokens
|
// Extract all backticked tokens
|
||||||
const tokens: string[] = [];
|
const tokens: string[] = [];
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
|
|
@ -54,6 +68,49 @@ export function parseEdgeVocabulary(md: string): EdgeVocabulary {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse table cells (split by |, skip first and last empty cells)
|
||||||
|
const cells = line.split("|").map(c => c.trim()).filter(c => c);
|
||||||
|
|
||||||
|
// Extract description and category from cells
|
||||||
|
// Expected order: Canonical | Inverse | Aliases | Description | Category (optional)
|
||||||
|
let description: string | undefined = undefined;
|
||||||
|
let category: string | undefined = undefined;
|
||||||
|
|
||||||
|
// Try to extract from cells after aliases (index 3+)
|
||||||
|
// Description is usually the first text cell after aliases
|
||||||
|
// Category might be in brackets, short, or in a separate column
|
||||||
|
for (let i = 3; i < cells.length; i++) {
|
||||||
|
const cell = cells[i];
|
||||||
|
if (!cell || !cell.trim()) continue;
|
||||||
|
|
||||||
|
const trimmed = cell.trim();
|
||||||
|
|
||||||
|
// Check if this looks like a category:
|
||||||
|
// - Short text (< 40 chars)
|
||||||
|
// - Might be in brackets [Category]
|
||||||
|
// - Might be all caps
|
||||||
|
// - Might match category pattern
|
||||||
|
const looksLikeCategory =
|
||||||
|
trimmed.length < 40 && (
|
||||||
|
/^\[.+\]$/.test(trimmed) || // [Category]
|
||||||
|
trimmed === trimmed.toUpperCase() || // ALL CAPS
|
||||||
|
/^[A-ZÄÖÜ][a-zäöüß]+(\s+[A-ZÄÖÜ][a-zäöüß]+)*$/.test(trimmed) // Title Case
|
||||||
|
);
|
||||||
|
|
||||||
|
if (looksLikeCategory && !category) {
|
||||||
|
// Remove brackets if present
|
||||||
|
category = trimmed.replace(/^\[|\]$/g, "");
|
||||||
|
} else if (!description && trimmed.length > 0) {
|
||||||
|
// First substantial cell is likely description
|
||||||
|
// Remove markdown formatting but keep content
|
||||||
|
description = trimmed
|
||||||
|
.replace(/\*\*/g, "") // Remove bold
|
||||||
|
.replace(/\*/g, "") // Remove italic
|
||||||
|
.replace(/`/g, "") // Remove code
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if aliases cell contains "(Kein Alias)"
|
// Check if aliases cell contains "(Kein Alias)"
|
||||||
const hasNoAliases = /\(Kein Alias\)/i.test(line);
|
const hasNoAliases = /\(Kein Alias\)/i.test(line);
|
||||||
|
|
||||||
|
|
@ -76,11 +133,16 @@ export function parseEdgeVocabulary(md: string): EdgeVocabulary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store canonical entry
|
// Store canonical entry with description and category
|
||||||
|
// Use currentCategory from H3 heading if available, otherwise use extracted category
|
||||||
|
const finalCategory = currentCategory || category;
|
||||||
|
|
||||||
byCanonical.set(canonical, {
|
byCanonical.set(canonical, {
|
||||||
canonical,
|
canonical,
|
||||||
inverse,
|
inverse,
|
||||||
aliases,
|
aliases,
|
||||||
|
description,
|
||||||
|
category: finalCategory,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build alias-to-canonical mapping (case-insensitive keys)
|
// Build alias-to-canonical mapping (case-insensitive keys)
|
||||||
|
|
@ -95,7 +157,9 @@ export function parseEdgeVocabulary(md: string): EdgeVocabulary {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skippedRows > 0) {
|
if (skippedRows > 0) {
|
||||||
console.warn(`parseEdgeVocabulary: Skipped ${skippedRows} rows with insufficient tokens`);
|
// Only warn if there are actually problematic rows (not just header/separator rows)
|
||||||
|
// Header and separator rows are expected and should not trigger warnings
|
||||||
|
console.debug(`parseEdgeVocabulary: Skipped ${skippedRows} data rows with insufficient tokens (this is normal if the file contains empty or malformed table rows)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ export interface EdgeTypeEntry {
|
||||||
canonical: CanonicalEdgeType;
|
canonical: CanonicalEdgeType;
|
||||||
inverse?: CanonicalEdgeType;
|
inverse?: CanonicalEdgeType;
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
|
description?: string; // Description from vocabulary table
|
||||||
|
category?: string; // Category/group from vocabulary table
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EdgeVocabulary {
|
export interface EdgeVocabulary {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user