feat: Add SchemaElementPopup component for displaying LinkML schema element previews
- Implemented a draggable, resizable, and minimizable popup component for displaying previews of LinkML schema elements (classes, slots, enums). - Integrated loading states and error handling for fetching element information. - Added navigation functionality to go to full element view. - Enhanced user experience with type badges and detailed descriptions for each element type. chore: Migrate AudioEventSegment, BayNumber, BoxNumber, and BudgetStatus classes to new YAML schema format - Created new YAML definitions for AudioEventSegment, BayNumber, BoxNumber, and BudgetStatus classes with detailed descriptions and attributes. - Migrated from deprecated slots to new class structures as part of Rule 53. - Updated imports and prefixes for consistency across schemas. chore: Archive deprecated slots for audio_event_segments, bay_number, and box_number - Archived previous slot definitions for audio_event_segments, bay_number, and box_number to maintain historical records. - Updated slot descriptions and ensured proper URI mappings for future reference.
This commit is contained in:
parent
b927bc4b43
commit
7c7d8c0270
17 changed files with 1652 additions and 49 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"generated": "2026-01-14T12:29:52.423Z",
|
"generated": "2026-01-14T14:05:38.322Z",
|
||||||
"schemaRoot": "/schemas/20251121/linkml",
|
"schemaRoot": "/schemas/20251121/linkml",
|
||||||
"totalFiles": 2884,
|
"totalFiles": 2884,
|
||||||
"categoryCounts": {
|
"categoryCounts": {
|
||||||
|
|
|
||||||
|
|
@ -1316,3 +1316,26 @@ fixes:
|
||||||
type: slot
|
type: slot
|
||||||
- label: TimeSpan
|
- label: TimeSpan
|
||||||
type: class
|
type: class
|
||||||
|
- original_slot_id: https://nde.nl/ontology/hc/slot/xpath_matched_text
|
||||||
|
revision:
|
||||||
|
- label: has_or_had_text
|
||||||
|
type: slot
|
||||||
|
- label: TextSegment
|
||||||
|
type: class
|
||||||
|
- original_slot_id: https://nde.nl/ontology/hc/slot/xpath_match_score
|
||||||
|
revision:
|
||||||
|
- label: has_or_had_score
|
||||||
|
type: slot
|
||||||
|
- label: XPathScore
|
||||||
|
type: class
|
||||||
|
- original_slot_id: https://nde.nl/ontology/hc/slot/xpath
|
||||||
|
revision:
|
||||||
|
- label: has_or_had_provenance
|
||||||
|
type: slot
|
||||||
|
- label: Provenance
|
||||||
|
type: class
|
||||||
|
- label: has_or_had_provenance_path
|
||||||
|
type: slot
|
||||||
|
- label: XPath
|
||||||
|
type: class
|
||||||
|
-
|
||||||
|
|
|
||||||
479
frontend/src/components/linkml/SchemaElementPopup.css
Normal file
479
frontend/src/components/linkml/SchemaElementPopup.css
Normal file
|
|
@ -0,0 +1,479 @@
|
||||||
|
/**
|
||||||
|
* SchemaElementPopup.css
|
||||||
|
*
|
||||||
|
* Styles for the schema element popup shown when clicking on class/slot/enum
|
||||||
|
* names in the Imports/Exports accordion sections in the LinkML viewer.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.schema-popup {
|
||||||
|
position: fixed;
|
||||||
|
width: 400px;
|
||||||
|
height: 320px;
|
||||||
|
max-height: calc(100vh - 100px);
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 10200; /* Above SemanticDetailsPanel (10100) */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dragging state */
|
||||||
|
.schema-popup--dragging {
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25), 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Minimized state */
|
||||||
|
.schema-popup--minimized {
|
||||||
|
height: auto !important;
|
||||||
|
max-height: 52px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup--minimized .schema-popup__header {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header - default blue for classes */
|
||||||
|
.schema-popup__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
cursor: grab;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup--dragging .schema-popup__header {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__name {
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__minimize,
|
||||||
|
.schema-popup__close {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__minimize:hover,
|
||||||
|
.schema-popup__close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type badge */
|
||||||
|
.schema-popup__type-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.schema-popup__content {
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__loading,
|
||||||
|
.schema-popup__error {
|
||||||
|
padding: 24px 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__error {
|
||||||
|
color: #dc2626;
|
||||||
|
background: #fef2f2;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.schema-popup__section {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__section-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* URI display */
|
||||||
|
.schema-popup__uri {
|
||||||
|
display: block;
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #475569;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Values */
|
||||||
|
.schema-popup__value {
|
||||||
|
display: inline-block;
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1e40af;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__count {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Properties (for slots) */
|
||||||
|
.schema-popup__properties {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__prop {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__prop--true {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mappings summary */
|
||||||
|
.schema-popup__mappings {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__mapping-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__mapping-badge--exact {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__mapping-badge--close {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__mapping-badge--related {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.schema-popup__actions {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__navigate {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__navigate:hover {
|
||||||
|
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resize handles */
|
||||||
|
.schema-popup__resize {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__resize--n {
|
||||||
|
top: 0;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
height: 6px;
|
||||||
|
cursor: n-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__resize--s {
|
||||||
|
bottom: 0;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
height: 6px;
|
||||||
|
cursor: s-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__resize--e {
|
||||||
|
right: 0;
|
||||||
|
top: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
width: 6px;
|
||||||
|
cursor: e-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__resize--w {
|
||||||
|
left: 0;
|
||||||
|
top: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
width: 6px;
|
||||||
|
cursor: w-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__resize--ne {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
cursor: ne-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__resize--nw {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
cursor: nw-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__resize--se {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
cursor: se-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__resize--sw {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
cursor: sw-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SE corner grip indicator */
|
||||||
|
.schema-popup__resize--se::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-right: 2px solid #94a3b8;
|
||||||
|
border-bottom: 2px solid #94a3b8;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__resize--se:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode */
|
||||||
|
[data-theme="dark"] .schema-popup {
|
||||||
|
background: #1e293b;
|
||||||
|
border-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__header {
|
||||||
|
background: linear-gradient(135deg, #1e3a8a 0%, #1d4ed8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__content {
|
||||||
|
background: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__section {
|
||||||
|
border-bottom-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__section-label {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__description {
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__uri {
|
||||||
|
background: #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__value {
|
||||||
|
background: #334155;
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__count {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__prop {
|
||||||
|
background: #334155;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__prop--true {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__mapping-badge--exact {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__mapping-badge--close {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__mapping-badge--related {
|
||||||
|
background: rgba(245, 158, 11, 0.15);
|
||||||
|
color: #fcd34d;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__actions {
|
||||||
|
border-top-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__error {
|
||||||
|
background: rgba(220, 38, 38, 0.15);
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__loading {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .schema-popup__resize--se::after {
|
||||||
|
border-color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive - Full screen on mobile */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.schema-popup {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
max-height: 100vh !important;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__header {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__resize {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__minimize {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-popup__close {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
549
frontend/src/components/linkml/SchemaElementPopup.tsx
Normal file
549
frontend/src/components/linkml/SchemaElementPopup.tsx
Normal file
|
|
@ -0,0 +1,549 @@
|
||||||
|
/**
|
||||||
|
* SchemaElementPopup.tsx
|
||||||
|
*
|
||||||
|
* A popup component that displays LinkML schema element previews when clicking on
|
||||||
|
* class/slot/enum names in the Imports/Exports accordion sections.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Draggable (drag from header)
|
||||||
|
* - Minimizable (collapse to title bar only)
|
||||||
|
* - Resizable (drag edges and corners)
|
||||||
|
* - Shows preview info for classes, slots, and enums
|
||||||
|
* - "Go to" button to navigate to full element view
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useLayoutEffect, useState, useRef, useCallback } from 'react';
|
||||||
|
import { linkmlSchemaService } from '../../lib/linkml/linkml-schema-service';
|
||||||
|
import { isTargetInsideAny } from '../../utils/dom';
|
||||||
|
import './SchemaElementPopup.css';
|
||||||
|
|
||||||
|
export type SchemaElementType = 'class' | 'slot' | 'enum';
|
||||||
|
|
||||||
|
interface SchemaElementPopupProps {
|
||||||
|
/** The element name to look up */
|
||||||
|
elementName: string;
|
||||||
|
/** Type of element */
|
||||||
|
elementType: SchemaElementType;
|
||||||
|
/** Called when user wants to close the popup */
|
||||||
|
onClose: () => void;
|
||||||
|
/** Called when user clicks "Go to" button */
|
||||||
|
onNavigate: (elementName: string, elementType: SchemaElementType) => void;
|
||||||
|
/** Initial position (optional, defaults to center) */
|
||||||
|
initialPosition?: { x: number; y: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Size {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw' | null;
|
||||||
|
|
||||||
|
// Minimum panel dimensions
|
||||||
|
const MIN_WIDTH = 300;
|
||||||
|
const MIN_HEIGHT = 150;
|
||||||
|
const DEFAULT_WIDTH = 400;
|
||||||
|
const DEFAULT_HEIGHT = 320;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified element info structure
|
||||||
|
*/
|
||||||
|
interface ElementInfo {
|
||||||
|
name: string;
|
||||||
|
type: SchemaElementType;
|
||||||
|
description?: string;
|
||||||
|
uri?: string;
|
||||||
|
parentClass?: string;
|
||||||
|
range?: string;
|
||||||
|
required?: boolean;
|
||||||
|
multivalued?: boolean;
|
||||||
|
slotCount?: number;
|
||||||
|
valueCount?: number;
|
||||||
|
mappings?: {
|
||||||
|
exact?: string[];
|
||||||
|
close?: string[];
|
||||||
|
related?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SchemaElementPopup: React.FC<SchemaElementPopupProps> = ({
|
||||||
|
elementName,
|
||||||
|
elementType,
|
||||||
|
onClose,
|
||||||
|
onNavigate,
|
||||||
|
initialPosition,
|
||||||
|
}) => {
|
||||||
|
const [elementInfo, setElementInfo] = useState<ElementInfo | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Draggable state
|
||||||
|
const [position, setPosition] = useState<Position | null>(initialPosition || null);
|
||||||
|
const [isPositioned, setIsPositioned] = useState(!!initialPosition);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const dragStartRef = useRef<{ x: number; y: number; posX: number; posY: number } | null>(null);
|
||||||
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Minimized state
|
||||||
|
const [isMinimized, setIsMinimized] = useState(false);
|
||||||
|
|
||||||
|
// Resize state
|
||||||
|
const [size, setSize] = useState<Size>({ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT });
|
||||||
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
const [resizeDirection, setResizeDirection] = useState<ResizeDirection>(null);
|
||||||
|
const resizeStartRef = useRef<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
posX: number;
|
||||||
|
posY: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
// Load element information
|
||||||
|
useEffect(() => {
|
||||||
|
const loadInfo = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let info: ElementInfo | null = null;
|
||||||
|
|
||||||
|
if (elementType === 'class') {
|
||||||
|
const semanticInfo = await linkmlSchemaService.getClassSemanticInfo(elementName);
|
||||||
|
if (semanticInfo) {
|
||||||
|
info = {
|
||||||
|
name: semanticInfo.className,
|
||||||
|
type: 'class',
|
||||||
|
description: semanticInfo.description,
|
||||||
|
uri: semanticInfo.classUri,
|
||||||
|
parentClass: semanticInfo.parentClass,
|
||||||
|
slotCount: semanticInfo.slots?.length || 0,
|
||||||
|
mappings: semanticInfo.mappings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (elementType === 'slot') {
|
||||||
|
const slotInfo = await linkmlSchemaService.getSlotInfo(elementName);
|
||||||
|
if (slotInfo) {
|
||||||
|
info = {
|
||||||
|
name: slotInfo.name,
|
||||||
|
type: 'slot',
|
||||||
|
description: slotInfo.description,
|
||||||
|
uri: slotInfo.slotUri,
|
||||||
|
range: slotInfo.range,
|
||||||
|
required: slotInfo.required,
|
||||||
|
multivalued: slotInfo.multivalued,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (elementType === 'enum') {
|
||||||
|
const enumInfo = await linkmlSchemaService.getEnumSemanticInfo(elementName);
|
||||||
|
if (enumInfo) {
|
||||||
|
info = {
|
||||||
|
name: enumInfo.enumName,
|
||||||
|
type: 'enum',
|
||||||
|
description: enumInfo.description,
|
||||||
|
// EnumSemanticInfo doesn't have URI - leave undefined
|
||||||
|
valueCount: enumInfo.values?.length || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
setElementInfo(info);
|
||||||
|
} else {
|
||||||
|
setError(`Could not find ${elementType} "${elementName}"`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[SchemaElementPopup] Error loading element info:', err);
|
||||||
|
setError(`Failed to load: ${err}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadInfo();
|
||||||
|
}, [elementName, elementType]);
|
||||||
|
|
||||||
|
// Center the popup on initial load if no position provided
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!initialPosition && panelRef.current && !position) {
|
||||||
|
const rect = panelRef.current.getBoundingClientRect();
|
||||||
|
setPosition({
|
||||||
|
x: (window.innerWidth - rect.width) / 2,
|
||||||
|
y: (window.innerHeight - rect.height) / 3,
|
||||||
|
});
|
||||||
|
setIsPositioned(true);
|
||||||
|
}
|
||||||
|
}, [initialPosition, position]);
|
||||||
|
|
||||||
|
// Drag handlers
|
||||||
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (isTargetInsideAny(e.target, ['.schema-popup__close', '.schema-popup__minimize', '.schema-popup__navigate'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
|
||||||
|
const panel = panelRef.current;
|
||||||
|
if (panel) {
|
||||||
|
const rect = panel.getBoundingClientRect();
|
||||||
|
const currentX = position?.x ?? rect.left;
|
||||||
|
const currentY = position?.y ?? rect.top;
|
||||||
|
|
||||||
|
dragStartRef.current = {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
posX: currentX,
|
||||||
|
posY: currentY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [position]);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||||
|
if (!isDragging || !dragStartRef.current) return;
|
||||||
|
|
||||||
|
const deltaX = e.clientX - dragStartRef.current.x;
|
||||||
|
const deltaY = e.clientY - dragStartRef.current.y;
|
||||||
|
|
||||||
|
const newX = dragStartRef.current.posX + deltaX;
|
||||||
|
const newY = dragStartRef.current.posY + deltaY;
|
||||||
|
|
||||||
|
const maxX = window.innerWidth - 100;
|
||||||
|
const maxY = window.innerHeight - 50;
|
||||||
|
|
||||||
|
setPosition({
|
||||||
|
x: Math.max(0, Math.min(newX, maxX)),
|
||||||
|
y: Math.max(0, Math.min(newY, maxY)),
|
||||||
|
});
|
||||||
|
}, [isDragging]);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
setIsDragging(false);
|
||||||
|
dragStartRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Add/remove global mouse event listeners for dragging
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDragging) {
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
|
document.body.style.cursor = 'grabbing';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
} else {
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
};
|
||||||
|
}, [isDragging, handleMouseMove, handleMouseUp]);
|
||||||
|
|
||||||
|
// Toggle minimize
|
||||||
|
const toggleMinimize = useCallback(() => {
|
||||||
|
setIsMinimized(prev => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Resize handlers
|
||||||
|
const handleResizeMouseDown = useCallback((direction: ResizeDirection) => (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
setIsResizing(true);
|
||||||
|
setResizeDirection(direction);
|
||||||
|
|
||||||
|
const panel = panelRef.current;
|
||||||
|
if (panel) {
|
||||||
|
const rect = panel.getBoundingClientRect();
|
||||||
|
resizeStartRef.current = {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
posX: position?.x ?? rect.left,
|
||||||
|
posY: position?.y ?? rect.top,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [size, position]);
|
||||||
|
|
||||||
|
const handleResizeMouseMove = useCallback((e: MouseEvent) => {
|
||||||
|
if (!isResizing || !resizeStartRef.current || !resizeDirection) return;
|
||||||
|
|
||||||
|
const deltaX = e.clientX - resizeStartRef.current.x;
|
||||||
|
const deltaY = e.clientY - resizeStartRef.current.y;
|
||||||
|
|
||||||
|
let newWidth = resizeStartRef.current.width;
|
||||||
|
let newHeight = resizeStartRef.current.height;
|
||||||
|
let newX = resizeStartRef.current.posX;
|
||||||
|
let newY = resizeStartRef.current.posY;
|
||||||
|
|
||||||
|
if (resizeDirection.includes('e')) {
|
||||||
|
newWidth = Math.max(MIN_WIDTH, resizeStartRef.current.width + deltaX);
|
||||||
|
}
|
||||||
|
if (resizeDirection.includes('w')) {
|
||||||
|
const potentialWidth = resizeStartRef.current.width - deltaX;
|
||||||
|
if (potentialWidth >= MIN_WIDTH) {
|
||||||
|
newWidth = potentialWidth;
|
||||||
|
newX = resizeStartRef.current.posX + deltaX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resizeDirection.includes('s')) {
|
||||||
|
newHeight = Math.max(MIN_HEIGHT, resizeStartRef.current.height + deltaY);
|
||||||
|
}
|
||||||
|
if (resizeDirection.includes('n')) {
|
||||||
|
const potentialHeight = resizeStartRef.current.height - deltaY;
|
||||||
|
if (potentialHeight >= MIN_HEIGHT) {
|
||||||
|
newHeight = potentialHeight;
|
||||||
|
newY = resizeStartRef.current.posY + deltaY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSize({ width: newWidth, height: newHeight });
|
||||||
|
|
||||||
|
if (resizeDirection.includes('w') || resizeDirection.includes('n')) {
|
||||||
|
setPosition({ x: newX, y: newY });
|
||||||
|
}
|
||||||
|
}, [isResizing, resizeDirection]);
|
||||||
|
|
||||||
|
const handleResizeMouseUp = useCallback(() => {
|
||||||
|
setIsResizing(false);
|
||||||
|
setResizeDirection(null);
|
||||||
|
resizeStartRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Add/remove global mouse event listeners for resizing
|
||||||
|
useEffect(() => {
|
||||||
|
if (isResizing) {
|
||||||
|
window.addEventListener('mousemove', handleResizeMouseMove);
|
||||||
|
window.addEventListener('mouseup', handleResizeMouseUp);
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousemove', handleResizeMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleResizeMouseUp);
|
||||||
|
if (!isDragging) {
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isResizing, handleResizeMouseMove, handleResizeMouseUp, isDragging]);
|
||||||
|
|
||||||
|
// Handle navigate button click
|
||||||
|
const handleNavigate = useCallback(() => {
|
||||||
|
onNavigate(elementName, elementType);
|
||||||
|
onClose();
|
||||||
|
}, [elementName, elementType, onNavigate, onClose]);
|
||||||
|
|
||||||
|
// Render type badge
|
||||||
|
const renderTypeBadge = (type: SchemaElementType) => {
|
||||||
|
const colors: Record<SchemaElementType, string> = {
|
||||||
|
'class': '#3B82F6', // Blue
|
||||||
|
'slot': '#10B981', // Green
|
||||||
|
'enum': '#F59E0B', // Amber
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="schema-popup__type-badge"
|
||||||
|
style={{ backgroundColor: colors[type] }}
|
||||||
|
>
|
||||||
|
{type}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate panel style
|
||||||
|
const panelStyle: React.CSSProperties = {
|
||||||
|
width: size.width,
|
||||||
|
height: isMinimized ? 'auto' : size.height,
|
||||||
|
visibility: isPositioned ? 'visible' : 'hidden',
|
||||||
|
...(position && {
|
||||||
|
position: 'fixed',
|
||||||
|
left: position.x,
|
||||||
|
top: position.y,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={panelRef}
|
||||||
|
className={`schema-popup ${isMinimized ? 'schema-popup--minimized' : ''} ${isDragging ? 'schema-popup--dragging' : ''}`}
|
||||||
|
style={panelStyle}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
className="schema-popup__header"
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
>
|
||||||
|
<div className="schema-popup__title">
|
||||||
|
<code className="schema-popup__name">{elementName}</code>
|
||||||
|
{renderTypeBadge(elementType)}
|
||||||
|
</div>
|
||||||
|
<div className="schema-popup__controls">
|
||||||
|
<button
|
||||||
|
className="schema-popup__minimize"
|
||||||
|
onClick={toggleMinimize}
|
||||||
|
title={isMinimized ? 'Expand' : 'Minimize'}
|
||||||
|
>
|
||||||
|
{isMinimized ? '□' : '−'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="schema-popup__close"
|
||||||
|
onClick={onClose}
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{!isMinimized && (
|
||||||
|
<div className="schema-popup__content">
|
||||||
|
{loading && (
|
||||||
|
<div className="schema-popup__loading">
|
||||||
|
Loading {elementType} information...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="schema-popup__error">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && elementInfo && (
|
||||||
|
<>
|
||||||
|
{/* 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 */}
|
||||||
|
{elementInfo.uri && (
|
||||||
|
<div className="schema-popup__section">
|
||||||
|
<span className="schema-popup__section-label">URI</span>
|
||||||
|
<code className="schema-popup__uri">{elementInfo.uri}</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Parent class (for classes) */}
|
||||||
|
{elementInfo.parentClass && (
|
||||||
|
<div className="schema-popup__section">
|
||||||
|
<span className="schema-popup__section-label">Parent Class</span>
|
||||||
|
<code className="schema-popup__value">{elementInfo.parentClass}</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Range (for slots) */}
|
||||||
|
{elementInfo.range && (
|
||||||
|
<div className="schema-popup__section">
|
||||||
|
<span className="schema-popup__section-label">Range</span>
|
||||||
|
<code className="schema-popup__value">{elementInfo.range}</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Properties (for slots) */}
|
||||||
|
{elementInfo.type === 'slot' && (
|
||||||
|
<div className="schema-popup__section schema-popup__properties">
|
||||||
|
{elementInfo.required !== undefined && (
|
||||||
|
<span className={`schema-popup__prop ${elementInfo.required ? 'schema-popup__prop--true' : ''}`}>
|
||||||
|
{elementInfo.required ? 'required' : 'optional'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{elementInfo.multivalued !== undefined && (
|
||||||
|
<span className={`schema-popup__prop ${elementInfo.multivalued ? 'schema-popup__prop--true' : ''}`}>
|
||||||
|
{elementInfo.multivalued ? 'multivalued' : 'single-valued'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Slot count (for classes) */}
|
||||||
|
{elementInfo.slotCount !== undefined && elementInfo.slotCount > 0 && (
|
||||||
|
<div className="schema-popup__section">
|
||||||
|
<span className="schema-popup__section-label">Slots</span>
|
||||||
|
<span className="schema-popup__count">{elementInfo.slotCount} slots</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Value count (for enums) */}
|
||||||
|
{elementInfo.valueCount !== undefined && elementInfo.valueCount > 0 && (
|
||||||
|
<div className="schema-popup__section">
|
||||||
|
<span className="schema-popup__section-label">Values</span>
|
||||||
|
<span className="schema-popup__count">{elementInfo.valueCount} permissible values</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mappings (summary) */}
|
||||||
|
{elementInfo.mappings && (
|
||||||
|
(elementInfo.mappings.exact?.length ||
|
||||||
|
elementInfo.mappings.close?.length ||
|
||||||
|
elementInfo.mappings.related?.length) ? (
|
||||||
|
<div className="schema-popup__section">
|
||||||
|
<span className="schema-popup__section-label">Mappings</span>
|
||||||
|
<div className="schema-popup__mappings">
|
||||||
|
{elementInfo.mappings.exact?.length ? (
|
||||||
|
<span className="schema-popup__mapping-badge schema-popup__mapping-badge--exact">
|
||||||
|
{elementInfo.mappings.exact.length} exact
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{elementInfo.mappings.close?.length ? (
|
||||||
|
<span className="schema-popup__mapping-badge schema-popup__mapping-badge--close">
|
||||||
|
{elementInfo.mappings.close.length} close
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{elementInfo.mappings.related?.length ? (
|
||||||
|
<span className="schema-popup__mapping-badge schema-popup__mapping-badge--related">
|
||||||
|
{elementInfo.mappings.related.length} related
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigate button */}
|
||||||
|
<div className="schema-popup__actions">
|
||||||
|
<button
|
||||||
|
className="schema-popup__navigate"
|
||||||
|
onClick={handleNavigate}
|
||||||
|
>
|
||||||
|
Go to {elementType} →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Resize handles */}
|
||||||
|
{!isMinimized && (
|
||||||
|
<>
|
||||||
|
<div className="schema-popup__resize schema-popup__resize--n" onMouseDown={handleResizeMouseDown('n')} />
|
||||||
|
<div className="schema-popup__resize schema-popup__resize--s" onMouseDown={handleResizeMouseDown('s')} />
|
||||||
|
<div className="schema-popup__resize schema-popup__resize--e" onMouseDown={handleResizeMouseDown('e')} />
|
||||||
|
<div className="schema-popup__resize schema-popup__resize--w" onMouseDown={handleResizeMouseDown('w')} />
|
||||||
|
<div className="schema-popup__resize schema-popup__resize--ne" onMouseDown={handleResizeMouseDown('ne')} />
|
||||||
|
<div className="schema-popup__resize schema-popup__resize--nw" onMouseDown={handleResizeMouseDown('nw')} />
|
||||||
|
<div className="schema-popup__resize schema-popup__resize--se" onMouseDown={handleResizeMouseDown('se')} />
|
||||||
|
<div className="schema-popup__resize schema-popup__resize--sw" onMouseDown={handleResizeMouseDown('sw')} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SchemaElementPopup;
|
||||||
|
|
@ -46,6 +46,7 @@ import {
|
||||||
isUniversalElement,
|
isUniversalElement,
|
||||||
} from '../lib/schema-custodian-mapping';
|
} from '../lib/schema-custodian-mapping';
|
||||||
import { OntologyTermPopup } from '../components/ontology/OntologyTermPopup';
|
import { OntologyTermPopup } from '../components/ontology/OntologyTermPopup';
|
||||||
|
import { SchemaElementPopup, type SchemaElementType } from '../components/linkml/SchemaElementPopup';
|
||||||
import './LinkMLViewerPage.css';
|
import './LinkMLViewerPage.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1299,6 +1300,9 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
// State for ontology term popup (shows when clicking mapping tags like rico:Rule)
|
// State for ontology term popup (shows when clicking mapping tags like rico:Rule)
|
||||||
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)
|
||||||
|
const [schemaElementPopup, setSchemaElementPopup] = useState<{ name: string; type: SchemaElementType } | null>(null);
|
||||||
|
|
||||||
// Sync custodian filter to URL params
|
// Sync custodian filter to URL params
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentParam = searchParams.get('custodian');
|
const currentParam = searchParams.get('custodian');
|
||||||
|
|
@ -1835,6 +1839,18 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [categories, setSearchParams]);
|
}, [categories, setSearchParams]);
|
||||||
|
|
||||||
|
// Handler for when user clicks "Go to" in the schema element popup
|
||||||
|
const handleSchemaElementNavigate = useCallback((elementName: string, elementType: SchemaElementType) => {
|
||||||
|
if (elementType === 'class') {
|
||||||
|
navigateToClass(elementName);
|
||||||
|
} else if (elementType === 'enum') {
|
||||||
|
navigateToEnum(elementName);
|
||||||
|
} else if (elementType === 'slot') {
|
||||||
|
navigateToSlot(elementName);
|
||||||
|
}
|
||||||
|
setSchemaElementPopup(null);
|
||||||
|
}, [navigateToClass, navigateToEnum, navigateToSlot]);
|
||||||
|
|
||||||
// Track if initialization has already happened (prevents re-init on URL param changes)
|
// Track if initialization has already happened (prevents re-init on URL param changes)
|
||||||
const isInitializedRef = useRef(false);
|
const isInitializedRef = useRef(false);
|
||||||
|
|
||||||
|
|
@ -2470,7 +2486,7 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
<div className="linkml-viewer__imports-list">
|
<div className="linkml-viewer__imports-list">
|
||||||
<button
|
<button
|
||||||
className="linkml-viewer__imports-link"
|
className="linkml-viewer__imports-link"
|
||||||
onClick={() => navigateToClass(classImports[cls.name].parentClass!)}
|
onClick={() => setSchemaElementPopup({ name: classImports[cls.name].parentClass!, type: 'class' })}
|
||||||
>
|
>
|
||||||
{classImports[cls.name].parentClass}
|
{classImports[cls.name].parentClass}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -2486,7 +2502,7 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
key={mixin}
|
key={mixin}
|
||||||
className="linkml-viewer__imports-link"
|
className="linkml-viewer__imports-link"
|
||||||
onClick={() => navigateToClass(mixin)}
|
onClick={() => setSchemaElementPopup({ name: mixin, type: 'class' })}
|
||||||
>
|
>
|
||||||
{mixin}
|
{mixin}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -3191,11 +3207,10 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
className="linkml-viewer__imports-link"
|
className="linkml-viewer__imports-link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const range = slotImports[slot.name].rangeType!;
|
const range = slotImports[slot.name].rangeType!;
|
||||||
if (range.isClass) {
|
setSchemaElementPopup({
|
||||||
navigateToClass(range.name);
|
name: range.name,
|
||||||
} else if (range.isEnum) {
|
type: range.isClass ? 'class' : 'enum'
|
||||||
navigateToEnum(range.name);
|
});
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{slotImports[slot.name].rangeType!.name}
|
{slotImports[slot.name].rangeType!.name}
|
||||||
|
|
@ -3214,11 +3229,10 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
key={type.name}
|
key={type.name}
|
||||||
className="linkml-viewer__imports-link"
|
className="linkml-viewer__imports-link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (type.isClass) {
|
setSchemaElementPopup({
|
||||||
navigateToClass(type.name);
|
name: type.name,
|
||||||
} else if (type.isEnum) {
|
type: type.isClass ? 'class' : 'enum'
|
||||||
navigateToEnum(type.name);
|
});
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{type.name}
|
{type.name}
|
||||||
|
|
@ -3267,7 +3281,7 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
key={cls}
|
key={cls}
|
||||||
className="linkml-viewer__exports-link"
|
className="linkml-viewer__exports-link"
|
||||||
onClick={() => navigateToClass(cls)}
|
onClick={() => setSchemaElementPopup({ name: cls, type: 'class' })}
|
||||||
>
|
>
|
||||||
{cls}
|
{cls}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -3284,7 +3298,7 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
key={cls}
|
key={cls}
|
||||||
className="linkml-viewer__exports-link"
|
className="linkml-viewer__exports-link"
|
||||||
onClick={() => navigateToClass(cls)}
|
onClick={() => setSchemaElementPopup({ name: cls, type: 'class' })}
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"generated": "2026-01-14T14:05:38.322Z",
|
"generated": "2026-01-14T14:13:07.428Z",
|
||||||
"schemaRoot": "/schemas/20251121/linkml",
|
"schemaRoot": "/schemas/20251121/linkml",
|
||||||
"totalFiles": 2884,
|
"totalFiles": 2884,
|
||||||
"categoryCounts": {
|
"categoryCounts": {
|
||||||
|
|
|
||||||
172
schemas/20251121/linkml/modules/classes/AudioEventSegment.yaml
Normal file
172
schemas/20251121/linkml/modules/classes/AudioEventSegment.yaml
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
id: https://nde.nl/ontology/hc/class/AudioEventSegment
|
||||||
|
name: audio_event_segment_class
|
||||||
|
title: Audio Event Segment Class
|
||||||
|
description: |
|
||||||
|
A temporal segment of audio containing a detected audio event (speech, music, silence, etc.).
|
||||||
|
|
||||||
|
MIGRATED from audio_event_segments slot (Rule 53).
|
||||||
|
Uses generic has_or_had_segment slot with range narrowed to AudioEventSegment.
|
||||||
|
imports:
|
||||||
|
- linkml:types
|
||||||
|
- ../slots/start_seconds
|
||||||
|
- ../slots/end_seconds
|
||||||
|
- ../slots/start_time
|
||||||
|
- ../slots/end_time
|
||||||
|
- ../slots/segment_index
|
||||||
|
- ../slots/segment_text
|
||||||
|
- ../slots/confidence
|
||||||
|
- ../slots/specificity_annotation
|
||||||
|
- ../slots/template_specificity
|
||||||
|
- ./SpecificityAnnotation
|
||||||
|
- ./TemplateSpecificityScores
|
||||||
|
- ../enums/AudioEventTypeEnum
|
||||||
|
prefixes:
|
||||||
|
linkml: https://w3id.org/linkml/
|
||||||
|
hc: https://nde.nl/ontology/hc/
|
||||||
|
schema: http://schema.org/
|
||||||
|
dcterms: http://purl.org/dc/terms/
|
||||||
|
crm: http://www.cidoc-crm.org/cidoc-crm/
|
||||||
|
oa: http://www.w3.org/ns/oa#
|
||||||
|
ma: http://www.w3.org/ns/ma-ont#
|
||||||
|
default_prefix: hc
|
||||||
|
|
||||||
|
classes:
|
||||||
|
AudioEventSegment:
|
||||||
|
class_uri: hc:AudioEventSegment
|
||||||
|
description: |
|
||||||
|
A temporal segment of audio containing a detected audio event.
|
||||||
|
|
||||||
|
**DEFINITION**:
|
||||||
|
|
||||||
|
AudioEventSegment represents a bounded temporal portion of audio content
|
||||||
|
where a specific type of audio event has been detected. This includes:
|
||||||
|
- Speech segments (with optional speaker/language info)
|
||||||
|
- Music segments (with optional genre/type info)
|
||||||
|
- Silence segments (gaps between audio)
|
||||||
|
- Sound event segments (applause, laughter, ambient sounds)
|
||||||
|
- Noise segments (for quality assessment)
|
||||||
|
|
||||||
|
**RELATIONSHIP TO VideoTimeSegment**:
|
||||||
|
|
||||||
|
AudioEventSegment is a specialized sibling of VideoTimeSegment:
|
||||||
|
- Both extend CIDOC-CRM E52_Time-Span concept
|
||||||
|
- VideoTimeSegment: general video temporal segments
|
||||||
|
- AudioEventSegment: audio-specific event segments
|
||||||
|
|
||||||
|
**AUDIO EVENT TYPES**:
|
||||||
|
|
||||||
|
| Event Type | Description | Example |
|
||||||
|
|------------|-------------|---------|
|
||||||
|
| SPEECH | Human speech detected | Interview segment |
|
||||||
|
| MUSIC | Music detected | Background soundtrack |
|
||||||
|
| SILENCE | Very low or no audio | Gap between segments |
|
||||||
|
| SOUND_EVENT | Non-speech/music sounds | Applause, footsteps |
|
||||||
|
| NOISE | Noise/interference | Quality issue marker |
|
||||||
|
| MIXED | Multiple event types | Overlapping audio |
|
||||||
|
|
||||||
|
**HERITAGE USE CASES**:
|
||||||
|
|
||||||
|
| Content Type | Application |
|
||||||
|
|--------------|-------------|
|
||||||
|
| Oral histories | Speech segment identification |
|
||||||
|
| Virtual tours | Background music detection |
|
||||||
|
| Lecture recordings | Audience reaction segments |
|
||||||
|
| Conservation videos | Narration vs ambient sound |
|
||||||
|
| Archival footage | Audio quality assessment |
|
||||||
|
|
||||||
|
**PROVENANCE**:
|
||||||
|
|
||||||
|
Created as part of slot migration (Rule 53) from deprecated
|
||||||
|
`audio_event_segments` slot to generic `has_or_had_segment` pattern.
|
||||||
|
exact_mappings:
|
||||||
|
- hc:AudioEventSegment
|
||||||
|
close_mappings:
|
||||||
|
- crm:E52_Time-Span
|
||||||
|
- ma:MediaFragment
|
||||||
|
related_mappings:
|
||||||
|
- oa:FragmentSelector
|
||||||
|
slots:
|
||||||
|
- start_seconds
|
||||||
|
- end_seconds
|
||||||
|
- start_time
|
||||||
|
- end_time
|
||||||
|
- segment_index
|
||||||
|
- segment_text
|
||||||
|
- confidence
|
||||||
|
- specificity_annotation
|
||||||
|
- template_specificity
|
||||||
|
attributes:
|
||||||
|
audio_event_type:
|
||||||
|
range: AudioEventTypeEnum
|
||||||
|
required: true
|
||||||
|
description: The type of audio event detected in this segment.
|
||||||
|
examples:
|
||||||
|
- value: SPEECH
|
||||||
|
description: Speech detected in this segment
|
||||||
|
- value: MUSIC
|
||||||
|
description: Music detected in this segment
|
||||||
|
slot_usage:
|
||||||
|
start_seconds:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
minimum_value: 0.0
|
||||||
|
description: Start time in seconds for this audio event segment.
|
||||||
|
examples:
|
||||||
|
- value: 0.0
|
||||||
|
description: Audio event starts at beginning
|
||||||
|
- value: 45.5
|
||||||
|
description: Audio event starts at 45.5 seconds
|
||||||
|
end_seconds:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
minimum_value: 0.0
|
||||||
|
description: End time in seconds for this audio event segment.
|
||||||
|
examples:
|
||||||
|
- value: 15.0
|
||||||
|
description: Audio event ends at 15 seconds
|
||||||
|
- value: 60.0
|
||||||
|
description: Audio event ends at 1 minute
|
||||||
|
start_time:
|
||||||
|
range: string
|
||||||
|
required: false
|
||||||
|
pattern: "^PT(\\d+H)?(\\d+M)?(\\d+(\\.\\d+)?S)?$"
|
||||||
|
description: Start time in ISO 8601 duration format.
|
||||||
|
examples:
|
||||||
|
- value: PT0M30S
|
||||||
|
description: 30 seconds from start
|
||||||
|
end_time:
|
||||||
|
range: string
|
||||||
|
required: false
|
||||||
|
pattern: "^PT(\\d+H)?(\\d+M)?(\\d+(\\.\\d+)?S)?$"
|
||||||
|
description: End time in ISO 8601 duration format.
|
||||||
|
examples:
|
||||||
|
- value: PT0M45S
|
||||||
|
description: 45 seconds from start
|
||||||
|
segment_text:
|
||||||
|
range: string
|
||||||
|
required: false
|
||||||
|
description: Text content for this segment (e.g., speech transcript, music description).
|
||||||
|
examples:
|
||||||
|
- value: "Welcome to the Rijksmuseum"
|
||||||
|
description: Speech transcript text
|
||||||
|
- value: "Classical background music"
|
||||||
|
description: Music segment description
|
||||||
|
confidence:
|
||||||
|
range: float
|
||||||
|
required: false
|
||||||
|
minimum_value: 0.0
|
||||||
|
maximum_value: 1.0
|
||||||
|
description: Confidence score (0.0-1.0) for the audio event detection.
|
||||||
|
examples:
|
||||||
|
- value: 0.95
|
||||||
|
description: High confidence detection
|
||||||
|
- value: 0.72
|
||||||
|
description: Medium confidence detection
|
||||||
|
comments:
|
||||||
|
- Audio event segment for speech, music, silence, sound event detection
|
||||||
|
- Temporal boundaries with start/end seconds (primary) and ISO 8601 (secondary)
|
||||||
|
- Confidence scoring for AI-generated detections
|
||||||
|
- Part of Rule 53 slot migration from audio_event_segments
|
||||||
|
see_also:
|
||||||
|
- https://www.w3.org/TR/media-frags/
|
||||||
|
- https://www.w3.org/ns/ma-ont
|
||||||
92
schemas/20251121/linkml/modules/classes/BayNumber.yaml
Normal file
92
schemas/20251121/linkml/modules/classes/BayNumber.yaml
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
id: https://nde.nl/ontology/hc/class/BayNumber
|
||||||
|
name: bay_number_class
|
||||||
|
title: Bay Number Class
|
||||||
|
description: |
|
||||||
|
A storage bay or section identifier within a storage row.
|
||||||
|
|
||||||
|
MIGRATED from bay_number slot (Rule 53).
|
||||||
|
Uses generic has_or_had_identifier slot with range narrowed to BayNumber.
|
||||||
|
imports:
|
||||||
|
- linkml:types
|
||||||
|
- ../slots/identifier_value
|
||||||
|
- ../slots/specificity_annotation
|
||||||
|
- ../slots/template_specificity
|
||||||
|
- ./SpecificityAnnotation
|
||||||
|
- ./TemplateSpecificityScores
|
||||||
|
prefixes:
|
||||||
|
linkml: https://w3id.org/linkml/
|
||||||
|
hc: https://nde.nl/ontology/hc/
|
||||||
|
schema: http://schema.org/
|
||||||
|
dcterms: http://purl.org/dc/terms/
|
||||||
|
crm: http://www.cidoc-crm.org/cidoc-crm/
|
||||||
|
default_prefix: hc
|
||||||
|
|
||||||
|
classes:
|
||||||
|
BayNumber:
|
||||||
|
class_uri: hc:BayNumber
|
||||||
|
description: |
|
||||||
|
An identifier for a storage bay or section within a row/aisle of a storage facility.
|
||||||
|
|
||||||
|
**DEFINITION**:
|
||||||
|
|
||||||
|
BayNumber represents a discrete location identifier within a storage system.
|
||||||
|
In heritage storage facilities, storage is typically organized hierarchically:
|
||||||
|
|
||||||
|
```
|
||||||
|
Storage Facility
|
||||||
|
└── Zone (environmental control)
|
||||||
|
└── Row/Aisle (physical corridor)
|
||||||
|
└── Bay/Section (THIS CLASS - vertical unit in row)
|
||||||
|
└── Shelf (horizontal level within bay)
|
||||||
|
└── Storage Unit (box, drawer, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
**TYPICAL VALUES**:
|
||||||
|
|
||||||
|
| Format | Example | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| Numeric | "3", "12" | Sequential bay numbers |
|
||||||
|
| Alphabetic | "A", "C", "AA" | Lettered bays |
|
||||||
|
| Mixed | "3A", "B2" | Combined formats |
|
||||||
|
| Descriptive | "North-3" | Location-based |
|
||||||
|
|
||||||
|
**HERITAGE USE CASES**:
|
||||||
|
|
||||||
|
| Institution Type | Bay Naming Convention |
|
||||||
|
|------------------|----------------------|
|
||||||
|
| Archives | Sequential numeric (1, 2, 3...) |
|
||||||
|
| Museums | Alphanumeric by collection area |
|
||||||
|
| Libraries | By call number range |
|
||||||
|
| Natural history | By specimen type |
|
||||||
|
|
||||||
|
**PROVENANCE**:
|
||||||
|
|
||||||
|
Created as part of slot migration (Rule 53) from deprecated
|
||||||
|
`bay_number` slot to generic `has_or_had_identifier` pattern.
|
||||||
|
exact_mappings:
|
||||||
|
- hc:BayNumber
|
||||||
|
close_mappings:
|
||||||
|
- crm:E42_Identifier
|
||||||
|
related_mappings:
|
||||||
|
- schema:identifier
|
||||||
|
slots:
|
||||||
|
- specificity_annotation
|
||||||
|
- template_specificity
|
||||||
|
attributes:
|
||||||
|
value:
|
||||||
|
range: string
|
||||||
|
required: true
|
||||||
|
description: The bay number/identifier value.
|
||||||
|
examples:
|
||||||
|
- value: "3"
|
||||||
|
description: Numeric bay number
|
||||||
|
- value: "C"
|
||||||
|
description: Alphabetic bay identifier
|
||||||
|
- value: "North-3"
|
||||||
|
description: Location-descriptive bay
|
||||||
|
comments:
|
||||||
|
- Storage bay identifier within a row/aisle
|
||||||
|
- Part of hierarchical storage location addressing
|
||||||
|
- Part of Rule 53 slot migration from bay_number
|
||||||
|
see_also:
|
||||||
|
- https://nde.nl/ontology/hc/StorageUnit
|
||||||
97
schemas/20251121/linkml/modules/classes/BoxNumber.yaml
Normal file
97
schemas/20251121/linkml/modules/classes/BoxNumber.yaml
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
id: https://nde.nl/ontology/hc/class/BoxNumber
|
||||||
|
name: box_number_class
|
||||||
|
title: Box Number Class
|
||||||
|
description: |
|
||||||
|
A storage box number or position identifier on a shelf.
|
||||||
|
|
||||||
|
MIGRATED from box_number slot (Rule 53).
|
||||||
|
Uses generic has_or_had_identifier slot with range narrowed to BoxNumber.
|
||||||
|
imports:
|
||||||
|
- linkml:types
|
||||||
|
- ../slots/identifier_value
|
||||||
|
- ../slots/specificity_annotation
|
||||||
|
- ../slots/template_specificity
|
||||||
|
- ./SpecificityAnnotation
|
||||||
|
- ./TemplateSpecificityScores
|
||||||
|
prefixes:
|
||||||
|
linkml: https://w3id.org/linkml/
|
||||||
|
hc: https://nde.nl/ontology/hc/
|
||||||
|
schema: http://schema.org/
|
||||||
|
dcterms: http://purl.org/dc/terms/
|
||||||
|
crm: http://www.cidoc-crm.org/cidoc-crm/
|
||||||
|
default_prefix: hc
|
||||||
|
|
||||||
|
classes:
|
||||||
|
BoxNumber:
|
||||||
|
class_uri: hc:BoxNumber
|
||||||
|
description: |
|
||||||
|
An identifier for a storage box or its position on a shelf.
|
||||||
|
|
||||||
|
**DEFINITION**:
|
||||||
|
|
||||||
|
BoxNumber represents the position or identifier of a storage box within
|
||||||
|
a storage unit hierarchy. Archive boxes are the most common physical
|
||||||
|
containers for heritage materials, particularly in archives.
|
||||||
|
|
||||||
|
```
|
||||||
|
Shelf
|
||||||
|
└── Box 1 (THIS CLASS - position on shelf)
|
||||||
|
└── Box 2
|
||||||
|
└── Box 3
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**TYPICAL VALUES**:
|
||||||
|
|
||||||
|
| Type | Example | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| Sequential | 1, 2, 3, 12 | Position on shelf left-to-right |
|
||||||
|
| Inventory | 145, 2024-0042 | Unique box inventory number |
|
||||||
|
| Combined | 12.3 | Bay 12, Box 3 |
|
||||||
|
|
||||||
|
**ARCHIVE BOX STANDARDS**:
|
||||||
|
|
||||||
|
Heritage institutions typically use acid-free archive boxes conforming to:
|
||||||
|
- ISO 16245 (Boxes for documents)
|
||||||
|
- ANSI/NISO Z39.77 (Guidelines for materials in archives)
|
||||||
|
|
||||||
|
**HERITAGE USE CASES**:
|
||||||
|
|
||||||
|
| Material Type | Box Format |
|
||||||
|
|---------------|------------|
|
||||||
|
| Documents | Standard archive box (legal/letter) |
|
||||||
|
| Photographs | Photo storage boxes |
|
||||||
|
| Oversized | Flat boxes, tubes |
|
||||||
|
| Fragile | Custom padded boxes |
|
||||||
|
|
||||||
|
**PROVENANCE**:
|
||||||
|
|
||||||
|
Created as part of slot migration (Rule 53) from deprecated
|
||||||
|
`box_number` slot to generic `has_or_had_identifier` pattern.
|
||||||
|
exact_mappings:
|
||||||
|
- hc:BoxNumber
|
||||||
|
close_mappings:
|
||||||
|
- crm:E42_Identifier
|
||||||
|
related_mappings:
|
||||||
|
- schema:identifier
|
||||||
|
slots:
|
||||||
|
- specificity_annotation
|
||||||
|
- template_specificity
|
||||||
|
attributes:
|
||||||
|
value:
|
||||||
|
range: integer
|
||||||
|
required: true
|
||||||
|
minimum_value: 1
|
||||||
|
description: The box number (position on shelf or inventory number).
|
||||||
|
examples:
|
||||||
|
- value: 12
|
||||||
|
description: Box at position 12 on shelf
|
||||||
|
- value: 145
|
||||||
|
description: Archive box inventory number 145
|
||||||
|
comments:
|
||||||
|
- Storage box position identifier
|
||||||
|
- Typically integer representing shelf position or inventory number
|
||||||
|
- Part of Rule 53 slot migration from box_number
|
||||||
|
see_also:
|
||||||
|
- https://nde.nl/ontology/hc/StorageUnit
|
||||||
|
- https://www.wikidata.org/wiki/Q854619
|
||||||
|
|
@ -16,7 +16,10 @@ imports:
|
||||||
- ../slots/budget_currency
|
- ../slots/budget_currency
|
||||||
- ../slots/has_or_had_description
|
- ../slots/has_or_had_description
|
||||||
- ../slots/has_or_had_label
|
- ../slots/has_or_had_label
|
||||||
- ../slots/budget_status
|
# REMOVED - migrated to has_or_had_status with range BudgetStatus (Rule 53)
|
||||||
|
# - ../slots/budget_status
|
||||||
|
- ../slots/has_or_had_status
|
||||||
|
- ./BudgetStatus
|
||||||
- ../slots/has_or_had_type
|
- ../slots/has_or_had_type
|
||||||
- ../slots/capital_budget
|
- ../slots/capital_budget
|
||||||
- ./BudgetType
|
- ./BudgetType
|
||||||
|
|
@ -97,7 +100,9 @@ classes:
|
||||||
- budget_currency
|
- budget_currency
|
||||||
- has_or_had_description
|
- has_or_had_description
|
||||||
- has_or_had_label
|
- has_or_had_label
|
||||||
- budget_status
|
# MIGRATED from budget_status to has_or_had_status (Rule 53)
|
||||||
|
# - budget_status
|
||||||
|
- has_or_had_status
|
||||||
- has_or_had_type
|
- has_or_had_type
|
||||||
- capital_budget
|
- capital_budget
|
||||||
- digitization_budget
|
- digitization_budget
|
||||||
|
|
@ -249,12 +254,25 @@ classes:
|
||||||
description: Agent (person/organization) that approved this budget
|
description: Agent (person/organization) that approved this budget
|
||||||
range: string
|
range: string
|
||||||
required: false
|
required: false
|
||||||
budget_status:
|
# MIGRATED from budget_status to has_or_had_status (Rule 53)
|
||||||
range: string
|
# budget_status:
|
||||||
|
# range: string
|
||||||
|
# required: true
|
||||||
|
# examples:
|
||||||
|
# - value: ACTIVE
|
||||||
|
# description: Current fiscal year budget in effect
|
||||||
|
has_or_had_status:
|
||||||
|
description: |
|
||||||
|
MIGRATED from budget_status (Rule 53).
|
||||||
|
Current status of this budget in its lifecycle.
|
||||||
|
Uses BudgetStatus class for structured status tracking.
|
||||||
|
range: BudgetStatus
|
||||||
required: true
|
required: true
|
||||||
examples:
|
examples:
|
||||||
- value: ACTIVE
|
- value: '{value: "ACTIVE", effective_date: "2024-01-01"}'
|
||||||
description: Current fiscal year budget in effect
|
description: Budget currently in effect
|
||||||
|
- value: '{value: "DRAFT", effective_date: "2023-10-01"}'
|
||||||
|
description: Budget under development
|
||||||
revision_number:
|
revision_number:
|
||||||
range: integer
|
range: integer
|
||||||
required: false
|
required: false
|
||||||
|
|
@ -319,7 +337,9 @@ classes:
|
||||||
endowment_draw: 5000000.0
|
endowment_draw: 5000000.0
|
||||||
approval_date: '2023-11-15'
|
approval_date: '2023-11-15'
|
||||||
is_or_was_approved_by: Board of Directors
|
is_or_was_approved_by: Board of Directors
|
||||||
budget_status: ACTIVE
|
has_or_had_status:
|
||||||
|
value: ACTIVE
|
||||||
|
effective_date: '2024-01-01'
|
||||||
refers_to_custodian: https://nde.nl/ontology/hc/nl-nh-ams-m-rm-q190804
|
refers_to_custodian: https://nde.nl/ontology/hc/nl-nh-ams-m-rm-q190804
|
||||||
description: Major museum annual operating budget
|
description: Major museum annual operating budget
|
||||||
- value:
|
- value:
|
||||||
|
|
@ -341,6 +361,8 @@ classes:
|
||||||
internal_funding: 2500000.0
|
internal_funding: 2500000.0
|
||||||
approval_date: '2024-03-01'
|
approval_date: '2024-03-01'
|
||||||
is_or_was_approved_by: Province of Noord-Holland
|
is_or_was_approved_by: Province of Noord-Holland
|
||||||
budget_status: ACTIVE
|
has_or_had_status:
|
||||||
|
value: ACTIVE
|
||||||
|
effective_date: '2024-04-01'
|
||||||
refers_to_custodian: https://nde.nl/ontology/hc/nl-nh-haa-a-nha
|
refers_to_custodian: https://nde.nl/ontology/hc/nl-nh-haa-a-nha
|
||||||
description: Regional archive government-funded budget
|
description: Regional archive government-funded budget
|
||||||
|
|
|
||||||
105
schemas/20251121/linkml/modules/classes/BudgetStatus.yaml
Normal file
105
schemas/20251121/linkml/modules/classes/BudgetStatus.yaml
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
id: https://nde.nl/ontology/hc/class/BudgetStatus
|
||||||
|
name: budget_status_class
|
||||||
|
title: Budget Status Class
|
||||||
|
description: |
|
||||||
|
Status of a heritage custodian budget throughout its lifecycle.
|
||||||
|
|
||||||
|
MIGRATED from budget_status slot (Rule 53).
|
||||||
|
Uses generic has_or_had_status slot with range narrowed to BudgetStatus.
|
||||||
|
imports:
|
||||||
|
- linkml:types
|
||||||
|
- ../slots/specificity_annotation
|
||||||
|
- ../slots/template_specificity
|
||||||
|
- ./SpecificityAnnotation
|
||||||
|
- ./TemplateSpecificityScores
|
||||||
|
prefixes:
|
||||||
|
linkml: https://w3id.org/linkml/
|
||||||
|
hc: https://nde.nl/ontology/hc/
|
||||||
|
schema: http://schema.org/
|
||||||
|
dcterms: http://purl.org/dc/terms/
|
||||||
|
frapo: http://purl.org/cerif/frapo/
|
||||||
|
default_prefix: hc
|
||||||
|
|
||||||
|
classes:
|
||||||
|
BudgetStatus:
|
||||||
|
class_uri: hc:BudgetStatus
|
||||||
|
description: |
|
||||||
|
Status of a budget document throughout its lifecycle.
|
||||||
|
|
||||||
|
**DEFINITION**:
|
||||||
|
|
||||||
|
BudgetStatus represents the current state of a budget document
|
||||||
|
as it moves through the approval and execution lifecycle.
|
||||||
|
|
||||||
|
**BUDGET LIFECYCLE STAGES**:
|
||||||
|
|
||||||
|
```
|
||||||
|
DRAFT → PROPOSED → APPROVED → ACTIVE → REVISED → CLOSED
|
||||||
|
↓ ↓
|
||||||
|
REJECTED SUPERSEDED
|
||||||
|
```
|
||||||
|
|
||||||
|
**STATUS VALUES**:
|
||||||
|
|
||||||
|
| Status | Description | Typical Duration |
|
||||||
|
|--------|-------------|------------------|
|
||||||
|
| DRAFT | Under development | Weeks/months |
|
||||||
|
| PROPOSED | Submitted for approval | Days/weeks |
|
||||||
|
| APPROVED | Officially approved | Until fiscal start |
|
||||||
|
| ACTIVE | Currently in effect | Fiscal year |
|
||||||
|
| REVISED | Modified after approval | Variable |
|
||||||
|
| CLOSED | Fiscal period ended | Permanent |
|
||||||
|
| REJECTED | Not approved | Terminal |
|
||||||
|
| SUPERSEDED | Replaced by revision | Terminal |
|
||||||
|
|
||||||
|
**HERITAGE INSTITUTION CONTEXT**:
|
||||||
|
|
||||||
|
Heritage institution budgets typically follow these approval paths:
|
||||||
|
|
||||||
|
| Institution Type | Approval Authority |
|
||||||
|
|------------------|-------------------|
|
||||||
|
| Museum (stichting) | Board of Directors |
|
||||||
|
| Regional Archive | Provincial Government |
|
||||||
|
| National Library | Ministry of Culture |
|
||||||
|
| University Collection | University Board |
|
||||||
|
|
||||||
|
**PROVENANCE**:
|
||||||
|
|
||||||
|
Created as part of slot migration (Rule 53) from deprecated
|
||||||
|
`budget_status` slot to generic `has_or_had_status` pattern.
|
||||||
|
exact_mappings:
|
||||||
|
- hc:BudgetStatus
|
||||||
|
close_mappings:
|
||||||
|
- schema:status
|
||||||
|
related_mappings:
|
||||||
|
- dcterms:status
|
||||||
|
slots:
|
||||||
|
- specificity_annotation
|
||||||
|
- template_specificity
|
||||||
|
attributes:
|
||||||
|
value:
|
||||||
|
range: string
|
||||||
|
required: true
|
||||||
|
description: |
|
||||||
|
The budget status value.
|
||||||
|
Valid values: DRAFT, PROPOSED, APPROVED, ACTIVE, REVISED, CLOSED, REJECTED, SUPERSEDED
|
||||||
|
examples:
|
||||||
|
- value: ACTIVE
|
||||||
|
description: Budget currently in effect
|
||||||
|
- value: DRAFT
|
||||||
|
description: Budget under development
|
||||||
|
- value: CLOSED
|
||||||
|
description: Fiscal period ended
|
||||||
|
effective_date:
|
||||||
|
range: date
|
||||||
|
required: false
|
||||||
|
description: Date when this status became effective.
|
||||||
|
examples:
|
||||||
|
- value: "2024-01-01"
|
||||||
|
description: Status effective from start of year
|
||||||
|
comments:
|
||||||
|
- Budget lifecycle status tracking
|
||||||
|
- Supports audit trail of budget state changes
|
||||||
|
- Part of Rule 53 slot migration from budget_status
|
||||||
|
see_also:
|
||||||
|
- https://nde.nl/ontology/hc/Budget
|
||||||
|
|
@ -17,8 +17,13 @@ imports:
|
||||||
- ../slots/unit_name
|
- ../slots/unit_name
|
||||||
- ../slots/unit_type
|
- ../slots/unit_type
|
||||||
- ../slots/capacity_item
|
- ../slots/capacity_item
|
||||||
- ../slots/bay_number
|
# REMOVED - migrated to has_or_had_identifier with range BayNumber (Rule 53)
|
||||||
- ../slots/box_number
|
# - ../slots/bay_number
|
||||||
|
# REMOVED - migrated to has_or_had_identifier with range BoxNumber (Rule 53)
|
||||||
|
# - ../slots/box_number
|
||||||
|
- ../slots/has_or_had_identifier
|
||||||
|
- ./BayNumber
|
||||||
|
- ./BoxNumber
|
||||||
- ../slots/current_item_count
|
- ../slots/current_item_count
|
||||||
- ../slots/drawer_number
|
- ../slots/drawer_number
|
||||||
- ../slots/part_of_storage
|
- ../slots/part_of_storage
|
||||||
|
|
@ -75,8 +80,10 @@ classes:
|
||||||
- hc:EnvironmentalZone
|
- hc:EnvironmentalZone
|
||||||
- schema:Place
|
- schema:Place
|
||||||
slots:
|
slots:
|
||||||
- bay_number
|
# MIGRATED from bay_number and box_number to has_or_had_identifier (Rule 53)
|
||||||
- box_number
|
# - bay_number
|
||||||
|
# - box_number
|
||||||
|
- has_or_had_identifier
|
||||||
- capacity_item
|
- capacity_item
|
||||||
- current_item_count
|
- current_item_count
|
||||||
- drawer_number
|
- drawer_number
|
||||||
|
|
@ -124,11 +131,22 @@ classes:
|
||||||
- value: A
|
- value: A
|
||||||
- value: '12'
|
- value: '12'
|
||||||
- value: North-3
|
- value: North-3
|
||||||
bay_number:
|
has_or_had_identifier:
|
||||||
|
description: |
|
||||||
|
MIGRATED from bay_number and box_number (Rule 53).
|
||||||
|
Storage location identifiers including bay and box numbers.
|
||||||
|
Use BayNumber for bay/section identifiers, BoxNumber for box positions.
|
||||||
range: string
|
range: string
|
||||||
|
multivalued: true
|
||||||
examples:
|
examples:
|
||||||
- value: '3'
|
- value: '[{type: BayNumber, value: "3"}, {type: BoxNumber, value: 12}]'
|
||||||
- value: C
|
description: Bay 3, Box 12
|
||||||
|
# DEPRECATED - use has_or_had_identifier with range BayNumber
|
||||||
|
# bay_number:
|
||||||
|
# range: string
|
||||||
|
# examples:
|
||||||
|
# - value: '3'
|
||||||
|
# - value: C
|
||||||
shelf_number:
|
shelf_number:
|
||||||
range: integer
|
range: integer
|
||||||
examples:
|
examples:
|
||||||
|
|
@ -138,10 +156,11 @@ classes:
|
||||||
range: integer
|
range: integer
|
||||||
examples:
|
examples:
|
||||||
- value: 3
|
- value: 3
|
||||||
box_number:
|
# DEPRECATED - use has_or_had_identifier with range BoxNumber
|
||||||
range: integer
|
# box_number:
|
||||||
examples:
|
# range: integer
|
||||||
- value: 12
|
# examples:
|
||||||
|
# - value: 12
|
||||||
capacity_item:
|
capacity_item:
|
||||||
range: integer
|
range: integer
|
||||||
current_item_count:
|
current_item_count:
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ imports:
|
||||||
- linkml:types
|
- linkml:types
|
||||||
- ./VideoAnnotation
|
- ./VideoAnnotation
|
||||||
- ./VideoTimeSegment
|
- ./VideoTimeSegment
|
||||||
- ../slots/audio_event_segments
|
- ./AudioEventSegment
|
||||||
|
# REMOVED - migrated to has_or_had_segment with range AudioEventSegment (Rule 53)
|
||||||
|
# - ../slots/audio_event_segments
|
||||||
|
- ../slots/has_or_had_segment
|
||||||
- ../slots/has_audio_quality_score
|
- ../slots/has_audio_quality_score
|
||||||
- ../slots/diarization_confidence
|
- ../slots/diarization_confidence
|
||||||
- ../slots/diarization_enabled
|
- ../slots/diarization_enabled
|
||||||
|
|
@ -112,7 +115,9 @@ classes:
|
||||||
- wikidata:Q11028
|
- wikidata:Q11028
|
||||||
- wikidata:Q638
|
- wikidata:Q638
|
||||||
slots:
|
slots:
|
||||||
- audio_event_segments
|
# MIGRATED from audio_event_segments to has_or_had_segment (Rule 53)
|
||||||
|
# - audio_event_segments
|
||||||
|
- has_or_had_segment
|
||||||
- audio_quality_score
|
- audio_quality_score
|
||||||
- diarization_enabled
|
- diarization_enabled
|
||||||
- has_or_had_diarization_segment
|
- has_or_had_diarization_segment
|
||||||
|
|
@ -138,6 +143,20 @@ classes:
|
||||||
- has_or_had_speech_segment
|
- has_or_had_speech_segment
|
||||||
- template_specificity
|
- template_specificity
|
||||||
slot_usage:
|
slot_usage:
|
||||||
|
has_or_had_segment:
|
||||||
|
description: |
|
||||||
|
MIGRATED from audio_event_segments (Rule 53).
|
||||||
|
Audio event segments detected in the video content.
|
||||||
|
range: AudioEventSegment
|
||||||
|
multivalued: true
|
||||||
|
required: false
|
||||||
|
inlined_as_list: true
|
||||||
|
examples:
|
||||||
|
- value: '[{audio_event_type: SPEECH, start_seconds: 0.0, end_seconds: 15.0, segment_text: "Speech detected - Speaker 1", confidence: 0.95}]'
|
||||||
|
description: Speech detection segment
|
||||||
|
- value: '[{audio_event_type: MUSIC, start_seconds: 30.0, end_seconds: 60.0, segment_text: "Background classical music", confidence: 0.88}]'
|
||||||
|
description: Music detection segment
|
||||||
|
# NOTE: has_audio_event_segment is deprecated - use has_or_had_segment above
|
||||||
has_audio_event_segment:
|
has_audio_event_segment:
|
||||||
range: VideoTimeSegment
|
range: VideoTimeSegment
|
||||||
multivalued: true
|
multivalued: true
|
||||||
|
|
|
||||||
|
|
@ -546,10 +546,14 @@ fixes:
|
||||||
|
|
||||||
- original_slot_id: https://nde.nl/ontology/hc/slot/audio_event_segments
|
- original_slot_id: https://nde.nl/ontology/hc/slot/audio_event_segments
|
||||||
processed:
|
processed:
|
||||||
status: false
|
status: true
|
||||||
timestamp: null
|
timestamp: "2026-01-14T10:30:00Z"
|
||||||
session: null
|
session: "slot-migration-session-8"
|
||||||
notes: "Maps to existing AudioEventSegment class"
|
notes: |
|
||||||
|
MIGRATED: audio_event_segments → has_or_had_segment + AudioEventSegment
|
||||||
|
- Created AudioEventSegment.yaml class (hc:AudioEventSegment)
|
||||||
|
- Updated VideoAudioAnnotation.yaml: imports, slots, slot_usage
|
||||||
|
- Archived to modules/slots/archive/audio_event_segments_archived_20260114.yaml
|
||||||
revision:
|
revision:
|
||||||
- label: has_or_had_segment
|
- label: has_or_had_segment
|
||||||
type: slot
|
type: slot
|
||||||
|
|
@ -686,10 +690,14 @@ fixes:
|
||||||
|
|
||||||
- original_slot_id: https://nde.nl/ontology/hc/slot/bay_number
|
- original_slot_id: https://nde.nl/ontology/hc/slot/bay_number
|
||||||
processed:
|
processed:
|
||||||
status: false
|
status: true
|
||||||
timestamp: null
|
timestamp: "2026-01-14T10:45:00Z"
|
||||||
session: null
|
session: "slot-migration-session-8"
|
||||||
notes: "Requires BayNumber class creation"
|
notes: |
|
||||||
|
MIGRATED: bay_number → has_or_had_identifier + BayNumber
|
||||||
|
- Created BayNumber.yaml class (hc:BayNumber)
|
||||||
|
- Updated StorageUnit.yaml: imports, slots, slot_usage
|
||||||
|
- Archived to modules/slots/archive/bay_number_archived_20260114.yaml
|
||||||
revision:
|
revision:
|
||||||
- label: has_or_had_identifier
|
- label: has_or_had_identifier
|
||||||
type: slot
|
type: slot
|
||||||
|
|
@ -939,10 +947,14 @@ fixes:
|
||||||
|
|
||||||
- original_slot_id: https://nde.nl/ontology/hc/slot/box_number
|
- original_slot_id: https://nde.nl/ontology/hc/slot/box_number
|
||||||
processed:
|
processed:
|
||||||
status: false
|
status: true
|
||||||
timestamp: null
|
timestamp: "2026-01-14T10:45:00Z"
|
||||||
session: null
|
session: "slot-migration-session-8"
|
||||||
notes: "Requires BoxNumber class creation"
|
notes: |
|
||||||
|
MIGRATED: box_number → has_or_had_identifier + BoxNumber
|
||||||
|
- Created BoxNumber.yaml class (hc:BoxNumber)
|
||||||
|
- Updated StorageUnit.yaml: imports, slots, slot_usage
|
||||||
|
- Archived to modules/slots/archive/box_number_archived_20260114.yaml
|
||||||
revision:
|
revision:
|
||||||
- label: has_or_had_identifier
|
- label: has_or_had_identifier
|
||||||
type: slot
|
type: slot
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue