17 KiB
Displaying Uploaded Images and Documents in the Details Side Panel
Problem
When users upload genealogy data using GEDCOM ZIP (GDZ) files or by selecting local images alongside a GEDCOM file, the images display correctly on the family tree chart nodes. However, these same photos, as well as any other uploaded document attachments, fail to render in the details side panel and events list. This occurs because the side panel reads directly from the raw GEDCOM entries, which only contain relative local file paths that the browser cannot resolve. The goal of this feature is to pass the mapped local object URLs to the details panel and resolve these paths dynamically, ensuring a consistent and complete viewing experience for all uploaded media.
The Technical Plan
To make uploaded images and files visible in the side panel, we will connect the loaded browser URL mapping to the components that display individual details and event lists.
The plan involves four major areas:
- Data Model Augmentation: We will store the temporary file mapping (
imagesmap) in the mainTopolaDatacontainer so it is available alongside the parsed GEDCOM data. - Propagating the Mapping: The main
Appcomponent holds the loadedTopolaData. We will pass theimagesmap as a prop to theSidePanelcomponent, which will in turn pass it down toDetailsandEvents. - Resolving Relative Paths: We will introduce a utility function that looks up relative paths or filenames in the
imagesmap. If a relative path is referenced in the GEDCOM, we will replace it with the resolved browser URL. - Permissive File Extraction: We will modify the parser utility to stop filtering out non-web paths (paths that do not start with
http), allowing the application to process relative file paths.
Alternatives Considered
1. Pre-resolving URLs Directly in the Raw GEDCOM Data Model
We considered modifying the parsed GedcomData object to replace relative file paths with blob: URLs at load time, similar to how we pre-process chartData.
- Why Rejected:
GedcomDatais designed to be a raw, immutable representation of the source GEDCOM file structure. Directly mutating this tree could break features that expect raw tag data (such as exports, raw view logs, or metadata parsing). Keeping theimagesresolver map separate maintains a clean separation of concerns and preserves the integrity of the original GEDCOM entries.
2. Base64-Encoding and Persisting Images in Session Storage
We considered serializing the local file contents as Base64 strings so they could be saved in sessionStorage and survive page reloads.
- Why Rejected: Browser
sessionStorageis typically limited to 5MB. Genealogy ZIP archives can easily contain tens or hundreds of megabytes of media files, which would instantly exceed this quota and cause the application to crash. The performance cost of encoding and decoding large files in session storage is also prohibitive.
3. Rendering Placeholders for Unresolved Local Media References
We considered rendering warning cards or broken image placeholders for relative paths that cannot be resolved (i.e., when a GEDCOM lists a photo that wasn't included in the uploaded files/ZIP).
- Why Rejected: GEDCOM files created by desktop software often contain references to hundreds of local photos stored on the user's computer. Since users typically upload only the GEDCOM file (or a zip containing a subset of files), displaying error panels or placeholders for every missing image would clutter the UI. Returning
nullto gracefully hide missing media keeps the side panel clean and focused on available information.
4. Storing both full relative path and base filename in the images map
We considered storing files under both their full path (e.g. photos/person.jpg) and their base filename (e.g. person.jpg) inside the images map to handle cases where the GEDCOM file does not preserve the folder name.
- Why Rejected: We assume the GEDCOM file references files preserving the folder name as exported by the source application. Storing both keys could also lead to collisions if two different folders contain files with the same name.
Detailed Implementation
This section lists every file that will be created or changed, the step-by-step changes, and the technical rationale.
1. gedcom_util.ts
Proposed Changes:
- Update
TopolaDatainterface to addimages?: Map<string, string>;. - In
convertGedcom, return the inputimagesmap in the output object. - Remove
entry.data.startsWith('http')check insidefindFileEntryand ensureentry.datais a non-empty string before running predicate checks to avoid crashes. - Add
.webpto theIMAGE_EXTENSIONSlist to support modern image formats. - Update
isImageFileto strip query parameters (?) and hashes (#) from the path/URL before verifying the extension. - Centralize filename extraction inside
getFileName: ifTITLorFORMis missing, extract the filename fromfileEntry.data(normalizing backslashes to forward slashes and stripping query parameters or hashes first). - Implement and export
isBrowserLoadable(url: string): booleanwhich returnstrueif the URL starts withhttp://,https://,blob:,data:, or//. - Implement and export
resolveFileUrl(url: string, images?: Map<string, string>): string. This function:- Returns the input URL immediately if it is browser-loadable (checked via
isBrowserLoadable). - Replaces all backslashes (
\) with forward slashes (/). - Checks the
imagesmap for the normalized lowercase path first, then for the lowercase base filename. - Returns the mapped
blob:URL if found; otherwise, returns the original normalized URL.
- Robustness check: The function will safely check if
imagesis a validMap(usingimages instanceof Map) to avoid runtime errors when data is loaded from session storage.
- Returns the input URL immediately if it is browser-loadable (checked via
- Refactor
getImageFileEntryandgetNonImageFileEntrytogetImageFileEntriesandgetNonImageFileEntriesto return all matching file entries within anOBJEtag, supporting multimedia objects with multiple file attachments. - Update
filterImageinsidegedcom_util.tsto resolve the URL using the newresolveFileUrlhelper, ensuring case-insensitive lookups for chart node images.
Rationale:
Extending the model interface TopolaData binds the lifetime of the images map to the active loaded dataset. Removing startsWith('http') allows extracting relative paths. Centralizing the loadability check, URL decoding, case normalization, and base filename extraction helpers avoids code duplication. Supporting multiple file entries per OBJE record aligns with the GEDCOM multimedia specifications.
2. load_data.ts
Proposed Changes:
- In
prepareData, pass theimagesmap parameter toconvertGedcomand return it inside the resulting data object. - Ensure that the
imagesmap is not stored insessionStorageby explicitly deleting or omitting theimageskey before callingJSON.stringify(data)inprepareData. This guarantees thatdata.imageswill beundefined(and not a plain empty object{}) when restored fromsessionStorage. - In
loadGedzip, store the unzipped files in theimagesmap using lowercase keys and normalizing backslashes to forward slashes to ensure case-insensitive lookup. - Wrap the unzipping/extraction loop in
loadGedzipin atry-catchblock. If extraction fails or no GEDCOM file is found, revoke allblob:URLs created so far in the loop. - Implement
try-catchcleanup blocks in bothloadGedcomandloadFromUrlto ensure that if file loading or parsing throws an error, allblob:URLs in theimagesmap are immediately revoked to prevent memory leaks.
Rationale:
Connects the ingestion pipeline to the TopolaData model. Deleting images prior to sessionStorage serialization prevents restoring a broken plain object {}. Adding try-catch blocks to cleanup images on loading or parsing failures prevents memory leaks from unrevoked blob: URLs.
3. side-panel.tsx
Proposed Changes:
- Locate
<Details>rendering block inside thetabsdefinition. - Pass the map to the details panel:
images={data.images}.
Rationale:
Acts as the intermediary component that forwards data properties from the root application state to the details view pane.
4. details.tsx
Proposed Changes:
- Update the
Propsinterface forDetailsto acceptimages?: Map<string, string>;. - Pass
props.imagestoimageDetailsandfileDetails(using inline arrow functions insidegetSectionForEachMatchingEntryandcombineAllMatchingEntriesIntoSingleSection), and<Events ... images={props.images} />. - In
imageDetails, retrieve all image file entries usinggetImageFileEntries. For each, resolve the URL usingresolveFileUrl(..., images)and renderWrappedImagewith the resolved URL and its filename. - In
fileDetails, retrieve all non-image file entries usinggetNonImageFileEntries. For each, resolve the URL usingresolveFileUrl(..., images)and pass the resolved URLs toAdditionalFiles.
Rationale:
Directly resolves image and non-image document URLs from the loaded files list. Iterating over all resolved files from getImageFileEntries and getNonImageFileEntries supports multiple attachments per object record.
5. events.tsx
Proposed Changes:
- Update the
Propsinterface forEventsto acceptimages?: Map<string, string>;. - Pass
imagestotoEvent->toIndiEvent&toFamilyEvents->eventImages&eventFiles. - In
eventImages(entry, gedcom, images), extract all image files usinggetImageFileEntries. For each, resolve the URL usingresolveFileUrland add to the returned array. - In
eventFiles(entry, gedcom, images), extract all non-image files usinggetNonImageFileEntries. For each, resolve the URL usingresolveFileUrland add to the returned array.
Rationale:
Allows life events to load, resolve, and render all associated media files, supporting multiple files and case-insensitive resolution.
6. additional-files.tsx
Proposed Changes:
- Map file extensions to Semantic UI icons (e.g.
file pdf outlinefor PDF,file image outlinefor images, etc.) to improve the design aesthetics. - Add the
downloadattribute to the<a>tag forblob:URLs to trigger a file download instead of triggering browser security blocks on top-level navigations:download={file.url.startsWith('blob:') ? file.filename || true : undefined} - Update
AdditionalFilescomponent to check if the file is browser-loadable usingisBrowserLoadable(file.url). For non-loadable URLs:- Do not render the
<a>link (thus preventing download/navigation actions). - Render the filename as grayed-out plain text with an italicized
(File not uploaded)helper message.
- Do not render the
Rationale:
Implements premium file icon layouts based on file types. Adding the download attribute is critical to bypass browser-level blocks on opening blob: URLs in new tabs. Checking loadability prevents users from navigating to broken local paths.
7. wrapped-image.tsx
Proposed Changes:
- Update
WrappedImageto check ifprops.urlis browser-loadable usingisBrowserLoadable. If not, immediately render a clean gray card placeholder (displaying the image icon, the title or filename, and a "File not uploaded" badge) without rendering the<img>tag or trying to load the image.
Rationale:
Prevents browser 404/CORS console errors when attempting to load raw local relative paths as images, presenting a consistent and clean "File not uploaded" message to the user instead.
8. app.tsx
Proposed Changes:
- Add a
useEffectcleanup hook in theAppcomponent that monitors changes to the activedatastate. Whendatachanges, it iterates over the olddata.imagesmap and callsURL.revokeObjectURL(url)for allblob:URLs. - In the path routing
useEffectof theAppcomponent, if the path is not/view, explicitly reset thedatastate toundefined. This triggers the cleanup hook to revoke all activeblob:URLs when leaving the chart viewer (e.g. going back to the landing page).
Rationale:
Prevents browser memory leaks from unrevoked object URLs when users upload and switch between different genealogy files, or navigate away from the viewer.
9. upload_menu.tsx
Proposed Changes:
- Update the file input
acceptattribute to allow selecting.webpimages. - Remove the local
isImageFileNamehelper function and importisImageFilefromgedcom_util.tsto ensure consistent file extension support. - Convert individually uploaded filenames to lowercase when storing them as keys in the
imagesmap to ensure case-insensitive matching:images.set(file.name.toLowerCase(), URL.createObjectURL(file));
Rationale:
Extends modern image format support and unifies the image extension logic across the codebase. Storing keys as lowercase enables case-insensitive lookups for individually uploaded files.
10. Translation JSON Files
Proposed Changes:
- Add the localization key
"media.not_uploaded"to all translation files (bg.json,cs.json,de.json,fr.json,it.json,pl.json,ru.json).
Rationale:
Ensures the new "File not uploaded" text badge is localized in all supported languages of the application.
Verification Plan
We will verify the changes using automated unit tests, end-to-end regression checks, and manual visual testing.
Automated Unit Tests
We will run and add unit tests to ensure correct data resolution:
- Existing Test Verification: Run
npm testto verify all current unit tests pass. resolveFileUrlUnit Tests: Add tests insrc/util/gedcom_util.spec.tsto verify:- Resolving a relative path that matches a key in the
imagesmap returns the correctblob:URL. - Resolving a filename that matches a key in the
imagesmap returns the correctblob:URL. - Resolving a URL that doesn't exist in the
imagesmap returns the original URL unchanged.
- Resolving a relative path that matches a key in the
findFileEntryPermissive Extract: Add tests insrc/util/gedcom_util.spec.tsto verify thatfindFileEntrymatches and returns local relativeFILEentries (e.g.,images/photo.jpg), which were previously filtered out.- Session cache recovery verification: Add a test that verifies reloading the page correctly loads data from
sessionStoragewhendata.imagesisundefined, ensuring no crashes occur.
Playwright Visual & E2E Tests
We will add automated verification of the upload flow and visual rendering:
- Add visual test: Create a new test case in
tests/details_visual.spec.ts:- Start at
/. - Setup Playwright's
filechooserlistener. - Trigger the file upload menu, select and upload the
src/datasource/testdata/test.gdzfile. - Click the node for the individual that contains the photo (e.g., Radobod).
- Wait for the side panel to open and the image to load.
- Match sidebar screenshot against a visual regression baseline (
details-uploaded-gdz-photo.png).
- Start at
- Add missing file placeholder test: Verify that if a linked attachment is missing, a "File not uploaded" label or placeholder card renders in the details pane, matching a baseline screenshot.
- Run
npm run test:visualto confirm tests execute and pass.
Manual / Visual Verification
We will manually verify the visual rendering on a local development server:
- Run
npm run devto launch the local application. - Prepare a test GDZ file: Zip the contents of the
docker/examples/photosdirectory (which containsfamily.gedreferencing relative photo paths under aphotossubfolder). - Upload the resulting GDZ file to the application.
- Verify that:
- The primary individual's photo displays correctly in the side panel Details tab.
- Any documents or images linked to events (such as birth or marriage events) are rendered correctly in the event info cards.
- GEDCOM files referencing missing images render "File not uploaded" placeholders gracefully instead of breaking or showing broken image icons.
Future Considerations
IndexedDB for Upload Persistence
Currently, we reject persisting images in sessionStorage due to the 5MB size limit. In the future, we could consider using IndexedDB to store the zipped .gdz archives or the extracted file blobs. Since IndexedDB supports much larger storage quotas (typically hundreds of megabytes or more), this would allow uploaded genealogy trees and their media attachments to persist across page reloads and browser restarts, providing a much smoother user experience.