feat(linkml-viewer): add visual indicators for slot_usage overrides
- 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.
This commit is contained in:
parent
7ec4e05dd4
commit
1ad717767a
2 changed files with 96 additions and 17 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) && (
|
||||
<div className="linkml-viewer__class-slots-content">
|
||||
{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 (
|
||||
<div key={slotName} className="linkml-viewer__item linkml-viewer__item--slot-fallback">
|
||||
<h4 className="linkml-viewer__item-name">{slotName}</h4>
|
||||
|
|
@ -2578,7 +2598,8 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const renderSlotDetails = (slot: LinkMLSlot) => {
|
||||
const renderSlotDetails = (slot: LinkMLSlot, slotUsage: Partial<LinkMLSlot> | 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 (
|
||||
<span
|
||||
className={`linkml-viewer__override-marker ${overridden ? 'linkml-viewer__override-marker--overridden' : 'linkml-viewer__override-marker--inherited'}`}
|
||||
title={overridden ? t('overriddenInSlotUsage') : t('inheritedFromGeneric')}
|
||||
>
|
||||
{overridden ? t('slotUsageOverrideMarker') : ''}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={slot.name} className={`linkml-viewer__item ${!matchesFilter ? 'linkml-viewer__item--filtered-out' : ''}`}>
|
||||
<div key={slot.name} className={`linkml-viewer__item ${!matchesFilter ? 'linkml-viewer__item--filtered-out' : ''} ${hasSlotUsageOverrides ? 'linkml-viewer__item--has-usage' : ''}`}>
|
||||
<h4 className="linkml-viewer__item-name">
|
||||
{slot.name}
|
||||
{hasSlotUsageOverrides && <span className="linkml-viewer__badge linkml-viewer__badge--usage" title={t('slotUsageTooltip')}>{t('slotUsageBadge')}</span>}
|
||||
{slot.required && <span className="linkml-viewer__badge linkml-viewer__badge--required">{t('required')}</span>}
|
||||
{slot.multivalued && <span className="linkml-viewer__badge linkml-viewer__badge--multi">{t('multivalued')}</span>}
|
||||
{slot.identifier && <span className="linkml-viewer__badge linkml-viewer__badge--identifier">{t('identifier')}</span>}
|
||||
|
|
@ -2621,13 +2663,13 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
{/* Slot URI - semantic predicate */}
|
||||
{slot.slot_uri && (
|
||||
<div className="linkml-viewer__slot-uri">
|
||||
<span className="linkml-viewer__label">{t('slotUri')}</span>
|
||||
<span className="linkml-viewer__label"><OverrideMarker property="slot_uri" />{t('slotUri')}</span>
|
||||
<code className="linkml-viewer__uri-value">{slot.slot_uri}</code>
|
||||
</div>
|
||||
)}
|
||||
{slot.range && (
|
||||
<div className="linkml-viewer__range">
|
||||
<span className="linkml-viewer__label">{t('range')}</span>
|
||||
<span className="linkml-viewer__label"><OverrideMarker property="range" />{t('range')}</span>
|
||||
{rangeIsEnum ? (
|
||||
<button
|
||||
className={`linkml-viewer__range-enum-toggle ${isExpanded ? 'linkml-viewer__range-enum-toggle--expanded' : ''}`}
|
||||
|
|
@ -2646,19 +2688,20 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
{rangeIsEnum && isExpanded && slot.range && renderEnumValues(slot.name, slot.range)}
|
||||
{slot.description && (
|
||||
<div className="linkml-viewer__description linkml-viewer__markdown">
|
||||
<span className="linkml-viewer__label linkml-viewer__label--inline"><OverrideMarker property="description" /></span>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(slot.description)}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
{slot.pattern && (
|
||||
<div className="linkml-viewer__pattern">
|
||||
<span className="linkml-viewer__label">{t('pattern')}</span>
|
||||
<span className="linkml-viewer__label"><OverrideMarker property="pattern" />{t('pattern')}</span>
|
||||
<code>{slot.pattern}</code>
|
||||
</div>
|
||||
)}
|
||||
{/* Examples section */}
|
||||
{slot.examples && slot.examples.length > 0 && (
|
||||
<div className="linkml-viewer__examples">
|
||||
<span className="linkml-viewer__label">{t('examples')}</span>
|
||||
<span className="linkml-viewer__label"><OverrideMarker property="examples" />{t('examples')}</span>
|
||||
<ul className="linkml-viewer__examples-list">
|
||||
{slot.examples.map((example, idx) => (
|
||||
<li key={idx} className="linkml-viewer__example-item">
|
||||
|
|
@ -2674,7 +2717,7 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
{/* Semantic Mappings - 5 types per LinkML spec */}
|
||||
{slot.exact_mappings && slot.exact_mappings.length > 0 && (
|
||||
<div className="linkml-viewer__mappings">
|
||||
<span className="linkml-viewer__label">{t('exactMappings')}</span>
|
||||
<span className="linkml-viewer__label"><OverrideMarker property="exact_mappings" />{t('exactMappings')}</span>
|
||||
<div className="linkml-viewer__tag-list">
|
||||
{slot.exact_mappings.map(mapping => (
|
||||
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--exact">{mapping}</span>
|
||||
|
|
@ -2684,7 +2727,7 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
)}
|
||||
{slot.close_mappings && slot.close_mappings.length > 0 && (
|
||||
<div className="linkml-viewer__mappings">
|
||||
<span className="linkml-viewer__label">{t('closeMappings')}</span>
|
||||
<span className="linkml-viewer__label"><OverrideMarker property="close_mappings" />{t('closeMappings')}</span>
|
||||
<div className="linkml-viewer__tag-list">
|
||||
{slot.close_mappings.map(mapping => (
|
||||
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--close">{mapping}</span>
|
||||
|
|
@ -2694,7 +2737,7 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
)}
|
||||
{slot.narrow_mappings && slot.narrow_mappings.length > 0 && (
|
||||
<div className="linkml-viewer__mappings">
|
||||
<span className="linkml-viewer__label">{t('narrowMappings')}</span>
|
||||
<span className="linkml-viewer__label"><OverrideMarker property="narrow_mappings" />{t('narrowMappings')}</span>
|
||||
<div className="linkml-viewer__tag-list">
|
||||
{slot.narrow_mappings.map(mapping => (
|
||||
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--narrow">{mapping}</span>
|
||||
|
|
@ -2704,7 +2747,7 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
)}
|
||||
{slot.broad_mappings && slot.broad_mappings.length > 0 && (
|
||||
<div className="linkml-viewer__mappings">
|
||||
<span className="linkml-viewer__label">{t('broadMappings')}</span>
|
||||
<span className="linkml-viewer__label"><OverrideMarker property="broad_mappings" />{t('broadMappings')}</span>
|
||||
<div className="linkml-viewer__tag-list">
|
||||
{slot.broad_mappings.map(mapping => (
|
||||
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--broad">{mapping}</span>
|
||||
|
|
@ -2714,7 +2757,7 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
)}
|
||||
{slot.related_mappings && slot.related_mappings.length > 0 && (
|
||||
<div className="linkml-viewer__mappings">
|
||||
<span className="linkml-viewer__label">{t('relatedMappings')}</span>
|
||||
<span className="linkml-viewer__label"><OverrideMarker property="related_mappings" />{t('relatedMappings')}</span>
|
||||
<div className="linkml-viewer__tag-list">
|
||||
{slot.related_mappings.map(mapping => (
|
||||
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--related">{mapping}</span>
|
||||
|
|
@ -2725,7 +2768,7 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
{/* Comments section */}
|
||||
{slot.comments && slot.comments.length > 0 && (
|
||||
<div className="linkml-viewer__comments">
|
||||
<span className="linkml-viewer__label">{t('comments')}</span>
|
||||
<span className="linkml-viewer__label"><OverrideMarker property="comments" />{t('comments')}</span>
|
||||
<ul className="linkml-viewer__comments-list">
|
||||
{slot.comments.map((comment, idx) => (
|
||||
<li key={idx} className="linkml-viewer__comment-item">{comment}</li>
|
||||
|
|
@ -2989,7 +3032,7 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
</button>
|
||||
{expandedSections.has('slots') && (
|
||||
<div className="linkml-viewer__section-content">
|
||||
{slots.map(renderSlotDetails)}
|
||||
{slots.map(slot => renderSlotDetails(slot))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue