From 0d8ca681f8a293be5cc367358539aa62292a31a8 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Thu, 4 Jun 2020 22:41:44 +0200 Subject: [PATCH] finished history toolbar buttons (#117) * added history toolbar functionality * export now adds a version number * renamed OldHistoryEntry to V0HistoryEntry Signed-off-by: Philip Molares Signed-off-by: Tilman Vatteroth Co-authored-by: Tilman Vatteroth --- public/locales/en.json | 6 ++ .../history-toolbar/export-history-button.tsx | 18 ++++ .../history-toolbar/history-toolbar.tsx | 26 +++--- .../history-toolbar/import-history-button.tsx | 93 +++++++++++++++++++ .../landing/pages/history/history.tsx | 38 +++++++- src/utils/historyUtils.ts | 49 ++++++---- 6 files changed, 197 insertions(+), 33 deletions(-) create mode 100644 src/components/landing/pages/history/history-toolbar/export-history-button.tsx create mode 100644 src/components/landing/pages/history/history-toolbar/import-history-button.tsx diff --git a/public/locales/en.json b/public/locales/en.json index d784aded8..cf002c259 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -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": { diff --git a/src/components/landing/pages/history/history-toolbar/export-history-button.tsx b/src/components/landing/pages/history/history-toolbar/export-history-button.tsx new file mode 100644 index 000000000..1a43af688 --- /dev/null +++ b/src/components/landing/pages/history/history-toolbar/export-history-button.tsx @@ -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 = ({ onExportHistory }) => { + const { t } = useTranslation() + + return ( + + ) +} diff --git a/src/components/landing/pages/history/history-toolbar/history-toolbar.tsx b/src/components/landing/pages/history/history-toolbar/history-toolbar.tsx index 4ece09660..f097b988d 100644 --- a/src/components/landing/pages/history/history-toolbar/history-toolbar.tsx +++ b/src/components/landing/pages/history/history-toolbar/history-toolbar.tsx @@ -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 = ({ onSettingsChange, tags, onClearHistory }) => { +export const HistoryToolbar: React.FC = ({ onSettingsChange, tags, onClearHistory, onRefreshHistory, onExportHistory, onImportHistory }) => { const [t] = useTranslation() const [state, setState] = useState(initState) @@ -94,20 +100,16 @@ export const HistoryToolbar: React.FC = ({ onSettingsChange variant={'light'}> - + - + - diff --git a/src/components/landing/pages/history/history-toolbar/import-history-button.tsx b/src/components/landing/pages/history/history-toolbar/import-history-button.tsx new file mode 100644 index 000000000..20a8dc570 --- /dev/null +++ b/src/components/landing/pages/history/history-toolbar/import-history-button.tsx @@ -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 = ({ onImportHistory }) => { + const { t } = useTranslation() + const uploadInput = useRef(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) => { + 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 ( +
+ + + + + +   + + + + {fileName !== '' + ?
+ :
+ } +
+
+
+ ) +} diff --git a/src/components/landing/pages/history/history.tsx b/src/components/landing/pages/history/history.tsx index b2ad56948..2fac26bb1 100644 --- a/src/components/landing/pages/history/history.tsx +++ b/src/components/landing/pages/history/history.tsx @@ -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(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} /> { + 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() +}