mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-03-15 20:13:45 +00:00
Refactoring: Move data loading code under the datasource directory
This commit is contained in:
170
src/app.tsx
170
src/app.tsx
@@ -3,17 +3,30 @@ import * as queryString from 'query-string';
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {analyticsEvent} from './util/analytics';
|
import {analyticsEvent} from './util/analytics';
|
||||||
import {Chart, ChartType} from './chart';
|
import {Chart, ChartType} from './chart';
|
||||||
|
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
|
||||||
import {Details} from './details';
|
import {Details} from './details';
|
||||||
import {FormattedMessage, InjectedIntl} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import {getSelection, loadFromUrl, loadGedcom} from './datasource/load_data';
|
|
||||||
import {getSoftware, TopolaData} from './util/gedcom_util';
|
import {getSoftware, TopolaData} from './util/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, PRIVATE_ID_PREFIX} from './datasource/wikitree';
|
|
||||||
import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
|
import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
|
||||||
import {TopBar} from './menu/top_bar';
|
import {TopBar} from './menu/top_bar';
|
||||||
|
import {
|
||||||
|
getSelection,
|
||||||
|
loadGedcom,
|
||||||
|
UploadSourceSpec,
|
||||||
|
UrlSourceSpec,
|
||||||
|
GedcomUrlDataSource,
|
||||||
|
UploadedDataSource,
|
||||||
|
} from './datasource/load_data';
|
||||||
|
import {
|
||||||
|
loadWikiTree,
|
||||||
|
PRIVATE_ID_PREFIX,
|
||||||
|
WikiTreeDataSource,
|
||||||
|
WikiTreeSourceSpec,
|
||||||
|
} from './datasource/wikitree';
|
||||||
|
|
||||||
/** Shows an error message in the middle of the screen. */
|
/** Shows an error message in the middle of the screen. */
|
||||||
function ErrorMessage(props: {message?: string}) {
|
function ErrorMessage(props: {message?: string}) {
|
||||||
@@ -83,159 +96,8 @@ interface GedcomMessage extends EmbeddedMessage {
|
|||||||
gedcom?: string;
|
gedcom?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Supported data sources. */
|
|
||||||
enum DataSourceEnum {
|
|
||||||
UPLOADED,
|
|
||||||
GEDCOM_URL,
|
|
||||||
WIKITREE,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UploadSourceSpec {
|
|
||||||
source: DataSourceEnum.UPLOADED;
|
|
||||||
gedcom?: string;
|
|
||||||
/** Hash of the GEDCOM contents. */
|
|
||||||
hash: string;
|
|
||||||
images?: Map<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UrlSourceSpec {
|
|
||||||
source: DataSourceEnum.GEDCOM_URL;
|
|
||||||
/** URL of the data that is loaded or is being loaded. */
|
|
||||||
url: string;
|
|
||||||
handleCors: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WikiTreeSourceSpec {
|
|
||||||
source: DataSourceEnum.WIKITREE;
|
|
||||||
authcode?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type DataSourceSpec = UrlSourceSpec | UploadSourceSpec | WikiTreeSourceSpec;
|
type DataSourceSpec = UrlSourceSpec | UploadSourceSpec | WikiTreeSourceSpec;
|
||||||
|
|
||||||
/** Source specification together with individual selection. */
|
|
||||||
interface SourceSelection<SourceSpecT> {
|
|
||||||
spec: SourceSpecT;
|
|
||||||
selection?: IndiInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Interface encapsulating functions specific for a data source. */
|
|
||||||
interface DataSource<SourceSpecT> {
|
|
||||||
/**
|
|
||||||
* Returns true if the application is now loading a completely new data set
|
|
||||||
* and the existing one should be wiped.
|
|
||||||
*/
|
|
||||||
isNewData(
|
|
||||||
newSource: SourceSelection<SourceSpecT>,
|
|
||||||
oldSource: SourceSelection<SourceSpecT>,
|
|
||||||
data?: TopolaData,
|
|
||||||
): boolean;
|
|
||||||
/** Loads data from the data source. */
|
|
||||||
loadData(spec: SourceSelection<SourceSpecT>): Promise<TopolaData>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Files opened from the local computer. */
|
|
||||||
class UploadedDataSource implements DataSource<UploadSourceSpec> {
|
|
||||||
// isNewData(args: Arguments, state: State): boolean {
|
|
||||||
isNewData(
|
|
||||||
newSource: SourceSelection<UploadSourceSpec>,
|
|
||||||
oldSource: SourceSelection<UploadSourceSpec>,
|
|
||||||
data?: TopolaData,
|
|
||||||
): boolean {
|
|
||||||
return newSource.spec.hash !== oldSource.spec.hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadData(
|
|
||||||
source: SourceSelection<UploadSourceSpec>,
|
|
||||||
): Promise<TopolaData> {
|
|
||||||
try {
|
|
||||||
const data = await loadGedcom(
|
|
||||||
source.spec.hash,
|
|
||||||
source.spec.gedcom,
|
|
||||||
source.spec.images,
|
|
||||||
);
|
|
||||||
const software = getSoftware(data.gedcom.head);
|
|
||||||
analyticsEvent('upload_file_loaded', {
|
|
||||||
event_label: software,
|
|
||||||
event_value: (source.spec.images && source.spec.images.size) || 0,
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
analyticsEvent('upload_file_error');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GEDCOM file loaded by pointing to a URL. */
|
|
||||||
class GedcomUrlDataSource implements DataSource<UrlSourceSpec> {
|
|
||||||
isNewData(
|
|
||||||
newSource: SourceSelection<UrlSourceSpec>,
|
|
||||||
oldSource: SourceSelection<UrlSourceSpec>,
|
|
||||||
data?: TopolaData,
|
|
||||||
): boolean {
|
|
||||||
return newSource.spec.url !== oldSource.spec.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadData(source: SourceSelection<UrlSourceSpec>): Promise<TopolaData> {
|
|
||||||
try {
|
|
||||||
const data = await loadFromUrl(source.spec.url, source.spec.handleCors);
|
|
||||||
const software = getSoftware(data.gedcom.head);
|
|
||||||
analyticsEvent('upload_file_loaded', {event_label: software});
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
analyticsEvent('url_file_error');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Loading data from the WikiTree API. */
|
|
||||||
class WikiTreeDataSource implements DataSource<WikiTreeSourceSpec> {
|
|
||||||
constructor(private intl: InjectedIntl) {}
|
|
||||||
|
|
||||||
isNewData(
|
|
||||||
newSource: SourceSelection<WikiTreeSourceSpec>,
|
|
||||||
oldSource: SourceSelection<WikiTreeSourceSpec>,
|
|
||||||
data?: TopolaData,
|
|
||||||
): boolean {
|
|
||||||
if (!newSource.selection) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (oldSource.selection?.id === newSource.selection.id) {
|
|
||||||
// Selection unchanged -> don't reload.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
data &&
|
|
||||||
data.chartData.indis.some((indi) => indi.id === newSource.selection?.id)
|
|
||||||
) {
|
|
||||||
// New selection exists in current view -> animate instead of reloading.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadData(
|
|
||||||
source: SourceSelection<WikiTreeSourceSpec>,
|
|
||||||
): Promise<TopolaData> {
|
|
||||||
if (!source.selection) {
|
|
||||||
throw new Error('WikiTree id needs to be provided');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const data = await loadWikiTree(
|
|
||||||
source.selection.id,
|
|
||||||
this.intl,
|
|
||||||
source.spec.authcode,
|
|
||||||
);
|
|
||||||
analyticsEvent('wikitree_loaded');
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
analyticsEvent('wikitree_error');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Arguments passed to the application, primarily through URL parameters. */
|
/** Arguments passed to the application, primarily through URL parameters. */
|
||||||
interface Arguments {
|
interface Arguments {
|
||||||
sourceSpec?: DataSourceSpec;
|
sourceSpec?: DataSourceSpec;
|
||||||
|
|||||||
30
src/datasource/data_source.ts
Normal file
30
src/datasource/data_source.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import {IndiInfo} from 'topola';
|
||||||
|
import {TopolaData} from '../util/gedcom_util';
|
||||||
|
|
||||||
|
/** Supported data sources. */
|
||||||
|
export enum DataSourceEnum {
|
||||||
|
UPLOADED,
|
||||||
|
GEDCOM_URL,
|
||||||
|
WIKITREE,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Source specification together with individual selection. */
|
||||||
|
export interface SourceSelection<SourceSpecT> {
|
||||||
|
spec: SourceSpecT;
|
||||||
|
selection?: IndiInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Interface encapsulating functions specific for a data source. */
|
||||||
|
export interface DataSource<SourceSpecT> {
|
||||||
|
/**
|
||||||
|
* Returns true if the application is now loading a completely new data set
|
||||||
|
* and the existing one should be wiped.
|
||||||
|
*/
|
||||||
|
isNewData(
|
||||||
|
newSource: SourceSelection<SourceSpecT>,
|
||||||
|
oldSource: SourceSelection<SourceSpecT>,
|
||||||
|
data?: TopolaData,
|
||||||
|
): boolean;
|
||||||
|
/** Loads data from the data source. */
|
||||||
|
loadData(spec: SourceSelection<SourceSpecT>): Promise<TopolaData>;
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import {convertGedcom, TopolaData} from '../util/gedcom_util';
|
import {analyticsEvent} from '../util/analytics';
|
||||||
|
import {convertGedcom, getSoftware, TopolaData} from '../util/gedcom_util';
|
||||||
|
import {DataSource, DataSourceEnum, SourceSelection} from './data_source';
|
||||||
import {IndiInfo, JsonGedcomData} from 'topola';
|
import {IndiInfo, JsonGedcomData} from 'topola';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,3 +93,74 @@ export async function loadGedcom(
|
|||||||
}
|
}
|
||||||
return prepareData(gedcom, hash, images);
|
return prepareData(gedcom, hash, images);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UploadSourceSpec {
|
||||||
|
source: DataSourceEnum.UPLOADED;
|
||||||
|
gedcom?: string;
|
||||||
|
/** Hash of the GEDCOM contents. */
|
||||||
|
hash: string;
|
||||||
|
images?: Map<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Files opened from the local computer. */
|
||||||
|
export class UploadedDataSource implements DataSource<UploadSourceSpec> {
|
||||||
|
// isNewData(args: Arguments, state: State): boolean {
|
||||||
|
isNewData(
|
||||||
|
newSource: SourceSelection<UploadSourceSpec>,
|
||||||
|
oldSource: SourceSelection<UploadSourceSpec>,
|
||||||
|
data?: TopolaData,
|
||||||
|
): boolean {
|
||||||
|
return newSource.spec.hash !== oldSource.spec.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadData(
|
||||||
|
source: SourceSelection<UploadSourceSpec>,
|
||||||
|
): Promise<TopolaData> {
|
||||||
|
try {
|
||||||
|
const data = await loadGedcom(
|
||||||
|
source.spec.hash,
|
||||||
|
source.spec.gedcom,
|
||||||
|
source.spec.images,
|
||||||
|
);
|
||||||
|
const software = getSoftware(data.gedcom.head);
|
||||||
|
analyticsEvent('upload_file_loaded', {
|
||||||
|
event_label: software,
|
||||||
|
event_value: (source.spec.images && source.spec.images.size) || 0,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
analyticsEvent('upload_file_error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UrlSourceSpec {
|
||||||
|
source: DataSourceEnum.GEDCOM_URL;
|
||||||
|
/** URL of the data that is loaded or is being loaded. */
|
||||||
|
url: string;
|
||||||
|
handleCors: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GEDCOM file loaded by pointing to a URL. */
|
||||||
|
export class GedcomUrlDataSource implements DataSource<UrlSourceSpec> {
|
||||||
|
isNewData(
|
||||||
|
newSource: SourceSelection<UrlSourceSpec>,
|
||||||
|
oldSource: SourceSelection<UrlSourceSpec>,
|
||||||
|
data?: TopolaData,
|
||||||
|
): boolean {
|
||||||
|
return newSource.spec.url !== oldSource.spec.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadData(source: SourceSelection<UrlSourceSpec>): Promise<TopolaData> {
|
||||||
|
try {
|
||||||
|
const data = await loadFromUrl(source.spec.url, source.spec.handleCors);
|
||||||
|
const software = getSoftware(data.gedcom.head);
|
||||||
|
analyticsEvent('upload_file_loaded', {event_label: software});
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
analyticsEvent('url_file_error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
|
import {analyticsEvent} from '../util/analytics';
|
||||||
|
import {DataSource, DataSourceEnum, SourceSelection} from './data_source';
|
||||||
import {Date, DateOrRange, JsonFam, JsonIndi} from 'topola';
|
import {Date, DateOrRange, JsonFam, JsonIndi} from 'topola';
|
||||||
import {GedcomData, normalizeGedcom, TopolaData} from '../util/gedcom_util';
|
import {GedcomData, normalizeGedcom, TopolaData} from '../util/gedcom_util';
|
||||||
import {GedcomEntry} from 'parse-gedcom';
|
import {GedcomEntry} from 'parse-gedcom';
|
||||||
@@ -559,3 +561,55 @@ function getSet<K, V>(map: Map<K, Set<V>>, key: K): Set<V> {
|
|||||||
map.set(key, newSet);
|
map.set(key, newSet);
|
||||||
return newSet;
|
return newSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WikiTreeSourceSpec {
|
||||||
|
source: DataSourceEnum.WIKITREE;
|
||||||
|
authcode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Loading data from the WikiTree API. */
|
||||||
|
export class WikiTreeDataSource implements DataSource<WikiTreeSourceSpec> {
|
||||||
|
constructor(private intl: InjectedIntl) {}
|
||||||
|
|
||||||
|
isNewData(
|
||||||
|
newSource: SourceSelection<WikiTreeSourceSpec>,
|
||||||
|
oldSource: SourceSelection<WikiTreeSourceSpec>,
|
||||||
|
data?: TopolaData,
|
||||||
|
): boolean {
|
||||||
|
if (!newSource.selection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (oldSource.selection?.id === newSource.selection.id) {
|
||||||
|
// Selection unchanged -> don't reload.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
data.chartData.indis.some((indi) => indi.id === newSource.selection?.id)
|
||||||
|
) {
|
||||||
|
// New selection exists in current view -> animate instead of reloading.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadData(
|
||||||
|
source: SourceSelection<WikiTreeSourceSpec>,
|
||||||
|
): Promise<TopolaData> {
|
||||||
|
if (!source.selection) {
|
||||||
|
throw new Error('WikiTree id needs to be provided');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await loadWikiTree(
|
||||||
|
source.selection.id,
|
||||||
|
this.intl,
|
||||||
|
source.spec.authcode,
|
||||||
|
);
|
||||||
|
analyticsEvent('wikitree_loaded');
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
analyticsEvent('wikitree_error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user