feat(core): update Table filter logic
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user