mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
Merge branch 'main' into archive_table
This commit is contained in:
commit
b1d9437d99
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "unleash-frontend",
|
||||
"description": "unleash your features",
|
||||
"version": "4.11.0-beta.2",
|
||||
"version": "4.13.0-beta.0",
|
||||
"keywords": [
|
||||
"unleash",
|
||||
"feature toggle",
|
||||
|
@ -1,41 +0,0 @@
|
||||
import { IGetSearchContextOutput } from 'hooks/useSearch';
|
||||
import { FC, useState } from 'react';
|
||||
import { useAsyncDebounce } from 'react-table';
|
||||
import { TableSearchField } from './TableSearchField/TableSearchField';
|
||||
|
||||
interface ITableSearchProps {
|
||||
initialValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
hasFilters?: boolean;
|
||||
getSearchContext?: () => IGetSearchContextOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `Search` instead.
|
||||
*/
|
||||
export const TableSearch: FC<ITableSearchProps> = ({
|
||||
initialValue,
|
||||
onChange = () => {},
|
||||
placeholder,
|
||||
hasFilters,
|
||||
getSearchContext,
|
||||
}) => {
|
||||
const [searchInputState, setSearchInputState] = useState(initialValue);
|
||||
const debouncedOnSearch = useAsyncDebounce(onChange, 200);
|
||||
|
||||
const onSearchChange = (value: string) => {
|
||||
debouncedOnSearch(value);
|
||||
setSearchInputState(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableSearchField
|
||||
value={searchInputState!}
|
||||
onChange={onSearchChange}
|
||||
placeholder={placeholder}
|
||||
hasFilters={hasFilters}
|
||||
getSearchContext={getSearchContext}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,46 +0,0 @@
|
||||
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%',
|
||||
},
|
||||
}));
|
@ -1,110 +0,0 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { IconButton, InputBase, Tooltip } from '@mui/material';
|
||||
import { Search, Close } from '@mui/icons-material';
|
||||
import classnames from 'classnames';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useStyles } from './TableSearchField.styles';
|
||||
import { TableSearchFieldSuggestions } from './TableSearchFieldSuggestions/TableSearchFieldSuggestions';
|
||||
import { IGetSearchContextOutput } from 'hooks/useSearch';
|
||||
import { useKeyboardShortcut } from 'hooks/useKeyboardShortcut';
|
||||
|
||||
interface ITableSearchFieldProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
hasFilters?: boolean;
|
||||
getSearchContext?: () => IGetSearchContextOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `Search` instead.
|
||||
*/
|
||||
export const TableSearchField = ({
|
||||
value = '',
|
||||
onChange,
|
||||
className,
|
||||
placeholder: customPlaceholder,
|
||||
hasFilters,
|
||||
getSearchContext,
|
||||
}: ITableSearchFieldProps) => {
|
||||
const ref = useRef<HTMLInputElement>();
|
||||
const { classes: styles } = useStyles();
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
|
||||
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'
|
||||
)}
|
||||
>
|
||||
<Search
|
||||
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 => onChange(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={
|
||||
<TableSearchFieldSuggestions
|
||||
getSearchContext={getSearchContext!}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,72 +0,0 @@
|
||||
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>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,62 +0,0 @@
|
||||
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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,150 +0,0 @@
|
||||
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 TableSearchFieldSuggestionsProps {
|
||||
getSearchContext: () => IGetSearchContextOutput;
|
||||
}
|
||||
|
||||
export const TableSearchFieldSuggestions: VFC<
|
||||
TableSearchFieldSuggestionsProps
|
||||
> = ({ 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>
|
||||
);
|
||||
};
|
@ -1,4 +1,3 @@
|
||||
export { TableSearch } from './TableSearch/TableSearch';
|
||||
export { SortableTableHeader } from './SortableTableHeader/SortableTableHeader';
|
||||
export { TableBody, TableRow } from '@mui/material';
|
||||
export { Table } from './Table/Table';
|
||||
|
Loading…
Reference in New Issue
Block a user