Merged ChartView component into App to push state up.

Hide "print" and "download" buttons when not displaying a chart.
This commit is contained in:
Przemek Wiech
2019-02-28 23:19:27 +01:00
parent 34a54e4404
commit 33d4f8f885
4 changed files with 196 additions and 211 deletions

View File

@@ -1,42 +1,185 @@
import * as queryString from 'query-string';
import * as React from 'react';
import {ChartView} from './chart_view';
import {HashRouter as Router, Route, RouteComponentProps, Switch} from 'react-router-dom';
import {Chart} from './chart';
import {getSelection, loadFromUrl, loadGedcom} from './load_data';
import {IndiInfo, JsonGedcomData} from 'topola';
import {Intro} from './intro';
import {Loader, Message} from 'semantic-ui-react';
import {Route, RouteComponentProps, Switch, Redirect} from 'react-router-dom';
import {TopBar} from './top_bar';
export class App extends React.Component<{}, {}> {
chartViewRef?: ChartView;
/** Shows an error message. */
export function ErrorMessage(props: {message: string}) {
return (
<Message negative className="error">
<Message.Header>Failed to load file</Message.Header>
<p>{props.message}</p>
</Message>
);
}
interface State {
/** Loaded data. */
data?: JsonGedcomData;
/** Selected individual. */
selection?: IndiInfo;
/** Hash of the GEDCOM contents. */
hash?: string;
/** Error to display. */
error?: string;
/** True if currently loading. */
loading: boolean;
/** URL of the data that is loaded or is being loaded. */
url?: string;
}
export class App extends React.Component<RouteComponentProps, {}> {
state: State = {loading: false};
chartRef: Chart | null = null;
private isNewData(
hash: string | undefined,
url: string | undefined,
): boolean {
return (
!!(hash && hash !== this.state.hash) || !!(url && this.state.url !== url)
);
}
componentDidMount() {
this.componentDidUpdate();
}
componentDidUpdate() {
if (this.props.location.pathname !== '/view') {
return;
}
const gedcom = this.props.location.state && this.props.location.state.data;
const search = queryString.parse(this.props.location.search);
const getParam = (name: string) => {
const value = search[name];
return typeof value === 'string' ? value : undefined;
};
const url = getParam('url');
const indi = getParam('indi');
const parsedGen = Number(getParam('gen'));
const generation = !isNaN(parsedGen) ? parsedGen : undefined;
const hash = getParam('file');
const handleCors = getParam('handleCors') !== 'false';
if (!url && !hash) {
this.props.history.replace({pathname: '/'});
} else if (this.isNewData(hash, url)) {
const loadedData = hash
? loadGedcom(hash, gedcom)
: loadFromUrl(url!, handleCors);
loadedData.then(
(data) => {
// Set state with data.
this.setState(
Object.assign({}, this.state, {
data,
hash,
selection: getSelection(data, indi, generation),
error: undefined,
loading: false,
url,
}),
);
},
(error) => {
// Set error state.
this.setState(
Object.assign({}, this.state, {
error: error.message,
loading: false,
}),
);
},
);
// Set loading state.
this.setState(
Object.assign({}, this.state, {
data: undefined,
selection: undefined,
hash,
error: undefined,
loading: true,
url,
}),
);
} else if (this.state.data && this.state.selection) {
// Update selection if it has changed in the URL.
const selection = getSelection(this.state.data, indi, generation);
if (
this.state.selection.id !== selection.id ||
this.state.selection.generation !== selection.generation
) {
this.setState(
Object.assign({}, this.state, {
selection,
}),
);
}
}
}
/**
* Called when the user clicks an individual box in the chart.
* Updates the browser URL.
*/
private onSelection = (selection: IndiInfo) => {
const location = this.props.location;
const search = queryString.parse(location.search);
search.indi = selection.id;
search.gen = String(selection.generation);
location.search = queryString.stringify(search);
this.props.history.push(location);
};
private renderMainArea = () => {
if (this.state.data && this.state.selection) {
return (
<Chart
data={this.state.data}
onSelection={this.onSelection}
selection={this.state.selection}
ref={(ref) => (this.chartRef = ref)}
/>
);
}
if (this.state.error) {
return <ErrorMessage message={this.state.error!} />;
}
return <Loader active size="large" />;
};
render() {
return (
<Router>
<>
<Route
component={(props: RouteComponentProps) => (
render={(props: RouteComponentProps) => (
<TopBar
{...props}
onPrint={() => this.chartViewRef && this.chartViewRef.print()}
onDownloadSvg={() =>
this.chartViewRef && this.chartViewRef.downloadSvg()
}
onDownloadPng={() =>
this.chartViewRef && this.chartViewRef.downloadPng()
showingChart={
!!(
this.props.history.location.pathname === '/view' &&
this.state.data &&
this.state.selection
)
}
onPrint={() => this.chartRef && this.chartRef.print()}
onDownloadSvg={() => this.chartRef && this.chartRef.downloadSvg()}
onDownloadPng={() => this.chartRef && this.chartRef.downloadPng()}
/>
)}
/>
<Switch>
<Route exact path="/" component={Intro} />
<Route
exact
path="/view"
component={(props: RouteComponentProps) => (
<ChartView {...props} ref={(ref) => (this.chartViewRef = ref!)} />
)}
/>
<Route exact path="/view" render={this.renderMainArea} />
<Redirect to={'/'} />
</Switch>
</>
</Router>
);
}
}

View File

@@ -1,168 +0,0 @@
import * as queryString from 'query-string';
import * as React from 'react';
import {Chart} from './chart';
import {IndiInfo, JsonGedcomData} from 'topola';
import {Loader, Message} from 'semantic-ui-react';
import {RouteComponentProps} from 'react-router-dom';
import {getSelection, loadFromUrl, loadGedcom} from './load_data';
/** Shows an error message. */
export function ErrorMessage(props: {message: string}) {
return (
<Message negative className="error">
<Message.Header>Failed to load file</Message.Header>
<p>{props.message}</p>
</Message>
);
}
interface State {
/** Loaded data. */
data?: JsonGedcomData;
/** Selected individual. */
selection?: IndiInfo;
/** Hash of the GEDCOM contents. */
hash?: string;
/** Error to display. */
error?: string;
/** True if currently loading. */
loading: boolean;
/** URL of the data that is loaded or is being loaded. */
url?: string;
}
/** The main area of the application dedicated for rendering the family chart. */
export class ChartView extends React.Component<RouteComponentProps, State> {
state: State = {loading: false};
chartRef: Chart | null = null;
/**
* Called when the user clicks an individual box in the chart.
* Updates the browser URL.
*/
onSelection = (selection: IndiInfo) => {
const location = this.props.location;
const search = queryString.parse(location.search);
search.indi = selection.id;
search.gen = String(selection.generation);
location.search = queryString.stringify(search);
this.props.history.push(location);
};
isNewData(hash: string | undefined, url: string | undefined): boolean {
return (
!!(hash && hash !== this.state.hash) || !!(url && this.state.url !== url)
);
}
componentDidMount() {
this.componentDidUpdate();
}
componentDidUpdate() {
const gedcom = this.props.location.state && this.props.location.state.data;
const search = queryString.parse(this.props.location.search);
const getParam = (name: string) => {
const value = search[name];
return typeof value === 'string' ? value : undefined;
};
const url = getParam('url');
const indi = getParam('indi');
const parsedGen = Number(getParam('gen'));
const generation = !isNaN(parsedGen) ? parsedGen : undefined;
const hash = getParam('file');
const handleCors = getParam('handleCors') !== 'false';
if (!url && !hash) {
this.props.history.replace({pathname: '/'});
} else if (this.isNewData(hash, url)) {
const loadedData = hash
? loadGedcom(hash, gedcom)
: loadFromUrl(url!, handleCors);
loadedData.then(
(data) => {
// Set state with data.
this.setState(
Object.assign({}, this.state, {
data,
hash,
selection: getSelection(data, indi, generation),
error: undefined,
loading: false,
url,
}),
);
},
(error) => {
// Set error state.
this.setState(
Object.assign({}, this.state, {
error: error.message,
loading: false,
}),
);
},
);
// Set loading state.
this.setState(
Object.assign({}, this.state, {
data: undefined,
selection: undefined,
hash,
error: undefined,
loading: true,
url,
}),
);
} else if (this.state.data && this.state.selection) {
// Update selection if it has changed in the URL.
const selection = getSelection(this.state.data, indi, generation);
if (
this.state.selection.id !== selection.id ||
this.state.selection.generation !== selection.generation
) {
this.setState(
Object.assign({}, this.state, {
selection,
}),
);
}
}
}
render() {
if (this.state.data && this.state.selection) {
return (
<Chart
data={this.state.data}
onSelection={this.onSelection}
selection={this.state.selection}
ref={(ref) => (this.chartRef = ref)}
/>
);
}
if (this.state.error) {
return <ErrorMessage message={this.state.error!} />;
}
return <Loader active size="large" />;
}
/** Shows the print dialog to print the currently displayed chart. */
print() {
if (this.chartRef) {
this.chartRef.print();
}
}
downloadSvg() {
if (this.chartRef) {
this.chartRef.downloadSvg();
}
}
downloadPng() {
if (this.chartRef) {
this.chartRef.downloadPng();
}
}
}

View File

@@ -6,6 +6,7 @@ import messages_pl from './translations/pl.json';
import {addLocaleData} from 'react-intl';
import {App} from './app';
import {detect} from 'detect-browser';
import {HashRouter as Router, Route} from 'react-router-dom';
import {IntlProvider} from 'react-intl';
import './index.css';
import 'semantic-ui-css/semantic.min.css';
@@ -30,7 +31,9 @@ if (browser && browser.name === 'ie') {
} else {
ReactDOM.render(
<IntlProvider locale={language} messages={messages[language]}>
<App />
<Router>
<Route component={App} />
</Router>
</IntlProvider>,
document.querySelector('#root'),
);

View File

@@ -22,6 +22,7 @@ interface State {
}
interface Props {
showingChart: boolean;
onPrint: () => void;
onDownloadSvg: () => void;
onDownloadPng: () => void;
@@ -144,6 +145,33 @@ export class TopBar extends React.Component<
</Modal>
);
const chartMenus = this.props.showingChart ? (
<>
<Menu.Item as="a" onClick={() => this.props.onPrint()}>
<Icon name="print" />
<FormattedMessage id="menu.print" defaultMessage="Print" />
</Menu.Item>
<Dropdown
trigger={
<div>
<Icon name="download" />
<FormattedMessage id="menu.download" defaultMessage="Download" />
</div>
}
className="item"
>
<Dropdown.Menu>
<Dropdown.Item onClick={() => this.props.onDownloadPng()}>
<FormattedMessage id="menu.png_file" defaultMessage="PNG file" />
</Dropdown.Item>
<Dropdown.Item onClick={() => this.props.onDownloadSvg()}>
<FormattedMessage id="menu.svg_file" defaultMessage="SVG file" />
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</>
) : null;
return (
<Menu attached="top" inverted color="blue" size="large">
<Link to="/">
@@ -174,28 +202,7 @@ export class TopBar extends React.Component<
/>
</Menu.Item>
</label>
<Menu.Item as="a" onClick={() => this.props.onPrint()}>
<Icon name="print" />
<FormattedMessage id="menu.print" defaultMessage="Print" />
</Menu.Item>
<Dropdown
trigger={
<div>
<Icon name="download" />
<FormattedMessage id="menu.download" defaultMessage="Download" />
</div>
}
className="item"
>
<Dropdown.Menu>
<Dropdown.Item onClick={() => this.props.onDownloadPng()}>
<FormattedMessage id="menu.png_file" defaultMessage="PNG file" />
</Dropdown.Item>
<Dropdown.Item onClick={() => this.props.onDownloadSvg()}>
<FormattedMessage id="menu.svg_file" defaultMessage="SVG file" />
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
{chartMenus}
<Menu.Item
as="a"
href="https://github.com/PeWu/topola-viewer"