Introduced explicit application state

This commit is contained in:
Przemek Wiech
2020-05-05 18:34:03 +02:00
parent aaba839a46
commit f2e3120dc9

View File

@@ -52,6 +52,14 @@ function ErrorPopup(props: ErrorPopupProps) {
); );
} }
enum AppState {
INITIAL,
LOADING,
ERROR,
SHOWING_CHART,
LOADING_MORE,
}
/** /**
* Message types used in embedded mode. * Message types used in embedded mode.
* When the parent is ready to receive messages, it sends PARENT_READY. * When the parent is ready to receive messages, it sends PARENT_READY.
@@ -91,7 +99,11 @@ class UploadedDataSource implements DataSource {
isNewData(args: Arguments, state: State): boolean { isNewData(args: Arguments, state: State): boolean {
return ( return (
args.hash !== state.hash || args.hash !== state.hash ||
!!(args.gedcom && !state.loading && !state.data) !!(
args.gedcom &&
state.state !== AppState.LOADING &&
state.state !== AppState.SHOWING_CHART
)
); );
} }
@@ -245,6 +257,8 @@ function hasUpdatedValues<T>(state: T, changes: Partial<T> | undefined) {
} }
interface State { interface State {
/** State of the application. */
state: AppState;
/** Loaded data. */ /** Loaded data. */
data?: TopolaData; data?: TopolaData;
/** Selected individual. */ /** Selected individual. */
@@ -253,8 +267,6 @@ interface State {
hash?: string; hash?: string;
/** Error to display. */ /** Error to display. */
error?: string; error?: string;
/** True if data is currently being loaded. */
loading: boolean;
/** URL of the data that is loaded or is being loaded. */ /** URL of the data that is loaded or is being loaded. */
url?: string; url?: string;
/** Whether the side panel is shown. */ /** Whether the side panel is shown. */
@@ -269,14 +281,13 @@ interface State {
showErrorPopup: boolean; showErrorPopup: boolean;
/** Source of the data. */ /** Source of the data. */
source?: DataSourceEnum; source?: DataSourceEnum;
loadingMore?: boolean;
/** Freeze animations after initial chart render. */ /** Freeze animations after initial chart render. */
freezeAnimation?: boolean; freezeAnimation?: boolean;
} }
export class App extends React.Component<RouteComponentProps, {}> { export class App extends React.Component<RouteComponentProps, {}> {
state: State = { state: State = {
loading: false, state: AppState.INITIAL,
embedded: false, embedded: false,
standalone: true, standalone: true,
chartType: ChartType.Hourglass, chartType: ChartType.Hourglass,
@@ -317,8 +328,8 @@ export class App extends React.Component<RouteComponentProps, {}> {
private setError(error: string) { private setError(error: string) {
this.setState( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
error: error, state: AppState.ERROR,
loading: false, error,
}), }),
); );
} }
@@ -341,10 +352,9 @@ export class App extends React.Component<RouteComponentProps, {}> {
// Set state with data. // Set state with data.
this.setState( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
state: AppState.SHOWING_CHART,
data, data,
selection: getSelection(data.chartData), selection: getSelection(data.chartData),
error: undefined,
loading: false,
}), }),
); );
} catch (error) { } catch (error) {
@@ -360,14 +370,19 @@ export class App extends React.Component<RouteComponentProps, {}> {
async componentDidUpdate() { async componentDidUpdate() {
if (this.props.location.pathname !== '/view') { if (this.props.location.pathname !== '/view') {
if (this.state.state !== AppState.INITIAL) {
this.setState(Object.assign({}, this.state, {state: AppState.INITIAL}));
}
return; return;
} }
const args = getArguments(this.props.location); const args = getArguments(this.props.location);
if (args.embedded && !this.state.embedded) { if (args.embedded && !this.state.embedded) {
// Enter embedded mode.
this.setState( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
state: AppState.LOADING,
embedded: true, embedded: true,
standalone: false, standalone: false,
showSidePanel: args.showSidePanel, showSidePanel: args.showSidePanel,
@@ -387,18 +402,16 @@ export class App extends React.Component<RouteComponentProps, {}> {
if (!dataSource) { if (!dataSource) {
this.props.history.replace({pathname: '/'}); this.props.history.replace({pathname: '/'});
} else if ( } else if (
(!this.state.loading && !this.state.data && !this.state.error) || this.state.state === AppState.INITIAL ||
args.source !== this.state.source || args.source !== this.state.source ||
dataSource.isNewData(args, this.state) dataSource.isNewData(args, this.state)
) { ) {
// Set loading state. // Set loading state.
this.setState( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
data: undefined, state: AppState.LOADING,
selection: {id: args.indi}, selection: {id: args.indi},
hash: args.hash, hash: args.hash,
error: undefined,
loading: true,
url: args.url, url: args.url,
standalone: args.standalone, standalone: args.standalone,
chartType: args.chartType, chartType: args.chartType,
@@ -411,11 +424,10 @@ export class App extends React.Component<RouteComponentProps, {}> {
// Set state with data. // Set state with data.
this.setState( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
state: AppState.SHOWING_CHART,
data, data,
hash: args.hash, hash: args.hash,
selection: getSelection(data.chartData, args.indi, args.generation), selection: getSelection(data.chartData, args.indi, args.generation),
error: undefined,
loading: false,
url: args.url, url: args.url,
showSidePanel: args.showSidePanel, showSidePanel: args.showSidePanel,
standalone: args.standalone, standalone: args.standalone,
@@ -427,10 +439,13 @@ export class App extends React.Component<RouteComponentProps, {}> {
} catch (error) { } catch (error) {
this.setError(error.message); this.setError(error.message);
} }
} else if (this.state.data && this.state.selection) { } else if (
this.state.state === AppState.SHOWING_CHART ||
this.state.state === AppState.LOADING_MORE
) {
// Update selection if it has changed in the URL. // Update selection if it has changed in the URL.
const selection = getSelection( const selection = getSelection(
this.state.data.chartData, this.state.data!.chartData,
args.indi, args.indi,
args.generation, args.generation,
); );
@@ -439,7 +454,9 @@ export class App extends React.Component<RouteComponentProps, {}> {
(!this.state.selection || this.state.selection.id !== selection.id); (!this.state.selection || this.state.selection.id !== selection.id);
this.updateDisplay(selection, { this.updateDisplay(selection, {
chartType: args.chartType, chartType: args.chartType,
loadingMore: loadMoreFromWikitree || undefined, state: loadMoreFromWikitree
? AppState.LOADING_MORE
: AppState.SHOWING_CHART,
}); });
if (loadMoreFromWikitree) { if (loadMoreFromWikitree) {
try { try {
@@ -451,17 +468,15 @@ export class App extends React.Component<RouteComponentProps, {}> {
); );
this.setState( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
state: AppState.SHOWING_CHART,
data, data,
hash: args.hash, hash: args.hash,
selection, selection,
error: undefined,
loading: false,
url: args.url, url: args.url,
showSidePanel: args.showSidePanel, showSidePanel: args.showSidePanel,
standalone: args.standalone, standalone: args.standalone,
chartType: args.chartType, chartType: args.chartType,
source: args.source, source: args.source,
loadingMore: false,
}), }),
); );
} catch (error) { } catch (error) {
@@ -473,9 +488,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
}, },
{error}, {error},
), ),
{ {state: AppState.SHOWING_CHART},
loadingMore: false,
},
); );
} }
} }
@@ -561,7 +574,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
this.chartRef && this.chartRef.downloadSvg(); this.chartRef && this.chartRef.downloadSvg();
}; };
onDismissErrorPopup = () => { private onDismissErrorPopup = () => {
this.setState( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
showErrorPopup: false, showErrorPopup: false,
@@ -570,7 +583,9 @@ export class App extends React.Component<RouteComponentProps, {}> {
}; };
private renderMainArea = () => { private renderMainArea = () => {
if (this.state.data && this.state.selection) { switch (this.state.state) {
case AppState.SHOWING_CHART:
case AppState.LOADING_MORE:
return ( return (
<div id="content"> <div id="content">
<ErrorPopup <ErrorPopup
@@ -578,12 +593,12 @@ export class App extends React.Component<RouteComponentProps, {}> {
message={this.state.error} message={this.state.error}
onDismiss={this.onDismissErrorPopup} onDismiss={this.onDismissErrorPopup}
/> />
{this.state.loadingMore ? ( {this.state.state === AppState.LOADING_MORE ? (
<Loader active size="small" className="loading-more" /> <Loader active size="small" className="loading-more" />
) : null} ) : null}
<Chart <Chart
data={this.state.data.chartData} data={this.state.data!.chartData}
selection={this.state.selection} selection={this.state.selection!}
chartType={this.state.chartType} chartType={this.state.chartType}
onSelection={this.onSelection} onSelection={this.onSelection}
freezeAnimation={this.state.freezeAnimation} freezeAnimation={this.state.freezeAnimation}
@@ -592,18 +607,21 @@ export class App extends React.Component<RouteComponentProps, {}> {
{this.state.showSidePanel ? ( {this.state.showSidePanel ? (
<Responsive minWidth={768} id="sidePanel"> <Responsive minWidth={768} id="sidePanel">
<Details <Details
gedcom={this.state.data.gedcom} gedcom={this.state.data!.gedcom}
indi={this.state.selection.id} indi={this.state.selection!.id}
/> />
</Responsive> </Responsive>
) : null} ) : null}
</div> </div>
); );
}
if (this.state.error) { case AppState.ERROR:
return <ErrorMessage message={this.state.error!} />; return <ErrorMessage message={this.state.error!} />;
}
case AppState.INITIAL:
case AppState.LOADING:
return <Loader active size="large" />; return <Loader active size="large" />;
}
}; };
render() { render() {
@@ -618,11 +636,9 @@ export class App extends React.Component<RouteComponentProps, {}> {
this.state.source !== DataSourceEnum.WIKITREE this.state.source !== DataSourceEnum.WIKITREE
} }
showingChart={ showingChart={
!!(
this.props.history.location.pathname === '/view' && this.props.history.location.pathname === '/view' &&
this.state.data && (this.state.state === AppState.SHOWING_CHART ||
this.state.selection this.state.state === AppState.LOADING_MORE)
)
} }
standalone={this.state.standalone} standalone={this.state.standalone}
eventHandlers={{ eventHandlers={{