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 {
 | 
			
		||||
    TableSearch,
 | 
			
		||||
    SortableTableHeader,
 | 
			
		||||
    TableCell,
 | 
			
		||||
    TablePlaceholder,
 | 
			
		||||
@ -26,6 +25,7 @@ import { formatExpiredAt } from 'component/Reporting/ReportExpiredCell/formatExp
 | 
			
		||||
import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell';
 | 
			
		||||
import theme from 'themes/theme';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { Search } from 'component/common/Search/Search';
 | 
			
		||||
 | 
			
		||||
interface IReportTableProps {
 | 
			
		||||
    projectId: string;
 | 
			
		||||
@ -95,7 +95,7 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => {
 | 
			
		||||
        <PageHeader
 | 
			
		||||
            title="Overview"
 | 
			
		||||
            actions={
 | 
			
		||||
                <TableSearch
 | 
			
		||||
                <Search
 | 
			
		||||
                    initialValue={globalFilter}
 | 
			
		||||
                    onChange={setGlobalFilter}
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import { useTable, useGlobalFilter, useSortBy } from 'react-table';
 | 
			
		||||
import { PageContent } from 'component/common/PageContent/PageContent';
 | 
			
		||||
import {
 | 
			
		||||
    SortableTableHeader,
 | 
			
		||||
    TableSearch,
 | 
			
		||||
    TableCell,
 | 
			
		||||
    TablePlaceholder,
 | 
			
		||||
} 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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
 | 
			
		||||
import { Search } from 'component/common/Search/Search';
 | 
			
		||||
 | 
			
		||||
export const ApiTokenTable = () => {
 | 
			
		||||
    const { tokens, loading } = useApiTokens();
 | 
			
		||||
@ -57,7 +57,7 @@ export const ApiTokenTable = () => {
 | 
			
		||||
    }, [setHiddenColumns, hiddenColumns]);
 | 
			
		||||
 | 
			
		||||
    const headerSearch = (
 | 
			
		||||
        <TableSearch initialValue={globalFilter} onChange={setGlobalFilter} />
 | 
			
		||||
        <Search initialValue={globalFilter} onChange={setGlobalFilter} />
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const headerActions = (
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,6 @@ import {
 | 
			
		||||
    TableCell,
 | 
			
		||||
    TableRow,
 | 
			
		||||
    TablePlaceholder,
 | 
			
		||||
    TableSearch,
 | 
			
		||||
} from 'component/common/Table';
 | 
			
		||||
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
 | 
			
		||||
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 theme from 'themes/theme';
 | 
			
		||||
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
 | 
			
		||||
import { Search } from 'component/common/Search/Search';
 | 
			
		||||
 | 
			
		||||
const ROOTROLE = 'root';
 | 
			
		||||
const BUILTIN_ROLE_TYPE = 'project';
 | 
			
		||||
@ -190,7 +190,7 @@ const ProjectRoleList = () => {
 | 
			
		||||
                    title="Project roles"
 | 
			
		||||
                    actions={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <TableSearch
 | 
			
		||||
                            <Search
 | 
			
		||||
                                initialValue={globalFilter}
 | 
			
		||||
                                onChange={setGlobalFilter}
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,6 @@ import {
 | 
			
		||||
    TableCell,
 | 
			
		||||
    TableRow,
 | 
			
		||||
    TablePlaceholder,
 | 
			
		||||
    TableSearch,
 | 
			
		||||
} from 'component/common/Table';
 | 
			
		||||
import ChangePassword from './ChangePassword/ChangePassword';
 | 
			
		||||
import DeleteUser from './DeleteUser/DeleteUser';
 | 
			
		||||
@ -34,6 +33,7 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
 | 
			
		||||
import theme from 'themes/theme';
 | 
			
		||||
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
 | 
			
		||||
import { UsersActionsCell } from './UsersActionsCell/UsersActionsCell';
 | 
			
		||||
import { Search } from 'component/common/Search/Search';
 | 
			
		||||
 | 
			
		||||
const StyledAvatar = styled(Avatar)(({ theme }) => ({
 | 
			
		||||
    width: theme.spacing(4),
 | 
			
		||||
@ -248,7 +248,7 @@ const UsersList = () => {
 | 
			
		||||
                    title="Users"
 | 
			
		||||
                    actions={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <TableSearch
 | 
			
		||||
                            <Search
 | 
			
		||||
                                initialValue={globalFilter}
 | 
			
		||||
                                onChange={setGlobalFilter}
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,40 @@
 | 
			
		||||
import { useMemo, useState } from 'react';
 | 
			
		||||
import { useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import { CircularProgress } from '@mui/material';
 | 
			
		||||
import { Warning } from '@mui/icons-material';
 | 
			
		||||
import { AppsLinkList, styles as themeStyles } from 'component/common';
 | 
			
		||||
import { SearchField } from 'component/common/SearchField/SearchField';
 | 
			
		||||
import { PageContent } from 'component/common/PageContent/PageContent';
 | 
			
		||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
 | 
			
		||||
import useApplications from 'hooks/api/getters/useApplications/useApplications';
 | 
			
		||||
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 = () => {
 | 
			
		||||
    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 regExp = new RegExp(filter, 'i');
 | 
			
		||||
        return filter
 | 
			
		||||
        const regExp = new RegExp(searchValue, 'i');
 | 
			
		||||
        return searchValue
 | 
			
		||||
            ? applications?.filter(a => regExp.test(a.appName))
 | 
			
		||||
            : applications;
 | 
			
		||||
    }, [applications, filter]);
 | 
			
		||||
    }, [applications, searchValue]);
 | 
			
		||||
 | 
			
		||||
    const renderNoApplications = () => (
 | 
			
		||||
        <>
 | 
			
		||||
@ -44,10 +61,19 @@ export const ApplicationList = () => {
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <div className={themeStyles.searchField}>
 | 
			
		||||
                <SearchField initialValue={filter} updateValue={setFilter} />
 | 
			
		||||
            </div>
 | 
			
		||||
            <PageContent header={<PageHeader title="Applications" />}>
 | 
			
		||||
            <PageContent
 | 
			
		||||
                header={
 | 
			
		||||
                    <PageHeader
 | 
			
		||||
                        title="Applications"
 | 
			
		||||
                        actions={
 | 
			
		||||
                            <Search
 | 
			
		||||
                                initialValue={searchValue}
 | 
			
		||||
                                onChange={setSearchValue}
 | 
			
		||||
                            />
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                }
 | 
			
		||||
            >
 | 
			
		||||
                <div className={themeStyles.fullwidth}>
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @deprecated use `Search` instead.
 | 
			
		||||
 */
 | 
			
		||||
export const SearchField: VFC<ISearchFieldProps> = ({
 | 
			
		||||
    updateValue,
 | 
			
		||||
    initialValue = '',
 | 
			
		||||
 | 
			
		||||
@ -9,5 +9,6 @@ export const useStyles = makeStyles()(theme => ({
 | 
			
		||||
        justifyContent: 'center',
 | 
			
		||||
        alignItems: 'center',
 | 
			
		||||
        marginTop: theme.spacing(2),
 | 
			
		||||
        width: '100%',
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,9 @@ interface ITableSearchProps {
 | 
			
		||||
    getSearchContext?: () => IGetSearchContextOutput;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @deprecated use `Search` instead.
 | 
			
		||||
 */
 | 
			
		||||
export const TableSearch: FC<ITableSearchProps> = ({
 | 
			
		||||
    initialValue,
 | 
			
		||||
    onChange = () => {},
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,9 @@ interface ITableSearchFieldProps {
 | 
			
		||||
    getSearchContext?: () => IGetSearchContextOutput;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @deprecated use `Search` instead.
 | 
			
		||||
 */
 | 
			
		||||
export const TableSearchField = ({
 | 
			
		||||
    value = '',
 | 
			
		||||
    onChange,
 | 
			
		||||
@ -28,16 +31,20 @@ export const TableSearchField = ({
 | 
			
		||||
    const ref = useRef<HTMLInputElement>();
 | 
			
		||||
    const { classes: styles } = useStyles();
 | 
			
		||||
    const [showSuggestions, setShowSuggestions] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const hotkey = useKeyboardShortcut(
 | 
			
		||||
        { modifiers: ['ctrl'], key: 'k', preventDefault: true },
 | 
			
		||||
        () => {
 | 
			
		||||
            ref.current?.focus();
 | 
			
		||||
            setShowSuggestions(true);
 | 
			
		||||
            if (document.activeElement === ref.current) {
 | 
			
		||||
                ref.current?.blur();
 | 
			
		||||
            } else {
 | 
			
		||||
                ref.current?.focus();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
    useKeyboardShortcut({ key: 'Escape' }, () => {
 | 
			
		||||
        if (document.activeElement === ref.current) {
 | 
			
		||||
            setShowSuggestions(suggestions => !suggestions);
 | 
			
		||||
            ref.current?.blur();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`;
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,6 @@ import {
 | 
			
		||||
    TableCell,
 | 
			
		||||
    TableRow,
 | 
			
		||||
    TablePlaceholder,
 | 
			
		||||
    TableSearch,
 | 
			
		||||
} from 'component/common/Table';
 | 
			
		||||
import { PageContent } from 'component/common/PageContent/PageContent';
 | 
			
		||||
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 { Adjust } from '@mui/icons-material';
 | 
			
		||||
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
 | 
			
		||||
import { Search } from 'component/common/Search/Search';
 | 
			
		||||
 | 
			
		||||
const ContextList: VFC = () => {
 | 
			
		||||
    const [showDelDialogue, setShowDelDialogue] = useState(false);
 | 
			
		||||
@ -164,7 +164,7 @@ const ContextList: VFC = () => {
 | 
			
		||||
                    title="Context fields"
 | 
			
		||||
                    actions={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <TableSearch
 | 
			
		||||
                            <Search
 | 
			
		||||
                                initialValue={globalFilter}
 | 
			
		||||
                                onChange={setGlobalFilter}
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@ import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironmen
 | 
			
		||||
import { CreateEnvironmentButton } from 'component/environments/CreateEnvironmentButton/CreateEnvironmentButton';
 | 
			
		||||
import { useTable, useGlobalFilter } from 'react-table';
 | 
			
		||||
import {
 | 
			
		||||
    TableSearch,
 | 
			
		||||
    SortableTableHeader,
 | 
			
		||||
    Table,
 | 
			
		||||
    TablePlaceholder,
 | 
			
		||||
@ -24,6 +23,7 @@ import useEnvironmentApi, {
 | 
			
		||||
} from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
 | 
			
		||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { Search } from 'component/common/Search/Search';
 | 
			
		||||
 | 
			
		||||
const StyledAlert = styled(Alert)(({ theme }) => ({
 | 
			
		||||
    marginBottom: theme.spacing(4),
 | 
			
		||||
@ -71,7 +71,7 @@ export const EnvironmentTable = () => {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const headerSearch = (
 | 
			
		||||
        <TableSearch initialValue={globalFilter} onChange={setGlobalFilter} />
 | 
			
		||||
        <Search initialValue={globalFilter} onChange={setGlobalFilter} />
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const headerActions = (
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,6 @@ import {
 | 
			
		||||
    TableCell,
 | 
			
		||||
    TableRow,
 | 
			
		||||
    TablePlaceholder,
 | 
			
		||||
    TableSearch,
 | 
			
		||||
} from 'component/common/Table';
 | 
			
		||||
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
 | 
			
		||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
 | 
			
		||||
@ -29,6 +28,7 @@ import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton'
 | 
			
		||||
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
 | 
			
		||||
import { useStyles } from './styles';
 | 
			
		||||
import { useSearch } from 'hooks/useSearch';
 | 
			
		||||
import { Search } from 'component/common/Search/Search';
 | 
			
		||||
 | 
			
		||||
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
 | 
			
		||||
    name: 'Name of the feature',
 | 
			
		||||
@ -210,7 +210,7 @@ export const FeatureToggleListTable: VFC = () => {
 | 
			
		||||
                                condition={!isSmallScreen}
 | 
			
		||||
                                show={
 | 
			
		||||
                                    <>
 | 
			
		||||
                                        <TableSearch
 | 
			
		||||
                                        <Search
 | 
			
		||||
                                            initialValue={searchValue}
 | 
			
		||||
                                            onChange={setSearchValue}
 | 
			
		||||
                                            hasFilters
 | 
			
		||||
@ -238,7 +238,7 @@ export const FeatureToggleListTable: VFC = () => {
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        condition={isSmallScreen}
 | 
			
		||||
                        show={
 | 
			
		||||
                            <TableSearch
 | 
			
		||||
                            <Search
 | 
			
		||||
                                initialValue={searchValue}
 | 
			
		||||
                                onChange={setSearchValue}
 | 
			
		||||
                                hasFilters
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,6 @@ import {
 | 
			
		||||
    TableCell,
 | 
			
		||||
    TableRow,
 | 
			
		||||
    TablePlaceholder,
 | 
			
		||||
    TableSearch,
 | 
			
		||||
} from 'component/common/Table';
 | 
			
		||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
 | 
			
		||||
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 { useSearch } from 'hooks/useSearch';
 | 
			
		||||
import { useMediaQuery } from '@mui/material';
 | 
			
		||||
import { Search } from 'component/common/Search/Search';
 | 
			
		||||
 | 
			
		||||
interface IProjectFeatureTogglesProps {
 | 
			
		||||
    features: IProject['features'];
 | 
			
		||||
@ -412,11 +412,9 @@ export const ProjectFeatureToggles = ({
 | 
			
		||||
                            <ConditionallyRender
 | 
			
		||||
                                condition={!isSmallScreen}
 | 
			
		||||
                                show={
 | 
			
		||||
                                    <TableSearch
 | 
			
		||||
                                    <Search
 | 
			
		||||
                                        initialValue={searchValue}
 | 
			
		||||
                                        onChange={value =>
 | 
			
		||||
                                            setSearchValue(value)
 | 
			
		||||
                                        }
 | 
			
		||||
                                        onChange={setSearchValue}
 | 
			
		||||
                                        hasFilters
 | 
			
		||||
                                        getSearchContext={getSearchContext}
 | 
			
		||||
                                    />
 | 
			
		||||
@ -454,7 +452,7 @@ export const ProjectFeatureToggles = ({
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        condition={isSmallScreen}
 | 
			
		||||
                        show={
 | 
			
		||||
                            <TableSearch
 | 
			
		||||
                            <Search
 | 
			
		||||
                                initialValue={searchValue}
 | 
			
		||||
                                onChange={setSearchValue}
 | 
			
		||||
                                hasFilters
 | 
			
		||||
 | 
			
		||||
@ -21,20 +21,4 @@ export const useStyles = makeStyles()(theme => ({
 | 
			
		||||
        fontFamily: theme.typography.fontFamily,
 | 
			
		||||
        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 { Link, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { useContext, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
 | 
			
		||||
import { mutate } from 'swr';
 | 
			
		||||
import { getProjectFetcher } from 'hooks/api/getters/useProject/getProjectFetcher';
 | 
			
		||||
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 ApiError from 'component/common/ApiError/ApiError';
 | 
			
		||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { SearchField } from 'component/common/SearchField/SearchField';
 | 
			
		||||
import classnames from 'classnames';
 | 
			
		||||
import { TablePlaceholder } from 'component/common/Table';
 | 
			
		||||
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 = {
 | 
			
		||||
    [index: string]: boolean;
 | 
			
		||||
@ -51,14 +55,30 @@ export const ProjectListNew = () => {
 | 
			
		||||
    const [fetchedProjects, setFetchedProjects] = useState<projectMap>({});
 | 
			
		||||
    const ref = useLoading(loading);
 | 
			
		||||
    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 regExp = new RegExp(filter, 'i');
 | 
			
		||||
        return filter
 | 
			
		||||
        const regExp = new RegExp(searchValue, 'i');
 | 
			
		||||
        return searchValue
 | 
			
		||||
            ? projects.filter(project => regExp.test(project.name))
 | 
			
		||||
            : projects;
 | 
			
		||||
    }, [projects, filter]);
 | 
			
		||||
    }, [projects, searchValue]);
 | 
			
		||||
 | 
			
		||||
    const handleHover = (projectId: string) => {
 | 
			
		||||
        if (fetchedProjects[projectId]) {
 | 
			
		||||
@ -129,39 +149,69 @@ export const ProjectListNew = () => {
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div ref={ref}>
 | 
			
		||||
            <div className={styles.searchBarContainer}>
 | 
			
		||||
                <SearchField
 | 
			
		||||
                    initialValue={filter}
 | 
			
		||||
                    updateValue={setFilter}
 | 
			
		||||
                    showValueChip
 | 
			
		||||
                    className={classnames(styles.searchBar, {
 | 
			
		||||
                        skeleton: loading,
 | 
			
		||||
                    })}
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
            <PageContent
 | 
			
		||||
                header={
 | 
			
		||||
                    <PageHeader
 | 
			
		||||
                        title="Projects"
 | 
			
		||||
                        actions={
 | 
			
		||||
                            <ResponsiveButton
 | 
			
		||||
                                Icon={Add}
 | 
			
		||||
                                onClick={() => navigate('/projects/create')}
 | 
			
		||||
                                maxWidth="700px"
 | 
			
		||||
                                permission={CREATE_PROJECT}
 | 
			
		||||
                                disabled={createButtonData.disabled}
 | 
			
		||||
                            >
 | 
			
		||||
                                New project
 | 
			
		||||
                            </ResponsiveButton>
 | 
			
		||||
                            <>
 | 
			
		||||
                                <ConditionallyRender
 | 
			
		||||
                                    condition={!isSmallScreen}
 | 
			
		||||
                                    show={
 | 
			
		||||
                                        <>
 | 
			
		||||
                                            <Search
 | 
			
		||||
                                                initialValue={searchValue}
 | 
			
		||||
                                                onChange={setSearchValue}
 | 
			
		||||
                                            />
 | 
			
		||||
                                            <PageHeader.Divider />
 | 
			
		||||
                                        </>
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
                                <ResponsiveButton
 | 
			
		||||
                                    Icon={Add}
 | 
			
		||||
                                    onClick={() => navigate('/projects/create')}
 | 
			
		||||
                                    maxWidth="700px"
 | 
			
		||||
                                    permission={CREATE_PROJECT}
 | 
			
		||||
                                    disabled={createButtonData.disabled}
 | 
			
		||||
                                >
 | 
			
		||||
                                    New project
 | 
			
		||||
                                </ResponsiveButton>
 | 
			
		||||
                            </>
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                    >
 | 
			
		||||
                        <ConditionallyRender
 | 
			
		||||
                            condition={isSmallScreen}
 | 
			
		||||
                            show={
 | 
			
		||||
                                <Search
 | 
			
		||||
                                    initialValue={searchValue}
 | 
			
		||||
                                    onChange={setSearchValue}
 | 
			
		||||
                                />
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                    </PageHeader>
 | 
			
		||||
                }
 | 
			
		||||
            >
 | 
			
		||||
                <ConditionallyRender condition={error} show={renderError()} />
 | 
			
		||||
                <div className={styles.container}>
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        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()}
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { PageContent } from 'component/common/PageContent/PageContent';
 | 
			
		||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
 | 
			
		||||
import {
 | 
			
		||||
    TableSearch,
 | 
			
		||||
    SortableTableHeader,
 | 
			
		||||
    TableCell,
 | 
			
		||||
    TablePlaceholder,
 | 
			
		||||
@ -25,6 +24,7 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
 | 
			
		||||
import theme from 'themes/theme';
 | 
			
		||||
import { SegmentDocsWarning } from 'component/segments/SegmentDocs/SegmentDocs';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { Search } from 'component/common/Search/Search';
 | 
			
		||||
 | 
			
		||||
export const SegmentTable = () => {
 | 
			
		||||
    const { segments, loading } = useSegments();
 | 
			
		||||
@ -87,7 +87,7 @@ export const SegmentTable = () => {
 | 
			
		||||
                    title="Segments"
 | 
			
		||||
                    actions={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <TableSearch
 | 
			
		||||
                            <Search
 | 
			
		||||
                                initialValue={globalFilter}
 | 
			
		||||
                                onChange={setGlobalFilter}
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,6 @@ import {
 | 
			
		||||
    TableCell,
 | 
			
		||||
    TableRow,
 | 
			
		||||
    TablePlaceholder,
 | 
			
		||||
    TableSearch,
 | 
			
		||||
} from 'component/common/Table';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { PageContent } from 'component/common/PageContent/PageContent';
 | 
			
		||||
@ -38,6 +37,7 @@ import { sortTypes } from 'utils/sortTypes';
 | 
			
		||||
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
 | 
			
		||||
import { AddStrategyButton } from './AddStrategyButton/AddStrategyButton';
 | 
			
		||||
import { StatusBadge } from 'component/common/StatusBadge/StatusBadge';
 | 
			
		||||
import { Search } from 'component/common/Search/Search';
 | 
			
		||||
 | 
			
		||||
interface IDialogueMetaData {
 | 
			
		||||
    show: boolean;
 | 
			
		||||
@ -357,7 +357,7 @@ export const StrategiesList = () => {
 | 
			
		||||
                    title="Strategies"
 | 
			
		||||
                    actions={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <TableSearch
 | 
			
		||||
                            <Search
 | 
			
		||||
                                initialValue={globalFilter}
 | 
			
		||||
                                onChange={setGlobalFilter}
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,6 @@ import {
 | 
			
		||||
    TableCell,
 | 
			
		||||
    TableRow,
 | 
			
		||||
    TablePlaceholder,
 | 
			
		||||
    TableSearch,
 | 
			
		||||
} from 'component/common/Table';
 | 
			
		||||
import { Delete, Edit, Label } from '@mui/icons-material';
 | 
			
		||||
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 { sortTypes } from 'utils/sortTypes';
 | 
			
		||||
import { AddTagTypeButton } from './AddTagTypeButton/AddTagTypeButton';
 | 
			
		||||
import { Search } from 'component/common/Search/Search';
 | 
			
		||||
 | 
			
		||||
export const TagTypeList = () => {
 | 
			
		||||
    const [deletion, setDeletion] = useState<{
 | 
			
		||||
@ -192,7 +192,7 @@ export const TagTypeList = () => {
 | 
			
		||||
                    title="Tag types"
 | 
			
		||||
                    actions={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <TableSearch
 | 
			
		||||
                            <Search
 | 
			
		||||
                                initialValue={globalFilter}
 | 
			
		||||
                                onChange={setGlobalFilter}
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user