From 8e33f92a043fa70a4d99b8c6f0d5c9fa23320f64 Mon Sep 17 00:00:00 2001 From: Przemek Wiech Date: Sun, 2 Feb 2020 00:11:14 +0100 Subject: [PATCH] Added WikiTree login button --- package-lock.json | 10 ++ package.json | 2 + src/app.tsx | 4 +- src/top_bar.tsx | 237 +++++++++++++++++++++++++++++---------- src/translations/pl.json | 4 + src/wikitree.ts | 33 +++++- 6 files changed, 229 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index b5cdca7..8797cfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1784,6 +1784,11 @@ "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", "dev": true }, + "@types/js-cookie": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.4.tgz", + "integrity": "sha512-WTfSE1Eauak/Nrg6cA9FgPTFvVawejsai6zXoq0QYTQ3mxONeRtGhKxa7wMlUzWWmzrmTeV+rwLjHgsCntdrsA==" + }, "@types/jspdf": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/jspdf/-/jspdf-1.2.2.tgz", @@ -9354,6 +9359,11 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", diff --git a/package.json b/package.json index 7328159..478f9d4 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "main": "src/index.tsx", "dependencies": { + "@types/js-cookie": "^2.2.4", "array.prototype.flatmap": "^1.2.1", "canvas-toBlob": "^1.0.0", "d3": "^5.7.0", @@ -11,6 +12,7 @@ "file-saver": "^2.0.1", "history": "^4.7.2", "javascript-natural-sort": "^0.7.1", + "js-cookie": "^2.2.1", "jspdf": "^1.5.3", "lunr": "^2.3.6", "md5": "^2.2.1", diff --git a/src/app.tsx b/src/app.tsx index 1081ea3..108dbe7 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -247,6 +247,7 @@ export class App extends React.Component { const standalone = getParam('standalone') !== 'false'; // True by default. const view = getParam('view'); const source = getParam('source'); + const authcode = getParam('?authcode'); const chartTypes = new Map([ ['relatives', ChartType.Relatives], @@ -278,7 +279,7 @@ export class App extends React.Component { ); const data = source === 'wikitree' - ? await loadWikiTree(indi!) + ? await loadWikiTree(indi!, authcode) : hash ? await loadGedcom(hash, gedcom, images) : await loadFromUrl(url!, handleCors); @@ -485,6 +486,7 @@ export class App extends React.Component { onDownloadPng: this.onDownloadPng, onDownloadSvg: this.onDownloadSvg, }} + showWikiTreeLogin={this.state.wikiTreeSource} /> )} /> diff --git a/src/top_bar.tsx b/src/top_bar.tsx index 651bdab..95c9969 100644 --- a/src/top_bar.tsx +++ b/src/top_bar.tsx @@ -1,5 +1,6 @@ import * as queryString from 'query-string'; import * as React from 'react'; +import Cookies from 'js-cookie'; import debounce from 'debounce'; import md5 from 'md5'; import {analyticsEvent} from './analytics'; @@ -24,10 +25,18 @@ import { SearchResultProps, } from 'semantic-ui-react'; +enum WikiTreeLoginState { + UNKNOWN, + NOT_LOGGED_IN, + LOGGED_IN, +} + /** Menus and dialogs state. */ interface State { loadUrlDialogOpen: boolean; url?: string; + wikiTreeLoginState: WikiTreeLoginState; + wikiTreeLoginUsername?: string; searchResults: SearchResultProps[]; } @@ -48,6 +57,8 @@ interface Props { /** Whether to show the "All relatives" chart type in the menu. */ allowAllRelativesChart: boolean; eventHandlers: EventHandlers; + /** Whether to show the 'Log in to WikiTree' button. */ + showWikiTreeLogin: boolean; } function loadFileAsText(file: File): Promise { @@ -72,13 +83,21 @@ export class TopBar extends React.Component< state: State = { loadUrlDialogOpen: false, searchResults: [], + wikiTreeLoginState: WikiTreeLoginState.UNKNOWN, }; - inputRef?: Input; + /** Make intl appear in this.context. */ + static contextTypes = { + intl: intlShape, + }; + + urlInputRef: React.RefObject = React.createRef(); + wikiTreeLoginFormRef: React.RefObject = React.createRef(); + wikiTreeReturnUrlRef: React.RefObject = React.createRef(); searchRef?: {setValue(value: string): void}; searchIndex?: SearchIndex; /** Handles the "Upload file" button. */ - async handleUpload(event: React.SyntheticEvent) { + private async handleUpload(event: React.SyntheticEvent) { const files = (event.target as HTMLInputElement).files; if (!files || !files.length) { return; @@ -132,20 +151,20 @@ export class TopBar extends React.Component< } /** Opens the "Load from URL" dialog. */ - handleLoadFromUrl() { + private openLoadUrlDialog() { this.setState( Object.assign({}, this.state, {loadUrlDialogOpen: true}), - () => this.inputRef!.focus(), + () => this.urlInputRef.current!.focus(), ); } /** Cancels the "Load from URL" dialog. */ - handleClose() { + private handleClose() { this.setState(Object.assign({}, this.state, {loadUrlDialogOpen: false})); } /** Upload button clicked in the "Load from URL" dialog. */ - handleLoad() { + private handleLoad() { this.setState( Object.assign({}, this.state, { loadUrlDialogOpen: false, @@ -161,7 +180,7 @@ export class TopBar extends React.Component< } /** Called when the URL input is typed into. */ - handleUrlChange(event: React.SyntheticEvent) { + private handleUrlChange(event: React.SyntheticEvent) { this.setState( Object.assign({}, this.state, { url: (event.target as HTMLInputElement).value, @@ -170,7 +189,7 @@ export class TopBar extends React.Component< } /** On search input change. */ - handleSearch(input: string | undefined) { + private handleSearch(input: string | undefined) { if (!input) { return; } @@ -181,13 +200,13 @@ export class TopBar extends React.Component< } /** On search result selected. */ - handleResultSelect(id: string) { + private handleResultSelect(id: string) { analyticsEvent('search_result_selected'); this.props.eventHandlers.onSelection({id, generation: 0}); this.searchRef!.setValue(''); } - initializeSearchIndex() { + private initializeSearchIndex() { if (this.props.gedcom) { this.searchIndex = buildSearchIndex(this.props.gedcom); } @@ -203,23 +222,50 @@ 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 wikiTreeLoginState = + Cookies.get('wikidb_wtb_UserID') !== undefined + ? WikiTreeLoginState.LOGGED_IN + : WikiTreeLoginState.NOT_LOGGED_IN; + if (this.state.wikiTreeLoginState !== wikiTreeLoginState) { + const wikiTreeLoginUsername = Cookies.get('wikidb_wtb_UserName'); + this.setState( + Object.assign({}, this.state, { + wikiTreeLoginState, + wikiTreeLoginUsername, + }), + ); + } + } + + async componentDidMount() { + this.checkWikiTreeLoginState(); this.initializeSearchIndex(); } componentDidUpdate(prevProps: Props) { + this.checkWikiTreeLoginState(); if (prevProps.gedcom !== this.props.gedcom) { this.initializeSearchIndex(); } } - /** Make intl appear in this.context. */ - static contextTypes = { - intl: intlShape, - }; - - render() { - const loadFromUrlModal = ( + private loadFromUrlModal() { + return ( this.handleClose()} @@ -239,7 +285,7 @@ export class TopBar extends React.Component< placeholder="https://" fluid onChange={(e) => this.handleUrlChange(e)} - ref={(ref) => (this.inputRef = ref!)} + ref={this.urlInputRef} />

); + } - const chartMenus = this.props.showingChart ? ( + private chartMenus() { + if (!this.props.showingChart) { + return null; + } + return ( <> this.props.eventHandlers.onPrint()}> @@ -367,16 +418,21 @@ export class TopBar extends React.Component< } /> - ) : null; + ); + } - const fileMenus = this.props.standalone ? ( + private fileMenus() { + if (!this.props.standalone) { + return null; + } + return ( <> Topola Genealogy - this.handleLoadFromUrl()}> + this.openLoadUrlDialog()}> - ) : null; - - const sourceLink = this.props.standalone ? ( - <> - - - - - ) : ( - <> - - - - ); + } + private wikiTreeLoginMenu() { + if (!this.props.showWikiTreeLogin) { + return null; + } + const wikiTreeLogoUrl = + 'https://www.wikitree.com/photo.php/a/a5/WikiTree_Images.png'; + switch (this.state.wikiTreeLoginState) { + case WikiTreeLoginState.NOT_LOGGED_IN: + return ( + this.wikiTreeLogin()}> + WikiTree logo + +

+ + +
+ + ); + 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 ( + + WikiTree logo + + + ); + default: + return null; + } + } + + private sourceLink() { + return ( + + + + ); + } + + private poweredByLink() { + return ( + + + + ); + } + + render() { return ( - {fileMenus} - {chartMenus} - {sourceLink} - {loadFromUrlModal} + {this.fileMenus()} + {this.chartMenus()} + + {this.wikiTreeLoginMenu()} + {this.props.standalone ? this.sourceLink() : this.poweredByLink()} + + {this.loadFromUrlModal()} ); } diff --git a/src/translations/pl.json b/src/translations/pl.json index cc72908..91968f2 100644 --- a/src/translations/pl.json +++ b/src/translations/pl.json @@ -10,6 +10,10 @@ "menu.hourglass": "Wykres klepsydrowy", "menu.relatives": "Wszyscy krewni", "menu.fancy": "Ozdobne drzewo (eksperymentalne)", + "menu.wikitree_login": "Zaloguj do WikiTree", + "menu.wikitree_logged_in": "Zalogowano", + "menu.wikitree_popup_username": "Zalogowano do WikiTree jako {username}", + "menu.wikitree_popup": "Zalogowano do WikiTree", "menu.github": "Źródła na GitHub", "menu.powered_by": "Topola Genealogy", "menu.search.placeholder": "Szukaj osoby", diff --git a/src/wikitree.ts b/src/wikitree.ts index 25ba5b4..ace2631 100644 --- a/src/wikitree.ts +++ b/src/wikitree.ts @@ -1,5 +1,6 @@ -import {GedcomData, TopolaData} from './gedcom_util'; +import Cookies from 'js-cookie'; import {Date, JsonFam, JsonIndi} from 'topola'; +import {GedcomData, TopolaData} from './gedcom_util'; import {GedcomEntry} from 'parse-gedcom'; /** WikiTree API getAncestors request. */ @@ -17,7 +18,12 @@ interface GetRelatives { getSpouses?: true; } -type WikiTreeRequest = GetAncestorsRequest | GetRelatives; +interface ClientLogin { + action: 'clientLogin'; + authcode: string; +} + +type WikiTreeRequest = GetAncestorsRequest | GetRelatives | ClientLogin; /** Person structure returned from WikiTree API. */ interface Person { @@ -128,14 +134,35 @@ async function getRelatives(keys: string[], handleCors: boolean) { return result.concat(fetchedResults); } +export async function clientLogin(authcode: string) { + const response = await wikiTreeGet( + { + action: 'clientLogin', + authcode, + }, + false, + ); + return response.clientLogin; +} + /** * Loads data from WikiTree to populate an hourglass chart starting from the * given person ID. */ -export async function loadWikiTree(key: string): Promise { +export async function loadWikiTree( + key: string, + authcode?: string, +): Promise { // Work around CORS if not in apps.wikitree.com domain. const handleCors = window.location.hostname !== 'apps.wikitree.com'; + if (!handleCors && !Cookies.get('wikidb_wtb_UserID') && authcode) { + const loginResult = await clientLogin(authcode); + if (loginResult.result === 'Success') { + sessionStorage.clear(); + } + } + const everyone: Person[] = []; // Fetch the ancestors of the input person and ancestors of his/her spouses.