mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-04-13 00:25:20 +00: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.
|
||||
- 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.
|
||||
- The history page supports URL parameters that allow bookmarking of a specific search of tags filter.
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
@ -94,6 +94,7 @@
|
|||
"react-router": "5.2.0",
|
||||
"react-router-bootstrap": "0.25.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-router-use-location-state": "2.5.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-use": "17.2.4",
|
||||
"redux": "4.0.5",
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../redux'
|
||||
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 { refreshHistoryState } from '../../redux/history/methods'
|
||||
import { HistoryEntry } from '../../redux/history/types'
|
||||
|
@ -20,7 +20,7 @@ export const HistoryPage: React.FC = () => {
|
|||
const { t } = useTranslation()
|
||||
|
||||
const allEntries = useSelector((state: ApplicationState) => state.history)
|
||||
const [toolbarState, setToolbarState] = useState<HistoryToolbarState>(toolbarInitState)
|
||||
const [toolbarState, setToolbarState] = useState<HistoryToolbarState>(initToolbarState)
|
||||
|
||||
const entriesToShow = useMemo<HistoryEntry[]>(() =>
|
||||
sortAndFilterEntries(allEntries, toolbarState),
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
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 { Typeahead } from 'react-bootstrap-typeahead'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useQueryState } from 'react-router-use-location-state'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
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 { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
||||
|
||||
export type HistoryToolbarChange = (settings: HistoryToolbarState) => void;
|
||||
export type HistoryToolbarChange = (newState: HistoryToolbarState) => void;
|
||||
|
||||
export interface HistoryToolbarState {
|
||||
viewState: ViewStateEnum
|
||||
interface ToolbarSortState {
|
||||
titleSortDirection: SortModeEnum
|
||||
lastVisitedSortDirection: SortModeEnum
|
||||
}
|
||||
|
||||
interface ToolbarFilterState {
|
||||
viewState: ViewStateEnum
|
||||
keywordSearch: string
|
||||
selectedTags: string[]
|
||||
}
|
||||
|
||||
export type HistoryToolbarState = ToolbarSortState & ToolbarFilterState
|
||||
|
||||
export enum ViewStateEnum {
|
||||
CARD,
|
||||
TABLE
|
||||
|
@ -40,17 +47,20 @@ export interface HistoryToolbarProps {
|
|||
onSettingsChange: HistoryToolbarChange
|
||||
}
|
||||
|
||||
export const initState: HistoryToolbarState = {
|
||||
viewState: ViewStateEnum.CARD,
|
||||
const initSortState: ToolbarSortState = {
|
||||
titleSortDirection: SortModeEnum.no,
|
||||
lastVisitedSortDirection: SortModeEnum.down,
|
||||
lastVisitedSortDirection: SortModeEnum.down
|
||||
}
|
||||
|
||||
export const initToolbarState: HistoryToolbarState = {
|
||||
...initSortState,
|
||||
viewState: ViewStateEnum.CARD,
|
||||
keywordSearch: '',
|
||||
selectedTags: []
|
||||
}
|
||||
|
||||
export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange }) => {
|
||||
const { t } = useTranslation()
|
||||
const [state, setState] = useState<HistoryToolbarState>(initState)
|
||||
const historyEntries = useSelector((state: ApplicationState) => state.history)
|
||||
const userExists = useSelector((state: ApplicationState) => !!state.user)
|
||||
|
||||
|
@ -60,33 +70,37 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
|||
return [...new Set(allTags)]
|
||||
}, [historyEntries])
|
||||
|
||||
const titleSortChanged = (direction: SortModeEnum) => {
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
titleSortDirection: direction,
|
||||
lastVisitedSortDirection: SortModeEnum.no
|
||||
}))
|
||||
}
|
||||
const previousState = useRef(initToolbarState)
|
||||
const [searchState, setSearchState] = useQueryState('search', initToolbarState.keywordSearch)
|
||||
const [tagsState, setTagsState] = useQueryState('tags', initToolbarState.selectedTags)
|
||||
const [viewState, setViewState] = useState(initToolbarState.viewState)
|
||||
const [sortState, setSortState] = useState<ToolbarSortState>(initSortState)
|
||||
|
||||
const lastVisitedSortChanged = (direction: SortModeEnum) => {
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
const titleSortChanged = useCallback((direction: SortModeEnum) => {
|
||||
setSortState({
|
||||
lastVisitedSortDirection: SortModeEnum.no,
|
||||
titleSortDirection: direction
|
||||
})
|
||||
}, [setSortState])
|
||||
|
||||
const lastVisitedSortChanged = useCallback((direction: SortModeEnum) => {
|
||||
setSortState({
|
||||
lastVisitedSortDirection: direction,
|
||||
titleSortDirection: SortModeEnum.no
|
||||
}))
|
||||
}
|
||||
})
|
||||
}, [setSortState])
|
||||
|
||||
const keywordSearchChanged = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setState(prevState => ({ ...prevState, keywordSearch: event.currentTarget.value }))
|
||||
}
|
||||
const keywordSearchChanged = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchState(event.currentTarget.value ?? '')
|
||||
}, [setSearchState])
|
||||
|
||||
const toggleViewChanged = (newViewState: ViewStateEnum) => {
|
||||
setState((prevState) => ({ ...prevState, viewState: newViewState }))
|
||||
}
|
||||
const toggleViewChanged = useCallback((newViewState: ViewStateEnum) => {
|
||||
setViewState(newViewState)
|
||||
}, [setViewState])
|
||||
|
||||
const selectedTagsChanged = (selected: string[]) => {
|
||||
setState(prevState => ({ ...prevState, selectedTags: selected }))
|
||||
}
|
||||
const selectedTagsChanged = useCallback((selected: string[]) => {
|
||||
setTagsState(selected)
|
||||
}, [setTagsState])
|
||||
|
||||
const refreshHistory = useCallback(() => {
|
||||
refreshHistoryState()
|
||||
|
@ -116,30 +130,45 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
|||
}, [userExists, historyEntries, t, refreshHistory])
|
||||
|
||||
useEffect(() => {
|
||||
onSettingsChange(state)
|
||||
}, [onSettingsChange, state])
|
||||
const newState: HistoryToolbarState = {
|
||||
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 (
|
||||
<Form inline={ true }>
|
||||
<InputGroup className={ 'mr-1 mb-1' }>
|
||||
<Typeahead id={ 'tagsSelection' } options={ tags } multiple={ true }
|
||||
placeholder={ t('landing.history.toolbar.selectTags') }
|
||||
onChange={ selectedTagsChanged }/>
|
||||
onChange={ selectedTagsChanged }
|
||||
selected={ tagsState }
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup className={ 'mr-1 mb-1' }>
|
||||
<FormControl
|
||||
placeholder={ t('landing.history.toolbar.searchKeywords') }
|
||||
aria-label={ t('landing.history.toolbar.searchKeywords') }
|
||||
onChange={ keywordSearchChanged }
|
||||
value={ searchState }
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup className={ 'mr-1 mb-1' }>
|
||||
<SortButton onDirectionChange={ titleSortChanged } direction={ state.titleSortDirection }
|
||||
<SortButton onDirectionChange={ titleSortChanged } direction={ sortState.titleSortDirection }
|
||||
variant={ 'light' }><Trans
|
||||
i18nKey={ 'landing.history.toolbar.sortByTitle' }/></SortButton>
|
||||
</InputGroup>
|
||||
<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>
|
||||
</InputGroup>
|
||||
<InputGroup className={ 'mr-1 mb-1' }>
|
||||
|
@ -164,7 +193,7 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
|||
</InputGroup>
|
||||
</ShowIf>
|
||||
<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) => {
|
||||
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"
|
||||
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:
|
||||
version "4.3.4"
|
||||
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-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:
|
||||
version "5.2.0"
|
||||
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"
|
||||
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:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-7.0.0.tgz#15f0efbd5a4e08a8cc51901f21a89ba836f2116e"
|
||||
|
|
Loading…
Add table
Reference in a new issue