feat(linkml): Add slot_usage comparison popup in schema viewer
- Add 'slot_usage' type to SchemaElementPopup for comparing generic slots vs class overrides - Show side-by-side comparison table with property, generic value, and override value - Display green 'changed' badges for modified properties - Add dual navigation buttons (Go to class / Go to slot) - Include comprehensive dark mode support - Match styling to main page's comparison view (green color scheme)
This commit is contained in:
parent
853419d6c2
commit
a981bb7ca3
4 changed files with 466 additions and 11 deletions
|
|
@ -477,3 +477,214 @@
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Slot Usage Comparison View Styles
|
||||||
|
Matches LinkMLViewerPage.css formatting
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Overrides list - shown at top of comparison view */
|
||||||
|
.schema-popup__overrides-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__override-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #388e3c;
|
||||||
|
border: 1px solid #388e3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comparison panel container - matches main page */
|
||||||
|
.schema-popup__slot-comparison {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
border: 1px solid #388e3c;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comparison header with three columns */
|
||||||
|
.schema-popup__slot-comparison-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 90px 1fr 1fr;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: #e8f5e9;
|
||||||
|
border-bottom: 1px solid #388e3c;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #388e3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__slot-comparison-header h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comparison content rows */
|
||||||
|
.schema-popup__slot-comparison-content {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__slot-comparison-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 90px 1fr 1fr;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 11px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__slot-comparison-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__slot-comparison-row--changed {
|
||||||
|
background: rgba(56, 142, 60, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Property label column */
|
||||||
|
.schema-popup__slot-comparison-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic value column */
|
||||||
|
.schema-popup__slot-comparison-generic {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__slot-comparison-generic code,
|
||||||
|
.schema-popup__slot-comparison-override code {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override value column */
|
||||||
|
.schema-popup__slot-comparison-override {
|
||||||
|
word-break: break-word;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.375rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__slot-comparison-override--diff code {
|
||||||
|
background: #e8f5e9;
|
||||||
|
border: 1px solid #388e3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Diff badge */
|
||||||
|
.schema-popup__slot-comparison-diff-badge {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: #388e3c;
|
||||||
|
color: white;
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty/inherited states */
|
||||||
|
.schema-popup__slot-comparison-empty,
|
||||||
|
.schema-popup__slot-comparison-inherited {
|
||||||
|
font-style: italic;
|
||||||
|
color: #999;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dual action buttons */
|
||||||
|
.schema-popup__actions--dual {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__actions--dual .schema-popup__navigate {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__navigate--secondary {
|
||||||
|
background: linear-gradient(135deg, #059669 0%, #10b981 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__navigate--secondary:hover {
|
||||||
|
background: linear-gradient(135deg, #047857 0%, #059669 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode for slot_usage comparison */
|
||||||
|
[data-theme="dark"] .schema-popup__override-badge {
|
||||||
|
background: rgba(56, 142, 60, 0.15);
|
||||||
|
color: #86efac;
|
||||||
|
border-color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__slot-comparison {
|
||||||
|
border-color: #4ade80;
|
||||||
|
background: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__slot-comparison-header {
|
||||||
|
background: rgba(56, 142, 60, 0.15);
|
||||||
|
color: #86efac;
|
||||||
|
border-bottom-color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__slot-comparison-row {
|
||||||
|
border-bottom-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__slot-comparison-row--changed {
|
||||||
|
background: rgba(56, 142, 60, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__slot-comparison-label {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__slot-comparison-generic code,
|
||||||
|
[data-theme="dark"] .schema-popup__slot-comparison-override code {
|
||||||
|
background: #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__slot-comparison-override--diff code {
|
||||||
|
background: rgba(56, 142, 60, 0.15);
|
||||||
|
border-color: #4ade80;
|
||||||
|
color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__slot-comparison-diff-badge {
|
||||||
|
background: #4ade80;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__slot-comparison-empty,
|
||||||
|
[data-theme="dark"] .schema-popup__slot-comparison-inherited {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { linkmlSchemaService } from '../../lib/linkml/linkml-schema-service';
|
||||||
import { isTargetInsideAny } from '../../utils/dom';
|
import { isTargetInsideAny } from '../../utils/dom';
|
||||||
import './SchemaElementPopup.css';
|
import './SchemaElementPopup.css';
|
||||||
|
|
||||||
export type SchemaElementType = 'class' | 'slot' | 'enum';
|
export type SchemaElementType = 'class' | 'slot' | 'enum' | 'slot_usage';
|
||||||
|
|
||||||
interface SchemaElementPopupProps {
|
interface SchemaElementPopupProps {
|
||||||
/** The element name to look up */
|
/** The element name to look up */
|
||||||
|
|
@ -30,6 +30,10 @@ interface SchemaElementPopupProps {
|
||||||
onNavigate: (elementName: string, elementType: SchemaElementType) => void;
|
onNavigate: (elementName: string, elementType: SchemaElementType) => void;
|
||||||
/** Initial position (optional, defaults to center) */
|
/** Initial position (optional, defaults to center) */
|
||||||
initialPosition?: { x: number; y: number };
|
initialPosition?: { x: number; y: number };
|
||||||
|
/** For slot_usage type: the slot name being overridden */
|
||||||
|
slotName?: string;
|
||||||
|
/** For slot_usage type: list of overridden properties */
|
||||||
|
overrides?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Position {
|
interface Position {
|
||||||
|
|
@ -71,14 +75,44 @@ interface ElementInfo {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slot usage comparison info - shows how a class overrides a generic slot
|
||||||
|
*/
|
||||||
|
interface SlotUsageComparisonInfo {
|
||||||
|
slotName: string;
|
||||||
|
className: string;
|
||||||
|
overrides: string[];
|
||||||
|
genericSlot: {
|
||||||
|
description?: string;
|
||||||
|
range?: string;
|
||||||
|
required?: boolean;
|
||||||
|
multivalued?: boolean;
|
||||||
|
slot_uri?: string;
|
||||||
|
pattern?: string;
|
||||||
|
identifier?: boolean;
|
||||||
|
};
|
||||||
|
slotUsage: {
|
||||||
|
description?: string;
|
||||||
|
range?: string;
|
||||||
|
required?: boolean;
|
||||||
|
multivalued?: boolean;
|
||||||
|
slot_uri?: string;
|
||||||
|
pattern?: string;
|
||||||
|
identifier?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const SchemaElementPopup: React.FC<SchemaElementPopupProps> = ({
|
export const SchemaElementPopup: React.FC<SchemaElementPopupProps> = ({
|
||||||
elementName,
|
elementName,
|
||||||
elementType,
|
elementType,
|
||||||
onClose,
|
onClose,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
initialPosition,
|
initialPosition,
|
||||||
|
slotName,
|
||||||
|
overrides,
|
||||||
}) => {
|
}) => {
|
||||||
const [elementInfo, setElementInfo] = useState<ElementInfo | null>(null);
|
const [elementInfo, setElementInfo] = useState<ElementInfo | null>(null);
|
||||||
|
const [slotUsageComparison, setSlotUsageComparison] = useState<SlotUsageComparisonInfo | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -151,6 +185,49 @@ export const SchemaElementPopup: React.FC<SchemaElementPopupProps> = ({
|
||||||
valueCount: enumInfo.values?.length || 0,
|
valueCount: enumInfo.values?.length || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (elementType === 'slot_usage' && slotName) {
|
||||||
|
// For slot_usage comparison: load both generic slot definition and class's slot_usage
|
||||||
|
const [genericSlotDef, classDef] = await Promise.all([
|
||||||
|
linkmlSchemaService.getSlotDefinition(slotName),
|
||||||
|
linkmlSchemaService.getClassDefinition(elementName),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (genericSlotDef && classDef) {
|
||||||
|
const slotUsage = classDef.slot_usage?.[slotName] || {};
|
||||||
|
|
||||||
|
setSlotUsageComparison({
|
||||||
|
slotName,
|
||||||
|
className: elementName,
|
||||||
|
overrides: overrides || [],
|
||||||
|
genericSlot: {
|
||||||
|
description: genericSlotDef.description,
|
||||||
|
range: genericSlotDef.range,
|
||||||
|
required: genericSlotDef.required,
|
||||||
|
multivalued: genericSlotDef.multivalued,
|
||||||
|
slot_uri: genericSlotDef.slot_uri,
|
||||||
|
pattern: genericSlotDef.pattern,
|
||||||
|
identifier: genericSlotDef.identifier,
|
||||||
|
},
|
||||||
|
slotUsage: {
|
||||||
|
description: slotUsage.description,
|
||||||
|
range: slotUsage.range,
|
||||||
|
required: slotUsage.required,
|
||||||
|
multivalued: slotUsage.multivalued,
|
||||||
|
slot_uri: slotUsage.slot_uri,
|
||||||
|
pattern: slotUsage.pattern,
|
||||||
|
identifier: slotUsage.identifier,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set a minimal info object for the slot_usage case
|
||||||
|
info = {
|
||||||
|
name: `${elementName}.${slotName}`,
|
||||||
|
type: 'slot_usage',
|
||||||
|
description: slotUsage.description || genericSlotDef.description,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
setError(`Could not find slot "${slotName}" or class "${elementName}"`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info) {
|
if (info) {
|
||||||
|
|
@ -167,7 +244,7 @@ export const SchemaElementPopup: React.FC<SchemaElementPopupProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
loadInfo();
|
loadInfo();
|
||||||
}, [elementName, elementType]);
|
}, [elementName, elementType, slotName, overrides]);
|
||||||
|
|
||||||
// Center the popup on initial load if no position provided
|
// Center the popup on initial load if no position provided
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
|
@ -350,13 +427,15 @@ export const SchemaElementPopup: React.FC<SchemaElementPopupProps> = ({
|
||||||
'class': '#3B82F6', // Blue
|
'class': '#3B82F6', // Blue
|
||||||
'slot': '#10B981', // Green
|
'slot': '#10B981', // Green
|
||||||
'enum': '#F59E0B', // Amber
|
'enum': '#F59E0B', // Amber
|
||||||
|
'slot_usage': '#8B5CF6', // Purple for slot_usage comparison
|
||||||
};
|
};
|
||||||
|
const displayType = type === 'slot_usage' ? 'slot override' : type;
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="schema-popup__type-badge"
|
className="schema-popup__type-badge"
|
||||||
style={{ backgroundColor: colors[type] }}
|
style={{ backgroundColor: colors[type] }}
|
||||||
>
|
>
|
||||||
{type}
|
{displayType}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -423,14 +502,130 @@ export const SchemaElementPopup: React.FC<SchemaElementPopupProps> = ({
|
||||||
|
|
||||||
{!loading && !error && elementInfo && (
|
{!loading && !error && elementInfo && (
|
||||||
<>
|
<>
|
||||||
{/* Description */}
|
{/* Slot Usage Comparison View - matches LinkMLViewerPage format */}
|
||||||
{elementInfo.description && (
|
{elementType === 'slot_usage' && slotUsageComparison && (
|
||||||
<div className="schema-popup__section">
|
<>
|
||||||
<span className="schema-popup__section-label">Description</span>
|
{/* Header info - class and slot names */}
|
||||||
<p className="schema-popup__description">{elementInfo.description}</p>
|
<div className="schema-popup__section">
|
||||||
</div>
|
<span className="schema-popup__section-label">Class</span>
|
||||||
|
<code className="schema-popup__value">{slotUsageComparison.className}</code>
|
||||||
|
</div>
|
||||||
|
<div className="schema-popup__section">
|
||||||
|
<span className="schema-popup__section-label">Slot</span>
|
||||||
|
<code className="schema-popup__value">{slotUsageComparison.slotName}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Overridden properties summary as badges */}
|
||||||
|
{slotUsageComparison.overrides.length > 0 && (
|
||||||
|
<div className="schema-popup__section">
|
||||||
|
<span className="schema-popup__section-label">Overridden Properties</span>
|
||||||
|
<div className="schema-popup__overrides-list">
|
||||||
|
{slotUsageComparison.overrides.map(prop => (
|
||||||
|
<span key={prop} className="schema-popup__override-badge">{prop}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Side-by-side comparison - uses same structure as main page */}
|
||||||
|
<div className="schema-popup__slot-comparison">
|
||||||
|
<div className="schema-popup__slot-comparison-header">
|
||||||
|
<span></span>
|
||||||
|
<h5>Generic Slot</h5>
|
||||||
|
<h5>Class Override</h5>
|
||||||
|
</div>
|
||||||
|
<div className="schema-popup__slot-comparison-content">
|
||||||
|
{/* Compare each property */}
|
||||||
|
{(['range', 'description', 'required', 'multivalued', 'slot_uri', 'pattern', 'identifier'] as const).map(prop => {
|
||||||
|
const genericVal = slotUsageComparison.genericSlot[prop];
|
||||||
|
const overrideVal = slotUsageComparison.slotUsage[prop];
|
||||||
|
const hasGeneric = genericVal !== undefined && genericVal !== null;
|
||||||
|
const hasOverride = overrideVal !== undefined && overrideVal !== null;
|
||||||
|
const isChanged = hasOverride && hasGeneric && genericVal !== overrideVal;
|
||||||
|
const isNewInOverride = hasOverride && !hasGeneric;
|
||||||
|
|
||||||
|
// Skip if neither has a value
|
||||||
|
if (!hasGeneric && !hasOverride) return null;
|
||||||
|
|
||||||
|
const formatValue = (val: unknown): string => {
|
||||||
|
if (val === undefined || val === null) return '';
|
||||||
|
if (typeof val === 'boolean') return val ? 'true' : 'false';
|
||||||
|
const str = String(val);
|
||||||
|
// Longer truncation for description, shorter for other fields
|
||||||
|
const maxLen = prop === 'description' ? 80 : 50;
|
||||||
|
return str.length > maxLen ? str.substring(0, maxLen - 3) + '...' : str;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={prop}
|
||||||
|
className={`schema-popup__slot-comparison-row ${isChanged || isNewInOverride ? 'schema-popup__slot-comparison-row--changed' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="schema-popup__slot-comparison-label">{prop}</div>
|
||||||
|
<div className="schema-popup__slot-comparison-generic">
|
||||||
|
{hasGeneric ? (
|
||||||
|
<code title={typeof genericVal === 'string' ? genericVal : undefined}>
|
||||||
|
{formatValue(genericVal)}
|
||||||
|
</code>
|
||||||
|
) : (
|
||||||
|
<span className="schema-popup__slot-comparison-empty">(not defined)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={`schema-popup__slot-comparison-override ${isChanged || isNewInOverride ? 'schema-popup__slot-comparison-override--diff' : ''}`}>
|
||||||
|
{hasOverride ? (
|
||||||
|
<>
|
||||||
|
<code title={typeof overrideVal === 'string' ? overrideVal : undefined}>
|
||||||
|
{formatValue(overrideVal)}
|
||||||
|
</code>
|
||||||
|
{(isChanged || isNewInOverride) && (
|
||||||
|
<span className="schema-popup__slot-comparison-diff-badge">changed</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="schema-popup__slot-comparison-inherited">(inherited)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigate buttons - one for class, one for slot */}
|
||||||
|
<div className="schema-popup__actions schema-popup__actions--dual">
|
||||||
|
<button
|
||||||
|
className="schema-popup__navigate"
|
||||||
|
onClick={() => {
|
||||||
|
onNavigate(slotUsageComparison.className, 'class');
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Go to class →
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="schema-popup__navigate schema-popup__navigate--secondary"
|
||||||
|
onClick={() => {
|
||||||
|
onNavigate(slotUsageComparison.slotName, 'slot');
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Go to slot →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Regular element view (non-slot_usage) */}
|
||||||
|
{elementType !== 'slot_usage' && (
|
||||||
|
<>
|
||||||
|
{/* Description */}
|
||||||
|
{elementInfo.description && (
|
||||||
|
<div className="schema-popup__section">
|
||||||
|
<span className="schema-popup__section-label">Description</span>
|
||||||
|
<p className="schema-popup__description">{elementInfo.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* URI */}
|
{/* URI */}
|
||||||
{elementInfo.uri && (
|
{elementInfo.uri && (
|
||||||
<div className="schema-popup__section">
|
<div className="schema-popup__section">
|
||||||
|
|
@ -524,6 +719,8 @@ export const SchemaElementPopup: React.FC<SchemaElementPopupProps> = ({
|
||||||
Go to {elementType} →
|
Go to {elementType} →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1984,6 +1984,41 @@ class LinkMLSchemaService {
|
||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw ClassDefinition for a class.
|
||||||
|
* Useful for accessing slot_usage and other class-level metadata directly.
|
||||||
|
*/
|
||||||
|
async getClassDefinition(className: string): Promise<ClassDefinition | null> {
|
||||||
|
await this.initialize();
|
||||||
|
|
||||||
|
const schema = this.classSchemas.get(className);
|
||||||
|
const classDef = schema?.classes?.[className];
|
||||||
|
|
||||||
|
if (!classDef) {
|
||||||
|
debugLog(`[LinkMLSchemaService] getClassDefinition: class ${className} not found`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return classDef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw SlotDefinition for a slot.
|
||||||
|
* Useful for accessing all properties including pattern, identifier, etc.
|
||||||
|
*/
|
||||||
|
async getSlotDefinition(slotName: string): Promise<SlotDefinition | null> {
|
||||||
|
await this.initialize();
|
||||||
|
|
||||||
|
const slotDef = this.slotSchemas.get(slotName);
|
||||||
|
|
||||||
|
if (!slotDef) {
|
||||||
|
debugLog(`[LinkMLSchemaService] getSlotDefinition: slot ${slotName} not found`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return slotDef;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get dependency count for a single class.
|
* Get dependency count for a single class.
|
||||||
* More efficient than getting full import/export info when only count is needed.
|
* More efficient than getting full import/export info when only count is needed.
|
||||||
|
|
|
||||||
|
|
@ -1301,7 +1301,12 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
const [ontologyPopupCurie, setOntologyPopupCurie] = useState<string | null>(null);
|
const [ontologyPopupCurie, setOntologyPopupCurie] = useState<string | null>(null);
|
||||||
|
|
||||||
// State for schema element popup (shows when clicking class/slot/enum links in Imports/Exports sections)
|
// State for schema element popup (shows when clicking class/slot/enum links in Imports/Exports sections)
|
||||||
const [schemaElementPopup, setSchemaElementPopup] = useState<{ name: string; type: SchemaElementType } | null>(null);
|
const [schemaElementPopup, setSchemaElementPopup] = useState<{
|
||||||
|
name: string;
|
||||||
|
type: SchemaElementType;
|
||||||
|
slotName?: string;
|
||||||
|
overrides?: string[];
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// Sync custodian filter to URL params
|
// Sync custodian filter to URL params
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -3298,7 +3303,12 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
key={cls}
|
key={cls}
|
||||||
className="linkml-viewer__exports-link"
|
className="linkml-viewer__exports-link"
|
||||||
onClick={() => setSchemaElementPopup({ name: cls, type: 'class' })}
|
onClick={() => setSchemaElementPopup({
|
||||||
|
name: cls,
|
||||||
|
type: 'slot_usage',
|
||||||
|
slotName: slot.name,
|
||||||
|
overrides: overrides
|
||||||
|
})}
|
||||||
title={`Overrides: ${overrides.join(', ')}`}
|
title={`Overrides: ${overrides.join(', ')}`}
|
||||||
>
|
>
|
||||||
{cls} <span className="linkml-viewer__exports-via">{overrides.length} {overrides.length === 1 ? 'override' : 'overrides'}</span>
|
{cls} <span className="linkml-viewer__exports-via">{overrides.length} {overrides.length === 1 ? 'override' : 'overrides'}</span>
|
||||||
|
|
@ -4164,6 +4174,8 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
<SchemaElementPopup
|
<SchemaElementPopup
|
||||||
elementName={schemaElementPopup.name}
|
elementName={schemaElementPopup.name}
|
||||||
elementType={schemaElementPopup.type}
|
elementType={schemaElementPopup.type}
|
||||||
|
slotName={schemaElementPopup.slotName}
|
||||||
|
overrides={schemaElementPopup.overrides}
|
||||||
onClose={() => setSchemaElementPopup(null)}
|
onClose={() => setSchemaElementPopup(null)}
|
||||||
onNavigate={handleSchemaElementNavigate}
|
onNavigate={handleSchemaElementNavigate}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue