mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-29 06:15:29 -04:00
The History PR: I - Move to redux (#1156)
This commit is contained in:
parent
bba2b207c4
commit
8e5a667d18
24 changed files with 629 additions and 417 deletions
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { loadAllConfig } from './configLoader'
|
||||
import { setUpI18n } from './i18n'
|
||||
import { refreshHistoryState } from '../../../redux/history/methods'
|
||||
|
||||
const customDelay: () => Promise<void> = async () => {
|
||||
if (window.localStorage.getItem('customDelay')) {
|
||||
|
@ -27,6 +28,9 @@ export const createSetUpTaskList = (baseUrl: string): InitTask[] => {
|
|||
}, {
|
||||
name: 'Load config',
|
||||
task: loadAllConfig(baseUrl)
|
||||
}, {
|
||||
name: 'Load history state',
|
||||
task: refreshHistoryState()
|
||||
}, {
|
||||
name: 'Add Delay',
|
||||
task: customDelay()
|
||||
|
|
|
@ -9,24 +9,28 @@ import { Dropdown } from 'react-bootstrap'
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { HistoryEntryOrigin } from '../history-page'
|
||||
import { DeleteNoteItem } from './delete-note-item'
|
||||
import './entry-menu.scss'
|
||||
import { RemoveNoteEntryItem } from './remove-note-entry-item'
|
||||
import { HistoryEntryOrigin } from '../../../redux/history/types'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
|
||||
export interface EntryMenuProps {
|
||||
id: string;
|
||||
title: string
|
||||
location: HistoryEntryOrigin
|
||||
origin: HistoryEntryOrigin
|
||||
isDark: boolean;
|
||||
onRemove: () => void
|
||||
onDelete: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, location, isDark, onRemove, onDelete, className }) => {
|
||||
export const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, origin, isDark, onRemove, onDelete, className }) => {
|
||||
useTranslation()
|
||||
|
||||
const userExists = useSelector((state: ApplicationState) => !!state.user)
|
||||
|
||||
return (
|
||||
<Dropdown className={ `d-inline-flex ${ className || '' }` }>
|
||||
<Dropdown.Toggle variant={ isDark ? 'secondary' : 'light' } id={ `dropdown-card-${ id }` }
|
||||
|
@ -40,13 +44,13 @@ export const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, location, isDar
|
|||
<Trans i18nKey="landing.history.menu.recentNotes"/>
|
||||
</Dropdown.Header>
|
||||
|
||||
<ShowIf condition={ location === HistoryEntryOrigin.LOCAL }>
|
||||
<ShowIf condition={ origin === HistoryEntryOrigin.LOCAL }>
|
||||
<Dropdown.Item disabled>
|
||||
<ForkAwesomeIcon icon="laptop" fixedWidth={ true } className="mx-2"/>
|
||||
<Trans i18nKey="landing.history.menu.entryLocal"/>
|
||||
</Dropdown.Item>
|
||||
</ShowIf>
|
||||
<ShowIf condition={ location === HistoryEntryOrigin.REMOTE }>
|
||||
<ShowIf condition={ origin === HistoryEntryOrigin.REMOTE }>
|
||||
<Dropdown.Item disabled>
|
||||
<ForkAwesomeIcon icon="cloud" fixedWidth={ true } className="mx-2"/>
|
||||
<Trans i18nKey="landing.history.menu.entryRemote"/>
|
||||
|
@ -54,9 +58,10 @@ export const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, location, isDar
|
|||
</ShowIf>
|
||||
<RemoveNoteEntryItem onConfirm={ onRemove } noteTitle={ title }/>
|
||||
|
||||
<Dropdown.Divider/>
|
||||
|
||||
<DeleteNoteItem onConfirm={ onDelete } noteTitle={ title }/>
|
||||
<ShowIf condition={ userExists }>
|
||||
<Dropdown.Divider/>
|
||||
<DeleteNoteItem onConfirm={ onDelete } noteTitle={ title }/>
|
||||
</ShowIf>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
|
|
|
@ -7,17 +7,17 @@
|
|||
import React from 'react'
|
||||
import { Row } from 'react-bootstrap'
|
||||
import { Pager } from '../../common/pagination/pager'
|
||||
import { HistoryEntriesProps } from '../history-content/history-content'
|
||||
import { HistoryEntriesProps, HistoryEventHandlers } from '../history-content/history-content'
|
||||
import { HistoryCard } from './history-card'
|
||||
|
||||
export const HistoryCardList: React.FC<HistoryEntriesProps> = ({ entries, onPinClick, onRemoveClick, onDeleteClick, pageIndex, onLastPageIndexChange }) => {
|
||||
export const HistoryCardList: React.FC<HistoryEntriesProps & HistoryEventHandlers> = ({ entries, onPinClick, onRemoveClick, onDeleteClick, pageIndex, onLastPageIndexChange }) => {
|
||||
return (
|
||||
<Row className="justify-content-start">
|
||||
<Pager numberOfElementsPerPage={ 9 } pageIndex={ pageIndex } onLastPageIndexChange={ onLastPageIndexChange }>
|
||||
{
|
||||
entries.map((entry) => (
|
||||
<HistoryCard
|
||||
key={ entry.id }
|
||||
key={ entry.identifier }
|
||||
entry={ entry }
|
||||
onPinClick={ onPinClick }
|
||||
onRemoveClick={ onRemoveClick }
|
||||
|
|
|
@ -5,26 +5,34 @@
|
|||
*/
|
||||
|
||||
import { DateTime } from 'luxon'
|
||||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Badge, Card } from 'react-bootstrap'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { EntryMenu } from '../entry-menu/entry-menu'
|
||||
import { HistoryEntryProps } from '../history-content/history-content'
|
||||
import { HistoryEntryProps, HistoryEventHandlers } from '../history-content/history-content'
|
||||
import { PinButton } from '../pin-button/pin-button'
|
||||
import { formatHistoryDate } from '../utils'
|
||||
import './history-card.scss'
|
||||
|
||||
export const HistoryCard: React.FC<HistoryEntryProps> = ({ entry, onPinClick, onRemoveClick, onDeleteClick }) => {
|
||||
export const HistoryCard: React.FC<HistoryEntryProps & HistoryEventHandlers> = ({ entry, onPinClick, onRemoveClick, onDeleteClick }) => {
|
||||
const onRemove = useCallback(() => {
|
||||
onRemoveClick(entry.identifier)
|
||||
}, [onRemoveClick, entry.identifier])
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
onDeleteClick(entry.identifier)
|
||||
}, [onDeleteClick, entry.identifier])
|
||||
|
||||
return (
|
||||
<div className="p-2 col-xs-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<Card className="card-min-height" text={ 'dark' } bg={ 'light' }>
|
||||
<Card.Body className="p-2 d-flex flex-row justify-content-between">
|
||||
<div className={ 'd-flex flex-column' }>
|
||||
<PinButton isDark={ false } isPinned={ entry.pinned }
|
||||
onPinClick={ () => onPinClick(entry.id, entry.location) }/>
|
||||
<PinButton isDark={ false } isPinned={ entry.pinStatus }
|
||||
onPinClick={ () => onPinClick(entry.identifier) }/>
|
||||
</div>
|
||||
<Link to={ `/n/${ entry.id }` } className="text-decoration-none flex-fill text-dark">
|
||||
<Link to={ `/n/${ entry.identifier }` } className="text-decoration-none flex-fill text-dark">
|
||||
<div className={ 'd-flex flex-column justify-content-between' }>
|
||||
<Card.Title className="m-0 mt-1dot5">{ entry.title }</Card.Title>
|
||||
<div>
|
||||
|
@ -44,12 +52,12 @@ export const HistoryCard: React.FC<HistoryEntryProps> = ({ entry, onPinClick, on
|
|||
</Link>
|
||||
<div className={ 'd-flex flex-column' }>
|
||||
<EntryMenu
|
||||
id={ entry.id }
|
||||
id={ entry.identifier }
|
||||
title={ entry.title }
|
||||
location={ entry.location }
|
||||
origin={ entry.origin }
|
||||
isDark={ false }
|
||||
onRemove={ () => onRemoveClick(entry.id, entry.location) }
|
||||
onDelete={ () => onDeleteClick(entry.id, entry.location) }
|
||||
onRemove={ onRemove }
|
||||
onDelete={ onDelete }
|
||||
/>
|
||||
</div>
|
||||
</Card.Body>
|
||||
|
|
|
@ -4,46 +4,67 @@
|
|||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import React, { Fragment, useCallback, useState } from 'react'
|
||||
import { Alert, Row } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { PagerPagination } from '../../common/pagination/pager-pagination'
|
||||
import { HistoryCardList } from '../history-card/history-card-list'
|
||||
import { HistoryEntryOrigin, LocatedHistoryEntry } from '../history-page'
|
||||
import { HistoryTable } from '../history-table/history-table'
|
||||
import { ViewStateEnum } from '../history-toolbar/history-toolbar'
|
||||
import { HistoryEntry } from '../../../redux/history/types'
|
||||
import { removeHistoryEntry, toggleHistoryEntryPinning } from '../../../redux/history/methods'
|
||||
import { deleteNote } from '../../../api/notes'
|
||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
||||
|
||||
type OnEntryClick = (entryId: string, location: HistoryEntryOrigin) => void
|
||||
type OnEntryClick = (entryId: string) => void
|
||||
|
||||
export interface HistoryEventHandlers {
|
||||
onPinClick: OnEntryClick
|
||||
onRemoveClick: OnEntryClick
|
||||
onDeleteClick: OnEntryClick
|
||||
}
|
||||
|
||||
export interface HistoryContentProps {
|
||||
viewState: ViewStateEnum
|
||||
entries: LocatedHistoryEntry[]
|
||||
onPinClick: OnEntryClick
|
||||
onRemoveClick: OnEntryClick
|
||||
onDeleteClick: OnEntryClick
|
||||
entries: HistoryEntry[]
|
||||
}
|
||||
|
||||
export interface HistoryEntryProps {
|
||||
entry: LocatedHistoryEntry,
|
||||
onPinClick: OnEntryClick
|
||||
onRemoveClick: OnEntryClick
|
||||
onDeleteClick: OnEntryClick
|
||||
entry: HistoryEntry,
|
||||
}
|
||||
|
||||
export interface HistoryEntriesProps {
|
||||
entries: LocatedHistoryEntry[]
|
||||
onPinClick: OnEntryClick
|
||||
onRemoveClick: OnEntryClick
|
||||
onDeleteClick: OnEntryClick
|
||||
entries: HistoryEntry[]
|
||||
pageIndex: number
|
||||
onLastPageIndexChange: (lastPageIndex: number) => void
|
||||
}
|
||||
|
||||
export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entries, onPinClick, onRemoveClick, onDeleteClick }) => {
|
||||
useTranslation()
|
||||
export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entries }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0)
|
||||
const [lastPageIndex, setLastPageIndex] = useState(0)
|
||||
|
||||
const onPinClick = useCallback((noteId: string) => {
|
||||
toggleHistoryEntryPinning(noteId).catch(
|
||||
showErrorNotification(t('landing.history.error.updateEntry.text'))
|
||||
)
|
||||
}, [t])
|
||||
|
||||
const onDeleteClick = useCallback((noteId: string) => {
|
||||
deleteNote(noteId).then(() => {
|
||||
return removeHistoryEntry(noteId)
|
||||
}).catch(
|
||||
showErrorNotification(t('landing.history.error.deleteNote.text'))
|
||||
)
|
||||
}, [t])
|
||||
|
||||
const onRemoveClick = useCallback((noteId: string) => {
|
||||
removeHistoryEntry(noteId).catch(
|
||||
showErrorNotification(t('landing.history.error.deleteEntry.text'))
|
||||
)
|
||||
}, [t])
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<Row className={ 'justify-content-center' }>
|
||||
|
|
|
@ -4,226 +4,48 @@
|
|||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import React, { Fragment, useEffect, useMemo, useState } from 'react'
|
||||
import { Row } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { deleteHistory, deleteHistoryEntry, getHistory, setHistory, updateHistoryEntry } from '../../api/history'
|
||||
import { deleteNote } from '../../api/notes'
|
||||
import { ApplicationState } from '../../redux'
|
||||
import { download } from '../common/download/download'
|
||||
import { ErrorModal } from '../common/modals/error-modal'
|
||||
import { HistoryContent } from './history-content/history-content'
|
||||
import { HistoryToolbar, HistoryToolbarState, initState as toolbarInitState } from './history-toolbar/history-toolbar'
|
||||
|
||||
import {
|
||||
collectEntries,
|
||||
loadHistoryFromLocalStore,
|
||||
mergeEntryArrays,
|
||||
setHistoryToLocalStore,
|
||||
sortAndFilterEntries
|
||||
} from './utils'
|
||||
|
||||
export interface HistoryEntry {
|
||||
id: string,
|
||||
title: string,
|
||||
lastVisited: string,
|
||||
tags: string[],
|
||||
pinned: boolean
|
||||
}
|
||||
|
||||
export interface HistoryJson {
|
||||
version: number,
|
||||
entries: HistoryEntry[]
|
||||
}
|
||||
|
||||
export type LocatedHistoryEntry = HistoryEntry & HistoryEntryLocation
|
||||
|
||||
export interface HistoryEntryLocation {
|
||||
location: HistoryEntryOrigin
|
||||
}
|
||||
|
||||
export enum HistoryEntryOrigin {
|
||||
LOCAL = 'local',
|
||||
REMOTE = 'remote'
|
||||
}
|
||||
import { sortAndFilterEntries } from './utils'
|
||||
import { refreshHistoryState } from '../../redux/history/methods'
|
||||
import { HistoryEntry } from '../../redux/history/types'
|
||||
import { showErrorNotification } from '../../redux/ui-notifications/methods'
|
||||
|
||||
export const HistoryPage: React.FC = () => {
|
||||
useTranslation()
|
||||
const [localHistoryEntries, setLocalHistoryEntries] = useState<HistoryEntry[]>(loadHistoryFromLocalStore)
|
||||
const [remoteHistoryEntries, setRemoteHistoryEntries] = useState<HistoryEntry[]>([])
|
||||
const { t } = useTranslation()
|
||||
|
||||
const allEntries = useSelector((state: ApplicationState) => state.history)
|
||||
const [toolbarState, setToolbarState] = useState<HistoryToolbarState>(toolbarInitState)
|
||||
const userExists = useSelector((state: ApplicationState) => !!state.user)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const historyWrite = useCallback((entries: HistoryEntry[]) => {
|
||||
if (!entries) {
|
||||
return
|
||||
}
|
||||
setHistoryToLocalStore(entries)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
historyWrite(localHistoryEntries)
|
||||
}, [historyWrite, localHistoryEntries])
|
||||
|
||||
const importHistory = useCallback((entries: HistoryEntry[]): void => {
|
||||
if (userExists) {
|
||||
setHistory(entries)
|
||||
.then(() => setRemoteHistoryEntries(entries))
|
||||
.catch(() => setError('setHistory'))
|
||||
} else {
|
||||
setLocalHistoryEntries(entries)
|
||||
}
|
||||
}, [userExists])
|
||||
|
||||
const refreshHistory = useCallback(() => {
|
||||
const localHistory = loadHistoryFromLocalStore()
|
||||
setLocalHistoryEntries(localHistory)
|
||||
if (userExists) {
|
||||
getHistory()
|
||||
.then((remoteHistory) => setRemoteHistoryEntries(remoteHistory))
|
||||
.catch(() => setError('getHistory'))
|
||||
}
|
||||
}, [userExists])
|
||||
|
||||
useEffect(() => {
|
||||
refreshHistory()
|
||||
}, [refreshHistory])
|
||||
|
||||
const exportHistory = useCallback(() => {
|
||||
const dataObject: HistoryJson = {
|
||||
version: 2,
|
||||
entries: mergeEntryArrays(localHistoryEntries, remoteHistoryEntries)
|
||||
}
|
||||
download(JSON.stringify(dataObject), `history_${ (new Date()).getTime() }.json`, 'application/json')
|
||||
}, [localHistoryEntries, remoteHistoryEntries])
|
||||
|
||||
const clearHistory = useCallback(() => {
|
||||
setLocalHistoryEntries([])
|
||||
if (userExists) {
|
||||
deleteHistory()
|
||||
.then(() => setRemoteHistoryEntries([]))
|
||||
.catch(() => setError('deleteHistory'))
|
||||
}
|
||||
historyWrite([])
|
||||
}, [historyWrite, userExists])
|
||||
|
||||
const uploadAll = useCallback((): void => {
|
||||
const newHistory = mergeEntryArrays(localHistoryEntries, remoteHistoryEntries)
|
||||
if (userExists) {
|
||||
setHistory(newHistory)
|
||||
.then(() => {
|
||||
setRemoteHistoryEntries(newHistory)
|
||||
setLocalHistoryEntries([])
|
||||
historyWrite([])
|
||||
})
|
||||
.catch(() => setError('setHistory'))
|
||||
}
|
||||
}, [historyWrite, localHistoryEntries, remoteHistoryEntries, userExists])
|
||||
|
||||
const removeFromHistoryClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => {
|
||||
if (location === HistoryEntryOrigin.LOCAL) {
|
||||
setLocalHistoryEntries((entries) => entries.filter(entry => entry.id !== entryId))
|
||||
} else if (location === HistoryEntryOrigin.REMOTE) {
|
||||
deleteHistoryEntry(entryId)
|
||||
.then(() => setRemoteHistoryEntries((entries) => entries.filter(entry => entry.id !== entryId)))
|
||||
.catch(() => setError('deleteEntry'))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const deleteNoteClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => {
|
||||
if (userExists) {
|
||||
deleteNote(entryId)
|
||||
.then(() => {
|
||||
removeFromHistoryClick(entryId, location)
|
||||
})
|
||||
.catch(() => setError('deleteNote'))
|
||||
}
|
||||
}, [userExists, removeFromHistoryClick])
|
||||
|
||||
const pinClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => {
|
||||
if (location === HistoryEntryOrigin.LOCAL) {
|
||||
setLocalHistoryEntries((entries) => {
|
||||
return entries.map((entry) => {
|
||||
if (entry.id === entryId) {
|
||||
entry.pinned = !entry.pinned
|
||||
}
|
||||
return entry
|
||||
})
|
||||
})
|
||||
} else if (location === HistoryEntryOrigin.REMOTE) {
|
||||
const foundEntry = remoteHistoryEntries.find(entry => entry.id === entryId)
|
||||
if (!foundEntry) {
|
||||
setError('notFoundEntry')
|
||||
return
|
||||
}
|
||||
const changedEntry = {
|
||||
...foundEntry,
|
||||
pinned: !foundEntry.pinned
|
||||
}
|
||||
updateHistoryEntry(entryId, changedEntry)
|
||||
.then(() => setRemoteHistoryEntries((entries) => (
|
||||
entries.map((entry) => {
|
||||
if (entry.id === entryId) {
|
||||
entry.pinned = !entry.pinned
|
||||
}
|
||||
return entry
|
||||
})
|
||||
)
|
||||
))
|
||||
.catch(() => setError('updateEntry'))
|
||||
}
|
||||
}, [remoteHistoryEntries])
|
||||
|
||||
const resetError = () => {
|
||||
setError('')
|
||||
}
|
||||
|
||||
const allEntries = useMemo(() => {
|
||||
return collectEntries(localHistoryEntries, remoteHistoryEntries)
|
||||
}, [localHistoryEntries, remoteHistoryEntries])
|
||||
|
||||
const tags = useMemo<string[]>(() => {
|
||||
return allEntries.map(entry => entry.tags)
|
||||
.reduce((a, b) => ([...a, ...b]), [])
|
||||
.filter((value, index, array) => {
|
||||
if (index === 0) {
|
||||
return true
|
||||
}
|
||||
return (value !== array[index - 1])
|
||||
})
|
||||
}, [allEntries])
|
||||
|
||||
const entriesToShow = useMemo<LocatedHistoryEntry[]>(() =>
|
||||
const entriesToShow = useMemo<HistoryEntry[]>(() =>
|
||||
sortAndFilterEntries(allEntries, toolbarState),
|
||||
[allEntries, toolbarState])
|
||||
|
||||
return <Fragment>
|
||||
<ErrorModal show={ error !== '' } onHide={ resetError }
|
||||
titleI18nKey={ error !== '' ? `landing.history.error.${ error }.title` : '' }>
|
||||
<h5>
|
||||
<Trans i18nKey={ error !== '' ? `landing.history.error.${ error }.text` : '' }/>
|
||||
</h5>
|
||||
</ErrorModal>
|
||||
<h1 className="mb-4"><Trans i18nKey="landing.navigation.history"/></h1>
|
||||
<Row className={ 'justify-content-center mt-5 mb-3' }>
|
||||
<HistoryToolbar
|
||||
onSettingsChange={ setToolbarState }
|
||||
tags={ tags }
|
||||
onClearHistory={ clearHistory }
|
||||
onRefreshHistory={ refreshHistory }
|
||||
onExportHistory={ exportHistory }
|
||||
onImportHistory={ importHistory }
|
||||
onUploadAll={ uploadAll }
|
||||
useEffect(() => {
|
||||
refreshHistoryState().catch(
|
||||
showErrorNotification(t('landing.history.error.getHistory.text'))
|
||||
)
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<h1 className="mb-4">
|
||||
<Trans i18nKey="landing.navigation.history"/>
|
||||
</h1>
|
||||
<Row className={ 'justify-content-center mt-5 mb-3' }>
|
||||
<HistoryToolbar
|
||||
onSettingsChange={ setToolbarState }
|
||||
/>
|
||||
</Row>
|
||||
<HistoryContent
|
||||
viewState={ toolbarState.viewState }
|
||||
entries={ entriesToShow }
|
||||
/>
|
||||
</Row>
|
||||
<HistoryContent
|
||||
viewState={ toolbarState.viewState }
|
||||
entries={ entriesToShow }
|
||||
onPinClick={ pinClick }
|
||||
onRemoveClick={ removeFromHistoryClick }
|
||||
onDeleteClick={ deleteNoteClick }
|
||||
/>
|
||||
</Fragment>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,15 +8,15 @@ import React from 'react'
|
|||
import { Badge } from 'react-bootstrap'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { EntryMenu } from '../entry-menu/entry-menu'
|
||||
import { HistoryEntryProps } from '../history-content/history-content'
|
||||
import { HistoryEntryProps, HistoryEventHandlers } from '../history-content/history-content'
|
||||
import { PinButton } from '../pin-button/pin-button'
|
||||
import { formatHistoryDate } from '../utils'
|
||||
|
||||
export const HistoryTableRow: React.FC<HistoryEntryProps> = ({ entry, onPinClick, onRemoveClick, onDeleteClick }) => {
|
||||
export const HistoryTableRow: React.FC<HistoryEntryProps & HistoryEventHandlers> = ({ entry, onPinClick, onRemoveClick, onDeleteClick }) => {
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<Link to={ `/n/${ entry.id }` } className="text-light">
|
||||
<Link to={ `/n/${ entry.identifier }` } className="text-light">
|
||||
{ entry.title }
|
||||
</Link>
|
||||
</td>
|
||||
|
@ -28,15 +28,15 @@ export const HistoryTableRow: React.FC<HistoryEntryProps> = ({ entry, onPinClick
|
|||
}
|
||||
</td>
|
||||
<td>
|
||||
<PinButton isDark={ true } isPinned={ entry.pinned } onPinClick={ () => onPinClick(entry.id, entry.location) }
|
||||
<PinButton isDark={ true } isPinned={ entry.pinStatus } onPinClick={ () => onPinClick(entry.identifier) }
|
||||
className={ 'mb-1 mr-1' }/>
|
||||
<EntryMenu
|
||||
id={ entry.id }
|
||||
id={ entry.identifier }
|
||||
title={ entry.title }
|
||||
location={ entry.location }
|
||||
origin={ entry.origin }
|
||||
isDark={ true }
|
||||
onRemove={ () => onRemoveClick(entry.id, entry.location) }
|
||||
onDelete={ () => onDeleteClick(entry.id, entry.location) }
|
||||
onRemove={ () => onRemoveClick(entry.identifier) }
|
||||
onDelete={ () => onDeleteClick(entry.identifier) }
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -8,11 +8,11 @@ import React from 'react'
|
|||
import { Table } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Pager } from '../../common/pagination/pager'
|
||||
import { HistoryEntriesProps } from '../history-content/history-content'
|
||||
import { HistoryEntriesProps, HistoryEventHandlers } from '../history-content/history-content'
|
||||
import { HistoryTableRow } from './history-table-row'
|
||||
import './history-table.scss'
|
||||
|
||||
export const HistoryTable: React.FC<HistoryEntriesProps> = ({ entries, onPinClick, onRemoveClick, onDeleteClick, pageIndex, onLastPageIndexChange }) => {
|
||||
export const HistoryTable: React.FC<HistoryEntriesProps & HistoryEventHandlers> = ({ entries, onPinClick, onRemoveClick, onDeleteClick, pageIndex, onLastPageIndexChange }) => {
|
||||
useTranslation()
|
||||
return (
|
||||
<Table striped bordered hover size="sm" variant="dark" className={ 'history-table' }>
|
||||
|
@ -29,7 +29,7 @@ export const HistoryTable: React.FC<HistoryEntriesProps> = ({ entries, onPinClic
|
|||
{
|
||||
entries.map((entry) =>
|
||||
<HistoryTableRow
|
||||
key={ entry.id }
|
||||
key={ entry.identifier }
|
||||
entry={ entry }
|
||||
onPinClick={ onPinClick }
|
||||
onRemoveClick={ onRemoveClick }
|
||||
|
|
|
@ -4,33 +4,38 @@
|
|||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import React, { Fragment, useCallback, useState } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { DeletionModal } from '../../common/modals/deletion-modal'
|
||||
import { deleteAllHistoryEntries, refreshHistoryState } from '../../../redux/history/methods'
|
||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
||||
|
||||
export interface ClearHistoryButtonProps {
|
||||
onClearHistory: () => void
|
||||
}
|
||||
|
||||
export const ClearHistoryButton: React.FC<ClearHistoryButtonProps> = ({ onClearHistory }) => {
|
||||
export const ClearHistoryButton: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
const handleShow = () => setShow(true)
|
||||
const handleClose = () => setShow(false)
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
deleteAllHistoryEntries().catch(error => {
|
||||
showErrorNotification(t('landing.history.error.deleteEntry.text'))(error)
|
||||
refreshHistoryState().catch(
|
||||
showErrorNotification(t('landing.history.error.getHistory.text'))
|
||||
)
|
||||
})
|
||||
handleClose()
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Button variant={ 'light' } title={ t('landing.history.toolbar.clear') } onClick={ handleShow }>
|
||||
<ForkAwesomeIcon icon={ 'trash' }/>
|
||||
</Button>
|
||||
<DeletionModal
|
||||
onConfirm={ () => {
|
||||
onClearHistory()
|
||||
handleClose()
|
||||
} }
|
||||
onConfirm={ onConfirm }
|
||||
deletionButtonI18nKey={ 'landing.history.toolbar.clear' }
|
||||
show={ show }
|
||||
onHide={ handleClose }
|
||||
|
|
|
@ -8,16 +8,13 @@ import React from 'react'
|
|||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { downloadHistory } from '../../../redux/history/methods'
|
||||
|
||||
export interface ExportHistoryButtonProps {
|
||||
onExportHistory: () => void
|
||||
}
|
||||
|
||||
export const ExportHistoryButton: React.FC<ExportHistoryButtonProps> = ({ onExportHistory }) => {
|
||||
export const ExportHistoryButton: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Button variant={ 'light' } title={ t('landing.history.toolbar.export') } onClick={ onExportHistory }>
|
||||
<Button variant={ 'light' } title={ t('landing.history.toolbar.export') } onClick={ downloadHistory }>
|
||||
<ForkAwesomeIcon icon='download'/>
|
||||
</Button>
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react'
|
||||
import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Button, Form, FormControl, InputGroup, ToggleButton, ToggleButtonGroup } from 'react-bootstrap'
|
||||
import { Typeahead } from 'react-bootstrap-typeahead'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
@ -12,12 +12,14 @@ import { useSelector } from 'react-redux'
|
|||
import { ApplicationState } from '../../../redux'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { HistoryEntry } from '../history-page'
|
||||
import { SortButton, SortModeEnum } from '../sort-button/sort-button'
|
||||
import { ClearHistoryButton } from './clear-history-button'
|
||||
import { ExportHistoryButton } from './export-history-button'
|
||||
import { ImportHistoryButton } from './import-history-button'
|
||||
import './typeahead-hacks.scss'
|
||||
import { HistoryEntryOrigin } from '../../../redux/history/types'
|
||||
import { importHistoryEntries, refreshHistoryState, setHistoryEntries } from '../../../redux/history/methods'
|
||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
||||
|
||||
export type HistoryToolbarChange = (settings: HistoryToolbarState) => void;
|
||||
|
||||
|
@ -36,12 +38,6 @@ export enum ViewStateEnum {
|
|||
|
||||
export interface HistoryToolbarProps {
|
||||
onSettingsChange: HistoryToolbarChange
|
||||
tags: string[]
|
||||
onClearHistory: () => void
|
||||
onRefreshHistory: () => void
|
||||
onExportHistory: () => void
|
||||
onImportHistory: (entries: HistoryEntry[]) => void
|
||||
onUploadAll: () => void
|
||||
}
|
||||
|
||||
export const initState: HistoryToolbarState = {
|
||||
|
@ -52,11 +48,18 @@ export const initState: HistoryToolbarState = {
|
|||
selectedTags: []
|
||||
}
|
||||
|
||||
export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange, tags, onClearHistory, onRefreshHistory, onExportHistory, onImportHistory, onUploadAll }) => {
|
||||
const [t] = useTranslation()
|
||||
export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange }) => {
|
||||
const { t } = useTranslation()
|
||||
const [state, setState] = useState<HistoryToolbarState>(initState)
|
||||
const historyEntries = useSelector((state: ApplicationState) => state.history)
|
||||
const userExists = useSelector((state: ApplicationState) => !!state.user)
|
||||
|
||||
const tags = useMemo<string[]>(() => {
|
||||
const allTags = historyEntries.map(entry => entry.tags)
|
||||
.flat()
|
||||
return [...new Set(allTags)]
|
||||
}, [historyEntries])
|
||||
|
||||
const titleSortChanged = (direction: SortModeEnum) => {
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
|
@ -85,6 +88,33 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
|||
setState(prevState => ({ ...prevState, selectedTags: selected }))
|
||||
}
|
||||
|
||||
const refreshHistory = useCallback(() => {
|
||||
refreshHistoryState()
|
||||
.catch(
|
||||
showErrorNotification(t('landing.history.error.getHistory.text'))
|
||||
)
|
||||
}, [t])
|
||||
|
||||
const onUploadAllToRemote = useCallback(() => {
|
||||
if (!userExists) {
|
||||
return
|
||||
}
|
||||
const localEntries = historyEntries.filter(entry => entry.origin === HistoryEntryOrigin.LOCAL)
|
||||
.map(entry => entry.identifier)
|
||||
historyEntries.forEach(entry => entry.origin = HistoryEntryOrigin.REMOTE)
|
||||
importHistoryEntries(historyEntries)
|
||||
.catch(error => {
|
||||
showErrorNotification(t('landing.history.error.setHistory.text'))(error)
|
||||
historyEntries.forEach(entry => {
|
||||
if (localEntries.includes(entry.identifier)) {
|
||||
entry.origin = HistoryEntryOrigin.LOCAL
|
||||
}
|
||||
})
|
||||
setHistoryEntries(historyEntries)
|
||||
refreshHistory()
|
||||
})
|
||||
}, [userExists, historyEntries, t, refreshHistory])
|
||||
|
||||
useEffect(() => {
|
||||
onSettingsChange(state)
|
||||
}, [onSettingsChange, state])
|
||||
|
@ -113,28 +143,28 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
|||
variant={ 'light' }><Trans i18nKey={ 'landing.history.toolbar.sortByLastVisited' }/></SortButton>
|
||||
</InputGroup>
|
||||
<InputGroup className={ 'mr-1 mb-1' }>
|
||||
<ExportHistoryButton onExportHistory={ onExportHistory }/>
|
||||
<ExportHistoryButton/>
|
||||
</InputGroup>
|
||||
<InputGroup className={ 'mr-1 mb-1' }>
|
||||
<ImportHistoryButton onImportHistory={ onImportHistory }/>
|
||||
<ImportHistoryButton/>
|
||||
</InputGroup>
|
||||
<InputGroup className={ 'mr-1 mb-1' }>
|
||||
<ClearHistoryButton onClearHistory={ onClearHistory }/>
|
||||
<ClearHistoryButton/>
|
||||
</InputGroup>
|
||||
<InputGroup className={ 'mr-1 mb-1' }>
|
||||
<Button variant={ 'light' } title={ t('landing.history.toolbar.refresh') } onClick={ onRefreshHistory }>
|
||||
<ForkAwesomeIcon icon='refresh'/>
|
||||
<Button variant={ 'light' } title={ t('landing.history.toolbar.refresh') } onClick={ refreshHistory }>
|
||||
<ForkAwesomeIcon icon="refresh"/>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
<ShowIf condition={ userExists }>
|
||||
<InputGroup className={ 'mr-1 mb-1' }>
|
||||
<Button variant={ 'light' } title={ t('landing.history.toolbar.uploadAll') } onClick={ onUploadAll }>
|
||||
<ForkAwesomeIcon icon='cloud-upload'/>
|
||||
<Button variant={ 'light' } title={ t('landing.history.toolbar.uploadAll') } onClick={ onUploadAllToRemote }>
|
||||
<ForkAwesomeIcon icon="cloud-upload"/>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</ShowIf>
|
||||
<InputGroup className={ 'mr-1 mb-1' }>
|
||||
<ToggleButtonGroup type="radio" name="options" dir='ltr' value={ state.viewState } className={ 'button-height' }
|
||||
<ToggleButtonGroup type="radio" name="options" dir="ltr" value={ state.viewState } className={ 'button-height' }
|
||||
onChange={ (newViewState: ViewStateEnum) => {
|
||||
toggleViewChanged(newViewState)
|
||||
} }>
|
||||
|
|
|
@ -4,34 +4,50 @@
|
|||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { ErrorModal } from '../../common/modals/error-modal'
|
||||
import { HistoryEntry, HistoryJson } from '../history-page'
|
||||
import { convertV1History, V1HistoryEntry } from '../utils'
|
||||
import { HistoryEntry, HistoryEntryOrigin, HistoryExportJson, V1HistoryEntry } from '../../../redux/history/types'
|
||||
import {
|
||||
convertV1History,
|
||||
importHistoryEntries,
|
||||
mergeHistoryEntries,
|
||||
refreshHistoryState
|
||||
} from '../../../redux/history/methods'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
||||
|
||||
export interface ImportHistoryButtonProps {
|
||||
onImportHistory: (entries: HistoryEntry[]) => void
|
||||
}
|
||||
|
||||
export const ImportHistoryButton: React.FC<ImportHistoryButtonProps> = ({ onImportHistory }) => {
|
||||
export const ImportHistoryButton: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const userExists = useSelector((state: ApplicationState) => !!state.user)
|
||||
const historyState = useSelector((state: ApplicationState) => state.history)
|
||||
const uploadInput = useRef<HTMLInputElement>(null)
|
||||
const [show, setShow] = useState(false)
|
||||
const [fileName, setFilename] = useState('')
|
||||
const [i18nKey, setI18nKey] = useState('')
|
||||
|
||||
const handleShow = (key: string) => {
|
||||
const handleShow = useCallback((key: string) => {
|
||||
setI18nKey(key)
|
||||
setShow(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleClose = () => {
|
||||
const handleClose = useCallback(() => {
|
||||
setI18nKey('')
|
||||
setShow(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onImportHistory = useCallback((entries: HistoryEntry[]): void => {
|
||||
entries.forEach(entry => entry.origin = userExists ? HistoryEntryOrigin.REMOTE : HistoryEntryOrigin.LOCAL)
|
||||
importHistoryEntries(mergeHistoryEntries(historyState, entries)).catch(error => {
|
||||
showErrorNotification(t('landing.history.error.setHistory.text'))(error)
|
||||
refreshHistoryState().catch(
|
||||
showErrorNotification(t('landing.history.error.getHistory.text'))
|
||||
)
|
||||
})
|
||||
}, [historyState, userExists, t])
|
||||
|
||||
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { validity, files } = event.target
|
||||
|
@ -47,7 +63,7 @@ export const ImportHistoryButton: React.FC<ImportHistoryButtonProps> = ({ onImpo
|
|||
if (event.target && event.target.result) {
|
||||
try {
|
||||
const result = event.target.result as string
|
||||
const data = JSON.parse(result) as HistoryJson
|
||||
const data = JSON.parse(result) as HistoryExportJson
|
||||
if (data) {
|
||||
if (data.version) {
|
||||
if (data.version === 2) {
|
||||
|
|
|
@ -6,47 +6,25 @@
|
|||
|
||||
import { DateTime } from 'luxon'
|
||||
import { SortModeEnum } from './sort-button/sort-button'
|
||||
import { HistoryEntry, HistoryEntryOrigin, LocatedHistoryEntry } from './history-page'
|
||||
import { HistoryToolbarState } from './history-toolbar/history-toolbar'
|
||||
import { HistoryEntry } from '../../redux/history/types'
|
||||
|
||||
export function collectEntries(localEntries: HistoryEntry[], remoteEntries: HistoryEntry[]): LocatedHistoryEntry[] {
|
||||
const locatedLocalEntries = locateEntries(localEntries, HistoryEntryOrigin.LOCAL)
|
||||
const locatedRemoteEntries = locateEntries(remoteEntries, HistoryEntryOrigin.REMOTE)
|
||||
return mergeEntryArrays(locatedLocalEntries, locatedRemoteEntries)
|
||||
}
|
||||
export const formatHistoryDate = (date: string): string => DateTime.fromISO(date).toFormat('DDDD T')
|
||||
|
||||
export function sortAndFilterEntries(entries: LocatedHistoryEntry[], toolbarState: HistoryToolbarState): LocatedHistoryEntry[] {
|
||||
export const sortAndFilterEntries = (entries: HistoryEntry[], toolbarState: HistoryToolbarState): HistoryEntry[] => {
|
||||
const filteredBySelectedTagsEntries = filterBySelectedTags(entries, toolbarState.selectedTags)
|
||||
const filteredByKeywordSearchEntries = filterByKeywordSearch(filteredBySelectedTagsEntries, toolbarState.keywordSearch)
|
||||
return sortEntries(filteredByKeywordSearchEntries, toolbarState)
|
||||
}
|
||||
|
||||
function locateEntries(entries: HistoryEntry[], location: HistoryEntryOrigin): LocatedHistoryEntry[] {
|
||||
return entries.map(entry => {
|
||||
return {
|
||||
...entry,
|
||||
location: location
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function mergeEntryArrays<T extends HistoryEntry>(localEntries: T[], remoteEntries: T[]): T[] {
|
||||
const filteredLocalEntries = localEntries.filter(localEntry => {
|
||||
const entry = remoteEntries.find(remoteEntry => remoteEntry.id === localEntry.id)
|
||||
return !entry
|
||||
})
|
||||
|
||||
return filteredLocalEntries.concat(remoteEntries)
|
||||
}
|
||||
|
||||
function filterBySelectedTags(entries: LocatedHistoryEntry[], selectedTags: string[]): LocatedHistoryEntry[] {
|
||||
const filterBySelectedTags = (entries: HistoryEntry[], selectedTags: string[]): HistoryEntry[] => {
|
||||
return entries.filter(entry => {
|
||||
return (selectedTags.length === 0 || arrayCommonCheck(entry.tags, selectedTags))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function arrayCommonCheck<T>(array1: T[], array2: T[]): boolean {
|
||||
const arrayCommonCheck = <T> (array1: T[], array2: T[]): boolean => {
|
||||
const foundElement = array1.find((element1) =>
|
||||
array2.find((element2) =>
|
||||
element2 === element1
|
||||
|
@ -55,18 +33,21 @@ function arrayCommonCheck<T>(array1: T[], array2: T[]): boolean {
|
|||
return !!foundElement
|
||||
}
|
||||
|
||||
function filterByKeywordSearch(entries: LocatedHistoryEntry[], keywords: string): LocatedHistoryEntry[] {
|
||||
const filterByKeywordSearch = (entries: HistoryEntry[], keywords: string): HistoryEntry[] => {
|
||||
const searchTerm = keywords.toLowerCase()
|
||||
return entries.filter(entry => entry.title.toLowerCase()
|
||||
.includes(searchTerm))
|
||||
return entries.filter(
|
||||
entry => entry.title
|
||||
.toLowerCase()
|
||||
.includes(searchTerm)
|
||||
)
|
||||
}
|
||||
|
||||
function sortEntries(entries: LocatedHistoryEntry[], viewState: HistoryToolbarState): LocatedHistoryEntry[] {
|
||||
const sortEntries = (entries: HistoryEntry[], viewState: HistoryToolbarState): HistoryEntry[] => {
|
||||
return entries.sort((firstEntry, secondEntry) => {
|
||||
if (firstEntry.pinned && !secondEntry.pinned) {
|
||||
if (firstEntry.pinStatus && !secondEntry.pinStatus) {
|
||||
return -1
|
||||
}
|
||||
if (!firstEntry.pinned && secondEntry.pinned) {
|
||||
if (!firstEntry.pinStatus && secondEntry.pinStatus) {
|
||||
return 1
|
||||
}
|
||||
|
||||
|
@ -86,47 +67,3 @@ function sortEntries(entries: LocatedHistoryEntry[], viewState: HistoryToolbarSt
|
|||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
export function formatHistoryDate(date: string): string {
|
||||
return DateTime.fromISO(date)
|
||||
.toFormat('DDDD T')
|
||||
}
|
||||
|
||||
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: DateTime.fromMillis(entry.time)
|
||||
.toISO(),
|
||||
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 V1HistoryEntry[] : []
|
||||
return convertV1History(oldHistory)
|
||||
} else {
|
||||
return JSON.parse(historyJsonString) as HistoryEntry[]
|
||||
}
|
||||
}
|
||||
|
||||
export function setHistoryToLocalStore(entries: HistoryEntry[]): void {
|
||||
window.localStorage.setItem('history', JSON.stringify(entries))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue