mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-17 08:34:54 -04:00
The History PR: II - Add URL params (#1157)
* Add location state dependency Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Split toolbar state into single location states Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Add CHANGELOG entry Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Pin dependency Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Use react state for view because of side-effects The locationState was resetted on each search or tags filter update because these updates pushed a change to the location thus resulting in loss of the location state for the view. Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Remove unneeded import Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Change CHANGELOG entry Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Removed unnecessary typecast Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
d6eabae1b1
commit
003658dc4d
5 changed files with 87 additions and 37 deletions
|
@ -71,6 +71,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
- Improved security by wrapping the markdown rendering into an iframe.
|
- Improved security by wrapping the markdown rendering into an iframe.
|
||||||
- The intro page content can be changed by editing `public/intro.md`.
|
- The intro page content can be changed by editing `public/intro.md`.
|
||||||
- When pasting tables (e.g. from LibreOffice Calc or MS Excel) they get reformatted to markdown tables.
|
- When pasting tables (e.g. from LibreOffice Calc or MS Excel) they get reformatted to markdown tables.
|
||||||
|
- The history page supports URL parameters that allow bookmarking of a specific search of tags filter.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,7 @@
|
||||||
"react-router": "5.2.0",
|
"react-router": "5.2.0",
|
||||||
"react-router-bootstrap": "0.25.0",
|
"react-router-bootstrap": "0.25.0",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
|
"react-router-use-location-state": "2.5.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"react-use": "17.2.4",
|
"react-use": "17.2.4",
|
||||||
"redux": "4.0.5",
|
"redux": "4.0.5",
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { ApplicationState } from '../../redux'
|
import { ApplicationState } from '../../redux'
|
||||||
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, initToolbarState } from './history-toolbar/history-toolbar'
|
||||||
import { sortAndFilterEntries } from './utils'
|
import { sortAndFilterEntries } from './utils'
|
||||||
import { refreshHistoryState } from '../../redux/history/methods'
|
import { refreshHistoryState } from '../../redux/history/methods'
|
||||||
import { HistoryEntry } from '../../redux/history/types'
|
import { HistoryEntry } from '../../redux/history/types'
|
||||||
|
@ -20,7 +20,7 @@ export const HistoryPage: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const allEntries = useSelector((state: ApplicationState) => state.history)
|
const allEntries = useSelector((state: ApplicationState) => state.history)
|
||||||
const [toolbarState, setToolbarState] = useState<HistoryToolbarState>(toolbarInitState)
|
const [toolbarState, setToolbarState] = useState<HistoryToolbarState>(initToolbarState)
|
||||||
|
|
||||||
const entriesToShow = useMemo<HistoryEntry[]>(() =>
|
const entriesToShow = useMemo<HistoryEntry[]>(() =>
|
||||||
sortAndFilterEntries(allEntries, toolbarState),
|
sortAndFilterEntries(allEntries, toolbarState),
|
||||||
|
|
|
@ -4,11 +4,13 @@
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'
|
import equal from 'fast-deep-equal'
|
||||||
|
import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Button, Form, FormControl, InputGroup, ToggleButton, ToggleButtonGroup } from 'react-bootstrap'
|
import { Button, Form, FormControl, InputGroup, ToggleButton, ToggleButtonGroup } from 'react-bootstrap'
|
||||||
import { Typeahead } from 'react-bootstrap-typeahead'
|
import { Typeahead } from 'react-bootstrap-typeahead'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
import { useQueryState } from 'react-router-use-location-state'
|
||||||
import { ApplicationState } from '../../../redux'
|
import { ApplicationState } from '../../../redux'
|
||||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
|
@ -21,16 +23,21 @@ import { HistoryEntryOrigin } from '../../../redux/history/types'
|
||||||
import { importHistoryEntries, refreshHistoryState, setHistoryEntries } from '../../../redux/history/methods'
|
import { importHistoryEntries, refreshHistoryState, setHistoryEntries } from '../../../redux/history/methods'
|
||||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
||||||
|
|
||||||
export type HistoryToolbarChange = (settings: HistoryToolbarState) => void;
|
export type HistoryToolbarChange = (newState: HistoryToolbarState) => void;
|
||||||
|
|
||||||
export interface HistoryToolbarState {
|
interface ToolbarSortState {
|
||||||
viewState: ViewStateEnum
|
|
||||||
titleSortDirection: SortModeEnum
|
titleSortDirection: SortModeEnum
|
||||||
lastVisitedSortDirection: SortModeEnum
|
lastVisitedSortDirection: SortModeEnum
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolbarFilterState {
|
||||||
|
viewState: ViewStateEnum
|
||||||
keywordSearch: string
|
keywordSearch: string
|
||||||
selectedTags: string[]
|
selectedTags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HistoryToolbarState = ToolbarSortState & ToolbarFilterState
|
||||||
|
|
||||||
export enum ViewStateEnum {
|
export enum ViewStateEnum {
|
||||||
CARD,
|
CARD,
|
||||||
TABLE
|
TABLE
|
||||||
|
@ -40,17 +47,20 @@ export interface HistoryToolbarProps {
|
||||||
onSettingsChange: HistoryToolbarChange
|
onSettingsChange: HistoryToolbarChange
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initState: HistoryToolbarState = {
|
const initSortState: ToolbarSortState = {
|
||||||
viewState: ViewStateEnum.CARD,
|
|
||||||
titleSortDirection: SortModeEnum.no,
|
titleSortDirection: SortModeEnum.no,
|
||||||
lastVisitedSortDirection: SortModeEnum.down,
|
lastVisitedSortDirection: SortModeEnum.down
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initToolbarState: HistoryToolbarState = {
|
||||||
|
...initSortState,
|
||||||
|
viewState: ViewStateEnum.CARD,
|
||||||
keywordSearch: '',
|
keywordSearch: '',
|
||||||
selectedTags: []
|
selectedTags: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange }) => {
|
export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [state, setState] = useState<HistoryToolbarState>(initState)
|
|
||||||
const historyEntries = useSelector((state: ApplicationState) => state.history)
|
const historyEntries = useSelector((state: ApplicationState) => state.history)
|
||||||
const userExists = useSelector((state: ApplicationState) => !!state.user)
|
const userExists = useSelector((state: ApplicationState) => !!state.user)
|
||||||
|
|
||||||
|
@ -60,33 +70,37 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
||||||
return [...new Set(allTags)]
|
return [...new Set(allTags)]
|
||||||
}, [historyEntries])
|
}, [historyEntries])
|
||||||
|
|
||||||
const titleSortChanged = (direction: SortModeEnum) => {
|
const previousState = useRef(initToolbarState)
|
||||||
setState(prevState => ({
|
const [searchState, setSearchState] = useQueryState('search', initToolbarState.keywordSearch)
|
||||||
...prevState,
|
const [tagsState, setTagsState] = useQueryState('tags', initToolbarState.selectedTags)
|
||||||
titleSortDirection: direction,
|
const [viewState, setViewState] = useState(initToolbarState.viewState)
|
||||||
lastVisitedSortDirection: SortModeEnum.no
|
const [sortState, setSortState] = useState<ToolbarSortState>(initSortState)
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastVisitedSortChanged = (direction: SortModeEnum) => {
|
const titleSortChanged = useCallback((direction: SortModeEnum) => {
|
||||||
setState(prevState => ({
|
setSortState({
|
||||||
...prevState,
|
lastVisitedSortDirection: SortModeEnum.no,
|
||||||
|
titleSortDirection: direction
|
||||||
|
})
|
||||||
|
}, [setSortState])
|
||||||
|
|
||||||
|
const lastVisitedSortChanged = useCallback((direction: SortModeEnum) => {
|
||||||
|
setSortState({
|
||||||
lastVisitedSortDirection: direction,
|
lastVisitedSortDirection: direction,
|
||||||
titleSortDirection: SortModeEnum.no
|
titleSortDirection: SortModeEnum.no
|
||||||
}))
|
})
|
||||||
}
|
}, [setSortState])
|
||||||
|
|
||||||
const keywordSearchChanged = (event: ChangeEvent<HTMLInputElement>) => {
|
const keywordSearchChanged = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||||
setState(prevState => ({ ...prevState, keywordSearch: event.currentTarget.value }))
|
setSearchState(event.currentTarget.value ?? '')
|
||||||
}
|
}, [setSearchState])
|
||||||
|
|
||||||
const toggleViewChanged = (newViewState: ViewStateEnum) => {
|
const toggleViewChanged = useCallback((newViewState: ViewStateEnum) => {
|
||||||
setState((prevState) => ({ ...prevState, viewState: newViewState }))
|
setViewState(newViewState)
|
||||||
}
|
}, [setViewState])
|
||||||
|
|
||||||
const selectedTagsChanged = (selected: string[]) => {
|
const selectedTagsChanged = useCallback((selected: string[]) => {
|
||||||
setState(prevState => ({ ...prevState, selectedTags: selected }))
|
setTagsState(selected)
|
||||||
}
|
}, [setTagsState])
|
||||||
|
|
||||||
const refreshHistory = useCallback(() => {
|
const refreshHistory = useCallback(() => {
|
||||||
refreshHistoryState()
|
refreshHistoryState()
|
||||||
|
@ -116,30 +130,45 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
||||||
}, [userExists, historyEntries, t, refreshHistory])
|
}, [userExists, historyEntries, t, refreshHistory])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onSettingsChange(state)
|
const newState: HistoryToolbarState = {
|
||||||
}, [onSettingsChange, state])
|
selectedTags: tagsState,
|
||||||
|
keywordSearch: searchState,
|
||||||
|
viewState: viewState,
|
||||||
|
...sortState
|
||||||
|
}
|
||||||
|
// This is needed because the onSettingsChange triggers a state update in history-page which re-renders the toolbar.
|
||||||
|
// The re-rendering causes this effect to run again resulting in an infinite state update loop.
|
||||||
|
if (equal(previousState.current, newState)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onSettingsChange(newState)
|
||||||
|
previousState.current = newState
|
||||||
|
}, [onSettingsChange, tagsState, searchState, viewState, sortState])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form inline={ true }>
|
<Form inline={ true }>
|
||||||
<InputGroup className={ 'mr-1 mb-1' }>
|
<InputGroup className={ 'mr-1 mb-1' }>
|
||||||
<Typeahead id={ 'tagsSelection' } options={ tags } multiple={ true }
|
<Typeahead id={ 'tagsSelection' } options={ tags } multiple={ true }
|
||||||
placeholder={ t('landing.history.toolbar.selectTags') }
|
placeholder={ t('landing.history.toolbar.selectTags') }
|
||||||
onChange={ selectedTagsChanged }/>
|
onChange={ selectedTagsChanged }
|
||||||
|
selected={ tagsState }
|
||||||
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup className={ 'mr-1 mb-1' }>
|
<InputGroup className={ 'mr-1 mb-1' }>
|
||||||
<FormControl
|
<FormControl
|
||||||
placeholder={ t('landing.history.toolbar.searchKeywords') }
|
placeholder={ t('landing.history.toolbar.searchKeywords') }
|
||||||
aria-label={ t('landing.history.toolbar.searchKeywords') }
|
aria-label={ t('landing.history.toolbar.searchKeywords') }
|
||||||
onChange={ keywordSearchChanged }
|
onChange={ keywordSearchChanged }
|
||||||
|
value={ searchState }
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup className={ 'mr-1 mb-1' }>
|
<InputGroup className={ 'mr-1 mb-1' }>
|
||||||
<SortButton onDirectionChange={ titleSortChanged } direction={ state.titleSortDirection }
|
<SortButton onDirectionChange={ titleSortChanged } direction={ sortState.titleSortDirection }
|
||||||
variant={ 'light' }><Trans
|
variant={ 'light' }><Trans
|
||||||
i18nKey={ 'landing.history.toolbar.sortByTitle' }/></SortButton>
|
i18nKey={ 'landing.history.toolbar.sortByTitle' }/></SortButton>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup className={ 'mr-1 mb-1' }>
|
<InputGroup className={ 'mr-1 mb-1' }>
|
||||||
<SortButton onDirectionChange={ lastVisitedSortChanged } direction={ state.lastVisitedSortDirection }
|
<SortButton onDirectionChange={ lastVisitedSortChanged } direction={ sortState.lastVisitedSortDirection }
|
||||||
variant={ 'light' }><Trans i18nKey={ 'landing.history.toolbar.sortByLastVisited' }/></SortButton>
|
variant={ 'light' }><Trans i18nKey={ 'landing.history.toolbar.sortByLastVisited' }/></SortButton>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup className={ 'mr-1 mb-1' }>
|
<InputGroup className={ 'mr-1 mb-1' }>
|
||||||
|
@ -164,7 +193,7 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<InputGroup className={ 'mr-1 mb-1' }>
|
<InputGroup className={ 'mr-1 mb-1' }>
|
||||||
<ToggleButtonGroup type="radio" name="options" dir="ltr" value={ state.viewState } className={ 'button-height' }
|
<ToggleButtonGroup type="radio" name="options" dir="ltr" value={ viewState } className={ 'button-height' }
|
||||||
onChange={ (newViewState: ViewStateEnum) => {
|
onChange={ (newViewState: ViewStateEnum) => {
|
||||||
toggleViewChanged(newViewState)
|
toggleViewChanged(newViewState)
|
||||||
} }>
|
} }>
|
||||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -11661,6 +11661,11 @@ qs@~6.5.2:
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
||||||
|
|
||||||
|
query-state-core@^2.5.0:
|
||||||
|
version "2.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/query-state-core/-/query-state-core-2.5.0.tgz#7cac3fdc1f79c58c22f35efe8a5f5880f55728d3"
|
||||||
|
integrity sha512-XVo7I/K+gKXqu+HlxtGXfjUtQ+LPjs5bTHB4RC4vDs6yCYLmchc4IxcZWt5EdZZLqIg/CuY+PUxN141t3J17fQ==
|
||||||
|
|
||||||
query-string@^4.1.0:
|
query-string@^4.1.0:
|
||||||
version "4.3.4"
|
version "4.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
|
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
|
||||||
|
@ -11965,6 +11970,13 @@ react-router-dom@5.2.0:
|
||||||
tiny-invariant "^1.0.2"
|
tiny-invariant "^1.0.2"
|
||||||
tiny-warning "^1.0.0"
|
tiny-warning "^1.0.0"
|
||||||
|
|
||||||
|
react-router-use-location-state@2.5.0:
|
||||||
|
version "2.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-router-use-location-state/-/react-router-use-location-state-2.5.0.tgz#4fe1cb6aa3cd5f8f997cfb77e7a6d5d8a55ea00f"
|
||||||
|
integrity sha512-p0duQtatgL8SZzIITI3He2MP/4d9x9GQHJs93spPYAckVrvCRLAlQxS7k04RHYZb0e4yxwW76Rp/PBvezyez6g==
|
||||||
|
dependencies:
|
||||||
|
use-location-state "^2.5.0"
|
||||||
|
|
||||||
react-router@5.2.0:
|
react-router@5.2.0:
|
||||||
version "5.2.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
|
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
|
||||||
|
@ -14312,6 +14324,13 @@ url@^0.11.0:
|
||||||
punycode "1.3.2"
|
punycode "1.3.2"
|
||||||
querystring "0.2.0"
|
querystring "0.2.0"
|
||||||
|
|
||||||
|
use-location-state@^2.5.0:
|
||||||
|
version "2.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-location-state/-/use-location-state-2.5.0.tgz#c71e6b5391898fa23ee1a6242d193924db6767c0"
|
||||||
|
integrity sha512-Gsn37xXWTVa4gGZA8WobtmC7ixm46TkQUyr9MApLhh9YIDcxOKuLCH/0wuKY7YcrCsb5t/S0b77qP50/mbvibQ==
|
||||||
|
dependencies:
|
||||||
|
query-state-core "^2.5.0"
|
||||||
|
|
||||||
use-resize-observer@7.0.0:
|
use-resize-observer@7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-7.0.0.tgz#15f0efbd5a4e08a8cc51901f21a89ba836f2116e"
|
resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-7.0.0.tgz#15f0efbd5a4e08a8cc51901f21a89ba836f2116e"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue