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"
|
"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": {
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
.history-close {
|
.history-close {
|
||||||
opacity: 0.5;
|
.fa {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover .fa {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
.history-pin {
|
.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;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.pinned .fa {
|
||||||
color: #d43f3a;
|
color: #d43f3a;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
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 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>
|
||||||
|
|
|
@ -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 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>
|
<div className={'d-flex flex-column justify-content-between'}>
|
||||||
<Card.Body>
|
<Card.Title className="m-0 mt-1dot5">{entry.title}</Card.Title>
|
||||||
<div className="text-black-50">
|
|
||||||
<ForkAwesomeIcon icon="clock-o"/> {moment(entry.lastVisited).fromNow()}<br/>
|
|
||||||
{formatHistoryDate(entry.lastVisited)}
|
|
||||||
<div>
|
<div>
|
||||||
{
|
<div className="text-black-50 mt-2">
|
||||||
entry.tags.map((tag) => <Badge variant={'dark'} className={'mr-1 mb-1'}
|
<ForkAwesomeIcon icon="clock-o"/> {moment(entry.lastVisited).fromNow()}<br/>
|
||||||
key={tag}>{tag}</Badge>)
|
{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>
|
</div>
|
||||||
|
<div className={'d-flex flex-column'}>
|
||||||
|
<CloseButton isDark={false}/>
|
||||||
|
</div>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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}/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'}/>
|
||||||
|
|
||||||
<CloseButton isDark={true}/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,24 +117,43 @@ export const History: React.FC = () => {
|
||||||
return entry
|
return entry
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const resetError = () => {
|
||||||
|
setError('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = historyEntries.map(entry => entry.tags)
|
const allEntries = useMemo(() => {
|
||||||
.reduce((a, b) => ([...a, ...b]), [])
|
return collectEntries(localHistoryEntries, remoteHistoryEntries)
|
||||||
.filter((value, index, array) => {
|
}, [localHistoryEntries, remoteHistoryEntries])
|
||||||
if (index === 0) {
|
|
||||||
return true
|
const tags = useMemo<string[]>(() => {
|
||||||
}
|
return allEntries.map(entry => entry.tags)
|
||||||
return (value !== array[index - 1])
|
.reduce((a, b) => ([...a, ...b]), [])
|
||||||
})
|
.filter((value, index, array) => {
|
||||||
const entriesToShow = sortAndFilterEntries(historyEntries, viewState)
|
if (index === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return (value !== array[index - 1])
|
||||||
|
})
|
||||||
|
}, [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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue