Refactoring: Move data loading code under the datasource directory

This commit is contained in:
Przemek Wiech 2020-05-12 21:46:30 +02:00
parent 3eb48ce665
commit 6ef4dbc858
4 changed files with 174 additions and 155 deletions

View File

@ -3,17 +3,30 @@ import * as queryString from 'query-string';
import * as React from 'react';
import {analyticsEvent} from './util/analytics';
import {Chart, ChartType} from './chart';
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
import {Details} from './details';
import {FormattedMessage, InjectedIntl} from 'react-intl';
import {getSelection, loadFromUrl, loadGedcom} from './datasource/load_data';
import {FormattedMessage} from 'react-intl';
import {getSoftware, TopolaData} from './util/gedcom_util';
import {IndiInfo} from 'topola';
import {intlShape} from 'react-intl';
import {Intro} from './intro';
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 {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. */
function ErrorMessage(props: {message?: string}) {
@ -83,159 +96,8 @@ interface GedcomMessage extends EmbeddedMessage {
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;
/** 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. */
interface Arguments {
sourceSpec?: DataSourceSpec;

View 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>;
}

View File

@ -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';
/**
@ -91,3 +93,74 @@ export async function loadGedcom(
}
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;
}
}
}

View File

@ -1,4 +1,6 @@
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 {GedcomData, normalizeGedcom, TopolaData} from '../util/gedcom_util';
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);
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;
}
}
}