fix(linkml-viewer): 3D cube visualization bugs - prevent click-to-filter and parse JSON custodian_types

- Remove onFaceClick prop from CustodianTypeIndicator3D in class/slot/enum detail views
  to prevent accidental filtering when clicking decorative cubes (Bug 3)
- Add parseCustodianTypesAnnotation() helper to handle JSON-stringified arrays like '["A"]'
  in YAML annotations, fixing Bug 2 where all 19 letters appeared on every cube
- Legend bar retains onTypeClick for intentional filtering functionality
This commit is contained in:
kempersc 2025-12-23 20:40:32 +01:00
parent 5e8a432ef0
commit 54869589d1
2 changed files with 37 additions and 26 deletions

View file

@ -1018,14 +1018,43 @@ class LinkMLSchemaService {
if (!annotations?.custodian_types) {
return null;
}
// Handle both string array and comma-separated string formats
const types = annotations.custodian_types;
return this.parseCustodianTypesAnnotation(annotations.custodian_types);
}
/**
* Parse custodian_types annotation value into string array
* Handles multiple formats:
* - Array: ["A", "L", "M"]
* - JSON stringified array: '["A", "L"]' or '["A"]'
* - Comma-separated string: "A, L, M"
* - Single value: "A"
*/
private parseCustodianTypesAnnotation(types: unknown): string[] | null {
// Already an array
if (Array.isArray(types)) {
return types as string[];
}
if (typeof types === 'string') {
return types.split(',').map((t: string) => t.trim());
const trimmed = types.trim();
// Check if it's a JSON stringified array (starts with '[' and ends with ']')
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
const parsed = JSON.parse(trimmed);
if (Array.isArray(parsed)) {
return parsed.map((t: unknown) => String(t).trim());
}
} catch {
// JSON parse failed, fall through to comma-separated handling
console.warn(`[LinkMLSchemaService] Failed to parse custodian_types JSON: ${trimmed}`);
}
}
// Comma-separated string or single value
return trimmed.split(',').map((t: string) => t.trim()).filter(t => t.length > 0);
}
return null;
}
@ -1039,14 +1068,7 @@ class LinkMLSchemaService {
if (!slot?.annotations?.custodian_types) {
return null;
}
const types = slot.annotations.custodian_types;
if (Array.isArray(types)) {
return types as string[];
}
if (typeof types === 'string') {
return types.split(',').map((t: string) => t.trim());
}
return null;
return this.parseCustodianTypesAnnotation(slot.annotations.custodian_types);
}
/**
@ -1061,12 +1083,7 @@ class LinkMLSchemaService {
// Currently enum annotations might be on individual values
if ((enumDef as unknown as { annotations?: Record<string, unknown> })?.annotations?.custodian_types) {
const types = (enumDef as unknown as { annotations: Record<string, unknown> }).annotations.custodian_types;
if (Array.isArray(types)) {
return types as string[];
}
if (typeof types === 'string') {
return types.split(',').map((t: string) => t.trim());
}
return this.parseCustodianTypesAnnotation(types);
}
return null;
}

View file

@ -1145,7 +1145,7 @@ const LinkMLViewerPage: React.FC = () => {
showTooltip={true}
hoveredType={hoveredCustodianType}
onTypeHover={setHoveredCustodianType}
onFaceClick={handleCustodianTypeFilter}
// Removed onFaceClick - cube should be decorative, not trigger filtering
className="linkml-viewer__custodian-indicator"
/>
) : (
@ -1229,7 +1229,7 @@ const LinkMLViewerPage: React.FC = () => {
showTooltip={true}
hoveredType={hoveredCustodianType}
onTypeHover={setHoveredCustodianType}
onFaceClick={handleCustodianTypeFilter}
// Removed onFaceClick - cube should be decorative, not trigger filtering
className="linkml-viewer__custodian-indicator"
/>
) : (
@ -1242,12 +1242,6 @@ const LinkMLViewerPage: React.FC = () => {
)
)}
</h4>
{slot.slot_uri && (
<div className="linkml-viewer__uri">
<span className="linkml-viewer__label">{t('uri')}</span>
<code>{slot.slot_uri}</code>
</div>
)}
{slot.range && (
<div className="linkml-viewer__range">
<span className="linkml-viewer__label">{t('range')}</span>
@ -1322,7 +1316,7 @@ const LinkMLViewerPage: React.FC = () => {
showTooltip={true}
hoveredType={hoveredCustodianType}
onTypeHover={setHoveredCustodianType}
onFaceClick={handleCustodianTypeFilter}
// Removed onFaceClick - cube should be decorative, not trigger filtering
className="linkml-viewer__custodian-indicator"
/>
) : (