diff --git a/public/locales/en.json b/public/locales/en.json index b95c85fb7..ef54bdc36 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -26,6 +26,22 @@ "setHistory": { "title": "Upload History Error", "text": "While trying to upload the history to the server an error occurred" + }, + "deleteNote": { + "title": "Delete Note Error", + "text": "While trying to delete a note on the server an error occurred" + }, + "updateEntry": { + "title": "Update History Entry Error", + "text": "While trying to update a history entry on the server an error occurred" + }, + "deleteEntry": { + "title": "Delete History Entry Error", + "text": "While trying to delete a history entry on the server an error occurred" + }, + "notFoundEntry": { + "title": "History Entry not found", + "text": "We can't find the history entry you requested." } }, "noHistory": "No history", @@ -40,7 +56,8 @@ "export": "Export history", "import": "Import history", "clear": "Clear history", - "refresh": "Refresh history" + "refresh": "Refresh history", + "uploadAll": "Sync the complete history to the server" }, "modal": { "clearHistory": { @@ -60,6 +77,13 @@ "actions": "Actions", "tags": "Tags", "lastVisit": "Last Visit" + }, + "menu": { + "recentNotes": "Recent notes", + "entryLocal": "Saved in your browser history", + "entryRemote": "Saved in your user history", + "removeEntry": "Remove from history", + "deleteNote": "Delete note" } }, "navigation": { diff --git a/src/api/note.ts b/src/api/note.ts new file mode 100644 index 000000000..5333ce719 --- /dev/null +++ b/src/api/note.ts @@ -0,0 +1,10 @@ +import { expectResponseCode, getBackendUrl } from '../utils/apiUtils' +import { defaultFetchConfig } from './default' + +export const deleteNote = async (noteId: string): Promise => { + const response = await fetch(getBackendUrl() + `/notes/${noteId}`, { + ...defaultFetchConfig, + method: 'DELETE' + }) + expectResponseCode(response) +} diff --git a/src/components/landing/pages/history/common/close-button.scss b/src/components/landing/pages/history/common/close-button.scss deleted file mode 100644 index ff4aea08b..000000000 --- a/src/components/landing/pages/history/common/close-button.scss +++ /dev/null @@ -1,9 +0,0 @@ -.history-close { - .fa { - opacity: 0.5; - } - - &:hover .fa { - opacity: 1; - } -} diff --git a/src/components/landing/pages/history/common/close-button.tsx b/src/components/landing/pages/history/common/close-button.tsx deleted file mode 100644 index b32e99a61..000000000 --- a/src/components/landing/pages/history/common/close-button.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import { Button } from 'react-bootstrap' -import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon' -import './close-button.scss' - -export interface CloseButtonProps { - isDark: boolean; - className?: string -} - -const CloseButton: React.FC = ({ isDark, className }) => { - return ( - - ) -} - -export { CloseButton } diff --git a/src/components/landing/pages/history/common/entry-menu.scss b/src/components/landing/pages/history/common/entry-menu.scss new file mode 100644 index 000000000..002ce5da8 --- /dev/null +++ b/src/components/landing/pages/history/common/entry-menu.scss @@ -0,0 +1,10 @@ +.history-menu { + + .fa, &::after { + opacity: 0.5; + } + + &:hover .fa, &:hover::after { + opacity: 1; + } +} diff --git a/src/components/landing/pages/history/common/entry-menu.tsx b/src/components/landing/pages/history/common/entry-menu.tsx new file mode 100644 index 000000000..7ee02f170 --- /dev/null +++ b/src/components/landing/pages/history/common/entry-menu.tsx @@ -0,0 +1,59 @@ +import React from 'react' +import { Dropdown } from 'react-bootstrap' +import { Trans } from 'react-i18next' +import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon' +import { ShowIf } from '../../../../common/show-if/show-if' +import { HistoryEntryOrigin } from '../history' +import './entry-menu.scss' + +export interface EntryMenuProps { + id: string; + location: HistoryEntryOrigin + isDark: boolean; + onRemove: () => void + onDelete: () => void + className?: string +} + +const EntryMenu: React.FC = ({ id, location, isDark, onRemove, onDelete, className }) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export { EntryMenu } diff --git a/src/components/landing/pages/history/common/sync-status.scss b/src/components/landing/pages/history/common/sync-status.scss deleted file mode 100644 index 8f3617c0b..000000000 --- a/src/components/landing/pages/history/common/sync-status.scss +++ /dev/null @@ -1,10 +0,0 @@ -.sync-icon { - .fa { - opacity: 0.2; - transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out; - } - - &:hover .fa { - opacity: 1; - } -} diff --git a/src/components/landing/pages/history/common/sync-status.tsx b/src/components/landing/pages/history/common/sync-status.tsx deleted file mode 100644 index dff01fcaf..000000000 --- a/src/components/landing/pages/history/common/sync-status.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import { Button } from 'react-bootstrap' -import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon' -import { HistoryEntryOrigin } from '../history' -import './sync-status.scss' - -export interface SyncStatusProps { - isDark: boolean - location: HistoryEntryOrigin - onSync: () => void - className?: string -} - -export const SyncStatus: React.FC = ({ isDark, location, onSync, className }) => { - const icon = location === HistoryEntryOrigin.REMOTE ? 'cloud' : 'laptop' - return ( - - ) -} diff --git a/src/components/landing/pages/history/history-card/history-card-list.tsx b/src/components/landing/pages/history/history-card/history-card-list.tsx index 230e57f8f..e3ee02bc4 100644 --- a/src/components/landing/pages/history/history-card/history-card-list.tsx +++ b/src/components/landing/pages/history/history-card/history-card-list.tsx @@ -4,7 +4,7 @@ import { Pager } from '../../../../common/pagination/pager' import { HistoryEntriesProps } from '../history-content/history-content' import { HistoryCard } from './history-card' -export const HistoryCardList: React.FC = ({ entries, onPinClick, onSyncClick, pageIndex, onLastPageIndexChange }) => { +export const HistoryCardList: React.FC = ({ entries, onPinClick, onRemoveClick, onDeleteClick, pageIndex, onLastPageIndexChange }) => { return ( @@ -14,7 +14,8 @@ export const HistoryCardList: React.FC = ({ entries, onPinC key={entry.id} entry={entry} onPinClick={onPinClick} - onSyncClick={onSyncClick} + onRemoveClick={onRemoveClick} + onDeleteClick={onDeleteClick} />)) } diff --git a/src/components/landing/pages/history/history-card/history-card.tsx b/src/components/landing/pages/history/history-card/history-card.tsx index c1131d830..6cfad7555 100644 --- a/src/components/landing/pages/history/history-card/history-card.tsx +++ b/src/components/landing/pages/history/history-card/history-card.tsx @@ -3,21 +3,18 @@ import React from 'react' import { Badge, Card } from 'react-bootstrap' import { formatHistoryDate } from '../../../../../utils/historyUtils' import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon' -import { CloseButton } from '../common/close-button' +import { EntryMenu } from '../common/entry-menu' 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 = ({ entry, onPinClick, onSyncClick }) => { +export const HistoryCard: React.FC = ({ entry, onPinClick, onRemoveClick }) => { return (
- onPinClick(entry.id)}/> - onSyncClick(entry.id)} - className={'mt-1'}/> + onPinClick(entry.id, entry.location)}/>
{entry.title} @@ -34,7 +31,13 @@ export const HistoryCard: React.FC = ({ entry, onPinClick, on
- + onRemoveClick(entry.id, entry.location)} + onDelete={() => onRemoveClick(entry.id, entry.location)} + />
diff --git a/src/components/landing/pages/history/history-content/history-content.tsx b/src/components/landing/pages/history/history-content/history-content.tsx index fa6bfd8b7..d4367ca5a 100644 --- a/src/components/landing/pages/history/history-content/history-content.tsx +++ b/src/components/landing/pages/history/history-content/history-content.tsx @@ -1,34 +1,39 @@ import React, { Fragment, useState } from 'react' import { Alert, Row } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' -import { LocatedHistoryEntry } from '../history' import { PagerPagination } from '../../../../common/pagination/pager-pagination' +import { HistoryEntryOrigin, 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' +type OnEntryClick = (entryId: string, location: HistoryEntryOrigin) => void + export interface HistoryContentProps { viewState: ViewStateEnum entries: LocatedHistoryEntry[] - onPinClick: (entryId: string) => void - onSyncClick: (entryId: string) => void + onPinClick: OnEntryClick + onRemoveClick: OnEntryClick + onDeleteClick: OnEntryClick } export interface HistoryEntryProps { entry: LocatedHistoryEntry, - onPinClick: (entryId: string) => void - onSyncClick: (entryId: string) => void + onPinClick: OnEntryClick + onRemoveClick: OnEntryClick + onDeleteClick: OnEntryClick } export interface HistoryEntriesProps { entries: LocatedHistoryEntry[] - onPinClick: (entryId: string) => void - onSyncClick: (entryId: string) => void + onPinClick: OnEntryClick + onRemoveClick: OnEntryClick + onDeleteClick: OnEntryClick pageIndex: number onLastPageIndexChange: (lastPageIndex: number) => void } -export const HistoryContent: React.FC = ({ viewState, entries, onPinClick, onSyncClick }) => { +export const HistoryContent: React.FC = ({ viewState, entries, onPinClick, onRemoveClick, onDeleteClick }) => { useTranslation() const [pageIndex, setPageIndex] = useState(0) const [lastPageIndex, setLastPageIndex] = useState(0) @@ -49,11 +54,16 @@ export const HistoryContent: React.FC = ({ viewState, entri case ViewStateEnum.CARD: return case ViewStateEnum.TABLE: - return } } diff --git a/src/components/landing/pages/history/history-table/history-table-row.tsx b/src/components/landing/pages/history/history-table/history-table-row.tsx index d031c96f6..d098f35c8 100644 --- a/src/components/landing/pages/history/history-table/history-table-row.tsx +++ b/src/components/landing/pages/history/history-table/history-table-row.tsx @@ -1,12 +1,11 @@ import React from 'react' import { Badge } from 'react-bootstrap' import { formatHistoryDate } from '../../../../../utils/historyUtils' -import { CloseButton } from '../common/close-button' +import { EntryMenu } from '../common/entry-menu' import { PinButton } from '../common/pin-button' -import { SyncStatus } from '../common/sync-status' import { HistoryEntryProps } from '../history-content/history-content' -export const HistoryTableRow: React.FC = ({ entry, onPinClick, onSyncClick }) => { +export const HistoryTableRow: React.FC = ({ entry, onPinClick, onRemoveClick }) => { return ( {entry.title} @@ -18,9 +17,14 @@ export const HistoryTableRow: React.FC = ({ entry, onPinClick } - onSyncClick(entry.id)} className={'mb-1 mr-1'}/> - onPinClick(entry.id)} className={'mb-1 mr-1'}/> - + onPinClick(entry.id, entry.location)} className={'mb-1 mr-1'}/> + onRemoveClick(entry.id, entry.location)} + onDelete={() => onRemoveClick(entry.id, entry.location)} + /> ) diff --git a/src/components/landing/pages/history/history-table/history-table.tsx b/src/components/landing/pages/history/history-table/history-table.tsx index b1a69ea0b..134650bd4 100644 --- a/src/components/landing/pages/history/history-table/history-table.tsx +++ b/src/components/landing/pages/history/history-table/history-table.tsx @@ -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 = ({ entries, onPinClick, onSyncClick, pageIndex, onLastPageIndexChange }) => { +export const HistoryTable: React.FC = ({ entries, onPinClick, onRemoveClick, onDeleteClick, pageIndex, onLastPageIndexChange }) => { useTranslation() return ( @@ -26,7 +26,8 @@ export const HistoryTable: React.FC = ({ entries, onPinClic key={entry.id} entry={entry} onPinClick={onPinClick} - onSyncClick={onSyncClick} + onRemoveClick={onRemoveClick} + onDeleteClick={onDeleteClick} />) } diff --git a/src/components/landing/pages/history/history-toolbar/history-toolbar.tsx b/src/components/landing/pages/history/history-toolbar/history-toolbar.tsx index 0e2e4e23b..f1b3feee3 100644 --- a/src/components/landing/pages/history/history-toolbar/history-toolbar.tsx +++ b/src/components/landing/pages/history/history-toolbar/history-toolbar.tsx @@ -2,7 +2,10 @@ import React, { ChangeEvent, useEffect, useState } from 'react' import { Button, Form, FormControl, InputGroup, ToggleButton, ToggleButtonGroup } from 'react-bootstrap' import { Typeahead } from 'react-bootstrap-typeahead' import { Trans, useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import { ApplicationState } from '../../../../../redux' import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon' +import { ShowIf } from '../../../../common/show-if/show-if' import { SortButton, SortModeEnum } from '../../../../common/sort-button/sort-button' import { HistoryEntry } from '../history' import { ClearHistoryButton } from './clear-history-button' @@ -32,6 +35,7 @@ export interface HistoryToolbarProps { onRefreshHistory: () => void onExportHistory: () => void onImportHistory: (entries: HistoryEntry[]) => void + onUploadAll: () => void } export const initState: HistoryToolbarState = { @@ -42,9 +46,10 @@ export const initState: HistoryToolbarState = { selectedTags: [] } -export const HistoryToolbar: React.FC = ({ onSettingsChange, tags, onClearHistory, onRefreshHistory, onExportHistory, onImportHistory }) => { +export const HistoryToolbar: React.FC = ({ onSettingsChange, tags, onClearHistory, onRefreshHistory, onExportHistory, onImportHistory, onUploadAll }) => { const [t] = useTranslation() const [state, setState] = useState(initState) + const user = useSelector((state: ApplicationState) => state.user) const titleSortChanged = (direction: SortModeEnum) => { setState(prevState => ({ @@ -113,6 +118,13 @@ export const HistoryToolbar: React.FC = ({ onSettingsChange + + + + + { diff --git a/src/components/landing/pages/history/history.tsx b/src/components/landing/pages/history/history.tsx index 4521ee7ef..7472a9f66 100644 --- a/src/components/landing/pages/history/history.tsx +++ b/src/components/landing/pages/history/history.tsx @@ -2,7 +2,8 @@ import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'reac import { Row } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import { deleteHistory, getHistory, setHistory } from '../../../../api/history' +import { deleteHistory, deleteHistoryEntry, getHistory, setHistory, updateHistoryEntry } from '../../../../api/history' +import { deleteNote } from '../../../../api/note' import { ApplicationState } from '../../../../redux' import { collectEntries, @@ -65,10 +66,9 @@ export const History: React.FC = () => { .then(() => setRemoteHistoryEntries(entries)) .catch(() => setError('setHistory')) } else { - historyWrite(entries) setLocalHistoryEntries(entries) } - }, [historyWrite, user]) + }, [user]) const refreshHistory = useCallback(() => { const localHistory = loadHistoryFromLocalStore() @@ -102,22 +102,72 @@ export const History: React.FC = () => { historyWrite([]) }, [historyWrite, user]) - const syncClick = useCallback((entryId: string): void => { - console.log(entryId) - // ToDo: add syncClick + const uploadAll = useCallback((): void => { + const newHistory = mergeEntryArrays(localHistoryEntries, remoteHistoryEntries) + if (user) { + setHistory(newHistory) + .then(() => { + setRemoteHistoryEntries(newHistory) + setLocalHistoryEntries([]) + historyWrite([]) + }) + .catch(() => setError('setHistory')) + } + }, [historyWrite, localHistoryEntries, remoteHistoryEntries, user]) + + const deleteClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => { + if (user) { + deleteNote(entryId) + .then(() => { + if (location === HistoryEntryOrigin.LOCAL) { + setLocalHistoryEntries(entries => entries.filter(entry => entry.id !== entryId)) + } else if (location === HistoryEntryOrigin.REMOTE) { + setRemoteHistoryEntries(entries => entries.filter(entry => entry.id !== entryId)) + } + }) + .catch(() => setError('deleteNote')) + } + }, [user]) + + const removeClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => { + if (location === HistoryEntryOrigin.LOCAL) { + setLocalHistoryEntries((entries) => entries.filter(entry => entry.id !== entryId)) + } else if (location === HistoryEntryOrigin.REMOTE) { + deleteHistoryEntry(entryId) + .then(() => setRemoteHistoryEntries((entries) => entries.filter(entry => entry.id !== entryId))) + .catch(() => setError('deleteEntry')) + } }, []) - const 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 - } - return entry + const pinClick = useCallback((entryId: string, location: HistoryEntryOrigin): void => { + if (location === HistoryEntryOrigin.LOCAL) { + setLocalHistoryEntries((entries) => { + return entries.map((entry) => { + if (entry.id === entryId) { + entry.pinned = !entry.pinned + } + return entry + }) }) - }) - }, []) + } else if (location === HistoryEntryOrigin.REMOTE) { + const entry = remoteHistoryEntries.find(entry => entry.id === entryId) + if (!entry) { + setError('notFoundEntry') + return + } + entry.pinned = !entry.pinned + updateHistoryEntry(entryId, entry) + .then(() => setRemoteHistoryEntries((entries) => { + return entries.map((entry) => { + if (entry.id === entryId) { + entry.pinned = !entry.pinned + } + return entry + }) + })) + .catch(() => setError('updateEntry')) + } + }, [remoteHistoryEntries]) const resetError = () => { setError('') @@ -159,12 +209,15 @@ export const History: React.FC = () => { onRefreshHistory={refreshHistory} onExportHistory={exportHistory} onImportHistory={importHistory} + onUploadAll={uploadAll} /> - ) diff --git a/src/utils/historyUtils.ts b/src/utils/historyUtils.ts index 6d7610b20..d0fa588a6 100644 --- a/src/utils/historyUtils.ts +++ b/src/utils/historyUtils.ts @@ -29,13 +29,13 @@ function locateEntries (entries: HistoryEntry[], location: HistoryEntryOrigin): }) } -export function mergeEntryArrays (locatedLocalEntries: T[], locatedRemoteEntries: T[]): T[] { - const filteredLocalEntries = locatedLocalEntries.filter(localEntry => { - const entry = locatedRemoteEntries.find(remoteEntry => remoteEntry.id === localEntry.id) +export function mergeEntryArrays (localEntries: T[], remoteEntries: T[]): T[] { + const filteredLocalEntries = localEntries.filter(localEntry => { + const entry = remoteEntries.find(remoteEntry => remoteEntry.id === localEntry.id) return !entry }) - return filteredLocalEntries.concat(locatedRemoteEntries) + return filteredLocalEntries.concat(remoteEntries) } function filterBySelectedTags (entries: LocatedHistoryEntry[], selectedTags: string[]): LocatedHistoryEntry[] {