diff --git a/package-lock.json b/package-lock.json index d7a2c09..b5cdca7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14558,9 +14558,9 @@ "dev": true }, "topola": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/topola/-/topola-2.5.1.tgz", - "integrity": "sha512-g1hDeT0X4HHg/p1/Wr/fgUceN9JlP6LC1rIJkfP7lPwWVZcplzfdCDzuyIKPfkrUsE5OZAhw3LoPu6xKv1u/dg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/topola/-/topola-3.0.0.tgz", + "integrity": "sha512-taln1nxS819MpxtIwqCjW47VljOT9HBhpaxtjSqVEKPBzXV8/zLIemF2NkIMSuH+7jnKJllpXAc1FAseBC0cfA==", "requires": { "array-flat-polyfill": "^1.0.1", "d3": "^5.4.0", diff --git a/package.json b/package.json index cc4c565..f0b3dd0 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "react-router-dom": "^4.3.1", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^0.84.0", - "topola": "^2.5.1" + "topola": "^3.0.0" }, "devDependencies": { "@types/array.prototype.flatmap": "^1.2.0", diff --git a/src/app.tsx b/src/app.tsx index 9d1d46f..1081ea3 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -74,6 +74,16 @@ interface GedcomMessage extends EmbeddedMessage { gedcom?: string; } +/** Returs true if the changes object has values that are different than those in state. */ +function hasUpdatedValues(state: T, changes: Partial | undefined) { + if (!changes) { + return false; + } + return Object.entries(changes).some( + ([key, value]) => value !== undefined && state[key] !== value, + ); +} + interface State { /** Loaded data. */ data?: TopolaData; @@ -99,6 +109,7 @@ interface State { showErrorPopup: boolean; /** True if data is loaded from WikiTree. */ wikiTreeSource: boolean; + loadingMore?: boolean; } export class App extends React.Component { @@ -135,18 +146,18 @@ export class App extends React.Component { } /** Sets the state with a new individual selection and chart type. */ - private updateDisplay(selection: IndiInfo, chartType?: ChartType) { + private updateDisplay( + selection: IndiInfo, + otherStateChanges?: Partial, + ) { if ( !this.state.selection || this.state.selection.id !== selection.id || this.state.selection!.generation !== selection.generation || - (chartType !== undefined && chartType !== this.state.chartType) + hasUpdatedValues(this.state, otherStateChanges) ) { this.setState( - Object.assign({}, this.state, { - selection, - chartType: chartType !== undefined ? chartType : this.state.chartType, - }), + Object.assign({}, this.state, {selection}, otherStateChanges), ); } } @@ -308,7 +319,31 @@ export class App extends React.Component { indi, generation, ); - this.updateDisplay(selection, chartType); + const loadMoreFromWikitree = + source === 'wikitree' && + (!this.state.selection || this.state.selection.id !== selection.id); + this.updateDisplay(selection, { + chartType, + loadingMore: loadMoreFromWikitree || undefined, + }); + if (loadMoreFromWikitree) { + const data = await loadWikiTree(indi!); + this.setState( + Object.assign({}, this.state, { + data, + hash, + selection: getSelection(data.chartData, indi, generation), + error: undefined, + loading: false, + url, + showSidePanel, + standalone, + chartType, + wikiTreeSource: source === 'wikitree', + loadingMore: false, + }), + ); + } } } @@ -399,6 +434,9 @@ export class App extends React.Component { message={this.state.error} onDismiss={this.onDismissErrorPopup} /> + {this.state.loadingMore ? ( + + ) : null} { updateSvgSize: false, locale: this.context.intl.locale, }); + } else { + this.chart!.setData(this.props.data); } const chartInfo = this.chart!.render({ startIndi: this.props.selection.id, @@ -227,9 +229,7 @@ export class Chart extends React.PureComponent { } componentDidUpdate(prevProps: ChartProps) { - const initialRender = - this.props.data !== prevProps.data || - this.props.chartType !== prevProps.chartType; + const initialRender = this.props.chartType !== prevProps.chartType; this.renderChart({initialRender}); } diff --git a/src/wikitree.ts b/src/wikitree.ts index 1c75c46..2b5aeb7 100644 --- a/src/wikitree.ts +++ b/src/wikitree.ts @@ -63,8 +63,16 @@ async function wikiTreeGet(request: WikiTreeRequest, handleCors: boolean) { return JSON.parse(responseBody); } -/** Retrieves ancestors from WikiTree for the given person ID. */ +/** + * Retrieves ancestors from WikiTree for the given person ID. + * Uses sessionStorage for caching responses. + */ async function getAncestors(key: string, handleCors: boolean) { + const cacheKey = `wikitree:ancestors:${key}`; + const cachedData = sessionStorage.getItem(cacheKey); + if (cachedData) { + return JSON.parse(cachedData); + } const response = await wikiTreeGet( { action: 'getAncestors', @@ -73,21 +81,48 @@ async function getAncestors(key: string, handleCors: boolean) { }, handleCors, ); - return response[0].ancestors as Person[]; + const result = response[0].ancestors as Person[]; + sessionStorage.setItem(cacheKey, JSON.stringify(result)); + return result; } -/** Retrieves relatives from WikiTree for the given array of person IDs. */ +/** + * Retrieves relatives from WikiTree for the given array of person IDs. + * Uses sessionStorage for caching responses. + */ async function getRelatives(keys: string[], handleCors: boolean) { + const result: Person[] = []; + const keysToFetch: string[] = []; + keys.forEach((key) => { + const cachedData = sessionStorage.getItem(`wikitree:relatives:${key}`); + if (cachedData) { + result.push(JSON.parse(cachedData)); + } else { + keysToFetch.push(key); + } + }); + if (keysToFetch.length === 0) { + return result; + } const response = await wikiTreeGet( { action: 'getRelatives', - keys: keys.join(','), + keys: keysToFetch.join(','), getChildren: true, getSpouses: true, }, handleCors, ); - return response[0].items.map((x: {person: Person}) => x.person) as Person[]; + const fetchedResults = response[0].items.map( + (x: {person: Person}) => x.person, + ) as Person[]; + fetchedResults.forEach((person) => { + sessionStorage.setItem( + `wikitree:relatives:${person.Name}`, + JSON.stringify(person), + ); + }); + return result.concat(fetchedResults); } /**