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"
},
"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": {

View file

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

View file

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

View file

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

View file

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

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 { 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>

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 { 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>

View file

@ -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}/>
}
}

View file

@ -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)
}}/>
&nbsp;
<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>
)

View file

@ -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>

View file

@ -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>

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

View file

@ -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>

View file

@ -16,3 +16,10 @@ body {
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 { 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