feat(core): update Table filter logic

This commit is contained in:
Rémi DOREAU
2020-09-14 23:26:32 +02:00
parent 10304d0303
commit b565c93537
3 changed files with 245 additions and 44 deletions
@@ -0,0 +1,131 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, useState } from 'react';
import { BackstageTheme } from '@backstage/theme';
import { Button, makeStyles } from '@material-ui/core';
import { Select } from '../Select';
import { CheckboxTree } from '../CheckboxTree';
import { CheckboxTreeProps } from '../CheckboxTree/CheckboxTree';
import { SelectProps } from '../Select/Select';
const useSubvalueCellStyles = makeStyles<BackstageTheme>(theme => ({
root: {
height: '100%',
width: '315px',
display: 'flex',
flexDirection: 'column',
marginRight: theme.spacing(3),
},
value: {},
header: {
display: 'flex',
alignItems: 'center',
height: '60px',
justifyContent: 'space-between',
borderBottom: `1px solid ${theme.palette.grey[500]}`,
},
filters: {
display: 'flex',
flexDirection: 'column',
'& > *': {
marginTop: theme.spacing(2),
},
},
clear: {
textTransform: 'none',
},
}));
type Without<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Filter = {
type: 'select' | 'checkbox-tree' | 'multiple-select';
element:
| Without<CheckboxTreeProps, 'onChange'>
| Without<SelectProps, 'onChange'>;
};
type Props = {
filters: Filter[];
onChangeFilters: (arg: any) => any;
};
export const Filters = (props: Props) => {
const classes = useSubvalueCellStyles();
const { onChangeFilters } = props;
const [filters, setFilters] = useState(props.filters);
const [selectedFilters, setSelectedFilters] = useState({});
// Trigger re-rendering
const handleClick = () => {
setSelectedFilters({});
setFilters([
...props.filters.map(el => ({
type: el.type,
element: { ...el.element },
})),
]);
};
useEffect(() => {
onChangeFilters(selectedFilters);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedFilters]);
// As material table doesn't provide a way to add a column filter tab we will make our own filter logic
return (
<div className={classes.root}>
<div className={classes.header}>
<div className={classes.value}>Filters</div>
<Button className={classes.clear} color="primary" onClick={handleClick}>
Clear all
</Button>
</div>
<div className={classes.filters}>
{filters?.length &&
filters.map(filter =>
filter.type === 'checkbox-tree' ? (
<CheckboxTree
key={filter.element.label}
{...(filter.element as CheckboxTreeProps)}
onChange={
el => ({})
// setSelectedFilters({
// ...selectedFilters,
// [filter.element.label]: el,
// })
}
/>
) : (
<Select
key={filter.element.label}
{...(filter.element as SelectProps)}
onChange={el =>
setSelectedFilters({
...selectedFilters,
[filter.element.label]: el,
})
}
/>
),
)}
</div>
</div>
);
};
@@ -16,6 +16,7 @@
import React from 'react';
import { Table, SubvalueCell, TableColumn } from './';
import { TableFilter } from './Table';
export default {
title: 'Table',
@@ -244,13 +245,28 @@ export const FilterTable = () => {
},
];
const filters: TableFilter[] = [
{
column: 'Column 1',
type: 'select',
},
{
column: 'Column 2',
type: 'multiple-select',
},
{
column: 'Numeric value',
type: 'checkbox-tree',
},
];
return (
<div style={containerStyle}>
<Table
options={{ paging: false, padding: 'dense' }}
data={testData10}
columns={columns}
filters={["test"]}
filters={filters}
/>
</div>
);
+97 -43
View File
@@ -45,7 +45,8 @@ import MTable, {
MTableToolbar,
Options,
} from 'material-table';
import React, { forwardRef, useState } from 'react';
import React, { forwardRef, useEffect, useState } from 'react';
import { Filters } from './Filters';
const tableIcons = {
Add: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
@@ -113,7 +114,7 @@ const useHeaderStyles = makeStyles<BackstageTheme>(theme => ({
},
}));
const useToolbarStyles = makeStyles<BackstageTheme>((theme) => ({
const useToolbarStyles = makeStyles<BackstageTheme>(theme => ({
root: {
padding: theme.spacing(3, 0, 2.5, 2.5),
},
@@ -138,12 +139,7 @@ const useFilterStyles = makeStyles<BackstageTheme>(() => ({
const useTableStyles = makeStyles<BackstageTheme>(() => ({
root: {
display: 'flex',
alignItems: 'center',
},
filters: {
height: '100%',
width: '250px',
}
}));
function convertColumns<T extends object>(
@@ -173,11 +169,16 @@ export interface TableColumn<T extends object = {}> extends Column<T> {
width?: string;
}
export type TableFilter = {
column: string;
type: 'select' | 'multiple-select' | 'checkbox-tree';
};
export interface TableProps<T extends object = {}>
extends MaterialTableProps<T> {
columns: TableColumn<T>[];
subtitle?: string;
filters?: any[];
filters?: TableFilter[];
}
export function Table<T extends object = {}>({
@@ -193,9 +194,12 @@ export function Table<T extends object = {}>({
const tableClasses = useTableStyles();
const filtersClasses = useFilterStyles();
const { data, ...propsWithoutData } = props;
const theme = useTheme<BackstageTheme>();
const [filtersOpen, toggleFilters] = useState(false);
const [tableData, setTableData] = useState(data as any[]);
const MTColumns = convertColumns(columns, theme);
@@ -205,44 +209,94 @@ export function Table<T extends object = {}>({
},
};
const getFieldByTitle = (titleValue: string | keyof T) =>
columns.find(el => el.title === titleValue)?.field;
const onChangeFilters = (selectedFilters: any) => {
if (Object.values(selectedFilters).length) {
const newData = (props.data as any[]).filter(
el =>
!!Object.entries(selectedFilters).find(
([key, value]) => el[getFieldByTitle(key)] === value,
),
);
setTableData(newData);
}
};
const constructFilters = (filterConfig: TableFilter[], dataValue: any[]) => {
const extractColumnData = (column: string | keyof T) =>
dataValue.map(el => ({ label: el[column], options: [] }));
return filterConfig.map(filter => ({
type: filter.type,
element:
filter.type === 'checkbox-tree'
? {
label: filter.column,
subCategories: extractColumnData(
getFieldByTitle(filter.column) || '',
),
}
: {
placeholder: 'All results',
label: filter.column,
multiple: filter.type === 'multiple-select',
items: dataValue.map(el => ({
label: el[getFieldByTitle(filter.column) || ''],
value: el[getFieldByTitle(filter.column) || ''],
})),
},
}));
};
return (
<div className={tableClasses.root}>
{filtersOpen && <div className={tableClasses.filters}>Filters</div>}
<MTable<T>
components={{
Header: headerProps => (
<MTableHeader classes={headerClasses} {...headerProps} />
),
Toolbar: toolbarProps =>
filters?.length ? (
<div className={filtersClasses.root}>
<div className={filtersClasses.root}>
<IconButton onClick={() => toggleFilters(el => !el)} aria-label="filter list">
<FilterList />
</IconButton>
<Typography variant="h6">Filters</Typography>
</div>
<MTableToolbar classes={toolbarClasses} {...toolbarProps} />
</div>
) : (
<MTableToolbar classes={toolbarClasses} {...toolbarProps} />
{filtersOpen && filters?.length && (
<Filters
filters={constructFilters(filters, props.data as any[])}
onChangeFilters={onChangeFilters}
/>
)}
<MTable<T>
components={{
Header: headerProps => (
<MTableHeader classes={headerClasses} {...headerProps} />
),
}}
options={{ ...defaultOptions, ...options }}
columns={MTColumns}
icons={tableIcons}
title={
<>
<Typography variant="h5">{title}</Typography>
{subtitle && (
<Typography color="textSecondary" variant="body1">
{subtitle}
</Typography>
)}
</>
}
{...props}
/>
Toolbar: toolbarProps =>
filters?.length ? (
<div className={filtersClasses.root}>
<div className={filtersClasses.root}>
<IconButton
onClick={() => toggleFilters(el => !el)}
aria-label="filter list"
>
<FilterList />
</IconButton>
<Typography variant="h6">Filters</Typography>
</div>
<MTableToolbar classes={toolbarClasses} {...toolbarProps} />
</div>
) : (
<MTableToolbar classes={toolbarClasses} {...toolbarProps} />
),
}}
options={{ ...defaultOptions, ...options }}
columns={MTColumns}
icons={tableIcons}
title={
<>
<Typography variant="h5">{title}</Typography>
{subtitle && (
<Typography color="textSecondary" variant="body1">
{subtitle}
</Typography>
)}
</>
}
data={tableData}
{...propsWithoutData}
/>
</div>
);
}