Skip to content

Commit 1a617cf

Browse files
committed
Polishing html preview field output
1 parent ce090b2 commit 1a617cf

2 files changed

Lines changed: 238 additions & 39 deletions

File tree

src/export/ROCrate/RoCrateHtmlGenerator.tsx

Lines changed: 196 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react";
22
import { renderToStaticMarkup } from "react-dom/server";
3-
import * as Path from "path";
3+
import filesize from "filesize";
44

55
// --- Type Definitions ---
66
type 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

3435
const 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
7173
const 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
8385
const 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
9191
const 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

9697
const 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
124126
function 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-zA-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

Comments
 (0)