Files
topola-viewer/src/sidepanel/details/details.tsx

410 lines
9.7 KiB
TypeScript

import flatMap from 'array.prototype.flatmap';
import {GedcomEntry} from 'parse-gedcom';
import {FormattedMessage} from 'react-intl';
import {Header, Item} from 'semantic-ui-react';
import {
dereference,
GedcomData,
getData,
getFileName,
getImageFileEntry,
getNonImageFileEntry,
mapToSource,
resolveFileUrl,
} from '../../util/gedcom_util';
import {Config, Ids} from '../config/config';
import {AdditionalFiles, FileEntry} from './additional-files';
import {ALL_SUPPORTED_EVENT_TYPES, Events} from './events';
import {ImmediateFamily} from './immediate-family';
import {MultilineText} from './multiline-text';
import {Sources} from './sources';
import {TranslatedTag} from './translated-tag';
import {WrappedImage} from './wrapped-image';
const EXCLUDED_TAGS = [
...ALL_SUPPORTED_EVENT_TYPES,
'NAME',
'SEX',
'FAMC',
'FAMS',
'NOTE',
'SOUR',
'FACT',
];
function dataDetails(entry: GedcomEntry) {
const lines = [];
if (entry.data) {
lines.push(...getData(entry));
}
entry.tree
.filter((subentry) => subentry.tag === 'NOTE')
.forEach((note) =>
getData(note).forEach((line) => lines.push(<i>{line}</i>)),
);
if (!lines.length) {
return null;
}
return (
<>
<Header sub>
<TranslatedTag tag={entry.tag} />
</Header>
<span>
<MultilineText lines={lines} />
</span>
</>
);
}
function attributeDetails(entry: GedcomEntry) {
if (!entry.data) {
return null;
}
const attributeName = entry.tree
.filter((subentry) => subentry.tag === 'TYPE')
.flatMap((type) => getData(type))
.join()
.trim();
const attributeValue = getData(entry).join(' ').trim();
if (attributeName) {
return (
<>
<Header sub>
<TranslatedTag tag={entry.tag} />
</Header>
<div>
<b>{attributeName}</b>: {attributeValue}
</div>
</>
);
} else {
return (
<>
<Header sub>
<TranslatedTag tag={entry.tag} />
</Header>
<div>{attributeValue}</div>
</>
);
}
}
function imageDetails(
objectEntryReference: GedcomEntry,
gedcom: GedcomData,
images?: Map<string, string>,
) {
const imageEntry = dereference(
objectEntryReference,
gedcom,
(gedcom) => gedcom.other,
);
const imageFileEntry = getImageFileEntry(imageEntry);
if (!imageFileEntry || !hasData(imageEntry)) {
return null;
}
return (
<div className="person-image">
<WrappedImage
url={resolveFileUrl(imageFileEntry.data, images)}
filename={getFileName(imageFileEntry) || ''}
/>
</div>
);
}
function sourceDetails(
sourceReferenceEntries: GedcomEntry[],
gedcom: GedcomData,
) {
const sources = sourceReferenceEntries.map((sourceEntryReference) =>
mapToSource(sourceEntryReference, gedcom),
);
if (!sources.length) {
return null;
}
return (
<>
<div className="item-header">
<Header as="span" size="small">
<TranslatedTag tag="SOUR" />
</Header>
</div>
<Sources sources={sources} />
</>
);
}
function fileDetails(
objectEntries: GedcomEntry[],
gedcom: GedcomData,
images?: Map<string, string>,
) {
const files: FileEntry[] = [];
objectEntries
.map((objectEntry) =>
dereference(objectEntry, gedcom, (gedcom) => gedcom.other),
)
.forEach((objectEntry) => {
const fileEntry = getNonImageFileEntry(objectEntry);
if (fileEntry) {
files.push({
url: resolveFileUrl(fileEntry.data, images),
filename: getFileName(fileEntry),
titl: objectEntry.tree.find((entry) => entry.tag === 'TITL')?.data,
});
}
});
if (!files.length) {
return null;
}
return (
<>
<div className="item-header">
<Header as="span" size="small">
<TranslatedTag tag="OBJE" />
</Header>
</div>
<AdditionalFiles files={files} />
</>
);
}
function noteDetails(noteEntryReference: GedcomEntry, gedcom: GedcomData) {
const noteEntry = dereference(
noteEntryReference,
gedcom,
(gedcom) => gedcom.other,
);
if (!noteEntry || !hasData(noteEntry)) {
return null;
}
return (
<MultilineText
lines={getData(noteEntry).map((line, index) => (
<i key={index}>{line}</i>
))}
/>
);
}
function nameDetails(entry: GedcomEntry) {
const prefix = entry.tree.find((entry) => entry.tag === 'NPFX')?.data;
let given = entry.tree.find((entry) => entry.tag === 'GIVN')?.data;
let rufname = entry.tree.find((entry) => entry.tag === '_RUFNAME')?.data;
const nickname = entry.tree.find((entry) => entry.tag === 'NICK')?.data;
const surnamePrefix = entry.tree.find((entry) => entry.tag === 'SPFX')?.data;
const surname = entry.tree.find((entry) => entry.tag === 'SURN')?.data;
const suffix = entry.tree.find((entry) => entry.tag === 'NSFX')?.data;
// If _RUFNAME is included in GIVN, then replace this part in GIVN with this part in quotation marks,
// so that this name is not shown twice.
if (given && rufname && given.includes(rufname)) {
given = given.replace(rufname, `"${rufname}"`);
rufname = undefined;
}
const fullNameParts = [
prefix,
given,
rufname && `"${rufname}"`,
nickname && `(${nickname})`,
surnamePrefix,
surname,
suffix,
].filter(Boolean);
const fullName =
fullNameParts.join(' ').trim() || entry.data.replaceAll('/', '') || '';
const nameType = entry.tree.find(
(entry) => entry.tag === 'TYPE' && entry.data !== 'Unknown',
)?.data;
return (
<>
<Header as="span" size="large">
{fullName ? (
fullName
) : (
<FormattedMessage id="name.unknown_name" defaultMessage="N.N." />
)}
</Header>
{fullName && nameType && (
<Item.Meta>
<TranslatedTag tag={nameType} />
</Item.Meta>
)}
</>
);
}
function getSectionForEachMatchingEntry(
entries: GedcomEntry[],
gedcom: GedcomData,
tags: string[],
detailsFunction: (
entry: GedcomEntry,
gedcom: GedcomData,
images?: Map<string, string>,
) => React.ReactNode | null,
images?: Map<string, string>,
): React.ReactNode[] {
return flatMap(tags, (tag) =>
entries
.filter((entry) => entry.tag === tag)
.map((entry) => detailsFunction(entry, gedcom, images)),
)
.filter((element) => element !== null)
.map((element, index) => (
<Item key={index}>
<Item.Content>{element}</Item.Content>
</Item>
));
}
function combineAllMatchingEntriesIntoSingleSection(
entries: GedcomEntry[],
gedcom: GedcomData,
tags: string[],
detailsFunction: (
entries: GedcomEntry[],
gedcom: GedcomData,
images?: Map<string, string>,
) => React.ReactNode | null,
images?: Map<string, string>,
): React.ReactNode {
const entriesWithMatchingTag = flatMap(tags, (tag) =>
entries.filter((entry) => entry.tag === tag),
).filter((element) => element !== null);
const sectionWithDetails = entriesWithMatchingTag.length
? detailsFunction(entriesWithMatchingTag, gedcom, images)
: null;
if (!sectionWithDetails) {
return null;
}
return (
<Item>
<Item.Content>{sectionWithDetails}</Item.Content>
</Item>
);
}
/**
* Returns true if there is displayable information in this entry.
* Returns false if there is no data in this entry or this is only a reference
* to another entry.
*/
function hasData(entry: GedcomEntry) {
return entry.tree.length > 0 || (entry.data && !entry.data.startsWith('@'));
}
function getOtherSections(entries: GedcomEntry[], gedcom: GedcomData) {
return entries
.filter((entry) => !EXCLUDED_TAGS.includes(entry.tag))
.map((entry) => dereference(entry, gedcom, (gedcom) => gedcom.other))
.filter(hasData)
.map((entry) => dataDetails(entry))
.filter((element) => element !== null)
.map((element, index) => (
<Item key={index}>
<Item.Content>{element}</Item.Content>
</Item>
));
}
function getSectionForId(indi: string): React.ReactNode {
return (
<Item>
<Item.Content>
<Header sub>
<FormattedMessage id="config.ids" defaultMessage="Identification" />
</Header>
<div>
<i>{indi}</i>
</div>
</Item.Content>
</Item>
);
}
interface Props {
gedcom: GedcomData;
indi: string;
config: Config;
images?: Map<string, string>;
}
export function Details(props: Props) {
const entries = props.gedcom.indis[props.indi].tree;
return (
<div className="details" data-testid="details">
<Item.Group divided>
{getSectionForEachMatchingEntry(
entries,
props.gedcom,
['NAME'],
nameDetails,
)}
{getSectionForEachMatchingEntry(
entries,
props.gedcom,
['OBJE'],
imageDetails,
props.images,
)}
<ImmediateFamily gedcom={props.gedcom} indi={props.indi} />
<Events
gedcom={props.gedcom}
entries={entries}
indi={props.indi}
images={props.images}
/>
{props.config.id === Ids.SHOW ? getSectionForId(props.indi) : null}
{getSectionForEachMatchingEntry(
entries,
props.gedcom,
['FACT'],
attributeDetails,
)}
{getOtherSections(entries, props.gedcom)}
{getSectionForEachMatchingEntry(
entries,
props.gedcom,
['NOTE'],
noteDetails,
)}
{combineAllMatchingEntriesIntoSingleSection(
entries,
props.gedcom,
['OBJE'],
fileDetails,
props.images,
)}
{combineAllMatchingEntriesIntoSingleSection(
entries,
props.gedcom,
['SOUR'],
sourceDetails,
)}
</Item.Group>
</div>
);
}