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 # Changelog
## 2021-10-31
- Show changelog in intro page and when an upgraded version is loaded
## 2021-10-27 ## 2021-10-27
- Showing events in details panel for WikiTree profiles - Show events in details panel for WikiTree profiles
## 2021-10-26 ## 2021-10-26

View File

@@ -3,7 +3,7 @@ describe('Intro page', () => {
cy.visit('/'); cy.visit('/');
}); });
it('displays intro text', () => { it('displays intro text', () => {
cy.contains('Here are some examples'); cy.contains('Examples');
}); });
it('displays menu', () => { it('displays menu', () => {
cy.contains('Open file'); 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-intl": "^5.15.5",
"react-linkify": "^0.2.2", "react-linkify": "^0.2.2",
"react-router-dom": "^5.2.0", "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-css": "^2.4.1",
"semantic-ui-react": "^2.0.3", "semantic-ui-react": "^2.0.3",
"topola": "^3.5.0" "topola": "^3.5.0",
"unified": "^10.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/array.prototype.flatmap": "^1.2.2", "@types/array.prototype.flatmap": "^1.2.2",
@@ -59,8 +63,8 @@
"typescript": "^4.2.3" "typescript": "^4.2.3"
}, },
"scripts": { "scripts": {
"start": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` REACT_APP_GIT_TIME=`git log -1 --format=%ci` react-scripts start", "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_GIT_SHA=`git rev-parse --short HEAD` REACT_APP_GIT_TIME=`git log -1 --format=%ci` react-scripts build", "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", "test": "react-scripts test --env=jsdom",
"prettier": "prettier --write src/**/*.{ts,tsx,json} && prettier --write src/*.{ts,tsx,json}", "prettier": "prettier --write src/**/*.{ts,tsx,json} && prettier --write src/*.{ts,tsx,json}",
"predeploy": "npm run build", "predeploy": "npm run build",

View File

@@ -2,14 +2,8 @@ import * as H from 'history';
import * as queryString from 'query-string'; import * as queryString from 'query-string';
import React from 'react'; import React from 'react';
import {analyticsEvent} from './util/analytics'; import {analyticsEvent} from './util/analytics';
import {Changelog} from './changelog';
import {Chart, ChartComponent, ChartType} from './chart'; import {Chart, ChartComponent, ChartType} from './chart';
import {
argsToConfig,
Config,
ConfigPanel,
configToArgs,
DEFALUT_CONFIG,
} from './config';
import {DataSourceEnum, SourceSelection} from './datasource/data_source'; import {DataSourceEnum, SourceSelection} from './datasource/data_source';
import {Details} from './details/details'; import {Details} from './details/details';
import {EmbeddedDataSource, EmbeddedSourceSpec} from './datasource/embedded'; 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 {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
import {TopBar} from './menu/top_bar'; import {TopBar} from './menu/top_bar';
import {TopolaData} from './util/gedcom_util'; import {TopolaData} from './util/gedcom_util';
import {
argsToConfig,
Config,
ConfigPanel,
configToArgs,
DEFALUT_CONFIG,
} from './config';
import { import {
getSelection, getSelection,
UploadSourceSpec, UploadSourceSpec,
@@ -536,6 +537,7 @@ class AppComponent extends React.Component<
<Tab panes={sidePanelTabs} /> <Tab panes={sidePanelTabs} />
</Media> </Media>
) : null} ) : null}
<Changelog />
</div> </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 { .ui.tabular.menu a {
text-transform: uppercase; text-transform: uppercase;
} }
.limit-height {
height: 300px;
overflow-y: scroll;
}

View File

@@ -1,10 +1,11 @@
import * as queryString from 'query-string'; import * as queryString from 'query-string';
import * as React from 'react'; import {useEffect, useState} from 'react';
import logo from './topola.jpg'; import logo from './topola.jpg';
import {Card, Grid, Image} from 'semantic-ui-react'; import {Card, Grid, Image} from 'semantic-ui-react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import {Media} from './util/media'; import {Media} from './util/media';
import {getChangelog, updateSeenVersion} from './changelog';
/** Link that loads a GEDCOM file from URL. */ /** Link that loads a GEDCOM file from URL. */
function GedcomLink(props: {url: string; text: string}) { function GedcomLink(props: {url: string; text: string}) {
@@ -21,9 +22,16 @@ function formatBuildDate(dateString: string) {
return dateString.slice(0, 16); return dateString.slice(0, 16);
} }
/** The intro page. */ function Contents() {
export function Intro() { const [changelog, setChangelog] = useState('');
const contents = ( useEffect(() => {
(async () => {
setChangelog(await getChangelog(1));
updateSeenVersion();
})();
});
return (
<> <>
<p> <p>
<FormattedMessage <FormattedMessage
@@ -44,14 +52,10 @@ export function Intro() {
} }
/> />
</p> </p>
<p>
<FormattedMessage <h3>
id="intro.examples" <FormattedMessage id="intro.examples" defaultMessage="Examples" />
defaultMessage={ </h3>
'Here are some examples from the web that you can view:'
}
/>
</p>
<ul> <ul>
<li> <li>
<GedcomLink <GedcomLink
@@ -76,27 +80,35 @@ export function Intro() {
) )
</li> </li>
</ul> </ul>
<p>
<b> <h3>
<FormattedMessage id="intro.privacy" defaultMessage="Privacy" /> <FormattedMessage id="intro.whats_new" defaultMessage="What's new" />
</b> </h3>
{': '} <span dangerouslySetInnerHTML={{__html: changelog}} />
<a href="https://github.com/PeWu/topola-viewer/blob/master/CHANGELOG.md">
<FormattedMessage <FormattedMessage
id="intro.privacy_note" id="intro.full_changelog"
defaultMessage={ defaultMessage="See full changelog"
'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> </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"> <p className="ui right aligned version">
version: {formatBuildDate(process.env.REACT_APP_GIT_TIME!)} ( version: {formatBuildDate(process.env.REACT_APP_GIT_TIME!)} (
<a <a
@@ -108,7 +120,10 @@ export function Intro() {
</p> </p>
</> </>
); );
}
/** The intro page. */
export function Intro() {
return ( return (
<div id="content"> <div id="content">
<div className="backgroundImage" /> <div className="backgroundImage" />
@@ -127,7 +142,9 @@ export function Intro() {
<Grid.Column width={5}> <Grid.Column width={5}>
<Image src={logo} alt="Topola logo" /> <Image src={logo} alt="Topola logo" />
</Grid.Column> </Grid.Column>
<Grid.Column width={11}>{contents}</Grid.Column> <Grid.Column width={11}>
<Contents />
</Grid.Column>
</Grid.Row> </Grid.Row>
</Grid> </Grid>
<Media at="small"> <Media at="small">
@@ -138,7 +155,7 @@ export function Intro() {
size="tiny" size="tiny"
className="blockImage" className="blockImage"
/> />
{contents} <Contents />
</Media> </Media>
</Card.Content> </Card.Content>
</Card> </Card>

View File

@@ -25,8 +25,10 @@
"intro.title": "Topola Genealogy", "intro.title": "Topola Genealogy",
"intro.description": "Topola Genealogy pozwala przeglądać drzewo genealogiczne w interaktywny sposób.", "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.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.from": "źródło:",
"intro.whats_new": "Co nowego?",
"intro.full_changelog": "Zobacz pełną listę zmian",
"intro.privacy": "Prywatność", "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).", "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", "load_from_url.title": "Otwórz z adresu URL",