finished history toolbar buttons (#117)

* added history toolbar functionality
* export now adds a version number
* renamed OldHistoryEntry to V0HistoryEntry

Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
Philip Molares 2020-06-04 22:41:44 +02:00 committed by GitHub
parent d7fdb73814
commit 0d8ca681f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 197 additions and 33 deletions

View file

@ -33,6 +33,12 @@
"title": "Delete history",
"question": "Do you want to clear the history?",
"disclaimer": "This won't delete any notes."
},
"importHistoryError": {
"title": "An error occurred",
"textWithFile": "While trying to import history from '{{fileName}}' an error occurred.",
"textWithoutFile": "You did not provide any files to upload the history from.",
"tooNewVersion": "The file '{{fileName}}' comes from a newer client and can't be imported."
}
},
"tableHeader": {

View file

@ -0,0 +1,18 @@
import React from 'react'
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon'
export interface ExportHistoryButtonProps {
onExportHistory: () => void
}
export const ExportHistoryButton: React.FC<ExportHistoryButtonProps> = ({ onExportHistory }) => {
const { t } = useTranslation()
return (
<Button variant={'light'} title={t('landing.history.toolbar.export')} onClick={onExportHistory}>
<ForkAwesomeIcon icon='download'/>
</Button>
)
}

View file

@ -1,11 +1,14 @@
import React, { ChangeEvent, useEffect, useState } from 'react'
import { Button, Form, FormControl, InputGroup, ToggleButton, ToggleButtonGroup } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { SortButton, SortModeEnum } from '../../../../sort-button/sort-button'
import { Typeahead } from 'react-bootstrap-typeahead'
import './typeahead-hacks.scss'
import { ClearHistoryButton } from './clear-history-button'
import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon'
import { SortButton, SortModeEnum } from '../../../../sort-button/sort-button'
import { HistoryEntry } from '../history'
import { ClearHistoryButton } from './clear-history-button'
import { ExportHistoryButton } from './export-history-button'
import { ImportHistoryButton } from './import-history-button'
import './typeahead-hacks.scss'
export type HistoryToolbarChange = (settings: HistoryToolbarState) => void;
@ -26,6 +29,9 @@ export interface HistoryToolbarProps {
onSettingsChange: HistoryToolbarChange
tags: string[]
onClearHistory: () => void
onRefreshHistory: () => void
onExportHistory: () => void
onImportHistory: (entries: HistoryEntry[]) => void
}
export const initState: HistoryToolbarState = {
@ -36,7 +42,7 @@ export const initState: HistoryToolbarState = {
selectedTags: []
}
export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange, tags, onClearHistory }) => {
export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange, tags, onClearHistory, onRefreshHistory, onExportHistory, onImportHistory }) => {
const [t] = useTranslation()
const [state, setState] = useState<HistoryToolbarState>(initState)
@ -94,20 +100,16 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
variant={'light'}><Trans i18nKey={'landing.history.toolbar.sortByLastVisited'}/></SortButton>
</InputGroup>
<InputGroup className={'mr-1 mb-1'}>
<Button variant={'light'} title={t('landing.history.toolbar.export')}>
<ForkAwesomeIcon icon='download'/>
</Button>
<ExportHistoryButton onExportHistory={onExportHistory}/>
</InputGroup>
<InputGroup className={'mr-1 mb-1'}>
<Button variant={'light'} title={t('landing.history.toolbar.import')}>
<ForkAwesomeIcon icon='upload'/>
</Button>
<ImportHistoryButton onImportHistory={onImportHistory}/>
</InputGroup>
<InputGroup className={'mr-1 mb-1'}>
<ClearHistoryButton onClearHistory={onClearHistory}/>
</InputGroup>
<InputGroup className={'mr-1 mb-1'}>
<Button variant={'light'} title={t('landing.history.toolbar.refresh')}>
<Button variant={'light'} title={t('landing.history.toolbar.refresh')} onClick={onRefreshHistory}>
<ForkAwesomeIcon icon='refresh'/>
</Button>
</InputGroup>

View file

@ -0,0 +1,93 @@
import React, { useRef, useState } from 'react'
import { Button, Modal } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon'
import { convertV1History, V1HistoryEntry } from '../../../../../utils/historyUtils'
import { HistoryEntry, HistoryJson } from '../history'
export interface ImportHistoryButtonProps {
onImportHistory: (entries: HistoryEntry[]) => void
}
export const ImportHistoryButton: React.FC<ImportHistoryButtonProps> = ({ onImportHistory }) => {
const { t } = useTranslation()
const uploadInput = useRef<HTMLInputElement>(null)
const [show, setShow] = useState(false)
const [fileName, setFilename] = useState('')
const [i18nKey, setI18nKey] = useState('')
const handleShow = (key: string) => {
setI18nKey(key)
setShow(true)
}
const handleClose = () => {
setI18nKey('')
setShow(false)
}
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const { validity, files } = event.target
if (files && files[0] && validity.valid) {
const file = files[0]
setFilename(file.name)
if (file.type !== 'application/json' && file.type !== '') {
handleShow('landing.history.modal.importHistoryError.textWithFile')
return
}
const fileReader = new FileReader()
fileReader.onload = (event) => {
if (event.target && event.target.result) {
try {
const result = event.target.result as string
const data = JSON.parse(result) as HistoryJson
if (data) {
if (data.version) {
if (data.version === 2) {
onImportHistory(data.entries)
} else {
// probably a newer version we can't support
handleShow('landing.history.modal.importHistoryError.tooNewVersion')
}
} else {
const oldEntries = JSON.parse(result) as V1HistoryEntry[]
onImportHistory(convertV1History(oldEntries))
}
}
} catch {
handleShow('landing.history.modal.importHistoryError.textWithFile')
}
}
}
fileReader.readAsText(file)
} else {
handleShow('landing.history.modal.importHistoryError.textWithOutFile')
}
}
return (
<div>
<input type='file' className="d-none" accept=".json" onChange={handleUpload}
ref={uploadInput}/>
<Button variant={'light'}
title={t('landing.history.toolbar.import')}
onClick={() => uploadInput.current?.click()}
>
<ForkAwesomeIcon icon='upload'/>
</Button>
<Modal show={show} onHide={handleClose} animation={true} className="text-dark">
<Modal.Header closeButton>
<Modal.Title>
<ForkAwesomeIcon icon='exclamation-circle'/>&nbsp;<Trans i18nKey={'landing.history.modal.importHistoryError.title'}/>
</Modal.Title>
</Modal.Header>
<Modal.Body className="text-dark text-center">
{fileName !== ''
? <h5><Trans i18nKey={i18nKey} values={{ fileName: fileName }}/></h5>
: <h5><Trans i18nKey={i18nKey}/></h5>
}
</Modal.Body>
</Modal>
</div>
)
}

View file

@ -1,7 +1,12 @@
import React, { Fragment, useEffect, useState } from 'react'
import { Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { loadHistoryFromLocalStore, setHistoryToLocalStore, sortAndFilterEntries } from '../../../../utils/historyUtils'
import {
downloadHistory,
loadHistoryFromLocalStore,
setHistoryToLocalStore,
sortAndFilterEntries
} from '../../../../utils/historyUtils'
import { HistoryContent } from './history-content/history-content'
import { HistoryToolbar, HistoryToolbarState, initState as toolbarInitState } from './history-toolbar/history-toolbar'
@ -13,6 +18,11 @@ export interface HistoryEntry {
pinned: boolean
}
export interface HistoryJson {
version: number,
entries: HistoryEntry[]
}
export type pinClick = (entryId: string) => void;
export const History: React.FC = () => {
@ -21,17 +31,34 @@ export const History: React.FC = () => {
const [viewState, setViewState] = useState<HistoryToolbarState>(toolbarInitState)
useEffect(() => {
const history = loadHistoryFromLocalStore()
setHistoryEntries(history)
refreshHistory()
}, [])
useEffect(() => {
if (historyEntries === []) {
if (!historyEntries || historyEntries === []) {
return
}
setHistoryToLocalStore(historyEntries)
}, [historyEntries])
const exportHistory = () => {
const dataObject: HistoryJson = {
version: 2,
entries: historyEntries
}
downloadHistory(dataObject)
}
const importHistory = (entries: HistoryEntry[]): void => {
setHistoryToLocalStore(entries)
setHistoryEntries(entries)
}
const refreshHistory = () => {
const history = loadHistoryFromLocalStore()
setHistoryEntries(history)
}
const clearHistory = () => {
setHistoryToLocalStore([])
setHistoryEntries([])
@ -66,6 +93,9 @@ export const History: React.FC = () => {
onSettingsChange={setViewState}
tags={tags}
onClearHistory={clearHistory}
onRefreshHistory={refreshHistory}
onExportHistory={exportHistory}
onImportHistory={importHistory}
/>
</Row>
<HistoryContent viewState={viewState.viewState}

View file

@ -1,5 +1,5 @@
import { HistoryEntry } from '../components/landing/pages/history/history'
import moment from 'moment'
import { HistoryEntry, HistoryJson } from '../components/landing/pages/history/history'
import { HistoryToolbarState } from '../components/landing/pages/history/history-toolbar/history-toolbar'
import { SortModeEnum } from '../components/sort-button/sort-button'
@ -58,30 +58,35 @@ export function formatHistoryDate (date: Date): string {
return moment(date).format('llll')
}
export interface OldHistoryEntry {
id: string;
text: string;
time: number;
tags: string[];
pinned: boolean;
export interface V1HistoryEntry {
id: string;
text: string;
time: number;
tags: string[];
pinned: boolean;
}
export function convertV1History (oldHistory: V1HistoryEntry[]): HistoryEntry[] {
return oldHistory.map((entry: V1HistoryEntry) => {
return {
id: entry.id,
title: entry.text,
lastVisited: moment(entry.time).toDate(),
tags: entry.tags,
pinned: entry.pinned
}
})
}
export function loadHistoryFromLocalStore (): HistoryEntry[] {
const historyJsonString = window.localStorage.getItem('history')
if (!historyJsonString) {
// if localStorage["history"] is empty we check the old localStorage["notehistory"]
// and convert it to the new format
const oldHistoryJsonString = window.localStorage.getItem('notehistory')
const oldHistory = oldHistoryJsonString ? JSON.parse(JSON.parse(oldHistoryJsonString)) as OldHistoryEntry[] : []
return oldHistory.map((entry: OldHistoryEntry) => {
return {
id: entry.id,
title: entry.text,
lastVisited: moment(entry.time).toDate(),
tags: entry.tags,
pinned: entry.pinned
}
})
const oldHistory = oldHistoryJsonString ? JSON.parse(JSON.parse(oldHistoryJsonString)) as V1HistoryEntry[] : []
return convertV1History(oldHistory)
} else {
return JSON.parse(historyJsonString) as HistoryEntry[]
}
@ -90,3 +95,13 @@ export function loadHistoryFromLocalStore (): HistoryEntry[] {
export function setHistoryToLocalStore (entries: HistoryEntry[]): void {
window.localStorage.setItem('history', JSON.stringify(entries))
}
export function downloadHistory (dataObject: HistoryJson): void {
const data = 'data:text/json;charset=utf-8;base64,' + Buffer.from(JSON.stringify(dataObject)).toString('base64')
const downloadLink = document.createElement('a')
downloadLink.setAttribute('href', data)
downloadLink.setAttribute('download', `history_${(new Date()).getTime()}.json`)
document.body.appendChild(downloadLink)
downloadLink.click()
downloadLink.remove()
}