Added settings tab with color settings (#6)

This commit is contained in:
Przemek Wiech 2021-10-25 13:33:34 +02:00
parent c564d592ec
commit 3914226042
9 changed files with 234 additions and 32 deletions

6
CHANGELOG.md Normal file
View File

@ -0,0 +1,6 @@
# Changelog
## 2021-10-25
- Added "Settings" tab in side panel
- Added color settings (none, by generation, by sex)

6
package-lock.json generated
View File

@ -17408,9 +17408,9 @@
"dev": true
},
"topola": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/topola/-/topola-3.3.6.tgz",
"integrity": "sha512-ZsmJz17htEBOrIo3aKHe7i4aM+Xl8XBHRXL3P7FpKHsyd8LrsuiNLjOVNMhW2qApk6TeMmhyU4OXq9cq0zNv0w==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/topola/-/topola-3.5.0.tgz",
"integrity": "sha512-w3PSG3nKgNpH3+ZiIzB5nTHaIj/mk+rVbtbQoLq9O6V2M4dBJGd+eiuUZ4BsNjbFeUR0iJ8zMYBegVRHHSg8fA==",
"requires": {
"array-flat-polyfill": "^1.0.1",
"d3-array": "^2.12.1",

View File

@ -29,7 +29,7 @@
"react-router-dom": "^5.2.0",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^2.0.3",
"topola": "^3.3.6"
"topola": "^3.5.0"
},
"devDependencies": {
"@types/array.prototype.flatmap": "^1.2.2",

View File

@ -1,16 +1,23 @@
import * as H from 'history';
import * as queryString from 'query-string';
import * as React from 'react';
import React from 'react';
import {analyticsEvent} from './util/analytics';
import {Chart, ChartComponent, ChartType} from './chart';
import {
argsToConfig,
Config,
ConfigPanel,
configToArgs,
DEFALUT_CONFIG,
} from './config';
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
import {Details} from './details';
import {EmbeddedDataSource, EmbeddedSourceSpec} from './datasource/embedded';
import {FormattedMessage, WrappedComponentProps, injectIntl} from 'react-intl';
import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
import {getI18nMessage} from './util/error_i18n';
import {IndiInfo} from 'topola';
import {Intro} from './intro';
import {Loader, Message, Portal} from 'semantic-ui-react';
import {Loader, Message, Portal, Tab} from 'semantic-ui-react';
import {Media} from './util/media';
import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
import {TopBar} from './menu/top_bar';
@ -80,7 +87,10 @@ type DataSourceSpec =
| WikiTreeSourceSpec
| EmbeddedSourceSpec;
/** Arguments passed to the application, primarily through URL parameters. */
/**
* Arguments passed to the application, primarily through URL parameters.
* Non-optional arguments get populated with default values.
*/
interface Arguments {
sourceSpec?: DataSourceSpec;
selection?: IndiInfo;
@ -88,6 +98,7 @@ interface Arguments {
standalone: boolean;
freezeAnimation?: boolean;
showSidePanel: boolean;
config: Config;
}
/**
@ -148,6 +159,7 @@ function getArguments(location: H.Location<any>): Arguments {
showSidePanel: getParam('sidePanel') !== 'false', // True by default.
standalone: getParam('standalone') !== 'false' && !embedded,
freezeAnimation: getParam('freeze') === 'true', // False by default
config: argsToConfig(search),
};
}
@ -185,6 +197,7 @@ interface State {
sourceSpec?: DataSourceSpec;
/** Freeze animations after initial chart render. */
freezeAnimation?: boolean;
config: Config;
}
class AppComponent extends React.Component<
@ -196,6 +209,7 @@ class AppComponent extends React.Component<
standalone: true,
chartType: ChartType.Hourglass,
showErrorPopup: false,
config: DEFALUT_CONFIG,
};
chartRef: ChartComponent | null = null;
@ -313,6 +327,7 @@ class AppComponent extends React.Component<
selection: args.selection,
standalone: args.standalone,
chartType: args.chartType,
config: args.config,
}),
);
try {
@ -374,6 +389,16 @@ class AppComponent extends React.Component<
}
}
private updateUrl(args: queryString.ParsedQuery<any>) {
const location = this.props.location;
const search = queryString.parse(location.search);
for (const key in args) {
search[key] = args[key];
}
location.search = queryString.stringify(search);
this.props.history.push(location);
}
/**
* Called when the user clicks an individual box in the chart.
* Updates the browser URL.
@ -384,12 +409,10 @@ class AppComponent extends React.Component<
return;
}
analyticsEvent('selection_changed');
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);
this.updateUrl({
indi: selection.id,
gen: selection.generation,
});
};
private onPrint = () => {
@ -460,6 +483,35 @@ class AppComponent extends React.Component<
switch (this.state.state) {
case AppState.SHOWING_CHART:
case AppState.LOADING_MORE:
const sidePanelTabs = [
{
menuItem: this.props.intl.formatMessage({
id: 'tab.info',
defaultMessage: 'Info',
}),
render: () => (
<Details
gedcom={this.state.data!.gedcom}
indi={this.state.selection!.id}
/>
),
},
{
menuItem: this.props.intl.formatMessage({
id: 'tab.settings',
defaultMessage: 'Settings',
}),
render: () => (
<ConfigPanel
config={this.state.config}
onChange={(config) => {
this.setState(Object.assign({}, this.state, {config}));
this.updateUrl(configToArgs(config));
}}
/>
),
},
];
return (
<div id="content">
<ErrorPopup
@ -476,14 +528,12 @@ class AppComponent extends React.Component<
chartType={this.state.chartType}
onSelection={this.onSelection}
freezeAnimation={this.state.freezeAnimation}
colors={this.state.config.color}
ref={(ref) => (this.chartRef = ref)}
/>
{this.state.showSidePanel ? (
<Media at="large" className="sidePanel">
<Details
gedcom={this.state.data!.gedcom}
indi={this.state.selection!.id}
/>
<Tab panes={sidePanelTabs} />
</Media>
) : null}
</div>

View File

@ -1,4 +1,5 @@
import * as React from 'react';
import {ChartColors} from './config';
import {injectIntl, WrappedComponentProps} from 'react-intl';
import {interpolateNumber} from 'd3-interpolate';
import {max, min} from 'd3-array';
@ -23,6 +24,7 @@ import {
RelativesChart,
FancyChart,
CircleRenderer,
ChartColors as TopolaChartColors,
} from 'topola';
/** How much to zoom when using the +/- buttons. */
@ -140,12 +142,19 @@ export enum ChartType {
Fancy,
}
const chartColors = new Map<ChartColors, TopolaChartColors>([
[ChartColors.NO_COLOR, TopolaChartColors.NO_COLOR],
[ChartColors.COLOR_BY_GENERATION, TopolaChartColors.COLOR_BY_GENERATION],
[ChartColors.COLOR_BY_SEX, TopolaChartColors.COLOR_BY_SEX],
]);
export interface ChartProps {
data: JsonGedcomData;
selection: IndiInfo;
chartType: ChartType;
onSelection: (indiInfo: IndiInfo) => void;
freezeAnimation?: boolean;
colors?: ChartColors;
}
/** Component showing the genealogy chart and handling transition animations. */
@ -195,7 +204,12 @@ export class ChartComponent extends React.PureComponent<
* If indiInfo is not given, it means that it is the initial render and no
* animation is performed.
*/
private renderChart(args: {initialRender: boolean} = {initialRender: false}) {
private renderChart(
args: {initialRender: boolean; resetPosition: boolean} = {
initialRender: false,
resetPosition: false,
},
) {
// Wait for animation to finish if animation is in progress.
if (!args.initialRender && this.animating) {
this.rerenderRequired = true;
@ -215,6 +229,7 @@ export class ChartComponent extends React.PureComponent<
renderer: this.getRendererType(),
svgSelector: '#chart',
indiCallback: (info) => this.props.onSelection(info),
colors: chartColors.get(this.props.colors!),
animate: true,
updateSvgSize: false,
locale: this.props.intl.locale,
@ -277,13 +292,15 @@ export class ChartComponent extends React.PureComponent<
.attr('transform', `translate(${offsetX}, ${offsetY})`)
.attr('width', chartInfo.size[0] * scale)
.attr('height', chartInfo.size[1] * scale);
if (args.initialRender) {
parent.scrollLeft = -dx;
parent.scrollTop = -dy;
} else {
svgTransition
.tween('scrollLeft', scrollLeftTween(-dx))
.tween('scrollTop', scrollTopTween(-dy));
if (args.resetPosition) {
if (args.initialRender) {
parent.scrollLeft = -dx;
parent.scrollTop = -dy;
} else {
svgTransition
.tween('scrollLeft', scrollLeftTween(-dx))
.tween('scrollTop', scrollTopTween(-dy));
}
}
// After the animation is finished, rerender the chart if required.
@ -292,18 +309,21 @@ export class ChartComponent extends React.PureComponent<
this.animating = false;
if (this.rerenderRequired) {
this.rerenderRequired = false;
this.renderChart({initialRender: false});
this.renderChart({initialRender: false, resetPosition: false});
}
});
}
componentDidMount() {
this.renderChart({initialRender: true});
this.renderChart({initialRender: true, resetPosition: true});
}
componentDidUpdate(prevProps: ChartProps) {
const initialRender = this.props.chartType !== prevProps.chartType;
this.renderChart({initialRender});
const initialRender =
this.props.chartType !== prevProps.chartType ||
this.props.colors !== prevProps.colors;
const resetPosition = this.props.chartType !== prevProps.chartType;
this.renderChart({initialRender, resetPosition});
}
render() {

107
src/config.tsx Normal file
View File

@ -0,0 +1,107 @@
import {Checkbox, Form} from 'semantic-ui-react';
import {FormattedMessage} from 'react-intl';
import {ParsedQuery} from 'query-string';
export enum ChartColors {
NO_COLOR,
COLOR_BY_SEX,
COLOR_BY_GENERATION,
}
export interface Config {
color: ChartColors;
}
export const DEFALUT_CONFIG: Config = {
color: ChartColors.COLOR_BY_GENERATION,
};
const COLOR_ARG = new Map<string, ChartColors>([
['n', ChartColors.NO_COLOR],
['g', ChartColors.COLOR_BY_GENERATION],
['s', ChartColors.COLOR_BY_SEX],
]);
const COLOR_ARG_INVERSE = new Map<ChartColors, string>();
COLOR_ARG.forEach((v, k) => COLOR_ARG_INVERSE.set(v, k));
export function argsToConfig(args: ParsedQuery<any>): Config {
const getParam = (name: string) => {
const value = args[name];
return typeof value === 'string' ? value : undefined;
};
return {
color: COLOR_ARG.get(getParam('c') ?? '') ?? DEFALUT_CONFIG.color,
};
}
export function configToArgs(config: Config): ParsedQuery<any> {
return {c: COLOR_ARG_INVERSE.get(config.color)};
}
export function ConfigPanel(props: {
config: Config;
onChange: (config: Config) => void;
}) {
return (
<>
<Form className="ui segments details">
<div className="ui segment">
<div className="ui sub header">
<FormattedMessage id="config.colors" defaultMessage="Colors" />
</div>
<Form.Field className="no-margin">
<Checkbox
radio
label={
<FormattedMessage
tagName="label"
id="config.colors.NO_COLOR"
defaultMessage="none"
/>
}
name="checkboxRadioGroup"
value="none"
checked={props.config.color === ChartColors.NO_COLOR}
onClick={() => props.onChange({color: ChartColors.NO_COLOR})}
/>
</Form.Field>
<Form.Field className="no-margin">
<Checkbox
radio
label={
<FormattedMessage
tagName="label"
id="config.colors.COLOR_BY_GENERATION"
defaultMessage="by generation"
/>
}
name="checkboxRadioGroup"
value="generation"
checked={props.config.color === ChartColors.COLOR_BY_GENERATION}
onClick={() =>
props.onChange({color: ChartColors.COLOR_BY_GENERATION})
}
/>
</Form.Field>
<Form.Field className="no-margin">
<Checkbox
radio
label={
<FormattedMessage
tagName="label"
id="config.colors.COLOR_BY_SEX"
defaultMessage="by sex"
/>
}
name="checkboxRadioGroup"
value="gender"
checked={props.config.color === ChartColors.COLOR_BY_SEX}
onClick={() => props.onChange({color: ChartColors.COLOR_BY_SEX})}
/>
</Form.Field>
</div>
</Form>
</>
);
}

View File

@ -221,7 +221,7 @@ class DetailsComponent extends React.Component<
.filter(hasData);
return (
<div className="ui segments" id="details">
<div className="ui segments details">
{getDetails(entries, ['NAME'], nameDetails)}
{getDetails(entries, EVENT_TAGS, (entry) =>
eventDetails(entry, this.props.intl),

View File

@ -141,3 +141,16 @@ div.zoom {
.ui.top.attached.menu {
margin-top: 0px;
}
.ui.segments.details {
margin: 0px !important;
border: 0px !important;
}
.ui.form .field.no-margin {
margin: 0;
}
.ui.tabular.menu a {
text-transform: uppercase;
}

View File

@ -70,5 +70,11 @@
"error.WIKITREE_ID_NOT_PROVIDED": "Identyfikator WikiTree nie został podany",
"error.WIKITREE_PROFILE_NOT_ACCESSIBLE": "Profil WikiTree {id} nie jest dostępny",
"error.WIKITREE_PROFILE_NOT_FOUND": "Profil WikiTree {id} nie istnieje",
"wikitree.private": "Prywatne"
"wikitree.private": "Prywatne",
"tab.info": "Info",
"tab.settings": "Ustawienia",
"config.colors": "Kolory",
"config.colors.NO_COLOR": "brak",
"config.colors.COLOR_BY_GENERATION": "według pokolenia",
"config.colors.COLOR_BY_SEX": "według płci"
}