mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-04-14 22:33:30 +00:00
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:
parent
d7fdb73814
commit
0d8ca681f8
6 changed files with 197 additions and 33 deletions
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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'/> <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>
|
||||
)
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue