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 { 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';

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'; 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%',
}, },

View File

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

View File

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

View File

@ -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')]: {

View File

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

View File

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

View File

@ -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: '*' }
); );

View File

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

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 { 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()
); );

View File

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