Merge pull request #7194 from ainhoaL/remove-checkboxtree

core-components: Remove CheckboxTree component (used in Table Filters)
This commit is contained in:
Patrik Oldsberg
2021-09-28 10:56:07 +02:00
committed by GitHub
9 changed files with 27 additions and 624 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/core-components': minor
---
Checkbox tree filters are no longer available in the Table component:
- Deleted the `CheckboxTree` component
- Removed the filter type `'checkbox-tree'` from the `TableFilter` types.
+1 -1
View File
@@ -1913,7 +1913,7 @@ export interface TableColumn<T extends object = {}> extends Column<T> {
// @public (undocumented)
export type TableFilter = {
column: string;
type: 'select' | 'multiple-select' | /** @deprecated */ 'checkbox-tree';
type: 'select' | 'multiple-select';
};
// Warning: (ae-missing-release-tag) "TableProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -1,105 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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, { useState } from 'react';
import { CheckboxTree } from './CheckboxTree';
const CHECKBOX_TREE_ITEMS = [
{
label: 'Generic subcategory name 1',
options: [
{
label: 'Option 1',
value: 1,
},
{
label: 'Option 2',
value: 2,
},
],
},
{
label: 'Generic subcategory name 2',
options: [
{
label: 'Option 1',
value: 1,
},
{
label: 'Option 2',
value: 2,
},
],
},
{
label: 'Generic subcategory name 3',
options: [
{
label: 'Option 1',
value: 1,
},
{
label: 'Option 2',
value: 2,
},
],
},
];
export default {
title: 'Inputs/CheckboxTree',
component: CheckboxTree,
};
export const Default = () => (
<CheckboxTree
onChange={() => {}}
label="default"
subCategories={CHECKBOX_TREE_ITEMS}
/>
);
export const DynamicTree = () => {
function generateTree(showMore: boolean = false) {
const t = [
{
label: 'Show more',
options: [],
},
];
if (showMore) {
t.push({
label: 'More',
options: [],
});
}
return t;
}
const [tree, setTree] = useState(generateTree());
return (
<CheckboxTree
onChange={state => {
setTree(generateTree(state.some(c => c.category === 'Show more')));
}}
label="default"
subCategories={tree}
/>
);
};
@@ -1,59 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { CheckboxTree } from './CheckboxTree';
const CHECKBOX_TREE_ITEMS = [
{
label: 'Generic subcategory name 1',
options: [
{
label: 'Option 1',
value: 1,
},
{
label: 'Option 2',
value: 2,
},
],
},
];
const minProps = {
onChange: jest.fn(),
label: 'Default',
subCategories: CHECKBOX_TREE_ITEMS,
};
describe('<CheckboxTree />', () => {
it('renders without exploding', async () => {
const { getByText, getByTestId } = render(<CheckboxTree {...minProps} />);
expect(getByText('Generic subcategory name 1')).toBeInTheDocument();
const checkbox = await getByTestId('expandable');
// Simulate click on expandable arrow
fireEvent.click(checkbox);
// Simulate click on option
const option = getByText('Option 1');
expect(getByText('Option 1')).toBeInTheDocument();
fireEvent.click(option);
expect(minProps.onChange).toHaveBeenCalled();
});
});
@@ -1,357 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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.
*/
/* eslint-disable guard-for-in */
import {
Checkbox,
Collapse,
List,
ListItem,
ListItemIcon,
ListItemText,
Typography,
} from '@material-ui/core';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import ExpandLess from '@material-ui/icons/ExpandLess';
import ExpandMore from '@material-ui/icons/ExpandMore';
import produce from 'immer';
import { isEqual } from 'lodash';
import React, { useEffect, useReducer } from 'react';
import { usePrevious } from 'react-use';
type IndexedObject<T> = {
[key: string]: T;
};
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
minWidth: 10,
maxWidth: 360,
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'transparent',
},
'&:active': {
animation: 'none',
transform: 'none',
},
},
nested: {
paddingLeft: theme.spacing(5),
height: '32px',
'&:hover': {
backgroundColor: 'transparent',
},
},
listItemIcon: {
minWidth: 10,
},
listItem: {
'&:hover': {
backgroundColor: 'transparent',
},
},
text: {
'& span, & svg': {
fontWeight: 'normal',
fontSize: 14,
},
},
}),
);
/* SUB_CATEGORY */
type SubCategory = {
label: string;
isChecked?: boolean;
isOpen?: boolean;
options?: Option[];
};
type SubCategoryWithIndexedOptions = {
label: string;
isChecked?: boolean;
isOpen?: boolean;
options: IndexedObject<Option>;
};
/* OPTION */
type Option = {
label: string;
value: string | number;
isChecked?: boolean;
};
type Selection = { category?: string; selectedChildren?: string[] }[];
export type CheckboxTreeProps = {
subCategories: SubCategory[];
label: string;
triggerReset?: boolean;
selected?: Selection;
onChange: (arg: Selection) => any;
};
/* REDUCER */
type checkOptionPayload = {
subCategoryLabel: string;
optionLabel: string;
};
type Action =
| { type: 'checkOption'; payload: checkOptionPayload }
| { type: 'checkCategory'; payload: string }
| { type: 'toggleCategory'; payload: string }
| {
type: 'updateCategories';
payload: IndexedObject<SubCategoryWithIndexedOptions>;
}
| { type: 'updateSelected'; payload: Selection }
| { type: 'triggerReset' };
const reducer = (
state: IndexedObject<SubCategoryWithIndexedOptions>,
action: Action,
) => {
switch (action.type) {
case 'checkOption': {
return produce(state, newState => {
const category = newState[action.payload.subCategoryLabel];
const option = category.options[action.payload.optionLabel];
option.isChecked = !option.isChecked;
category.isChecked = Object.values(category.options).every(
o => o.isChecked,
);
});
}
case 'checkCategory': {
return produce(state, newState => {
const category = newState[action.payload];
const options = category.options;
category.isChecked = !category.isChecked;
for (const option in options) {
options[option].isChecked = category.isChecked;
}
});
}
case 'toggleCategory':
return produce(state, newState => {
const category = newState[action.payload];
category.isOpen = !category.isOpen;
});
case 'triggerReset': {
return produce(state, newState => {
for (const category in newState) {
newState[category].isChecked = false;
for (const option in newState[category].options) {
newState[category].options[option].isChecked =
newState[category].isChecked;
}
}
});
}
case 'updateCategories': {
return produce(state, newState => {
for (const category in newState) {
delete newState[category];
}
for (const category in action.payload) {
newState[category] = action.payload[category];
if (state[category]) {
newState[category].isChecked = state[category].isChecked;
newState[category].isOpen = state[category].isOpen;
}
}
});
}
case 'updateSelected': {
return produce(state, newState => {
for (const category in newState) {
const selection = action.payload.find(s => s.category === category);
if (selection) {
newState[category].isChecked = true;
for (const option in newState[category].options) {
newState[category].options[option].isChecked =
selection.selectedChildren?.includes(option) || false;
}
}
}
});
}
default:
return state;
}
};
const indexer = (
arr: SubCategory[],
): IndexedObject<SubCategoryWithIndexedOptions> =>
arr.reduce((accumulator, el) => {
if (el.options) {
return {
...accumulator,
[el.label]: {
label: el.label,
isChecked: el.isChecked || false,
isOpen: false,
options: indexer(el.options),
},
};
}
return {
...accumulator,
[el.label]: { ...el, isChecked: el.isChecked || false },
};
}, {});
/**
*
* @deprecated CheckboxTree is no longer used in Table filters
*/
export function CheckboxTree(props: CheckboxTreeProps) {
const { subCategories, label, selected, onChange, triggerReset } = props;
const classes = useStyles();
const [state, dispatch] = useReducer(reducer, indexer(subCategories));
const handleOpen = (event: any, value: any) => {
event.stopPropagation();
dispatch({ type: 'toggleCategory', payload: value });
};
const previousSubCategories = usePrevious(subCategories);
useEffect(() => {
const values = Object.values(state).map(category => ({
category: category.isChecked ? category.label : undefined,
selectedChildren: Object.values(category.options)
.filter(option => option.isChecked)
.map(option => option.label),
}));
onChange(values);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state]);
useEffect(() => {
dispatch({ type: 'triggerReset' });
}, [triggerReset]);
useEffect(() => {
if (selected) {
dispatch({ type: 'updateSelected', payload: selected });
}
}, [selected]);
useEffect(() => {
if (!isEqual(subCategories, previousSubCategories)) {
dispatch({
type: 'updateCategories',
payload: indexer(subCategories),
});
}
}, [subCategories, previousSubCategories]);
return (
<div>
<Typography variant="button">{label}</Typography>
<List className={classes.root}>
{Object.values(state).map(item => (
<div key={item.label}>
<ListItem
className={classes.listItem}
dense
button
onClick={() =>
dispatch({
type: 'checkCategory',
payload: item.label,
})
}
>
<ListItemIcon className={classes.listItemIcon}>
<Checkbox
color="primary"
edge="start"
checked={item.isChecked}
tabIndex={-1}
disableRipple
/>
</ListItemIcon>
<ListItemText className={classes.text} primary={item.label} />
{Object.values(item.options).length ? (
<>
{item.isOpen ? (
<ExpandLess
data-testid="expandable"
onClick={event => handleOpen(event, item.label)}
/>
) : (
<ExpandMore
data-testid="expandable"
onClick={event => handleOpen(event, item.label)}
/>
)}
</>
) : null}
</ListItem>
<Collapse in={item.isOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{Object.values(item.options).map(option => (
<ListItem
button
key={option.label}
className={classes.nested}
onClick={() =>
dispatch({
type: 'checkOption',
payload: {
subCategoryLabel: item.label,
optionLabel: option.label,
},
})
}
>
<ListItemIcon className={classes.listItemIcon}>
<Checkbox
color="primary"
edge="start"
checked={option.isChecked}
tabIndex={-1}
disableRipple
/>
</ListItemIcon>
<ListItemText
className={classes.text}
primary={option.label}
/>
</ListItem>
))}
</List>
</Collapse>
</div>
))}
</List>
</div>
);
}
@@ -1,17 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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.
*/
export { CheckboxTree } from './CheckboxTree';
@@ -18,8 +18,6 @@ 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 => ({
@@ -53,10 +51,8 @@ const useSubvalueCellStyles = makeStyles<BackstageTheme>(theme => ({
export type Without<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Filter = {
type: 'select' | /** @deprecated */ 'checkbox-tree' | 'multiple-select';
element:
| Without<CheckboxTreeProps, 'onChange'>
| Without<SelectProps, 'onChange'>;
type: 'select' | 'multiple-select';
element: Without<SelectProps, 'onChange'>;
};
export type SelectedFilters = {
@@ -100,57 +96,20 @@ export const Filters = (props: Props) => {
</div>
<div className={classes.filters}>
{props.filters?.length &&
props.filters.map(filter =>
filter.type === 'checkbox-tree' ? (
<CheckboxTree
triggerReset={reset}
key={filter.element.label}
{...(filter.element as CheckboxTreeProps)}
selected={
selectedFilters[filter.element.label]
? (selectedFilters[filter.element.label] as string[]).map(
s => ({
category: s,
}),
)
: undefined
}
onChange={el =>
setSelectedFilters({
...selectedFilters,
[filter.element.label]: el
.filter(
(checkboxFilter: any) =>
checkboxFilter.category ||
checkboxFilter.selectedChildren.length,
)
.map((checkboxFilter: any) =>
checkboxFilter.category
? [
...checkboxFilter.selectedChildren,
checkboxFilter.category,
]
: checkboxFilter.selectedChildren,
)
.flat(),
})
}
/>
) : (
<Select
triggerReset={reset}
key={filter.element.label}
{...(filter.element as SelectProps)}
selected={selectedFilters[filter.element.label]}
onChange={el =>
setSelectedFilters({
...selectedFilters,
[filter.element.label]: el as any,
})
}
/>
),
)}
props.filters.map(filter => (
<Select
triggerReset={reset}
key={filter.element.label}
{...(filter.element as SelectProps)}
selected={selectedFilters[filter.element.label]}
onChange={el =>
setSelectedFilters({
...selectedFilters,
[filter.element.label]: el as any,
})
}
/>
))}
</div>
</div>
);
@@ -313,10 +313,6 @@ export const FilterTable = () => {
column: 'Column 2',
type: 'multiple-select',
},
{
column: 'Numeric value',
type: 'checkbox-tree',
},
];
return (
@@ -56,7 +56,6 @@ import React, {
useEffect,
useState,
} from 'react';
import { CheckboxTreeProps } from '../CheckboxTree/CheckboxTree';
import { SelectProps } from '../Select/Select';
import { Filter, Filters, SelectedFilters, Without } from './Filters';
@@ -191,7 +190,7 @@ export interface TableColumn<T extends object = {}> extends Column<T> {
export type TableFilter = {
column: string;
type: 'select' | 'multiple-select' | /** @deprecated */ 'checkbox-tree';
type: 'select' | 'multiple-select';
};
export type TableState = {
@@ -365,14 +364,6 @@ export function Table<T extends object = {}>(props: TableProps<T>) {
setSelectedFiltersLength(selectedFiltersArray.flat().length);
}, [data, selectedFilters, getFieldByTitle]);
// Check for deprecated checkbox-tree filter
useEffect(() => {
if (filters?.some(filter => filter.type === 'checkbox-tree')) {
// eslint-disable-next-line no-console
console.warn('"checkbox-tree" filter type is deprecated');
}
}, [filters]);
const constructFilters = (
filterConfig: TableFilter[],
dataValue: any[] | undefined,
@@ -403,16 +394,6 @@ export function Table<T extends object = {}>(props: TableProps<T>) {
return distinctValues;
};
const constructCheckboxTree = (
filter: TableFilter,
): Without<CheckboxTreeProps, 'onChange'> => ({
label: filter.column,
subCategories: [...extractDistinctValues(filter.column)].map(v => ({
label: v,
options: [],
})),
});
const constructSelect = (
filter: TableFilter,
): Without<SelectProps, 'onChange'> => {
@@ -429,10 +410,7 @@ export function Table<T extends object = {}>(props: TableProps<T>) {
return filterConfig.map(filter => ({
type: filter.type,
element:
filter.type === 'checkbox-tree'
? constructCheckboxTree(filter)
: constructSelect(filter),
element: constructSelect(filter),
}));
};