Refactored data loading code to separate data processing from component state.

This commit is contained in:
Przemek Wiech 2019-02-26 00:00:14 +01:00
parent f8bfebcaf1
commit 2ac314bdee

View File

@ -1,6 +1,5 @@
import * as queryString from 'query-string'; import * as queryString from 'query-string';
import * as React from 'react'; import * as React from 'react';
import md5 from 'md5';
import {Chart} from './chart'; import {Chart} from './chart';
import {convertGedcom} from './gedcom_util'; import {convertGedcom} from './gedcom_util';
import {IndiInfo, JsonGedcomData} from 'topola'; import {IndiInfo, JsonGedcomData} from 'topola';
@ -32,6 +31,50 @@ function getSelection(
}; };
} }
/** Fetches data from the given URL. Uses cors-anywhere if handleCors is true. */
function loadFromUrl(
url: string,
handleCors: boolean,
): Promise<JsonGedcomData> {
const cachedData = sessionStorage.getItem(url);
if (cachedData) {
return Promise.resolve(JSON.parse(cachedData));
}
const urlToFetch = handleCors
? 'https://cors-anywhere.herokuapp.com/' + url
: url;
return window
.fetch(urlToFetch)
.then((response) => {
if (response.status !== 200) {
return Promise.reject(new Error(response.statusText));
}
return response.text();
})
.then((gedcom) => {
const data = convertGedcom(gedcom);
const serializedData = JSON.stringify(data);
sessionStorage.setItem(url, serializedData);
return data;
});
}
/** Loads data from the given GEDCOM file contents. */
function loadGedcom(hash: string, gedcom?: string) {
const cachedData = sessionStorage.getItem(hash);
if (cachedData) {
return JSON.parse(cachedData);
}
if (!gedcom) {
throw new Error('Error loading data. Please upload your file again.');
}
const data = convertGedcom(gedcom);
const serializedData = JSON.stringify(data);
sessionStorage.setItem(hash, serializedData);
return data;
}
interface State { interface State {
/** Loaded data. */ /** Loaded data. */
data?: JsonGedcomData; data?: JsonGedcomData;
@ -44,7 +87,7 @@ interface State {
/** True if currently loading. */ /** True if currently loading. */
loading: boolean; loading: boolean;
/** URL of the data that is loaded or is being loaded. */ /** URL of the data that is loaded or is being loaded. */
loadedUrl?: string; url?: string;
} }
/** The main area of the application dedicated for rendering the family chart. */ /** The main area of the application dedicated for rendering the family chart. */
@ -65,104 +108,29 @@ export class ChartView extends React.Component<RouteComponentProps, State> {
this.props.history.push(location); this.props.history.push(location);
}; };
/** Loads a GEDCOM file from the given URL. */ isNewData(hash: string | undefined, url: string | undefined): boolean {
loadFromUrl( return (
url: string, !!(hash && hash !== this.state.hash) || !!(url && this.state.url !== url)
options: {
handleCors?: boolean;
indi?: string;
generation?: number;
} = {},
) {
const cachedData = sessionStorage.getItem(url);
if (cachedData) {
const data = JSON.parse(cachedData);
this.setState(
Object.assign({}, this.state, {
data,
selection: getSelection(data, options.indi, options.generation),
loadedUrl: url,
loading: false,
error: undefined,
hash: undefined,
}),
);
return;
}
this.setState(
Object.assign({}, this.state, {
loading: true,
loadedUrl: url,
data: undefined,
error: undefined,
}),
); );
const urlToFetch = options.handleCors
? 'https://cors-anywhere.herokuapp.com/' + url
: url;
window
.fetch(urlToFetch)
.then((response) => {
if (response.status !== 200) {
return Promise.reject(new Error(response.statusText));
}
return response.text();
})
.then((data) =>
this.setGedcom({
gedcom: data,
url,
indi: options.indi,
generation: options.generation,
}),
)
.catch((e: Error) =>
this.setState(
Object.assign({}, this.state, {error: e.message, loading: false}),
),
);
} }
/** loadData(
* Converts GEDCOM contents and sets the data in the state. gedcom: string | undefined,
* In case of an error reading the file, sets an error. hash: string | undefined,
*/ url: string | undefined,
setGedcom(input: { handleCors: boolean,
gedcom: string; ): Promise<JsonGedcomData> {
url?: string; if (!hash && !url) {
indi?: string; return Promise.reject(new Error('Precondition failed'));
generation?: number;
}) {
const hash = md5(input.gedcom);
try {
const data = convertGedcom(input.gedcom);
const serializedData = JSON.stringify(data);
sessionStorage.setItem(input.url || hash, serializedData);
this.setState(
Object.assign({}, this.state, {
data,
selection: getSelection(data, input.indi, input.generation),
hash,
loading: false,
loadedUrl: input.url,
error: undefined,
}),
);
} catch (e) {
this.setState(
Object.assign({}, this.state, {
data: undefined,
selection: undefined,
hash,
loading: false,
error: 'Failed to read GEDCOM file',
loadedUrl: input.url,
}),
);
} }
if (hash) {
try {
return Promise.resolve(loadGedcom(hash, gedcom));
} catch (e) {
return Promise.reject(new Error('Failed to read GEDCOM file'));
}
}
return loadFromUrl(url!, handleCors);
} }
componentDidMount() { componentDidMount() {
@ -183,15 +151,10 @@ export class ChartView extends React.Component<RouteComponentProps, State> {
const hash = getParam('file'); const hash = getParam('file');
const handleCors = getParam('handleCors') !== 'false'; const handleCors = getParam('handleCors') !== 'false';
if (hash && hash !== this.state.hash) { if (this.isNewData(hash, url)) {
// New "load from file" data. this.loadData(gedcom, hash, url, handleCors).then(
if (gedcom) { (data) => {
this.setGedcom({gedcom, indi, generation}); // Set state with data.
} else {
// Data is not present. Try loading from cache.
const cachedData = sessionStorage.getItem(hash);
if (cachedData) {
const data = JSON.parse(cachedData);
this.setState( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
data, data,
@ -199,23 +162,31 @@ export class ChartView extends React.Component<RouteComponentProps, State> {
selection: getSelection(data, indi, generation), selection: getSelection(data, indi, generation),
error: undefined, error: undefined,
loading: false, loading: false,
loadedUrl: undefined, url,
}), }),
); );
} else { },
// No data available. Redirect to main page. (error) => {
this.props.history.replace({pathname: '/'}); // Set error state.
} this.setState(
} Object.assign({}, this.state, {
} else if (!this.state.loading && url && this.state.loadedUrl !== url) { error: error.message,
// New URL to load data from. loading: false,
this.loadFromUrl(url, { }),
indi, );
generation, },
handleCors: url.startsWith('http') && handleCors, );
}); // Set loading state.
} else if (!url && !gedcom && hash !== this.state.hash) { this.setState(
this.props.history.replace({pathname: '/'}); Object.assign({}, this.state, {
data: undefined,
selection: undefined,
hash,
error: undefined,
loading: true,
url,
}),
);
} else if (this.state.data && this.state.selection) { } else if (this.state.data && this.state.selection) {
// Update selection if it has changed in the URL. // Update selection if it has changed in the URL.
const selection = getSelection(this.state.data, indi, generation); const selection = getSelection(this.state.data, indi, generation);