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",
|
"name": "unleash-frontend",
|
||||||
"description": "unleash your features",
|
"description": "unleash your features",
|
||||||
"version": "4.11.0-beta.2",
|
"version": "4.13.0-beta.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"unleash",
|
"unleash",
|
||||||
"feature toggle",
|
"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 { SortableTableHeader } from './SortableTableHeader/SortableTableHeader';
|
||||||
export { TableBody, TableRow } from '@mui/material';
|
export { TableBody, TableRow } from '@mui/material';
|
||||||
export { Table } from './Table/Table';
|
export { Table } from './Table/Table';
|
||||||
|
Loading…
Reference in New Issue
Block a user