From 1ad717767a412fe0f032b7ac1622c60ca674a217 Mon Sep 17 00:00:00 2001 From: kempersc Date: Fri, 9 Jan 2026 18:23:21 +0100 Subject: [PATCH] feat(linkml-viewer): add visual indicators for slot_usage overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add green 'slot_usage' badge for slots with class-specific overrides - Add ✦ markers next to properties that are overridden vs inherited - Add green left border styling for slots with slot_usage - Add i18n translations (nl/en) for override indicators - Merge generic slot definitions with class-specific slot_usage properties This helps users understand which slot properties come from the generic slot definition vs which are overridden at the class level via slot_usage. --- frontend/src/pages/LinkMLViewerPage.css | 36 ++++++++++++ frontend/src/pages/LinkMLViewerPage.tsx | 77 +++++++++++++++++++------ 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/frontend/src/pages/LinkMLViewerPage.css b/frontend/src/pages/LinkMLViewerPage.css index 9798795d96..9688ace2a8 100644 --- a/frontend/src/pages/LinkMLViewerPage.css +++ b/frontend/src/pages/LinkMLViewerPage.css @@ -672,6 +672,42 @@ color: var(--warning-color, #f57c00); } +.linkml-viewer__badge--usage { + background: var(--slot-usage-light, #e8f5e9); + color: var(--slot-usage-color, #388e3c); + font-weight: 500; +} + +/* Visual indicator for slots with slot_usage overrides */ +.linkml-viewer__item--has-usage { + border-left: 3px solid var(--slot-usage-color, #388e3c); + padding-left: 0.75rem; +} + +/* Override markers - show which properties come from slot_usage */ +.linkml-viewer__override-marker { + display: inline-block; + font-size: 0.75rem; + margin-right: 0.25rem; + font-weight: 600; +} + +.linkml-viewer__override-marker--overridden { + color: var(--slot-usage-color, #388e3c); + cursor: help; +} + +.linkml-viewer__override-marker--inherited { + /* Inherited properties don't show any marker */ + display: none; +} + +/* Label with inline override marker */ +.linkml-viewer__label--inline { + display: inline; + margin-right: 0; +} + /* Custodian Type Badge - shows which GLAMORCUBESFIXPHDNT types apply */ .linkml-viewer__custodian-badge { margin-left: 0.5rem; diff --git a/frontend/src/pages/LinkMLViewerPage.tsx b/frontend/src/pages/LinkMLViewerPage.tsx index 5b51b1dd8b..8491b1d5a6 100644 --- a/frontend/src/pages/LinkMLViewerPage.tsx +++ b/frontend/src/pages/LinkMLViewerPage.tsx @@ -968,6 +968,15 @@ const TEXT = { broadMappings: { nl: 'Bredere mappings:', en: 'Broad Mappings:' }, relatedMappings: { nl: 'Gerelateerde mappings:', en: 'Related Mappings:' }, comments: { nl: 'Opmerkingen:', en: 'Comments:' }, + // Slot usage indicator + slotUsageBadge: { nl: 'slot_usage', en: 'slot_usage' }, + slotUsageTooltip: { + nl: 'Deze slot heeft klasse-specifieke overschrijvingen gedefinieerd in slot_usage. Eigenschappen met ✦ komen uit de slot_usage, andere zijn overgenomen van de generieke slot-definitie.', + en: 'This slot has class-specific overrides defined in slot_usage. Properties marked with ✦ come from slot_usage, others are inherited from the generic slot definition.' + }, + slotUsageOverrideMarker: { nl: '✦', en: '✦' }, + inheritedFromGeneric: { nl: 'overgenomen', en: 'inherited' }, + overriddenInSlotUsage: { nl: 'overschreven in slot_usage', en: 'overridden in slot_usage' }, }; // Dynamically discover schema files from the modules directory @@ -2048,11 +2057,22 @@ const LinkMLViewerPage: React.FC = () => { {expandedClassSlots.has(cls.name) && (
{cls.slots.map(slotName => { - const slot = slotLookupMap.get(slotName); - if (slot) { - return renderSlotDetails(slot); + // Get generic slot definition from the global slot lookup + const genericSlot = slotLookupMap.get(slotName); + // Get class-specific slot_usage overrides (if any) + const slotUsage = cls.slot_usage?.[slotName]; + + // Merge generic slot with slot_usage overrides + // slot_usage properties take precedence over generic slot properties + if (genericSlot || slotUsage) { + const mergedSlot: LinkMLSlot = { + name: slotName, + ...(genericSlot || {}), + ...(slotUsage || {}), + }; + return renderSlotDetails(mergedSlot, slotUsage || null); } - // Fallback: show slot name as tag if definition not found + // Fallback: show slot name as tag if neither generic definition nor slot_usage found return (

{slotName}

@@ -2578,7 +2598,8 @@ const LinkMLViewerPage: React.FC = () => { ); }; - const renderSlotDetails = (slot: LinkMLSlot) => { + const renderSlotDetails = (slot: LinkMLSlot, slotUsage: Partial | null = null) => { + const hasSlotUsageOverrides = slotUsage !== null; const rangeIsEnum = slot.range && isEnumRange(slot.range); const enumKey = slot.range ? `${slot.name}:${slot.range}` : ''; const isExpanded = expandedEnumRanges.has(enumKey); @@ -2589,10 +2610,31 @@ const LinkMLViewerPage: React.FC = () => { // Check if this slot matches the current custodian type filter (multi-select) const matchesFilter = custodianTypeFilter.size === 0 || custodianTypes.some(t => custodianTypeFilter.has(t)); + // Helper to check if a property is overridden in slot_usage + const isOverridden = (property: keyof LinkMLSlot): boolean => { + if (!slotUsage) return false; + return slotUsage[property] !== undefined; + }; + + // Helper to render override marker + const OverrideMarker = ({ property }: { property: keyof LinkMLSlot }) => { + if (!hasSlotUsageOverrides) return null; + const overridden = isOverridden(property); + return ( + + {overridden ? t('slotUsageOverrideMarker') : ''} + + ); + }; + return ( -
+

{slot.name} + {hasSlotUsageOverrides && {t('slotUsageBadge')}} {slot.required && {t('required')}} {slot.multivalued && {t('multivalued')}} {slot.identifier && {t('identifier')}} @@ -2621,13 +2663,13 @@ const LinkMLViewerPage: React.FC = () => { {/* Slot URI - semantic predicate */} {slot.slot_uri && (
- {t('slotUri')} + {t('slotUri')} {slot.slot_uri}
)} {slot.range && (
- {t('range')} + {t('range')} {rangeIsEnum ? ( {expandedSections.has('slots') && (
- {slots.map(renderSlotDetails)} + {slots.map(slot => renderSlotDetails(slot))}
)}