mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-24 18:56:32 -05:00
show local and remote History (#128)
* added history toolbar functionality for local and remote history
This commit is contained in:
parent
23c854dc9a
commit
7b5b73a289
19 changed files with 336 additions and 130 deletions
37
public/api/v2/history
Normal file
37
public/api/v2/history
Normal file
|
@ -0,0 +1,37 @@
|
|||
[
|
||||
{
|
||||
"id": "29QLD0AmT-adevdOPECtqg",
|
||||
"title": "CodiMD community call 2020-04-26",
|
||||
"lastVisited": "2020-05-16T22:26:56.547Z",
|
||||
"tags": [
|
||||
"CodiMD",
|
||||
"Community Call"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "features",
|
||||
"title": "Features",
|
||||
"lastVisited": "2020-05-31T15:20:36.088Z",
|
||||
"tags": [
|
||||
"features",
|
||||
"cool",
|
||||
"updated"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ODakLc2MQkyyFc_Xmb53sg",
|
||||
"title": "CodiMD V2 API",
|
||||
"lastVisited": "2020-05-25T19:48:14.025Z",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"id": "l8JuWxApTR6Fqa0LCrpnLg",
|
||||
"title": "Community call - Let’s meet! (2020-06-06 18:00 UTC / 20:00 CEST)",
|
||||
"lastVisited": "2020-05-24T16:04:36.433Z",
|
||||
"tags": [
|
||||
"agenda",
|
||||
"CodiMD community",
|
||||
"community call"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -14,6 +14,20 @@
|
|||
"screenShotAltText": "CodiMD Screenshot"
|
||||
},
|
||||
"history": {
|
||||
"error": {
|
||||
"getHistory": {
|
||||
"title": "Load History Error",
|
||||
"text": "While trying to load the history form the server an error occurred"
|
||||
},
|
||||
"deleteHistory": {
|
||||
"title": "Delete History Error",
|
||||
"text": "While trying to delete the history on the server an error occurred"
|
||||
},
|
||||
"setHistory": {
|
||||
"title": "Upload History Error",
|
||||
"text": "While trying to upload the history to the server an error occurred"
|
||||
}
|
||||
},
|
||||
"noHistory": "No history",
|
||||
"localHistory": "Below is history from this browser",
|
||||
"toolbar": {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
.history-close {
|
||||
opacity: 0.5;
|
||||
.fa {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover .fa {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import React from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import './close-button.scss'
|
||||
import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon'
|
||||
import './close-button.scss'
|
||||
|
||||
export interface CloseButtonProps {
|
||||
isDark: boolean;
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CloseButton: React.FC<CloseButtonProps> = ({ isDark }) => {
|
||||
const CloseButton: React.FC<CloseButtonProps> = ({ isDark, className }) => {
|
||||
return (
|
||||
<Button variant={isDark ? 'secondary' : 'light'}>
|
||||
<ForkAwesomeIcon
|
||||
className="history-close"
|
||||
icon="times"
|
||||
/>
|
||||
<Button variant={isDark ? 'secondary' : 'light'} className={`history-close ${className || ''}`}>
|
||||
<ForkAwesomeIcon icon="times"/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
.history-pin {
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
.fa {
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover .fa {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.active {
|
||||
&.pinned .fa {
|
||||
color: #d43f3a;
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
import React from 'react'
|
||||
import './pin-button.scss'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon'
|
||||
import './pin-button.scss'
|
||||
|
||||
export interface PinButtonProps {
|
||||
isPinned: boolean;
|
||||
onPinClick: () => void;
|
||||
isDark: boolean;
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const PinButton: React.FC<PinButtonProps> = ({ isPinned, onPinClick, isDark }) => {
|
||||
export const PinButton: React.FC<PinButtonProps> = ({ isPinned, onPinClick, isDark, className }) => {
|
||||
return (
|
||||
<Button variant={isDark ? 'secondary' : 'light'}
|
||||
onClick={onPinClick}>
|
||||
<ForkAwesomeIcon
|
||||
icon="thumb-tack"
|
||||
className={`history-pin ${isPinned ? 'active' : ''}`}
|
||||
/>
|
||||
className={`history-pin ${className || ''} ${isPinned ? 'pinned' : ''}`} onClick={onPinClick}>
|
||||
<ForkAwesomeIcon icon="thumb-tack"/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
10
src/components/landing/pages/history/common/sync-status.scss
Normal file
10
src/components/landing/pages/history/common/sync-status.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
.sync-icon {
|
||||
.fa {
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover .fa {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
21
src/components/landing/pages/history/common/sync-status.tsx
Normal file
21
src/components/landing/pages/history/common/sync-status.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon'
|
||||
import { Location } from '../history'
|
||||
import './sync-status.scss'
|
||||
|
||||
export interface SyncStatusProps {
|
||||
isDark: boolean
|
||||
location: Location
|
||||
onSync: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const SyncStatus: React.FC<SyncStatusProps> = ({ isDark, location, onSync, className }) => {
|
||||
const icon = location === Location.REMOTE ? 'cloud' : 'laptop'
|
||||
return (
|
||||
<Button variant={isDark ? 'secondary' : 'light'} onClick={onSync} className={`sync-icon ${className || ''}`}>
|
||||
<ForkAwesomeIcon icon={icon}/>
|
||||
</Button>
|
||||
)
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
import React from 'react'
|
||||
import { Row } from 'react-bootstrap'
|
||||
import { Pager } from '../../../../pagination/pager'
|
||||
import { HistoryEntriesProps } from '../history-content/history-content'
|
||||
import { HistoryCard } from './history-card'
|
||||
import { Pager } from '../../../../pagination/pager'
|
||||
import { Row } from 'react-bootstrap'
|
||||
|
||||
export const HistoryCardList: React.FC<HistoryEntriesProps> = ({ entries, onPinClick, pageIndex, onLastPageIndexChange }) => {
|
||||
export const HistoryCardList: React.FC<HistoryEntriesProps> = ({ entries, onPinClick, onSyncClick, pageIndex, onLastPageIndexChange }) => {
|
||||
return (
|
||||
<Row className="justify-content-center">
|
||||
<Pager numberOfElementsPerPage={6} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}>
|
||||
<Row className="justify-content-start">
|
||||
<Pager numberOfElementsPerPage={9} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}>
|
||||
{
|
||||
entries.map((entry) => (
|
||||
<HistoryCard
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
onPinClick={onPinClick}
|
||||
onSyncClick={onSyncClick}
|
||||
/>))
|
||||
}
|
||||
</Pager>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.card-min-height {
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.card-footer-min-height {
|
||||
min-height: 27px;
|
||||
}
|
|
@ -1,34 +1,41 @@
|
|||
import moment from 'moment'
|
||||
import React from 'react'
|
||||
import { Badge, Card } from 'react-bootstrap'
|
||||
import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon'
|
||||
import { PinButton } from '../common/pin-button'
|
||||
import { CloseButton } from '../common/close-button'
|
||||
import moment from 'moment'
|
||||
import { HistoryEntryProps } from '../history-content/history-content'
|
||||
import { formatHistoryDate } from '../../../../../utils/historyUtils'
|
||||
import { CloseButton } from '../common/close-button'
|
||||
import { PinButton } from '../common/pin-button'
|
||||
import { SyncStatus } from '../common/sync-status'
|
||||
import { HistoryEntryProps } from '../history-content/history-content'
|
||||
import './history-card.scss'
|
||||
|
||||
export const HistoryCard: React.FC<HistoryEntryProps> = ({ entry, onPinClick }) => {
|
||||
export const HistoryCard: React.FC<HistoryEntryProps> = ({ entry, onPinClick, onSyncClick }) => {
|
||||
return (
|
||||
<div className="p-2 col-xs-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<Card className="p-0" text={'dark'} bg={'light'}>
|
||||
<div className="d-flex justify-content-between p-2 align-items-start">
|
||||
<PinButton isDark={false} isPinned={entry.pinned} onPinClick={() => {
|
||||
onPinClick(entry.id)
|
||||
}}/>
|
||||
<Card.Title className="m-0 mt-3">{entry.title}</Card.Title>
|
||||
<CloseButton isDark={false}/>
|
||||
</div>
|
||||
<Card.Body>
|
||||
<div className="text-black-50">
|
||||
<ForkAwesomeIcon icon="clock-o"/> {moment(entry.lastVisited).fromNow()}<br/>
|
||||
{formatHistoryDate(entry.lastVisited)}
|
||||
<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)}/>
|
||||
<SyncStatus isDark={false} location={entry.location} onSync={() => onSyncClick(entry.id)}
|
||||
className={'mt-1'}/>
|
||||
</div>
|
||||
<div className={'d-flex flex-column justify-content-between'}>
|
||||
<Card.Title className="m-0 mt-1dot5">{entry.title}</Card.Title>
|
||||
<div>
|
||||
{
|
||||
entry.tags.map((tag) => <Badge variant={'dark'} className={'mr-1 mb-1'}
|
||||
key={tag}>{tag}</Badge>)
|
||||
}
|
||||
<div className="text-black-50 mt-2">
|
||||
<ForkAwesomeIcon icon="clock-o"/> {moment(entry.lastVisited).fromNow()}<br/>
|
||||
{formatHistoryDate(entry.lastVisited)}
|
||||
</div>
|
||||
<div className={'card-footer-min-height p-0'}>
|
||||
{
|
||||
entry.tags.map((tag) => <Badge variant={'dark'} className={'mr-1 mb-1'} key={tag}>{tag}</Badge>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'d-flex flex-column'}>
|
||||
<CloseButton isDark={false}/>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
@ -1,31 +1,34 @@
|
|||
import React, { Fragment, useState } from 'react'
|
||||
import { HistoryEntry, pinClick } from '../history'
|
||||
import { HistoryTable } from '../history-table/history-table'
|
||||
import { Alert, Row } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { HistoryCardList } from '../history-card/history-card-list'
|
||||
import { ViewStateEnum } from '../history-toolbar/history-toolbar'
|
||||
import { PagerPagination } from '../../../../pagination/pager-pagination'
|
||||
import { LocatedHistoryEntry } from '../history'
|
||||
import { HistoryCardList } from '../history-card/history-card-list'
|
||||
import { HistoryTable } from '../history-table/history-table'
|
||||
import { ViewStateEnum } from '../history-toolbar/history-toolbar'
|
||||
|
||||
export interface HistoryContentProps {
|
||||
viewState: ViewStateEnum
|
||||
entries: HistoryEntry[]
|
||||
onPinClick: pinClick
|
||||
entries: LocatedHistoryEntry[]
|
||||
onPinClick: (entryId: string) => void
|
||||
onSyncClick: (entryId: string) => void
|
||||
}
|
||||
|
||||
export interface HistoryEntryProps {
|
||||
entry: HistoryEntry,
|
||||
onPinClick: pinClick
|
||||
entry: LocatedHistoryEntry,
|
||||
onPinClick: (entryId: string) => void
|
||||
onSyncClick: (entryId: string) => void
|
||||
}
|
||||
|
||||
export interface HistoryEntriesProps {
|
||||
entries: HistoryEntry[]
|
||||
onPinClick: pinClick
|
||||
entries: LocatedHistoryEntry[]
|
||||
onPinClick: (entryId: string) => void
|
||||
onSyncClick: (entryId: string) => void
|
||||
pageIndex: number
|
||||
onLastPageIndexChange: (lastPageIndex: number) => void
|
||||
}
|
||||
|
||||
export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entries, onPinClick }) => {
|
||||
export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entries, onPinClick, onSyncClick }) => {
|
||||
useTranslation()
|
||||
const [pageIndex, setPageIndex] = useState(0)
|
||||
const [lastPageIndex, setLastPageIndex] = useState(0)
|
||||
|
@ -44,10 +47,13 @@ export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entri
|
|||
switch (viewState) {
|
||||
default:
|
||||
case ViewStateEnum.CARD:
|
||||
return <HistoryCardList entries={entries} onPinClick={onPinClick} pageIndex={pageIndex}
|
||||
return <HistoryCardList entries={entries}
|
||||
onPinClick={onPinClick}
|
||||
onSyncClick={onSyncClick}
|
||||
pageIndex={pageIndex}
|
||||
onLastPageIndexChange={setLastPageIndex}/>
|
||||
case ViewStateEnum.TABLE:
|
||||
return <HistoryTable entries={entries} onPinClick={onPinClick} pageIndex={pageIndex}
|
||||
return <HistoryTable entries={entries} onPinClick={onPinClick} onSyncClick={onSyncClick} pageIndex={pageIndex}
|
||||
onLastPageIndexChange={setLastPageIndex}/>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,26 @@
|
|||
import React from 'react'
|
||||
import { PinButton } from '../common/pin-button'
|
||||
import { CloseButton } from '../common/close-button'
|
||||
import { HistoryEntryProps } from '../history-content/history-content'
|
||||
import { formatHistoryDate } from '../../../../../utils/historyUtils'
|
||||
import { Badge } from 'react-bootstrap'
|
||||
import { formatHistoryDate } from '../../../../../utils/historyUtils'
|
||||
import { CloseButton } from '../common/close-button'
|
||||
import { PinButton } from '../common/pin-button'
|
||||
import { SyncStatus } from '../common/sync-status'
|
||||
import { HistoryEntryProps } from '../history-content/history-content'
|
||||
|
||||
export const HistoryTableRow: React.FC<HistoryEntryProps> = ({ entry, onPinClick }) => {
|
||||
export const HistoryTableRow: React.FC<HistoryEntryProps> = ({ entry, onPinClick, onSyncClick }) => {
|
||||
return (
|
||||
<tr>
|
||||
<td>{entry.title}</td>
|
||||
<td>{formatHistoryDate(entry.lastVisited)}</td>
|
||||
<td>
|
||||
{
|
||||
entry.tags.map((tag) => <Badge variant={'dark'} className={'mr-1 mb-1'}
|
||||
entry.tags.map((tag) => <Badge variant={'light'} className={'mr-1 mb-1'}
|
||||
key={tag}>{tag}</Badge>)
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<PinButton isDark={true} isPinned={entry.pinned} onPinClick={() => {
|
||||
onPinClick(entry.id)
|
||||
}}/>
|
||||
|
||||
<CloseButton isDark={true}/>
|
||||
<SyncStatus isDark={true} location={entry.location} onSync={() => onSyncClick(entry.id)} className={'mb-1 mr-1'}/>
|
||||
<PinButton isDark={true} isPinned={entry.pinned} onPinClick={() => onPinClick(entry.id)} className={'mb-1 mr-1'}/>
|
||||
<CloseButton isDark={true} className={'mb-1 mr-1'}/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ import { HistoryEntriesProps } from '../history-content/history-content'
|
|||
import { HistoryTableRow } from './history-table-row'
|
||||
import './history-table.scss'
|
||||
|
||||
export const HistoryTable: React.FC<HistoryEntriesProps> = ({ entries, onPinClick, pageIndex, onLastPageIndexChange }) => {
|
||||
export const HistoryTable: React.FC<HistoryEntriesProps> = ({ entries, onPinClick, onSyncClick, pageIndex, onLastPageIndexChange }) => {
|
||||
useTranslation()
|
||||
return (
|
||||
<Table striped bordered hover size="sm" variant="dark" className={'history-table'}>
|
||||
|
@ -19,13 +19,14 @@ export const HistoryTable: React.FC<HistoryEntriesProps> = ({ entries, onPinClic
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Pager numberOfElementsPerPage={6} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}>
|
||||
<Pager numberOfElementsPerPage={12} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}>
|
||||
{
|
||||
entries.map((entry) =>
|
||||
<HistoryTableRow
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
onPinClick={onPinClick}
|
||||
onSyncClick={onSyncClick}
|
||||
/>)
|
||||
}
|
||||
</Pager>
|
||||
|
|
|
@ -37,7 +37,7 @@ export interface HistoryToolbarProps {
|
|||
export const initState: HistoryToolbarState = {
|
||||
viewState: ViewStateEnum.CARD,
|
||||
titleSortDirection: SortModeEnum.no,
|
||||
lastVisitedSortDirection: SortModeEnum.no,
|
||||
lastVisitedSortDirection: SortModeEnum.down,
|
||||
keywordSearch: '',
|
||||
selectedTags: []
|
||||
}
|
||||
|
@ -114,14 +114,16 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
|||
</Button>
|
||||
</InputGroup>
|
||||
<InputGroup className={'mr-1 mb-1'}>
|
||||
<ToggleButtonGroup type="radio" name="options" value={state.viewState}
|
||||
<ToggleButtonGroup type="radio" name="options" value={state.viewState} className={'button-height'}
|
||||
onChange={(newViewState: ViewStateEnum) => {
|
||||
toggleViewChanged(newViewState)
|
||||
}}>
|
||||
<ToggleButton className={'btn-light'} value={ViewStateEnum.CARD}><Trans
|
||||
i18nKey={'landing.history.toolbar.cards'}/></ToggleButton>
|
||||
<ToggleButton className={'btn-light'} value={ViewStateEnum.TABLE}><Trans
|
||||
i18nKey={'landing.history.toolbar.table'}/></ToggleButton>
|
||||
<ToggleButton className={'btn-light'} value={ViewStateEnum.CARD} title={t('landing.history.toolbar.cards')}>
|
||||
<ForkAwesomeIcon icon={'sticky-note'} className={'fa-fix-line-height'}/>
|
||||
</ToggleButton>
|
||||
<ToggleButton className={'btn-light'} value={ViewStateEnum.TABLE} title={t('landing.history.toolbar.table')}>
|
||||
<ForkAwesomeIcon icon={'table'} className={'fa-fix-line-height'}/>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Row } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { deleteHistory, getHistory, setHistory } from '../../../../api/history'
|
||||
import { ApplicationState } from '../../../../redux'
|
||||
import {
|
||||
collectEntries,
|
||||
downloadHistory,
|
||||
loadHistoryFromLocalStore,
|
||||
mergeEntryArrays,
|
||||
setHistoryToLocalStore,
|
||||
sortAndFilterEntries
|
||||
} from '../../../../utils/historyUtils'
|
||||
import { ErrorModal } from '../../../error-modal/error-modal'
|
||||
import { HistoryContent } from './history-content/history-content'
|
||||
import { HistoryToolbar, HistoryToolbarState, initState as toolbarInitState } from './history-toolbar/history-toolbar'
|
||||
|
||||
|
@ -23,49 +29,87 @@ export interface HistoryJson {
|
|||
entries: HistoryEntry[]
|
||||
}
|
||||
|
||||
export type pinClick = (entryId: string) => void;
|
||||
export type LocatedHistoryEntry = HistoryEntry & HistoryEntryLocation
|
||||
|
||||
export interface HistoryEntryLocation {
|
||||
location: Location
|
||||
}
|
||||
|
||||
export enum Location {
|
||||
LOCAL = 'local',
|
||||
REMOTE = 'remote'
|
||||
}
|
||||
|
||||
export const History: React.FC = () => {
|
||||
useTranslation()
|
||||
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([])
|
||||
const [viewState, setViewState] = useState<HistoryToolbarState>(toolbarInitState)
|
||||
const [localHistoryEntries, setLocalHistoryEntries] = useState<HistoryEntry[]>(loadHistoryFromLocalStore)
|
||||
const [remoteHistoryEntries, setRemoteHistoryEntries] = useState<HistoryEntry[]>([])
|
||||
const [toolbarState, setToolbarState] = useState<HistoryToolbarState>(toolbarInitState)
|
||||
const user = useSelector((state: ApplicationState) => state.user)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
refreshHistory()
|
||||
const historyWrite = useCallback((entries: HistoryEntry[]) => {
|
||||
if (!entries) {
|
||||
return
|
||||
}
|
||||
setHistoryToLocalStore(entries)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!historyEntries || historyEntries === []) {
|
||||
return
|
||||
}
|
||||
setHistoryToLocalStore(historyEntries)
|
||||
}, [historyEntries])
|
||||
historyWrite(localHistoryEntries)
|
||||
}, [historyWrite, localHistoryEntries])
|
||||
|
||||
const exportHistory = () => {
|
||||
const importHistory = useCallback((entries: HistoryEntry[]): void => {
|
||||
if (user) {
|
||||
setHistory(entries)
|
||||
.then(() => setRemoteHistoryEntries(entries))
|
||||
.catch(() => setError('setHistory'))
|
||||
} else {
|
||||
historyWrite(entries)
|
||||
setLocalHistoryEntries(entries)
|
||||
}
|
||||
}, [historyWrite, user])
|
||||
|
||||
const refreshHistory = useCallback(() => {
|
||||
const localHistory = loadHistoryFromLocalStore()
|
||||
setLocalHistoryEntries(localHistory)
|
||||
if (user) {
|
||||
getHistory()
|
||||
.then((remoteHistory) => setRemoteHistoryEntries(remoteHistory))
|
||||
.catch(() => setError('getHistory'))
|
||||
}
|
||||
}, [user])
|
||||
|
||||
useEffect(() => {
|
||||
refreshHistory()
|
||||
}, [refreshHistory])
|
||||
|
||||
const exportHistory = useCallback(() => {
|
||||
const dataObject: HistoryJson = {
|
||||
version: 2,
|
||||
entries: historyEntries
|
||||
entries: mergeEntryArrays(localHistoryEntries, remoteHistoryEntries)
|
||||
}
|
||||
downloadHistory(dataObject)
|
||||
}
|
||||
}, [localHistoryEntries, remoteHistoryEntries])
|
||||
|
||||
const importHistory = (entries: HistoryEntry[]): void => {
|
||||
setHistoryToLocalStore(entries)
|
||||
setHistoryEntries(entries)
|
||||
}
|
||||
const clearHistory = useCallback(() => {
|
||||
setLocalHistoryEntries([])
|
||||
if (user) {
|
||||
deleteHistory()
|
||||
.then(() => setRemoteHistoryEntries([]))
|
||||
.catch(() => setError('deleteHistory'))
|
||||
}
|
||||
historyWrite([])
|
||||
}, [historyWrite, user])
|
||||
|
||||
const refreshHistory = () => {
|
||||
const history = loadHistoryFromLocalStore()
|
||||
setHistoryEntries(history)
|
||||
}
|
||||
const syncClick = useCallback((entryId: string): void => {
|
||||
console.log(entryId)
|
||||
// ToDo: add syncClick
|
||||
}, [])
|
||||
|
||||
const clearHistory = () => {
|
||||
setHistoryToLocalStore([])
|
||||
setHistoryEntries([])
|
||||
}
|
||||
|
||||
const pinClick: pinClick = (entryId: string) => {
|
||||
setHistoryEntries((entries) => {
|
||||
const pinClick = useCallback((entryId: string): void => {
|
||||
// ToDo: determine if entry is local or remote
|
||||
setLocalHistoryEntries((entries) => {
|
||||
return entries.map((entry) => {
|
||||
if (entry.id === entryId) {
|
||||
entry.pinned = !entry.pinned
|
||||
|
@ -73,24 +117,43 @@ export const History: React.FC = () => {
|
|||
return entry
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
const resetError = () => {
|
||||
setError('')
|
||||
}
|
||||
|
||||
const tags = historyEntries.map(entry => entry.tags)
|
||||
.reduce((a, b) => ([...a, ...b]), [])
|
||||
.filter((value, index, array) => {
|
||||
if (index === 0) {
|
||||
return true
|
||||
}
|
||||
return (value !== array[index - 1])
|
||||
})
|
||||
const entriesToShow = sortAndFilterEntries(historyEntries, viewState)
|
||||
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[]>(() =>
|
||||
sortAndFilterEntries(allEntries, toolbarState),
|
||||
[allEntries, toolbarState])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ErrorModal show={error !== ''} onHide={resetError}
|
||||
title={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={setViewState}
|
||||
onSettingsChange={setToolbarState}
|
||||
tags={tags}
|
||||
onClearHistory={clearHistory}
|
||||
onRefreshHistory={refreshHistory}
|
||||
|
@ -98,9 +161,11 @@ export const History: React.FC = () => {
|
|||
onImportHistory={importHistory}
|
||||
/>
|
||||
</Row>
|
||||
<HistoryContent viewState={viewState.viewState}
|
||||
<HistoryContent viewState={toolbarState.viewState}
|
||||
entries={entriesToShow}
|
||||
onPinClick={pinClick}/>
|
||||
onPinClick={pinClick}
|
||||
onSyncClick={syncClick}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,16 +7,18 @@ export interface PagerPageProps {
|
|||
}
|
||||
|
||||
export const Pager: React.FC<PagerPageProps> = ({ children, numberOfElementsPerPage, pageIndex, onLastPageIndexChange }) => {
|
||||
const maxPageIndex = Math.ceil(React.Children.count(children) / numberOfElementsPerPage) - 1
|
||||
const correctedPageIndex = Math.min(maxPageIndex, Math.max(0, pageIndex))
|
||||
|
||||
useEffect(() => {
|
||||
const lastPageIndex = Math.ceil(React.Children.count(children) / numberOfElementsPerPage) - 1
|
||||
onLastPageIndexChange(lastPageIndex)
|
||||
}, [children, numberOfElementsPerPage, onLastPageIndexChange])
|
||||
onLastPageIndexChange(maxPageIndex)
|
||||
}, [children, maxPageIndex, numberOfElementsPerPage, onLastPageIndexChange])
|
||||
|
||||
return <Fragment>
|
||||
{
|
||||
React.Children.toArray(children).filter((value, index) => {
|
||||
const pageOfElement = Math.floor((index) / numberOfElementsPerPage)
|
||||
return (pageOfElement === pageIndex)
|
||||
return (pageOfElement === correctedPageIndex)
|
||||
})
|
||||
}
|
||||
</Fragment>
|
||||
|
|
|
@ -16,3 +16,10 @@ body {
|
|||
outline: 0 !important;
|
||||
}
|
||||
|
||||
.mt-1dot5 {
|
||||
margin-top: 0.375rem !important;
|
||||
}
|
||||
|
||||
.fa.fa-fix-line-height {
|
||||
line-height: inherit;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,39 @@
|
|||
import moment from 'moment'
|
||||
import { HistoryEntry, HistoryJson } from '../components/landing/pages/history/history'
|
||||
import { HistoryEntry, HistoryJson, LocatedHistoryEntry, Location } 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'
|
||||
|
||||
export function sortAndFilterEntries (entries: HistoryEntry[], viewState: HistoryToolbarState): HistoryEntry[] {
|
||||
return sortEntries(filterByKeywordSearch(filterBySelectedTags(entries, viewState.selectedTags), viewState.keywordSearch), viewState)
|
||||
export function collectEntries (localEntries: HistoryEntry[], remoteEntries: HistoryEntry[]): LocatedHistoryEntry[] {
|
||||
const locatedLocalEntries = locateEntries(localEntries, Location.LOCAL)
|
||||
const locatedRemoteEntries = locateEntries(remoteEntries, Location.REMOTE)
|
||||
return mergeEntryArrays(locatedLocalEntries, locatedRemoteEntries)
|
||||
}
|
||||
|
||||
function filterBySelectedTags (entries: HistoryEntry[], selectedTags: string[]): HistoryEntry[] {
|
||||
export function sortAndFilterEntries (entries: LocatedHistoryEntry[], toolbarState: HistoryToolbarState): LocatedHistoryEntry[] {
|
||||
const filteredBySelectedTagsEntries = filterBySelectedTags(entries, toolbarState.selectedTags)
|
||||
const filteredByKeywordSearchEntries = filterByKeywordSearch(filteredBySelectedTagsEntries, toolbarState.keywordSearch)
|
||||
return sortEntries(filteredByKeywordSearchEntries, toolbarState)
|
||||
}
|
||||
|
||||
function locateEntries (entries: HistoryEntry[], location: Location): LocatedHistoryEntry[] {
|
||||
return entries.map(entry => {
|
||||
return {
|
||||
...entry,
|
||||
location: location
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function mergeEntryArrays<T extends HistoryEntry> (locatedLocalEntries: T[], locatedRemoteEntries: T[]): T[] {
|
||||
const filteredLocalEntries = locatedLocalEntries.filter(localEntry => {
|
||||
const entry = locatedRemoteEntries.find(remoteEntry => remoteEntry.id === localEntry.id)
|
||||
return !entry
|
||||
})
|
||||
|
||||
return filteredLocalEntries.concat(locatedRemoteEntries)
|
||||
}
|
||||
|
||||
function filterBySelectedTags (entries: LocatedHistoryEntry[], selectedTags: string[]): LocatedHistoryEntry[] {
|
||||
return entries.filter(entry => {
|
||||
return (selectedTags.length === 0 || arrayCommonCheck(entry.tags, selectedTags))
|
||||
}
|
||||
|
@ -23,12 +49,12 @@ function arrayCommonCheck<T> (array1: T[], array2: T[]): boolean {
|
|||
return !!foundElement
|
||||
}
|
||||
|
||||
function filterByKeywordSearch (entries: HistoryEntry[], keywords: string): HistoryEntry[] {
|
||||
function filterByKeywordSearch (entries: LocatedHistoryEntry[], keywords: string): LocatedHistoryEntry[] {
|
||||
const searchTerm = keywords.toLowerCase()
|
||||
return entries.filter(entry => entry.title.toLowerCase().indexOf(searchTerm) !== -1)
|
||||
}
|
||||
|
||||
function sortEntries (entries: HistoryEntry[], viewState: HistoryToolbarState): HistoryEntry[] {
|
||||
function sortEntries (entries: LocatedHistoryEntry[], viewState: HistoryToolbarState): LocatedHistoryEntry[] {
|
||||
return entries.sort((firstEntry, secondEntry) => {
|
||||
if (firstEntry.pinned && !secondEntry.pinned) {
|
||||
return -1
|
||||
|
|
Loading…
Reference in a new issue