|
1 | 1 | // Author: Eryk Kulikowski @ KU Leuven (2025). Apache 2.0 License |
2 | | - |
3 | | -// === CDI Previewer: Tree Rendering & Node/Property Display === |
4 | | - |
5 | 2 | import { |
6 | 3 | getJsonData, |
7 | 4 | getIsEditMode, |
@@ -93,11 +90,9 @@ export function renderData() { |
93 | 90 |
|
94 | 91 | // Helper: Check if a value is a pure reference (no properties other than @id, @type, @context) |
95 | 92 | function isPureReference(value) { |
96 | | - // String reference like "#Sample_Key" |
97 | 93 | if (typeof value === "string" && isNodeReference(value)) { |
98 | 94 | return true; |
99 | 95 | } |
100 | | - // Object reference like {"@id": "#Sample_Key"} |
101 | 96 | if (value && typeof value === "object" && !Array.isArray(value)) { |
102 | 97 | const keys = Object.keys(value); |
103 | 98 | const nonMetadataKeys = keys.filter( |
@@ -331,13 +326,129 @@ export function renderPropertyTree( |
331 | 326 | container.append(childCard); |
332 | 327 | } |
333 | 328 | // else: Node already rendered elsewhere - the inline box already provides navigation |
| 329 | + } else { |
| 330 | + // The referenced ID wasn't found as a top-level node in the @graph. |
| 331 | + // This can happen when an object is inlined (has an @id plus nested |
| 332 | + // properties) inside the parent and isn't a separate graph node. In |
| 333 | + // that case we should find the inline object and render it inline so |
| 334 | + // nested fields are visible (instead of only showing a jump button). |
| 335 | + const tryInline = (val) => { |
| 336 | + if (!val) { |
| 337 | + return null; |
| 338 | + } |
| 339 | + if (Array.isArray(val)) { |
| 340 | + return val.find( |
| 341 | + (it) => |
| 342 | + it && |
| 343 | + typeof it === "object" && |
| 344 | + it["@id"] === refId && |
| 345 | + !isPureReference(it) |
| 346 | + ); |
| 347 | + } |
| 348 | + if ( |
| 349 | + typeof val === "object" && |
| 350 | + val !== null && |
| 351 | + val["@id"] === refId && |
| 352 | + !isPureReference(val) |
| 353 | + ) { |
| 354 | + return val; |
| 355 | + } |
| 356 | + return null; |
| 357 | + }; |
| 358 | + |
| 359 | + const inlineObj = tryInline(value); |
| 360 | + if (inlineObj) { |
| 361 | + // Track parent-child relationship using the inline object's @id |
| 362 | + setParentRelationship(refId, nodeId); |
| 363 | + |
| 364 | + const inlineCard = renderInlineObject(inlineObj, nodeId); |
| 365 | + if (inlineCard) { |
| 366 | + // Append inline node view at same depth |
| 367 | + container.append(inlineCard); |
| 368 | + // Mark as rendered to avoid duplicate rendering |
| 369 | + markNodeRendered(refId); |
| 370 | + } |
| 371 | + } |
334 | 372 | } |
335 | 373 | }); |
336 | 374 | } |
337 | 375 |
|
338 | 376 | return container; |
339 | 377 | } |
340 | 378 |
|
| 379 | +// Top-level helper: render a nested inline object as a small inline node card. |
| 380 | +// Accepts optional parent node id for context when rendering nested properties. |
| 381 | +function renderInlineObject(val, parentNodeId) { |
| 382 | + if (!val || typeof val !== "object" || Array.isArray(val)) { |
| 383 | + return null; |
| 384 | + } |
| 385 | + |
| 386 | + // Skip pure references - let them be rendered as clickable buttons |
| 387 | + if (isPureReference(val)) { |
| 388 | + return null; |
| 389 | + } |
| 390 | + |
| 391 | + const inlineCard = $("<div>").addClass("node-card inline-node-card").css({ |
| 392 | + "margin-top": "5px", |
| 393 | + "margin-bottom": "5px", |
| 394 | + }); |
| 395 | + |
| 396 | + const header = $("<div>").addClass("node-header"); |
| 397 | + const leftSide = $("<div>") |
| 398 | + .css("display", "flex") |
| 399 | + .css("align-items", "center"); |
| 400 | + |
| 401 | + leftSide.append( |
| 402 | + $("<span>") |
| 403 | + .addClass("glyphicon glyphicon-chevron-down collapse-icon") |
| 404 | + .css("margin-right", "10px") |
| 405 | + ); |
| 406 | + |
| 407 | + const nestedId = val["@id"]; |
| 408 | + if (nestedId) { |
| 409 | + leftSide.append($("<span>").addClass("node-id").text(nestedId)); |
| 410 | + } |
| 411 | + |
| 412 | + const nestedTypes = Array.isArray(val["@type"]) |
| 413 | + ? val["@type"] |
| 414 | + : val["@type"] |
| 415 | + ? [val["@type"]] |
| 416 | + : []; |
| 417 | + nestedTypes.forEach((t) => { |
| 418 | + if (t) { |
| 419 | + leftSide.append($("<span>").addClass("node-type").text(t)); |
| 420 | + } |
| 421 | + }); |
| 422 | + |
| 423 | + header.append(leftSide); |
| 424 | + header.click(function () { |
| 425 | + inlineCard.toggleClass("collapsed"); |
| 426 | + }); |
| 427 | + |
| 428 | + inlineCard.append(header); |
| 429 | + |
| 430 | + const body = $("<div>").addClass("node-body"); |
| 431 | + if (!getIsEditMode()) { |
| 432 | + body.addClass("view-mode"); |
| 433 | + } |
| 434 | + |
| 435 | + Object.keys(val).forEach((k) => { |
| 436 | + if (k === "@id" || k === "@type" || k === "@context") { |
| 437 | + return; |
| 438 | + } |
| 439 | + const nestedRow = renderProperty( |
| 440 | + k, |
| 441 | + val[k], |
| 442 | + nestedId || parentNodeId, |
| 443 | + nestedTypes |
| 444 | + ); |
| 445 | + body.append(nestedRow); |
| 446 | + }); |
| 447 | + |
| 448 | + inlineCard.append(body); |
| 449 | + return inlineCard; |
| 450 | +} |
| 451 | + |
341 | 452 | function renderProperty(key, value, nodeId, nodeTypes) { |
342 | 453 | const row = $("<div>") |
343 | 454 | .addClass("property-row") |
@@ -403,85 +514,15 @@ function renderProperty(key, value, nodeId, nodeTypes) { |
403 | 514 | .addClass("property-value") |
404 | 515 | .attr("data-testid", "property-value"); |
405 | 516 |
|
406 | | - // Helper: render a nested object value using a small inline node card |
407 | | - function renderInlineObject(val) { |
408 | | - if (!val || typeof val !== "object" || Array.isArray(val)) { |
409 | | - return null; |
410 | | - } |
411 | | - |
412 | | - // Skip pure references - let them be rendered as clickable buttons |
413 | | - if (isPureReference(val)) { |
414 | | - return null; |
415 | | - } |
416 | | - |
417 | | - const inlineCard = $("").addClass("node-card inline-node-card").css({ |
418 | | - "margin-top": "5px", |
419 | | - "margin-bottom": "5px", |
420 | | - }); |
421 | | - |
422 | | - const header = $("<div>").addClass("node-header"); |
423 | | - const leftSide = $("<div>") |
424 | | - .css("display", "flex") |
425 | | - .css("align-items", "center"); |
426 | | - |
427 | | - leftSide.append( |
428 | | - $("<span>") |
429 | | - .addClass("glyphicon glyphicon-chevron-down collapse-icon") |
430 | | - .css("margin-right", "10px") |
431 | | - ); |
432 | | - |
433 | | - const nestedId = val["@id"]; |
434 | | - if (nestedId) { |
435 | | - leftSide.append($("<span>").addClass("node-id").text(nestedId)); |
436 | | - } |
437 | | - |
438 | | - const nestedTypes = Array.isArray(val["@type"]) |
439 | | - ? val["@type"] |
440 | | - : val["@type"] |
441 | | - ? [val["@type"]] |
442 | | - : []; |
443 | | - nestedTypes.forEach((t) => { |
444 | | - if (t) { |
445 | | - leftSide.append($("<span>").addClass("node-type").text(t)); |
446 | | - } |
447 | | - }); |
448 | | - |
449 | | - header.append(leftSide); |
450 | | - header.click(function () { |
451 | | - inlineCard.toggleClass("collapsed"); |
452 | | - }); |
453 | | - |
454 | | - inlineCard.append(header); |
455 | | - |
456 | | - const body = $("<div>").addClass("node-body"); |
457 | | - if (!getIsEditMode()) { |
458 | | - body.addClass("view-mode"); |
459 | | - } |
460 | | - |
461 | | - Object.keys(val).forEach((k) => { |
462 | | - if (k === "@id" || k === "@type" || k === "@context") { |
463 | | - return; |
464 | | - } |
465 | | - const nestedRow = renderProperty( |
466 | | - k, |
467 | | - val[k], |
468 | | - nestedId || nodeId, |
469 | | - nestedTypes |
470 | | - ); |
471 | | - body.append(nestedRow); |
472 | | - }); |
473 | | - |
474 | | - inlineCard.append(body); |
475 | | - return inlineCard; |
476 | | - } |
| 517 | + // Note: Uses top-level renderInlineObject helper for inlined object rendering |
477 | 518 |
|
478 | 519 | if (Array.isArray(value)) { |
479 | 520 | // Array of values |
480 | 521 | value.forEach((val, idx) => { |
481 | 522 | const valDiv = $("<div>").addClass("array-value"); |
482 | 523 |
|
483 | 524 | // Try to render nested objects (like schema:Role) as inline node cards |
484 | | - const inlineCard = renderInlineObject(val); |
| 525 | + const inlineCard = renderInlineObject(val, nodeId); |
485 | 526 | if (inlineCard) { |
486 | 527 | valDiv.append(inlineCard); |
487 | 528 | } else { |
@@ -583,7 +624,7 @@ function renderProperty(key, value, nodeId, nodeTypes) { |
583 | 624 | } |
584 | 625 | } else { |
585 | 626 | // Single value |
586 | | - const inlineCard = renderInlineObject(value); |
| 627 | + const inlineCard = renderInlineObject(value, nodeId); |
587 | 628 | if (inlineCard) { |
588 | 629 | valueContainer.append(inlineCard); |
589 | 630 | } else { |
@@ -820,7 +861,12 @@ function showAddReferenceModal( |
820 | 861 | export function createValueInput(value, classification) { |
821 | 862 | // Check if value is a reference (either format) |
822 | 863 | const refId = extractReferenceId(value); |
823 | | - if (refId) { |
| 864 | + // Only treat as a clickable "reference" if this is a pure reference |
| 865 | + // (string-style '#id' or object with only @id and metadata). If the |
| 866 | + // value is an object that contains @id PLUS other properties, we |
| 867 | + // want to render it inline (showing its nested fields) instead of |
| 868 | + // hiding it behind a jump button. |
| 869 | + if (refId && isPureReference(value)) { |
824 | 870 | const refContainer = $("<div>") |
825 | 871 | .addClass("reference-container") |
826 | 872 | .attr( |
|
0 commit comments