mirror of
https://github.com/nejimakibird/model-weave.git
synced 2026-07-22 06:51:24 +00:00
feat: add rich hover and data ref diagnostics for flow diagrams
This commit is contained in:
parent
a34a6254d1
commit
9240edd8b4
9 changed files with 1169 additions and 60 deletions
351
main.js
351
main.js
|
|
@ -2974,6 +2974,8 @@ function localizeDiagnosticMessage(message, language) {
|
|||
[/^unresolved DFD flow source "([^"]+)"$/, (_match, source) => `DFD flow \u306E source "${source}" \u304C\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002`],
|
||||
[/^unresolved DFD flow target "([^"]+)"$/, (_match, target) => `DFD flow \u306E target "${target}" \u304C\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002`],
|
||||
[/^unresolved DFD object ref "([^"]+)"$/, (_match, ref) => `DFD\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306E\u53C2\u7167 "${ref}" \u306E\u53C2\u7167\u5148\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002ID\u307E\u305F\u306F\u30D5\u30A1\u30A4\u30EB\u540D\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`],
|
||||
[/^DFD flow data reference "([^"]+)" could not be resolved\. Check the data\/model id or file name\.$/, (_match, ref) => `DFD flow data reference "${ref}" \u306E\u53C2\u7167\u5148\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002data/model \u306E id \u307E\u305F\u306F\u30D5\u30A1\u30A4\u30EB\u540D\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`],
|
||||
[/^Flow Diagram flow data reference "([^"]+)" could not be resolved\. Check the data\/model id or file name\.$/, (_match, ref) => `Flow Diagram flow data reference "${ref}" \u306E\u53C2\u7167\u5148\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002data/model \u306E id \u307E\u305F\u306F\u30D5\u30A1\u30A4\u30EB\u540D\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`],
|
||||
[/^DFD object ref "([^"]+)" could not be resolved\. Check the ID or file name\.$/, (_match, ref) => `DFD\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306E\u53C2\u7167 "${ref}" \u306E\u53C2\u7167\u5148\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002ID\u307E\u305F\u306F\u30D5\u30A1\u30A4\u30EB\u540D\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`],
|
||||
[/^frontmatter parse error: unexpected list item "([^"]+)"$/, (_match, value) => `frontmatter \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u4E88\u671F\u3057\u306A\u3044\u30EA\u30B9\u30C8\u9805\u76EE\u3067\u3059: "${value}"`],
|
||||
[/^frontmatter parse error: malformed line "([^"]+)"$/, (_match, value) => `frontmatter \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u884C\u306E\u5F62\u5F0F\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044: "${value}"`],
|
||||
|
|
@ -4764,17 +4766,13 @@ function resolveDfdDiagramRelations(diagram, index) {
|
|||
warnings.push(createDfdFlowShapeWarning(diagram.path, context, "datastore -> datastore"));
|
||||
}
|
||||
}
|
||||
const flowData = resolveDfdFlowDataDisplay(flow.data, index, { suppressUnresolvedWarning: isFlowDiagram });
|
||||
if (flowData.warning) {
|
||||
warnings.push({
|
||||
code: "unresolved-reference",
|
||||
message: flowData.warning,
|
||||
severity: "warning",
|
||||
path: diagram.path,
|
||||
field: "Flows",
|
||||
context
|
||||
});
|
||||
}
|
||||
const flowData = resolveDfdFlowDataDisplay(flow.data, index);
|
||||
warnings.push(...resolveDfdFlowDataReferenceWarnings(
|
||||
diagram,
|
||||
flow.data,
|
||||
index,
|
||||
context
|
||||
));
|
||||
edges.push({
|
||||
id: flow.id,
|
||||
source: sourceEntry.node.id,
|
||||
|
|
@ -5006,6 +5004,9 @@ function resolveDfdDiagramObjects(diagram, index, domainContext) {
|
|||
domain,
|
||||
rowIndex: entry.rowIndex,
|
||||
local: !ref,
|
||||
refReference: resolvedIdentity?.parsed,
|
||||
refModelPath: resolvedIdentity?.resolvedModel?.path,
|
||||
refModelType: resolvedIdentity?.resolvedModel?.fileType,
|
||||
compatibilityMode: entry.compatibilityMode
|
||||
},
|
||||
object: resolvedObject
|
||||
|
|
@ -5053,7 +5054,25 @@ function resolveDfdFlowEndpoint(value, registry, index) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function resolveDfdFlowDataDisplay(rawValue, index, options = {}) {
|
||||
function resolveDfdFlowDataReferenceWarnings(diagram, rawValue, index, context) {
|
||||
const wikilinks = rawValue ? extractWikilinkReferences(rawValue) : [];
|
||||
if (wikilinks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const diagramLabel = diagram.schema === "flow_diagram" ? "Flow Diagram" : "DFD";
|
||||
return wikilinks.filter((reference) => !resolveReferenceIdentity(reference, index).resolvedModel).map((reference) => ({
|
||||
code: "unresolved-reference",
|
||||
message: `${diagramLabel} flow data reference "${reference}" could not be resolved. Check the data/model id or file name.`,
|
||||
severity: "warning",
|
||||
path: diagram.path,
|
||||
field: "data",
|
||||
context: {
|
||||
...context,
|
||||
referenceValue: reference
|
||||
}
|
||||
}));
|
||||
}
|
||||
function resolveDfdFlowDataDisplay(rawValue, index) {
|
||||
const trimmed = rawValue?.trim();
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
|
|
@ -5089,8 +5108,7 @@ function resolveDfdFlowDataDisplay(rawValue, index, options = {}) {
|
|||
if (reference.target) {
|
||||
return {
|
||||
label: getReferenceDisplayName(trimmed),
|
||||
reference,
|
||||
warning: options.suppressUnresolvedWarning ? void 0 : `unresolved flow data reference "${trimmed}"`
|
||||
reference
|
||||
};
|
||||
}
|
||||
return { label: getReferenceDisplayName(trimmed), reference };
|
||||
|
|
@ -16184,7 +16202,7 @@ function attachMermaidNodeInteractions(options) {
|
|||
}
|
||||
function attachGraphElementHoverPreview(options) {
|
||||
const fallback = options.rootEl ?? getElementHoverFallback(options.targetEl);
|
||||
if (!fallback || !options.target.linktext || !options.target.sourcePath) {
|
||||
if (!fallback || !canShowGraphInteractionHover(options.target)) {
|
||||
return () => void 0;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
|
|
@ -16210,7 +16228,7 @@ function attachGraphElementHoverPreview(options) {
|
|||
debugName: options.debugName,
|
||||
isDebugEnabled: options.isDebugEnabled
|
||||
}, fallback, event, false);
|
||||
const hoverLinkTarget = triggerGraphInteractionHoverPreview(
|
||||
const hoverLinkTarget = triggerGraphInteractionHover(
|
||||
options.app,
|
||||
source,
|
||||
resolveGraphHoverParent(options.targetEl, fallback, options.hoverParent),
|
||||
|
|
@ -16269,7 +16287,11 @@ function clearGraphHoverState(state, action, debug, rootEl, event, suppressSynth
|
|||
state.activeHoverSourcePath = "";
|
||||
state.activeHoverTargetEl = null;
|
||||
if (activeHoverTargetEl && !suppressSyntheticLeave) {
|
||||
dispatchGraphHoverTargetLeave(activeHoverTargetEl);
|
||||
if (isGraphFallbackHoverCard(activeHoverTargetEl)) {
|
||||
activeHoverTargetEl.remove();
|
||||
} else {
|
||||
dispatchGraphHoverTargetLeave(activeHoverTargetEl);
|
||||
}
|
||||
} else if (hadActiveNode && !suppressSyntheticLeave) {
|
||||
dispatchGraphHoverTargetLeave(rootEl);
|
||||
}
|
||||
|
|
@ -16331,24 +16353,29 @@ function getMermaidNodeInteractionFromEvent(event, interactions) {
|
|||
function findMermaidNodeInteractionByTarget(interactions, target) {
|
||||
return interactions.find((interaction) => interaction.target === target) ?? null;
|
||||
}
|
||||
function getGraphInteractionNativeTooltipText(target, fallbackTitle) {
|
||||
return target.nativeTooltip ?? fallbackTitle ?? target.label ?? target.linktext;
|
||||
}
|
||||
function setMermaidNodeTitle(nodeEl, target, formatTitle) {
|
||||
const titleText = formatTitle?.(target) ?? target.label ?? target.linktext;
|
||||
const existingTitle = nodeEl.querySelector("title");
|
||||
const titleText = getGraphInteractionNativeTooltipText(target, formatTitle?.(target));
|
||||
if (!titleText) {
|
||||
existingTitle?.remove();
|
||||
return;
|
||||
}
|
||||
const doc = nodeEl.ownerDocument;
|
||||
const title = nodeEl.querySelector("title") ?? doc.createElementNS("http://www.w3.org/2000/svg", "title");
|
||||
const title = existingTitle ?? doc.createElementNS("http://www.w3.org/2000/svg", "title");
|
||||
title.textContent = titleText;
|
||||
if (!title.parentElement) {
|
||||
nodeEl.prepend(title);
|
||||
}
|
||||
}
|
||||
function triggerMermaidNodeHoverPreview(options, source, targetEl, target, event, stateDebug) {
|
||||
if (!target.linktext || !target.sourcePath) {
|
||||
if (!canShowGraphInteractionHover(target)) {
|
||||
return null;
|
||||
}
|
||||
const hoverParent = resolveGraphHoverParent(targetEl, options.rootEl, options.hoverParent);
|
||||
return triggerGraphInteractionHoverPreview(
|
||||
return triggerGraphInteractionHover(
|
||||
options.app,
|
||||
source,
|
||||
hoverParent,
|
||||
|
|
@ -16362,6 +16389,83 @@ function triggerMermaidNodeHoverPreview(options, source, targetEl, target, event
|
|||
stateDebug
|
||||
);
|
||||
}
|
||||
function canShowGraphInteractionHover(target) {
|
||||
return Boolean(
|
||||
target.previewLinktext && target.sourcePath || target.linktext && target.sourcePath && !target.hoverRows?.length || target.hoverRows?.length
|
||||
);
|
||||
}
|
||||
function triggerGraphInteractionHover(app, source, hoverParent, targetEl, target, event, debug, stateDebug) {
|
||||
const previewLinktext = getGraphInteractionPreviewLinktext(target);
|
||||
if (previewLinktext) {
|
||||
const previewTarget = { ...target, linktext: previewLinktext };
|
||||
return triggerGraphInteractionHoverPreview(
|
||||
app,
|
||||
source,
|
||||
hoverParent,
|
||||
targetEl,
|
||||
previewTarget,
|
||||
event,
|
||||
debug,
|
||||
stateDebug
|
||||
) ?? triggerGraphInteractionFallbackHover(hoverParent, target, event);
|
||||
}
|
||||
return triggerGraphInteractionFallbackHover(hoverParent, target, event);
|
||||
}
|
||||
function getGraphInteractionPreviewLinktext(target) {
|
||||
const explicit = target.previewLinktext?.trim();
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
if (target.hoverRows?.length) {
|
||||
return null;
|
||||
}
|
||||
const linktext = target.linktext?.trim();
|
||||
return linktext && target.sourcePath ? linktext : null;
|
||||
}
|
||||
function triggerGraphInteractionFallbackHover(hoverParent, target, event) {
|
||||
if (!target.hoverRows?.length) {
|
||||
return null;
|
||||
}
|
||||
const card = createGraphFallbackHoverCard(hoverParent, target, event);
|
||||
return { targetEl: card, reusableAnchorTargetUsed: false };
|
||||
}
|
||||
function createGraphFallbackHoverCard(hoverParent, target, event) {
|
||||
const doc = hoverParent.ownerDocument;
|
||||
const existing = hoverParent.querySelectorAll(".model-weave-graph-hover-card");
|
||||
existing.forEach((element) => element.remove());
|
||||
const card = doc.createElement("div");
|
||||
card.className = "model-weave-graph-hover-card";
|
||||
card.setAttribute("role", "tooltip");
|
||||
const title = doc.createElement("div");
|
||||
title.className = "model-weave-graph-hover-card-title";
|
||||
title.textContent = target.hoverTitle ?? target.label ?? "Model Weave";
|
||||
card.appendChild(title);
|
||||
const rows = doc.createElement("dl");
|
||||
rows.className = "model-weave-graph-hover-card-rows";
|
||||
for (const row of target.hoverRows ?? []) {
|
||||
const term = doc.createElement("dt");
|
||||
term.textContent = row.label;
|
||||
const description = doc.createElement("dd");
|
||||
description.textContent = row.value?.trim() || "-";
|
||||
rows.appendChild(term);
|
||||
rows.appendChild(description);
|
||||
}
|
||||
card.appendChild(rows);
|
||||
hoverParent.appendChild(card);
|
||||
positionGraphFallbackHoverCard(card, hoverParent, event);
|
||||
return card;
|
||||
}
|
||||
function positionGraphFallbackHoverCard(card, hoverParent, event) {
|
||||
const view = hoverParent.ownerDocument.defaultView;
|
||||
const viewportWidth = view?.innerWidth ?? event.clientX + 360;
|
||||
const x = Math.min(Math.max(12, event.clientX + 14), Math.max(12, viewportWidth - 380));
|
||||
const y = Math.max(12, event.clientY + 14);
|
||||
card.style.left = `${x}px`;
|
||||
card.style.top = `${y}px`;
|
||||
}
|
||||
function isGraphFallbackHoverCard(element) {
|
||||
return element instanceof HTMLElement && element.classList.contains("model-weave-graph-hover-card");
|
||||
}
|
||||
function triggerGraphInteractionHoverPreview(app, source, hoverParent, targetEl, target, event, debug, stateDebug) {
|
||||
if (!target.linktext || !target.sourcePath) {
|
||||
return null;
|
||||
|
|
@ -18536,9 +18640,11 @@ function renderDfdMermaidDiagram(diagram, options) {
|
|||
shell3.root.appendChild(createObjectDetails(diagram, options?.dfdDetailLabels));
|
||||
shell3.root.appendChild(createFlowDetails(diagram.edges, options?.dfdDetailLabels));
|
||||
}
|
||||
const interactionTargets = buildDfdMermaidInteractionTargets(
|
||||
const sourcePath = options?.interactionSourcePath ?? diagram.diagram.path;
|
||||
const flowHoverMetadata = isFlowDiagramModel(diagram.diagram) ? buildFlowDiagramHoverMetadata(diagram, sourcePath) : null;
|
||||
const interactionTargets = flowHoverMetadata?.objects ?? buildDfdMermaidInteractionTargets(
|
||||
diagram,
|
||||
options?.interactionSourcePath ?? diagram.diagram.path
|
||||
sourcePath
|
||||
);
|
||||
const ready = renderMermaidSourceIntoShell(shell3, {
|
||||
source: buildDfdMermaidSource(diagram, options?.colorScheme),
|
||||
|
|
@ -18553,6 +18659,14 @@ function renderDfdMermaidDiagram(diagram, options) {
|
|||
sourcePanelCopyLabel: options?.sourcePanelCopyLabel,
|
||||
showRenderDebug: !options?.forExport && options?.showMermaidRenderDebug === true
|
||||
}).then(() => {
|
||||
if (!options?.forExport && options?.app && flowHoverMetadata) {
|
||||
attachFlowDiagramFlowHoverPreviews(
|
||||
shell3.surface,
|
||||
flowHoverMetadata.flows,
|
||||
options.app,
|
||||
options?.showMermaidRenderDebug === true
|
||||
);
|
||||
}
|
||||
if (!options?.forExport && options?.app && interactionTargets.length > 0) {
|
||||
attachMermaidNodeInteractions({
|
||||
app: options.app,
|
||||
|
|
@ -18562,8 +18676,19 @@ function renderDfdMermaidDiagram(diagram, options) {
|
|||
nodeClassName: "model-weave-mermaid-interactive-node",
|
||||
dragThreshold: 6,
|
||||
isDebugEnabled: () => options?.showMermaidRenderDebug === true,
|
||||
debugName: "DFD Mermaid",
|
||||
formatTitle: (target) => target.label ? `${target.label} (${target.targetType ?? "model"})` : target.linktext
|
||||
debugName: isFlowDiagramModel(diagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
|
||||
formatTitle: (target) => target.label ? `${target.label} (${target.targetType ?? "model"})` : target.linktext,
|
||||
openLinkText: isFlowDiagramModel(diagram.diagram) ? (target, event) => {
|
||||
const linktext = getFlowDiagramOpenLinkText(target);
|
||||
if (!linktext) {
|
||||
return;
|
||||
}
|
||||
return options.app?.workspace.openLinkText(
|
||||
linktext,
|
||||
target.sourcePath,
|
||||
event.ctrlKey || event.metaKey
|
||||
);
|
||||
} : void 0
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
|
|
@ -18579,6 +18704,182 @@ function renderDfdMermaidDiagram(diagram, options) {
|
|||
setMermaidRenderReadyPromise(shell3.root, ready);
|
||||
return shell3.root;
|
||||
}
|
||||
function buildFlowDiagramHoverMetadata(diagram, sourcePath) {
|
||||
if (!isFlowDiagramModel(diagram.diagram)) {
|
||||
return { objects: [], flows: [] };
|
||||
}
|
||||
const objects = diagram.nodes.map((node) => {
|
||||
const rows = buildFlowDiagramObjectHoverRows(node);
|
||||
const hoverTitle = "Flow Object";
|
||||
const target = {
|
||||
mermaidId: toMermaidNodeId(node.id),
|
||||
linktext: getFlowDiagramFallbackLinktext(sourcePath),
|
||||
sourcePath,
|
||||
label: node.label ?? node.id,
|
||||
kind: "flow-diagram-object",
|
||||
targetType: "flow_diagram_object",
|
||||
filePath: getStringMetadata(node.metadata, "refModelPath"),
|
||||
modelId: node.id,
|
||||
modelType: "flow-diagram",
|
||||
nodeId: node.id,
|
||||
hoverTitle,
|
||||
hoverRows: rows,
|
||||
hoverText: formatHoverText(hoverTitle, rows),
|
||||
previewLinktext: getResolvedPreviewLinktext(node.ref, getStringMetadata(node.metadata, "refModelPath")),
|
||||
nativeTooltip: formatFlowDiagramObjectTooltip(node)
|
||||
};
|
||||
return target;
|
||||
});
|
||||
const flows = diagram.edges.map((edge, index) => {
|
||||
const data = getStringMetadata(edge.metadata, "dataRaw") ?? edge.label;
|
||||
const rows = buildFlowDiagramFlowHoverRows(edge, data);
|
||||
const hoverTitle = "Flow";
|
||||
return {
|
||||
mermaidId: toFlowDiagramFlowMermaidId(edge, index),
|
||||
linktext: getFlowDiagramFallbackLinktext(sourcePath),
|
||||
sourcePath,
|
||||
edgeId: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.label,
|
||||
data,
|
||||
kind: "flow-diagram-flow",
|
||||
targetType: "flow_diagram_flow",
|
||||
modelId: edge.id,
|
||||
modelType: "flow-diagram",
|
||||
filePath: getStringMetadata(edge.metadata, "dataModelPath"),
|
||||
hoverTitle,
|
||||
hoverRows: rows,
|
||||
hoverText: formatHoverText(hoverTitle, rows),
|
||||
previewLinktext: getResolvedPreviewLinktext(data, getStringMetadata(edge.metadata, "dataModelPath")),
|
||||
nativeTooltip: formatFlowDiagramFlowTooltip(edge, data)
|
||||
};
|
||||
});
|
||||
return { objects, flows };
|
||||
}
|
||||
function formatFlowDiagramObjectTooltip(node) {
|
||||
const lines = [
|
||||
`Flow Object: ${node.id}`,
|
||||
node.label,
|
||||
`kind: ${typeof node.kind === "string" ? node.kind : "-"}`,
|
||||
`domain: ${getStringMetadata(node.metadata, "domain") ?? "-"}`,
|
||||
`ref: ${node.ref?.trim() || "-"}`
|
||||
];
|
||||
const notes = formatDiagramEdgeNotes(node.metadata?.notes);
|
||||
if (notes) {
|
||||
lines.push(notes);
|
||||
}
|
||||
return lines.filter((line) => Boolean(line && line.trim())).join("\n");
|
||||
}
|
||||
function formatFlowDiagramFlowTooltip(edge, data) {
|
||||
const lines = [
|
||||
`Flow: ${edge.id ?? "-"}`,
|
||||
`${edge.source} -> ${edge.target}`,
|
||||
`data: ${data?.trim() || "-"}`
|
||||
];
|
||||
const notes = formatDiagramEdgeNotes(edge.metadata?.notes);
|
||||
if (notes) {
|
||||
lines.push(notes);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
function buildFlowDiagramObjectHoverRows(node) {
|
||||
return [
|
||||
{ label: "id", value: node.id },
|
||||
{ label: "label", value: node.label },
|
||||
{ label: "kind", value: typeof node.kind === "string" ? node.kind : void 0 },
|
||||
{ label: "domain", value: getStringMetadata(node.metadata, "domain") },
|
||||
{ label: "ref", value: node.ref },
|
||||
{ label: "notes", value: formatDiagramEdgeNotes(node.metadata?.notes) }
|
||||
];
|
||||
}
|
||||
function buildFlowDiagramFlowHoverRows(edge, data) {
|
||||
return [
|
||||
{ label: "id", value: edge.id },
|
||||
{ label: "from", value: edge.source },
|
||||
{ label: "to", value: edge.target },
|
||||
{ label: "data", value: data },
|
||||
{ label: "notes", value: formatDiagramEdgeNotes(edge.metadata?.notes) }
|
||||
];
|
||||
}
|
||||
function formatHoverText(title, rows) {
|
||||
return [
|
||||
title,
|
||||
...rows.map((row) => `${row.label}: ${formatHoverValue(row.value)}`)
|
||||
].join("\n");
|
||||
}
|
||||
function formatHoverValue(value) {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed || "-";
|
||||
}
|
||||
function getStringMetadata(metadata, key) {
|
||||
const value = metadata?.[key];
|
||||
return typeof value === "string" ? value : void 0;
|
||||
}
|
||||
function toFlowDiagramFlowMermaidId(edge, index) {
|
||||
const id = edge.id?.trim() || `${edge.source}_${edge.target}_${index + 1}`;
|
||||
return `FLOW_${index + 1}_${toMermaidNodeId(id)}`;
|
||||
}
|
||||
function getResolvedPreviewLinktext(rawReference, resolvedPath) {
|
||||
if (!resolvedPath) {
|
||||
return void 0;
|
||||
}
|
||||
const trimmed = rawReference?.trim();
|
||||
if (!trimmed) {
|
||||
return void 0;
|
||||
}
|
||||
const parsed = parseReferenceValue(trimmed);
|
||||
return parsed?.kind === "wikilink" || parsed?.kind === "markdown_link" ? resolvedPath : void 0;
|
||||
}
|
||||
function getFlowDiagramFallbackLinktext(sourcePath) {
|
||||
return sourcePath;
|
||||
}
|
||||
function getFlowDiagramOpenLinkText(target) {
|
||||
const linktext = target.previewLinktext?.trim() || target.filePath?.trim();
|
||||
return linktext ? linktext : null;
|
||||
}
|
||||
function attachFlowDiagramFlowHoverPreviews(rootEl, flowTargets, app, showMermaidRenderDebug) {
|
||||
if (flowTargets.length === 0) {
|
||||
return;
|
||||
}
|
||||
const svg = rootEl.querySelector("svg");
|
||||
if (!svg) {
|
||||
return;
|
||||
}
|
||||
const edgeLabels = Array.from(svg.querySelectorAll("g.edgeLabel"));
|
||||
flowTargets.forEach((target, index) => {
|
||||
const labelEl = edgeLabels[index];
|
||||
if (!labelEl) {
|
||||
return;
|
||||
}
|
||||
setSvgNativeTooltip(labelEl, target.nativeTooltip);
|
||||
labelEl.addClass("model-weave-mermaid-interactive-flow");
|
||||
labelEl.setAttribute("data-model-weave-flow-id", target.edgeId ?? target.mermaidId);
|
||||
attachGraphElementHoverPreview({
|
||||
app,
|
||||
targetEl: labelEl,
|
||||
target,
|
||||
rootEl,
|
||||
source: "model-weave",
|
||||
isDebugEnabled: () => showMermaidRenderDebug,
|
||||
debugName: "Flow Diagram Mermaid Flow"
|
||||
});
|
||||
});
|
||||
}
|
||||
function setSvgNativeTooltip(element, text) {
|
||||
const existingTitle = element.querySelector("title");
|
||||
const trimmed = text?.trim();
|
||||
if (!trimmed) {
|
||||
existingTitle?.remove();
|
||||
element.removeAttribute("title");
|
||||
return;
|
||||
}
|
||||
const title = existingTitle ?? element.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "title");
|
||||
title.textContent = trimmed;
|
||||
if (!title.parentElement) {
|
||||
element.prepend(title);
|
||||
}
|
||||
}
|
||||
function buildDfdMermaidInteractionTargets(diagram, sourcePath) {
|
||||
return diagram.nodes.map((node) => {
|
||||
const object = getDfdObject(node);
|
||||
|
|
|
|||
|
|
@ -1445,6 +1445,8 @@ export function localizeDiagnosticMessage(message: string, language?: string): s
|
|||
[/^unresolved DFD flow source "([^"]+)"$/, (_match, source) => `DFD flow の source "${source}" が解決できません。`],
|
||||
[/^unresolved DFD flow target "([^"]+)"$/, (_match, target) => `DFD flow の target "${target}" が解決できません。`],
|
||||
[/^unresolved DFD object ref "([^"]+)"$/, (_match, ref) => `DFDオブジェクトの参照 "${ref}" の参照先が見つかりません。IDまたはファイル名を確認してください。`],
|
||||
[/^DFD flow data reference "([^"]+)" could not be resolved\. Check the data\/model id or file name\.$/, (_match, ref) => `DFD flow data reference "${ref}" の参照先が見つかりません。data/model の id またはファイル名を確認してください。`],
|
||||
[/^Flow Diagram flow data reference "([^"]+)" could not be resolved\. Check the data\/model id or file name\.$/, (_match, ref) => `Flow Diagram flow data reference "${ref}" の参照先が見つかりません。data/model の id またはファイル名を確認してください。`],
|
||||
[/^DFD object ref "([^"]+)" could not be resolved\. Check the ID or file name\.$/, (_match, ref) => `DFDオブジェクトの参照 "${ref}" の参照先が見つかりません。IDまたはファイル名を確認してください。`],
|
||||
[/^frontmatter parse error: unexpected list item "([^"]+)"$/, (_match, value) => `frontmatter の解析に失敗しました。予期しないリスト項目です: "${value}"`],
|
||||
[/^frontmatter parse error: malformed line "([^"]+)"$/, (_match, value) => `frontmatter の解析に失敗しました。行の形式を確認してください: "${value}"`],
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { mergeDomainDiagramSources } from "./domain-diagram-resolver";
|
|||
import { validateDomainEntries } from "../parsers/domains-parser";
|
||||
import {
|
||||
buildReferenceIdentityKeys,
|
||||
extractWikilinkReferences,
|
||||
findModelByReference,
|
||||
getReferenceDisplayName,
|
||||
parseReferenceValue,
|
||||
|
|
@ -234,17 +235,13 @@ function resolveDfdDiagramRelations(
|
|||
}
|
||||
}
|
||||
|
||||
const flowData = resolveDfdFlowDataDisplay(flow.data, index, { suppressUnresolvedWarning: isFlowDiagram });
|
||||
if (flowData.warning) {
|
||||
warnings.push({
|
||||
code: "unresolved-reference",
|
||||
message: flowData.warning,
|
||||
severity: "warning",
|
||||
path: diagram.path,
|
||||
field: "Flows",
|
||||
context
|
||||
});
|
||||
}
|
||||
const flowData = resolveDfdFlowDataDisplay(flow.data, index);
|
||||
warnings.push(...resolveDfdFlowDataReferenceWarnings(
|
||||
diagram,
|
||||
flow.data,
|
||||
index,
|
||||
context
|
||||
));
|
||||
|
||||
edges.push({
|
||||
id: flow.id,
|
||||
|
|
@ -536,6 +533,9 @@ function resolveDfdDiagramObjects(
|
|||
domain,
|
||||
rowIndex: entry.rowIndex,
|
||||
local: !ref,
|
||||
refReference: resolvedIdentity?.parsed,
|
||||
refModelPath: resolvedIdentity?.resolvedModel?.path,
|
||||
refModelType: resolvedIdentity?.resolvedModel?.fileType,
|
||||
compatibilityMode: entry.compatibilityMode
|
||||
},
|
||||
object: resolvedObject
|
||||
|
|
@ -599,15 +599,40 @@ function resolveDfdFlowEndpoint(
|
|||
return null;
|
||||
}
|
||||
|
||||
function resolveDfdFlowDataDisplay(
|
||||
function resolveDfdFlowDataReferenceWarnings(
|
||||
diagram: DfdDiagramModel | FlowDiagramModel,
|
||||
rawValue: string | undefined,
|
||||
index: ModelingVaultIndex,
|
||||
options: { suppressUnresolvedWarning?: boolean } = {}
|
||||
context: { section: string; rowIndex: number; relatedId?: string }
|
||||
): ValidationWarning[] {
|
||||
const wikilinks = rawValue ? extractWikilinkReferences(rawValue) : [];
|
||||
if (wikilinks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const diagramLabel = diagram.schema === "flow_diagram" ? "Flow Diagram" : "DFD";
|
||||
return wikilinks
|
||||
.filter((reference) => !resolveReferenceIdentity(reference, index).resolvedModel)
|
||||
.map((reference) => ({
|
||||
code: "unresolved-reference",
|
||||
message: `${diagramLabel} flow data reference "${reference}" could not be resolved. Check the data/model id or file name.`,
|
||||
severity: "warning",
|
||||
path: diagram.path,
|
||||
field: "data",
|
||||
context: {
|
||||
...context,
|
||||
referenceValue: reference
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function resolveDfdFlowDataDisplay(
|
||||
rawValue: string | undefined,
|
||||
index: ModelingVaultIndex
|
||||
): {
|
||||
label?: string;
|
||||
reference?: ReturnType<typeof parseReferenceValue>;
|
||||
model?: ParsedFileModel | null;
|
||||
warning?: string;
|
||||
} {
|
||||
const trimmed = rawValue?.trim();
|
||||
if (!trimmed) {
|
||||
|
|
@ -653,8 +678,7 @@ function resolveDfdFlowDataDisplay(
|
|||
if (reference.target) {
|
||||
return {
|
||||
label: getReferenceDisplayName(trimmed),
|
||||
reference,
|
||||
warning: options.suppressUnresolvedWarning ? undefined : `unresolved flow data reference "${trimmed}"`
|
||||
reference
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,14 @@ import {
|
|||
setMermaidRenderReadyPromise
|
||||
} from "./mermaid-shared";
|
||||
import { sanitizeMermaidId } from "./mermaid-helpers";
|
||||
import { parseReferenceValue } from "../core/reference-resolver";
|
||||
import { modelWeaveText } from "../i18n/language";
|
||||
import { attachMermaidNodeInteractions, type GraphInteractionTarget } from "../views/mermaid-node-interactions";
|
||||
import {
|
||||
attachGraphElementHoverPreview,
|
||||
attachMermaidNodeInteractions,
|
||||
type GraphInteractionHoverRow,
|
||||
type GraphInteractionTarget
|
||||
} from "../views/mermaid-node-interactions";
|
||||
|
||||
export interface DfdDetailLabels {
|
||||
displayedObjects: string;
|
||||
|
|
@ -37,6 +43,18 @@ export interface DfdDetailLabels {
|
|||
unresolved: string;
|
||||
}
|
||||
|
||||
export interface FlowDiagramHoverMetadata {
|
||||
objects: GraphInteractionTarget[];
|
||||
flows: FlowDiagramFlowHoverTarget[];
|
||||
}
|
||||
|
||||
export interface FlowDiagramFlowHoverTarget extends GraphInteractionTarget {
|
||||
edgeId?: string;
|
||||
source: string;
|
||||
target: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export function renderDfdMermaidDiagram(
|
||||
diagram: ResolvedDiagram,
|
||||
options?: {
|
||||
|
|
@ -94,9 +112,13 @@ export function renderDfdMermaidDiagram(
|
|||
shell.root.appendChild(createFlowDetails(diagram.edges, options?.dfdDetailLabels));
|
||||
}
|
||||
|
||||
const interactionTargets = buildDfdMermaidInteractionTargets(
|
||||
const sourcePath = options?.interactionSourcePath ?? diagram.diagram.path;
|
||||
const flowHoverMetadata = isFlowDiagramModel(diagram.diagram)
|
||||
? buildFlowDiagramHoverMetadata(diagram, sourcePath)
|
||||
: null;
|
||||
const interactionTargets = flowHoverMetadata?.objects ?? buildDfdMermaidInteractionTargets(
|
||||
diagram,
|
||||
options?.interactionSourcePath ?? diagram.diagram.path
|
||||
sourcePath
|
||||
);
|
||||
|
||||
const ready = renderMermaidSourceIntoShell(shell, {
|
||||
|
|
@ -113,6 +135,14 @@ export function renderDfdMermaidDiagram(
|
|||
showRenderDebug:
|
||||
!options?.forExport && options?.showMermaidRenderDebug === true
|
||||
}).then(() => {
|
||||
if (!options?.forExport && options?.app && flowHoverMetadata) {
|
||||
attachFlowDiagramFlowHoverPreviews(
|
||||
shell.surface,
|
||||
flowHoverMetadata.flows,
|
||||
options.app,
|
||||
options?.showMermaidRenderDebug === true
|
||||
);
|
||||
}
|
||||
if (!options?.forExport && options?.app && interactionTargets.length > 0) {
|
||||
attachMermaidNodeInteractions({
|
||||
app: options.app,
|
||||
|
|
@ -122,10 +152,23 @@ export function renderDfdMermaidDiagram(
|
|||
nodeClassName: "model-weave-mermaid-interactive-node",
|
||||
dragThreshold: 6,
|
||||
isDebugEnabled: () => options?.showMermaidRenderDebug === true,
|
||||
debugName: "DFD Mermaid",
|
||||
debugName: isFlowDiagramModel(diagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
|
||||
formatTitle: (target) => target.label
|
||||
? `${target.label} (${target.targetType ?? "model"})`
|
||||
: target.linktext
|
||||
: target.linktext,
|
||||
openLinkText: isFlowDiagramModel(diagram.diagram)
|
||||
? (target, event) => {
|
||||
const linktext = getFlowDiagramOpenLinkText(target);
|
||||
if (!linktext) {
|
||||
return;
|
||||
}
|
||||
return options.app?.workspace.openLinkText(
|
||||
linktext,
|
||||
target.sourcePath,
|
||||
event.ctrlKey || event.metaKey
|
||||
);
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
|
|
@ -144,6 +187,215 @@ export function renderDfdMermaidDiagram(
|
|||
}
|
||||
|
||||
|
||||
export function buildFlowDiagramHoverMetadata(
|
||||
diagram: ResolvedDiagram,
|
||||
sourcePath: string
|
||||
): FlowDiagramHoverMetadata {
|
||||
if (!isFlowDiagramModel(diagram.diagram)) {
|
||||
return { objects: [], flows: [] };
|
||||
}
|
||||
|
||||
const objects = diagram.nodes.map((node) => {
|
||||
const rows = buildFlowDiagramObjectHoverRows(node);
|
||||
const hoverTitle = "Flow Object";
|
||||
const target: GraphInteractionTarget = {
|
||||
mermaidId: toMermaidNodeId(node.id),
|
||||
linktext: getFlowDiagramFallbackLinktext(sourcePath),
|
||||
sourcePath,
|
||||
label: node.label ?? node.id,
|
||||
kind: "flow-diagram-object",
|
||||
targetType: "flow_diagram_object",
|
||||
filePath: getStringMetadata(node.metadata, "refModelPath"),
|
||||
modelId: node.id,
|
||||
modelType: "flow-diagram",
|
||||
nodeId: node.id,
|
||||
hoverTitle,
|
||||
hoverRows: rows,
|
||||
hoverText: formatHoverText(hoverTitle, rows),
|
||||
previewLinktext: getResolvedPreviewLinktext(node.ref, getStringMetadata(node.metadata, "refModelPath")),
|
||||
nativeTooltip: formatFlowDiagramObjectTooltip(node)
|
||||
};
|
||||
return target;
|
||||
});
|
||||
|
||||
const flows = diagram.edges.map((edge, index) => {
|
||||
const data = getStringMetadata(edge.metadata, "dataRaw") ?? edge.label;
|
||||
const rows = buildFlowDiagramFlowHoverRows(edge, data);
|
||||
const hoverTitle = "Flow";
|
||||
return {
|
||||
mermaidId: toFlowDiagramFlowMermaidId(edge, index),
|
||||
linktext: getFlowDiagramFallbackLinktext(sourcePath),
|
||||
sourcePath,
|
||||
edgeId: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.label,
|
||||
data,
|
||||
kind: "flow-diagram-flow",
|
||||
targetType: "flow_diagram_flow",
|
||||
modelId: edge.id,
|
||||
modelType: "flow-diagram",
|
||||
filePath: getStringMetadata(edge.metadata, "dataModelPath"),
|
||||
hoverTitle,
|
||||
hoverRows: rows,
|
||||
hoverText: formatHoverText(hoverTitle, rows),
|
||||
previewLinktext: getResolvedPreviewLinktext(data, getStringMetadata(edge.metadata, "dataModelPath")),
|
||||
nativeTooltip: formatFlowDiagramFlowTooltip(edge, data)
|
||||
};
|
||||
});
|
||||
|
||||
return { objects, flows };
|
||||
}
|
||||
|
||||
function formatFlowDiagramObjectTooltip(node: DiagramNode): string {
|
||||
const lines = [
|
||||
`Flow Object: ${node.id}`,
|
||||
node.label,
|
||||
`kind: ${typeof node.kind === "string" ? node.kind : "-"}`,
|
||||
`domain: ${getStringMetadata(node.metadata, "domain") ?? "-"}`,
|
||||
`ref: ${node.ref?.trim() || "-"}`
|
||||
];
|
||||
const notes = formatDiagramEdgeNotes(node.metadata?.notes);
|
||||
if (notes) {
|
||||
lines.push(notes);
|
||||
}
|
||||
return lines.filter((line): line is string => Boolean(line && line.trim())).join("\n");
|
||||
}
|
||||
|
||||
function formatFlowDiagramFlowTooltip(edge: DiagramEdge, data: string | undefined): string {
|
||||
const lines = [
|
||||
`Flow: ${edge.id ?? "-"}`,
|
||||
`${edge.source} -> ${edge.target}`,
|
||||
`data: ${data?.trim() || "-"}`
|
||||
];
|
||||
const notes = formatDiagramEdgeNotes(edge.metadata?.notes);
|
||||
if (notes) {
|
||||
lines.push(notes);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildFlowDiagramObjectHoverRows(node: DiagramNode): GraphInteractionHoverRow[] {
|
||||
return [
|
||||
{ label: "id", value: node.id },
|
||||
{ label: "label", value: node.label },
|
||||
{ label: "kind", value: typeof node.kind === "string" ? node.kind : undefined },
|
||||
{ label: "domain", value: getStringMetadata(node.metadata, "domain") },
|
||||
{ label: "ref", value: node.ref },
|
||||
{ label: "notes", value: formatDiagramEdgeNotes(node.metadata?.notes) }
|
||||
];
|
||||
}
|
||||
|
||||
function buildFlowDiagramFlowHoverRows(edge: DiagramEdge, data: string | undefined): GraphInteractionHoverRow[] {
|
||||
return [
|
||||
{ label: "id", value: edge.id },
|
||||
{ label: "from", value: edge.source },
|
||||
{ label: "to", value: edge.target },
|
||||
{ label: "data", value: data },
|
||||
{ label: "notes", value: formatDiagramEdgeNotes(edge.metadata?.notes) }
|
||||
];
|
||||
}
|
||||
|
||||
function formatHoverText(title: string, rows: GraphInteractionHoverRow[]): string {
|
||||
return [
|
||||
title,
|
||||
...rows.map((row) => `${row.label}: ${formatHoverValue(row.value)}`)
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function formatHoverValue(value: string | undefined): string {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed || "-";
|
||||
}
|
||||
|
||||
function getStringMetadata(metadata: Record<string, unknown> | undefined, key: string): string | undefined {
|
||||
const value = metadata?.[key];
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function toFlowDiagramFlowMermaidId(edge: DiagramEdge, index: number): string {
|
||||
const id = edge.id?.trim() || `${edge.source}_${edge.target}_${index + 1}`;
|
||||
return `FLOW_${index + 1}_${toMermaidNodeId(id)}`;
|
||||
}
|
||||
|
||||
function getResolvedPreviewLinktext(rawReference: string | undefined, resolvedPath: string | undefined): string | undefined {
|
||||
if (!resolvedPath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const trimmed = rawReference?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = parseReferenceValue(trimmed);
|
||||
return parsed?.kind === "wikilink" || parsed?.kind === "markdown_link"
|
||||
? resolvedPath
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function getFlowDiagramFallbackLinktext(sourcePath: string): string {
|
||||
return sourcePath;
|
||||
}
|
||||
|
||||
function getFlowDiagramOpenLinkText(target: GraphInteractionTarget): string | null {
|
||||
const linktext = target.previewLinktext?.trim() || target.filePath?.trim();
|
||||
return linktext ? linktext : null;
|
||||
}
|
||||
|
||||
function attachFlowDiagramFlowHoverPreviews(
|
||||
rootEl: HTMLElement,
|
||||
flowTargets: FlowDiagramFlowHoverTarget[],
|
||||
app: App,
|
||||
showMermaidRenderDebug: boolean
|
||||
): void {
|
||||
if (flowTargets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = rootEl.querySelector<SVGElement>("svg");
|
||||
if (!svg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const edgeLabels = Array.from(svg.querySelectorAll<SVGElement>("g.edgeLabel"));
|
||||
flowTargets.forEach((target, index) => {
|
||||
const labelEl = edgeLabels[index];
|
||||
if (!labelEl) {
|
||||
return;
|
||||
}
|
||||
setSvgNativeTooltip(labelEl, target.nativeTooltip);
|
||||
labelEl.addClass("model-weave-mermaid-interactive-flow");
|
||||
labelEl.setAttribute("data-model-weave-flow-id", target.edgeId ?? target.mermaidId);
|
||||
attachGraphElementHoverPreview({
|
||||
app,
|
||||
targetEl: labelEl,
|
||||
target,
|
||||
rootEl,
|
||||
source: "model-weave",
|
||||
isDebugEnabled: () => showMermaidRenderDebug,
|
||||
debugName: "Flow Diagram Mermaid Flow"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setSvgNativeTooltip(element: SVGElement, text: string | undefined): void {
|
||||
const existingTitle = element.querySelector("title");
|
||||
const trimmed = text?.trim();
|
||||
if (!trimmed) {
|
||||
existingTitle?.remove();
|
||||
element.removeAttribute("title");
|
||||
return;
|
||||
}
|
||||
|
||||
const title = existingTitle
|
||||
?? element.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "title");
|
||||
title.textContent = trimmed;
|
||||
if (!title.parentElement) {
|
||||
element.prepend(title);
|
||||
}
|
||||
}
|
||||
|
||||
function buildDfdMermaidInteractionTargets(
|
||||
diagram: ResolvedDiagram,
|
||||
sourcePath: string
|
||||
|
|
|
|||
|
|
@ -12,6 +12,16 @@ export interface GraphInteractionTarget {
|
|||
modelType?: string;
|
||||
nodeId?: string;
|
||||
status?: string;
|
||||
hoverTitle?: string;
|
||||
hoverRows?: GraphInteractionHoverRow[];
|
||||
hoverText?: string;
|
||||
previewLinktext?: string;
|
||||
nativeTooltip?: string;
|
||||
}
|
||||
|
||||
export interface GraphInteractionHoverRow {
|
||||
label: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface AttachMermaidNodeInteractionsOptions {
|
||||
|
|
@ -311,7 +321,7 @@ export function attachGraphElementHoverPreview(
|
|||
options: AttachGraphElementHoverPreviewOptions
|
||||
): () => void {
|
||||
const fallback = options.rootEl ?? getElementHoverFallback(options.targetEl);
|
||||
if (!fallback || !options.target.linktext || !options.target.sourcePath) {
|
||||
if (!fallback || !canShowGraphInteractionHover(options.target)) {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +352,7 @@ export function attachGraphElementHoverPreview(
|
|||
debugName: options.debugName,
|
||||
isDebugEnabled: options.isDebugEnabled
|
||||
}, fallback, event, false);
|
||||
const hoverLinkTarget = triggerGraphInteractionHoverPreview(
|
||||
const hoverLinkTarget = triggerGraphInteractionHover(
|
||||
options.app,
|
||||
source,
|
||||
resolveGraphHoverParent(options.targetEl, fallback, options.hoverParent),
|
||||
|
|
@ -420,7 +430,11 @@ function clearGraphHoverState(
|
|||
state.activeHoverTargetEl = null;
|
||||
|
||||
if (activeHoverTargetEl && !suppressSyntheticLeave) {
|
||||
dispatchGraphHoverTargetLeave(activeHoverTargetEl);
|
||||
if (isGraphFallbackHoverCard(activeHoverTargetEl)) {
|
||||
activeHoverTargetEl.remove();
|
||||
} else {
|
||||
dispatchGraphHoverTargetLeave(activeHoverTargetEl);
|
||||
}
|
||||
} else if (hadActiveNode && !suppressSyntheticLeave) {
|
||||
dispatchGraphHoverTargetLeave(rootEl);
|
||||
}
|
||||
|
|
@ -518,18 +532,27 @@ function findMermaidNodeInteractionByTarget(
|
|||
return interactions.find((interaction) => interaction.target === target) ?? null;
|
||||
}
|
||||
|
||||
export function getGraphInteractionNativeTooltipText(
|
||||
target: GraphInteractionTarget,
|
||||
fallbackTitle?: string
|
||||
): string | undefined {
|
||||
return target.nativeTooltip ?? fallbackTitle ?? target.label ?? target.linktext;
|
||||
}
|
||||
|
||||
function setMermaidNodeTitle(
|
||||
nodeEl: SVGElement,
|
||||
target: GraphInteractionTarget,
|
||||
formatTitle: AttachMermaidNodeInteractionsOptions["formatTitle"]
|
||||
): void {
|
||||
const titleText = formatTitle?.(target) ?? target.label ?? target.linktext;
|
||||
const existingTitle = nodeEl.querySelector("title");
|
||||
const titleText = getGraphInteractionNativeTooltipText(target, formatTitle?.(target));
|
||||
if (!titleText) {
|
||||
existingTitle?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = nodeEl.ownerDocument;
|
||||
const title = nodeEl.querySelector("title") ?? doc.createElementNS("http://www.w3.org/2000/svg", "title");
|
||||
const title = existingTitle ?? doc.createElementNS("http://www.w3.org/2000/svg", "title");
|
||||
title.textContent = titleText;
|
||||
if (!title.parentElement) {
|
||||
nodeEl.prepend(title);
|
||||
|
|
@ -544,13 +567,13 @@ function triggerMermaidNodeHoverPreview(
|
|||
event: MouseEvent,
|
||||
stateDebug?: HoverStateDebugFields
|
||||
): HoverLinkTargetResolution | null {
|
||||
if (!target.linktext || !target.sourcePath) {
|
||||
if (!canShowGraphInteractionHover(target)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hoverParent = resolveGraphHoverParent(targetEl, options.rootEl, options.hoverParent);
|
||||
|
||||
return triggerGraphInteractionHoverPreview(
|
||||
return triggerGraphInteractionHover(
|
||||
options.app,
|
||||
source,
|
||||
hoverParent,
|
||||
|
|
@ -565,6 +588,119 @@ function triggerMermaidNodeHoverPreview(
|
|||
);
|
||||
}
|
||||
|
||||
function canShowGraphInteractionHover(target: GraphInteractionTarget): boolean {
|
||||
return Boolean(
|
||||
(target.previewLinktext && target.sourcePath) ||
|
||||
(target.linktext && target.sourcePath && !target.hoverRows?.length) ||
|
||||
target.hoverRows?.length
|
||||
);
|
||||
}
|
||||
|
||||
function triggerGraphInteractionHover(
|
||||
app: App,
|
||||
source: string,
|
||||
hoverParent: HTMLElement,
|
||||
targetEl: HTMLElement | SVGElement,
|
||||
target: GraphInteractionTarget,
|
||||
event: MouseEvent,
|
||||
debug?: HoverPreviewDebugOptions,
|
||||
stateDebug?: HoverStateDebugFields
|
||||
): HoverLinkTargetResolution | null {
|
||||
const previewLinktext = getGraphInteractionPreviewLinktext(target);
|
||||
if (previewLinktext) {
|
||||
const previewTarget = { ...target, linktext: previewLinktext };
|
||||
return triggerGraphInteractionHoverPreview(
|
||||
app,
|
||||
source,
|
||||
hoverParent,
|
||||
targetEl,
|
||||
previewTarget,
|
||||
event,
|
||||
debug,
|
||||
stateDebug
|
||||
) ?? triggerGraphInteractionFallbackHover(hoverParent, target, event);
|
||||
}
|
||||
|
||||
return triggerGraphInteractionFallbackHover(hoverParent, target, event);
|
||||
}
|
||||
|
||||
function getGraphInteractionPreviewLinktext(target: GraphInteractionTarget): string | null {
|
||||
const explicit = target.previewLinktext?.trim();
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
if (target.hoverRows?.length) {
|
||||
return null;
|
||||
}
|
||||
const linktext = target.linktext?.trim();
|
||||
return linktext && target.sourcePath ? linktext : null;
|
||||
}
|
||||
|
||||
function triggerGraphInteractionFallbackHover(
|
||||
hoverParent: HTMLElement,
|
||||
target: GraphInteractionTarget,
|
||||
event: MouseEvent
|
||||
): HoverLinkTargetResolution | null {
|
||||
if (!target.hoverRows?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const card = createGraphFallbackHoverCard(hoverParent, target, event);
|
||||
return { targetEl: card, reusableAnchorTargetUsed: false };
|
||||
}
|
||||
|
||||
function createGraphFallbackHoverCard(
|
||||
hoverParent: HTMLElement,
|
||||
target: GraphInteractionTarget,
|
||||
event: MouseEvent
|
||||
): HTMLElement {
|
||||
const doc = hoverParent.ownerDocument;
|
||||
const existing = hoverParent.querySelectorAll<HTMLElement>(".model-weave-graph-hover-card");
|
||||
existing.forEach((element) => element.remove());
|
||||
|
||||
const card = doc.createElement("div");
|
||||
card.className = "model-weave-graph-hover-card";
|
||||
card.setAttribute("role", "tooltip");
|
||||
|
||||
const title = doc.createElement("div");
|
||||
title.className = "model-weave-graph-hover-card-title";
|
||||
title.textContent = target.hoverTitle ?? target.label ?? "Model Weave";
|
||||
card.appendChild(title);
|
||||
|
||||
const rows = doc.createElement("dl");
|
||||
rows.className = "model-weave-graph-hover-card-rows";
|
||||
for (const row of target.hoverRows ?? []) {
|
||||
const term = doc.createElement("dt");
|
||||
term.textContent = row.label;
|
||||
const description = doc.createElement("dd");
|
||||
description.textContent = row.value?.trim() || "-";
|
||||
rows.appendChild(term);
|
||||
rows.appendChild(description);
|
||||
}
|
||||
card.appendChild(rows);
|
||||
|
||||
hoverParent.appendChild(card);
|
||||
positionGraphFallbackHoverCard(card, hoverParent, event);
|
||||
return card;
|
||||
}
|
||||
|
||||
function positionGraphFallbackHoverCard(
|
||||
card: HTMLElement,
|
||||
hoverParent: HTMLElement,
|
||||
event: MouseEvent
|
||||
): void {
|
||||
const view = hoverParent.ownerDocument.defaultView;
|
||||
const viewportWidth = view?.innerWidth ?? event.clientX + 360;
|
||||
const x = Math.min(Math.max(12, event.clientX + 14), Math.max(12, viewportWidth - 380));
|
||||
const y = Math.max(12, event.clientY + 14);
|
||||
card.style.left = `${x}px`;
|
||||
card.style.top = `${y}px`;
|
||||
}
|
||||
|
||||
function isGraphFallbackHoverCard(element: HTMLElement | SVGElement): element is HTMLElement {
|
||||
return element instanceof HTMLElement && element.classList.contains("model-weave-graph-hover-card");
|
||||
}
|
||||
|
||||
function triggerGraphInteractionHoverPreview(
|
||||
app: App,
|
||||
source: string,
|
||||
|
|
|
|||
40
styles.css
40
styles.css
|
|
@ -1992,3 +1992,43 @@ body.model-weave-focus-mode-active .popover.hover-popover {
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.model-weave-graph-hover-card {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
max-width: min(360px, calc(100% - 24px));
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
background: var(--background-primary);
|
||||
box-shadow: var(--shadow-s);
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-small);
|
||||
line-height: 1.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.model-weave-graph-hover-card-title {
|
||||
margin-bottom: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.model-weave-graph-hover-card-rows {
|
||||
display: grid;
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
gap: 4px 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.model-weave-graph-hover-card-rows dt {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.model-weave-graph-hover-card-rows dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ await build({
|
|||
'export { renderClassDiagram } from "./src/renderers/class-renderer";',
|
||||
'export { buildVaultIndex, ensureVaultValidation, replaceVaultIndexFile } from "./src/core/vault-index";',
|
||||
'export { resolveObjectContext } from "./src/core/object-context-resolver";',
|
||||
'export { buildCurrentObjectDiagnostics, localizeDiagnosticMessage } from "./src/core/current-file-diagnostics";'
|
||||
'export { buildCurrentObjectDiagnostics, localizeDiagnosticMessage } from "./src/core/current-file-diagnostics";',
|
||||
'export { getExpectedHeaderForDiagnostic } from "./src/core/diagnostic-section-guidance";'
|
||||
].join("\n"),
|
||||
resolveDir: ".",
|
||||
sourcefile: "test-domains-model-entry.ts",
|
||||
|
|
@ -125,6 +126,7 @@ const {
|
|||
buildCurrentObjectDiagnostics,
|
||||
ensureVaultValidation,
|
||||
localizeDiagnosticMessage,
|
||||
getExpectedHeaderForDiagnostic,
|
||||
resolveDiagramRelations,
|
||||
replaceVaultIndexFile
|
||||
} = await import(
|
||||
|
|
@ -1849,6 +1851,79 @@ test("DFD files without local Domains remain compatible", () => {
|
|||
assert.equal(warnings.length, 0);
|
||||
});
|
||||
|
||||
function dfdDataWarningMarkdown(dataValue) {
|
||||
return `${dfdFrontmatter}
|
||||
## Objects
|
||||
|
||||
| id | label | kind | ref | notes |
|
||||
|---|---|---|---|---|
|
||||
| user | User | external | | User |
|
||||
| pick | Pick items | process | | Pick |
|
||||
|
||||
## Flows
|
||||
|
||||
| id | from | to | data | notes |
|
||||
|---|---|---|---|---|
|
||||
| request | user | pick | ${dataValue} | User request |
|
||||
`;
|
||||
}
|
||||
|
||||
function getDfdFlowDataWarnings(dataValue, extraFiles = []) {
|
||||
const { resolved } = resolveDfdWithFiles([
|
||||
{ path: "DFD-SHIPPING.md", content: dfdDataWarningMarkdown(dataValue) },
|
||||
...extraFiles
|
||||
]);
|
||||
return resolved.warnings.filter((warning) =>
|
||||
warning.code === "unresolved-reference" && /flow data reference/.test(warning.message)
|
||||
);
|
||||
}
|
||||
|
||||
test("dfd_diagram Flows.data plain text does not warn", () => {
|
||||
assert.equal(getDfdFlowDataWarnings("DATA-ORDER-DRAFT").length, 0);
|
||||
assert.equal(getDfdFlowDataWarnings("Search condition").length, 0);
|
||||
});
|
||||
|
||||
test("dfd_diagram Flows.data empty value does not warn", () => {
|
||||
assert.equal(getDfdFlowDataWarnings("").length, 0);
|
||||
});
|
||||
|
||||
test("dfd_diagram Flows.data resolved Wikilink does not warn", () => {
|
||||
const warnings = getDfdFlowDataWarnings("[[DATA-ORDER-DRAFT]]", [
|
||||
{ path: "DATA-ORDER-DRAFT.md", content: "---\ntype: data_object\nid: DATA-ORDER-DRAFT\nname: Order Draft\n---\n" }
|
||||
]);
|
||||
|
||||
assert.equal(warnings.length, 0);
|
||||
});
|
||||
|
||||
test("dfd_diagram Flows.data unresolved Wikilink emits unresolved-reference warning", () => {
|
||||
const warnings = getDfdFlowDataWarnings("[[DATA-NOT-EXISTS]]");
|
||||
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.equal(warnings[0].message, 'DFD flow data reference "[[DATA-NOT-EXISTS]]" could not be resolved. Check the data/model id or file name.');
|
||||
assert.equal(warnings[0].severity, "warning");
|
||||
assert.equal(warnings[0].field, "data");
|
||||
assert.equal(warnings[0].context.section, "Flows");
|
||||
assert.equal(warnings[0].context.rowIndex, 1);
|
||||
assert.equal(warnings[0].context.referenceValue, "[[DATA-NOT-EXISTS]]");
|
||||
assert.equal(getExpectedHeaderForDiagnostic(warnings[0]), null);
|
||||
assert.notEqual(warnings[0].context.referenceKind, "local-object-id");
|
||||
});
|
||||
|
||||
test("dfd_diagram Flows.data multiple Wikilinks warn only for unresolved links", () => {
|
||||
const warnings = getDfdFlowDataWarnings("[[DATA-A]], [[DATA-B]]", [
|
||||
{ path: "DATA-A.md", content: "---\ntype: data_object\nid: DATA-A\nname: Data A\n---\n" }
|
||||
]);
|
||||
|
||||
assert.deepEqual(warnings.map((warning) => warning.context.referenceValue), ["[[DATA-B]]"]);
|
||||
});
|
||||
|
||||
test("localizes DFD flow data reference diagnostics", () => {
|
||||
assert.equal(
|
||||
localizeDiagnosticMessage('DFD flow data reference "[[DATA-NOT-EXISTS]]" could not be resolved. Check the data/model id or file name.', "ja"),
|
||||
'DFD flow data reference "[[DATA-NOT-EXISTS]]" の参照先が見つかりません。data/model の id またはファイル名を確認してください。'
|
||||
);
|
||||
});
|
||||
|
||||
test("DFD Objects refs accept non-DFD Model Weave assets without unresolved warnings", () => {
|
||||
const dfd = `---
|
||||
type: dfd_diagram
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ await build({
|
|||
'export { isDiagramPreviewRouteFileType, isDfdLikeDiagramPreviewFileType } from "./src/core/preview-routing";',
|
||||
'export { isModelWeavePreviewSupportedFileType } from "./src/core/supported-formats";',
|
||||
'export { resolveDiagramRelations } from "./src/core/relation-resolver";',
|
||||
'export { buildDfdMermaidSource, getDfdMermaidColorSchemeTargets } from "./src/renderers/dfd-mermaid";',
|
||||
'export { buildDfdMermaidSource, buildFlowDiagramHoverMetadata, getDfdMermaidColorSchemeTargets } from "./src/renderers/dfd-mermaid";',
|
||||
'export { getAppliedColorSchemeRowsForTargets } from "./src/core/color-scheme";',
|
||||
'export { buildCurrentDiagramDiagnostics } from "./src/core/current-file-diagnostics";',
|
||||
'export { createModelWeaveTranslator } from "./src/i18n/messages";',
|
||||
|
|
@ -81,6 +81,7 @@ await build({
|
|||
const {
|
||||
buildCurrentDiagramDiagnostics,
|
||||
buildDfdMermaidSource,
|
||||
buildFlowDiagramHoverMetadata,
|
||||
createModelWeaveTranslator,
|
||||
getAppliedColorSchemeLowerPaneSlot,
|
||||
getAppliedColorSchemeRowsForTargets,
|
||||
|
|
@ -253,6 +254,255 @@ test("flow_diagram Flows.data appears as edge label", () => {
|
|||
assert.match(source, /order_process -->\|Order result\| session_store/);
|
||||
});
|
||||
|
||||
function getFlowDiagramDataWarnings(markdown, extraFiles = []) {
|
||||
const { model, resolved } = resolveFlow(markdown, extraFiles);
|
||||
return buildCurrentDiagramDiagnostics({ diagram: model }, resolved.warnings)
|
||||
.filter((warning) => warning.code === "unresolved-reference" && /flow data reference/.test(warning.message));
|
||||
}
|
||||
|
||||
test("flow_diagram Flows.data plain text does not warn", () => {
|
||||
const warnings = getFlowDiagramDataWarnings(flowMarkdown.replace("[[DATA-ORDER-REQUEST]]", "DATA-ORDER-DRAFT"));
|
||||
|
||||
assert.equal(warnings.length, 0);
|
||||
});
|
||||
|
||||
test("flow_diagram Flows.data empty value does not warn", () => {
|
||||
const warnings = getFlowDiagramDataWarnings(flowMarkdown.replace("[[DATA-ORDER-REQUEST]]", ""));
|
||||
|
||||
assert.equal(warnings.length, 0);
|
||||
});
|
||||
|
||||
test("flow_diagram Flows.data resolved Wikilink does not warn", () => {
|
||||
const warnings = getFlowDiagramDataWarnings(flowMarkdown, [
|
||||
{ path: "DATA-ORDER-REQUEST.md", content: "---\ntype: data_object\nid: DATA-ORDER-REQUEST\nname: Order Request\n---\n" }
|
||||
]);
|
||||
|
||||
assert.equal(warnings.length, 0);
|
||||
});
|
||||
|
||||
test("flow_diagram Flows.data unresolved Wikilink emits unresolved-reference warning", () => {
|
||||
const warnings = getFlowDiagramDataWarnings(flowMarkdown);
|
||||
const diagnostic = warnings.find((warning) => warning.context.referenceValue === "[[DATA-ORDER-REQUEST]]");
|
||||
|
||||
assert.ok(diagnostic);
|
||||
assert.equal(diagnostic.message, 'Flow Diagram flow data reference "[[DATA-ORDER-REQUEST]]" could not be resolved. Check the data/model id or file name.');
|
||||
assert.equal(diagnostic.severity, "warning");
|
||||
assert.equal(diagnostic.field, "data");
|
||||
assert.equal(diagnostic.context.section, "Flows");
|
||||
assert.equal(diagnostic.context.rowIndex, 1);
|
||||
assert.equal(getExpectedHeaderForDiagnostic(diagnostic), null);
|
||||
|
||||
const t = createModelWeaveTranslator("en");
|
||||
const details = getDiagnosticDetailEntries(diagnostic, t);
|
||||
assert.equal(details.some((entry) => /local .*Objects.* table/.test(entry.value)), false);
|
||||
});
|
||||
|
||||
test("flow_diagram Flows.data multiple Wikilinks warn only for unresolved links", () => {
|
||||
const markdown = flowMarkdown.replace("[[DATA-ORDER-REQUEST]]", "[[DATA-A]], [[DATA-B]]");
|
||||
const warnings = getFlowDiagramDataWarnings(markdown, [
|
||||
{ path: "DATA-A.md", content: "---\ntype: data_object\nid: DATA-A\nname: Data A\n---\n" }
|
||||
]);
|
||||
|
||||
assert.deepEqual(warnings.map((warning) => warning.context.referenceValue), ["[[DATA-B]]"]);
|
||||
});
|
||||
|
||||
test("flow_diagram object hover metadata is generated for each Objects row", () => {
|
||||
const { resolved } = resolveFlow();
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
|
||||
assert.equal(metadata.objects.length, resolved.nodes.length);
|
||||
assert.deepEqual(metadata.objects.map((target) => target.mermaidId), [
|
||||
"order_screen",
|
||||
"order_process",
|
||||
"session_store",
|
||||
"data_store",
|
||||
"mystery"
|
||||
]);
|
||||
});
|
||||
|
||||
test("flow_diagram object hover metadata includes row details and raw wikilink ref", () => {
|
||||
const { resolved } = resolveFlow();
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
const target = metadata.objects.find((entry) => entry.nodeId === "order_screen");
|
||||
|
||||
assert.ok(target);
|
||||
assert.equal(target.hoverTitle, "Flow Object");
|
||||
assert.deepEqual(
|
||||
Object.fromEntries(target.hoverRows.map((row) => [row.label, row.value])),
|
||||
{
|
||||
id: "order_screen",
|
||||
label: "Order Screen",
|
||||
kind: "screen",
|
||||
domain: "order",
|
||||
ref: "[[SCR-ORDER]]",
|
||||
notes: "Source screen"
|
||||
}
|
||||
);
|
||||
assert.match(target.hoverText, /Flow Object/);
|
||||
assert.match(target.hoverText, /ref: \[\[SCR-ORDER\]\]/);
|
||||
});
|
||||
|
||||
test("flow_diagram object hover metadata exposes resolved file path for resolved wikilink ref", () => {
|
||||
const { resolved } = resolveFlow(flowMarkdown, [
|
||||
{ path: "SCR-ORDER.md", content: "---\ntype: screen\nid: SCR-ORDER\nname: Order Screen\n---\n" }
|
||||
]);
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
const target = metadata.objects.find((entry) => entry.nodeId === "order_screen");
|
||||
|
||||
assert.ok(target);
|
||||
assert.equal(target.previewLinktext, "SCR-ORDER.md");
|
||||
assert.equal(target.filePath, "SCR-ORDER.md");
|
||||
assert.notEqual(target.previewLinktext, "[[SCR-ORDER]]");
|
||||
assert.match(target.nativeTooltip, /Flow Object: order_screen/);
|
||||
assert.match(target.nativeTooltip, /ref: \[\[SCR-ORDER\]\]/);
|
||||
assert.ok(target.hoverRows.length > 0);
|
||||
});
|
||||
|
||||
test("flow_diagram plain or unresolved object ref falls back to custom card metadata", () => {
|
||||
const markdown = flowMarkdown.replace("[[SCR-ORDER]]", "SCR-MISSING");
|
||||
const { resolved } = resolveFlow(markdown);
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
const target = metadata.objects.find((entry) => entry.nodeId === "order_screen");
|
||||
|
||||
assert.ok(target);
|
||||
assert.equal(target.previewLinktext, undefined);
|
||||
assert.equal(target.hoverTitle, "Flow Object");
|
||||
assert.equal(target.hoverRows.find((row) => row.label === "ref")?.value, "SCR-MISSING");
|
||||
assert.match(target.nativeTooltip, /ref: SCR-MISSING/);
|
||||
assert.notEqual(target.hoverText, undefined);
|
||||
});
|
||||
|
||||
test("flow_diagram unresolved object ref wikilink falls back without raw Page Preview target", () => {
|
||||
const markdown = flowMarkdown.replace("[[SCR-ORDER]]", "[[SCR-MISSING]]");
|
||||
const { resolved } = resolveFlow(markdown);
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
const target = metadata.objects.find((entry) => entry.nodeId === "order_screen");
|
||||
|
||||
assert.ok(target);
|
||||
assert.equal(target.previewLinktext, undefined);
|
||||
assert.equal(target.filePath, undefined);
|
||||
assert.equal(target.hoverRows.find((row) => row.label === "ref")?.value, "[[SCR-MISSING]]");
|
||||
assert.match(target.nativeTooltip, /ref: \[\[SCR-MISSING\]\]/);
|
||||
});
|
||||
|
||||
test("flow_diagram empty object ref has fallback card metadata and safe tooltip", () => {
|
||||
const { resolved } = resolveFlow();
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
const target = metadata.objects.find((entry) => entry.nodeId === "session_store");
|
||||
|
||||
assert.ok(target);
|
||||
assert.equal(target.previewLinktext, undefined);
|
||||
assert.equal(target.hoverRows.find((row) => row.label === "ref")?.value, undefined);
|
||||
assert.match(target.nativeTooltip, /Flow Object: session_store/);
|
||||
assert.match(target.nativeTooltip, /ref: -/);
|
||||
});
|
||||
|
||||
test("flow_diagram flow hover metadata is generated for each Flows row", () => {
|
||||
const { resolved } = resolveFlow();
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
|
||||
assert.equal(metadata.flows.length, resolved.edges.length);
|
||||
assert.deepEqual(metadata.flows.map((target) => target.mermaidId), [
|
||||
"FLOW_1_FLOW_001",
|
||||
"FLOW_2_FLOW_002",
|
||||
"FLOW_3_FLOW_003",
|
||||
"FLOW_4_FLOW_004"
|
||||
]);
|
||||
});
|
||||
|
||||
test("flow_diagram flow hover metadata includes row details and raw data ref", () => {
|
||||
const { resolved } = resolveFlow();
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
const target = metadata.flows.find((entry) => entry.edgeId === "FLOW-001");
|
||||
|
||||
assert.ok(target);
|
||||
assert.equal(target.hoverTitle, "Flow");
|
||||
assert.deepEqual(
|
||||
Object.fromEntries(target.hoverRows.map((row) => [row.label, row.value])),
|
||||
{
|
||||
id: "FLOW-001",
|
||||
from: "order_screen",
|
||||
to: "order_process",
|
||||
data: "[[DATA-ORDER-REQUEST]]",
|
||||
notes: "Submit order"
|
||||
}
|
||||
);
|
||||
assert.match(target.hoverText, /data: \[\[DATA-ORDER-REQUEST\]\]/);
|
||||
});
|
||||
|
||||
test("flow_diagram flow hover metadata exposes resolved file path for resolved wikilink data", () => {
|
||||
const { resolved } = resolveFlow(flowMarkdown, [
|
||||
{ path: "DATA-ORDER-REQUEST.md", content: "---\ntype: data_object\nid: DATA-ORDER-REQUEST\nname: Order Request\n---\n" }
|
||||
]);
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
const target = metadata.flows.find((entry) => entry.edgeId === "FLOW-001");
|
||||
|
||||
assert.ok(target);
|
||||
assert.equal(target.previewLinktext, "DATA-ORDER-REQUEST.md");
|
||||
assert.equal(target.filePath, "DATA-ORDER-REQUEST.md");
|
||||
assert.notEqual(target.previewLinktext, "[[DATA-ORDER-REQUEST]]");
|
||||
assert.match(target.nativeTooltip, /Flow: FLOW-001/);
|
||||
assert.match(target.nativeTooltip, /data: \[\[DATA-ORDER-REQUEST\]\]/);
|
||||
assert.ok(target.hoverRows.length > 0);
|
||||
});
|
||||
|
||||
test("flow_diagram plain flow data falls back to custom card metadata", () => {
|
||||
const { resolved } = resolveFlow();
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
const target = metadata.flows.find((entry) => entry.edgeId === "FLOW-002");
|
||||
|
||||
assert.ok(target);
|
||||
assert.equal(target.previewLinktext, undefined);
|
||||
assert.equal(target.hoverTitle, "Flow");
|
||||
assert.equal(target.hoverRows.find((row) => row.label === "data")?.value, "Order result");
|
||||
assert.match(target.nativeTooltip, /data: Order result/);
|
||||
assert.notEqual(target.hoverText, undefined);
|
||||
});
|
||||
|
||||
test("flow_diagram unresolved flow data wikilink falls back without raw Page Preview target", () => {
|
||||
const { resolved } = resolveFlow();
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
const target = metadata.flows.find((entry) => entry.edgeId === "FLOW-001");
|
||||
|
||||
assert.ok(target);
|
||||
assert.equal(target.previewLinktext, undefined);
|
||||
assert.equal(target.filePath, undefined);
|
||||
assert.equal(target.hoverRows.find((row) => row.label === "data")?.value, "[[DATA-ORDER-REQUEST]]");
|
||||
assert.match(target.nativeTooltip, /data: \[\[DATA-ORDER-REQUEST\]\]/);
|
||||
});
|
||||
|
||||
test("flow_diagram empty flow data has fallback card metadata and safe tooltip", () => {
|
||||
const markdown = flowMarkdown.replace("| FLOW-002 | order_process | session_store | Order result | Store result |", "| FLOW-002 | order_process | session_store | | Store result |");
|
||||
const { resolved } = resolveFlow(markdown);
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
const target = metadata.flows.find((entry) => entry.edgeId === "FLOW-002");
|
||||
|
||||
assert.ok(target);
|
||||
assert.equal(target.previewLinktext, undefined);
|
||||
assert.equal(target.hoverRows.find((row) => row.label === "data")?.value, undefined);
|
||||
assert.match(target.nativeTooltip, /Flow: FLOW-002/);
|
||||
assert.match(target.nativeTooltip, /data: -/);
|
||||
});
|
||||
|
||||
test("flow_diagram hover metadata is not native-tooltip-only", () => {
|
||||
const { resolved } = resolveFlow();
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
|
||||
assert.equal(metadata.objects.every((target) => target.hoverRows.length > 0), true);
|
||||
assert.equal(metadata.flows.every((target) => target.hoverRows.length > 0), true);
|
||||
assert.equal(metadata.objects.some((target) => target.previewLinktext), false);
|
||||
});
|
||||
|
||||
test("flow_diagram hover metadata uses stable node and flow mapping keys", () => {
|
||||
const { resolved } = resolveFlow(flowDomainMarkdown);
|
||||
const metadata = buildFlowDiagramHoverMetadata(resolved, "FLOW-ORDER-SCREEN-COMMUNICATION.md");
|
||||
|
||||
assert.equal(metadata.objects.find((target) => target.nodeId === "ORDER_ENTRY")?.mermaidId, "ORDER_ENTRY");
|
||||
assert.equal(metadata.objects.find((target) => target.nodeId === "SESSION_STORE")?.mermaidId, "SESSION_STORE");
|
||||
assert.equal(metadata.flows.find((target) => target.edgeId === "F01")?.mermaidId, "FLOW_1_F01");
|
||||
assert.equal(metadata.flows.find((target) => target.edgeId === "F05")?.mermaidId, "FLOW_5_F05");
|
||||
});
|
||||
|
||||
test("flow_diagram refs can point to screen, app_process, and data_object without DFD compatibility warnings", () => {
|
||||
const markdown = flowMarkdown.replace(
|
||||
"| data_store | Data Store | datastore | | order | Persistent state |",
|
||||
|
|
@ -519,4 +769,4 @@ test("Applied Color Scheme placement policy uses Details for color-aware preview
|
|||
test("DFD and Flow Applied Color Scheme placement is not Relationships", () => {
|
||||
assert.notEqual(getAppliedColorSchemeLowerPaneSlot("dfd_diagram"), "impact");
|
||||
assert.notEqual(getAppliedColorSchemeLowerPaneSlot("flow_diagram"), "impact");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const outputFile = "dist/test-mermaid-node-interactions.mjs";
|
|||
|
||||
await build({
|
||||
stdin: {
|
||||
contents: 'export { resolveGraphHoverParent, resolveGraphHoverLinkTargetElement } from "./src/views/mermaid-node-interactions";',
|
||||
contents: 'export { getGraphInteractionNativeTooltipText, resolveGraphHoverParent, resolveGraphHoverLinkTargetElement } from "./src/views/mermaid-node-interactions";',
|
||||
resolveDir: ".",
|
||||
sourcefile: "test-mermaid-node-interactions-entry.ts",
|
||||
loader: "ts"
|
||||
|
|
@ -33,9 +33,11 @@ await build({
|
|||
logLevel: "silent"
|
||||
});
|
||||
|
||||
const { resolveGraphHoverParent, resolveGraphHoverLinkTargetElement } = await import(
|
||||
`../${outputFile}?t=${Date.now()}`
|
||||
);
|
||||
const {
|
||||
getGraphInteractionNativeTooltipText,
|
||||
resolveGraphHoverParent,
|
||||
resolveGraphHoverLinkTargetElement
|
||||
} = await import(`../${outputFile}?t=${Date.now()}`);
|
||||
|
||||
class TestElement {
|
||||
constructor(className = "", parent = null) {
|
||||
|
|
@ -125,3 +127,30 @@ test("resolveGraphHoverLinkTargetElement falls back through viewport and viewer
|
|||
assert.equal(resolveGraphHoverLinkTargetElement(svgNode, viewerRoot), viewport);
|
||||
assert.equal(resolveGraphHoverLinkTargetElement(detached, viewerRoot), viewerRoot);
|
||||
});
|
||||
|
||||
|
||||
test("getGraphInteractionNativeTooltipText keeps normal tooltip fallback without rich metadata", () => {
|
||||
assert.equal(
|
||||
getGraphInteractionNativeTooltipText({
|
||||
mermaidId: "node",
|
||||
linktext: "MODEL.md",
|
||||
sourcePath: "SOURCE.md",
|
||||
label: "Model Node"
|
||||
}),
|
||||
"Model Node"
|
||||
);
|
||||
});
|
||||
|
||||
test("getGraphInteractionNativeTooltipText prefers explicit safe tooltip for rich hover metadata", () => {
|
||||
assert.equal(
|
||||
getGraphInteractionNativeTooltipText({
|
||||
mermaidId: "node",
|
||||
linktext: "SOURCE.md",
|
||||
sourcePath: "SOURCE.md",
|
||||
label: "[[MISSING]]",
|
||||
hoverRows: [{ label: "ref", value: "[[MISSING]]" }],
|
||||
nativeTooltip: "Flow Object: NODE\nref: [[MISSING]]"
|
||||
}),
|
||||
"Flow Object: NODE\nref: [[MISSING]]"
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue