mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-08-02 09:01:48 +00:00
Refactoring: Extracted embedded mode as a separate data source
This commit is contained in:
113
src/app.tsx
113
src/app.tsx
@@ -3,19 +3,19 @@ import * as queryString from 'query-string';
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {analyticsEvent} from './util/analytics';
|
import {analyticsEvent} from './util/analytics';
|
||||||
import {Chart, ChartType} from './chart';
|
import {Chart, ChartType} from './chart';
|
||||||
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
|
|
||||||
import {Details} from './details';
|
import {Details} from './details';
|
||||||
|
import {EmbeddedDataSource, EmbeddedSourceSpec} from './datasource/embedded';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import {getSoftware, TopolaData} from './util/gedcom_util';
|
import {TopolaData} from './util/gedcom_util';
|
||||||
import {IndiInfo} from 'topola';
|
import {IndiInfo} from 'topola';
|
||||||
import {intlShape} from 'react-intl';
|
import {intlShape} from 'react-intl';
|
||||||
import {Intro} from './intro';
|
import {Intro} from './intro';
|
||||||
import {Loader, Message, Portal, Responsive} from 'semantic-ui-react';
|
import {Loader, Message, Portal, Responsive} from 'semantic-ui-react';
|
||||||
import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
|
import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
|
||||||
import {TopBar} from './menu/top_bar';
|
import {TopBar} from './menu/top_bar';
|
||||||
|
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
|
||||||
import {
|
import {
|
||||||
getSelection,
|
getSelection,
|
||||||
loadGedcom,
|
|
||||||
UploadSourceSpec,
|
UploadSourceSpec,
|
||||||
UrlSourceSpec,
|
UrlSourceSpec,
|
||||||
GedcomUrlDataSource,
|
GedcomUrlDataSource,
|
||||||
@@ -73,37 +73,17 @@ enum AppState {
|
|||||||
LOADING_MORE,
|
LOADING_MORE,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
type DataSourceSpec =
|
||||||
* Message types used in embedded mode.
|
| UrlSourceSpec
|
||||||
* When the parent is ready to receive messages, it sends PARENT_READY.
|
| UploadSourceSpec
|
||||||
* When the child (this app) is ready to receive messages, it sends READY.
|
| WikiTreeSourceSpec
|
||||||
* When the child receives PARENT_READY, it sends READY.
|
| EmbeddedSourceSpec;
|
||||||
* When the parent receives READY, it sends data in a GEDCOM message.
|
|
||||||
*/
|
|
||||||
enum EmbeddedMessageType {
|
|
||||||
GEDCOM = 'gedcom',
|
|
||||||
READY = 'ready',
|
|
||||||
PARENT_READY = 'parent_ready',
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Message sent to parent or received from parent in embedded mode. */
|
|
||||||
interface EmbeddedMessage {
|
|
||||||
message: EmbeddedMessageType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GedcomMessage extends EmbeddedMessage {
|
|
||||||
message: EmbeddedMessageType.GEDCOM;
|
|
||||||
gedcom?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type DataSourceSpec = UrlSourceSpec | UploadSourceSpec | WikiTreeSourceSpec;
|
|
||||||
|
|
||||||
/** Arguments passed to the application, primarily through URL parameters. */
|
/** Arguments passed to the application, primarily through URL parameters. */
|
||||||
interface Arguments {
|
interface Arguments {
|
||||||
sourceSpec?: DataSourceSpec;
|
sourceSpec?: DataSourceSpec;
|
||||||
selection?: IndiInfo;
|
selection?: IndiInfo;
|
||||||
chartType: ChartType;
|
chartType: ChartType;
|
||||||
embedded: boolean;
|
|
||||||
standalone: boolean;
|
standalone: boolean;
|
||||||
freezeAnimation?: boolean;
|
freezeAnimation?: boolean;
|
||||||
showSidePanel: boolean;
|
showSidePanel: boolean;
|
||||||
@@ -128,6 +108,7 @@ function getArguments(location: H.Location<any>): Arguments {
|
|||||||
|
|
||||||
const hash = getParam('file');
|
const hash = getParam('file');
|
||||||
const url = getParam('url');
|
const url = getParam('url');
|
||||||
|
const embedded = getParam('embedded') === 'true'; // False by default.
|
||||||
var sourceSpec: DataSourceSpec | undefined = undefined;
|
var sourceSpec: DataSourceSpec | undefined = undefined;
|
||||||
if (getParam('source') === 'wikitree') {
|
if (getParam('source') === 'wikitree') {
|
||||||
sourceSpec = {
|
sourceSpec = {
|
||||||
@@ -147,6 +128,8 @@ function getArguments(location: H.Location<any>): Arguments {
|
|||||||
url,
|
url,
|
||||||
handleCors: getParam('handleCors') !== 'false', // True by default.
|
handleCors: getParam('handleCors') !== 'false', // True by default.
|
||||||
};
|
};
|
||||||
|
} else if (embedded) {
|
||||||
|
sourceSpec = {source: DataSourceEnum.EMBEDDED};
|
||||||
}
|
}
|
||||||
|
|
||||||
const indi = getParam('indi');
|
const indi = getParam('indi');
|
||||||
@@ -162,13 +145,15 @@ function getArguments(location: H.Location<any>): Arguments {
|
|||||||
chartType: chartTypes.get(view) || ChartType.Hourglass,
|
chartType: chartTypes.get(view) || ChartType.Hourglass,
|
||||||
|
|
||||||
showSidePanel: getParam('sidePanel') !== 'false', // True by default.
|
showSidePanel: getParam('sidePanel') !== 'false', // True by default.
|
||||||
embedded: getParam('embedded') === 'true', // False by default.
|
standalone: getParam('standalone') !== 'false' && !embedded,
|
||||||
standalone: getParam('standalone') !== 'false', // True by default.
|
|
||||||
freezeAnimation: getParam('freeze') === 'true', // False by default
|
freezeAnimation: getParam('freeze') === 'true', // False by default
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returs true if the changes object has values that are different than those in state. */
|
/**
|
||||||
|
* Returs true if the changes object has values that are different than those
|
||||||
|
* in state.
|
||||||
|
*/
|
||||||
function hasUpdatedValues<T>(state: T, changes: Partial<T> | undefined) {
|
function hasUpdatedValues<T>(state: T, changes: Partial<T> | undefined) {
|
||||||
if (!changes) {
|
if (!changes) {
|
||||||
return false;
|
return false;
|
||||||
@@ -189,8 +174,6 @@ interface State {
|
|||||||
error?: string;
|
error?: string;
|
||||||
/** Whether the side panel is shown. */
|
/** Whether the side panel is shown. */
|
||||||
showSidePanel?: boolean;
|
showSidePanel?: boolean;
|
||||||
/** Whether the app is in embedded mode, i.e. embedded in an iframe. */
|
|
||||||
embedded: boolean;
|
|
||||||
/** Whether the app is in standalone mode, i.e. showing 'open file' menus. */
|
/** Whether the app is in standalone mode, i.e. showing 'open file' menus. */
|
||||||
standalone: boolean;
|
standalone: boolean;
|
||||||
/** Type of displayed chart. */
|
/** Type of displayed chart. */
|
||||||
@@ -206,7 +189,6 @@ interface State {
|
|||||||
export class App extends React.Component<RouteComponentProps, {}> {
|
export class App extends React.Component<RouteComponentProps, {}> {
|
||||||
state: State = {
|
state: State = {
|
||||||
state: AppState.INITIAL,
|
state: AppState.INITIAL,
|
||||||
embedded: false,
|
|
||||||
standalone: true,
|
standalone: true,
|
||||||
chartType: ChartType.Hourglass,
|
chartType: ChartType.Hourglass,
|
||||||
showErrorPopup: false,
|
showErrorPopup: false,
|
||||||
@@ -245,36 +227,6 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onMessage(message: EmbeddedMessage) {
|
|
||||||
if (message.message === EmbeddedMessageType.PARENT_READY) {
|
|
||||||
// Parent didn't receive the first 'ready' message, so we need to send it again.
|
|
||||||
window.parent.postMessage({message: EmbeddedMessageType.READY}, '*');
|
|
||||||
} else if (message.message === EmbeddedMessageType.GEDCOM) {
|
|
||||||
const gedcom = (message as GedcomMessage).gedcom;
|
|
||||||
if (!gedcom) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const data = await loadGedcom('', gedcom);
|
|
||||||
const software = getSoftware(data.gedcom.head);
|
|
||||||
analyticsEvent('embedded_file_loaded', {
|
|
||||||
event_label: software,
|
|
||||||
});
|
|
||||||
// Set state with data.
|
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {
|
|
||||||
state: AppState.SHOWING_CHART,
|
|
||||||
data,
|
|
||||||
selection: getSelection(data.chartData),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
analyticsEvent('embedded_file_error');
|
|
||||||
this.setError(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.componentDidUpdate();
|
this.componentDidUpdate();
|
||||||
}
|
}
|
||||||
@@ -284,6 +236,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
private readonly wikiTreeDataSource = new WikiTreeDataSource(
|
private readonly wikiTreeDataSource = new WikiTreeDataSource(
|
||||||
this.context.intl,
|
this.context.intl,
|
||||||
);
|
);
|
||||||
|
private readonly embeddedDataSource = new EmbeddedDataSource();
|
||||||
|
|
||||||
private isNewData(sourceSpec: DataSourceSpec, selection?: IndiInfo) {
|
private isNewData(sourceSpec: DataSourceSpec, selection?: IndiInfo) {
|
||||||
if (
|
if (
|
||||||
@@ -317,6 +270,12 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
oldSouce as SourceSelection<WikiTreeSourceSpec>,
|
oldSouce as SourceSelection<WikiTreeSourceSpec>,
|
||||||
this.state.data,
|
this.state.data,
|
||||||
);
|
);
|
||||||
|
case DataSourceEnum.EMBEDDED:
|
||||||
|
return this.embeddedDataSource.isNewData(
|
||||||
|
newSource as SourceSelection<EmbeddedSourceSpec>,
|
||||||
|
oldSouce as SourceSelection<EmbeddedSourceSpec>,
|
||||||
|
this.state.data,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +287,8 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
return this.gedcomUrlDataSource.loadData({spec: sourceSpec, selection});
|
return this.gedcomUrlDataSource.loadData({spec: sourceSpec, selection});
|
||||||
case DataSourceEnum.WIKITREE:
|
case DataSourceEnum.WIKITREE:
|
||||||
return this.wikiTreeDataSource.loadData({spec: sourceSpec, selection});
|
return this.wikiTreeDataSource.loadData({spec: sourceSpec, selection});
|
||||||
|
case DataSourceEnum.EMBEDDED:
|
||||||
|
return this.embeddedDataSource.loadData({spec: sourceSpec, selection});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,25 +302,6 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
|
|
||||||
const args = getArguments(this.props.location);
|
const args = getArguments(this.props.location);
|
||||||
|
|
||||||
if (args.embedded && !this.state.embedded) {
|
|
||||||
// Enter embedded mode.
|
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {
|
|
||||||
state: AppState.LOADING,
|
|
||||||
embedded: true,
|
|
||||||
standalone: false,
|
|
||||||
showSidePanel: args.showSidePanel,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
// Notify the parent window that we are ready.
|
|
||||||
window.parent.postMessage('ready', '*');
|
|
||||||
window.addEventListener('message', (data) => this.onMessage(data.data));
|
|
||||||
}
|
|
||||||
if (args.embedded) {
|
|
||||||
// If the app is embedded, do not run the normal loading code.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!args.sourceSpec) {
|
if (!args.sourceSpec) {
|
||||||
this.props.history.replace({pathname: '/'});
|
this.props.history.replace({pathname: '/'});
|
||||||
} else if (
|
} else if (
|
||||||
@@ -447,11 +389,6 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
analyticsEvent('selection_changed');
|
analyticsEvent('selection_changed');
|
||||||
if (this.state.embedded) {
|
|
||||||
// In embedded mode the URL doesn't change.
|
|
||||||
this.updateDisplay(selection);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const location = this.props.location;
|
const location = this.props.location;
|
||||||
const search = queryString.parse(location.search);
|
const search = queryString.parse(location.search);
|
||||||
search.indi = selection.id;
|
search.indi = selection.id;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export enum DataSourceEnum {
|
|||||||
UPLOADED,
|
UPLOADED,
|
||||||
GEDCOM_URL,
|
GEDCOM_URL,
|
||||||
WIKITREE,
|
WIKITREE,
|
||||||
|
EMBEDDED,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Source specification together with individual selection. */
|
/** Source specification together with individual selection. */
|
||||||
|
|||||||
82
src/datasource/embedded.ts
Normal file
82
src/datasource/embedded.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import {analyticsEvent} from '../util/analytics';
|
||||||
|
import {DataSource, DataSourceEnum, SourceSelection} from './data_source';
|
||||||
|
import {getSoftware, TopolaData} from '../util/gedcom_util';
|
||||||
|
import {loadGedcom} from './load_data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message types used in embedded mode.
|
||||||
|
* When the parent is ready to receive messages, it sends PARENT_READY.
|
||||||
|
* When the child (this app) is ready to receive messages, it sends READY.
|
||||||
|
* When the child receives PARENT_READY, it sends READY.
|
||||||
|
* When the parent receives READY, it sends data in a GEDCOM message.
|
||||||
|
*/
|
||||||
|
enum EmbeddedMessageType {
|
||||||
|
GEDCOM = 'gedcom',
|
||||||
|
READY = 'ready',
|
||||||
|
PARENT_READY = 'parent_ready',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Message sent to parent or received from parent in embedded mode. */
|
||||||
|
interface EmbeddedMessage {
|
||||||
|
message: EmbeddedMessageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GedcomMessage extends EmbeddedMessage {
|
||||||
|
message: EmbeddedMessageType.GEDCOM;
|
||||||
|
gedcom?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmbeddedSourceSpec {
|
||||||
|
source: DataSourceEnum.EMBEDDED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GEDCOM file received from outside of the iframe. */
|
||||||
|
export class EmbeddedDataSource implements DataSource<EmbeddedSourceSpec> {
|
||||||
|
isNewData(
|
||||||
|
newSource: SourceSelection<EmbeddedSourceSpec>,
|
||||||
|
oldSource: SourceSelection<EmbeddedSourceSpec>,
|
||||||
|
data?: TopolaData,
|
||||||
|
): boolean {
|
||||||
|
// Never reload data.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onMessage(
|
||||||
|
message: EmbeddedMessage,
|
||||||
|
resolve: (value: TopolaData) => void,
|
||||||
|
reject: (reason: any) => void,
|
||||||
|
) {
|
||||||
|
if (message.message === EmbeddedMessageType.PARENT_READY) {
|
||||||
|
// Parent didn't receive the first 'ready' message, so we need to send it again.
|
||||||
|
window.parent.postMessage({message: EmbeddedMessageType.READY}, '*');
|
||||||
|
} else if (message.message === EmbeddedMessageType.GEDCOM) {
|
||||||
|
const gedcom = (message as GedcomMessage).gedcom;
|
||||||
|
if (!gedcom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await loadGedcom('', gedcom);
|
||||||
|
const software = getSoftware(data.gedcom.head);
|
||||||
|
analyticsEvent('embedded_file_loaded', {
|
||||||
|
event_label: software,
|
||||||
|
});
|
||||||
|
resolve(data);
|
||||||
|
} catch (error) {
|
||||||
|
analyticsEvent('embedded_file_error');
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadData(
|
||||||
|
source: SourceSelection<EmbeddedSourceSpec>,
|
||||||
|
): Promise<TopolaData> {
|
||||||
|
// Notify the parent window that we are ready.
|
||||||
|
return new Promise<TopolaData>((resolve, reject) => {
|
||||||
|
window.parent.postMessage({message: EmbeddedMessageType.READY}, '*');
|
||||||
|
window.addEventListener('message', (data) =>
|
||||||
|
this.onMessage(data.data, resolve, reject),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user