Skip to content

Commit bf7a689

Browse files
authored
Merge pull request #10848 from marmelab/ColumnsButton-filter-input
Add filter input to `<ColumnsButton>` when there are many columns
2 parents 82d0cd6 + 14a5846 commit bf7a689

File tree

11 files changed

+168
-6
lines changed

11 files changed

+168
-6
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createContext, useContext } from 'react';
2+
3+
export const DataTableColumnFilterContext = createContext<string | undefined>(
4+
undefined
5+
);
6+
7+
export const useDataTableColumnFilterContext = () =>
8+
useContext(DataTableColumnFilterContext);

packages/ra-core/src/dataTable/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './DataTableBase';
22
export * from './DataTableCallbacksContext';
33
export * from './DataTableColumnRankContext';
4+
export * from './DataTableColumnFilterContext';
45
export * from './DataTableConfigContext';
56
export * from './DataTableDataContext';
67
export * from './DataTableRenderContext';

packages/ra-core/src/i18n/TranslationMessages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface TranslationMessages extends StringMap {
2828
remove: string;
2929
save: string;
3030
search: string;
31+
search_columns: string;
3132
select_all: string;
3233
select_all_button: string;
3334
select_row: string;

packages/ra-language-english/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const englishMessages: TranslationMessages = {
2424
remove: 'Remove',
2525
save: 'Save',
2626
search: 'Search',
27+
search_columns: 'Search columns',
2728
select_all: 'Select all',
2829
select_all_button: 'Select all',
2930
select_row: 'Select this row',

packages/ra-language-french/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const frenchMessages: TranslationMessages = {
2828
select_all_button: 'Tout sélectionner',
2929
select_row: 'Sélectionner cette ligne',
3030
search: 'Rechercher',
31+
search_columns: 'Filtrer les colonnes',
3132
show: 'Afficher',
3233
sort: 'Trier',
3334
undo: 'Annuler',

packages/ra-ui-materialui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"autosuggest-highlight": "^3.1.1",
7373
"clsx": "^2.1.1",
7474
"css-mediaquery": "^0.1.2",
75+
"diacritic": "^0.0.2",
7576
"dompurify": "^3.2.4",
7677
"inflection": "^3.0.0",
7778
"jsonexport": "^3.2.0",
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as React from 'react';
2+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3+
import { Basic, FewColumns, LabelTypes } from './ColumnsButton.stories';
4+
5+
describe('ColumnsButton', () => {
6+
it('should render one row per column unless they are hidden', async () => {
7+
render(<Basic />);
8+
fireEvent.click(await screen.findByText('ra.action.select_columns'));
9+
await screen.findByLabelText('c_0');
10+
await screen.findByLabelText('c_1');
11+
await screen.findByLabelText('c_2');
12+
await screen.findByLabelText('c_3');
13+
await screen.findByLabelText('c_4');
14+
await screen.findByLabelText('c_5');
15+
// await screen.findByLabelText('c_6'); // hidden
16+
await screen.findByLabelText('c_7');
17+
});
18+
it('should not render the filter input when there are too few columns', async () => {
19+
render(<FewColumns />);
20+
fireEvent.click(await screen.findByText('ra.action.select_columns'));
21+
await screen.findByLabelText('c_0');
22+
expect(screen.queryByText('ra.action.search_columns')).toBeNull();
23+
});
24+
it('should render a filter input when there are many columns', async () => {
25+
render(<LabelTypes />);
26+
fireEvent.click(await screen.findByText('ra.action.select_columns'));
27+
await screen.findByLabelText('resources.test.fields.col0');
28+
expect(
29+
screen
30+
.getByRole('menu')
31+
.querySelectorAll('li:not(.columns-selector-actions)')
32+
).toHaveLength(7);
33+
// Typing a filter
34+
fireEvent.change(
35+
screen.getByPlaceholderText('ra.action.search_columns'),
36+
{
37+
// filter should be case and diacritics insensitive
38+
target: { value: 'DiA' },
39+
}
40+
);
41+
await waitFor(() => {
42+
expect(
43+
screen
44+
.getByRole('menu')
45+
.querySelectorAll('li:not(.columns-selector-actions)')
46+
).toHaveLength(1);
47+
});
48+
screen.getByLabelText('Téstïng diàcritics');
49+
// Clear the filter
50+
fireEvent.click(screen.getByLabelText('ra.action.clear_input_value'));
51+
await waitFor(() => {
52+
expect(
53+
screen
54+
.getByRole('menu')
55+
.querySelectorAll('li:not(.columns-selector-actions)')
56+
).toHaveLength(7);
57+
});
58+
});
59+
});

packages/ra-ui-materialui/src/list/datatable/ColumnsButton.stories.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,39 @@ export const Basic = () => (
6565
</DataTable>
6666
</Wrapper>
6767
);
68+
69+
export const FewColumns = () => (
70+
<Wrapper aside={<ColumnsButton />} actions={null}>
71+
<DataTable bulkActionButtons={false}>
72+
<DataTable.Col source="col0" label="c_0" />
73+
<DataTable.Col source="col1" label="c_1" />
74+
<DataTable.Col source="col2" label="c_2" />
75+
<DataTable.Col source="col3" label="c_3" />
76+
<DataTable.Col source="col4" label="c_4" />
77+
</DataTable>
78+
</Wrapper>
79+
);
80+
81+
export const LabelTypes = () => (
82+
<Wrapper aside={<ColumnsButton />} actions={null}>
83+
<DataTable bulkActionButtons={false} hiddenColumns={['col5']}>
84+
<DataTable.Col source="col0" />
85+
<DataTable.Col source="col1" label="column 1" />
86+
<DataTable.Col source="col2" label="Testing Label Case" />
87+
<DataTable.Col source="col3" label="Téstïng diàcritics" />
88+
<DataTable.Col
89+
source="col4"
90+
label={
91+
<span>
92+
Testing React <i>Element</i>
93+
</span>
94+
}
95+
/>
96+
<DataTable.Col source="col5" />
97+
<HideMe>
98+
<DataTable.Col source="col6" />
99+
</HideMe>
100+
<DataTable.Col source="col7" />
101+
</DataTable>
102+
</Wrapper>
103+
);

packages/ra-ui-materialui/src/list/datatable/ColumnsSelector.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,22 @@ import {
55
useStore,
66
DataTableColumnRankContext,
77
useDataTableStoreContext,
8+
useTranslate,
9+
DataTableColumnFilterContext,
810
} from 'ra-core';
9-
import { Box } from '@mui/material';
11+
import { Box, InputAdornment } from '@mui/material';
12+
import SearchIcon from '@mui/icons-material/Search';
1013

1114
import { Button } from '../../button';
15+
import { ResettableTextField } from '../../input/ResettableTextField';
1216

1317
/**
1418
* Render DataTable.Col elements in the ColumnsButton selector using a React Portal.
1519
*
1620
* @see ColumnsButton
1721
*/
1822
export const ColumnsSelector = ({ children }: ColumnsSelectorProps) => {
23+
const translate = useTranslate();
1924
const { storeKey, defaultHiddenColumns } = useDataTableStoreContext();
2025
const [columnRanks, setColumnRanks] = useStore<number[] | undefined>(
2126
`${storeKey}_columnRanks`
@@ -53,19 +58,55 @@ export const ColumnsSelector = ({ children }: ColumnsSelectorProps) => {
5358
};
5459
}, [elementId, container]);
5560

61+
const [columnFilter, setColumnFilter] = React.useState<string>('');
62+
5663
if (!container) return null;
5764

5865
const childrenArray = Children.toArray(children);
5966
const paddedColumnRanks = padRanks(columnRanks ?? [], childrenArray.length);
67+
const shouldDisplaySearchInput = childrenArray.length > 5;
6068

6169
return createPortal(
6270
<>
71+
{shouldDisplaySearchInput ? (
72+
<ResettableTextField
73+
hiddenLabel
74+
label=""
75+
value={columnFilter}
76+
onChange={e => {
77+
if (typeof e === 'string') {
78+
setColumnFilter(e);
79+
return;
80+
}
81+
setColumnFilter(e.target.value);
82+
}}
83+
placeholder={translate('ra.action.search_columns', {
84+
_: 'Search columns',
85+
})}
86+
InputProps={{
87+
endAdornment: (
88+
<InputAdornment position="end">
89+
<SearchIcon color="disabled" />
90+
</InputAdornment>
91+
),
92+
}}
93+
resettable
94+
autoFocus
95+
size="small"
96+
sx={{ mb: 1 }}
97+
/>
98+
) : null}
6399
{paddedColumnRanks.map((position, index) => (
64100
<DataTableColumnRankContext.Provider
65101
value={position}
66102
key={index}
67103
>
68-
{childrenArray[position]}
104+
<DataTableColumnFilterContext.Provider
105+
value={columnFilter}
106+
key={index}
107+
>
108+
{childrenArray[position]}
109+
</DataTableColumnFilterContext.Provider>
69110
</DataTableColumnRankContext.Provider>
70111
))}
71112
<Box

packages/ra-ui-materialui/src/list/datatable/ColumnsSelectorItem.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
useTranslateLabel,
66
useDataTableStoreContext,
77
useDataTableColumnRankContext,
8+
useDataTableColumnFilterContext,
89
} from 'ra-core';
10+
import * as diacritic from 'diacritic';
911

1012
import { FieldToggle } from '../../preferences';
1113
import { DataTableColumnProps } from './DataTableColumn';
@@ -24,14 +26,16 @@ export const ColumnsSelectorItem = ({
2426
const [columnRanks, setColumnRanks] = useStore<number[]>(
2527
`${storeKey}_columnRanks`
2628
);
29+
const columnFilter = useDataTableColumnFilterContext();
2730
const translateLabel = useTranslateLabel();
2831
if (!source && !label) return null;
2932
const fieldLabel = translateLabel({
3033
label: typeof label === 'string' ? label : undefined,
3134
resource,
3235
source,
33-
});
36+
}) as string;
3437
const isColumnHidden = hiddenColumns.includes(source!);
38+
const isColumnFiltered = fieldLabelMatchesFilter(fieldLabel, columnFilter);
3539

3640
const handleMove = (index1, index2) => {
3741
const colRanks = !columnRanks
@@ -69,7 +73,7 @@ export const ColumnsSelectorItem = ({
6973
setColumnRanks(newColumnRanks);
7074
};
7175

72-
return (
76+
return isColumnFiltered ? (
7377
<FieldToggle
7478
key={columnRank}
7579
source={source!}
@@ -85,7 +89,7 @@ export const ColumnsSelectorItem = ({
8589
}
8690
onMove={handleMove}
8791
/>
88-
);
92+
) : null;
8993
};
9094

9195
const padRanks = (ranks: number[], length: number) =>
@@ -95,3 +99,11 @@ const padRanks = (ranks: number[], length: number) =>
9599
(_, i) => ranks.length + i
96100
)
97101
);
102+
103+
const fieldLabelMatchesFilter = (fieldLabel: string, columnFilter?: string) =>
104+
columnFilter
105+
? diacritic
106+
.clean(fieldLabel)
107+
.toLowerCase()
108+
.includes(diacritic.clean(columnFilter).toLowerCase())
109+
: true;

0 commit comments

Comments
 (0)