Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext, useContext } from 'react';

export const DataTableColumnFilterContext = createContext<string | undefined>(
undefined
);

export const useDataTableColumnFilterContext = () =>
useContext(DataTableColumnFilterContext);
1 change: 1 addition & 0 deletions packages/ra-core/src/dataTable/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './DataTableBase';
export * from './DataTableCallbacksContext';
export * from './DataTableColumnRankContext';
export * from './DataTableColumnFilterContext';
export * from './DataTableConfigContext';
export * from './DataTableDataContext';
export * from './DataTableRenderContext';
Expand Down
1 change: 1 addition & 0 deletions packages/ra-core/src/i18n/TranslationMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface TranslationMessages extends StringMap {
remove: string;
save: string;
search: string;
search_columns: string;
select_all: string;
select_all_button: string;
select_row: string;
Expand Down
1 change: 1 addition & 0 deletions packages/ra-language-english/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const englishMessages: TranslationMessages = {
remove: 'Remove',
save: 'Save',
search: 'Search',
search_columns: 'Search columns',
select_all: 'Select all',
select_all_button: 'Select all',
select_row: 'Select this row',
Expand Down
1 change: 1 addition & 0 deletions packages/ra-language-french/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const frenchMessages: TranslationMessages = {
select_all_button: 'Tout sélectionner',
select_row: 'Sélectionner cette ligne',
search: 'Rechercher',
search_columns: 'Filtrer les colonnes',
show: 'Afficher',
sort: 'Trier',
undo: 'Annuler',
Expand Down
1 change: 1 addition & 0 deletions packages/ra-ui-materialui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"autosuggest-highlight": "^3.1.1",
"clsx": "^2.1.1",
"css-mediaquery": "^0.1.2",
"diacritic": "^0.0.2",
"dompurify": "^3.2.4",
"inflection": "^3.0.0",
"jsonexport": "^3.2.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Basic, FewColumns, LabelTypes } from './ColumnsButton.stories';

describe('ColumnsButton', () => {
it('should render one row per column unless they are hidden', async () => {
render(<Basic />);
fireEvent.click(await screen.findByText('ra.action.select_columns'));
await screen.findByLabelText('c_0');
await screen.findByLabelText('c_1');
await screen.findByLabelText('c_2');
await screen.findByLabelText('c_3');
await screen.findByLabelText('c_4');
await screen.findByLabelText('c_5');
// await screen.findByLabelText('c_6'); // hidden
await screen.findByLabelText('c_7');
});
it('should not render the filter input when there are too few columns', async () => {
render(<FewColumns />);
fireEvent.click(await screen.findByText('ra.action.select_columns'));
await screen.findByLabelText('c_0');
expect(screen.queryByText('ra.action.search_columns')).toBeNull();
});
it('should render a filter input when there are many columns', async () => {
render(<LabelTypes />);
fireEvent.click(await screen.findByText('ra.action.select_columns'));
await screen.findByLabelText('resources.test.fields.col0');
expect(
screen
.getByRole('menu')
.querySelectorAll('li:not(.columns-selector-actions)')
).toHaveLength(7);
// Typing a filter
fireEvent.change(
screen.getByPlaceholderText('ra.action.search_columns'),
{
// filter should be case and diacritics insensitive
target: { value: 'DiA' },
}
);
await waitFor(() => {
expect(
screen
.getByRole('menu')
.querySelectorAll('li:not(.columns-selector-actions)')
).toHaveLength(1);
});
screen.getByLabelText('Téstïng diàcritics');
// Clear the filter
fireEvent.click(screen.getByLabelText('ra.action.clear_input_value'));
await waitFor(() => {
expect(
screen
.getByRole('menu')
.querySelectorAll('li:not(.columns-selector-actions)')
).toHaveLength(7);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,39 @@ export const Basic = () => (
</DataTable>
</Wrapper>
);

export const FewColumns = () => (
<Wrapper aside={<ColumnsButton />} actions={null}>
<DataTable bulkActionButtons={false}>
<DataTable.Col source="col0" label="c_0" />
<DataTable.Col source="col1" label="c_1" />
<DataTable.Col source="col2" label="c_2" />
<DataTable.Col source="col3" label="c_3" />
<DataTable.Col source="col4" label="c_4" />
</DataTable>
</Wrapper>
);

export const LabelTypes = () => (
<Wrapper aside={<ColumnsButton />} actions={null}>
<DataTable bulkActionButtons={false} hiddenColumns={['col5']}>
<DataTable.Col source="col0" />
<DataTable.Col source="col1" label="column 1" />
<DataTable.Col source="col2" label="Testing Label Case" />
<DataTable.Col source="col3" label="Téstïng diàcritics" />
<DataTable.Col
source="col4"
label={
<span>
Testing React <i>Element</i>
</span>
}
/>
<DataTable.Col source="col5" />
<HideMe>
<DataTable.Col source="col6" />
</HideMe>
<DataTable.Col source="col7" />
</DataTable>
</Wrapper>
);
45 changes: 43 additions & 2 deletions packages/ra-ui-materialui/src/list/datatable/ColumnsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@ import {
useStore,
DataTableColumnRankContext,
useDataTableStoreContext,
useTranslate,
DataTableColumnFilterContext,
} from 'ra-core';
import { Box } from '@mui/material';
import { Box, InputAdornment } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';

import { Button } from '../../button';
import { ResettableTextField } from '../../input/ResettableTextField';

/**
* Render DataTable.Col elements in the ColumnsButton selector using a React Portal.
*
* @see ColumnsButton
*/
export const ColumnsSelector = ({ children }: ColumnsSelectorProps) => {
const translate = useTranslate();
const { storeKey, defaultHiddenColumns } = useDataTableStoreContext();
const [columnRanks, setColumnRanks] = useStore<number[] | undefined>(
`${storeKey}_columnRanks`
Expand Down Expand Up @@ -53,19 +58,55 @@ export const ColumnsSelector = ({ children }: ColumnsSelectorProps) => {
};
}, [elementId, container]);

const [columnFilter, setColumnFilter] = React.useState<string>('');

if (!container) return null;

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

return createPortal(
<>
{shouldDisplaySearchInput ? (
<ResettableTextField
hiddenLabel
label=""
value={columnFilter}
onChange={e => {
if (typeof e === 'string') {
setColumnFilter(e);
return;
}
setColumnFilter(e.target.value);
}}
placeholder={translate('ra.action.search_columns', {
_: 'Search columns',
})}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<SearchIcon color="disabled" />
</InputAdornment>
),
}}
resettable
autoFocus
size="small"
sx={{ mb: 1 }}
/>
) : null}
{paddedColumnRanks.map((position, index) => (
<DataTableColumnRankContext.Provider
value={position}
key={index}
>
{childrenArray[position]}
<DataTableColumnFilterContext.Provider
value={columnFilter}
key={index}
>
{childrenArray[position]}
</DataTableColumnFilterContext.Provider>
</DataTableColumnRankContext.Provider>
))}
<Box
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
useTranslateLabel,
useDataTableStoreContext,
useDataTableColumnRankContext,
useDataTableColumnFilterContext,
} from 'ra-core';
import * as diacritic from 'diacritic';

import { FieldToggle } from '../../preferences';
import { DataTableColumnProps } from './DataTableColumn';
Expand All @@ -24,14 +26,16 @@ export const ColumnsSelectorItem = ({
const [columnRanks, setColumnRanks] = useStore<number[]>(
`${storeKey}_columnRanks`
);
const columnFilter = useDataTableColumnFilterContext();
const translateLabel = useTranslateLabel();
if (!source && !label) return null;
const fieldLabel = translateLabel({
label: typeof label === 'string' ? label : undefined,
resource,
source,
});
}) as string;
const isColumnHidden = hiddenColumns.includes(source!);
const isColumnFiltered = fieldLabelMatchesFilter(fieldLabel, columnFilter);

const handleMove = (index1, index2) => {
const colRanks = !columnRanks
Expand Down Expand Up @@ -69,7 +73,7 @@ export const ColumnsSelectorItem = ({
setColumnRanks(newColumnRanks);
};

return (
return isColumnFiltered ? (
<FieldToggle
key={columnRank}
source={source!}
Expand All @@ -85,7 +89,7 @@ export const ColumnsSelectorItem = ({
}
onMove={handleMove}
/>
);
) : null;
};

const padRanks = (ranks: number[], length: number) =>
Expand All @@ -95,3 +99,11 @@ const padRanks = (ranks: number[], length: number) =>
(_, i) => ranks.length + i
)
);

const fieldLabelMatchesFilter = (fieldLabel: string, columnFilter?: string) =>
columnFilter
? diacritic
.clean(fieldLabel)
.toLowerCase()
.includes(diacritic.clean(columnFilter).toLowerCase())
: true;
3 changes: 2 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8499,7 +8499,7 @@ __metadata:
languageName: node
linkType: hard

"diacritic@npm:0.0.2":
"diacritic@npm:0.0.2, diacritic@npm:^0.0.2":
version: 0.0.2
resolution: "diacritic@npm:0.0.2"
checksum: 1d9dd0a1188a8186d4fce4a695fc8cb0d65c31a8b3c59cd926636e49a05b30d6bb3f4144018be40bdf0a4937d16bb6705f3b1d1ff9684a426d922fb039f8d8ae
Expand Down Expand Up @@ -16316,6 +16316,7 @@ __metadata:
cross-env: "npm:^5.2.0"
css-mediaquery: "npm:^0.1.2"
csstype: "npm:^3.1.3"
diacritic: "npm:^0.0.2"
dompurify: "npm:^3.2.4"
expect: "npm:^27.4.6"
file-api: "npm:~0.10.4"
Expand Down
Loading