11import React from "react" ;
22import { renderToStaticMarkup } from "react-dom/server" ;
3- import * as Path from "path " ;
3+ import filesize from "filesize " ;
44
55// --- Type Definitions ---
66type RoCrateEntity = {
@@ -26,10 +26,11 @@ const FILTERED_ENTITY_IDS = [
2626 "ldac:AuthorizedAccess"
2727] ;
2828
29- // Preferred property whitelist per entity type (ordered)
30- // Each item can be a string property name or an object { property, label }
29+ // Each item can be a string property name or an object { property, label, type }
3130// Project: order to reflect prior preview expectations
32- type OrderEntry = string | { property : string ; label ?: string } ;
31+ type OrderEntry =
32+ | string
33+ | { property : string ; label ?: string ; type ?: "date" | "size" } ;
3334
3435const PROJECT_FIELDS : OrderEntry [ ] = [
3536 "description" ,
@@ -66,31 +67,31 @@ const PERSON_FIELDS: OrderEntry[] = [
6667 "gender" ,
6768 "ldac:age"
6869] ;
70+ const ORGANIZATION_FIELDS : OrderEntry [ ] = [ "description" , "url" ] ;
6971
70- // Default fields for entities that don't have specific whitelists
72+ // Default fields for entities that don't have specific field lists
7173const DEFAULT_FIELDS : OrderEntry [ ] = [
7274 "name" ,
7375 "description" ,
7476 "encodingFormat" ,
75- "contentSize" ,
76- "dateCreated" ,
77- "dateModified" ,
77+ { property : "contentSize" , type : "size" } ,
78+ { property : "dateCreated" , label : "Date Created" , type : "date" } ,
79+ { property : "dateModified" , label : "Date Modified" , type : "date" } ,
7880 "creator" ,
7981 "license"
8082] ;
8183
8284// Fields for LDAC DataReuseLicense entities
8385const LICENSE_FIELDS : OrderEntry [ ] = [
84- "name" ,
8586 "description" ,
86- { property : "ldac:access" , label : "Access" } ,
87- "license"
87+ { property : "ldac:access" , label : "Access" }
8888] ;
8989
9090// Fields for Digital Document entities
9191const DIGITAL_DOCUMENT_FIELDS : OrderEntry [ ] = [
92- { property : "encodingFormat" , label : "Encoding format" } ,
93- { property : "ldac:materialType" , label : "Material type" }
92+ { property : "encodingFormat" , label : "Encoding Format" } ,
93+ { property : "ldac:materialType" , label : "Material type" } ,
94+ { property : "contentSize" , label : "Content size" , type : "size" }
9495] ;
9596
9697const getFieldsForEntity = ( entity : RoCrateEntity ) : OrderEntry [ ] => {
@@ -100,6 +101,7 @@ const getFieldsForEntity = (entity: RoCrateEntity): OrderEntry[] => {
100101 // Check for Event type first since sessions can have both Dataset and Event types
101102 if ( types . includes ( "Event" ) ) return SESSION_FIELDS ;
102103 if ( types . includes ( "Person" ) ) return PERSON_FIELDS ;
104+ if ( types . includes ( "Organization" ) ) return ORGANIZATION_FIELDS ;
103105 if ( types . includes ( "ldac:DataReuseLicense" ) ) return LICENSE_FIELDS ;
104106 // Both DigitalDocument and file entities (ImageObject, VideoObject, AudioObject) should use the same fields
105107 if (
@@ -111,7 +113,7 @@ const getFieldsForEntity = (entity: RoCrateEntity): OrderEntry[] => {
111113 return DIGITAL_DOCUMENT_FIELDS ;
112114 if ( entity [ "@id" ] === "./" || types . includes ( "Dataset" ) )
113115 return PROJECT_FIELDS ;
114- // Pure whitelist: instead of returning empty array, return default fields
116+
115117 return DEFAULT_FIELDS ;
116118} ;
117119
@@ -123,8 +125,6 @@ const createAnchorId = (id: string): string => {
123125// Transforms property labels for display
124126function formatPropertyLabel ( propertyName : string ) : string {
125127 // Explicit mappings for clarity
126- if ( propertyName === "ldac:subjectLanguage" ) return "Subject Language" ;
127- if ( propertyName === "inLanguage" ) return "Working Language" ;
128128 if ( propertyName === "hasPart" ) return "parts" ;
129129 if ( propertyName === "pcdm:hasMember" ) return "events" ;
130130 const withoutPrefix = propertyName . replace ( / ^ [ a - z A - Z ] + : / , "" ) ;
@@ -388,8 +388,9 @@ const PropertyValue: React.FC<{
388388 value : any ;
389389 graph : RoCrateEntity [ ] ;
390390 propertyName ?: string ;
391- } > = ( { value, graph, propertyName } ) => {
392- // Handle missing/undefined/null values - show "Unknown" for whitelisted fields
391+ fieldType ?: string ;
392+ } > = ( { value, graph, propertyName, fieldType } ) => {
393+ // Handle missing/undefined/null values
393394 if ( value === null || value === undefined ) {
394395 return (
395396 < span style = { { fontStyle : "italic" , color : "var(--color-text-muted)" } } >
@@ -398,6 +399,38 @@ const PropertyValue: React.FC<{
398399 ) ;
399400 }
400401
402+ // Handle date formatting for fields with type "date"
403+ if ( fieldType === "date" && typeof value === "string" ) {
404+ try {
405+ const date = new Date ( value ) ;
406+ if ( ! isNaN ( date . getTime ( ) ) ) {
407+ // Format as YYYY-MM-DD (date only, no time)
408+ const formattedDate = date . toISOString ( ) . split ( "T" ) [ 0 ] ;
409+ return < > { formattedDate } </ > ;
410+ }
411+ } catch ( e ) {
412+ // If date parsing fails, fall through to normal rendering
413+ }
414+ }
415+
416+ // Handle size formatting for fields with type "size"
417+ if (
418+ fieldType === "size" &&
419+ ( typeof value === "string" || typeof value === "number" )
420+ ) {
421+ try {
422+ const sizeInBytes =
423+ typeof value === "string" ? parseInt ( value , 10 ) : value ;
424+ if ( ! isNaN ( sizeInBytes ) && sizeInBytes >= 0 ) {
425+ // Format as human readable size (e.g., "74 MB")
426+ const formattedSize = filesize ( sizeInBytes , { round : 0 } ) ;
427+ return < > { formattedSize } </ > ;
428+ }
429+ } catch ( e ) {
430+ // If size parsing fails, fall through to normal rendering
431+ }
432+ }
433+
401434 if ( Array . isArray ( value ) ) {
402435 return (
403436 < >
@@ -407,6 +440,7 @@ const PropertyValue: React.FC<{
407440 value = { item }
408441 graph = { graph }
409442 propertyName = { propertyName }
443+ fieldType = { fieldType }
410444 />
411445 { index < value . length - 1 && ", " }
412446 </ React . Fragment >
@@ -488,7 +522,9 @@ const PropertyValue: React.FC<{
488522
489523 // Check if this is an LDAC term ID that should link externally even if not found in graph
490524 if ( ! referencedEntity && id . startsWith ( "ldac:" ) ) {
491- const displayName = id . replace ( "ldac:" , "" ) ;
525+ const termName = id . replace ( "ldac:" , "" ) ;
526+ // Convert camelCase to spaced words using title case (e.g., "PrimaryMaterial" -> "Primary Material")
527+ const displayName = termName . replace ( / ( [ a - z ] ) ( [ A - Z ] ) / g, "$1 $2" ) ;
492528 const externalUrl = getLdacTermUrl ( id ) ;
493529 return (
494530 < a href = { externalUrl } target = "_blank" rel = "noopener noreferrer" >
@@ -547,7 +583,9 @@ const PropertyValue: React.FC<{
547583 { languageCodes . map ( ( code , index ) => {
548584 // Try to find a Language entity in the graph for this code
549585 const languageEntity = graph . find (
550- ( entity ) => entity [ "@type" ] === "Language" && entity . code === code
586+ ( entity ) =>
587+ entity [ "@type" ] === "Language" &&
588+ entity [ "@id" ] === `#language_${ code } `
551589 ) ;
552590
553591 if ( languageEntity ) {
@@ -656,6 +694,54 @@ const Entity: React.FC<{
656694
657695 if ( isImage ) {
658696 const displayPath = getDisplayPath ( id ) ;
697+ const fields = getFieldsForEntity ( entity ) ;
698+ const labelOverrideMap = new Map < string , string > ( ) ;
699+ fields . forEach ( ( entry ) => {
700+ if ( typeof entry !== "string" && entry ?. label && entry . property ) {
701+ labelOverrideMap . set ( entry . property , entry . label ) ;
702+ }
703+ } ) ;
704+
705+ const propertiesToRender : Array < [ string , any , string ?] > = [ ] ;
706+ const usedLabels = new Set < string > ( ) ; // Track labels to avoid duplicates
707+
708+ fields . forEach ( ( entry ) => {
709+ const key = typeof entry === "string" ? entry : entry . property ;
710+ const fieldType = typeof entry === "string" ? undefined : entry . type ;
711+ const label = labelOverrideMap . get ( key ) ?? formatPropertyLabel ( key ) ;
712+ const value = ( entity as any ) [ key ] ;
713+
714+ // Check if this property has a value
715+ const hasValue = value !== null && value !== undefined ;
716+
717+ // If we already have a label and this property has no value, skip it
718+ if ( usedLabels . has ( label ) && ! hasValue ) {
719+ return ;
720+ }
721+
722+ // If this property has a value, it can override a previous "Unknown" entry with the same label
723+ if ( hasValue && usedLabels . has ( label ) ) {
724+ // Find and remove any existing entry with this label that has no value
725+ const existingIndex = propertiesToRender . findIndex (
726+ ( [ existingKey , existingValue ] ) => {
727+ const existingLabel =
728+ labelOverrideMap . get ( existingKey ) ??
729+ formatPropertyLabel ( existingKey ) ;
730+ return (
731+ existingLabel === label &&
732+ ( existingValue === null || existingValue === undefined )
733+ ) ;
734+ }
735+ ) ;
736+ if ( existingIndex !== - 1 ) {
737+ propertiesToRender . splice ( existingIndex , 1 ) ;
738+ }
739+ }
740+
741+ propertiesToRender . push ( [ key , value , fieldType ] ) ;
742+ usedLabels . add ( label ) ;
743+ } ) ;
744+
659745 return (
660746 < div className = { entityClasses } id = { anchorId } >
661747 < EntityHeader />
@@ -688,13 +774,77 @@ const Entity: React.FC<{
688774 >
689775 Image could not be loaded: { displayPath }
690776 </ div >
777+ { propertiesToRender . map ( ( [ key , value , fieldType ] ) => {
778+ const label = labelOverrideMap . get ( key ) ?? formatPropertyLabel ( key ) ;
779+ return (
780+ < div key = { key } className = "property" >
781+ < span className = "property-name" > { label } :</ span >
782+ < span className = "property-value" >
783+ < PropertyValue
784+ value = { value }
785+ graph = { graph }
786+ propertyName = { key }
787+ fieldType = { fieldType }
788+ />
789+ </ span >
790+ </ div >
791+ ) ;
792+ } ) }
691793 </ div >
692794 ) ;
693795 }
694796
695797 if ( isVideo || isAudio ) {
696798 const MediaTag = isVideo ? "video" : "audio" ;
697799 const displayPath = getDisplayPath ( id ) ;
800+ const fields = getFieldsForEntity ( entity ) ;
801+ const labelOverrideMap = new Map < string , string > ( ) ;
802+ fields . forEach ( ( entry ) => {
803+ if ( typeof entry !== "string" && entry ?. label && entry . property ) {
804+ labelOverrideMap . set ( entry . property , entry . label ) ;
805+ }
806+ } ) ;
807+
808+ const propertiesToRender : Array < [ string , any , string ?] > = [ ] ;
809+ const usedLabels = new Set < string > ( ) ; // Track labels to avoid duplicates
810+
811+ fields . forEach ( ( entry ) => {
812+ const key = typeof entry === "string" ? entry : entry . property ;
813+ const fieldType = typeof entry === "string" ? undefined : entry . type ;
814+ const label = labelOverrideMap . get ( key ) ?? formatPropertyLabel ( key ) ;
815+ const value = ( entity as any ) [ key ] ;
816+
817+ // Check if this property has a value
818+ const hasValue = value !== null && value !== undefined ;
819+
820+ // If we already have a label and this property has no value, skip it
821+ if ( usedLabels . has ( label ) && ! hasValue ) {
822+ return ;
823+ }
824+
825+ // If this property has a value, it can override a previous "Unknown" entry with the same label
826+ if ( hasValue && usedLabels . has ( label ) ) {
827+ // Find and remove any existing entry with this label that has no value
828+ const existingIndex = propertiesToRender . findIndex (
829+ ( [ existingKey , existingValue ] ) => {
830+ const existingLabel =
831+ labelOverrideMap . get ( existingKey ) ??
832+ formatPropertyLabel ( existingKey ) ;
833+ return (
834+ existingLabel === label &&
835+ ( existingValue === null || existingValue === undefined )
836+ ) ;
837+ }
838+ ) ;
839+ if ( existingIndex !== - 1 ) {
840+ propertiesToRender . splice ( existingIndex , 1 ) ;
841+ }
842+ }
843+
844+ propertiesToRender . push ( [ key , value , fieldType ] ) ;
845+ usedLabels . add ( label ) ;
846+ } ) ;
847+
698848 return (
699849 < div className = { entityClasses } id = { anchorId } >
700850 < EntityHeader />
@@ -708,11 +858,26 @@ const Entity: React.FC<{
708858 < source src = { displayPath } type = { entity . encodingFormat || "" } />
709859 Your browser does not support the { MediaTag } tag.
710860 </ MediaTag >
861+ { propertiesToRender . map ( ( [ key , value , fieldType ] ) => {
862+ const label = labelOverrideMap . get ( key ) ?? formatPropertyLabel ( key ) ;
863+ return (
864+ < div key = { key } className = "property" >
865+ < span className = "property-name" > { label } :</ span >
866+ < span className = "property-value" >
867+ < PropertyValue
868+ value = { value }
869+ graph = { graph }
870+ propertyName = { key }
871+ fieldType = { fieldType }
872+ />
873+ </ span >
874+ </ div >
875+ ) ;
876+ } ) }
711877 </ div >
712878 ) ;
713879 }
714880
715- // Generic Entity View - Pure whitelist approach
716881 const fields = getFieldsForEntity ( entity ) ;
717882 const labelOverrideMap = new Map < string , string > ( ) ;
718883 fields . forEach ( ( entry ) => {
@@ -721,12 +886,12 @@ const Entity: React.FC<{
721886 }
722887 } ) ;
723888
724- // Pure whitelist approach: render ONLY whitelisted fields, always render them regardless of existence
725- const propertiesToRender : Array < [ string , any ] > = [ ] ;
889+ const propertiesToRender : Array < [ string , any , string ?] > = [ ] ;
726890 const usedLabels = new Set < string > ( ) ; // Track labels to avoid duplicates
727891
728892 fields . forEach ( ( entry ) => {
729893 const key = typeof entry === "string" ? entry : entry . property ;
894+ const fieldType = typeof entry === "string" ? undefined : entry . type ;
730895 const label = labelOverrideMap . get ( key ) ?? formatPropertyLabel ( key ) ;
731896 const value = ( entity as any ) [ key ] ;
732897
@@ -757,14 +922,13 @@ const Entity: React.FC<{
757922 }
758923 }
759924
760- propertiesToRender . push ( [ key , value ] ) ;
925+ propertiesToRender . push ( [ key , value , fieldType ] ) ;
761926 usedLabels . add ( label ) ;
762927 } ) ;
763928
764929 // Special lists for root dataset - using direct entity type checking instead of filtering
765930 const specialLists : { [ key : string ] : RoCrateEntity [ ] } = { } ;
766931
767- // Pure whitelist approach: only populate lists for root dataset, check types directly
768932 if ( isRootDataset ) {
769933 specialLists [ "Sessions" ] = [ ] ;
770934 specialLists [ "People" ] = [ ] ;
@@ -828,13 +992,18 @@ const Entity: React.FC<{
828992 (< a href = "https://github.com/onset/lameta" > github</ a > ) software.
829993 </ p >
830994 ) }
831- { propertiesToRender . map ( ( [ key , value ] ) => {
995+ { propertiesToRender . map ( ( [ key , value , fieldType ] ) => {
832996 const label = labelOverrideMap . get ( key ) ?? formatPropertyLabel ( key ) ;
833997 return (
834998 < div key = { key } className = "property" >
835999 < span className = "property-name" > { label } :</ span >
8361000 < span className = "property-value" >
837- < PropertyValue value = { value } graph = { graph } propertyName = { key } />
1001+ < PropertyValue
1002+ value = { value }
1003+ graph = { graph }
1004+ propertyName = { key }
1005+ fieldType = { fieldType }
1006+ />
8381007 </ span >
8391008 </ div >
8401009 ) ;
0 commit comments