From 768e6d7111e104ea665999e8d7219ae198a712c9 Mon Sep 17 00:00:00 2001 From: Przemek Wiech Date: Thu, 5 Mar 2020 21:50:54 +0100 Subject: [PATCH] Refactored app.tsx to split the componentDidUpdate function --- src/app.tsx | 312 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 205 insertions(+), 107 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index dd0c558..45805ac 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,3 +1,4 @@ +import * as H from 'history'; import * as queryString from 'query-string'; import * as React from 'react'; import {analyticsEvent} from './analytics'; @@ -10,9 +11,9 @@ import {IndiInfo} from 'topola'; import {intlShape} from 'react-intl'; import {Intro} from './intro'; import {Loader, Message, Portal, Responsive} from 'semantic-ui-react'; +import {loadWikiTree} from './wikitree'; import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom'; import {TopBar} from './top_bar'; -import {loadWikiTree} from './wikitree'; /** Shows an error message in the middle of the screen. */ function ErrorMessage(props: {message?: string}) { @@ -74,6 +75,158 @@ interface GedcomMessage extends EmbeddedMessage { gedcom?: string; } +/** Interface encapsulating functions specific for a data source. */ +interface DataSource { + /** + * Returns true if the application is now loading a completely new data set + * and the existing one should be wiped. + */ + isNewData(args: Arguments, state: State): boolean; + /** Loads data from the data source. */ + loadData(args: Arguments): Promise; +} + +/** Files opened from the local computer. */ +class UploadedDataSource implements DataSource { + isNewData(args: Arguments, state: State): boolean { + return ( + args.hash !== state.hash || + !!(args.gedcom && !state.loading && !state.data) + ); + } + + async loadData(args: Arguments): Promise { + try { + const data = await loadGedcom(args.hash!, args.gedcom, args.images); + const software = getSoftware(data.gedcom.head); + analyticsEvent('upload_file_loaded', { + event_label: software, + event_value: (args.images && args.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 { + isNewData(args: Arguments, state: State): boolean { + return args.url !== state.url; + } + + async loadData(args: Arguments): Promise { + try { + const data = await loadFromUrl(args.url!, args.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 { + isNewData(args: Arguments, state: State): boolean { + // WikiTree is always a single data source. + return false; + } + + async loadData(args: Arguments): Promise { + try { + const data = await loadWikiTree(args.indi!, args.authcode); + analyticsEvent('wikitree_loaded'); + return data; + } catch (error) { + analyticsEvent('wikitree_error'); + throw error; + } + } +} + +/** Supported data sources. */ +enum DataSourceEnum { + UPLOADED, + GEDCOM_URL, + 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. */ +interface Arguments { + showSidePanel: boolean; + embedded: boolean; + url?: string; + indi?: string; + generation?: number; + hash?: string; + handleCors: boolean; + standalone: boolean; + source?: DataSourceEnum; + authcode?: string; + chartType: ChartType; + gedcom?: string; + images?: Map; +} + +/** + * Retrieve arguments passed into the application through the URL and uploaded + * data. + */ +function getArguments(location: H.Location): Arguments { + const search = queryString.parse(location.search); + const getParam = (name: string) => { + const value = search[name]; + return typeof value === 'string' ? value : undefined; + }; + + const parsedGen = Number(getParam('gen')); + const view = getParam('view'); + const chartTypes = new Map([ + ['relatives', ChartType.Relatives], + ['fancy', ChartType.Fancy], + ]); + const hash = getParam('file'); + const url = getParam('url'); + const source = + getParam('source') === 'wikitree' + ? DataSourceEnum.WIKITREE + : hash + ? DataSourceEnum.UPLOADED + : url + ? DataSourceEnum.GEDCOM_URL + : undefined; + return { + showSidePanel: getParam('sidePanel') !== 'false', // True by default. + embedded: getParam('embedded') === 'true', // False by default. + url, + indi: getParam('indi'), + generation: !isNaN(parsedGen) ? parsedGen : undefined, + hash, + handleCors: getParam('handleCors') !== 'false', // True by default. + standalone: getParam('standalone') !== 'false', // True by default. + source, + authcode: getParam('?authcode'), + + // Hourglass is the default view. + chartType: chartTypes.get(view) || ChartType.Hourglass, + + gedcom: location.state && location.state.data, + images: location.state && location.state.images, + }; +} + /** Returs true if the changes object has values that are different than those in state. */ function hasUpdatedValues(state: T, changes: Partial | undefined) { if (!changes) { @@ -107,8 +260,8 @@ interface State { chartType: ChartType; /** Whether to show the error popup. */ showErrorPopup: boolean; - /** True if data is loaded from WikiTree. */ - wikiTreeSource: boolean; + /** Source of the data. */ + source?: DataSourceEnum; loadingMore?: boolean; } @@ -119,7 +272,6 @@ export class App extends React.Component { standalone: true, chartType: ChartType.Hourglass, showErrorPopup: false, - wikiTreeSource: false, }; chartRef: Chart | null = null; @@ -128,23 +280,6 @@ export class App extends React.Component { intl: intlShape, }; - private isNewData( - hash: string | undefined, - url: string | undefined, - gedcom: string | undefined, - source: string | undefined, - ): boolean { - return ( - !!(hash && hash !== this.state.hash) || - !!(url && this.state.url !== url) || - (!!gedcom && !this.state.loading && !this.state.data) || - (source === 'wikitree' && - !this.state.loading && - !this.state.data && - !this.state.error) - ); - } - /** Sets the state with a new individual selection and chart type. */ private updateDisplay( selection: IndiInfo, @@ -212,136 +347,97 @@ export class App extends React.Component { return; } - const search = queryString.parse(this.props.location.search); - const getParam = (name: string) => { - const value = search[name]; - return typeof value === 'string' ? value : undefined; - }; + const args = getArguments(this.props.location); - const showSidePanel = getParam('sidePanel') !== 'false'; // True by default. - const embedded = getParam('embedded') === 'true'; // False by default. - - if (embedded && !this.state.embedded) { + if (args.embedded && !this.state.embedded) { this.setState( Object.assign({}, this.state, { embedded: true, standalone: false, - showSidePanel, + showSidePanel: args.showSidePanel, }), ); // Notify the parent window that we are ready. window.parent.postMessage('ready', '*'); window.addEventListener('message', (data) => this.onMessage(data.data)); } - if (embedded) { + if (args.embedded) { // If the app is embedded, do not run the normal loading code. return; } - const url = getParam('url'); - const indi = getParam('indi'); - const parsedGen = Number(getParam('gen')); - const generation = !isNaN(parsedGen) ? parsedGen : undefined; - const hash = getParam('file'); - const handleCors = getParam('handleCors') !== 'false'; // True by default. - const standalone = getParam('standalone') !== 'false'; // True by default. - const view = getParam('view'); - const source = getParam('source'); - const authcode = getParam('?authcode'); + const dataSource = DATA_SOURCES.get(args.source!); - const chartTypes = new Map([ - ['relatives', ChartType.Relatives], - ['fancy', ChartType.Fancy], - ]); - // Hourglass is the default view. - const chartType = chartTypes.get(view) || ChartType.Hourglass; - - const gedcom = this.props.location.state && this.props.location.state.data; - const images = - this.props.location.state && this.props.location.state.images; - - if (!url && !hash && !source) { + if (!dataSource) { this.props.history.replace({pathname: '/'}); - } else if (this.isNewData(hash, url, gedcom, source)) { + } else if ( + (!this.state.loading && !this.state.data && !this.state.error) || + args.source !== this.state.source || + dataSource.isNewData(args, this.state) + ) { + // Set loading state. + this.setState( + Object.assign({}, this.state, { + data: undefined, + selection: undefined, + hash: args.hash, + error: undefined, + loading: true, + url: args.url, + standalone: args.standalone, + chartType: args.chartType, + source: args.source, + }), + ); try { - // Set loading state. - this.setState( - Object.assign({}, this.state, { - data: undefined, - selection: undefined, - hash, - error: undefined, - loading: true, - url, - standalone, - chartType, - wikiTreeSource: source === 'wikitree', - }), - ); - const data = - source === 'wikitree' - ? await loadWikiTree(indi!, authcode) - : hash - ? await loadGedcom(hash, gedcom, images) - : await loadFromUrl(url!, handleCors); - - const software = getSoftware(data.gedcom.head); - if (source === 'wikitree') { - analyticsEvent('wikitree_loaded'); - } else { - analyticsEvent(hash ? 'upload_file_loaded' : 'url_file_loaded', { - event_label: software, - event_value: (images && images.size) || 0, - }); - } + const data = await dataSource.loadData(args); // Set state with data. this.setState( Object.assign({}, this.state, { data, - hash, - selection: getSelection(data.chartData, indi, generation), + hash: args.hash, + selection: getSelection(data.chartData, args.indi, args.generation), error: undefined, loading: false, - url, - showSidePanel, - standalone, - chartType, - wikiTreeSource: source === 'wikitree', + url: args.url, + showSidePanel: args.showSidePanel, + standalone: args.standalone, + chartType: args.chartType, + source: args.source, }), ); } catch (error) { - analyticsEvent(hash ? 'upload_file_error' : 'url_file_error'); this.setError(error.message); } } else if (this.state.data && this.state.selection) { // Update selection if it has changed in the URL. const selection = getSelection( this.state.data.chartData, - indi, - generation, + args.indi, + args.generation, ); const loadMoreFromWikitree = - source === 'wikitree' && + args.source === DataSourceEnum.WIKITREE && (!this.state.selection || this.state.selection.id !== selection.id); this.updateDisplay(selection, { - chartType, + chartType: args.chartType, loadingMore: loadMoreFromWikitree || undefined, }); if (loadMoreFromWikitree) { - const data = await loadWikiTree(indi!); + const data = await loadWikiTree(args.indi!); this.setState( Object.assign({}, this.state, { data, - hash, - selection: getSelection(data.chartData, indi, generation), + hash: args.hash, + selection: getSelection(data.chartData, args.indi, args.generation), error: undefined, loading: false, - url, - showSidePanel, - standalone, - chartType, - wikiTreeSource: source === 'wikitree', + url: args.url, + showSidePanel: args.showSidePanel, + standalone: args.standalone, + chartType: args.chartType, + source: args.source, loadingMore: false, }), ); @@ -471,7 +567,9 @@ export class App extends React.Component { { onDownloadPng: this.onDownloadPng, onDownloadSvg: this.onDownloadSvg, }} - showWikiTreeLogin={this.state.wikiTreeSource} + showWikiTreeLogin={this.state.source === DataSourceEnum.WIKITREE} /> )} />