feat(linkml-viewer): UX improvements - entry counts, deep links, settings persistence
All checks were successful
Deploy Frontend / build-and-deploy (push) Successful in 4m4s
All checks were successful
Deploy Frontend / build-and-deploy (push) Successful in 4m4s
- Add entry count badge next to schema file name showing (xC, yE, zS) counts - Add tooltip explaining LinkML file names vs class names - Remove redundant section headers (Classes, Enums, Slots collapsible sections) - Add URL params for enum (?enum=) and slot (?slot=) deep linking - Persist category filters, dev tools visibility, and legend visibility to localStorage - Set 'Main Schema' filter to OFF by default (confusing for users) - Add Rule 48: Class files must not define inline slots
This commit is contained in:
parent
eff3153f3f
commit
3e6c2367ad
3 changed files with 334 additions and 58 deletions
140
.opencode/rules/class-files-no-inline-slots.md
Normal file
140
.opencode/rules/class-files-no-inline-slots.md
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
# Rule 48: Class Files Must Not Define Inline Slots
|
||||||
|
|
||||||
|
🚨 **CRITICAL**: LinkML class files in `schemas/20251121/linkml/modules/classes/` MUST NOT define their own slots inline. All slots MUST be imported from the centralized `modules/slots/` directory.
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
When class files define their own slots (e.g., `AccessRestriction.yaml` defining its own slot properties), this creates:
|
||||||
|
|
||||||
|
1. **Duplication**: Same slot semantics defined in multiple places
|
||||||
|
2. **Inconsistency**: Slot definitions may diverge between files
|
||||||
|
3. **Frontend Issues**: LinkML viewer cannot properly render slot relationships
|
||||||
|
4. **Maintenance Burden**: Changes require updates in multiple locations
|
||||||
|
|
||||||
|
## Architecture Requirement
|
||||||
|
|
||||||
|
```
|
||||||
|
schemas/20251121/linkml/
|
||||||
|
├── modules/
|
||||||
|
│ ├── classes/ # Class definitions ONLY
|
||||||
|
│ │ └── *.yaml # NO inline slot definitions
|
||||||
|
│ ├── slots/ # ALL slot definitions go here
|
||||||
|
│ │ └── *.yaml # One file per slot or logical group
|
||||||
|
│ └── enums/ # Enumeration definitions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Correct Pattern
|
||||||
|
|
||||||
|
**Class file** (`modules/classes/AccessRestriction.yaml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: https://nde.nl/ontology/hc/class/AccessRestriction
|
||||||
|
name: AccessRestriction
|
||||||
|
prefixes:
|
||||||
|
hc: https://nde.nl/ontology/hc/
|
||||||
|
linkml: https://w3id.org/linkml/
|
||||||
|
|
||||||
|
imports:
|
||||||
|
- linkml:types
|
||||||
|
- ../slots/restriction_type # Import slot from centralized location
|
||||||
|
- ../slots/restriction_reason
|
||||||
|
- ../slots/applies_from
|
||||||
|
- ../slots/applies_until
|
||||||
|
|
||||||
|
default_range: string
|
||||||
|
|
||||||
|
classes:
|
||||||
|
AccessRestriction:
|
||||||
|
class_uri: hc:AccessRestriction
|
||||||
|
description: >-
|
||||||
|
Describes access restrictions on heritage collections or items.
|
||||||
|
slots:
|
||||||
|
- restriction_type # Reference slot by name
|
||||||
|
- restriction_reason
|
||||||
|
- applies_from
|
||||||
|
- applies_until
|
||||||
|
```
|
||||||
|
|
||||||
|
**Slot file** (`modules/slots/restriction_type.yaml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: https://nde.nl/ontology/hc/slot/restriction_type
|
||||||
|
name: restriction_type
|
||||||
|
prefixes:
|
||||||
|
hc: https://nde.nl/ontology/hc/
|
||||||
|
schema: http://schema.org/
|
||||||
|
linkml: https://w3id.org/linkml/
|
||||||
|
|
||||||
|
imports:
|
||||||
|
- linkml:types
|
||||||
|
|
||||||
|
slots:
|
||||||
|
restriction_type:
|
||||||
|
slot_uri: hc:restrictionType
|
||||||
|
description: The type of access restriction applied.
|
||||||
|
range: string
|
||||||
|
exact_mappings:
|
||||||
|
- schema:accessMode
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-Pattern (WRONG)
|
||||||
|
|
||||||
|
**DO NOT** define slots inline in class files:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# WRONG - AccessRestriction.yaml with inline slots
|
||||||
|
classes:
|
||||||
|
AccessRestriction:
|
||||||
|
slots:
|
||||||
|
- restriction_type
|
||||||
|
|
||||||
|
slots: # ❌ DO NOT define slots here
|
||||||
|
restriction_type:
|
||||||
|
description: Type of restriction
|
||||||
|
range: string
|
||||||
|
```
|
||||||
|
|
||||||
|
## Identifying Violations
|
||||||
|
|
||||||
|
To find class files that incorrectly define slots:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find class files with inline slot definitions
|
||||||
|
grep -l "^slots:" schemas/20251121/linkml/modules/classes/*.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Files that match need refactoring:
|
||||||
|
1. Extract slot definitions to `modules/slots/`
|
||||||
|
2. Add imports for the extracted slots
|
||||||
|
3. Remove inline `slots:` section from class file
|
||||||
|
|
||||||
|
## Known Violations to Fix
|
||||||
|
|
||||||
|
The following class files have been identified as defining their own slots and require refactoring:
|
||||||
|
|
||||||
|
- `AccessRestriction.yaml` - visible at https://nde.nl/ontology/hc/class/AccessRestriction
|
||||||
|
- (Add others as discovered)
|
||||||
|
|
||||||
|
## Migration Workflow
|
||||||
|
|
||||||
|
1. **Identify inline slots** in class file
|
||||||
|
2. **Check if slot exists** in `modules/slots/`
|
||||||
|
3. **If exists**: Remove inline definition, add import
|
||||||
|
4. **If not exists**: Create new slot file in `modules/slots/`, then add import
|
||||||
|
5. **Validate**: Run `linkml-validate` to ensure schema integrity
|
||||||
|
6. **Update manifest**: Regenerate `manifest.json` if needed
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
- **Single Source of Truth**: Each slot defined exactly once
|
||||||
|
- **Reusability**: Slots can be used across multiple classes
|
||||||
|
- **Frontend Compatibility**: LinkML viewer depends on centralized slots for proper edge rendering in UML diagrams
|
||||||
|
- **Semantic Consistency**: `slot_uri` and mappings defined once, applied everywhere
|
||||||
|
- **Maintenance**: Changes to slot semantics applied in one place
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- Rule 38: Slot Centralization and Semantic URI Requirements
|
||||||
|
- Rule 39: Slot Naming Convention (RiC-O Style)
|
||||||
|
- Rule 42: No Ontology Prefixes in Slot Names
|
||||||
|
- Rule 43: Slot Nouns Must Be Singular
|
||||||
|
|
@ -479,6 +479,23 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary, #212121);
|
color: var(--text-primary, #212121);
|
||||||
margin: 0 0 0.5rem 0;
|
margin: 0 0 0.5rem 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkml-viewer__schema-name .linkml-viewer__info-icon {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkml-viewer__entry-count {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary, #757575);
|
||||||
|
font-weight: 400;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.linkml-viewer__schema-desc {
|
.linkml-viewer__schema-desc {
|
||||||
|
|
@ -505,6 +522,11 @@
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Flat section - renders content directly without collapsible header */
|
||||||
|
.linkml-viewer__section--flat {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.linkml-viewer__section-header {
|
.linkml-viewer__section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -1019,6 +1019,15 @@ const TEXT = {
|
||||||
// Copy functionality
|
// Copy functionality
|
||||||
copyUri: { nl: 'URI kopiëren', en: 'Copy URI' },
|
copyUri: { nl: 'URI kopiëren', en: 'Copy URI' },
|
||||||
uriCopied: { nl: 'URI gekopieerd!', en: 'URI copied!' },
|
uriCopied: { nl: 'URI gekopieerd!', en: 'URI copied!' },
|
||||||
|
// Schema file name tooltip
|
||||||
|
linkmlFileNameTooltip: {
|
||||||
|
nl: 'Dit is de LinkML-bestandsnaam. Deze komt vaak overeen met de klassenaam, maar niet altijd. Een bestand kan meerdere klassen, enumeraties of slots bevatten.',
|
||||||
|
en: 'This is the LinkML file name. It often corresponds to the class name, but not always. A single file can contain multiple classes, enumerations, or slots.'
|
||||||
|
},
|
||||||
|
// Entry count abbreviations
|
||||||
|
entryCountClasses: { nl: 'K', en: 'C' },
|
||||||
|
entryCountEnums: { nl: 'E', en: 'E' },
|
||||||
|
entryCountSlots: { nl: 'S', en: 'S' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dynamically discover schema files from the modules directory
|
// Dynamically discover schema files from the modules directory
|
||||||
|
|
@ -1058,14 +1067,35 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
const [_custodianTypesLoaded, setCustodianTypesLoaded] = useState(false);
|
const [_custodianTypesLoaded, setCustodianTypesLoaded] = useState(false);
|
||||||
|
|
||||||
// State for sidebar search and category filters
|
// State for sidebar search and category filters
|
||||||
|
// Category filters are persisted in localStorage
|
||||||
const [sidebarSearch, setSidebarSearch] = useState<string>('');
|
const [sidebarSearch, setSidebarSearch] = useState<string>('');
|
||||||
const [categoryFilters, setCategoryFilters] = useState<Record<string, boolean>>({
|
const [categoryFilters, setCategoryFilters] = useState<Record<string, boolean>>(() => {
|
||||||
main: true,
|
try {
|
||||||
class: true,
|
const saved = localStorage.getItem('linkml-viewer-category-filters');
|
||||||
enum: true,
|
if (saved) {
|
||||||
slot: true,
|
return JSON.parse(saved);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore parse errors
|
||||||
|
}
|
||||||
|
// Default: main OFF (confusing), others ON
|
||||||
|
return {
|
||||||
|
main: false,
|
||||||
|
class: true,
|
||||||
|
enum: true,
|
||||||
|
slot: true,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Persist category filters to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('linkml-viewer-category-filters', JSON.stringify(categoryFilters));
|
||||||
|
} catch {
|
||||||
|
// Ignore localStorage errors (e.g., private browsing)
|
||||||
|
}
|
||||||
|
}, [categoryFilters]);
|
||||||
|
|
||||||
// State for sidebar collapse
|
// State for sidebar collapse
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
@ -1146,13 +1176,47 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
const [hoveredCustodianType, setHoveredCustodianType] = useState<CustodianTypeCode | null>(null);
|
const [hoveredCustodianType, setHoveredCustodianType] = useState<CustodianTypeCode | null>(null);
|
||||||
|
|
||||||
// State for slot_usage legend visibility toggle
|
// State for slot_usage legend visibility toggle
|
||||||
const [showSlotUsageLegend, setShowSlotUsageLegend] = useState(false);
|
// Persisted in localStorage
|
||||||
|
const [showSlotUsageLegend, setShowSlotUsageLegend] = useState(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('linkml-viewer-show-legend');
|
||||||
|
return saved === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Persist legend visibility to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('linkml-viewer-show-legend', showSlotUsageLegend ? 'true' : 'false');
|
||||||
|
} catch {
|
||||||
|
// Ignore localStorage errors (e.g., private browsing)
|
||||||
|
}
|
||||||
|
}, [showSlotUsageLegend]);
|
||||||
|
|
||||||
// State for Settings menu visibility
|
// State for Settings menu visibility
|
||||||
const [showSettingsMenu, setShowSettingsMenu] = useState(false);
|
const [showSettingsMenu, setShowSettingsMenu] = useState(false);
|
||||||
|
|
||||||
// State for Developer tools menu visibility
|
// State for Developer tools menu visibility
|
||||||
const [showDeveloperTools, setShowDeveloperTools] = useState(false);
|
// Persisted in localStorage
|
||||||
|
const [showDeveloperTools, setShowDeveloperTools] = useState(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('linkml-viewer-show-devtools');
|
||||||
|
return saved === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Persist developer tools visibility to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('linkml-viewer-show-devtools', showDeveloperTools ? 'true' : 'false');
|
||||||
|
} catch {
|
||||||
|
// Ignore localStorage errors (e.g., private browsing)
|
||||||
|
}
|
||||||
|
}, [showDeveloperTools]);
|
||||||
|
|
||||||
// State for sidebar category collapse (Classes, Enums, Slots headers)
|
// State for sidebar category collapse (Classes, Enums, Slots headers)
|
||||||
const [collapsedCategories, setCollapsedCategories] = useState<Set<string>>(new Set());
|
const [collapsedCategories, setCollapsedCategories] = useState<Set<string>>(new Set());
|
||||||
|
|
@ -1624,12 +1688,48 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [categories, setSearchParams]);
|
}, [categories, setSearchParams]);
|
||||||
|
|
||||||
|
// Navigate to an enum by updating URL params and selecting the schema file
|
||||||
|
const navigateToEnum = useCallback((enumName: string) => {
|
||||||
|
setSearchParams({ enum: enumName });
|
||||||
|
setHighlightedClass(enumName); // Reuse highlight state for enums
|
||||||
|
|
||||||
|
// Find and select the enum file
|
||||||
|
const enumFile = categories
|
||||||
|
.find(cat => cat.name === 'enum')
|
||||||
|
?.files.find(file => file.name === enumName);
|
||||||
|
|
||||||
|
if (enumFile) {
|
||||||
|
setSelectedSchema(enumFile);
|
||||||
|
// Ensure enums section is expanded
|
||||||
|
setExpandedSections(prev => new Set([...prev, 'enums']));
|
||||||
|
}
|
||||||
|
}, [categories, setSearchParams]);
|
||||||
|
|
||||||
|
// Navigate to a slot by updating URL params and selecting the schema file
|
||||||
|
const navigateToSlot = useCallback((slotName: string) => {
|
||||||
|
setSearchParams({ slot: slotName });
|
||||||
|
setHighlightedClass(slotName); // Reuse highlight state for slots
|
||||||
|
|
||||||
|
// Find and select the slot file
|
||||||
|
const slotFile = categories
|
||||||
|
.find(cat => cat.name === 'slot')
|
||||||
|
?.files.find(file => file.name === slotName);
|
||||||
|
|
||||||
|
if (slotFile) {
|
||||||
|
setSelectedSchema(slotFile);
|
||||||
|
// Ensure slots section is expanded
|
||||||
|
setExpandedSections(prev => new Set([...prev, 'slots']));
|
||||||
|
}
|
||||||
|
}, [categories, setSearchParams]);
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
// Handle URL parameters for deep linking (only used on initial mount)
|
// Handle URL parameters for deep linking (only used on initial mount)
|
||||||
const handleUrlParams = useCallback((cats: SchemaCategory[], currentSearchParams: URLSearchParams) => {
|
const handleUrlParams = useCallback((cats: SchemaCategory[], currentSearchParams: URLSearchParams) => {
|
||||||
const classParam = currentSearchParams.get('class');
|
const classParam = currentSearchParams.get('class');
|
||||||
|
const enumParam = currentSearchParams.get('enum');
|
||||||
|
const slotParam = currentSearchParams.get('slot');
|
||||||
|
|
||||||
if (classParam) {
|
if (classParam) {
|
||||||
setHighlightedClass(classParam);
|
setHighlightedClass(classParam);
|
||||||
|
|
@ -1644,6 +1744,32 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
// Ensure classes section is expanded
|
// Ensure classes section is expanded
|
||||||
setExpandedSections(prev => new Set([...prev, 'classes']));
|
setExpandedSections(prev => new Set([...prev, 'classes']));
|
||||||
}
|
}
|
||||||
|
} else if (enumParam) {
|
||||||
|
setHighlightedClass(enumParam);
|
||||||
|
|
||||||
|
// Find the schema file that contains this enum
|
||||||
|
const enumFile = cats
|
||||||
|
.find(cat => cat.name === 'enum')
|
||||||
|
?.files.find(file => file.name === enumParam);
|
||||||
|
|
||||||
|
if (enumFile) {
|
||||||
|
setSelectedSchema(enumFile);
|
||||||
|
// Ensure enums section is expanded
|
||||||
|
setExpandedSections(prev => new Set([...prev, 'enums']));
|
||||||
|
}
|
||||||
|
} else if (slotParam) {
|
||||||
|
setHighlightedClass(slotParam);
|
||||||
|
|
||||||
|
// Find the schema file that contains this slot
|
||||||
|
const slotFile = cats
|
||||||
|
.find(cat => cat.name === 'slot')
|
||||||
|
?.files.find(file => file.name === slotParam);
|
||||||
|
|
||||||
|
if (slotFile) {
|
||||||
|
setSelectedSchema(slotFile);
|
||||||
|
// Ensure slots section is expanded
|
||||||
|
setExpandedSections(prev => new Set([...prev, 'slots']));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -1675,7 +1801,9 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
|
|
||||||
// Select main schema by default if no URL param set the schema
|
// Select main schema by default if no URL param set the schema
|
||||||
const classParam = searchParams.get('class');
|
const classParam = searchParams.get('class');
|
||||||
if (!classParam && cats[0]?.files.length > 0) {
|
const enumParam = searchParams.get('enum');
|
||||||
|
const slotParam = searchParams.get('slot');
|
||||||
|
if (!classParam && !enumParam && !slotParam && cats[0]?.files.length > 0) {
|
||||||
setSelectedSchema(cats[0].files[0]);
|
setSelectedSchema(cats[0].files[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3117,7 +3245,19 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
<div className="linkml-viewer__visual">
|
<div className="linkml-viewer__visual">
|
||||||
{/* Schema Metadata */}
|
{/* Schema Metadata */}
|
||||||
<div className="linkml-viewer__metadata">
|
<div className="linkml-viewer__metadata">
|
||||||
<h2 className="linkml-viewer__schema-name">{formatDisplayName(schema.name || schema.title || '') || t('unnamedSchema')}</h2>
|
<h2 className="linkml-viewer__schema-name">
|
||||||
|
{formatDisplayName(schema.name || schema.title || '') || t('unnamedSchema')}
|
||||||
|
<span className="linkml-viewer__info-icon" data-tooltip={t('linkmlFileNameTooltip')}>ⓘ</span>
|
||||||
|
{(matchingClassCount > 0 || matchingEnumCount > 0 || matchingSlotCount > 0) && (
|
||||||
|
<span className="linkml-viewer__entry-count">
|
||||||
|
({matchingClassCount > 0 && `${matchingClassCount}${t('entryCountClasses')}`}
|
||||||
|
{matchingClassCount > 0 && (matchingEnumCount > 0 || matchingSlotCount > 0) && ', '}
|
||||||
|
{matchingEnumCount > 0 && `${matchingEnumCount}${t('entryCountEnums')}`}
|
||||||
|
{matchingEnumCount > 0 && matchingSlotCount > 0 && ', '}
|
||||||
|
{matchingSlotCount > 0 && `${matchingSlotCount}${t('entryCountSlots')}`})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
{schema.description && (
|
{schema.description && (
|
||||||
<div className="linkml-viewer__schema-desc linkml-viewer__markdown">
|
<div className="linkml-viewer__schema-desc linkml-viewer__markdown">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(schema.description)}</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(schema.description)}</ReactMarkdown>
|
||||||
|
|
@ -3152,63 +3292,24 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Classes */}
|
{/* Classes - rendered directly without collapsible header */}
|
||||||
{classes.length > 0 && (
|
{classes.length > 0 && (
|
||||||
<div className="linkml-viewer__section">
|
<div className="linkml-viewer__section linkml-viewer__section--flat">
|
||||||
<button
|
{classes.map(renderClassDetails)}
|
||||||
className="linkml-viewer__section-header"
|
|
||||||
onClick={() => toggleSection('classes')}
|
|
||||||
>
|
|
||||||
<span className="linkml-viewer__section-icon">
|
|
||||||
{expandedSections.has('classes') ? '▼' : '▶'}
|
|
||||||
</span>
|
|
||||||
{t('classes')} ({custodianTypeFilter.size > 0 ? `${matchingClassCount}/${classes.length}` : classes.length})
|
|
||||||
</button>
|
|
||||||
{expandedSections.has('classes') && (
|
|
||||||
<div className="linkml-viewer__section-content">
|
|
||||||
{classes.map(renderClassDetails)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Slots */}
|
{/* Slots - rendered directly without collapsible header */}
|
||||||
{slots.length > 0 && (
|
{slots.length > 0 && (
|
||||||
<div className="linkml-viewer__section">
|
<div className="linkml-viewer__section linkml-viewer__section--flat">
|
||||||
<button
|
{slots.map(slot => renderSlotDetails(slot))}
|
||||||
className="linkml-viewer__section-header"
|
|
||||||
onClick={() => toggleSection('slots')}
|
|
||||||
>
|
|
||||||
<span className="linkml-viewer__section-icon">
|
|
||||||
{expandedSections.has('slots') ? '▼' : '▶'}
|
|
||||||
</span>
|
|
||||||
{t('slots')} ({custodianTypeFilter.size > 0 ? `${matchingSlotCount}/${slots.length}` : slots.length})
|
|
||||||
</button>
|
|
||||||
{expandedSections.has('slots') && (
|
|
||||||
<div className="linkml-viewer__section-content">
|
|
||||||
{slots.map(slot => renderSlotDetails(slot))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Enums */}
|
{/* Enums - rendered directly without collapsible header */}
|
||||||
{enums.length > 0 && (
|
{enums.length > 0 && (
|
||||||
<div className="linkml-viewer__section">
|
<div className="linkml-viewer__section linkml-viewer__section--flat">
|
||||||
<button
|
{enums.map(renderEnumDetails)}
|
||||||
className="linkml-viewer__section-header"
|
|
||||||
onClick={() => toggleSection('enums')}
|
|
||||||
>
|
|
||||||
<span className="linkml-viewer__section-icon">
|
|
||||||
{expandedSections.has('enums') ? '▼' : '▶'}
|
|
||||||
</span>
|
|
||||||
{t('enumerations')} ({custodianTypeFilter.size > 0 ? `${matchingEnumCount}/${enums.length}` : enums.length})
|
|
||||||
</button>
|
|
||||||
{expandedSections.has('enums') && (
|
|
||||||
<div className="linkml-viewer__section-content">
|
|
||||||
{enums.map(renderEnumDetails)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -3406,7 +3507,20 @@ const LinkMLViewerPage: React.FC = () => {
|
||||||
className={`linkml-viewer-page__file-item ${
|
className={`linkml-viewer-page__file-item ${
|
||||||
selectedSchema?.path === file.path ? 'linkml-viewer-page__file-item--active' : ''
|
selectedSchema?.path === file.path ? 'linkml-viewer-page__file-item--active' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setSelectedSchema(file)}
|
onClick={() => {
|
||||||
|
setSelectedSchema(file);
|
||||||
|
// Update URL params based on category type for deep linking
|
||||||
|
if (category.name === 'class') {
|
||||||
|
navigateToClass(file.name);
|
||||||
|
} else if (category.name === 'enum') {
|
||||||
|
navigateToEnum(file.name);
|
||||||
|
} else if (category.name === 'slot') {
|
||||||
|
navigateToSlot(file.name);
|
||||||
|
} else {
|
||||||
|
// For main schemas, clear the deep link params
|
||||||
|
setSearchParams({});
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{file.name}
|
{file.name}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue