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:
Erik Michelson 2021-04-24 23:25:01 +02:00 committed by GitHub
parent d6eabae1b1
commit 003658dc4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 87 additions and 37 deletions

View file

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

View file

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

View file

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

View file

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

View file

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