show local and remote History (#128)

* added history toolbar functionality for local and remote history
This commit is contained in:
Philip Molares 2020-06-07 01:09:04 +02:00 committed by GitHub
parent 23c854dc9a
commit 7b5b73a289
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 336 additions and 130 deletions

37
public/api/v2/history Normal file
View 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 - Lets meet! (2020-06-06 18:00 UTC / 20:00 CEST)",
"lastVisited": "2020-05-24T16:04:36.433Z",
"tags": [
"agenda",
"CodiMD community",
"community call"
]
}
]

View file

@ -14,6 +14,20 @@
"screenShotAltText": "CodiMD Screenshot" "screenShotAltText": "CodiMD Screenshot"
}, },
"history": { "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", "noHistory": "No history",
"localHistory": "Below is history from this browser", "localHistory": "Below is history from this browser",
"toolbar": { "toolbar": {

View file

@ -1,7 +1,9 @@
.history-close { .history-close {
.fa {
opacity: 0.5; opacity: 0.5;
}
&:hover { &:hover .fa {
opacity: 1; opacity: 1;
} }
} }

View file

@ -1,19 +1,17 @@
import React from 'react' import React from 'react'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import './close-button.scss'
import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon' import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon'
import './close-button.scss'
export interface CloseButtonProps { export interface CloseButtonProps {
isDark: boolean; isDark: boolean;
className?: string
} }
const CloseButton: React.FC<CloseButtonProps> = ({ isDark }) => { const CloseButton: React.FC<CloseButtonProps> = ({ isDark, className }) => {
return ( return (
<Button variant={isDark ? 'secondary' : 'light'}> <Button variant={isDark ? 'secondary' : 'light'} className={`history-close ${className || ''}`}>
<ForkAwesomeIcon <ForkAwesomeIcon icon="times"/>
className="history-close"
icon="times"
/>
</Button> </Button>
) )
} }

View file

@ -1,12 +1,15 @@
.history-pin { .history-pin {
.fa {
opacity: 0.2; opacity: 0.2;
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out; transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
}
&:hover { &:hover .fa {
opacity: 1; opacity: 1;
} }
&.active { &.pinned .fa {
color: #d43f3a; color: #d43f3a;
opacity: 1; opacity: 1;
} }

View file

@ -1,22 +1,20 @@
import React from 'react' import React from 'react'
import './pin-button.scss'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon' import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon'
import './pin-button.scss'
export interface PinButtonProps { export interface PinButtonProps {
isPinned: boolean; isPinned: boolean;
onPinClick: () => void; onPinClick: () => void;
isDark: boolean; isDark: boolean;
className?: string
} }
export const PinButton: React.FC<PinButtonProps> = ({ isPinned, onPinClick, isDark }) => { export const PinButton: React.FC<PinButtonProps> = ({ isPinned, onPinClick, isDark, className }) => {
return ( return (
<Button variant={isDark ? 'secondary' : 'light'} <Button variant={isDark ? 'secondary' : 'light'}
onClick={onPinClick}> className={`history-pin ${className || ''} ${isPinned ? 'pinned' : ''}`} onClick={onPinClick}>
<ForkAwesomeIcon <ForkAwesomeIcon icon="thumb-tack"/>
icon="thumb-tack"
className={`history-pin ${isPinned ? 'active' : ''}`}
/>
</Button> </Button>
) )
} }

View 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;
}
}

View 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>
)
}

View file

@ -1,19 +1,20 @@
import React from 'react' import React from 'react'
import { Row } from 'react-bootstrap'
import { Pager } from '../../../../pagination/pager'
import { HistoryEntriesProps } from '../history-content/history-content' import { HistoryEntriesProps } from '../history-content/history-content'
import { HistoryCard } from './history-card' 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 ( return (
<Row className="justify-content-center"> <Row className="justify-content-start">
<Pager numberOfElementsPerPage={6} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}> <Pager numberOfElementsPerPage={9} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}>
{ {
entries.map((entry) => ( entries.map((entry) => (
<HistoryCard <HistoryCard
key={entry.id} key={entry.id}
entry={entry} entry={entry}
onPinClick={onPinClick} onPinClick={onPinClick}
onSyncClick={onSyncClick}
/>)) />))
} }
</Pager> </Pager>

View file

@ -0,0 +1,7 @@
.card-min-height {
min-height: 160px;
}
.card-footer-min-height {
min-height: 27px;
}

View file

@ -1,34 +1,41 @@
import moment from 'moment'
import React from 'react' import React from 'react'
import { Badge, Card } from 'react-bootstrap' import { Badge, Card } from 'react-bootstrap'
import { ForkAwesomeIcon } from '../../../../../fork-awesome/fork-awesome-icon' 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 { 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 ( return (
<div className="p-2 col-xs-12 col-sm-6 col-md-6 col-lg-4"> <div className="p-2 col-xs-12 col-sm-6 col-md-6 col-lg-4">
<Card className="p-0" text={'dark'} bg={'light'}> <Card className="card-min-height" text={'dark'} bg={'light'}>
<div className="d-flex justify-content-between p-2 align-items-start"> <Card.Body className="p-2 d-flex flex-row justify-content-between">
<PinButton isDark={false} isPinned={entry.pinned} onPinClick={() => { <div className={'d-flex flex-column'}>
onPinClick(entry.id) <PinButton isDark={false} isPinned={entry.pinned} onPinClick={() => onPinClick(entry.id)}/>
}}/> <SyncStatus isDark={false} location={entry.location} onSync={() => onSyncClick(entry.id)}
<Card.Title className="m-0 mt-3">{entry.title}</Card.Title> className={'mt-1'}/>
<CloseButton isDark={false}/>
</div> </div>
<Card.Body> <div className={'d-flex flex-column justify-content-between'}>
<div className="text-black-50"> <Card.Title className="m-0 mt-1dot5">{entry.title}</Card.Title>
<div>
<div className="text-black-50 mt-2">
<ForkAwesomeIcon icon="clock-o"/> {moment(entry.lastVisited).fromNow()}<br/> <ForkAwesomeIcon icon="clock-o"/> {moment(entry.lastVisited).fromNow()}<br/>
{formatHistoryDate(entry.lastVisited)} {formatHistoryDate(entry.lastVisited)}
<div> </div>
<div className={'card-footer-min-height p-0'}>
{ {
entry.tags.map((tag) => <Badge variant={'dark'} className={'mr-1 mb-1'} entry.tags.map((tag) => <Badge variant={'dark'} className={'mr-1 mb-1'} key={tag}>{tag}</Badge>)
key={tag}>{tag}</Badge>)
} }
</div> </div>
</div> </div>
</div>
<div className={'d-flex flex-column'}>
<CloseButton isDark={false}/>
</div>
</Card.Body> </Card.Body>
</Card> </Card>
</div> </div>

View file

@ -1,31 +1,34 @@
import React, { Fragment, useState } from 'react' 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 { Alert, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' 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 { 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 { export interface HistoryContentProps {
viewState: ViewStateEnum viewState: ViewStateEnum
entries: HistoryEntry[] entries: LocatedHistoryEntry[]
onPinClick: pinClick onPinClick: (entryId: string) => void
onSyncClick: (entryId: string) => void
} }
export interface HistoryEntryProps { export interface HistoryEntryProps {
entry: HistoryEntry, entry: LocatedHistoryEntry,
onPinClick: pinClick onPinClick: (entryId: string) => void
onSyncClick: (entryId: string) => void
} }
export interface HistoryEntriesProps { export interface HistoryEntriesProps {
entries: HistoryEntry[] entries: LocatedHistoryEntry[]
onPinClick: pinClick onPinClick: (entryId: string) => void
onSyncClick: (entryId: string) => void
pageIndex: number pageIndex: number
onLastPageIndexChange: (lastPageIndex: number) => void onLastPageIndexChange: (lastPageIndex: number) => void
} }
export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entries, onPinClick }) => { export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entries, onPinClick, onSyncClick }) => {
useTranslation() useTranslation()
const [pageIndex, setPageIndex] = useState(0) const [pageIndex, setPageIndex] = useState(0)
const [lastPageIndex, setLastPageIndex] = useState(0) const [lastPageIndex, setLastPageIndex] = useState(0)
@ -44,10 +47,13 @@ export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entri
switch (viewState) { switch (viewState) {
default: default:
case ViewStateEnum.CARD: case ViewStateEnum.CARD:
return <HistoryCardList entries={entries} onPinClick={onPinClick} pageIndex={pageIndex} return <HistoryCardList entries={entries}
onPinClick={onPinClick}
onSyncClick={onSyncClick}
pageIndex={pageIndex}
onLastPageIndexChange={setLastPageIndex}/> onLastPageIndexChange={setLastPageIndex}/>
case ViewStateEnum.TABLE: case ViewStateEnum.TABLE:
return <HistoryTable entries={entries} onPinClick={onPinClick} pageIndex={pageIndex} return <HistoryTable entries={entries} onPinClick={onPinClick} onSyncClick={onSyncClick} pageIndex={pageIndex}
onLastPageIndexChange={setLastPageIndex}/> onLastPageIndexChange={setLastPageIndex}/>
} }
} }

View file

@ -1,27 +1,26 @@
import React from 'react' 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 { 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 ( return (
<tr> <tr>
<td>{entry.title}</td> <td>{entry.title}</td>
<td>{formatHistoryDate(entry.lastVisited)}</td> <td>{formatHistoryDate(entry.lastVisited)}</td>
<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>) key={tag}>{tag}</Badge>)
} }
</td> </td>
<td> <td>
<PinButton isDark={true} isPinned={entry.pinned} onPinClick={() => { <SyncStatus isDark={true} location={entry.location} onSync={() => onSyncClick(entry.id)} className={'mb-1 mr-1'}/>
onPinClick(entry.id) <PinButton isDark={true} isPinned={entry.pinned} onPinClick={() => onPinClick(entry.id)} className={'mb-1 mr-1'}/>
}}/> <CloseButton isDark={true} className={'mb-1 mr-1'}/>
&nbsp;
<CloseButton isDark={true}/>
</td> </td>
</tr> </tr>
) )

View file

@ -6,7 +6,7 @@ import { HistoryEntriesProps } from '../history-content/history-content'
import { HistoryTableRow } from './history-table-row' import { HistoryTableRow } from './history-table-row'
import './history-table.scss' 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() useTranslation()
return ( return (
<Table striped bordered hover size="sm" variant="dark" className={'history-table'}> <Table striped bordered hover size="sm" variant="dark" className={'history-table'}>
@ -19,13 +19,14 @@ export const HistoryTable: React.FC<HistoryEntriesProps> = ({ entries, onPinClic
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<Pager numberOfElementsPerPage={6} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}> <Pager numberOfElementsPerPage={12} pageIndex={pageIndex} onLastPageIndexChange={onLastPageIndexChange}>
{ {
entries.map((entry) => entries.map((entry) =>
<HistoryTableRow <HistoryTableRow
key={entry.id} key={entry.id}
entry={entry} entry={entry}
onPinClick={onPinClick} onPinClick={onPinClick}
onSyncClick={onSyncClick}
/>) />)
} }
</Pager> </Pager>

View file

@ -37,7 +37,7 @@ export interface HistoryToolbarProps {
export const initState: HistoryToolbarState = { export const initState: HistoryToolbarState = {
viewState: ViewStateEnum.CARD, viewState: ViewStateEnum.CARD,
titleSortDirection: SortModeEnum.no, titleSortDirection: SortModeEnum.no,
lastVisitedSortDirection: SortModeEnum.no, lastVisitedSortDirection: SortModeEnum.down,
keywordSearch: '', keywordSearch: '',
selectedTags: [] selectedTags: []
} }
@ -114,14 +114,16 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
</Button> </Button>
</InputGroup> </InputGroup>
<InputGroup className={'mr-1 mb-1'}> <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) => { onChange={(newViewState: ViewStateEnum) => {
toggleViewChanged(newViewState) toggleViewChanged(newViewState)
}}> }}>
<ToggleButton className={'btn-light'} value={ViewStateEnum.CARD}><Trans <ToggleButton className={'btn-light'} value={ViewStateEnum.CARD} title={t('landing.history.toolbar.cards')}>
i18nKey={'landing.history.toolbar.cards'}/></ToggleButton> <ForkAwesomeIcon icon={'sticky-note'} className={'fa-fix-line-height'}/>
<ToggleButton className={'btn-light'} value={ViewStateEnum.TABLE}><Trans </ToggleButton>
i18nKey={'landing.history.toolbar.table'}/></ToggleButton> <ToggleButton className={'btn-light'} value={ViewStateEnum.TABLE} title={t('landing.history.toolbar.table')}>
<ForkAwesomeIcon icon={'table'} className={'fa-fix-line-height'}/>
</ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
</InputGroup> </InputGroup>
</Form> </Form>

View file

@ -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 { Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { deleteHistory, getHistory, setHistory } from '../../../../api/history'
import { ApplicationState } from '../../../../redux'
import { import {
collectEntries,
downloadHistory, downloadHistory,
loadHistoryFromLocalStore, loadHistoryFromLocalStore,
mergeEntryArrays,
setHistoryToLocalStore, setHistoryToLocalStore,
sortAndFilterEntries sortAndFilterEntries
} from '../../../../utils/historyUtils' } from '../../../../utils/historyUtils'
import { ErrorModal } from '../../../error-modal/error-modal'
import { HistoryContent } from './history-content/history-content' import { HistoryContent } from './history-content/history-content'
import { HistoryToolbar, HistoryToolbarState, initState as toolbarInitState } from './history-toolbar/history-toolbar' import { HistoryToolbar, HistoryToolbarState, initState as toolbarInitState } from './history-toolbar/history-toolbar'
@ -23,49 +29,87 @@ export interface HistoryJson {
entries: HistoryEntry[] 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 = () => { export const History: React.FC = () => {
useTranslation() useTranslation()
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([]) const [localHistoryEntries, setLocalHistoryEntries] = useState<HistoryEntry[]>(loadHistoryFromLocalStore)
const [viewState, setViewState] = useState<HistoryToolbarState>(toolbarInitState) const [remoteHistoryEntries, setRemoteHistoryEntries] = useState<HistoryEntry[]>([])
const [toolbarState, setToolbarState] = useState<HistoryToolbarState>(toolbarInitState)
const user = useSelector((state: ApplicationState) => state.user)
const [error, setError] = useState('')
useEffect(() => { const historyWrite = useCallback((entries: HistoryEntry[]) => {
refreshHistory() if (!entries) {
return
}
setHistoryToLocalStore(entries)
}, []) }, [])
useEffect(() => { useEffect(() => {
if (!historyEntries || historyEntries === []) { historyWrite(localHistoryEntries)
return }, [historyWrite, localHistoryEntries])
}
setHistoryToLocalStore(historyEntries)
}, [historyEntries])
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 = { const dataObject: HistoryJson = {
version: 2, version: 2,
entries: historyEntries entries: mergeEntryArrays(localHistoryEntries, remoteHistoryEntries)
} }
downloadHistory(dataObject) downloadHistory(dataObject)
} }, [localHistoryEntries, remoteHistoryEntries])
const importHistory = (entries: HistoryEntry[]): void => { const clearHistory = useCallback(() => {
setHistoryToLocalStore(entries) setLocalHistoryEntries([])
setHistoryEntries(entries) if (user) {
deleteHistory()
.then(() => setRemoteHistoryEntries([]))
.catch(() => setError('deleteHistory'))
} }
historyWrite([])
}, [historyWrite, user])
const refreshHistory = () => { const syncClick = useCallback((entryId: string): void => {
const history = loadHistoryFromLocalStore() console.log(entryId)
setHistoryEntries(history) // ToDo: add syncClick
} }, [])
const clearHistory = () => { const pinClick = useCallback((entryId: string): void => {
setHistoryToLocalStore([]) // ToDo: determine if entry is local or remote
setHistoryEntries([]) setLocalHistoryEntries((entries) => {
}
const pinClick: pinClick = (entryId: string) => {
setHistoryEntries((entries) => {
return entries.map((entry) => { return entries.map((entry) => {
if (entry.id === entryId) { if (entry.id === entryId) {
entry.pinned = !entry.pinned entry.pinned = !entry.pinned
@ -73,9 +117,18 @@ export const History: React.FC = () => {
return entry return entry
}) })
}) })
}, [])
const resetError = () => {
setError('')
} }
const tags = historyEntries.map(entry => entry.tags) const allEntries = useMemo(() => {
return collectEntries(localHistoryEntries, remoteHistoryEntries)
}, [localHistoryEntries, remoteHistoryEntries])
const tags = useMemo<string[]>(() => {
return allEntries.map(entry => entry.tags)
.reduce((a, b) => ([...a, ...b]), []) .reduce((a, b) => ([...a, ...b]), [])
.filter((value, index, array) => { .filter((value, index, array) => {
if (index === 0) { if (index === 0) {
@ -83,14 +136,24 @@ export const History: React.FC = () => {
} }
return (value !== array[index - 1]) return (value !== array[index - 1])
}) })
const entriesToShow = sortAndFilterEntries(historyEntries, viewState) }, [allEntries])
const entriesToShow = useMemo<LocatedHistoryEntry[]>(() =>
sortAndFilterEntries(allEntries, toolbarState),
[allEntries, toolbarState])
return ( return (
<Fragment> <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> <h1 className="mb-4"><Trans i18nKey="landing.navigation.history"/></h1>
<Row className={'justify-content-center mt-5 mb-3'}> <Row className={'justify-content-center mt-5 mb-3'}>
<HistoryToolbar <HistoryToolbar
onSettingsChange={setViewState} onSettingsChange={setToolbarState}
tags={tags} tags={tags}
onClearHistory={clearHistory} onClearHistory={clearHistory}
onRefreshHistory={refreshHistory} onRefreshHistory={refreshHistory}
@ -98,9 +161,11 @@ export const History: React.FC = () => {
onImportHistory={importHistory} onImportHistory={importHistory}
/> />
</Row> </Row>
<HistoryContent viewState={viewState.viewState} <HistoryContent viewState={toolbarState.viewState}
entries={entriesToShow} entries={entriesToShow}
onPinClick={pinClick}/> onPinClick={pinClick}
onSyncClick={syncClick}
/>
</Fragment> </Fragment>
) )
} }

View file

@ -7,16 +7,18 @@ export interface PagerPageProps {
} }
export const Pager: React.FC<PagerPageProps> = ({ children, numberOfElementsPerPage, pageIndex, onLastPageIndexChange }) => { 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(() => { useEffect(() => {
const lastPageIndex = Math.ceil(React.Children.count(children) / numberOfElementsPerPage) - 1 onLastPageIndexChange(maxPageIndex)
onLastPageIndexChange(lastPageIndex) }, [children, maxPageIndex, numberOfElementsPerPage, onLastPageIndexChange])
}, [children, numberOfElementsPerPage, onLastPageIndexChange])
return <Fragment> return <Fragment>
{ {
React.Children.toArray(children).filter((value, index) => { React.Children.toArray(children).filter((value, index) => {
const pageOfElement = Math.floor((index) / numberOfElementsPerPage) const pageOfElement = Math.floor((index) / numberOfElementsPerPage)
return (pageOfElement === pageIndex) return (pageOfElement === correctedPageIndex)
}) })
} }
</Fragment> </Fragment>

View file

@ -16,3 +16,10 @@ body {
outline: 0 !important; outline: 0 !important;
} }
.mt-1dot5 {
margin-top: 0.375rem !important;
}
.fa.fa-fix-line-height {
line-height: inherit;
}

View file

@ -1,13 +1,39 @@
import moment from 'moment' 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 { HistoryToolbarState } from '../components/landing/pages/history/history-toolbar/history-toolbar'
import { SortModeEnum } from '../components/sort-button/sort-button' import { SortModeEnum } from '../components/sort-button/sort-button'
export function sortAndFilterEntries (entries: HistoryEntry[], viewState: HistoryToolbarState): HistoryEntry[] { export function collectEntries (localEntries: HistoryEntry[], remoteEntries: HistoryEntry[]): LocatedHistoryEntry[] {
return sortEntries(filterByKeywordSearch(filterBySelectedTags(entries, viewState.selectedTags), viewState.keywordSearch), viewState) 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 entries.filter(entry => {
return (selectedTags.length === 0 || arrayCommonCheck(entry.tags, selectedTags)) return (selectedTags.length === 0 || arrayCommonCheck(entry.tags, selectedTags))
} }
@ -23,12 +49,12 @@ function arrayCommonCheck<T> (array1: T[], array2: T[]): boolean {
return !!foundElement return !!foundElement
} }
function filterByKeywordSearch (entries: HistoryEntry[], keywords: string): HistoryEntry[] { function filterByKeywordSearch (entries: LocatedHistoryEntry[], keywords: string): LocatedHistoryEntry[] {
const searchTerm = keywords.toLowerCase() const searchTerm = keywords.toLowerCase()
return entries.filter(entry => entry.title.toLowerCase().indexOf(searchTerm) !== -1) 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) => { return entries.sort((firstEntry, secondEntry) => {
if (firstEntry.pinned && !secondEntry.pinned) { if (firstEntry.pinned && !secondEntry.pinned) {
return -1 return -1