mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
refactor: improve feature toggle search state (#741)
* refactor: rename createPersistentGlobalStateHook helper * refactor: move features filter state out of localStorage * refactor: show search state in page title * refactor: remove unused import * refactor: add a state chip to SearchField * refactor: improve var names
This commit is contained in:
parent
38c26ec052
commit
94ecaa80a8
@ -1,9 +1,8 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { CircularProgress } from '@material-ui/core';
|
import { CircularProgress } from '@material-ui/core';
|
||||||
import { Warning } from '@material-ui/icons';
|
import { Warning } from '@material-ui/icons';
|
||||||
|
|
||||||
import { AppsLinkList, styles as commonStyles } from '../../common';
|
import { AppsLinkList, styles as commonStyles } from '../../common';
|
||||||
import SearchField from '../../common/SearchField/SearchField';
|
import { SearchField } from 'component/common/SearchField/SearchField';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
import useApplications from '../../../hooks/api/getters/useApplications/useApplications';
|
import useApplications from '../../../hooks/api/getters/useApplications/useApplications';
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { debounce } from 'debounce';
|
|
||||||
import { InputBase } from '@material-ui/core';
|
|
||||||
import SearchIcon from '@material-ui/icons/Search';
|
|
||||||
|
|
||||||
import { useStyles } from './styles';
|
|
||||||
|
|
||||||
function SearchField({ initialValue = '', updateValue, className = '' }) {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
const [localValue, setLocalValue] = useState(initialValue);
|
|
||||||
const debounceUpdateValue = debounce(updateValue, 500);
|
|
||||||
|
|
||||||
const handleChange = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const v = e.target.value || '';
|
|
||||||
setLocalValue(v);
|
|
||||||
debounceUpdateValue(v);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyPress = e => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
updateValue(localValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateNow = () => {
|
|
||||||
updateValue(localValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={classnames(styles.search, className)}>
|
|
||||||
<SearchIcon className={styles.searchIcon} />
|
|
||||||
<InputBase
|
|
||||||
placeholder="Search…"
|
|
||||||
classes={{
|
|
||||||
root: styles.inputRoot,
|
|
||||||
input: styles.input,
|
|
||||||
}}
|
|
||||||
inputProps={{ 'aria-label': 'search' }}
|
|
||||||
value={localValue}
|
|
||||||
onChange={handleChange}
|
|
||||||
onBlur={updateNow}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchField.propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
updateValue: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchField;
|
|
74
frontend/src/component/common/SearchField/SearchField.tsx
Normal file
74
frontend/src/component/common/SearchField/SearchField.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { debounce } from 'debounce';
|
||||||
|
import { InputBase, Chip } from '@material-ui/core';
|
||||||
|
import SearchIcon from '@material-ui/icons/Search';
|
||||||
|
import { useStyles } from './styles';
|
||||||
|
import ConditionallyRender from 'component/common/ConditionallyRender';
|
||||||
|
|
||||||
|
interface ISearchFieldProps {
|
||||||
|
updateValue: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
initialValue?: string;
|
||||||
|
className?: string;
|
||||||
|
showValueChip?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchField = ({
|
||||||
|
updateValue,
|
||||||
|
initialValue = '',
|
||||||
|
className = '',
|
||||||
|
showValueChip,
|
||||||
|
}: ISearchFieldProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const [localValue, setLocalValue] = useState(initialValue);
|
||||||
|
const debounceUpdateValue = debounce(updateValue, 500);
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const value = event.target.value || '';
|
||||||
|
setLocalValue(value);
|
||||||
|
debounceUpdateValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
updateValue(localValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateNow = () => {
|
||||||
|
updateValue(localValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = () => {
|
||||||
|
setLocalValue('');
|
||||||
|
updateValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={classnames(styles.search, className)}>
|
||||||
|
<SearchIcon className={styles.searchIcon} />
|
||||||
|
<InputBase
|
||||||
|
placeholder="Search..."
|
||||||
|
classes={{ root: styles.inputRoot }}
|
||||||
|
inputProps={{ 'aria-label': 'search' }}
|
||||||
|
value={localValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={updateNow}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(showValueChip && localValue)}
|
||||||
|
show={
|
||||||
|
<Chip
|
||||||
|
label={localValue}
|
||||||
|
onDelete={onDelete}
|
||||||
|
title="Clear search query"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,12 @@
|
|||||||
import { makeStyles } from '@material-ui/styles';
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
export const useStyles = makeStyles(theme => ({
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '1rem',
|
||||||
|
},
|
||||||
search: {
|
search: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -8,9 +14,6 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
borderRadius: '25px',
|
borderRadius: '25px',
|
||||||
padding: '0.25rem 0.5rem',
|
padding: '0.25rem 0.5rem',
|
||||||
maxWidth: '450px',
|
maxWidth: '450px',
|
||||||
[theme.breakpoints.down('sm')]: {
|
|
||||||
margin: '0 auto',
|
|
||||||
},
|
|
||||||
[theme.breakpoints.down('xs')]: {
|
[theme.breakpoints.down('xs')]: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
|
@ -5,20 +5,15 @@ import { Link } from 'react-router-dom';
|
|||||||
import { Button, IconButton, List, ListItem, Tooltip } from '@material-ui/core';
|
import { Button, IconButton, List, ListItem, Tooltip } from '@material-ui/core';
|
||||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||||
import { Add } from '@material-ui/icons';
|
import { Add } from '@material-ui/icons';
|
||||||
|
|
||||||
import FeatureToggleListItem from './FeatureToggleListItem';
|
import FeatureToggleListItem from './FeatureToggleListItem';
|
||||||
import SearchField from '../../common/SearchField/SearchField';
|
import { SearchField } from '../../common/SearchField/SearchField';
|
||||||
import FeatureToggleListActions from './FeatureToggleListActions';
|
import FeatureToggleListActions from './FeatureToggleListActions';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
|
|
||||||
import loadingFeatures from './loadingFeatures';
|
import loadingFeatures from './loadingFeatures';
|
||||||
|
|
||||||
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
||||||
|
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
|
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
|
||||||
import { getCreateTogglePath } from '../../../utils/route-path-helpers';
|
import { getCreateTogglePath } from '../../../utils/route-path-helpers';
|
||||||
@ -101,7 +96,11 @@ const FeatureToggleList = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerTitle = archive ? 'Archived Features' : 'Features';
|
const headerTitle = filter.query
|
||||||
|
? 'Search results'
|
||||||
|
: archive
|
||||||
|
? 'Archived Features'
|
||||||
|
: 'Features';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.featureContainer}>
|
<div className={styles.featureContainer}>
|
||||||
@ -109,6 +108,7 @@ const FeatureToggleList = ({
|
|||||||
<SearchField
|
<SearchField
|
||||||
initialValue={filter.query}
|
initialValue={filter.query}
|
||||||
updateValue={setFilterQuery}
|
updateValue={setFilterQuery}
|
||||||
|
showValueChip={!mobileView}
|
||||||
className={classnames(styles.searchBar, {
|
className={classnames(styles.searchBar, {
|
||||||
skeleton: loading,
|
skeleton: loading,
|
||||||
})}
|
})}
|
||||||
|
@ -5,13 +5,15 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="makeStyles-searchBarContainer-3"
|
className="makeStyles-searchBarContainer-3"
|
||||||
>
|
>
|
||||||
<div>
|
<div
|
||||||
|
className="makeStyles-container-6"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-search-6 makeStyles-searchBar-4"
|
className="makeStyles-search-7 makeStyles-searchBar-4"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="MuiSvgIcon-root makeStyles-searchIcon-7"
|
className="MuiSvgIcon-root makeStyles-searchIcon-8"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
@ -20,7 +22,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
<div
|
||||||
className="MuiInputBase-root makeStyles-inputRoot-8"
|
className="MuiInputBase-root makeStyles-inputRoot-9"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyPress={[Function]}
|
onKeyPress={[Function]}
|
||||||
>
|
>
|
||||||
@ -31,7 +33,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
placeholder="Search…"
|
placeholder="Search..."
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
@ -55,29 +57,29 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-9"
|
className="makeStyles-headerContainer-10"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerTitleContainer-13"
|
className="makeStyles-headerTitleContainer-14"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className=""
|
className=""
|
||||||
data-loading={true}
|
data-loading={true}
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
className="MuiTypography-root makeStyles-headerTitle-14 MuiTypography-h2"
|
className="MuiTypography-root makeStyles-headerTitle-15 MuiTypography-h2"
|
||||||
>
|
>
|
||||||
Features
|
Features
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerActions-15"
|
className="makeStyles-headerActions-16"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-actionsContainer-1"
|
className="makeStyles-actionsContainer-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-actions-16"
|
className="makeStyles-actions-17"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
className="MuiTypography-root MuiTypography-body2"
|
className="MuiTypography-root MuiTypography-body2"
|
||||||
@ -171,7 +173,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-bodyContainer-10"
|
className="makeStyles-bodyContainer-11"
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
className="MuiList-root MuiList-padding"
|
className="MuiList-root MuiList-padding"
|
||||||
@ -197,13 +199,15 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="makeStyles-searchBarContainer-3"
|
className="makeStyles-searchBarContainer-3"
|
||||||
>
|
>
|
||||||
<div>
|
<div
|
||||||
|
className="makeStyles-container-6"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-search-6 makeStyles-searchBar-4"
|
className="makeStyles-search-7 makeStyles-searchBar-4"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="MuiSvgIcon-root makeStyles-searchIcon-7"
|
className="MuiSvgIcon-root makeStyles-searchIcon-8"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
@ -212,7 +216,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
<div
|
||||||
className="MuiInputBase-root makeStyles-inputRoot-8"
|
className="MuiInputBase-root makeStyles-inputRoot-9"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyPress={[Function]}
|
onKeyPress={[Function]}
|
||||||
>
|
>
|
||||||
@ -223,7 +227,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
placeholder="Search…"
|
placeholder="Search..."
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
@ -247,29 +251,29 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-9"
|
className="makeStyles-headerContainer-10"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerTitleContainer-13"
|
className="makeStyles-headerTitleContainer-14"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className=""
|
className=""
|
||||||
data-loading={true}
|
data-loading={true}
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
className="MuiTypography-root makeStyles-headerTitle-14 MuiTypography-h2"
|
className="MuiTypography-root makeStyles-headerTitle-15 MuiTypography-h2"
|
||||||
>
|
>
|
||||||
Features
|
Features
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerActions-15"
|
className="makeStyles-headerActions-16"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-actionsContainer-1"
|
className="makeStyles-actionsContainer-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-actions-16"
|
className="makeStyles-actions-17"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
className="MuiTypography-root MuiTypography-body2"
|
className="MuiTypography-root MuiTypography-body2"
|
||||||
@ -366,7 +370,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-bodyContainer-10"
|
className="makeStyles-bodyContainer-11"
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
className="MuiList-root MuiList-padding"
|
className="MuiList-root MuiList-padding"
|
||||||
|
@ -11,6 +11,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
searchBarContainer: {
|
searchBarContainer: {
|
||||||
marginBottom: '2rem',
|
marginBottom: '2rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
gap: '1rem',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
[theme.breakpoints.down('xs')]: {
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
@ -15,7 +15,6 @@ import { Tooltip } from '@material-ui/core';
|
|||||||
import ConditionallyRender from '../../../../../common/ConditionallyRender';
|
import ConditionallyRender from '../../../../../common/ConditionallyRender';
|
||||||
import { useStyles } from './FeatureStrategyEditable.styles';
|
import { useStyles } from './FeatureStrategyEditable.styles';
|
||||||
import { Delete } from '@material-ui/icons';
|
import { Delete } from '@material-ui/icons';
|
||||||
import { PRODUCTION } from '../../../../../../constants/environmentTypes';
|
|
||||||
import {
|
import {
|
||||||
DELETE_STRATEGY_ID,
|
DELETE_STRATEGY_ID,
|
||||||
STRATEGY_ACCORDION_ID,
|
STRATEGY_ACCORDION_ID,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { getBasePath } from '../utils/format-path';
|
import { getBasePath } from '../utils/format-path';
|
||||||
import { createPersistentGlobalState } from './usePersistentGlobalState';
|
import { createPersistentGlobalStateHook } from './usePersistentGlobalState';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export interface IEventSettings {
|
export interface IEventSettings {
|
||||||
@ -21,7 +21,7 @@ const createInitialValue = (): IEventSettings => {
|
|||||||
return { showData: false };
|
return { showData: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
const useGlobalState = createPersistentGlobalState<IEventSettings>(
|
const useGlobalState = createPersistentGlobalStateHook<IEventSettings>(
|
||||||
`${getBasePath()}:useEventSettings:v1`,
|
`${getBasePath()}:useEventSettings:v1`,
|
||||||
createInitialValue()
|
createInitialValue()
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { IFeatureToggle } from '../interfaces/featureToggle';
|
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { getBasePath } from '../utils/format-path';
|
import { createGlobalStateHook } from 'hooks/useGlobalState';
|
||||||
import { createPersistentGlobalState } from './usePersistentGlobalState';
|
|
||||||
|
|
||||||
export interface IFeaturesFilter {
|
export interface IFeaturesFilter {
|
||||||
query?: string;
|
query?: string;
|
||||||
@ -16,8 +15,8 @@ export interface IFeaturesSortOutput {
|
|||||||
|
|
||||||
// Store the features filter state globally, and in localStorage.
|
// Store the features filter state globally, and in localStorage.
|
||||||
// When changing the format of IFeaturesFilter, change the version as well.
|
// When changing the format of IFeaturesFilter, change the version as well.
|
||||||
const useFeaturesFilterState = createPersistentGlobalState<IFeaturesFilter>(
|
const useFeaturesFilterState = createGlobalStateHook<IFeaturesFilter>(
|
||||||
`${getBasePath()}:useFeaturesFilter:v1`,
|
'useFeaturesFilterState',
|
||||||
{ project: '*' }
|
{ project: '*' }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { IFeatureToggle } from '../interfaces/featureToggle';
|
import { IFeatureToggle } from '../interfaces/featureToggle';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { getBasePath } from '../utils/format-path';
|
import { getBasePath } from '../utils/format-path';
|
||||||
import { createPersistentGlobalState } from './usePersistentGlobalState';
|
import { createPersistentGlobalStateHook } from './usePersistentGlobalState';
|
||||||
|
|
||||||
type FeaturesSortType =
|
type FeaturesSortType =
|
||||||
| 'name'
|
| 'name'
|
||||||
@ -29,7 +29,7 @@ export interface IFeaturesFilterSortOption {
|
|||||||
|
|
||||||
// Store the features sort state globally, and in localStorage.
|
// Store the features sort state globally, and in localStorage.
|
||||||
// When changing the format of IFeaturesSort, change the version as well.
|
// When changing the format of IFeaturesSort, change the version as well.
|
||||||
const useFeaturesSortState = createPersistentGlobalState<IFeaturesSort>(
|
const useFeaturesSortState = createPersistentGlobalStateHook<IFeaturesSort>(
|
||||||
`${getBasePath()}:useFeaturesSort:v1`,
|
`${getBasePath()}:useFeaturesSort:v1`,
|
||||||
{ type: 'name' }
|
{ type: 'name' }
|
||||||
);
|
);
|
||||||
|
23
frontend/src/hooks/useGlobalState.ts
Normal file
23
frontend/src/hooks/useGlobalState.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createGlobalState } from 'react-hooks-global-state';
|
||||||
|
|
||||||
|
type UseGlobalState<T> = () => [
|
||||||
|
value: T,
|
||||||
|
setValue: React.Dispatch<React.SetStateAction<T>>
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create a hook that stores global state (shared across all hook instances).
|
||||||
|
export const createGlobalStateHook = <T>(
|
||||||
|
key: string,
|
||||||
|
initialValue: T
|
||||||
|
): UseGlobalState<T> => {
|
||||||
|
const container = createGlobalState<{ [key: string]: T }>({
|
||||||
|
[key]: initialValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setGlobalState = (value: React.SetStateAction<T>) => {
|
||||||
|
container.setGlobalState(key, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => [container.useGlobalState(key)[0], setGlobalState];
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { getBasePath } from '../utils/format-path';
|
import { getBasePath } from '../utils/format-path';
|
||||||
import { createPersistentGlobalState } from './usePersistentGlobalState';
|
import { createPersistentGlobalStateHook } from './usePersistentGlobalState';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export interface ILocationSettings {
|
export interface ILocationSettings {
|
||||||
@ -23,7 +23,7 @@ const createInitialValue = (): ILocationSettings => {
|
|||||||
return { locale: navigator.language };
|
return { locale: navigator.language };
|
||||||
};
|
};
|
||||||
|
|
||||||
const useGlobalState = createPersistentGlobalState<ILocationSettings>(
|
const useGlobalState = createPersistentGlobalStateHook<ILocationSettings>(
|
||||||
`${getBasePath()}:useLocationSettings:v1`,
|
`${getBasePath()}:useLocationSettings:v1`,
|
||||||
createInitialValue()
|
createInitialValue()
|
||||||
);
|
);
|
||||||
|
@ -10,7 +10,7 @@ type UsePersistentGlobalState<T> = () => [
|
|||||||
// Create a hook that stores global state (shared across all hook instances).
|
// Create a hook that stores global state (shared across all hook instances).
|
||||||
// The state is also persisted to localStorage and restored on page load.
|
// The state is also persisted to localStorage and restored on page load.
|
||||||
// The localStorage state is not synced between tabs.
|
// The localStorage state is not synced between tabs.
|
||||||
export const createPersistentGlobalState = <T extends object>(
|
export const createPersistentGlobalStateHook = <T extends object>(
|
||||||
key: string,
|
key: string,
|
||||||
initialValue: T
|
initialValue: T
|
||||||
): UsePersistentGlobalState<T> => {
|
): UsePersistentGlobalState<T> => {
|
||||||
|
Loading…
Reference in New Issue
Block a user