mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-08-01 08:31:47 +00:00
Refactoring: extracted load menus to separate files
This commit is contained in:
32
src/menu/menu_item.tsx
Normal file
32
src/menu/menu_item.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
Dropdown,
|
||||||
|
MenuItemProps,
|
||||||
|
DropdownItemProps,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
|
|
||||||
|
export enum MenuType {
|
||||||
|
Menu,
|
||||||
|
Dropdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menuType: MenuType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MenuItem extends React.Component<
|
||||||
|
Props & MenuItemProps & DropdownItemProps
|
||||||
|
> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.props.menuType === MenuType.Menu ? (
|
||||||
|
<Menu.Item {...this.props}>{this.props.children}</Menu.Item>
|
||||||
|
) : (
|
||||||
|
<Dropdown.Item {...this.props}>{this.props.children}</Dropdown.Item>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,20 @@
|
|||||||
import * as queryString from 'query-string';
|
import * as queryString from 'query-string';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import debounce from 'debounce';
|
import debounce from 'debounce';
|
||||||
import md5 from 'md5';
|
|
||||||
import wikitreeLogo from './wikitree.png';
|
|
||||||
import {analyticsEvent} from '../util/analytics';
|
import {analyticsEvent} from '../util/analytics';
|
||||||
import {buildSearchIndex, SearchIndex} from './search_index';
|
import {buildSearchIndex, SearchIndex} from './search_index';
|
||||||
import {displaySearchResult} from './search_util';
|
import {displaySearchResult} from './search_util';
|
||||||
import {FormattedMessage, intlShape} from 'react-intl';
|
import {FormattedMessage, intlShape} from 'react-intl';
|
||||||
import {getLoggedInUserName} from '../datasource/wikitree';
|
|
||||||
import {IndiInfo, JsonGedcomData} from 'topola';
|
import {IndiInfo, JsonGedcomData} from 'topola';
|
||||||
import {Link} from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
|
import {MenuType} from './menu_item';
|
||||||
import {RouteComponentProps} from 'react-router-dom';
|
import {RouteComponentProps} from 'react-router-dom';
|
||||||
|
import {UploadMenu} from './upload_menu';
|
||||||
|
import {UrlMenu} from './url_menu';
|
||||||
|
import {WikiTreeLoginMenu, WikiTreeMenu} from './wikitree_menu';
|
||||||
import {
|
import {
|
||||||
Header,
|
|
||||||
Button,
|
|
||||||
Icon,
|
Icon,
|
||||||
Menu,
|
Menu,
|
||||||
Modal,
|
|
||||||
Input,
|
|
||||||
Form,
|
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Search,
|
Search,
|
||||||
SearchProps,
|
SearchProps,
|
||||||
@@ -26,12 +22,6 @@ import {
|
|||||||
Responsive,
|
Responsive,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
|
|
||||||
enum WikiTreeLoginState {
|
|
||||||
UNKNOWN,
|
|
||||||
NOT_LOGGED_IN,
|
|
||||||
LOGGED_IN,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ScreenSize {
|
enum ScreenSize {
|
||||||
LARGE,
|
LARGE,
|
||||||
SMALL,
|
SMALL,
|
||||||
@@ -39,12 +29,6 @@ enum ScreenSize {
|
|||||||
|
|
||||||
/** Menus and dialogs state. */
|
/** Menus and dialogs state. */
|
||||||
interface State {
|
interface State {
|
||||||
loadUrlDialogOpen: boolean;
|
|
||||||
wikiTreeIdDialogOpen: boolean;
|
|
||||||
url?: string;
|
|
||||||
wikiTreeId?: string;
|
|
||||||
wikiTreeLoginState: WikiTreeLoginState;
|
|
||||||
wikiTreeLoginUsername?: string;
|
|
||||||
searchResults: SearchResultProps[];
|
searchResults: SearchResultProps[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,179 +53,21 @@ interface Props {
|
|||||||
showWikiTreeMenus: boolean;
|
showWikiTreeMenus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadFileAsText(file: File): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (evt: ProgressEvent) => {
|
|
||||||
resolve((evt.target as FileReader).result as string);
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isImageFileName(fileName: string) {
|
|
||||||
const lower = fileName.toLowerCase();
|
|
||||||
return lower.endsWith('.jpg') || lower.endsWith('.png');
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TopBar extends React.Component<
|
export class TopBar extends React.Component<
|
||||||
RouteComponentProps & Props,
|
RouteComponentProps & Props,
|
||||||
State
|
State
|
||||||
> {
|
> {
|
||||||
state: State = {
|
state: State = {
|
||||||
loadUrlDialogOpen: false,
|
|
||||||
wikiTreeIdDialogOpen: false,
|
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
wikiTreeLoginState: WikiTreeLoginState.UNKNOWN,
|
|
||||||
};
|
};
|
||||||
/** Make intl appear in this.context. */
|
/** Make intl appear in this.context. */
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
};
|
};
|
||||||
|
|
||||||
urlInputRef: React.RefObject<Input> = React.createRef();
|
|
||||||
wikiTreeIdInputRef: React.RefObject<Input> = React.createRef();
|
|
||||||
wikiTreeLoginFormRef: React.RefObject<HTMLFormElement> = React.createRef();
|
|
||||||
wikiTreeReturnUrlRef: React.RefObject<HTMLInputElement> = React.createRef();
|
|
||||||
searchRef?: {setValue(value: string): void};
|
searchRef?: {setValue(value: string): void};
|
||||||
searchIndex?: SearchIndex;
|
searchIndex?: SearchIndex;
|
||||||
|
|
||||||
/** Handles the "Upload file" button. */
|
|
||||||
private async handleUpload(event: React.SyntheticEvent<HTMLInputElement>) {
|
|
||||||
const files = (event.target as HTMLInputElement).files;
|
|
||||||
if (!files || !files.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const filesArray = Array.from(files);
|
|
||||||
(event.target as HTMLInputElement).value = ''; // Reset the file input.
|
|
||||||
analyticsEvent('upload_files_selected', {
|
|
||||||
event_value: files.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
const gedcomFile =
|
|
||||||
filesArray.length === 1
|
|
||||||
? filesArray[0]
|
|
||||||
: filesArray.find((file) => file.name.toLowerCase().endsWith('.ged')) ||
|
|
||||||
filesArray[0];
|
|
||||||
|
|
||||||
// Convert uploaded images to object URLs.
|
|
||||||
const images = filesArray
|
|
||||||
.filter(
|
|
||||||
(file) => file.name !== gedcomFile.name && isImageFileName(file.name),
|
|
||||||
)
|
|
||||||
.map((file) => ({
|
|
||||||
name: file.name,
|
|
||||||
url: URL.createObjectURL(file),
|
|
||||||
}));
|
|
||||||
const imageMap = new Map(
|
|
||||||
images.map((entry) => [entry.name, entry.url] as [string, string]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await loadFileAsText(gedcomFile);
|
|
||||||
const imageFileNames = images
|
|
||||||
.map((image) => image.name)
|
|
||||||
.sort()
|
|
||||||
.join('|');
|
|
||||||
// Hash GEDCOM contents with uploaded image file names.
|
|
||||||
const hash = md5(md5(data) + imageFileNames);
|
|
||||||
|
|
||||||
// Use history.replace() when reuploading the same file and history.push() when loading
|
|
||||||
// a new file.
|
|
||||||
const search = queryString.parse(this.props.location.search);
|
|
||||||
const historyPush =
|
|
||||||
search.file === hash
|
|
||||||
? this.props.history.replace
|
|
||||||
: this.props.history.push;
|
|
||||||
|
|
||||||
historyPush({
|
|
||||||
pathname: '/view',
|
|
||||||
search: queryString.stringify({file: hash}),
|
|
||||||
state: {data, images: imageMap},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Opens the "Load from URL" dialog. */
|
|
||||||
private openLoadUrlDialog() {
|
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {loadUrlDialogOpen: true}),
|
|
||||||
() => this.urlInputRef.current!.focus(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private openWikiTreeIdDialog() {
|
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {wikiTreeIdDialogOpen: true}),
|
|
||||||
() => this.wikiTreeIdInputRef.current!.focus(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Cancels any of the open dialogs. */
|
|
||||||
private handleClose() {
|
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {
|
|
||||||
loadUrlDialogOpen: false,
|
|
||||||
wikiTreeIdDialogOpen: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Load button clicked in the "Load from URL" dialog. */
|
|
||||||
private handleLoad() {
|
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {
|
|
||||||
loadUrlDialogOpen: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (this.state.url) {
|
|
||||||
analyticsEvent('url_selected');
|
|
||||||
this.props.history.push({
|
|
||||||
pathname: '/view',
|
|
||||||
search: queryString.stringify({url: this.state.url}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Select button clicked in the "Select WikiTree ID" dialog. */
|
|
||||||
private handleSelectWikiTreeId() {
|
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {
|
|
||||||
wikiTreeIdDialogOpen: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (this.state.wikiTreeId) {
|
|
||||||
analyticsEvent('wikitree_id_selected');
|
|
||||||
const search = queryString.parse(this.props.location.search);
|
|
||||||
const standalone =
|
|
||||||
search.standalone !== undefined ? search.standalone : true;
|
|
||||||
this.props.history.push({
|
|
||||||
pathname: '/view',
|
|
||||||
search: queryString.stringify({
|
|
||||||
indi: this.state.wikiTreeId,
|
|
||||||
source: 'wikitree',
|
|
||||||
standalone,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Called when the URL input is typed into. */
|
|
||||||
private handleUrlChange(value: string) {
|
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {
|
|
||||||
url: value,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Called when the URL input is typed into. */
|
|
||||||
private handleWikiTreeIdChange(value: string) {
|
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {
|
|
||||||
wikiTreeId: value,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** On search input change. */
|
/** On search input change. */
|
||||||
private handleSearch(input: string | undefined) {
|
private handleSearch(input: string | undefined) {
|
||||||
if (!input) {
|
if (!input) {
|
||||||
@@ -266,7 +92,7 @@ export class TopBar extends React.Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changeView(view: string) {
|
private changeView(view: string) {
|
||||||
const location = this.props.location;
|
const location = this.props.location;
|
||||||
const search = queryString.parse(location.search);
|
const search = queryString.parse(location.search);
|
||||||
if (search.view !== view) {
|
if (search.view !== view) {
|
||||||
@@ -276,192 +102,16 @@ export class TopBar extends React.Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
componentDidMount() {
|
||||||
* Redirect to the WikiTree Apps login page with a return URL pointing to
|
|
||||||
* Topola Viewer hosted on apps.wikitree.com.
|
|
||||||
*/
|
|
||||||
private wikiTreeLogin() {
|
|
||||||
const wikiTreeTopolaUrl =
|
|
||||||
'https://apps.wikitree.com/apps/wiech13/topola-viewer';
|
|
||||||
// Append '&' because the login page appends '?authcode=...' to this URL.
|
|
||||||
// TODO: remove ?authcode if it is in the current URL.
|
|
||||||
const returnUrl = `${wikiTreeTopolaUrl}${window.location.hash}&`;
|
|
||||||
this.wikiTreeReturnUrlRef.current!.value = returnUrl;
|
|
||||||
this.wikiTreeLoginFormRef.current!.submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkWikiTreeLoginState() {
|
|
||||||
const wikiTreeLoginUsername = getLoggedInUserName();
|
|
||||||
const wikiTreeLoginState = wikiTreeLoginUsername
|
|
||||||
? WikiTreeLoginState.LOGGED_IN
|
|
||||||
: WikiTreeLoginState.NOT_LOGGED_IN;
|
|
||||||
if (this.state.wikiTreeLoginState !== wikiTreeLoginState) {
|
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {
|
|
||||||
wikiTreeLoginState,
|
|
||||||
wikiTreeLoginUsername,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
this.checkWikiTreeLoginState();
|
|
||||||
this.initializeSearchIndex();
|
this.initializeSearchIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
this.checkWikiTreeLoginState();
|
|
||||||
if (prevProps.data !== this.props.data) {
|
if (prevProps.data !== this.props.data) {
|
||||||
this.initializeSearchIndex();
|
this.initializeSearchIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadFromUrlModal() {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
open={this.state.loadUrlDialogOpen}
|
|
||||||
onClose={() => this.handleClose()}
|
|
||||||
centered={false}
|
|
||||||
>
|
|
||||||
<Header>
|
|
||||||
<Icon name="cloud download" />
|
|
||||||
<FormattedMessage
|
|
||||||
id="load_from_url.title"
|
|
||||||
defaultMessage="Load from URL"
|
|
||||||
children={(txt) => txt}
|
|
||||||
/>
|
|
||||||
</Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<Form onSubmit={() => this.handleLoad()}>
|
|
||||||
<Input
|
|
||||||
placeholder="https://"
|
|
||||||
fluid
|
|
||||||
onChange={(e, data) => this.handleUrlChange(data.value)}
|
|
||||||
ref={this.urlInputRef}
|
|
||||||
/>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id="load_from_url.comment"
|
|
||||||
defaultMessage={
|
|
||||||
'Data from the URL will be loaded through {link} to avoid CORS issues.'
|
|
||||||
}
|
|
||||||
values={{
|
|
||||||
link: (
|
|
||||||
<a href="https://cors-anywhere.herokuapp.com/">
|
|
||||||
cors-anywhere.herokuapp.com
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</Form>
|
|
||||||
</Modal.Content>
|
|
||||||
<Modal.Actions>
|
|
||||||
<Button secondary onClick={() => this.handleClose()}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="load_from_url.cancel"
|
|
||||||
defaultMessage="Cancel"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Button primary onClick={() => this.handleLoad()}>
|
|
||||||
<FormattedMessage id="load_from_url.load" defaultMessage="Load" />
|
|
||||||
</Button>
|
|
||||||
</Modal.Actions>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private enterWikiTreeId(event: React.MouseEvent, id: string) {
|
|
||||||
event.preventDefault(); // Do not follow link in href.
|
|
||||||
((this.wikiTreeIdInputRef.current as unknown) as {
|
|
||||||
inputRef: HTMLInputElement;
|
|
||||||
}).inputRef.value = id;
|
|
||||||
this.handleWikiTreeIdChange(id);
|
|
||||||
this.wikiTreeIdInputRef.current!.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private wikiTreeIdModal() {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
open={this.state.wikiTreeIdDialogOpen}
|
|
||||||
onClose={() => this.handleClose()}
|
|
||||||
centered={false}
|
|
||||||
>
|
|
||||||
<Header>
|
|
||||||
<img
|
|
||||||
src={wikitreeLogo}
|
|
||||||
alt="WikiTree logo"
|
|
||||||
style={{width: '32px', height: '32px'}}
|
|
||||||
/>
|
|
||||||
<FormattedMessage
|
|
||||||
id="select_wikitree_id.title"
|
|
||||||
defaultMessage="Select WikiTree ID"
|
|
||||||
children={(txt) => txt}
|
|
||||||
/>
|
|
||||||
</Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<Form onSubmit={() => this.handleSelectWikiTreeId()}>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id="select_wikitree_id.comment"
|
|
||||||
defaultMessage={
|
|
||||||
'Enter a {wikiTreeLink} profile ID. Examples: {example1}, {example2}.'
|
|
||||||
}
|
|
||||||
values={{
|
|
||||||
wikiTreeLink: (
|
|
||||||
<a
|
|
||||||
href="https://wikitree.com/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
WikiTree
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
example1: (
|
|
||||||
<span
|
|
||||||
onClick={(e) => this.enterWikiTreeId(e, 'Wojtyla-13')}
|
|
||||||
className="link-span"
|
|
||||||
>
|
|
||||||
Wojtyla-13
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
example2: (
|
|
||||||
<span
|
|
||||||
onClick={(e) => this.enterWikiTreeId(e, 'Skłodowska-2')}
|
|
||||||
className="link-span"
|
|
||||||
>
|
|
||||||
Skłodowska-2
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<Input
|
|
||||||
fluid
|
|
||||||
onChange={(e, data) => this.handleWikiTreeIdChange(data.value)}
|
|
||||||
ref={this.wikiTreeIdInputRef}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
</Modal.Content>
|
|
||||||
<Modal.Actions>
|
|
||||||
<Button secondary onClick={() => this.handleClose()}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="select_wikitree_id.cancel"
|
|
||||||
defaultMessage="Cancel"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Button primary onClick={() => this.handleSelectWikiTreeId()}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="select_wikitree_id.load"
|
|
||||||
defaultMessage="Load"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</Modal.Actions>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private search() {
|
private search() {
|
||||||
return (
|
return (
|
||||||
<Search
|
<Search
|
||||||
@@ -641,36 +291,16 @@ export class TopBar extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fileMenus(screenSize: ScreenSize) {
|
private fileMenus(screenSize: ScreenSize) {
|
||||||
const loadWikiTreeItem = (
|
|
||||||
<>
|
|
||||||
<img src={wikitreeLogo} alt="WikiTree logo" className="menu-icon" />
|
|
||||||
<FormattedMessage
|
|
||||||
id="menu.select_wikitree_id"
|
|
||||||
defaultMessage="Select WikiTree ID"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
// In standalone WikiTree mode, show only the "Select WikiTree ID" menu.
|
// In standalone WikiTree mode, show only the "Select WikiTree ID" menu.
|
||||||
if (!this.props.standalone && this.props.showWikiTreeMenus) {
|
if (!this.props.standalone && this.props.showWikiTreeMenus) {
|
||||||
switch (screenSize) {
|
switch (screenSize) {
|
||||||
case ScreenSize.LARGE:
|
case ScreenSize.LARGE:
|
||||||
return (
|
return <WikiTreeMenu menuType={MenuType.Menu} {...this.props} />;
|
||||||
<>
|
|
||||||
<Menu.Item onClick={() => this.openWikiTreeIdDialog()}>
|
|
||||||
{loadWikiTreeItem}
|
|
||||||
</Menu.Item>
|
|
||||||
{this.wikiTreeIdModal()}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case ScreenSize.SMALL:
|
case ScreenSize.SMALL:
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dropdown.Item onClick={() => this.openWikiTreeIdDialog()}>
|
<WikiTreeMenu menuType={MenuType.Dropdown} {...this.props} />
|
||||||
{loadWikiTreeItem}
|
|
||||||
</Dropdown.Item>
|
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
{this.wikiTreeIdModal()}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -681,35 +311,6 @@ export class TopBar extends React.Component<
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const openFileItem = (
|
|
||||||
<>
|
|
||||||
<Icon name="folder open" />
|
|
||||||
<FormattedMessage id="menu.open_file" defaultMessage="Open file" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
const loadUrlItem = (
|
|
||||||
<>
|
|
||||||
<Icon name="cloud download" />
|
|
||||||
<FormattedMessage
|
|
||||||
id="menu.load_from_url"
|
|
||||||
defaultMessage="Load from URL"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
const commonElements = (
|
|
||||||
<>
|
|
||||||
{this.loadFromUrlModal()}
|
|
||||||
{this.wikiTreeIdModal()}
|
|
||||||
<input
|
|
||||||
className="hidden"
|
|
||||||
type="file"
|
|
||||||
accept=".ged,image/*"
|
|
||||||
id="fileInput"
|
|
||||||
multiple
|
|
||||||
onChange={(e) => this.handleUpload(e)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
switch (screenSize) {
|
switch (screenSize) {
|
||||||
case ScreenSize.LARGE:
|
case ScreenSize.LARGE:
|
||||||
// Show dropdown if chart is shown, otherwise show individual menu
|
// Show dropdown if chart is shown, otherwise show individual menu
|
||||||
@@ -725,51 +326,27 @@ export class TopBar extends React.Component<
|
|||||||
className="item"
|
className="item"
|
||||||
>
|
>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
<Dropdown.Item as="label" htmlFor="fileInput">
|
<UploadMenu menuType={MenuType.Dropdown} {...this.props} />
|
||||||
{openFileItem}
|
<UrlMenu menuType={MenuType.Dropdown} {...this.props} />
|
||||||
</Dropdown.Item>
|
<WikiTreeMenu menuType={MenuType.Dropdown} {...this.props} />
|
||||||
<Dropdown.Item onClick={() => this.openLoadUrlDialog()}>
|
|
||||||
{loadUrlItem}
|
|
||||||
</Dropdown.Item>
|
|
||||||
<Dropdown.Item onClick={() => this.openWikiTreeIdDialog()}>
|
|
||||||
{loadWikiTreeItem}
|
|
||||||
</Dropdown.Item>
|
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<label htmlFor="fileInput">
|
<UploadMenu menuType={MenuType.Menu} {...this.props} />
|
||||||
<Menu.Item as="a">{openFileItem}</Menu.Item>
|
<UrlMenu menuType={MenuType.Menu} {...this.props} />
|
||||||
</label>
|
<WikiTreeMenu menuType={MenuType.Menu} {...this.props} />
|
||||||
<Menu.Item onClick={() => this.openLoadUrlDialog()}>
|
|
||||||
{loadUrlItem}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item onClick={() => this.openWikiTreeIdDialog()}>
|
|
||||||
{loadWikiTreeItem}
|
|
||||||
</Menu.Item>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{menus}
|
|
||||||
{commonElements}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
return menus;
|
||||||
|
|
||||||
case ScreenSize.SMALL:
|
case ScreenSize.SMALL:
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dropdown.Item as="label" htmlFor="fileInput">
|
<UploadMenu menuType={MenuType.Dropdown} {...this.props} />
|
||||||
{openFileItem}
|
<UrlMenu menuType={MenuType.Dropdown} {...this.props} />
|
||||||
</Dropdown.Item>
|
<WikiTreeMenu menuType={MenuType.Dropdown} {...this.props} />
|
||||||
<Dropdown.Item onClick={() => this.openLoadUrlDialog()}>
|
|
||||||
{loadUrlItem}
|
|
||||||
</Dropdown.Item>
|
|
||||||
<Dropdown.Item onClick={() => this.openWikiTreeIdDialog()}>
|
|
||||||
{loadWikiTreeItem}
|
|
||||||
</Dropdown.Item>
|
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
{commonElements}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -779,112 +356,17 @@ export class TopBar extends React.Component<
|
|||||||
if (!this.props.showWikiTreeMenus) {
|
if (!this.props.showWikiTreeMenus) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
switch (this.state.wikiTreeLoginState) {
|
return (
|
||||||
case WikiTreeLoginState.NOT_LOGGED_IN:
|
<>
|
||||||
const loginForm = (
|
<WikiTreeLoginMenu
|
||||||
<form
|
menuType={
|
||||||
action="https://api.wikitree.com/api.php"
|
screenSize === ScreenSize.SMALL ? MenuType.Dropdown : MenuType.Menu
|
||||||
method="POST"
|
}
|
||||||
style={{display: 'hidden'}}
|
{...this.props}
|
||||||
ref={this.wikiTreeLoginFormRef}
|
/>
|
||||||
>
|
{screenSize === ScreenSize.SMALL ? <Dropdown.Divider /> : null}
|
||||||
<input type="hidden" name="action" value="clientLogin" />
|
</>
|
||||||
<input
|
);
|
||||||
type="hidden"
|
|
||||||
name="returnURL"
|
|
||||||
ref={this.wikiTreeReturnUrlRef}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
switch (screenSize) {
|
|
||||||
case ScreenSize.LARGE:
|
|
||||||
return (
|
|
||||||
<Menu.Item onClick={() => this.wikiTreeLogin()}>
|
|
||||||
<img
|
|
||||||
src={wikitreeLogo}
|
|
||||||
alt="WikiTree logo"
|
|
||||||
className="menu-icon"
|
|
||||||
/>
|
|
||||||
<FormattedMessage
|
|
||||||
id="menu.wikitree_login"
|
|
||||||
defaultMessage="Log in to WikiTree"
|
|
||||||
/>
|
|
||||||
{loginForm}
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
|
|
||||||
case ScreenSize.SMALL:
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Dropdown.Item onClick={() => this.wikiTreeLogin()}>
|
|
||||||
<img
|
|
||||||
src={wikitreeLogo}
|
|
||||||
alt="WikiTree logo"
|
|
||||||
className="menu-icon"
|
|
||||||
/>
|
|
||||||
<FormattedMessage
|
|
||||||
id="menu.wikitree_login"
|
|
||||||
defaultMessage="Log in to WikiTree"
|
|
||||||
/>
|
|
||||||
{loginForm}
|
|
||||||
</Dropdown.Item>
|
|
||||||
<Dropdown.Divider />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WikiTreeLoginState.LOGGED_IN:
|
|
||||||
const tooltip = this.state.wikiTreeLoginUsername
|
|
||||||
? this.context.intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: 'menu.wikitree_popup_username',
|
|
||||||
defaultMessage: 'Logged in to WikiTree as {username}',
|
|
||||||
},
|
|
||||||
{username: this.state.wikiTreeLoginUsername},
|
|
||||||
)
|
|
||||||
: this.context.intl.formatMessage({
|
|
||||||
id: 'menu.wikitree_popup',
|
|
||||||
defaultMessage: 'Logged in to WikiTree',
|
|
||||||
});
|
|
||||||
switch (screenSize) {
|
|
||||||
case ScreenSize.LARGE:
|
|
||||||
return (
|
|
||||||
<Menu.Item title={tooltip}>
|
|
||||||
<img
|
|
||||||
src={wikitreeLogo}
|
|
||||||
alt="WikiTree logo"
|
|
||||||
className="menu-icon"
|
|
||||||
/>
|
|
||||||
<FormattedMessage
|
|
||||||
id="menu.wikitree_logged_in"
|
|
||||||
defaultMessage="Logged in"
|
|
||||||
/>
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
|
|
||||||
case ScreenSize.SMALL:
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Menu.Item title={tooltip}>
|
|
||||||
<img
|
|
||||||
src={wikitreeLogo}
|
|
||||||
alt="WikiTree logo"
|
|
||||||
className="menu-icon"
|
|
||||||
/>
|
|
||||||
<FormattedMessage
|
|
||||||
id="menu.wikitree_logged_in"
|
|
||||||
defaultMessage="Logged in"
|
|
||||||
/>
|
|
||||||
</Menu.Item>
|
|
||||||
<Dropdown.Divider />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private mobileMenus() {
|
private mobileMenus() {
|
||||||
|
|||||||
113
src/menu/upload_menu.tsx
Normal file
113
src/menu/upload_menu.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import * as queryString from 'query-string';
|
||||||
|
import * as React from 'react';
|
||||||
|
import md5 from 'md5';
|
||||||
|
import {analyticsEvent} from '../util/analytics';
|
||||||
|
import {Dropdown, Icon, Menu} from 'semantic-ui-react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {MenuType} from './menu_item';
|
||||||
|
import {RouteComponentProps} from 'react-router-dom';
|
||||||
|
|
||||||
|
function loadFileAsText(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (evt: ProgressEvent) => {
|
||||||
|
resolve((evt.target as FileReader).result as string);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImageFileName(fileName: string) {
|
||||||
|
const lower = fileName.toLowerCase();
|
||||||
|
return lower.endsWith('.jpg') || lower.endsWith('.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menuType: MenuType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Displays and handles the "Open file" menu. */
|
||||||
|
export class UploadMenu extends React.Component<RouteComponentProps & Props> {
|
||||||
|
private async handleUpload(event: React.SyntheticEvent<HTMLInputElement>) {
|
||||||
|
const files = (event.target as HTMLInputElement).files;
|
||||||
|
if (!files || !files.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const filesArray = Array.from(files);
|
||||||
|
(event.target as HTMLInputElement).value = ''; // Reset the file input.
|
||||||
|
analyticsEvent('upload_files_selected', {
|
||||||
|
event_value: files.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
const gedcomFile =
|
||||||
|
filesArray.length === 1
|
||||||
|
? filesArray[0]
|
||||||
|
: filesArray.find((file) => file.name.toLowerCase().endsWith('.ged')) ||
|
||||||
|
filesArray[0];
|
||||||
|
|
||||||
|
// Convert uploaded images to object URLs.
|
||||||
|
const images = filesArray
|
||||||
|
.filter(
|
||||||
|
(file) => file.name !== gedcomFile.name && isImageFileName(file.name),
|
||||||
|
)
|
||||||
|
.map((file) => ({
|
||||||
|
name: file.name,
|
||||||
|
url: URL.createObjectURL(file),
|
||||||
|
}));
|
||||||
|
const imageMap = new Map(
|
||||||
|
images.map((entry) => [entry.name, entry.url] as [string, string]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await loadFileAsText(gedcomFile);
|
||||||
|
const imageFileNames = images
|
||||||
|
.map((image) => image.name)
|
||||||
|
.sort()
|
||||||
|
.join('|');
|
||||||
|
// Hash GEDCOM contents with uploaded image file names.
|
||||||
|
const hash = md5(md5(data) + imageFileNames);
|
||||||
|
|
||||||
|
// Use history.replace() when reuploading the same file and history.push() when loading
|
||||||
|
// a new file.
|
||||||
|
const search = queryString.parse(this.props.location.search);
|
||||||
|
const historyPush =
|
||||||
|
search.file === hash
|
||||||
|
? this.props.history.replace
|
||||||
|
: this.props.history.push;
|
||||||
|
|
||||||
|
historyPush({
|
||||||
|
pathname: '/view',
|
||||||
|
search: queryString.stringify({file: hash}),
|
||||||
|
state: {data, images: imageMap},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
<Icon name="folder open" />
|
||||||
|
<FormattedMessage id="menu.open_file" defaultMessage="Open file" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.props.menuType === MenuType.Menu ? (
|
||||||
|
<label htmlFor="fileInput">
|
||||||
|
<Menu.Item as="a">{content}</Menu.Item>
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
<Dropdown.Item as="label" htmlFor="fileInput">
|
||||||
|
{content}
|
||||||
|
</Dropdown.Item>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
className="hidden"
|
||||||
|
type="file"
|
||||||
|
accept=".ged,image/*"
|
||||||
|
id="fileInput"
|
||||||
|
multiple
|
||||||
|
onChange={(e) => this.handleUpload(e)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
140
src/menu/url_menu.tsx
Normal file
140
src/menu/url_menu.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import * as queryString from 'query-string';
|
||||||
|
import * as React from 'react';
|
||||||
|
import {analyticsEvent} from '../util/analytics';
|
||||||
|
import {Button, Form, Header, Icon, Input, Modal} from 'semantic-ui-react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {MenuItem, MenuType} from './menu_item';
|
||||||
|
import {RouteComponentProps} from 'react-router-dom';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menuType: MenuType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
dialogOpen: boolean;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Displays and handles the "Open URL" menu. */
|
||||||
|
export class UrlMenu extends React.Component<
|
||||||
|
RouteComponentProps & Props,
|
||||||
|
State
|
||||||
|
> {
|
||||||
|
state: State = {dialogOpen: false};
|
||||||
|
|
||||||
|
inputRef: React.RefObject<Input> = React.createRef();
|
||||||
|
|
||||||
|
/** Opens the "Load from URL" dialog. */
|
||||||
|
private openDialog() {
|
||||||
|
this.setState(Object.assign({}, this.state, {dialogOpen: true}), () =>
|
||||||
|
this.inputRef.current!.focus(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancels any of the open dialogs. */
|
||||||
|
private handleClose() {
|
||||||
|
this.setState(
|
||||||
|
Object.assign({}, this.state, {
|
||||||
|
dialogOpen: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load button clicked in the "Load from URL" dialog. */
|
||||||
|
private handleLoad() {
|
||||||
|
this.setState(
|
||||||
|
Object.assign({}, this.state, {
|
||||||
|
dialogOpen: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (this.state.url) {
|
||||||
|
analyticsEvent('url_selected');
|
||||||
|
this.props.history.push({
|
||||||
|
pathname: '/view',
|
||||||
|
search: queryString.stringify({url: this.state.url}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the URL input is typed into. */
|
||||||
|
private handleUrlChange(value: string) {
|
||||||
|
this.setState(
|
||||||
|
Object.assign({}, this.state, {
|
||||||
|
url: value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadFromUrlModal() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={this.state.dialogOpen}
|
||||||
|
onClose={() => this.handleClose()}
|
||||||
|
centered={false}
|
||||||
|
>
|
||||||
|
<Header>
|
||||||
|
<Icon name="cloud download" />
|
||||||
|
<FormattedMessage
|
||||||
|
id="load_from_url.title"
|
||||||
|
defaultMessage="Load from URL"
|
||||||
|
children={(txt) => txt}
|
||||||
|
/>
|
||||||
|
</Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<Form onSubmit={() => this.handleLoad()}>
|
||||||
|
<Input
|
||||||
|
placeholder="https://"
|
||||||
|
fluid
|
||||||
|
onChange={(e, data) => this.handleUrlChange(data.value)}
|
||||||
|
ref={this.inputRef}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="load_from_url.comment"
|
||||||
|
defaultMessage={
|
||||||
|
'Data from the URL will be loaded through {link} to avoid CORS issues.'
|
||||||
|
}
|
||||||
|
values={{
|
||||||
|
link: (
|
||||||
|
<a href="https://cors-anywhere.herokuapp.com/">
|
||||||
|
cors-anywhere.herokuapp.com
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</Form>
|
||||||
|
</Modal.Content>
|
||||||
|
<Modal.Actions>
|
||||||
|
<Button secondary onClick={() => this.handleClose()}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="load_from_url.cancel"
|
||||||
|
defaultMessage="Cancel"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Button primary onClick={() => this.handleLoad()}>
|
||||||
|
<FormattedMessage id="load_from_url.load" defaultMessage="Load" />
|
||||||
|
</Button>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => this.openDialog()}
|
||||||
|
menuType={this.props.menuType}
|
||||||
|
>
|
||||||
|
<Icon name="cloud download" />
|
||||||
|
<FormattedMessage
|
||||||
|
id="menu.load_from_url"
|
||||||
|
defaultMessage="Load from URL"
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
{this.loadFromUrlModal()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
311
src/menu/wikitree_menu.tsx
Normal file
311
src/menu/wikitree_menu.tsx
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
import * as queryString from 'query-string';
|
||||||
|
import * as React from 'react';
|
||||||
|
import wikitreeLogo from './wikitree.png';
|
||||||
|
import {analyticsEvent} from '../util/analytics';
|
||||||
|
import {FormattedMessage, intlShape} from 'react-intl';
|
||||||
|
import {getLoggedInUserName} from '../datasource/wikitree';
|
||||||
|
import {MenuItem, MenuType} from './menu_item';
|
||||||
|
import {RouteComponentProps} from 'react-router-dom';
|
||||||
|
import {Header, Button, Modal, Input, Form} from 'semantic-ui-react';
|
||||||
|
|
||||||
|
enum WikiTreeLoginState {
|
||||||
|
UNKNOWN,
|
||||||
|
NOT_LOGGED_IN,
|
||||||
|
LOGGED_IN,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menuType: MenuType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
dialogOpen: boolean;
|
||||||
|
wikiTreeId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Displays and handles the "Select WikiTree ID" menu. */
|
||||||
|
export class WikiTreeMenu extends React.Component<
|
||||||
|
RouteComponentProps & Props,
|
||||||
|
State
|
||||||
|
> {
|
||||||
|
state: State = {
|
||||||
|
dialogOpen: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
inputRef: React.RefObject<Input> = React.createRef();
|
||||||
|
|
||||||
|
private openDialog() {
|
||||||
|
this.setState(Object.assign({}, this.state, {dialogOpen: true}), () =>
|
||||||
|
this.inputRef.current!.focus(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancels any of the open dialogs. */
|
||||||
|
private handleClose() {
|
||||||
|
this.setState(
|
||||||
|
Object.assign({}, this.state, {
|
||||||
|
dialogOpen: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Select button clicked in the "Select WikiTree ID" dialog. */
|
||||||
|
private handleSelectId() {
|
||||||
|
this.setState(
|
||||||
|
Object.assign({}, this.state, {
|
||||||
|
dialogOpen: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (this.state.wikiTreeId) {
|
||||||
|
analyticsEvent('wikitree_id_selected');
|
||||||
|
const search = queryString.parse(this.props.location.search);
|
||||||
|
const standalone =
|
||||||
|
search.standalone !== undefined ? search.standalone : true;
|
||||||
|
this.props.history.push({
|
||||||
|
pathname: '/view',
|
||||||
|
search: queryString.stringify({
|
||||||
|
indi: this.state.wikiTreeId,
|
||||||
|
source: 'wikitree',
|
||||||
|
standalone,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the WikiTree ID input is typed into. */
|
||||||
|
private handleIdChange(value: string) {
|
||||||
|
this.setState(
|
||||||
|
Object.assign({}, this.state, {
|
||||||
|
wikiTreeId: value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enterId(event: React.MouseEvent, id: string) {
|
||||||
|
event.preventDefault(); // Do not follow link in href.
|
||||||
|
((this.inputRef.current as unknown) as {
|
||||||
|
inputRef: HTMLInputElement;
|
||||||
|
}).inputRef.value = id;
|
||||||
|
this.handleIdChange(id);
|
||||||
|
this.inputRef.current!.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private wikiTreeIdModal() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={this.state.dialogOpen}
|
||||||
|
onClose={() => this.handleClose()}
|
||||||
|
centered={false}
|
||||||
|
>
|
||||||
|
<Header>
|
||||||
|
<img
|
||||||
|
src={wikitreeLogo}
|
||||||
|
alt="WikiTree logo"
|
||||||
|
style={{width: '32px', height: '32px'}}
|
||||||
|
/>
|
||||||
|
<FormattedMessage
|
||||||
|
id="select_wikitree_id.title"
|
||||||
|
defaultMessage="Select WikiTree ID"
|
||||||
|
children={(txt) => txt}
|
||||||
|
/>
|
||||||
|
</Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<Form onSubmit={() => this.handleSelectId()}>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="select_wikitree_id.comment"
|
||||||
|
defaultMessage={
|
||||||
|
'Enter a {wikiTreeLink} profile ID. Examples: {example1}, {example2}.'
|
||||||
|
}
|
||||||
|
values={{
|
||||||
|
wikiTreeLink: (
|
||||||
|
<a
|
||||||
|
href="https://wikitree.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
WikiTree
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
example1: (
|
||||||
|
<span
|
||||||
|
onClick={(e) => this.enterId(e, 'Wojtyla-13')}
|
||||||
|
className="link-span"
|
||||||
|
>
|
||||||
|
Wojtyla-13
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
example2: (
|
||||||
|
<span
|
||||||
|
onClick={(e) => this.enterId(e, 'Skłodowska-2')}
|
||||||
|
className="link-span"
|
||||||
|
>
|
||||||
|
Skłodowska-2
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
fluid
|
||||||
|
onChange={(e, data) => this.handleIdChange(data.value)}
|
||||||
|
ref={this.inputRef}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Modal.Content>
|
||||||
|
<Modal.Actions>
|
||||||
|
<Button secondary onClick={() => this.handleClose()}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="select_wikitree_id.cancel"
|
||||||
|
defaultMessage="Cancel"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Button primary onClick={() => this.handleSelectId()}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="select_wikitree_id.load"
|
||||||
|
defaultMessage="Load"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem
|
||||||
|
menuType={this.props.menuType}
|
||||||
|
onClick={() => this.openDialog()}
|
||||||
|
>
|
||||||
|
<img src={wikitreeLogo} alt="WikiTree logo" className="menu-icon" />
|
||||||
|
<FormattedMessage
|
||||||
|
id="menu.select_wikitree_id"
|
||||||
|
defaultMessage="Select WikiTree ID"
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
{this.wikiTreeIdModal()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginState {
|
||||||
|
wikiTreeLoginState: WikiTreeLoginState;
|
||||||
|
wikiTreeLoginUsername?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Displays and handles the "Log in to WikiTree" menu. */
|
||||||
|
export class WikiTreeLoginMenu extends React.Component<
|
||||||
|
RouteComponentProps & Props,
|
||||||
|
LoginState
|
||||||
|
> {
|
||||||
|
state: LoginState = {
|
||||||
|
wikiTreeLoginState: WikiTreeLoginState.UNKNOWN,
|
||||||
|
};
|
||||||
|
/** Make intl appear in this.context. */
|
||||||
|
static contextTypes = {
|
||||||
|
intl: intlShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
wikiTreeLoginFormRef: React.RefObject<HTMLFormElement> = React.createRef();
|
||||||
|
wikiTreeReturnUrlRef: React.RefObject<HTMLInputElement> = React.createRef();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the WikiTree Apps login page with a return URL pointing to
|
||||||
|
* Topola Viewer hosted on apps.wikitree.com.
|
||||||
|
*/
|
||||||
|
private wikiTreeLogin() {
|
||||||
|
const wikiTreeTopolaUrl =
|
||||||
|
'https://apps.wikitree.com/apps/wiech13/topola-viewer';
|
||||||
|
// Append '&' because the login page appends '?authcode=...' to this URL.
|
||||||
|
// TODO: remove ?authcode if it is in the current URL.
|
||||||
|
const returnUrl = `${wikiTreeTopolaUrl}${window.location.hash}&`;
|
||||||
|
this.wikiTreeReturnUrlRef.current!.value = returnUrl;
|
||||||
|
this.wikiTreeLoginFormRef.current!.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkWikiTreeLoginState() {
|
||||||
|
const wikiTreeLoginUsername = getLoggedInUserName();
|
||||||
|
const wikiTreeLoginState = wikiTreeLoginUsername
|
||||||
|
? WikiTreeLoginState.LOGGED_IN
|
||||||
|
: WikiTreeLoginState.NOT_LOGGED_IN;
|
||||||
|
if (this.state.wikiTreeLoginState !== wikiTreeLoginState) {
|
||||||
|
this.setState(
|
||||||
|
Object.assign({}, this.state, {
|
||||||
|
wikiTreeLoginState,
|
||||||
|
wikiTreeLoginUsername,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.checkWikiTreeLoginState();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.checkWikiTreeLoginState();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
switch (this.state.wikiTreeLoginState) {
|
||||||
|
case WikiTreeLoginState.NOT_LOGGED_IN:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem
|
||||||
|
menuType={this.props.menuType}
|
||||||
|
onClick={() => this.wikiTreeLogin()}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={wikitreeLogo}
|
||||||
|
alt="WikiTree logo"
|
||||||
|
className="menu-icon"
|
||||||
|
/>
|
||||||
|
<FormattedMessage
|
||||||
|
id="menu.wikitree_login"
|
||||||
|
defaultMessage="Log in to WikiTree"
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
<form
|
||||||
|
action="https://api.wikitree.com/api.php"
|
||||||
|
method="POST"
|
||||||
|
style={{display: 'hidden'}}
|
||||||
|
ref={this.wikiTreeLoginFormRef}
|
||||||
|
>
|
||||||
|
<input type="hidden" name="action" value="clientLogin" />
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="returnURL"
|
||||||
|
ref={this.wikiTreeReturnUrlRef}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case WikiTreeLoginState.LOGGED_IN:
|
||||||
|
const tooltip = this.state.wikiTreeLoginUsername
|
||||||
|
? this.context.intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'menu.wikitree_popup_username',
|
||||||
|
defaultMessage: 'Logged in to WikiTree as {username}',
|
||||||
|
},
|
||||||
|
{username: this.state.wikiTreeLoginUsername},
|
||||||
|
)
|
||||||
|
: this.context.intl.formatMessage({
|
||||||
|
id: 'menu.wikitree_popup',
|
||||||
|
defaultMessage: 'Logged in to WikiTree',
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<MenuItem menuType={this.props.menuType} title={tooltip}>
|
||||||
|
<img src={wikitreeLogo} alt="WikiTree logo" className="menu-icon" />
|
||||||
|
<FormattedMessage
|
||||||
|
id="menu.wikitree_logged_in"
|
||||||
|
defaultMessage="Logged in"
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user