1
0
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:
olav 2022-02-23 15:08:44 +01:00 committed by GitHub
parent 38c26ec052
commit 94ecaa80a8
14 changed files with 149 additions and 106 deletions

View File

@ -1,9 +1,8 @@
import { useMemo, useState } from 'react';
import { CircularProgress } from '@material-ui/core';
import { Warning } from '@material-ui/icons';
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 HeaderTitle from '../../common/HeaderTitle';
import useApplications from '../../../hooks/api/getters/useApplications/useApplications';

View File

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

View 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>
);
};

View File

@ -1,6 +1,12 @@
import { makeStyles } from '@material-ui/styles';
export const useStyles = makeStyles(theme => ({
container: {
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: '1rem',
},
search: {
display: 'flex',
alignItems: 'center',
@ -8,9 +14,6 @@ export const useStyles = makeStyles(theme => ({
borderRadius: '25px',
padding: '0.25rem 0.5rem',
maxWidth: '450px',
[theme.breakpoints.down('sm')]: {
margin: '0 auto',
},
[theme.breakpoints.down('xs')]: {
width: '100%',
},

View File

@ -5,20 +5,15 @@ import { Link } from 'react-router-dom';
import { Button, IconButton, List, ListItem, Tooltip } from '@material-ui/core';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import { Add } from '@material-ui/icons';
import FeatureToggleListItem from './FeatureToggleListItem';
import SearchField from '../../common/SearchField/SearchField';
import { SearchField } from '../../common/SearchField/SearchField';
import FeatureToggleListActions from './FeatureToggleListActions';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import PageContent from '../../common/PageContent/PageContent';
import HeaderTitle from '../../common/HeaderTitle';
import loadingFeatures from './loadingFeatures';
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
import AccessContext from '../../../contexts/AccessContext';
import { useStyles } from './styles';
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
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 (
<div className={styles.featureContainer}>
@ -109,6 +108,7 @@ const FeatureToggleList = ({
<SearchField
initialValue={filter.query}
updateValue={setFilterQuery}
showValueChip={!mobileView}
className={classnames(styles.searchBar, {
skeleton: loading,
})}

View File

@ -5,13 +5,15 @@ exports[`renders correctly with one feature 1`] = `
<div
className="makeStyles-searchBarContainer-3"
>
<div>
<div
className="makeStyles-container-6"
>
<div
className="makeStyles-search-6 makeStyles-searchBar-4"
className="makeStyles-search-7 makeStyles-searchBar-4"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root makeStyles-searchIcon-7"
className="MuiSvgIcon-root makeStyles-searchIcon-8"
focusable="false"
viewBox="0 0 24 24"
>
@ -20,7 +22,7 @@ exports[`renders correctly with one feature 1`] = `
/>
</svg>
<div
className="MuiInputBase-root makeStyles-inputRoot-8"
className="MuiInputBase-root makeStyles-inputRoot-9"
onClick={[Function]}
onKeyPress={[Function]}
>
@ -31,7 +33,7 @@ exports[`renders correctly with one feature 1`] = `
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Search"
placeholder="Search..."
type="text"
value=""
/>
@ -55,29 +57,29 @@ exports[`renders correctly with one feature 1`] = `
}
>
<div
className="makeStyles-headerContainer-9"
className="makeStyles-headerContainer-10"
>
<div
className="makeStyles-headerTitleContainer-13"
className="makeStyles-headerTitleContainer-14"
>
<div
className=""
data-loading={true}
>
<h2
className="MuiTypography-root makeStyles-headerTitle-14 MuiTypography-h2"
className="MuiTypography-root makeStyles-headerTitle-15 MuiTypography-h2"
>
Features
</h2>
</div>
<div
className="makeStyles-headerActions-15"
className="makeStyles-headerActions-16"
>
<div
className="makeStyles-actionsContainer-1"
>
<div
className="makeStyles-actions-16"
className="makeStyles-actions-17"
>
<p
className="MuiTypography-root MuiTypography-body2"
@ -171,7 +173,7 @@ exports[`renders correctly with one feature 1`] = `
</div>
</div>
<div
className="makeStyles-bodyContainer-10"
className="makeStyles-bodyContainer-11"
>
<ul
className="MuiList-root MuiList-padding"
@ -197,13 +199,15 @@ exports[`renders correctly with one feature without permissions 1`] = `
<div
className="makeStyles-searchBarContainer-3"
>
<div>
<div
className="makeStyles-container-6"
>
<div
className="makeStyles-search-6 makeStyles-searchBar-4"
className="makeStyles-search-7 makeStyles-searchBar-4"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root makeStyles-searchIcon-7"
className="MuiSvgIcon-root makeStyles-searchIcon-8"
focusable="false"
viewBox="0 0 24 24"
>
@ -212,7 +216,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
/>
</svg>
<div
className="MuiInputBase-root makeStyles-inputRoot-8"
className="MuiInputBase-root makeStyles-inputRoot-9"
onClick={[Function]}
onKeyPress={[Function]}
>
@ -223,7 +227,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Search"
placeholder="Search..."
type="text"
value=""
/>
@ -247,29 +251,29 @@ exports[`renders correctly with one feature without permissions 1`] = `
}
>
<div
className="makeStyles-headerContainer-9"
className="makeStyles-headerContainer-10"
>
<div
className="makeStyles-headerTitleContainer-13"
className="makeStyles-headerTitleContainer-14"
>
<div
className=""
data-loading={true}
>
<h2
className="MuiTypography-root makeStyles-headerTitle-14 MuiTypography-h2"
className="MuiTypography-root makeStyles-headerTitle-15 MuiTypography-h2"
>
Features
</h2>
</div>
<div
className="makeStyles-headerActions-15"
className="makeStyles-headerActions-16"
>
<div
className="makeStyles-actionsContainer-1"
>
<div
className="makeStyles-actions-16"
className="makeStyles-actions-17"
>
<p
className="MuiTypography-root MuiTypography-body2"
@ -366,7 +370,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
</div>
</div>
<div
className="makeStyles-bodyContainer-10"
className="makeStyles-bodyContainer-11"
>
<ul
className="MuiList-root MuiList-padding"

View File

@ -11,6 +11,7 @@ export const useStyles = makeStyles(theme => ({
searchBarContainer: {
marginBottom: '2rem',
display: 'flex',
gap: '1rem',
justifyContent: 'space-between',
alignItems: 'center',
[theme.breakpoints.down('xs')]: {

View File

@ -15,7 +15,6 @@ import { Tooltip } from '@material-ui/core';
import ConditionallyRender from '../../../../../common/ConditionallyRender';
import { useStyles } from './FeatureStrategyEditable.styles';
import { Delete } from '@material-ui/icons';
import { PRODUCTION } from '../../../../../../constants/environmentTypes';
import {
DELETE_STRATEGY_ID,
STRATEGY_ACCORDION_ID,

View File

@ -1,5 +1,5 @@
import { getBasePath } from '../utils/format-path';
import { createPersistentGlobalState } from './usePersistentGlobalState';
import { createPersistentGlobalStateHook } from './usePersistentGlobalState';
import React from 'react';
export interface IEventSettings {
@ -21,7 +21,7 @@ const createInitialValue = (): IEventSettings => {
return { showData: false };
};
const useGlobalState = createPersistentGlobalState<IEventSettings>(
const useGlobalState = createPersistentGlobalStateHook<IEventSettings>(
`${getBasePath()}:useEventSettings:v1`,
createInitialValue()
);

View File

@ -1,7 +1,6 @@
import { IFeatureToggle } from '../interfaces/featureToggle';
import { IFeatureToggle } from 'interfaces/featureToggle';
import React, { useMemo } from 'react';
import { getBasePath } from '../utils/format-path';
import { createPersistentGlobalState } from './usePersistentGlobalState';
import { createGlobalStateHook } from 'hooks/useGlobalState';
export interface IFeaturesFilter {
query?: string;
@ -16,8 +15,8 @@ export interface IFeaturesSortOutput {
// Store the features filter state globally, and in localStorage.
// When changing the format of IFeaturesFilter, change the version as well.
const useFeaturesFilterState = createPersistentGlobalState<IFeaturesFilter>(
`${getBasePath()}:useFeaturesFilter:v1`,
const useFeaturesFilterState = createGlobalStateHook<IFeaturesFilter>(
'useFeaturesFilterState',
{ project: '*' }
);

View File

@ -1,7 +1,7 @@
import { IFeatureToggle } from '../interfaces/featureToggle';
import React, { useMemo } from 'react';
import { getBasePath } from '../utils/format-path';
import { createPersistentGlobalState } from './usePersistentGlobalState';
import { createPersistentGlobalStateHook } from './usePersistentGlobalState';
type FeaturesSortType =
| 'name'
@ -29,7 +29,7 @@ export interface IFeaturesFilterSortOption {
// Store the features sort state globally, and in localStorage.
// When changing the format of IFeaturesSort, change the version as well.
const useFeaturesSortState = createPersistentGlobalState<IFeaturesSort>(
const useFeaturesSortState = createPersistentGlobalStateHook<IFeaturesSort>(
`${getBasePath()}:useFeaturesSort:v1`,
{ type: 'name' }
);

View 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];
};

View File

@ -1,5 +1,5 @@
import { getBasePath } from '../utils/format-path';
import { createPersistentGlobalState } from './usePersistentGlobalState';
import { createPersistentGlobalStateHook } from './usePersistentGlobalState';
import React from 'react';
export interface ILocationSettings {
@ -23,7 +23,7 @@ const createInitialValue = (): ILocationSettings => {
return { locale: navigator.language };
};
const useGlobalState = createPersistentGlobalState<ILocationSettings>(
const useGlobalState = createPersistentGlobalStateHook<ILocationSettings>(
`${getBasePath()}:useLocationSettings:v1`,
createInitialValue()
);

View File

@ -10,7 +10,7 @@ type UsePersistentGlobalState<T> = () => [
// 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 localStorage state is not synced between tabs.
export const createPersistentGlobalState = <T extends object>(
export const createPersistentGlobalStateHook = <T extends object>(
key: string,
initialValue: T
): UsePersistentGlobalState<T> => {