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,40 +583,45 @@ export class App extends React.Component<RouteComponentProps, {}> {
}; };
private renderMainArea = () => { private renderMainArea = () => {
if (this.state.data && this.state.selection) { switch (this.state.state) {
return ( case AppState.SHOWING_CHART:
<div id="content"> case AppState.LOADING_MORE:
<ErrorPopup return (
open={this.state.showErrorPopup} <div id="content">
message={this.state.error} <ErrorPopup
onDismiss={this.onDismissErrorPopup} open={this.state.showErrorPopup}
/> message={this.state.error}
{this.state.loadingMore ? ( onDismiss={this.onDismissErrorPopup}
<Loader active size="small" className="loading-more" /> />
) : null} {this.state.state === AppState.LOADING_MORE ? (
<Chart <Loader active size="small" className="loading-more" />
data={this.state.data.chartData} ) : null}
selection={this.state.selection} <Chart
chartType={this.state.chartType} data={this.state.data!.chartData}
onSelection={this.onSelection} selection={this.state.selection!}
freezeAnimation={this.state.freezeAnimation} chartType={this.state.chartType}
ref={(ref) => (this.chartRef = ref)} onSelection={this.onSelection}
/> freezeAnimation={this.state.freezeAnimation}
{this.state.showSidePanel ? ( ref={(ref) => (this.chartRef = ref)}
<Responsive minWidth={768} id="sidePanel"> />
<Details {this.state.showSidePanel ? (
gedcom={this.state.data.gedcom} <Responsive minWidth={768} id="sidePanel">
indi={this.state.selection.id} <Details
/> gedcom={this.state.data!.gedcom}
</Responsive> indi={this.state.selection!.id}
) : null} />
</div> </Responsive>
); ) : null}
</div>
);
case AppState.ERROR:
return <ErrorMessage message={this.state.error!} />;
case AppState.INITIAL:
case AppState.LOADING:
return <Loader active size="large" />;
} }
if (this.state.error) {
return <ErrorMessage message={this.state.error!} />;
}
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.state === AppState.SHOWING_CHART ||
this.state.data && this.state.state === AppState.LOADING_MORE)
this.state.selection
)
} }
standalone={this.state.standalone} standalone={this.state.standalone}
eventHandlers={{ eventHandlers={{