Use wikitree-js library for WikiTree API

This commit is contained in:
Przemek Więch
2022-08-17 11:18:17 +02:00
parent 7c6b1dd390
commit a92f06e43d
4 changed files with 1033 additions and 473 deletions

1318
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,8 @@
"semantic-ui-css": "^2.4.1", "semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^2.0.3", "semantic-ui-react": "^2.0.3",
"topola": "^3.5.0", "topola": "^3.5.0",
"unified": "^10.1.0" "unified": "^10.1.0",
"wikitree-js": "^0.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/array.prototype.flatmap": "^1.2.2", "@types/array.prototype.flatmap": "^1.2.2",
@@ -71,7 +72,7 @@
"build:default": "REACT_APP_CHANGELOG=`cat CHANGELOG.md` REACT_APP_GIT_SHA=`git rev-parse --short HEAD` REACT_APP_GIT_TIME=`git log -1 --format=%ci` react-scripts build", "build:default": "REACT_APP_CHANGELOG=`cat CHANGELOG.md` REACT_APP_GIT_SHA=`git rev-parse --short HEAD` REACT_APP_GIT_TIME=`git log -1 --format=%ci` react-scripts build",
"build:windows": "react-scripts build", "build:windows": "react-scripts build",
"test": "react-scripts test --env=jsdom", "test": "react-scripts test --env=jsdom",
"prettier": "prettier --write src/**/*.{ts,tsx,json} && prettier --write src/*.{ts,tsx,json}", "prettier": "prettier --write src/**/*.{ts,tsx,json} --end-of-line lf && prettier --write src/*.{ts,tsx,json} --end-of-line lf",
"predeploy": "npm run build", "predeploy": "npm run build",
"deploy": "gh-pages -d build", "deploy": "gh-pages -d build",
"predeploy-wikitree": "npm run build", "predeploy-wikitree": "npm run build",

View File

