diff --git a/docs/Datagrid.md b/docs/Datagrid.md index 36ec485320a..fb1a45b57ac 100644 --- a/docs/Datagrid.md +++ b/docs/Datagrid.md @@ -695,6 +695,7 @@ The `` component accepts the usual `className` prop. You can also over | Rule name | Description | | ------------------------------ |--------------------------------------------------| +| `& .RaDatagrid-root` | Applied to the root div element | | `& .RaDatagrid-tableWrapper` | Applied to the div that wraps table element | | `& .RaDatagrid-table` | Applied to the table element | | `& .RaDatagrid-thead` | Applied to the table header | @@ -797,6 +798,105 @@ const PostList = () => ( ``` {% endraw %} +## Showing / Hiding Columns + +The [``](./SelectColumnsButton.md) lets users hide or show datagrid columns. + +![SelectColumnsButton](./img/SelectColumnsButton.gif) + +```jsx +import { + DatagridConfigurable, + List, + SelectColumnsButton, + TextField, + TopToolbar, +} from "react-admin"; + +const PostListActions = () => ( + + + +); + +const PostList = () => ( + }> + + + + + + + +); +``` + +`` must be used in conjunction with ``, the configurable version of ``, described in the next section. + +## Configurable + +You can let end users customize the fields displayed in the `` by using the `` component instead. + +![DatagridConfigurable](./img/DatagridConfigurable.gif) + +```diff +import { + List, +- Datagrid, ++ DatagridConfigurable, + TextField, +} from 'react-admin'; + +const PostList = () => ( + +- ++ + + + + +- ++ + +); +``` + +When users enter the configuration mode and select the ``, they can show / hide datagrid columns. They can also use the [``](./SelectColumnsButton.md) + +By default, `` renders all child fields. But you can also omit some of them by passing an `omit` prop containing an array of field sources: + +```jsx +// by default, hide the id and author columns +// users can choose to show them in configuration mode +const PostList = () => ( + + + + + + + + +); +``` + +If you render more than one `` in the same page, you must pass a unique `preferenceKey` prop to each one: + +```jsx +const PostList = () => ( + + + + + + + + +); +``` + +`` accepts the same props as ``. + ## Customizing Column Sort ![Sort Column Header](./img/sort-column-header.gif) diff --git a/docs/Reference.md b/docs/Reference.md index 5e1bfb98740..649d32932cf 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -115,7 +115,7 @@ title: "Reference" * [``](https://marmelab.com/ra-enterprise/modules/ra-search#the-search-component) * [``](./FilteringTutorial.md#searchinput) * [``](./SelectArrayInput.md) -* [``](https://marmelab.com/ra-enterprise/modules/ra-preferences#selectcolumnsbutton-store-datagrid-columns-in-preferences) +* [``](./SelectColumnsButton.md) * [``](./SelectField.md) * [``](./SelectInput.md) * [``](./Show.md#show) diff --git a/docs/SelectColumnsButton.md b/docs/SelectColumnsButton.md new file mode 100644 index 00000000000..5b17a823e09 --- /dev/null +++ b/docs/SelectColumnsButton.md @@ -0,0 +1,83 @@ +--- +layout: default +title: "The SelectColumnsButton Component" +--- + +# `` + +This button lets users show or hide columns in a Datagrid. It must be used in conjunction with [``](./Datagrid.md#configurable). + +![SelectColumnsButton](./img/SelectColumnsButton.gif) + +```jsx +import { + DatagridConfigurable, + List, + SelectColumnsButton, + TextField, + TopToolbar, +} from "react-admin"; + +const PostListActions = () => ( + + + +); + +const PostList = () => ( + }> + + + + + + + +); +``` + +**Note**: `` doesn't work with `` - you must use `` instead. + +If you want to add the `` to the usual List Actions, use the following snippet: + +```jsx +const ListActions = () => ( + + + + + + +); +``` + +## `preferenceKey` + +If you include `` in a page that has more than one `` (e.g. in a dasboard), you have to link the two components by giving them the same `preferenceKey`: + +```jsx +const BookList = () => { + const { data, total, isLoading } = useGetList('books', { + pagination: { page: 1, perPage: 10 }, + sort, + }); + return ( +
+ + + + + + + +
+ ); +}; +``` \ No newline at end of file diff --git a/docs/img/DatagridConfigurable.gif b/docs/img/DatagridConfigurable.gif new file mode 100644 index 00000000000..51230755387 Binary files /dev/null and b/docs/img/DatagridConfigurable.gif differ diff --git a/docs/img/SelectColumnsButton.gif b/docs/img/SelectColumnsButton.gif new file mode 100644 index 00000000000..535f94b1b91 Binary files /dev/null and b/docs/img/SelectColumnsButton.gif differ diff --git a/docs/navigation.html b/docs/navigation.html index d3e1194500e..2df55b89ef4 100644 --- a/docs/navigation.html +++ b/docs/navigation.html @@ -75,6 +75,7 @@
  • <SavedQueriesList>
  • <Pagination>
  • <SortButton>
  • +
  • <SelectColumnsButton>
  • useList
  • useListContext
  • useListController
  • diff --git a/examples/demo/src/orders/OrderList.tsx b/examples/demo/src/orders/OrderList.tsx index e0337691324..1c8aced907a 100644 --- a/examples/demo/src/orders/OrderList.tsx +++ b/examples/demo/src/orders/OrderList.tsx @@ -3,7 +3,7 @@ import { Fragment, useCallback } from 'react'; import { AutocompleteInput, BooleanField, - Datagrid, + DatagridConfigurable, DateField, DateInput, List, @@ -16,6 +16,10 @@ import { TextInput, useGetList, useListContext, + TopToolbar, + SelectColumnsButton, + FilterButton, + ExportButton, } from 'react-admin'; import { useMediaQuery, Divider, Tabs, Tab, Theme } from '@mui/material'; @@ -25,12 +29,21 @@ import AddressField from '../visitors/AddressField'; import MobileGrid from './MobileGrid'; import { Customer } from '../types'; +const ListActions = () => ( + + + + + +); + const OrderList = () => ( } > @@ -130,7 +143,10 @@ const TabbedDatagrid = () => { ) : ( <> {filterValues.status === 'ordered' && ( - + @@ -143,6 +159,27 @@ const TabbedDatagrid = () => { + + + { }} sx={{ fontWeight: 'bold' }} /> - +
    )} {filterValues.status === 'delivered' && ( - + @@ -167,6 +207,27 @@ const TabbedDatagrid = () => { + + + { source="returned" sx={{ mt: -0.5, mb: -0.5 }} /> - +
    )} {filterValues.status === 'cancelled' && ( - + @@ -195,6 +259,27 @@ const TabbedDatagrid = () => { + + + { }} sx={{ fontWeight: 'bold' }} /> - - +
    )} )} diff --git a/examples/simple/src/posts/PostList.tsx b/examples/simple/src/posts/PostList.tsx index 63284b17901..ba582419651 100644 --- a/examples/simple/src/posts/PostList.tsx +++ b/examples/simple/src/posts/PostList.tsx @@ -10,10 +10,14 @@ import { BulkDeleteButton, BulkExportButton, ChipField, - Datagrid, + SelectColumnsButton, + CreateButton, + DatagridConfigurable, DateField, downloadCSV, EditButton, + ExportButton, + FilterButton, List, NumberField, ReferenceArrayField, @@ -23,6 +27,7 @@ import { SingleFieldList, TextField, TextInput, + TopToolbar, useTranslate, } from 'react-admin'; // eslint-disable-line import/no-unresolved @@ -54,7 +59,7 @@ const exporter = posts => { return jsonExport(data, (err, csv) => downloadCSV(csv, 'posts')); }; -const StyledDatagrid = styled(Datagrid)(({ theme }) => ({ +const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({ '& .title': { maxWidth: '20em', overflow: 'hidden', @@ -80,6 +85,15 @@ const PostListBulkActions = memo(({ children, ...props }) => ( )); +const PostListActions = () => ( + + + + + + +); + const PostListActionToolbar = ({ children, ...props }) => ( {children} ); @@ -103,6 +117,7 @@ const PostList = () => { filters={postFilter} sort={{ field: 'published_at', order: 'DESC' }} exporter={exporter} + actions={} > {isSmall ? ( { bulkActionButtons={} rowClick={rowClick} expand={PostPanel} - optimized + omit={['average_note']} > @@ -126,7 +141,6 @@ const PostList = () => { sortByOrder="DESC" cellClassName="publishedAt" /> - { + diff --git a/packages/ra-core/src/i18n/TranslationMessages.ts b/packages/ra-core/src/i18n/TranslationMessages.ts index 184bf0b54cd..95073bbddb8 100644 --- a/packages/ra-core/src/i18n/TranslationMessages.ts +++ b/packages/ra-core/src/i18n/TranslationMessages.ts @@ -42,6 +42,7 @@ export interface TranslationMessages extends StringMap { move_down: string; open: string; toggle_theme: string; + select_columns: string; }; boolean: { [key: string]: StringMap | string; diff --git a/packages/ra-core/src/preferences/usePreference.ts b/packages/ra-core/src/preferences/usePreference.ts index da44f026d0b..26dc90a1fe2 100644 --- a/packages/ra-core/src/preferences/usePreference.ts +++ b/packages/ra-core/src/preferences/usePreference.ts @@ -12,9 +12,9 @@ import { usePreferenceKey } from './PreferenceKeyContext'; * // this is equivalent to * const [theme, setTheme] = useStore('my-app.theme', 'light'); */ -export const usePreference = (key?: string, defaultValue?: any) => { +export const usePreference = (key?: string, defaultValue?: T) => { const preferenceKey = usePreferenceKey(); - return useStore( + return useStore( preferenceKey && key ? `${preferenceKey}.${key}` : preferenceKey ?? key, defaultValue ); diff --git a/packages/ra-core/src/store/memoryStore.tsx b/packages/ra-core/src/store/memoryStore.tsx index 29528f86047..ecc940811ac 100644 --- a/packages/ra-core/src/store/memoryStore.tsx +++ b/packages/ra-core/src/store/memoryStore.tsx @@ -50,10 +50,11 @@ export const memoryStore = (storage: any = {}): Store => { removeItems(keyPrefix: string): void { const flatStorage = flatten(storage); Object.keys(flatStorage).forEach(key => { - if (key.startsWith(keyPrefix)) { - unset(storage, key); - publish(key, undefined); + if (!key.startsWith(keyPrefix)) { + return; } + unset(storage, key); + publish(key, undefined); }); }, reset(): void { @@ -85,9 +86,7 @@ const flatten = (data: any) => { result[prop] = current; } else if (Array.isArray(current)) { // array - for (var i = 0, l = current.length; i < l; i++) - doFlatten(current[i], prop + '[' + i + ']'); - if (l === 0) result[prop] = []; + result[prop] = current; } else { // object var isEmpty = true; diff --git a/packages/ra-language-english/src/index.ts b/packages/ra-language-english/src/index.ts index a2a921fc434..ecf4036c309 100644 --- a/packages/ra-language-english/src/index.ts +++ b/packages/ra-language-english/src/index.ts @@ -38,6 +38,7 @@ const englishMessages: TranslationMessages = { move_down: 'Move down', open: 'Open', toggle_theme: 'Toggle Theme', + select_columns: 'Columns', }, boolean: { true: 'Yes', diff --git a/packages/ra-language-french/src/index.ts b/packages/ra-language-french/src/index.ts index 0c2674a0848..8e6d38f17fd 100644 --- a/packages/ra-language-french/src/index.ts +++ b/packages/ra-language-french/src/index.ts @@ -39,6 +39,7 @@ const frenchMessages: TranslationMessages = { move_down: 'Déplacer vers le bas', open: 'Ouvrir', toggle_theme: 'Thème clair/sombre', + select_columns: 'Colonnes', }, boolean: { true: 'Oui', diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleListConfigurable.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleListConfigurable.tsx index 28c0516a417..3f4c81c8018 100644 --- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleListConfigurable.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleListConfigurable.tsx @@ -9,7 +9,7 @@ export const SimpleListConfigurable = ({ preferenceKey, ...props }: SimpleListProps & { preferenceKey?: string }) => { - const resource = useResourceContext(); + const resource = useResourceContext(props); return ( } diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleListEditor.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleListEditor.tsx index d1522df0ba3..52f2a18b737 100644 --- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleListEditor.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleListEditor.tsx @@ -1,6 +1,10 @@ import * as React from 'react'; import { TextField } from '@mui/material'; -import { useTranslate, usePreferenceInput } from 'ra-core'; +import { + useTranslate, + usePreferenceInput, + useSetInspectorTitle, +} from 'ra-core'; /** * A component which provides a configuration UI to tweak the SimpleList @@ -17,6 +21,7 @@ export const SimpleListEditor = (props: SimpleListEditorProps) => { defaultTertiatyText = '', } = props; + useSetInspectorTitle('ra.inspector.simple_list', { _: 'List' }); const translate = useTranslate(); const primaryTextField = usePreferenceInput( diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx index 0e455072577..37b8bad5de0 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx @@ -227,7 +227,7 @@ export const Datagrid: FC = React.forwardRef((props, ref) => { */ return ( - + {bulkActionButtons !== false ? ( {isValidElement(bulkActionButtons) diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.spec.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.spec.tsx new file mode 100644 index 00000000000..b9b41281e3d --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.spec.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import expect from 'expect'; + +import { Basic, Omit, PreferenceKey } from './DatagridConfigurable.stories'; + +describe('', () => { + it('should render a datagrid with configurable columns', async () => { + render(); + screen.getByLabelText('Configure mode').click(); + await screen.findByText('Inspector'); + await screen.getAllByTitle('ra.configurable.customize')[0].click(); + await screen.findByText('Datagrid'); + expect(screen.queryByText('1869')).not.toBeNull(); + screen.getByLabelText('Year').click(); + expect(screen.queryByText('1869')).toBeNull(); + screen.getByLabelText('Year').click(); + expect(screen.queryByText('1869')).not.toBeNull(); + }); + it('should accept fields with a custom title', async () => { + render(); + screen.getByLabelText('Configure mode').click(); + await screen.findByText('Inspector'); + await screen.getAllByTitle('ra.configurable.customize')[0].click(); + await screen.findByText('Datagrid'); + expect(screen.queryByText('War and Peace')).not.toBeNull(); + screen.getByLabelText('Original title').click(); + expect(screen.queryByText('War and Peace')).toBeNull(); + screen.getByLabelText('Original title').click(); + expect(screen.queryByText('War and Peace')).not.toBeNull(); + }); + it('should accept fields with no source', async () => { + render(); + screen.getByLabelText('Configure mode').click(); + await screen.findByText('Inspector'); + await screen.getAllByTitle('ra.configurable.customize')[0].click(); + await screen.findByText('Datagrid'); + expect(screen.queryByText('Leo Tolstoy')).not.toBeNull(); + screen.getByLabelText('Author').click(); + expect(screen.queryByText('Leo Tolstoy')).toBeNull(); + screen.getByLabelText('Author').click(); + expect(screen.queryByText('Leo Tolstoy')).not.toBeNull(); + }); + describe('omit', () => { + it('should not render omitted columns by default', async () => { + render(); + expect(screen.queryByText('Original title')).toBeNull(); + expect(screen.queryByText('War and Peace')).toBeNull(); + screen.getByLabelText('Configure mode').click(); + await screen.findByText('Inspector'); + await screen.getAllByTitle('ra.configurable.customize')[0].click(); + await screen.findByText('Datagrid'); + screen.getByLabelText('Original title').click(); + expect(screen.queryByText('War and Peace')).not.toBeNull(); + }); + }); + describe('preferenceKey', () => { + it('should allow two ConfigurableDatagrid not to share the same preferences', async () => { + render(); + expect(screen.queryAllByText('War and Peace')).toHaveLength(2); + screen.getByLabelText('Configure mode').click(); + await screen.findByText('Inspector'); + await screen.getAllByTitle('ra.configurable.customize')[0].click(); + await screen.findByText('Datagrid'); + screen.getByLabelText('Original title').click(); + expect(screen.queryAllByText('War and Peace')).toHaveLength(1); + }); + }); +}); diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.stories.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.stories.tsx new file mode 100644 index 00000000000..b0264b9b038 --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.stories.tsx @@ -0,0 +1,126 @@ +import * as React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { PreferencesEditorContextProvider } from 'ra-core'; +import { Box } from '@mui/material'; + +import { DatagridConfigurable } from './DatagridConfigurable'; +import { Inspector, InspectorButton } from '../../preferences'; +import { TextField } from '../../field'; + +export default { title: 'ra-ui-materialui/list/DatagridConfigurable' }; + +const data = [ + { + id: 1, + title: 'War and Peace', + author: 'Leo Tolstoy', + year: 1869, + }, + { + id: 2, + title: 'Pride and Predjudice', + author: 'Jane Austen', + year: 1813, + }, + { + id: 3, + title: 'The Picture of Dorian Gray', + author: 'Oscar Wilde', + year: 1890, + }, + { + id: 4, + title: 'Le Petit Prince', + author: 'Antoine de Saint-Exupéry', + year: 1943, + }, +]; + +const AuthorField = () => ; +AuthorField.defaultProps = { label: 'Author' }; + +export const Basic = () => ( + + + + + + + + + + + + + + + + +); + +export const Omit = () => ( + + + + + + + + + + + + + + + + +); + +export const PreferenceKey = () => ( + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.tsx new file mode 100644 index 00000000000..f1d210cec5d --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.tsx @@ -0,0 +1,143 @@ +import * as React from 'react'; +import { useResourceContext, usePreference, useStore } from 'ra-core'; + +import { Configurable } from '../../preferences'; +import { Datagrid, DatagridProps } from './Datagrid'; +import { DatagridEditor } from './DatagridEditor'; + +/** + * A Datagrid that users can customize in configuration mode + * + * @example + * import { + * List, + * DatagridConfigurable, + * TextField, + * } from 'react-admin'; + * + * export const PostList = () => ( + * + * + * + * + * + * + * + * + * ); + */ +export const DatagridConfigurable = ({ + preferenceKey, + omit, + ...props +}: DatagridConfigurableProps) => { + if (props.optimized) { + throw new Error( + 'DatagridConfigurable does not support the optimized prop' + ); + } + const resource = useResourceContext(props); + const finalPreferenceKey = preferenceKey || `${resource}.datagrid`; + + const [availableColumns, setAvailableColumns] = useStore< + ConfigurableDatagridColumn[] + >(`preferences.${finalPreferenceKey}.availableColumns`, []); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, setOmit] = useStore( + `preferences.${finalPreferenceKey}.omit`, + omit + ); + + React.useEffect(() => { + // first render, or the preference have been cleared + const columns = React.Children.map(props.children, (child, index) => + React.isValidElement(child) && + (child.props.source || child.props.label) + ? { + index: String(index), + source: child.props.source, + label: child.props.label, + } + : null + ).filter(column => column != null); + if (JSON.stringify(columns) !== JSON.stringify(availableColumns)) { + setAvailableColumns(columns); + setOmit(omit); + } + }, [availableColumns]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + } + preferenceKey={finalPreferenceKey} + sx={{ + display: 'block', + '& .MuiBadge-root': { display: 'flex' }, + '& .RaDatagrid-root': { flex: 1 }, + '& .MuiBadge-badge': { zIndex: 2 }, + minHeight: 2, + }} + > + + + ); +}; + +export interface DatagridConfigurableProps extends DatagridProps { + /** + * Key to use to store the user's preferences for this datagrid. + * + * Set to '[resource].datagrid' by default. Pass a custom key if you need + * to display more than one ConfigurableDatagrid per resource. + */ + preferenceKey?: string; + /** + * columns to hide by default + * + * @example + * // by default, hide the id and author columns + * // users can choose to show show them in configuration mode + * const PostList = () => ( + * + * + * + * + * + * + * + * + * ); + */ + omit?: string[]; +} + +export interface ConfigurableDatagridColumn { + index: string; + source: string; + label?: string; +} + +DatagridConfigurable.propTypes = Datagrid.propTypes; + +/** + * This Datagrid filters its children depending on preferences + */ +const DatagridWithPreferences = ({ children, ...props }: DatagridProps) => { + const [availableColumns] = usePreference('availableColumns', []); + const [omit] = usePreference('omit', []); + const [columns] = usePreference( + 'columns', + availableColumns + .filter(column => !omit?.includes(column.source)) + .map(column => column.index) + ); + const childrenArray = React.Children.toArray(children); + return ( + + {columns === undefined + ? children + : columns.map(index => childrenArray[index])} + + ); +}; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridEditor.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridEditor.tsx new file mode 100644 index 00000000000..7c35df32294 --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridEditor.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import { usePreference, useSetInspectorTitle, useTranslate } from 'ra-core'; +import { Box, Button } from '@mui/material'; + +import { ConfigurableDatagridColumn } from './DatagridConfigurable'; +import { FieldEditor } from './FieldEditor'; + +export const DatagridEditor = () => { + const translate = useTranslate(); + useSetInspectorTitle('ra.inspector.datagrid', { _: 'Datagrid' }); + + const [availableColumns] = usePreference( + 'availableColumns', + [] + ); + const [omit] = usePreference('omit', []); + + const [columns, setColumns] = usePreference( + 'columns', + availableColumns + .filter(column => !omit?.includes(column.source)) + .map(column => column.index) + ); + + const handleToggle = event => { + if (event.target.checked) { + // add the column at the right position + setColumns( + availableColumns + .filter( + column => + column.index === event.target.name || + columns.includes(column.index) + ) + .map(column => column.index) + ); + } else { + setColumns(columns.filter(index => index !== event.target.name)); + } + }; + + const handleHideAll = () => { + setColumns([]); + }; + const handleShowAll = () => { + setColumns(availableColumns.map(column => column.index)); + }; + return ( +
    + {availableColumns.map(column => ( + + ))} + + + + +
    + ); +}; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridLoading.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridLoading.tsx index 6f49a792768..fccc3abe2ca 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridLoading.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridLoading.tsx @@ -31,55 +31,18 @@ const DatagridLoading = ({ const oneSecondHasPassed = useTimeout(1000); return oneSecondHasPassed ? ( - - - - {expand && ( - - )} - {hasBulkActions && ( - - - - )} - {times(nbChildren, key => ( - - - - ))} - - - - {times(nbFakeLines, key1 => ( - +
    +
    + + {expand && ( - - + className={DatagridClasses.expandHeader} + /> )} {hasBulkActions && ( )} - {times(nbChildren, key2 => ( + {times(nbChildren, key => ( ))} - ))} - -
    + + + {times(nbFakeLines, key1 => ( + + {expand && ( + + + + )} + {hasBulkActions && ( + + + + )} + {times(nbChildren, key2 => ( + + + + ))} + + ))} + + + ) : null; }; diff --git a/packages/ra-ui-materialui/src/list/datagrid/FieldEditor.tsx b/packages/ra-ui-materialui/src/list/datagrid/FieldEditor.tsx new file mode 100644 index 00000000000..e82eb4a4379 --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/FieldEditor.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { FieldTitle, useResourceContext } from 'ra-core'; +import { Switch, Typography } from '@mui/material'; + +/** + * UI to edit a field in a DatagridEditor + */ +export const FieldEditor = props => { + const { selected, label, onToggle, source, index } = props; + const resource = useResourceContext(); + return ( +
    + +
    + ); +}; diff --git a/packages/ra-ui-materialui/src/list/datagrid/SelectColumnsButton.spec.tsx b/packages/ra-ui-materialui/src/list/datagrid/SelectColumnsButton.spec.tsx new file mode 100644 index 00000000000..e1531e0c3b0 --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/SelectColumnsButton.spec.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import expect from 'expect'; + +import { Basic } from './SelectColumnsButton.stories'; + +describe('', () => { + it('should render a datagrid with configurable columns', async () => { + render(); + screen.getByText('Columns').click(); + expect(screen.queryByText('1869')).not.toBeNull(); + screen.getByLabelText('Year').click(); + expect(screen.queryByText('1869')).toBeNull(); + screen.getByLabelText('Year').click(); + expect(screen.queryByText('1869')).not.toBeNull(); + }); +}); diff --git a/packages/ra-ui-materialui/src/list/datagrid/SelectColumnsButton.stories.tsx b/packages/ra-ui-materialui/src/list/datagrid/SelectColumnsButton.stories.tsx new file mode 100644 index 00000000000..4f970d9e772 --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/SelectColumnsButton.stories.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { PreferencesEditorContextProvider } from 'ra-core'; +import { Box } from '@mui/material'; + +import { DatagridConfigurable } from './DatagridConfigurable'; +import { SelectColumnsButton } from './SelectColumnsButton'; +import { TextField } from '../../field'; + +export default { title: 'ra-ui-materialui/list/SelectColumnsButton' }; + +const data = [ + { + id: 1, + title: 'War and Peace', + author: 'Leo Tolstoy', + year: 1869, + }, + { + id: 2, + title: 'Pride and Predjudice', + author: 'Jane Austen', + year: 1813, + }, + { + id: 3, + title: 'The Picture of Dorian Gray', + author: 'Oscar Wilde', + year: 1890, + }, + { + id: 4, + title: 'Le Petit Prince', + author: 'Antoine de Saint-Exupéry', + year: 1943, + }, +]; + +const AuthorField = () => ; +AuthorField.defaultProps = { label: 'Author' }; + +export const Basic = () => ( + + + + + + + + + + + + + + + +); diff --git a/packages/ra-ui-materialui/src/list/datagrid/SelectColumnsButton.tsx b/packages/ra-ui-materialui/src/list/datagrid/SelectColumnsButton.tsx new file mode 100644 index 00000000000..9d0a69108a5 --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/SelectColumnsButton.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; +import { useStore, useTranslate, useResourceContext } from 'ra-core'; +import { Box, Button, Popover } from '@mui/material'; +import ViewWeekIcon from '@mui/icons-material/ViewWeek'; + +import { FieldEditor } from './FieldEditor'; +import { ConfigurableDatagridColumn } from './DatagridConfigurable'; + +/** + * Render s a button that lets users show / hide columns in a configurable datagrid + * + * @example + * import { SelectColumnsButton, DatagridConfigurable } from 'react-admin'; + * + * const PostListActions = () => ( + * + + + * + * ); + * + * const PostList = () => ( + * }> + * + * + * + ... + * + * + * ); + */ +export const SelectColumnsButton = props => { + const resource = useResourceContext(props); + const preferenceKey = + props.preferenceKey || `preferences.${resource}.datagrid`; + const [anchorEl, setAnchorEl] = React.useState(null); + const [availableColumns] = useStore( + `${preferenceKey}.availableColumns`, + [] + ); + const [omit] = useStore(`${preferenceKey}.omit`, []); + const [columns, setColumns] = useStore( + `${preferenceKey}.columns`, + availableColumns + .filter(column => !omit?.includes(column.source)) + .map(column => column.index) + ); + const translate = useTranslate(); + + const handleClick = (event): void => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = (): void => { + setAnchorEl(null); + }; + + const handleToggle = event => { + if (event.target.checked) { + // add the column at the right position + setColumns( + availableColumns + .filter( + column => + column.index === event.target.name || + columns.includes(column.index) + ) + .map(column => column.index) + ); + } else { + setColumns(columns.filter(index => index !== event.target.name)); + } + }; + + return ( + <> + + + + {availableColumns.map(column => ( + + ))} + + + + ); +}; + +export interface SelectColumnsButtonProps { + preferenceKey: string; +} diff --git a/packages/ra-ui-materialui/src/list/datagrid/index.ts b/packages/ra-ui-materialui/src/list/datagrid/index.ts index 1beb35adbc4..6c343b05785 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/index.ts +++ b/packages/ra-ui-materialui/src/list/datagrid/index.ts @@ -16,9 +16,11 @@ import DatagridRow, { import ExpandRowButton, { ExpandRowButtonProps } from './ExpandRowButton'; export * from './Datagrid'; +export * from './DatagridConfigurable'; export * from './DatagridContext'; export * from './DatagridContextProvider'; export * from './DatagridHeader'; +export * from './SelectColumnsButton'; export * from './useDatagridContext'; export * from './useDatagridStyles'; diff --git a/packages/ra-ui-materialui/src/list/datagrid/useDatagridStyles.tsx b/packages/ra-ui-materialui/src/list/datagrid/useDatagridStyles.tsx index b6eebdd9fae..c98150baeee 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/useDatagridStyles.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/useDatagridStyles.tsx @@ -3,6 +3,7 @@ import { styled } from '@mui/material'; const PREFIX = 'RaDatagrid'; export const DatagridClasses = { + root: `${PREFIX}-root`, table: `${PREFIX}-table`, tableWrapper: `${PREFIX}-tableWrapper`, thead: `${PREFIX}-thead`,