Show changelog in intro page and when a new version is loaded

This commit is contained in:
Przemek Wiech 2021-10-31 19:52:53 +01:00
parent 2946d20c85
commit 98b1a1e48e
9 changed files with 1889 additions and 58 deletions

View File

@ -1,8 +1,12 @@
# Changelog
## 2021-10-31
- Show changelog in intro page and when an upgraded version is loaded
## 2021-10-27
- Showing events in details panel for WikiTree profiles
- Show events in details panel for WikiTree profiles
## 2021-10-26

View File

@ -3,7 +3,7 @@ describe('Intro page', () => {
cy.visit('/');
});
it('displays intro text', () => {
cy.contains('Here are some examples');
cy.contains('Examples');
});
it('displays menu', () => {
cy.contains('Open file');

1728
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,9 +27,13 @@
"react-intl": "^5.15.5",
"react-linkify": "^0.2.2",
"react-router-dom": "^5.2.0",
"rehype-stringify": "^9.0.2",
"remark-parse": "^10.0.0",
"remark-rehype": "^10.0.0",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^2.0.3",
"topola": "^3.5.0"
"topola": "^3.5.0",
"unified": "^10.1.0"
},
"devDependencies": {
"@types/array.prototype.flatmap": "^1.2.2",
@ -59,8 +63,8 @@
"typescript": "^4.2.3"
},
"scripts": {
"start": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` REACT_APP_GIT_TIME=`git log -1 --format=%ci` react-scripts start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` REACT_APP_GIT_TIME=`git log -1 --format=%ci` react-scripts build",
"start": "REACT_APP_CHANGELOG=`cat CHANGELOG.md` REACT_APP_GIT_SHA=`git rev-parse --short HEAD` REACT_APP_GIT_TIME=`git log -1 --format=%ci` react-scripts start",
"build": "REACT_APP_CHANGELOG=`cat CHANGELOG.md` REACT_APP_GIT_SHA=`git rev-parse --short HEAD` REACT_APP_GIT_TIME=`git log -1 --format=%ci` react-scripts build",
"test": "react-scripts test --env=jsdom",
"prettier": "prettier --write src/**/*.{ts,tsx,json} && prettier --write src/*.{ts,tsx,json}",
"predeploy": "npm run build",

View File

@ -2,14 +2,8 @@ import * as H from 'history';
import * as queryString from 'query-string';
import React from 'react';
import {analyticsEvent} from './util/analytics';
import {Changelog} from './changelog';
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/details';
import {EmbeddedDataSource, EmbeddedSourceSpec} from './datasource/embedded';
@ -22,6 +16,13 @@ import {Media} from './util/media';
import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
import {TopBar} from './menu/top_bar';
import {TopolaData} from './util/gedcom_util';
import {
argsToConfig,
Config,
ConfigPanel,
configToArgs,
DEFALUT_CONFIG,
} from './config';
import {
getSelection,
UploadSourceSpec,
@ -536,6 +537,7 @@ class AppComponent extends React.Component<
<Tab panes={sidePanelTabs} />
</Media>
) : null}
<Changelog />
</div>
);

93
src/changelog.tsx Normal file
View File

@ -0,0 +1,93 @@
import rehypeStringify from 'rehype-stringify';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import {Button, Header, Modal} from 'semantic-ui-react';
import {unified} from 'unified';
import {useEffect, useState} from 'react';
import {FormattedMessage} from 'react-intl';
const LAST_SEEN_VERSION_KEY = 'last_seen_version';
/**
* Returns changelog as raw HTML.
*
* @param maxVersions Max number of versions to include in changelog
* @param seenVersion Last seen app version
*/
export async function getChangelog(maxVersions: number, seenVersion?: string) {
const seenVersionDate = seenVersion
? Date.parse(seenVersion.slice(0, 10))
: 0;
const changes = process.env
.REACT_APP_CHANGELOG!.split('##')
.slice(1, maxVersions + 1)
.map((notes) => {
const date = Date.parse(notes.split('\n')[0].trim());
return {date, notes: '####' + notes};
})
.filter((release) => release.date > seenVersionDate)
.map((release) => release.notes)
.join('\n');
const parsedChanges = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeStringify)
.process(changes);
return String(parsedChanges);
}
/** Stores in local storage the current app version as the last seen version. */
export function updateSeenVersion() {
localStorage.setItem(LAST_SEEN_VERSION_KEY, process.env.REACT_APP_GIT_TIME!);
}
/**
* Shows changelog entries if the user has seen an older version of
* Topola Viewer and is now seeing a newer one.
*/
export function Changelog() {
const [open, setOpen] = useState(false);
const [changelog, setChangelog] = useState('');
useEffect(() => {
(async () => {
const seenVersion = localStorage.getItem(LAST_SEEN_VERSION_KEY);
const currentVersion = process.env.REACT_APP_GIT_TIME!;
if (!seenVersion || seenVersion === currentVersion) {
return;
}
const changes = await getChangelog(3, seenVersion);
setChangelog(changes);
setOpen(!!changes);
updateSeenVersion();
})();
});
return (
<Modal open={open} centered={false}>
<Header>
<FormattedMessage
id="whats_new.title"
defaultMessage="What's new in this version?"
/>
</Header>
<Modal.Content className="limit-height">
<span dangerouslySetInnerHTML={{__html: changelog}} />
<a href="https://github.com/PeWu/topola-viewer/blob/master/CHANGELOG.md">
<FormattedMessage
id="intro.full_changelog"
defaultMessage="See full changelog"
/>
</a>
</Modal.Content>
<Modal.Actions>
<Button primary onClick={() => setOpen(false)}>
Close
</Button>
</Modal.Actions>
</Modal>
);
}