@@ -1,4 +1,3 @@
import Cookies from 'js-cookie';
import {analyticsEvent} from '../util/analytics'; import {analyticsEvent} from '../util/analytics';
import {DataSource, DataSourceEnum, SourceSelection} from './data_source'; import {DataSource, DataSourceEnum, SourceSelection} from './data_source';
import { import {
@@ -15,79 +14,17 @@ import {GedcomEntry} from 'parse-gedcom';
import {IntlShape} from 'react-intl'; import {IntlShape} from 'react-intl';
import {TopolaError} from '../util/error'; import {TopolaError} from '../util/error';
import {isValidDateOrRange} from '../util/date_util'; import {isValidDateOrRange} from '../util/date_util';
import {
getAncestors as getAncestorsApi,
getRelatives as getRelativesApi,
clientLogin,
getLoggedInUserName,
Person
} from 'wikitree-js';
/** Prefix for IDs of private individuals. */ /** Prefix for IDs of private individuals. */
export const PRIVATE_ID_PREFIX = '~Private'; export const PRIVATE_ID_PREFIX = '~Private';
/**
* Cookie where the logged in user name is stored. This cookie is shared
* between apps hosted on apps.wikitree.com.
*/
const USER_NAME_COOKIE = 'wikidb_wtb_UserName';
/** WikiTree API getAncestors request. */
interface GetAncestorsRequest {
action: 'getAncestors';
key: string;
fields: string;
}
/** WikiTree API getRelatives request. */
interface GetRelativesRequest {
action: 'getRelatives';
keys: string;
getChildren?: true;
getSpouses?: true;
}
/** WikiTree API clientLogin request. */
interface ClientLoginRequest {
action: 'clientLogin';
authcode: string;
}
/** WikiTree API clientLogin response. */
interface ClientLoginResponse {
result: string;
username: string;
}
type WikiTreeRequest =
| GetAncestorsRequest
| GetRelativesRequest
| ClientLoginRequest;
/** Person structure returned from WikiTree API. */
interface Person {
Id: number;
Name: string;
FirstName: string;
LastNameAtBirth: string;
RealName: string;
Spouses?: {[key: number]: Person};
Children: {[key: number]: Person};
Mother: number;
Father: number;
Gender: string;
BirthDate: string;
DeathDate: string;
BirthLocation: string;
DeathLocation: string;
BirthDateDecade: string;
DeathDateDecade: string;
marriage_location: string;
marriage_date: string;
DataStatus?: {
BirthDate: string;
DeathDate: string;
};
Photo: string;
PhotoData?: {
path: string;
url: string;
};
}
/** Gets item from session storage. Logs exception if one is thrown. */ /** Gets item from session storage. Logs exception if one is thrown. */
function getSessionStorageItem(key: string): string | null { function getSessionStorageItem(key: string): string | null {
try { try {
@@ -107,23 +44,13 @@ function setSessionStorageItem(key: string, value: string) {
} }
} }
/** Sends a request to the WikiTree API. Returns the parsed response JSON. */ function getApiOptions(handleCors: boolean) {
async function wikiTreeGet(request: WikiTreeRequest, handleCors: boolean) { return handleCors
const requestData = new FormData(); ? {
requestData.append('format', 'json'); apiUrl:
for (const key in request) { 'https://topola-cors.herokuapp.com/https://api.wikitree.com/api.php',
requestData.append(key, request[key]); }
} : {};
const apiUrl = handleCors
? 'https://topola-cors.herokuapp.com/https://api.wikitree.com/api.php'
: 'https://api.wikitree.com/api.php';
const response = await window.fetch(apiUrl, {
method: 'POST',
body: requestData,
credentials: handleCors ? undefined : 'include',
});
const responseBody = await response.text();
return JSON.parse(responseBody);
} }
/** /**
@@ -139,15 +66,7 @@ async function getAncestors(
if (cachedData) { if (cachedData) {
return JSON.parse(cachedData); return JSON.parse(cachedData);
} }
const response = await wikiTreeGet( const result = getAncestorsApi(key, {}, getApiOptions(handleCors));
{
action: 'getAncestors',
key: key,
fields: '*',
},
handleCors,
);
const result = response[0].ancestors as Person[];
setSessionStorageItem(cacheKey, JSON.stringify(result)); setSessionStorageItem(cacheKey, JSON.stringify(result));
return result; return result;
} }
@@ -173,16 +92,12 @@ async function getRelatives(
if (keysToFetch.length === 0) { if (keysToFetch.length === 0) {
return result; return result;
} }
const response = await wikiTreeGet( const response = await getRelativesApi(
{ keysToFetch,
action: 'getRelatives', {getChildren: true, getSpouses: true},
keys: keysToFetch.join(','), getApiOptions(handleCors),
getChildren: true,
getSpouses: true,
},
handleCors,
); );
if (response[0].items === null) { if (response === []) {
const id = keysToFetch[0]; const id = keysToFetch[0];
throw new TopolaError( throw new TopolaError(
'WIKITREE_PROFILE_NOT_FOUND', 'WIKITREE_PROFILE_NOT_FOUND',
@@ -190,41 +105,13 @@ async function getRelatives(
{id}, {id},
); );
} }
const fetchedResults = response[0].items.map( response.forEach((person) => {
(x: {person: Person}) => x.person,
) as Person[];
fetchedResults.forEach((person) => {
setSessionStorageItem( setSessionStorageItem(
`wikitree:relatives:${person.Name}`, `wikitree:relatives:${person.Name}`,
JSON.stringify(person), JSON.stringify(person),
); );
}); });
return result.concat(fetchedResults); return result.concat(response);
}
export async function clientLogin(
authcode: string,
): Promise<ClientLoginResponse> {
const response = await wikiTreeGet(
{
action: 'clientLogin',
authcode,
},
false,
);
return response.clientLogin;
}
/**
* Returns the logged in user name or undefined if not logged in.
*
* This is not an authoritative answer. The result of this function relies on
* the cookies set on the apps.wikitree.com domain under which this application
* is hosted. The authoritative source of login information is in cookies set on
* the api.wikitree.com domain.
*/
export function getLoggedInUserName(): string | undefined {
return Cookies.get(USER_NAME_COOKIE);
} }
/** /**
@@ -243,7 +130,6 @@ export async function loadWikiTree(
const loginResult = await clientLogin(authcode); const loginResult = await clientLogin(authcode);
if (loginResult.result === 'Success') { if (loginResult.result === 'Success') {
sessionStorage.clear(); sessionStorage.clear();
Cookies.set(USER_NAME_COOKIE, loginResult.username);
} }
} }
@@ -334,7 +220,7 @@ export async function loadWikiTree(
everyone.push(...allSpouses); everyone.push(...allSpouses);
// Fetch all children. // Fetch all children.
toFetch = people.flatMap((person) => toFetch = people.flatMap((person) =>
Object.values(person.Children).map((c) => c.Name), Object.values(person.Children || {}).map((c) => c.Name),
); );
generation++; generation++;
} }
@@ -474,7 +360,7 @@ function convertPerson(person: Person, intl: IntlShape): JsonIndi {
) { ) {
const parsedDate = parseDate( const parsedDate = parseDate(
person.BirthDate, person.BirthDate,
person.DataStatus && person.DataStatus.BirthDate, (person.DataStatus && person.DataStatus.BirthDate) || undefined,
); );
const date = parsedDate || parseDecade(person.BirthDateDecade); const date = parsedDate || parseDecade(person.BirthDateDecade);
indi.birth = Object.assign({}, date, {place: person.BirthLocation}); indi.birth = Object.assign({}, date, {place: person.BirthLocation});
@@ -486,7 +372,7 @@ function convertPerson(person: Person, intl: IntlShape): JsonIndi {
) { ) {
const parsedDate = parseDate( const parsedDate = parseDate(
person.DeathDate, person.DeathDate,
person.DataStatus && person.DataStatus.DeathDate, (person.DataStatus && person.DataStatus.DeathDate) || undefined,
); );
const date = parsedDate || parseDecade(person.DeathDateDecade); const date = parsedDate || parseDecade(person.DeathDateDecade);
indi.death = Object.assign({}, date, {place: person.DeathLocation}); indi.death = Object.assign({}, date, {place: person.DeathLocation});

View File

@@ -3,10 +3,13 @@ import wikitreeLogo from './wikitree.png';
import {analyticsEvent} from '../util/analytics'; import {analyticsEvent} from '../util/analytics';
import {Button, Form, Header, Input, Modal} from 'semantic-ui-react'; import {Button, Form, Header, Input, Modal} from 'semantic-ui-react';
import {FormattedMessage, useIntl} from 'react-intl'; import {FormattedMessage, useIntl} from 'react-intl';
import {getLoggedInUserName} from '../datasource/wikitree';
import {MenuItem, MenuType} from './menu_item'; import {MenuItem, MenuType} from './menu_item';
import {useEffect, useRef, useState} from 'react'; import {useEffect, useRef, useState} from 'react';
import {useHistory, useLocation} from 'react-router'; import {useHistory, useLocation} from 'react-router';
import {
getLoggedInUserName,
navigateToLoginPage,
} from 'wikitree-js';
interface Props { interface Props {
menuType: MenuType; menuType: MenuType;
@@ -150,8 +153,6 @@ export function WikiTreeMenu(props: Props) {
/** Displays and handles the "Log in to WikiTree" menu. */ /** Displays and handles the "Log in to WikiTree" menu. */
export function WikiTreeLoginMenu(props: Props) { export function WikiTreeLoginMenu(props: Props) {
const formRef = useRef<HTMLFormElement>(null);
const returnUrlRef = useRef<HTMLInputElement>(null);
const intl = useIntl(); const intl = useIntl();
/** /**
@@ -163,8 +164,7 @@ export function WikiTreeLoginMenu(props: Props) {
'https://apps.wikitree.com/apps/wiech13/topola-viewer'; 'https://apps.wikitree.com/apps/wiech13/topola-viewer';
// TODO: remove authcode if it is in the current URL. // TODO: remove authcode if it is in the current URL.
const returnUrl = `${wikiTreeTopolaUrl}${window.location.hash}`; const returnUrl = `${wikiTreeTopolaUrl}${window.location.hash}`;
returnUrlRef.current!.value = returnUrl; navigateToLoginPage(returnUrl);
formRef.current!.submit();
} }
const username = getLoggedInUserName(); const username = getLoggedInUserName();
@@ -178,15 +178,6 @@ export function WikiTreeLoginMenu(props: Props) {
defaultMessage="Log in to WikiTree" defaultMessage="Log in to WikiTree"
/> />
</MenuItem> </MenuItem>
<form
action="https://api.wikitree.com/api.php"
method="POST"
style={{display: 'hidden'}}
ref={formRef}
>
<input type="hidden" name="action" value="clientLogin" />
<input type="hidden" name="returnURL" ref={returnUrlRef} />
</form>
</> </>
); );
} }