mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-08-03 17:41:48 +00:00
WikiTree: Load ancestors from private profiles
This commit is contained in:
95
src/app.tsx
95
src/app.tsx
@@ -4,14 +4,14 @@ import * as React from 'react';
|
|||||||
import {analyticsEvent} from './analytics';
|
import {analyticsEvent} from './analytics';
|
||||||
import {Chart, ChartType} from './chart';
|
import {Chart, ChartType} from './chart';
|
||||||
import {Details} from './details';
|
import {Details} from './details';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage, InjectedIntl} from 'react-intl';
|
||||||
import {getSelection, loadFromUrl, loadGedcom} from './load_data';
|
import {getSelection, loadFromUrl, loadGedcom} from './load_data';
|
||||||
import {getSoftware, TopolaData} from './gedcom_util';
|
import {getSoftware, TopolaData} from './gedcom_util';
|
||||||
import {IndiInfo} from 'topola';
|
import {IndiInfo} from 'topola';
|
||||||
import {intlShape} from 'react-intl';
|
import {intlShape} from 'react-intl';
|
||||||
import {Intro} from './intro';
|
import {Intro} from './intro';
|
||||||
import {Loader, Message, Portal, Responsive} from 'semantic-ui-react';
|
import {Loader, Message, Portal, Responsive} from 'semantic-ui-react';
|
||||||
import {loadWikiTree} from './wikitree';
|
import {loadWikiTree, PRIVATE_ID_PREFIX} from './wikitree';
|
||||||
import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
|
import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
|
||||||
import {TopBar} from './top_bar';
|
import {TopBar} from './top_bar';
|
||||||
|
|
||||||
@@ -132,6 +132,8 @@ class GedcomUrlDataSource implements DataSource {
|
|||||||
|
|
||||||
/** Loading data from the WikiTree API. */
|
/** Loading data from the WikiTree API. */
|
||||||
class WikiTreeDataSource implements DataSource {
|
class WikiTreeDataSource implements DataSource {
|
||||||
|
constructor(private intl: InjectedIntl) {}
|
||||||
|
|
||||||
isNewData(args: Arguments, state: State): boolean {
|
isNewData(args: Arguments, state: State): boolean {
|
||||||
if (state.selection && state.selection.id === args.indi) {
|
if (state.selection && state.selection.id === args.indi) {
|
||||||
// Selection unchanged -> don't reload.
|
// Selection unchanged -> don't reload.
|
||||||
@@ -149,7 +151,7 @@ class WikiTreeDataSource implements DataSource {
|
|||||||
|
|
||||||
async loadData(args: Arguments): Promise<TopolaData> {
|
async loadData(args: Arguments): Promise<TopolaData> {
|
||||||
try {
|
try {
|
||||||
const data = await loadWikiTree(args.indi!, args.authcode);
|
const data = await loadWikiTree(args.indi!, this.intl, args.authcode);
|
||||||
analyticsEvent('wikitree_loaded');
|
analyticsEvent('wikitree_loaded');
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -166,13 +168,6 @@ enum DataSourceEnum {
|
|||||||
WIKITREE,
|
WIKITREE,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mapping from data source identifier to data source handler functions. */
|
|
||||||
const DATA_SOURCES = new Map([
|
|
||||||
[DataSourceEnum.UPLOADED, new UploadedDataSource()],
|
|
||||||
[DataSourceEnum.GEDCOM_URL, new GedcomUrlDataSource()],
|
|
||||||
[DataSourceEnum.WIKITREE, new WikiTreeDataSource()],
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** Arguments passed to the application, primarily through URL parameters. */
|
/** Arguments passed to the application, primarily through URL parameters. */
|
||||||
interface Arguments {
|
interface Arguments {
|
||||||
showSidePanel: boolean;
|
showSidePanel: boolean;
|
||||||
@@ -294,6 +289,13 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Mapping from data source identifier to data source handler functions. */
|
||||||
|
private readonly dataSources = new Map([
|
||||||
|
[DataSourceEnum.UPLOADED, new UploadedDataSource()],
|
||||||
|
[DataSourceEnum.GEDCOM_URL, new GedcomUrlDataSource()],
|
||||||
|
[DataSourceEnum.WIKITREE, new WikiTreeDataSource(this.context.intl)],
|
||||||
|
]);
|
||||||
|
|
||||||
/** Sets the state with a new individual selection and chart type. */
|
/** Sets the state with a new individual selection and chart type. */
|
||||||
private updateDisplay(
|
private updateDisplay(
|
||||||
selection: IndiInfo,
|
selection: IndiInfo,
|
||||||
@@ -380,7 +382,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataSource = DATA_SOURCES.get(args.source!);
|
const dataSource = this.dataSources.get(args.source!);
|
||||||
|
|
||||||
if (!dataSource) {
|
if (!dataSource) {
|
||||||
this.props.history.replace({pathname: '/'});
|
this.props.history.replace({pathname: '/'});
|
||||||
@@ -440,22 +442,42 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
loadingMore: loadMoreFromWikitree || undefined,
|
loadingMore: loadMoreFromWikitree || undefined,
|
||||||
});
|
});
|
||||||
if (loadMoreFromWikitree) {
|
if (loadMoreFromWikitree) {
|
||||||
const data = await loadWikiTree(args.indi!);
|
try {
|
||||||
this.setState(
|
const data = await loadWikiTree(args.indi!, this.context.intl);
|
||||||
Object.assign({}, this.state, {
|
const selection = getSelection(
|
||||||
data,
|
data.chartData,
|
||||||
hash: args.hash,
|
args.indi,
|
||||||
selection: getSelection(data.chartData, args.indi, args.generation),
|
args.generation,
|
||||||
error: undefined,
|
);
|
||||||
loading: false,
|
this.setState(
|
||||||
url: args.url,
|
Object.assign({}, this.state, {
|
||||||
showSidePanel: args.showSidePanel,
|
data,
|
||||||
standalone: args.standalone,
|
hash: args.hash,
|
||||||
chartType: args.chartType,
|
selection,
|
||||||
source: args.source,
|
error: undefined,
|
||||||
loadingMore: false,
|
loading: false,
|
||||||
}),
|
url: args.url,
|
||||||
);
|
showSidePanel: args.showSidePanel,
|
||||||
|
standalone: args.standalone,
|
||||||
|
chartType: args.chartType,
|
||||||
|
source: args.source,
|
||||||
|
loadingMore: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.showErrorPopup(
|
||||||
|
this.context.intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'error.failed_wikitree_load_more',
|
||||||
|
defaultMessage: 'Failed to load data from WikiTree. {error}',
|
||||||
|
},
|
||||||
|
{error},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
loadingMore: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -465,6 +487,10 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
* Updates the browser URL.
|
* Updates the browser URL.
|
||||||
*/
|
*/
|
||||||
private onSelection = (selection: IndiInfo) => {
|
private onSelection = (selection: IndiInfo) => {
|
||||||
|
// Don't allow selecting WikiTree private profiles.
|
||||||
|
if (selection.id.startsWith(PRIVATE_ID_PREFIX)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
analyticsEvent('selection_changed');
|
analyticsEvent('selection_changed');
|
||||||
if (this.state.embedded) {
|
if (this.state.embedded) {
|
||||||
// In embedded mode the URL doesn't change.
|
// In embedded mode the URL doesn't change.
|
||||||
@@ -484,12 +510,17 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
this.chartRef && this.chartRef.print();
|
this.chartRef && this.chartRef.print();
|
||||||
};
|
};
|
||||||
|
|
||||||
private showErrorPopup(message: string) {
|
private showErrorPopup(message: string, otherStateChanges?: Partial<State>) {
|
||||||
this.setState(
|
this.setState(
|
||||||
Object.assign({}, this.state, {
|
Object.assign(
|
||||||
showErrorPopup: true,
|
{},
|
||||||
error: message,
|
this.state,
|
||||||
}),
|
{
|
||||||
|
showErrorPopup: true,
|
||||||
|
error: message,
|
||||||
|
},
|
||||||
|
otherStateChanges,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,5 +60,7 @@
|
|||||||
"error.error": "Błąd",
|
"error.error": "Błąd",
|
||||||
"error.failed_pdf": "Nie udało się utworzyć pliku PDF. Spróbuj jeszcze raz z mniejszym diagramem lub pobierz plik SVG.",
|
"error.failed_pdf": "Nie udało się utworzyć pliku PDF. Spróbuj jeszcze raz z mniejszym diagramem lub pobierz plik SVG.",
|
||||||
"error.failed_png": "Nie udało się utworzyć pliku PNG. Spróbuj jeszcze raz z mniejszym diagramem lub pobierz plik SVG.",
|
"error.failed_png": "Nie udało się utworzyć pliku PNG. Spróbuj jeszcze raz z mniejszym diagramem lub pobierz plik SVG.",
|
||||||
"error.failed_to_load_file": "Błąd wczytywania pliku"
|
"error.failed_to_load_file": "Błąd wczytywania pliku",
|
||||||
|
"error.failed_wikitree_load_more": "Błąd podczas pobierania danych z WikiTree. {error}",
|
||||||
|
"wikitree.private": "Prywatne"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import Cookies from 'js-cookie';
|
|||||||
import {Date, JsonFam, JsonIndi, DateOrRange} from 'topola';
|
import {Date, JsonFam, JsonIndi, DateOrRange} from 'topola';
|
||||||
import {GedcomData, TopolaData, normalizeGedcom} from './gedcom_util';
|
import {GedcomData, TopolaData, normalizeGedcom} from './gedcom_util';
|
||||||
import {GedcomEntry} from 'parse-gedcom';
|
import {GedcomEntry} from 'parse-gedcom';
|
||||||
|
import {InjectedIntl} from 'react-intl';
|
||||||
|
|
||||||
|
/** Prefix for IDs of private individuals. */
|
||||||
|
export const PRIVATE_ID_PREFIX = '~Private';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cookie where the logged in user name is stored. This cookie is shared
|
* Cookie where the logged in user name is stored. This cookie is shared
|
||||||
@@ -109,7 +113,10 @@ async function wikiTreeGet(request: WikiTreeRequest, handleCors: boolean) {
|
|||||||
* Retrieves ancestors from WikiTree for the given person ID.
|
* Retrieves ancestors from WikiTree for the given person ID.
|
||||||
* Uses sessionStorage for caching responses.
|
* Uses sessionStorage for caching responses.
|
||||||
*/
|
*/
|
||||||
async function getAncestors(key: string, handleCors: boolean) {
|
async function getAncestors(
|
||||||
|
key: string,
|
||||||
|
handleCors: boolean,
|
||||||
|
): Promise<Person[]> {
|
||||||
const cacheKey = `wikitree:ancestors:${key}`;
|
const cacheKey = `wikitree:ancestors:${key}`;
|
||||||
const cachedData = getSessionStorageItem(cacheKey);
|
const cachedData = getSessionStorageItem(cacheKey);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
@@ -132,7 +139,10 @@ async function getAncestors(key: string, handleCors: boolean) {
|
|||||||
* Retrieves relatives from WikiTree for the given array of person IDs.
|
* Retrieves relatives from WikiTree for the given array of person IDs.
|
||||||
* Uses sessionStorage for caching responses.
|
* Uses sessionStorage for caching responses.
|
||||||
*/
|
*/
|
||||||
async function getRelatives(keys: string[], handleCors: boolean) {
|
async function getRelatives(
|
||||||
|
keys: string[],
|
||||||
|
handleCors: boolean,
|
||||||
|
): Promise<Person[]> {
|
||||||
const result: Person[] = [];
|
const result: Person[] = [];
|
||||||
const keysToFetch: string[] = [];
|
const keysToFetch: string[] = [];
|
||||||
keys.forEach((key) => {
|
keys.forEach((key) => {
|
||||||
@@ -201,6 +211,7 @@ export function getLoggedInUserName(): string | undefined {
|
|||||||
*/
|
*/
|
||||||
export async function loadWikiTree(
|
export async function loadWikiTree(
|
||||||
key: string,
|
key: string,
|
||||||
|
intl: InjectedIntl,
|
||||||
authcode?: string,
|
authcode?: string,
|
||||||
): Promise<TopolaData> {
|
): Promise<TopolaData> {
|
||||||
// Work around CORS if not in apps.wikitree.com domain.
|
// Work around CORS if not in apps.wikitree.com domain.
|
||||||
@@ -235,8 +246,51 @@ export async function loadWikiTree(
|
|||||||
.map((person) => person.Name)
|
.map((person) => person.Name)
|
||||||
.filter((key) => !!key);
|
.filter((key) => !!key);
|
||||||
const ancestorDetails = await getRelatives(ancestorKeys, handleCors);
|
const ancestorDetails = await getRelatives(ancestorKeys, handleCors);
|
||||||
|
|
||||||
|
// Map from person id to father id if the father profile is private.
|
||||||
|
const privateFathers: Map<number, number> = new Map();
|
||||||
|
// Map from person id to mother id if the mother profile is private.
|
||||||
|
const privateMothers: Map<number, number> = new Map();
|
||||||
|
|
||||||
|
// Andujst private individual ids so that there are no collisions in the case
|
||||||
|
// that ancestors were collected for more than one person.
|
||||||
|
ancestors.forEach((ancestorList, index) => {
|
||||||
|
const offset = 1000 * index;
|
||||||
|
// Adjust ids by offset.
|
||||||
|
ancestorList.forEach((person) => {
|
||||||
|
if (person.Id < 0) {
|
||||||
|
person.Id -= offset;
|
||||||
|
person.Name = `${PRIVATE_ID_PREFIX}${person.Id}`;
|
||||||
|
}
|
||||||
|
if (person.Father < 0) {
|
||||||
|
person.Father -= offset;
|
||||||
|
privateFathers.set(person.Id, person.Father);
|
||||||
|
}
|
||||||
|
if (person.Mother < 0) {
|
||||||
|
person.Mother -= offset;
|
||||||
|
privateMothers.set(person.Id, person.Mother);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the Father and Mother fields again because getRelatives doesn't return
|
||||||
|
// private parents.
|
||||||
|
ancestorDetails.forEach((person) => {
|
||||||
|
const privateFather = privateFathers.get(person.Id);
|
||||||
|
if (privateFather) {
|
||||||
|
person.Father = privateFather;
|
||||||
|
}
|
||||||
|
const privateMother = privateMothers.get(person.Id);
|
||||||
|
if (privateMother) {
|
||||||
|
person.Mother = privateMother;
|
||||||
|
}
|
||||||
|
});
|
||||||
everyone.push(...ancestorDetails);
|
everyone.push(...ancestorDetails);
|
||||||
|
|
||||||
|
// Collect private individuals.
|
||||||
|
const privateAncestors = ancestors.flat().filter((person) => person.Id < 0);
|
||||||
|
everyone.push(...privateAncestors);
|
||||||
|
|
||||||
// Limit the number of generations of descendants because there may be tens of
|
// Limit the number of generations of descendants because there may be tens of
|
||||||
// generations for some profiles.
|
// generations for some profiles.
|
||||||
const descendantGenerationLimit = 5;
|
const descendantGenerationLimit = 5;
|
||||||
@@ -291,7 +345,7 @@ export async function loadWikiTree(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
converted.add(person.Id);
|
converted.add(person.Id);
|
||||||
const indi = convertPerson(person);
|
const indi = convertPerson(person, intl);
|
||||||
if (person.Spouses) {
|
if (person.Spouses) {
|
||||||
Object.values(person.Spouses).forEach((spouse) => {
|
Object.values(person.Spouses).forEach((spouse) => {
|
||||||
const famId = getFamilyId(person.Id, spouse.Id);
|
const famId = getFamilyId(person.Id, spouse.Id);
|
||||||
@@ -350,11 +404,18 @@ function getFamilyId(spouse1: number, spouse2: number) {
|
|||||||
return `${spouse2}_${spouse1}`;
|
return `${spouse2}_${spouse1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertPerson(person: Person): JsonIndi {
|
function convertPerson(person: Person, intl: InjectedIntl): JsonIndi {
|
||||||
const indi: JsonIndi = {
|
const indi: JsonIndi = {
|
||||||
id: person.Name,
|
id: person.Name,
|
||||||
};
|
};
|
||||||
if (person.FirstName !== 'Unknown') {
|
if (person.Name.startsWith(PRIVATE_ID_PREFIX)) {
|
||||||
|
indi.hideId = true;
|
||||||
|
indi.firstName = intl.formatMessage({
|
||||||
|
id: 'wikitree.private',
|
||||||
|
defaultMessage: 'Private',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (person.FirstName && person.FirstName !== 'Unknown') {
|
||||||
indi.firstName = person.FirstName;
|
indi.firstName = person.FirstName;
|
||||||
}
|
}
|
||||||
if (person.LastNameAtBirth !== 'Unknown') {
|
if (person.LastNameAtBirth !== 'Unknown') {
|
||||||
@@ -450,15 +511,17 @@ function buildGedcom(indis: JsonIndi[]): GedcomData {
|
|||||||
data: `${indi.firstName || ''} /${indi.lastName || ''}/`,
|
data: `${indi.firstName || ''} /${indi.lastName || ''}/`,
|
||||||
tree: [],
|
tree: [],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
level: 1,
|
|
||||||
pointer: '',
|
|
||||||
tag: 'WWW',
|
|
||||||
data: `https://www.wikitree.com/wiki/${escapedId}`,
|
|
||||||
tree: [],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
if (!indi.id.startsWith('~')) {
|
||||||
|
gedcomIndis[indi.id].tree.push({
|
||||||
|
level: 1,
|
||||||
|
pointer: '',
|
||||||
|
tag: 'WWW',
|
||||||
|
data: `https://www.wikitree.com/wiki/${escapedId}`,
|
||||||
|
tree: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user