View File

@ -154,3 +154,8 @@ div.zoom {
.ui.tabular.menu a {
text-transform: uppercase;
}
.limit-height {
height: 300px;
overflow-y: scroll;
}

View File

@ -1,10 +1,11 @@
import * as queryString from 'query-string';
import * as React from 'react';
import {useEffect, useState} from 'react';
import logo from './topola.jpg';
import {Card, Grid, Image} from 'semantic-ui-react';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {Media} from './util/media';
import {getChangelog, updateSeenVersion} from './changelog';
/** Link that loads a GEDCOM file from URL. */
function GedcomLink(props: {url: string; text: string}) {
@ -21,9 +22,16 @@ function formatBuildDate(dateString: string) {
return dateString.slice(0, 16);
}
/** The intro page. */
export function Intro() {
const contents = (
function Contents() {
const [changelog, setChangelog] = useState('');
useEffect(() => {
(async () => {
setChangelog(await getChangelog(1));
updateSeenVersion();
})();
});
return (
<>
<p>
<FormattedMessage
@ -44,14 +52,10 @@ export function Intro() {
}
/>
</p>
<p>
<FormattedMessage
id="intro.examples"
defaultMessage={
'Here are some examples from the web that you can view:'
}
/>
</p>
<h3>
<FormattedMessage id="intro.examples" defaultMessage="Examples" />
</h3>
<ul>
<li>
<GedcomLink
@ -76,27 +80,35 @@ export function Intro() {
)
</li>
</ul>
<p>
<b>
<FormattedMessage id="intro.privacy" defaultMessage="Privacy" />
</b>
{': '}
<h3>
<FormattedMessage id="intro.whats_new" defaultMessage="What's new" />
</h3>
<span dangerouslySetInnerHTML={{__html: changelog}} />
<a href="https://github.com/PeWu/topola-viewer/blob/master/CHANGELOG.md">
<FormattedMessage
id="intro.privacy_note"
defaultMessage={
'When using the "load from file" option, this site does not' +
' send your data anywhere and files loaded from disk do not' +
' leave your computer. When using "load from URL", data is' +
' passed through the {link} service to deal with an issue with' +
' cross-site file loading in the browser (CORS).'
}
values={{
link: (
<a href="https://topola-cors.herokuapp.com/">cors-anywhere</a>
),
}}
id="intro.full_changelog"
defaultMessage="See full changelog"
/>
</p>
</a>
<h3>
<FormattedMessage id="intro.privacy" defaultMessage="Privacy" />
</h3>
<FormattedMessage
id="intro.privacy_note"
defaultMessage={
'When using the "load from file" option, this site does not' +
' send your data anywhere and files loaded from disk do not' +
' leave your computer. When using "load from URL", data is' +
' passed through the {link} service to deal with an issue with' +
' cross-site file loading in the browser (CORS).'
}
values={{
link: <a href="https://topola-cors.herokuapp.com/">cors-anywhere</a>,
}}
/>
<p className="ui right aligned version">
version: {formatBuildDate(process.env.REACT_APP_GIT_TIME!)} (
<a
@ -108,7 +120,10 @@ export function Intro() {
</p>
</>
);
}
/** The intro page. */
export function Intro() {
return (
<div id="content">
<div className="backgroundImage" />
@ -127,7 +142,9 @@ export function Intro() {
<Grid.Column width={5}>
<Image src={logo} alt="Topola logo" />
</Grid.Column>
<Grid.Column width={11}>{contents}</Grid.Column>
<Grid.Column width={11}>
<Contents />
</Grid.Column>
</Grid.Row>
</Grid>
<Media at="small">
@ -138,7 +155,7 @@ export function Intro() {
size="tiny"
className="blockImage"
/>
{contents}
<Contents />
</Media>
</Card.Content>
</Card>

View File

@ -25,8 +25,10 @@
"intro.title": "Topola Genealogy",
"intro.description": "Topola Genealogy pozwala przeglądać drzewo genealogiczne w interaktywny sposób.",
"intro.instructions": "Kliknij OTWÓRZ PLIK lub OTWÓRZ URL, aby załadować plik GEDCOM. Większość programów genealogicznych posiada funkcję eksportu do pliku GEDCOM.",
"intro.examples": "Poniżej jest kilka przykładów znalezionych w Internecie:",
"intro.examples": "Przykłady",
"intro.from": "źródło:",
"intro.whats_new": "Co nowego?",
"intro.full_changelog": "Zobacz pełną listę zmian",
"intro.privacy": "Prywatność",
"intro.privacy_note": "Używając funkcji \"Otwórz plik\", Twoje dane nie są nigdzie wysyłane i pozostają na Twoim komputerze. Używając funkcji \"Otwórz URL\", dane z podanego adresu przesyłane są przez usługę {link} w celu umożliwienia załadowania danych z innej domeny (CORS).",
"load_from_url.title": "Otwórz z adresu URL",