mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Merge branch 'main' into archive_table
This commit is contained in:
		
						commit
						3040256047
					
				| @ -1,6 +1,5 @@ | |||||||
| import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | ||||||
| import { | import { | ||||||
|     TableSearch, |  | ||||||
|     SortableTableHeader, |     SortableTableHeader, | ||||||
|     TableCell, |     TableCell, | ||||||
|     TablePlaceholder, |     TablePlaceholder, | ||||||
| @ -26,6 +25,7 @@ import { formatExpiredAt } from 'component/Reporting/ReportExpiredCell/formatExp | |||||||
| import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell'; | import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell'; | ||||||
| import theme from 'themes/theme'; | import theme from 'themes/theme'; | ||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
| 
 | 
 | ||||||
| interface IReportTableProps { | interface IReportTableProps { | ||||||
|     projectId: string; |     projectId: string; | ||||||
| @ -95,7 +95,7 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => { | |||||||
|         <PageHeader |         <PageHeader | ||||||
|             title="Overview" |             title="Overview" | ||||||
|             actions={ |             actions={ | ||||||
|                 <TableSearch |                 <Search | ||||||
|                     initialValue={globalFilter} |                     initialValue={globalFilter} | ||||||
|                     onChange={setGlobalFilter} |                     onChange={setGlobalFilter} | ||||||
|                 /> |                 /> | ||||||
|  | |||||||
| @ -3,7 +3,6 @@ import { useTable, useGlobalFilter, useSortBy } from 'react-table'; | |||||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | import { PageContent } from 'component/common/PageContent/PageContent'; | ||||||
| import { | import { | ||||||
|     SortableTableHeader, |     SortableTableHeader, | ||||||
|     TableSearch, |  | ||||||
|     TableCell, |     TableCell, | ||||||
|     TablePlaceholder, |     TablePlaceholder, | ||||||
| } from 'component/common/Table'; | } from 'component/common/Table'; | ||||||
| @ -25,6 +24,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | |||||||
| import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList'; | import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList'; | ||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
| import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; | import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
| 
 | 
 | ||||||
| export const ApiTokenTable = () => { | export const ApiTokenTable = () => { | ||||||
|     const { tokens, loading } = useApiTokens(); |     const { tokens, loading } = useApiTokens(); | ||||||
| @ -57,7 +57,7 @@ export const ApiTokenTable = () => { | |||||||
|     }, [setHiddenColumns, hiddenColumns]); |     }, [setHiddenColumns, hiddenColumns]); | ||||||
| 
 | 
 | ||||||
|     const headerSearch = ( |     const headerSearch = ( | ||||||
|         <TableSearch initialValue={globalFilter} onChange={setGlobalFilter} /> |         <Search initialValue={globalFilter} onChange={setGlobalFilter} /> | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const headerActions = ( |     const headerActions = ( | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ import { | |||||||
|     TableCell, |     TableCell, | ||||||
|     TableRow, |     TableRow, | ||||||
|     TablePlaceholder, |     TablePlaceholder, | ||||||
|     TableSearch, |  | ||||||
| } from 'component/common/Table'; | } from 'component/common/Table'; | ||||||
| import { useTable, useGlobalFilter, useSortBy } from 'react-table'; | import { useTable, useGlobalFilter, useSortBy } from 'react-table'; | ||||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||||
| @ -28,6 +27,7 @@ import { sortTypes } from 'utils/sortTypes'; | |||||||
| import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; | import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; | ||||||
| import theme from 'themes/theme'; | import theme from 'themes/theme'; | ||||||
| import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; | import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
| 
 | 
 | ||||||
| const ROOTROLE = 'root'; | const ROOTROLE = 'root'; | ||||||
| const BUILTIN_ROLE_TYPE = 'project'; | const BUILTIN_ROLE_TYPE = 'project'; | ||||||
| @ -190,7 +190,7 @@ const ProjectRoleList = () => { | |||||||
|                     title="Project roles" |                     title="Project roles" | ||||||
|                     actions={ |                     actions={ | ||||||
|                         <> |                         <> | ||||||
|                             <TableSearch |                             <Search | ||||||
|                                 initialValue={globalFilter} |                                 initialValue={globalFilter} | ||||||
|                                 onChange={setGlobalFilter} |                                 onChange={setGlobalFilter} | ||||||
|                             /> |                             /> | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ import { | |||||||
|     TableCell, |     TableCell, | ||||||
|     TableRow, |     TableRow, | ||||||
|     TablePlaceholder, |     TablePlaceholder, | ||||||
|     TableSearch, |  | ||||||
| } from 'component/common/Table'; | } from 'component/common/Table'; | ||||||
| import ChangePassword from './ChangePassword/ChangePassword'; | import ChangePassword from './ChangePassword/ChangePassword'; | ||||||
| import DeleteUser from './DeleteUser/DeleteUser'; | import DeleteUser from './DeleteUser/DeleteUser'; | ||||||
| @ -34,6 +33,7 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; | |||||||
| import theme from 'themes/theme'; | import theme from 'themes/theme'; | ||||||
| import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; | import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; | ||||||
| import { UsersActionsCell } from './UsersActionsCell/UsersActionsCell'; | import { UsersActionsCell } from './UsersActionsCell/UsersActionsCell'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
| 
 | 
 | ||||||
| const StyledAvatar = styled(Avatar)(({ theme }) => ({ | const StyledAvatar = styled(Avatar)(({ theme }) => ({ | ||||||
|     width: theme.spacing(4), |     width: theme.spacing(4), | ||||||
| @ -248,7 +248,7 @@ const UsersList = () => { | |||||||
|                     title="Users" |                     title="Users" | ||||||
|                     actions={ |                     actions={ | ||||||
|                         <> |                         <> | ||||||
|                             <TableSearch |                             <Search | ||||||
|                                 initialValue={globalFilter} |                                 initialValue={globalFilter} | ||||||
|                                 onChange={setGlobalFilter} |                                 onChange={setGlobalFilter} | ||||||
|                             /> |                             /> | ||||||
|  | |||||||
| @ -1,23 +1,40 @@ | |||||||
| import { useMemo, useState } from 'react'; | import { useEffect, useMemo, useState } from 'react'; | ||||||
| import { CircularProgress } from '@mui/material'; | import { CircularProgress } from '@mui/material'; | ||||||
| import { Warning } from '@mui/icons-material'; | import { Warning } from '@mui/icons-material'; | ||||||
| import { AppsLinkList, styles as themeStyles } from 'component/common'; | import { AppsLinkList, styles as themeStyles } from 'component/common'; | ||||||
| import { SearchField } from 'component/common/SearchField/SearchField'; |  | ||||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | import { PageContent } from 'component/common/PageContent/PageContent'; | ||||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||||
| import useApplications from 'hooks/api/getters/useApplications/useApplications'; | import useApplications from 'hooks/api/getters/useApplications/useApplications'; | ||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
|  | import { useSearchParams } from 'react-router-dom'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
|  | 
 | ||||||
|  | type PageQueryType = Partial<Record<'search', string>>; | ||||||
| 
 | 
 | ||||||
| export const ApplicationList = () => { | export const ApplicationList = () => { | ||||||
|     const { applications, loading } = useApplications(); |     const { applications, loading } = useApplications(); | ||||||
|     const [filter, setFilter] = useState(''); |     const [searchParams, setSearchParams] = useSearchParams(); | ||||||
|  |     const [searchValue, setSearchValue] = useState( | ||||||
|  |         searchParams.get('search') || '' | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     useEffect(() => { | ||||||
|  |         const tableState: PageQueryType = {}; | ||||||
|  |         if (searchValue) { | ||||||
|  |             tableState.search = searchValue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         setSearchParams(tableState, { | ||||||
|  |             replace: true, | ||||||
|  |         }); | ||||||
|  |     }, [searchValue, setSearchParams]); | ||||||
| 
 | 
 | ||||||
|     const filteredApplications = useMemo(() => { |     const filteredApplications = useMemo(() => { | ||||||
|         const regExp = new RegExp(filter, 'i'); |         const regExp = new RegExp(searchValue, 'i'); | ||||||
|         return filter |         return searchValue | ||||||
|             ? applications?.filter(a => regExp.test(a.appName)) |             ? applications?.filter(a => regExp.test(a.appName)) | ||||||
|             : applications; |             : applications; | ||||||
|     }, [applications, filter]); |     }, [applications, searchValue]); | ||||||
| 
 | 
 | ||||||
|     const renderNoApplications = () => ( |     const renderNoApplications = () => ( | ||||||
|         <> |         <> | ||||||
| @ -44,10 +61,19 @@ export const ApplicationList = () => { | |||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|         <> |         <> | ||||||
|             <div className={themeStyles.searchField}> |             <PageContent | ||||||
|                 <SearchField initialValue={filter} updateValue={setFilter} /> |                 header={ | ||||||
|             </div> |                     <PageHeader | ||||||
|             <PageContent header={<PageHeader title="Applications" />}> |                         title="Applications" | ||||||
|  |                         actions={ | ||||||
|  |                             <Search | ||||||
|  |                                 initialValue={searchValue} | ||||||
|  |                                 onChange={setSearchValue} | ||||||
|  |                             /> | ||||||
|  |                         } | ||||||
|  |                     /> | ||||||
|  |                 } | ||||||
|  |             > | ||||||
|                 <div className={themeStyles.fullwidth}> |                 <div className={themeStyles.fullwidth}> | ||||||
|                     <ConditionallyRender |                     <ConditionallyRender | ||||||
|                         condition={filteredApplications.length > 0} |                         condition={filteredApplications.length > 0} | ||||||
|  | |||||||
							
								
								
									
										46
									
								
								frontend/src/component/common/Search/Search.styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								frontend/src/component/common/Search/Search.styles.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | import { makeStyles } from 'tss-react/mui'; | ||||||
|  | 
 | ||||||
|  | export const useStyles = makeStyles()(theme => ({ | ||||||
|  |     container: { | ||||||
|  |         display: 'flex', | ||||||
|  |         flexGrow: 1, | ||||||
|  |         alignItems: 'center', | ||||||
|  |         position: 'relative', | ||||||
|  |         maxWidth: '400px', | ||||||
|  |         [theme.breakpoints.down('md')]: { | ||||||
|  |             marginTop: theme.spacing(1), | ||||||
|  |             maxWidth: '100%', | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     search: { | ||||||
|  |         display: 'flex', | ||||||
|  |         alignItems: 'center', | ||||||
|  |         backgroundColor: theme.palette.background.paper, | ||||||
|  |         border: `1px solid ${theme.palette.grey[300]}`, | ||||||
|  |         borderRadius: theme.shape.borderRadiusExtraLarge, | ||||||
|  |         padding: '3px 5px 3px 12px', | ||||||
|  |         width: '100%', | ||||||
|  |         zIndex: 3, | ||||||
|  |         '&.search-container:focus-within': { | ||||||
|  |             borderColor: theme.palette.primary.light, | ||||||
|  |             boxShadow: theme.boxShadows.main, | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     searchIcon: { | ||||||
|  |         marginRight: 8, | ||||||
|  |         color: theme.palette.inactiveIcon, | ||||||
|  |     }, | ||||||
|  |     clearContainer: { | ||||||
|  |         width: '30px', | ||||||
|  |         '& > button': { | ||||||
|  |             padding: '7px', | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     clearIcon: { | ||||||
|  |         color: theme.palette.grey[700], | ||||||
|  |         fontSize: '18px', | ||||||
|  |     }, | ||||||
|  |     inputRoot: { | ||||||
|  |         width: '100%', | ||||||
|  |     }, | ||||||
|  | })); | ||||||
							
								
								
									
										115
									
								
								frontend/src/component/common/Search/Search.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								frontend/src/component/common/Search/Search.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,115 @@ | |||||||
|  | import { useRef, useState } from 'react'; | ||||||
|  | import { IconButton, InputBase, Tooltip } from '@mui/material'; | ||||||
|  | import { Search as SearchIcon, Close } from '@mui/icons-material'; | ||||||
|  | import classnames from 'classnames'; | ||||||
|  | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
|  | import { useStyles } from './Search.styles'; | ||||||
|  | import { SearchSuggestions } from './SearchSuggestions/SearchSuggestions'; | ||||||
|  | import { IGetSearchContextOutput } from 'hooks/useSearch'; | ||||||
|  | import { useKeyboardShortcut } from 'hooks/useKeyboardShortcut'; | ||||||
|  | import { useAsyncDebounce } from 'react-table'; | ||||||
|  | 
 | ||||||
|  | interface ISearchProps { | ||||||
|  |     initialValue?: string; | ||||||
|  |     onChange: (value: string) => void; | ||||||
|  |     className?: string; | ||||||
|  |     placeholder?: string; | ||||||
|  |     hasFilters?: boolean; | ||||||
|  |     getSearchContext?: () => IGetSearchContextOutput; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const Search = ({ | ||||||
|  |     initialValue = '', | ||||||
|  |     onChange, | ||||||
|  |     className, | ||||||
|  |     placeholder: customPlaceholder, | ||||||
|  |     hasFilters, | ||||||
|  |     getSearchContext, | ||||||
|  | }: ISearchProps) => { | ||||||
|  |     const ref = useRef<HTMLInputElement>(); | ||||||
|  |     const { classes: styles } = useStyles(); | ||||||
|  |     const [showSuggestions, setShowSuggestions] = useState(false); | ||||||
|  | 
 | ||||||
|  |     const [value, setValue] = useState(initialValue); | ||||||
|  | 
 | ||||||
|  |     const debouncedOnChange = useAsyncDebounce(onChange, 200); | ||||||
|  | 
 | ||||||
|  |     const onSearchChange = (value: string) => { | ||||||
|  |         debouncedOnChange(value); | ||||||
|  |         setValue(value); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const hotkey = useKeyboardShortcut( | ||||||
|  |         { modifiers: ['ctrl'], key: 'k', preventDefault: true }, | ||||||
|  |         () => { | ||||||
|  |             if (document.activeElement === ref.current) { | ||||||
|  |                 ref.current?.blur(); | ||||||
|  |             } else { | ||||||
|  |                 ref.current?.focus(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     ); | ||||||
|  |     useKeyboardShortcut({ key: 'Escape' }, () => { | ||||||
|  |         if (document.activeElement === ref.current) { | ||||||
|  |             ref.current?.blur(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |     const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`; | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <div className={styles.container}> | ||||||
|  |             <div | ||||||
|  |                 className={classnames( | ||||||
|  |                     styles.search, | ||||||
|  |                     className, | ||||||
|  |                     'search-container' | ||||||
|  |                 )} | ||||||
|  |             > | ||||||
|  |                 <SearchIcon | ||||||
|  |                     className={classnames(styles.searchIcon, 'search-icon')} | ||||||
|  |                 /> | ||||||
|  |                 <InputBase | ||||||
|  |                     inputRef={ref} | ||||||
|  |                     placeholder={placeholder} | ||||||
|  |                     classes={{ | ||||||
|  |                         root: classnames(styles.inputRoot, 'input-container'), | ||||||
|  |                     }} | ||||||
|  |                     inputProps={{ 'aria-label': placeholder }} | ||||||
|  |                     value={value} | ||||||
|  |                     onChange={e => onSearchChange(e.target.value)} | ||||||
|  |                     onFocus={() => setShowSuggestions(true)} | ||||||
|  |                     onBlur={() => setShowSuggestions(false)} | ||||||
|  |                 /> | ||||||
|  |                 <div | ||||||
|  |                     className={classnames( | ||||||
|  |                         styles.clearContainer, | ||||||
|  |                         'clear-container' | ||||||
|  |                     )} | ||||||
|  |                 > | ||||||
|  |                     <ConditionallyRender | ||||||
|  |                         condition={Boolean(value)} | ||||||
|  |                         show={ | ||||||
|  |                             <Tooltip title="Clear search query" arrow> | ||||||
|  |                                 <IconButton | ||||||
|  |                                     size="small" | ||||||
|  |                                     onClick={() => { | ||||||
|  |                                         onChange(''); | ||||||
|  |                                         ref.current?.focus(); | ||||||
|  |                                     }} | ||||||
|  |                                 > | ||||||
|  |                                     <Close className={styles.clearIcon} /> | ||||||
|  |                                 </IconButton> | ||||||
|  |                             </Tooltip> | ||||||
|  |                         } | ||||||
|  |                     /> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |             <ConditionallyRender | ||||||
|  |                 condition={Boolean(hasFilters) && showSuggestions} | ||||||
|  |                 show={ | ||||||
|  |                     <SearchSuggestions getSearchContext={getSearchContext!} /> | ||||||
|  |                 } | ||||||
|  |             /> | ||||||
|  |         </div> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -0,0 +1,72 @@ | |||||||
|  | import { styled } from '@mui/material'; | ||||||
|  | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
|  | import { | ||||||
|  |     getSearchTextGenerator, | ||||||
|  |     IGetSearchContextOutput, | ||||||
|  | } from 'hooks/useSearch'; | ||||||
|  | import { VFC } from 'react'; | ||||||
|  | 
 | ||||||
|  | const StyledHeader = styled('span')(({ theme }) => ({ | ||||||
|  |     fontSize: theme.fontSizes.smallBody, | ||||||
|  |     color: theme.palette.text.primary, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledCode = styled('span')(({ theme }) => ({ | ||||||
|  |     backgroundColor: theme.palette.secondaryContainer, | ||||||
|  |     color: theme.palette.text.primary, | ||||||
|  |     padding: theme.spacing(0, 0.5), | ||||||
|  |     borderRadius: theme.spacing(0.5), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | interface ISearchDescriptionProps { | ||||||
|  |     filters: any[]; | ||||||
|  |     getSearchContext: () => IGetSearchContextOutput; | ||||||
|  |     searchableColumnsString: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const SearchDescription: VFC<ISearchDescriptionProps> = ({ | ||||||
|  |     filters, | ||||||
|  |     getSearchContext, | ||||||
|  |     searchableColumnsString, | ||||||
|  | }) => { | ||||||
|  |     const searchContext = getSearchContext(); | ||||||
|  |     const getSearchText = getSearchTextGenerator(searchContext.columns); | ||||||
|  |     const searchText = getSearchText(searchContext.searchValue); | ||||||
|  |     const searchFilters = filters.filter(filter => filter.values.length > 0); | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <> | ||||||
|  |             <ConditionallyRender | ||||||
|  |                 condition={Boolean(searchText)} | ||||||
|  |                 show={ | ||||||
|  |                     <> | ||||||
|  |                         <StyledHeader>Searching for:</StyledHeader> | ||||||
|  |                         <p> | ||||||
|  |                             <StyledCode>{searchText}</StyledCode>{' '} | ||||||
|  |                             {searchableColumnsString | ||||||
|  |                                 ? ` in ${searchableColumnsString}` | ||||||
|  |                                 : ''} | ||||||
|  |                         </p> | ||||||
|  |                     </> | ||||||
|  |                 } | ||||||
|  |             /> | ||||||
|  |             <ConditionallyRender | ||||||
|  |                 condition={searchFilters.length > 0} | ||||||
|  |                 show={ | ||||||
|  |                     <> | ||||||
|  |                         <StyledHeader>Filtering by:</StyledHeader> | ||||||
|  |                         {searchFilters.map(filter => ( | ||||||
|  |                             <p key={filter.name}> | ||||||
|  |                                 <StyledCode> | ||||||
|  |                                     {filter.values.join(',')} | ||||||
|  |                                 </StyledCode>{' '} | ||||||
|  |                                 in {filter.header}. Options:{' '} | ||||||
|  |                                 {filter.options.join(', ')} | ||||||
|  |                             </p> | ||||||
|  |                         ))} | ||||||
|  |                     </> | ||||||
|  |                 } | ||||||
|  |             /> | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -0,0 +1,62 @@ | |||||||
|  | import { styled } from '@mui/material'; | ||||||
|  | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
|  | import { IGetSearchContextOutput } from 'hooks/useSearch'; | ||||||
|  | import { VFC } from 'react'; | ||||||
|  | 
 | ||||||
|  | const StyledHeader = styled('span')(({ theme }) => ({ | ||||||
|  |     fontSize: theme.fontSizes.smallBody, | ||||||
|  |     color: theme.palette.text.primary, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledCode = styled('span')(({ theme }) => ({ | ||||||
|  |     backgroundColor: theme.palette.secondaryContainer, | ||||||
|  |     color: theme.palette.text.primary, | ||||||
|  |     padding: theme.spacing(0, 0.5), | ||||||
|  |     borderRadius: theme.spacing(0.5), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | interface ISearchInstructionsProps { | ||||||
|  |     filters: any[]; | ||||||
|  |     getSearchContext: () => IGetSearchContextOutput; | ||||||
|  |     searchableColumnsString: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const SearchInstructions: VFC<ISearchInstructionsProps> = ({ | ||||||
|  |     filters, | ||||||
|  |     getSearchContext, | ||||||
|  |     searchableColumnsString, | ||||||
|  | }) => { | ||||||
|  |     return ( | ||||||
|  |         <> | ||||||
|  |             <StyledHeader> | ||||||
|  |                 {filters.length > 0 | ||||||
|  |                     ? 'Filter your search with operators like:' | ||||||
|  |                     : `Start typing to search${ | ||||||
|  |                           searchableColumnsString | ||||||
|  |                               ? ` in ${searchableColumnsString}` | ||||||
|  |                               : '...' | ||||||
|  |                       }`}
 | ||||||
|  |             </StyledHeader> | ||||||
|  |             {filters.map(filter => ( | ||||||
|  |                 <p key={filter.name}> | ||||||
|  |                     Filter by {filter.header}:{' '} | ||||||
|  |                     <StyledCode> | ||||||
|  |                         {filter.name}:{filter.options[0]} | ||||||
|  |                     </StyledCode> | ||||||
|  |                     <ConditionallyRender | ||||||
|  |                         condition={filter.options.length > 1} | ||||||
|  |                         show={ | ||||||
|  |                             <> | ||||||
|  |                                 {' or '} | ||||||
|  |                                 <StyledCode> | ||||||
|  |                                     {filter.name}: | ||||||
|  |                                     {filter.options.slice(0, 2).join(',')} | ||||||
|  |                                 </StyledCode> | ||||||
|  |                             </> | ||||||
|  |                         } | ||||||
|  |                     /> | ||||||
|  |                 </p> | ||||||
|  |             ))} | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -0,0 +1,150 @@ | |||||||
|  | import { FilterList } from '@mui/icons-material'; | ||||||
|  | import { Box, Divider, Paper, styled } from '@mui/material'; | ||||||
|  | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
|  | import { | ||||||
|  |     getColumnValues, | ||||||
|  |     getFilterableColumns, | ||||||
|  |     getFilterValues, | ||||||
|  |     IGetSearchContextOutput, | ||||||
|  | } from 'hooks/useSearch'; | ||||||
|  | import { useMemo, VFC } from 'react'; | ||||||
|  | import { SearchDescription } from './SearchDescription/SearchDescription'; | ||||||
|  | import { SearchInstructions } from './SearchInstructions/SearchInstructions'; | ||||||
|  | 
 | ||||||
|  | const randomIndex = (arr: any[]) => Math.floor(Math.random() * arr.length); | ||||||
|  | 
 | ||||||
|  | const StyledPaper = styled(Paper)(({ theme }) => ({ | ||||||
|  |     position: 'absolute', | ||||||
|  |     width: '100%', | ||||||
|  |     left: 0, | ||||||
|  |     top: '20px', | ||||||
|  |     zIndex: 2, | ||||||
|  |     padding: theme.spacing(4, 1.5, 1.5), | ||||||
|  |     borderBottomLeftRadius: theme.spacing(1), | ||||||
|  |     borderBottomRightRadius: theme.spacing(1), | ||||||
|  |     boxShadow: '0px 8px 20px rgba(33, 33, 33, 0.15)', | ||||||
|  |     fontSize: theme.fontSizes.smallBody, | ||||||
|  |     color: theme.palette.text.secondary, | ||||||
|  |     wordBreak: 'break-word', | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledBox = styled(Box)(({ theme }) => ({ | ||||||
|  |     display: 'flex', | ||||||
|  |     gap: theme.spacing(2), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledFilterList = styled(FilterList)(({ theme }) => ({ | ||||||
|  |     color: theme.palette.text.secondary, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledDivider = styled(Divider)(({ theme }) => ({ | ||||||
|  |     border: `1px dashed ${theme.palette.dividerAlternative}`, | ||||||
|  |     margin: theme.spacing(1.5, 0), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledCode = styled('span')(({ theme }) => ({ | ||||||
|  |     backgroundColor: theme.palette.secondaryContainer, | ||||||
|  |     color: theme.palette.text.primary, | ||||||
|  |     padding: theme.spacing(0, 0.5), | ||||||
|  |     borderRadius: theme.spacing(0.5), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | interface SearchSuggestionsProps { | ||||||
|  |     getSearchContext: () => IGetSearchContextOutput; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({ | ||||||
|  |     getSearchContext, | ||||||
|  | }) => { | ||||||
|  |     const searchContext = getSearchContext(); | ||||||
|  | 
 | ||||||
|  |     const randomRow = useMemo( | ||||||
|  |         () => randomIndex(searchContext.data), | ||||||
|  |         [searchContext.data] | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const filters = getFilterableColumns(searchContext.columns) | ||||||
|  |         .map(column => { | ||||||
|  |             const filterOptions = searchContext.data.map(row => | ||||||
|  |                 getColumnValues(column, row) | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             return { | ||||||
|  |                 name: column.filterName, | ||||||
|  |                 header: column.Header ?? column.filterName, | ||||||
|  |                 options: [...new Set(filterOptions)].sort((a, b) => | ||||||
|  |                     a.localeCompare(b) | ||||||
|  |                 ), | ||||||
|  |                 suggestedOption: | ||||||
|  |                     filterOptions[randomRow] ?? `example-${column.filterName}`, | ||||||
|  |                 values: getFilterValues( | ||||||
|  |                     column.filterName, | ||||||
|  |                     searchContext.searchValue | ||||||
|  |                 ), | ||||||
|  |             }; | ||||||
|  |         }) | ||||||
|  |         .sort((a, b) => a.name.localeCompare(b.name)); | ||||||
|  | 
 | ||||||
|  |     const searchableColumns = searchContext.columns.filter( | ||||||
|  |         column => column.searchable && column.accessor | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const searchableColumnsString = searchableColumns | ||||||
|  |         .map(column => column.Header ?? column.accessor) | ||||||
|  |         .join(', '); | ||||||
|  | 
 | ||||||
|  |     const suggestedTextSearch = | ||||||
|  |         searchContext.data.length && searchableColumns.length | ||||||
|  |             ? getColumnValues( | ||||||
|  |                   searchableColumns[0], | ||||||
|  |                   searchContext.data[randomRow] | ||||||
|  |               ) | ||||||
|  |             : 'example-search-text'; | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <StyledPaper> | ||||||
|  |             <StyledBox> | ||||||
|  |                 <StyledFilterList /> | ||||||
|  |                 <Box> | ||||||
|  |                     <ConditionallyRender | ||||||
|  |                         condition={Boolean(searchContext.searchValue)} | ||||||
|  |                         show={ | ||||||
|  |                             <SearchDescription | ||||||
|  |                                 filters={filters} | ||||||
|  |                                 getSearchContext={getSearchContext} | ||||||
|  |                                 searchableColumnsString={ | ||||||
|  |                                     searchableColumnsString | ||||||
|  |                                 } | ||||||
|  |                             /> | ||||||
|  |                         } | ||||||
|  |                         elseShow={ | ||||||
|  |                             <SearchInstructions | ||||||
|  |                                 filters={filters} | ||||||
|  |                                 getSearchContext={getSearchContext} | ||||||
|  |                                 searchableColumnsString={ | ||||||
|  |                                     searchableColumnsString | ||||||
|  |                                 } | ||||||
|  |                             /> | ||||||
|  |                         } | ||||||
|  |                     /> | ||||||
|  |                 </Box> | ||||||
|  |             </StyledBox> | ||||||
|  |             <StyledDivider /> | ||||||
|  |             <ConditionallyRender | ||||||
|  |                 condition={filters.length > 0} | ||||||
|  |                 show="Combine filters and search." | ||||||
|  |             /> | ||||||
|  |             <p> | ||||||
|  |                 Example:{' '} | ||||||
|  |                 <StyledCode> | ||||||
|  |                     {filters.map(filter => ( | ||||||
|  |                         <span key={filter.name}> | ||||||
|  |                             {filter.name}:{filter.suggestedOption}{' '} | ||||||
|  |                         </span> | ||||||
|  |                     ))} | ||||||
|  |                     <span>{suggestedTextSearch}</span> | ||||||
|  |                 </StyledCode> | ||||||
|  |             </p> | ||||||
|  |         </StyledPaper> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -13,6 +13,9 @@ interface ISearchFieldProps { | |||||||
|     showValueChip?: boolean; |     showValueChip?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @deprecated use `Search` instead. | ||||||
|  |  */ | ||||||
| export const SearchField: VFC<ISearchFieldProps> = ({ | export const SearchField: VFC<ISearchFieldProps> = ({ | ||||||
|     updateValue, |     updateValue, | ||||||
|     initialValue = '', |     initialValue = '', | ||||||
|  | |||||||
| @ -9,5 +9,6 @@ export const useStyles = makeStyles()(theme => ({ | |||||||
|         justifyContent: 'center', |         justifyContent: 'center', | ||||||
|         alignItems: 'center', |         alignItems: 'center', | ||||||
|         marginTop: theme.spacing(2), |         marginTop: theme.spacing(2), | ||||||
|  |         width: '100%', | ||||||
|     }, |     }, | ||||||
| })); | })); | ||||||
|  | |||||||
| @ -11,6 +11,9 @@ interface ITableSearchProps { | |||||||
|     getSearchContext?: () => IGetSearchContextOutput; |     getSearchContext?: () => IGetSearchContextOutput; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @deprecated use `Search` instead. | ||||||
|  |  */ | ||||||
| export const TableSearch: FC<ITableSearchProps> = ({ | export const TableSearch: FC<ITableSearchProps> = ({ | ||||||
|     initialValue, |     initialValue, | ||||||
|     onChange = () => {}, |     onChange = () => {}, | ||||||
|  | |||||||
| @ -17,6 +17,9 @@ interface ITableSearchFieldProps { | |||||||
|     getSearchContext?: () => IGetSearchContextOutput; |     getSearchContext?: () => IGetSearchContextOutput; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @deprecated use `Search` instead. | ||||||
|  |  */ | ||||||
| export const TableSearchField = ({ | export const TableSearchField = ({ | ||||||
|     value = '', |     value = '', | ||||||
|     onChange, |     onChange, | ||||||
| @ -28,16 +31,20 @@ export const TableSearchField = ({ | |||||||
|     const ref = useRef<HTMLInputElement>(); |     const ref = useRef<HTMLInputElement>(); | ||||||
|     const { classes: styles } = useStyles(); |     const { classes: styles } = useStyles(); | ||||||
|     const [showSuggestions, setShowSuggestions] = useState(false); |     const [showSuggestions, setShowSuggestions] = useState(false); | ||||||
|  | 
 | ||||||
|     const hotkey = useKeyboardShortcut( |     const hotkey = useKeyboardShortcut( | ||||||
|         { modifiers: ['ctrl'], key: 'k', preventDefault: true }, |         { modifiers: ['ctrl'], key: 'k', preventDefault: true }, | ||||||
|         () => { |         () => { | ||||||
|  |             if (document.activeElement === ref.current) { | ||||||
|  |                 ref.current?.blur(); | ||||||
|  |             } else { | ||||||
|                 ref.current?.focus(); |                 ref.current?.focus(); | ||||||
|             setShowSuggestions(true); |             } | ||||||
|         } |         } | ||||||
|     ); |     ); | ||||||
|     useKeyboardShortcut({ key: 'Escape' }, () => { |     useKeyboardShortcut({ key: 'Escape' }, () => { | ||||||
|         if (document.activeElement === ref.current) { |         if (document.activeElement === ref.current) { | ||||||
|             setShowSuggestions(suggestions => !suggestions); |             ref.current?.blur(); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|     const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`; |     const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`; | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ import { | |||||||
|     TableCell, |     TableCell, | ||||||
|     TableRow, |     TableRow, | ||||||
|     TablePlaceholder, |     TablePlaceholder, | ||||||
|     TableSearch, |  | ||||||
| } from 'component/common/Table'; | } from 'component/common/Table'; | ||||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | import { PageContent } from 'component/common/PageContent/PageContent'; | ||||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||||
| @ -24,6 +23,7 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; | |||||||
| import { ContextActionsCell } from './ContextActionsCell/ContextActionsCell'; | import { ContextActionsCell } from './ContextActionsCell/ContextActionsCell'; | ||||||
| import { Adjust } from '@mui/icons-material'; | import { Adjust } from '@mui/icons-material'; | ||||||
| import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; | import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
| 
 | 
 | ||||||
| const ContextList: VFC = () => { | const ContextList: VFC = () => { | ||||||
|     const [showDelDialogue, setShowDelDialogue] = useState(false); |     const [showDelDialogue, setShowDelDialogue] = useState(false); | ||||||
| @ -164,7 +164,7 @@ const ContextList: VFC = () => { | |||||||
|                     title="Context fields" |                     title="Context fields" | ||||||
|                     actions={ |                     actions={ | ||||||
|                         <> |                         <> | ||||||
|                             <TableSearch |                             <Search | ||||||
|                                 initialValue={globalFilter} |                                 initialValue={globalFilter} | ||||||
|                                 onChange={setGlobalFilter} |                                 onChange={setGlobalFilter} | ||||||
|                             /> |                             /> | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironmen | |||||||
| import { CreateEnvironmentButton } from 'component/environments/CreateEnvironmentButton/CreateEnvironmentButton'; | import { CreateEnvironmentButton } from 'component/environments/CreateEnvironmentButton/CreateEnvironmentButton'; | ||||||
| import { useTable, useGlobalFilter } from 'react-table'; | import { useTable, useGlobalFilter } from 'react-table'; | ||||||
| import { | import { | ||||||
|     TableSearch, |  | ||||||
|     SortableTableHeader, |     SortableTableHeader, | ||||||
|     Table, |     Table, | ||||||
|     TablePlaceholder, |     TablePlaceholder, | ||||||
| @ -24,6 +23,7 @@ import useEnvironmentApi, { | |||||||
| } from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; | } from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
| 
 | 
 | ||||||
| const StyledAlert = styled(Alert)(({ theme }) => ({ | const StyledAlert = styled(Alert)(({ theme }) => ({ | ||||||
|     marginBottom: theme.spacing(4), |     marginBottom: theme.spacing(4), | ||||||
| @ -71,7 +71,7 @@ export const EnvironmentTable = () => { | |||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const headerSearch = ( |     const headerSearch = ( | ||||||
|         <TableSearch initialValue={globalFilter} onChange={setGlobalFilter} /> |         <Search initialValue={globalFilter} onChange={setGlobalFilter} /> | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const headerActions = ( |     const headerActions = ( | ||||||
|  | |||||||
| @ -9,7 +9,6 @@ import { | |||||||
|     TableCell, |     TableCell, | ||||||
|     TableRow, |     TableRow, | ||||||
|     TablePlaceholder, |     TablePlaceholder, | ||||||
|     TableSearch, |  | ||||||
| } from 'component/common/Table'; | } from 'component/common/Table'; | ||||||
| import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures'; | import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures'; | ||||||
| import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | ||||||
| @ -29,6 +28,7 @@ import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton' | |||||||
| import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell'; | import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell'; | ||||||
| import { useStyles } from './styles'; | import { useStyles } from './styles'; | ||||||
| import { useSearch } from 'hooks/useSearch'; | import { useSearch } from 'hooks/useSearch'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
| 
 | 
 | ||||||
| export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({ | export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({ | ||||||
|     name: 'Name of the feature', |     name: 'Name of the feature', | ||||||
| @ -210,7 +210,7 @@ export const FeatureToggleListTable: VFC = () => { | |||||||
|                                 condition={!isSmallScreen} |                                 condition={!isSmallScreen} | ||||||
|                                 show={ |                                 show={ | ||||||
|                                     <> |                                     <> | ||||||
|                                         <TableSearch |                                         <Search | ||||||
|                                             initialValue={searchValue} |                                             initialValue={searchValue} | ||||||
|                                             onChange={setSearchValue} |                                             onChange={setSearchValue} | ||||||
|                                             hasFilters |                                             hasFilters | ||||||
| @ -238,7 +238,7 @@ export const FeatureToggleListTable: VFC = () => { | |||||||
|                     <ConditionallyRender |                     <ConditionallyRender | ||||||
|                         condition={isSmallScreen} |                         condition={isSmallScreen} | ||||||
|                         show={ |                         show={ | ||||||
|                             <TableSearch |                             <Search | ||||||
|                                 initialValue={searchValue} |                                 initialValue={searchValue} | ||||||
|                                 onChange={setSearchValue} |                                 onChange={setSearchValue} | ||||||
|                                 hasFilters |                                 hasFilters | ||||||
|  | |||||||
| @ -25,7 +25,6 @@ import { | |||||||
|     TableCell, |     TableCell, | ||||||
|     TableRow, |     TableRow, | ||||||
|     TablePlaceholder, |     TablePlaceholder, | ||||||
|     TableSearch, |  | ||||||
| } from 'component/common/Table'; | } from 'component/common/Table'; | ||||||
| import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | ||||||
| import useProject from 'hooks/api/getters/useProject/useProject'; | import useProject from 'hooks/api/getters/useProject/useProject'; | ||||||
| @ -44,6 +43,7 @@ import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureS | |||||||
| import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; | import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; | ||||||
| import { useSearch } from 'hooks/useSearch'; | import { useSearch } from 'hooks/useSearch'; | ||||||
| import { useMediaQuery } from '@mui/material'; | import { useMediaQuery } from '@mui/material'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
| 
 | 
 | ||||||
| interface IProjectFeatureTogglesProps { | interface IProjectFeatureTogglesProps { | ||||||
|     features: IProject['features']; |     features: IProject['features']; | ||||||
| @ -412,11 +412,9 @@ export const ProjectFeatureToggles = ({ | |||||||
|                             <ConditionallyRender |                             <ConditionallyRender | ||||||
|                                 condition={!isSmallScreen} |                                 condition={!isSmallScreen} | ||||||
|                                 show={ |                                 show={ | ||||||
|                                     <TableSearch |                                     <Search | ||||||
|                                         initialValue={searchValue} |                                         initialValue={searchValue} | ||||||
|                                         onChange={value => |                                         onChange={setSearchValue} | ||||||
|                                             setSearchValue(value) |  | ||||||
|                                         } |  | ||||||
|                                         hasFilters |                                         hasFilters | ||||||
|                                         getSearchContext={getSearchContext} |                                         getSearchContext={getSearchContext} | ||||||
|                                     /> |                                     /> | ||||||
| @ -454,7 +452,7 @@ export const ProjectFeatureToggles = ({ | |||||||
|                     <ConditionallyRender |                     <ConditionallyRender | ||||||
|                         condition={isSmallScreen} |                         condition={isSmallScreen} | ||||||
|                         show={ |                         show={ | ||||||
|                             <TableSearch |                             <Search | ||||||
|                                 initialValue={searchValue} |                                 initialValue={searchValue} | ||||||
|                                 onChange={setSearchValue} |                                 onChange={setSearchValue} | ||||||
|                                 hasFilters |                                 hasFilters | ||||||
|  | |||||||
| @ -21,20 +21,4 @@ export const useStyles = makeStyles()(theme => ({ | |||||||
|         fontFamily: theme.typography.fontFamily, |         fontFamily: theme.typography.fontFamily, | ||||||
|         pointer: 'cursor', |         pointer: 'cursor', | ||||||
|     }, |     }, | ||||||
|     searchBarContainer: { |  | ||||||
|         marginBottom: '2rem', |  | ||||||
|         display: 'flex', |  | ||||||
|         gap: '1rem', |  | ||||||
|         justifyContent: 'space-between', |  | ||||||
|         alignItems: 'center', |  | ||||||
|         [theme.breakpoints.down('sm')]: { |  | ||||||
|             display: 'block', |  | ||||||
|         }, |  | ||||||
|     }, |  | ||||||
|     searchBar: { |  | ||||||
|         minWidth: 450, |  | ||||||
|         [theme.breakpoints.down('sm')]: { |  | ||||||
|             minWidth: '100%', |  | ||||||
|         }, |  | ||||||
|     }, |  | ||||||
| })); | })); | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { useContext, useMemo, useState } from 'react'; | import { useContext, useEffect, useMemo, useState } from 'react'; | ||||||
| import { Link, useNavigate } from 'react-router-dom'; | import { Link, useNavigate, useSearchParams } from 'react-router-dom'; | ||||||
| import { mutate } from 'swr'; | import { mutate } from 'swr'; | ||||||
| import { getProjectFetcher } from 'hooks/api/getters/useProject/getProjectFetcher'; | import { getProjectFetcher } from 'hooks/api/getters/useProject/getProjectFetcher'; | ||||||
| import useProjects from 'hooks/api/getters/useProjects/useProjects'; | import useProjects from 'hooks/api/getters/useProjects/useProjects'; | ||||||
| @ -17,8 +17,12 @@ import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions'; | |||||||
| import { Add } from '@mui/icons-material'; | import { Add } from '@mui/icons-material'; | ||||||
| import ApiError from 'component/common/ApiError/ApiError'; | import ApiError from 'component/common/ApiError/ApiError'; | ||||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||||
| import { SearchField } from 'component/common/SearchField/SearchField'; | import { TablePlaceholder } from 'component/common/Table'; | ||||||
| import classnames from 'classnames'; | import { useMediaQuery } from '@mui/material'; | ||||||
|  | import theme from 'themes/theme'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
|  | 
 | ||||||
|  | type PageQueryType = Partial<Record<'search', string>>; | ||||||
| 
 | 
 | ||||||
| type projectMap = { | type projectMap = { | ||||||
|     [index: string]: boolean; |     [index: string]: boolean; | ||||||
| @ -51,14 +55,30 @@ export const ProjectListNew = () => { | |||||||
|     const [fetchedProjects, setFetchedProjects] = useState<projectMap>({}); |     const [fetchedProjects, setFetchedProjects] = useState<projectMap>({}); | ||||||
|     const ref = useLoading(loading); |     const ref = useLoading(loading); | ||||||
|     const { isOss } = useUiConfig(); |     const { isOss } = useUiConfig(); | ||||||
|     const [filter, setFilter] = useState(''); | 
 | ||||||
|  |     const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); | ||||||
|  |     const [searchParams, setSearchParams] = useSearchParams(); | ||||||
|  |     const [searchValue, setSearchValue] = useState( | ||||||
|  |         searchParams.get('search') || '' | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     useEffect(() => { | ||||||
|  |         const tableState: PageQueryType = {}; | ||||||
|  |         if (searchValue) { | ||||||
|  |             tableState.search = searchValue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         setSearchParams(tableState, { | ||||||
|  |             replace: true, | ||||||
|  |         }); | ||||||
|  |     }, [searchValue, setSearchParams]); | ||||||
| 
 | 
 | ||||||
|     const filteredProjects = useMemo(() => { |     const filteredProjects = useMemo(() => { | ||||||
|         const regExp = new RegExp(filter, 'i'); |         const regExp = new RegExp(searchValue, 'i'); | ||||||
|         return filter |         return searchValue | ||||||
|             ? projects.filter(project => regExp.test(project.name)) |             ? projects.filter(project => regExp.test(project.name)) | ||||||
|             : projects; |             : projects; | ||||||
|     }, [projects, filter]); |     }, [projects, searchValue]); | ||||||
| 
 | 
 | ||||||
|     const handleHover = (projectId: string) => { |     const handleHover = (projectId: string) => { | ||||||
|         if (fetchedProjects[projectId]) { |         if (fetchedProjects[projectId]) { | ||||||
| @ -129,21 +149,24 @@ export const ProjectListNew = () => { | |||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|         <div ref={ref}> |         <div ref={ref}> | ||||||
|             <div className={styles.searchBarContainer}> |  | ||||||
|                 <SearchField |  | ||||||
|                     initialValue={filter} |  | ||||||
|                     updateValue={setFilter} |  | ||||||
|                     showValueChip |  | ||||||
|                     className={classnames(styles.searchBar, { |  | ||||||
|                         skeleton: loading, |  | ||||||
|                     })} |  | ||||||
|                 /> |  | ||||||
|             </div> |  | ||||||
|             <PageContent |             <PageContent | ||||||
|                 header={ |                 header={ | ||||||
|                     <PageHeader |                     <PageHeader | ||||||
|                         title="Projects" |                         title="Projects" | ||||||
|                         actions={ |                         actions={ | ||||||
|  |                             <> | ||||||
|  |                                 <ConditionallyRender | ||||||
|  |                                     condition={!isSmallScreen} | ||||||
|  |                                     show={ | ||||||
|  |                                         <> | ||||||
|  |                                             <Search | ||||||
|  |                                                 initialValue={searchValue} | ||||||
|  |                                                 onChange={setSearchValue} | ||||||
|  |                                             /> | ||||||
|  |                                             <PageHeader.Divider /> | ||||||
|  |                                         </> | ||||||
|  |                                     } | ||||||
|  |                                 /> | ||||||
|                                 <ResponsiveButton |                                 <ResponsiveButton | ||||||
|                                     Icon={Add} |                                     Icon={Add} | ||||||
|                                     onClick={() => navigate('/projects/create')} |                                     onClick={() => navigate('/projects/create')} | ||||||
| @ -153,15 +176,42 @@ export const ProjectListNew = () => { | |||||||
|                                 > |                                 > | ||||||
|                                     New project |                                     New project | ||||||
|                                 </ResponsiveButton> |                                 </ResponsiveButton> | ||||||
|  |                             </> | ||||||
|  |                         } | ||||||
|  |                     > | ||||||
|  |                         <ConditionallyRender | ||||||
|  |                             condition={isSmallScreen} | ||||||
|  |                             show={ | ||||||
|  |                                 <Search | ||||||
|  |                                     initialValue={searchValue} | ||||||
|  |                                     onChange={setSearchValue} | ||||||
|  |                                 /> | ||||||
|                             } |                             } | ||||||
|                         /> |                         /> | ||||||
|  |                     </PageHeader> | ||||||
|                 } |                 } | ||||||
|             > |             > | ||||||
|                 <ConditionallyRender condition={error} show={renderError()} /> |                 <ConditionallyRender condition={error} show={renderError()} /> | ||||||
|                 <div className={styles.container}> |                 <div className={styles.container}> | ||||||
|                     <ConditionallyRender |                     <ConditionallyRender | ||||||
|                         condition={filteredProjects.length < 1 && !loading} |                         condition={filteredProjects.length < 1 && !loading} | ||||||
|                         show={<div>No projects available.</div>} |                         show={ | ||||||
|  |                             <ConditionallyRender | ||||||
|  |                                 condition={searchValue?.length > 0} | ||||||
|  |                                 show={ | ||||||
|  |                                     <TablePlaceholder> | ||||||
|  |                                         No projects found matching “ | ||||||
|  |                                         {searchValue} | ||||||
|  |                                         ” | ||||||
|  |                                     </TablePlaceholder> | ||||||
|  |                                 } | ||||||
|  |                                 elseShow={ | ||||||
|  |                                     <TablePlaceholder> | ||||||
|  |                                         No projects available. | ||||||
|  |                                     </TablePlaceholder> | ||||||
|  |                                 } | ||||||
|  |                             /> | ||||||
|  |                         } | ||||||
|                         elseShow={renderProjects()} |                         elseShow={renderProjects()} | ||||||
|                     /> |                     /> | ||||||
|                 </div> |                 </div> | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | import { PageContent } from 'component/common/PageContent/PageContent'; | ||||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||||
| import { | import { | ||||||
|     TableSearch, |  | ||||||
|     SortableTableHeader, |     SortableTableHeader, | ||||||
|     TableCell, |     TableCell, | ||||||
|     TablePlaceholder, |     TablePlaceholder, | ||||||
| @ -25,6 +24,7 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; | |||||||
| import theme from 'themes/theme'; | import theme from 'themes/theme'; | ||||||
| import { SegmentDocsWarning } from 'component/segments/SegmentDocs/SegmentDocs'; | import { SegmentDocsWarning } from 'component/segments/SegmentDocs/SegmentDocs'; | ||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
| 
 | 
 | ||||||
| export const SegmentTable = () => { | export const SegmentTable = () => { | ||||||
|     const { segments, loading } = useSegments(); |     const { segments, loading } = useSegments(); | ||||||
| @ -87,7 +87,7 @@ export const SegmentTable = () => { | |||||||
|                     title="Segments" |                     title="Segments" | ||||||
|                     actions={ |                     actions={ | ||||||
|                         <> |                         <> | ||||||
|                             <TableSearch |                             <Search | ||||||
|                                 initialValue={globalFilter} |                                 initialValue={globalFilter} | ||||||
|                                 onChange={setGlobalFilter} |                                 onChange={setGlobalFilter} | ||||||
|                             /> |                             /> | ||||||
|  | |||||||
| @ -19,7 +19,6 @@ import { | |||||||
|     TableCell, |     TableCell, | ||||||
|     TableRow, |     TableRow, | ||||||
|     TablePlaceholder, |     TablePlaceholder, | ||||||
|     TableSearch, |  | ||||||
| } from 'component/common/Table'; | } from 'component/common/Table'; | ||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | import { PageContent } from 'component/common/PageContent/PageContent'; | ||||||
| @ -38,6 +37,7 @@ import { sortTypes } from 'utils/sortTypes'; | |||||||
| import { useTable, useGlobalFilter, useSortBy } from 'react-table'; | import { useTable, useGlobalFilter, useSortBy } from 'react-table'; | ||||||
| import { AddStrategyButton } from './AddStrategyButton/AddStrategyButton'; | import { AddStrategyButton } from './AddStrategyButton/AddStrategyButton'; | ||||||
| import { StatusBadge } from 'component/common/StatusBadge/StatusBadge'; | import { StatusBadge } from 'component/common/StatusBadge/StatusBadge'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
| 
 | 
 | ||||||
| interface IDialogueMetaData { | interface IDialogueMetaData { | ||||||
|     show: boolean; |     show: boolean; | ||||||
| @ -357,7 +357,7 @@ export const StrategiesList = () => { | |||||||
|                     title="Strategies" |                     title="Strategies" | ||||||
|                     actions={ |                     actions={ | ||||||
|                         <> |                         <> | ||||||
|                             <TableSearch |                             <Search | ||||||
|                                 initialValue={globalFilter} |                                 initialValue={globalFilter} | ||||||
|                                 onChange={setGlobalFilter} |                                 onChange={setGlobalFilter} | ||||||
|                             /> |                             /> | ||||||
|  | |||||||
| @ -8,7 +8,6 @@ import { | |||||||
|     TableCell, |     TableCell, | ||||||
|     TableRow, |     TableRow, | ||||||
|     TablePlaceholder, |     TablePlaceholder, | ||||||
|     TableSearch, |  | ||||||
| } from 'component/common/Table'; | } from 'component/common/Table'; | ||||||
| import { Delete, Edit, Label } from '@mui/icons-material'; | import { Delete, Edit, Label } from '@mui/icons-material'; | ||||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||||
| @ -29,6 +28,7 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC | |||||||
| import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; | import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; | ||||||
| import { sortTypes } from 'utils/sortTypes'; | import { sortTypes } from 'utils/sortTypes'; | ||||||
| import { AddTagTypeButton } from './AddTagTypeButton/AddTagTypeButton'; | import { AddTagTypeButton } from './AddTagTypeButton/AddTagTypeButton'; | ||||||
|  | import { Search } from 'component/common/Search/Search'; | ||||||
| 
 | 
 | ||||||
| export const TagTypeList = () => { | export const TagTypeList = () => { | ||||||
|     const [deletion, setDeletion] = useState<{ |     const [deletion, setDeletion] = useState<{ | ||||||
| @ -192,7 +192,7 @@ export const TagTypeList = () => { | |||||||
|                     title="Tag types" |                     title="Tag types" | ||||||
|                     actions={ |                     actions={ | ||||||
|                         <> |                         <> | ||||||
|                             <TableSearch |                             <Search | ||||||
|                                 initialValue={globalFilter} |                                 initialValue={globalFilter} | ||||||
|                                 onChange={setGlobalFilter} |                                 onChange={setGlobalFilter} | ||||||
|                             /> |                             /> | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user