diff --git a/cypress/integration/create.js b/cypress/integration/create.js index a430d8edefc..cd451b8031b 100644 --- a/cypress/integration/create.js +++ b/cypress/integration/create.js @@ -42,7 +42,10 @@ describe('Create Page', () => { const backlinksContainer = cy .get(CreatePage.elements.input('backlinks.0.date')) .parents('.ra-input-backlinks'); - backlinksContainer.contains('Remove').click(); + // The button is visibility:hidden unless the user hovers on the row. + // It is not possible to simulate a CSS hover with cypress, so we use force: true + // see https://docs.cypress.io/api/commands/hover + backlinksContainer.get('.button-remove').click({ force: true }); CreatePage.setValues([ { type: 'input', diff --git a/docs/ArrayInput.md b/docs/ArrayInput.md index 1ba117ae740..f36383f635c 100644 --- a/docs/ArrayInput.md +++ b/docs/ArrayInput.md @@ -57,10 +57,10 @@ const OrderEdit = () => ( - - - - + + + + diff --git a/docs/SimpleFormIterator.md b/docs/SimpleFormIterator.md index c8e1ca4d25d..98eee5b7df4 100644 --- a/docs/SimpleFormIterator.md +++ b/docs/SimpleFormIterator.md @@ -5,7 +5,7 @@ title: "SimpleFormIterator" # `` -This component provides a UI for editing arrays of objects, one row per object and one line per field. +This component provides a UI for editing arrays of objects, one row per object. ![ArrayInput](./img/array-input.gif) @@ -26,6 +26,26 @@ import { SimpleFormIterator } from 'react-admin'; +const OrderEdit = () => ( + + + + + + + + + + + + + +); +``` + +In the example above, the inputs for each row appear inline, with no helper text. This dense layout is adapted to arrays with many items. If you need more room, omit the `inline` prop to use the default layout, where each input is displayed in a separate row. + +```jsx const OrderEdit = () => ( @@ -43,6 +63,8 @@ const OrderEdit = () => ( ); ``` +![Simple form iterator block](./img/array-input-block.png) + ## Props | Prop | Required | Type | Default | Description | @@ -53,6 +75,7 @@ const OrderEdit = () => ( | `disableAdd` | Optional | `boolean` | `false` | When true, the user cannot add new rows | | `disableRemove` | Optional | `boolean` | `false` | When true, the user cannot remove rows | | `disableReordering` | Optional | `boolean` | `false` | When true, the user cannot reorder rows | +| `fullWidth` | Optional | `boolean` | `false` | Set to true to push the actions to the right | | `getItemLabel` | Optional | `function` | `x => x` | Callback to render the label displayed in each row | | `inline` | Optional | `boolean` | `false` | When true, inputs are put on the same line | | `removeButton` | Optional | `ReactElement` | - | Component to render for the remove button | @@ -198,28 +221,38 @@ When true, the up and down buttons aren't rendered, so the user cannot reorder r ``` -## `getItemLabel` +## `fullWidth` -Callback to render the label displayed in each row. `` calls this function with the current row index as an argument. +When true, the row actions appear at the end of the row. ```jsx - `item #${index}`}> + ``` -Use a function returning an empty string to disable the line labels: +![SimpleFormIterator full width](./img/simple-form-iterator-fullWidth.png) + +This differs with the default behavior, where the row actions appear after the inputs. + +![SimpleFormIterator default width](./img/simple-form-iterator-fullWidth-false.png) + +## `getItemLabel` + +`` can add a label in front of each row, based on the row index. Set the `getItemLabel` prop with a callback to enable this feature. ```jsx - ''}> + `#${index + 1}`}> ``` +![SimpleFormIterator with iterm label](./img/array-input-item-label.png) + ## `inline` When true, inputs are put on the same line. Use this option to make the lines more compact, especially when the children are narrow inputs. @@ -234,6 +267,18 @@ When true, inputs are put on the same line. Use this option to make the lines mo ![Inline form iterator](./img/simple-form-iterator-inline.png) +Without this prop, `` will render one input per line. + +```jsx + + + + + +``` + +![Not Inline form iterator](./img/simple-form-iterator-not-inline.png) + ## `removeButton` This prop lets you pass a custom element to replace the default Remove button. @@ -293,16 +338,16 @@ const OrderEdit = () => ( ## `sx` -You can override the style of the root element (a `
    ` element) as well as those of the inner components thanks to the `sx` property. It relies on MUI System and supports CSS and shorthand properties (see [their documentation about it](https://mui.com/customization/how-to-customize/#overriding-nested-component-styles)). +You can override the style of the root element (a `
    ` element) as well as those of the inner components thanks to the `sx` property. It relies on MUI System and supports CSS and shorthand properties (see [their documentation about it](https://mui.com/customization/how-to-customize/#overriding-nested-component-styles)). This property accepts the following subclasses: | Rule name | Description | |--------------------------|-----------------------------------------------------------| | `RaSimpleFormIterator-action` | Applied to the action zone on each row (the one containing the Remove button) | +| `RaSimpleFormIterator-add` | Applied to the bottom line containing the Add button | | `RaSimpleFormIterator-form` | Applied to the subform on each row | -| `RaSimpleFormIterator-index` | Applied to the index label | -| `RaSimpleFormIterator-indexContainer` | Applied to the container of the index label and reorder buttons | +| `RaSimpleFormIterator-index` | Applied to the row label when `getItemLabel` is set | | `RaSimpleFormIterator-inline` | Applied to rows when `inline` is true | -| `RaSimpleFormIterator-leftIcon` | Applied to the left icon on each row | -| `RaSimpleFormIterator-line` | Applied to each row | \ No newline at end of file +| `RaSimpleFormIterator-line` | Applied to each row | +| `RaSimpleFormIterator-list` | Applied to the `
      ` element | \ No newline at end of file diff --git a/docs/img/array-input-block.png b/docs/img/array-input-block.png new file mode 100644 index 00000000000..81cb308bbe5 Binary files /dev/null and b/docs/img/array-input-block.png differ diff --git a/docs/img/array-input-item-label.png b/docs/img/array-input-item-label.png new file mode 100644 index 00000000000..938276f92f3 Binary files /dev/null and b/docs/img/array-input-item-label.png differ diff --git a/docs/img/array-input.gif b/docs/img/array-input.gif index ac2a919f9af..d867f535956 100644 Binary files a/docs/img/array-input.gif and b/docs/img/array-input.gif differ diff --git a/docs/img/simple-form-iterator-fullWidth-false.png b/docs/img/simple-form-iterator-fullWidth-false.png new file mode 100644 index 00000000000..fe193b9d103 Binary files /dev/null and b/docs/img/simple-form-iterator-fullWidth-false.png differ diff --git a/docs/img/simple-form-iterator-fullWidth.png b/docs/img/simple-form-iterator-fullWidth.png new file mode 100644 index 00000000000..3201c312319 Binary files /dev/null and b/docs/img/simple-form-iterator-fullWidth.png differ diff --git a/docs/img/simple-form-iterator-inline.png b/docs/img/simple-form-iterator-inline.png index d9c5aaf1cd7..edb620bffb0 100644 Binary files a/docs/img/simple-form-iterator-inline.png and b/docs/img/simple-form-iterator-inline.png differ diff --git a/docs/img/simple-form-iterator-not-inline.png b/docs/img/simple-form-iterator-not-inline.png new file mode 100644 index 00000000000..168ab7c461c Binary files /dev/null and b/docs/img/simple-form-iterator-not-inline.png differ diff --git a/examples/simple/src/posts/PostEdit.tsx b/examples/simple/src/posts/PostEdit.tsx index 28cd635e162..8e54a888866 100644 --- a/examples/simple/src/posts/PostEdit.tsx +++ b/examples/simple/src/posts/PostEdit.tsx @@ -148,12 +148,12 @@ const PostEdit = () => { {permissions === 'admin' && ( - + - + {({ @@ -180,6 +180,7 @@ const PostEdit = () => { name: 'Co-Writer', }, ]} + helperText={false} {...rest} /> ) : null diff --git a/packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx b/packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx index e34fa2d9948..670a827836a 100644 --- a/packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx +++ b/packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx @@ -41,7 +41,6 @@ export const IconButtonWithTooltip = ({ aria-label={translatedLabel} onClick={handleClick} {...props} - size="large" /> ); diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/AddItemButton.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/AddItemButton.tsx index 1d64ee16bcf..6f2ae62e50a 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/AddItemButton.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/AddItemButton.tsx @@ -2,13 +2,19 @@ import * as React from 'react'; import AddIcon from '@mui/icons-material/AddCircleOutline'; import { useSimpleFormIterator } from './useSimpleFormIterator'; -import { Button, ButtonProps } from '../../button'; +import { IconButtonWithTooltip, ButtonProps } from '../../button'; export const AddItemButton = (props: ButtonProps) => { const { add } = useSimpleFormIterator(); return ( - + add()} + color="primary" + {...props} + > + + ); }; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx index 2727089f9b0..977c7723621 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx @@ -162,12 +162,12 @@ describe('', () => { ); - fireEvent.click(screen.getByText('ra.action.add')); + fireEvent.click(screen.getByLabelText('ra.action.add')); fireEvent.click(screen.getByText('ra.action.save')); await waitFor(() => { expect(screen.queryByText('array_min_length')).not.toBeNull(); }); - fireEvent.click(screen.getByText('ra.action.add')); + fireEvent.click(screen.getByLabelText('ra.action.add')); const firstId = screen.getAllByLabelText( 'resources.bar.fields.arr.id *' )[0]; @@ -206,16 +206,14 @@ describe('', () => { setArrayInputVisible = setVisible; - return ( - visible && ( - - - - - - - ) - ); + return visible ? ( + + + + + + + ) : null; }; render( diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx index 8d20126de13..6e16169649e 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Admin } from 'react-admin'; import { Resource } from 'ra-core'; import { createMemoryHistory } from 'history'; +import { InputAdornment } from '@mui/material'; import { Edit } from '../../detail'; import { SimpleForm } from '../../form'; @@ -49,7 +50,8 @@ const BookEdit = () => { }} > - + + @@ -81,6 +83,7 @@ export const Disabled = () => ( }} > + @@ -106,6 +109,7 @@ const BookEditWithAutocomplete = () => { }} > + ( ); +const order = { + id: 1, + date: '2022-08-30', + customer: 'John Doe', + items: [ + { + name: 'Office Jeans', + price: 45.99, + quantity: 1, + extras: [ + { + type: 'card', + price: 2.99, + content: 'For you my love', + }, + { + type: 'gift package', + price: 1.99, + content: '', + }, + { + type: 'insurance', + price: 5, + content: '', + }, + ], + }, + { + name: 'Black Elegance Jeans', + price: 69.99, + quantity: 2, + extras: [ + { + type: 'card', + price: 2.99, + content: 'For you my love', + }, + ], + }, + { + name: 'Slim Fit Jeans', + price: 55.99, + quantity: 1, + }, + ], +}; + export const Realistic = () => ( - Promise.resolve({ - data: { - id: 1, - date: '2022-08-30', - customer: 'John Doe', - items: [ - { - name: 'Office Jeans', - price: 45.99, - quantity: 1, - }, - { - name: 'Black Elegance Jeans', - price: 69.99, - quantity: 2, - }, - { - name: 'Slim Fit Jeans', - price: 55.99, - quantity: 1, - }, - ], + getOne: (resource, params) => Promise.resolve({ data: order }), + update: (resource, params) => Promise.resolve(params), + } as any + } + history={createMemoryHistory({ initialEntries: ['/orders/1'] })} + > + ( + { + console.log(data); }, - }), + }} + > + + + + + + + + € + + ), + }} + sx={{ maxWidth: 120 }} + /> + + + + + + )} + /> + +); + +export const NestedInline = () => ( + Promise.resolve({ data: order }), update: (resource, params) => Promise.resolve(params), } as any } @@ -209,7 +297,7 @@ export const Realistic = () => ( - + ( source="quantity" helperText={false} /> + + + + + + + + + + + + )} + /> + +); + +export const ActionsLeft = () => ( + + ( + { + console.log(data); + }, + }} + > + + + + + + diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx index 11c8911e359..2a3b6ceaeda 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx @@ -15,6 +15,7 @@ import { FormControl, FormHelperText, FormControlProps, + styled, } from '@mui/material'; import { LinearProgress } from '../../layout'; @@ -138,15 +139,21 @@ export const ArrayInput = (props: ArrayInputProps) => { } return ( - @@ -177,13 +184,12 @@ export const ArrayInput = (props: ArrayInputProps) => { /> ) : null} - + ); }; ArrayInput.defaultProps = { options: {}, - fullWidth: true, }; export const getArrayInputError = error => { @@ -203,3 +209,26 @@ export interface ArrayInputProps isLoading?: boolean; record?: Partial; } + +const PREFIX = 'RaArrayInput'; + +export const ArrayInputClasses = { + root: `${PREFIX}-root`, + label: `${PREFIX}-label`, +}; + +const Root = styled(FormControl, { + name: PREFIX, + overridesResolver: (props, styles) => styles.root, +})(({ theme }) => ({ + marginTop: 0, + [`& .${ArrayInputClasses.label}`]: { + position: 'relative', + top: theme.spacing(0.5), + left: theme.spacing(-1.5), + }, + [`& .${ArrayInputClasses.root}`]: { + // nested ArrayInput + paddingLeft: theme.spacing(2), + }, +})); diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ReOrderButtons.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ReOrderButtons.tsx index 9b0bd5b3b90..c5a428aca0e 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ReOrderButtons.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ReOrderButtons.tsx @@ -1,31 +1,33 @@ import * as React from 'react'; import { IconButtonWithTooltip } from '../../button'; -import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; -import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowCircleUp'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowCircleDown'; import { useSimpleFormIteratorItem } from './useSimpleFormIteratorItem'; export const ReOrderButtons = ({ className }: { className?: string }) => { const { index, total, reOrder } = useSimpleFormIteratorItem(); return ( -
      + reOrder(index - 1)} disabled={index <= 0} + color="primary" > - + reOrder(index + 1)} disabled={total == null || index >= total - 1} + color="primary" > - + -
      + ); }; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/RemoveItemButton.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/RemoveItemButton.tsx index b2ddac440f8..32c43d96c1a 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/RemoveItemButton.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/RemoveItemButton.tsx @@ -1,15 +1,21 @@ import * as React from 'react'; import CloseIcon from '@mui/icons-material/RemoveCircleOutline'; -import { Button, ButtonProps } from '../../button'; +import { IconButtonWithTooltip, ButtonProps } from '../../button'; import { useSimpleFormIteratorItem } from './useSimpleFormIteratorItem'; export const RemoveItemButton = (props: Omit) => { const { remove } = useSimpleFormIteratorItem(); return ( - + remove()} + color="warning" + {...props} + > + + ); }; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx index 8ebeff9af4c..e08247c92ee 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx @@ -3,8 +3,8 @@ import { screen, render, fireEvent, - getByText, waitFor, + getByLabelText, } from '@testing-library/react'; import expect from 'expect'; import { FormDataConsumer, testDataProvider } from 'ra-core'; @@ -124,7 +124,7 @@ describe('', () => { ); - expect(screen.getByText('ra.action.add')).not.toBeNull(); + expect(screen.getByLabelText('ra.action.add')).not.toBeNull(); }); it('should not display add button if disableAdd is truthy', () => { @@ -140,7 +140,7 @@ describe('', () => { ); - expect(screen.queryAllByText('ra.action.add').length).toBe(0); + expect(screen.queryAllByLabelText('ra.action.add').length).toBe(0); }); it('should not display add button if disabled is truthy', () => { @@ -156,7 +156,7 @@ describe('', () => { ); - expect(screen.queryAllByText('ra.action.add').length).toBe(0); + expect(screen.queryAllByLabelText('ra.action.add').length).toBe(0); }); it('should not display remove button if disableRemove is truthy', () => { @@ -177,7 +177,7 @@ describe('', () => { ); - expect(screen.queryAllByText('ra.action.remove').length).toBe(0); + expect(screen.queryAllByLabelText('ra.action.remove').length).toBe(0); }); it('should not display remove button if disableRemove return value is truthy', () => { @@ -202,7 +202,7 @@ describe('', () => { ); - expect(screen.queryAllByText('ra.action.remove').length).toBe(1); + expect(screen.queryAllByLabelText('ra.action.remove').length).toBe(1); }); it('should not display remove button if disabled is truthy', () => { @@ -223,7 +223,7 @@ describe('', () => { ); - expect(screen.queryAllByText('ra.action.remove').length).toBe(0); + expect(screen.queryAllByLabelText('ra.action.remove').length).toBe(0); }); it('should add children row on add button click', async () => { @@ -240,8 +240,8 @@ describe('', () => { ); const addItemElement = screen - .getByText('ra.action.add') - .closest('button'); + .getByLabelText('ra.action.add') + .closest('button') as HTMLButtonElement; fireEvent.click(addItemElement); await waitFor(() => { @@ -271,7 +271,7 @@ describe('', () => { })) ).toEqual([{ email: '' }, { email: '' }]); - expect(screen.queryAllByText('ra.action.remove').length).toBe(2); + expect(screen.queryAllByLabelText('ra.action.remove').length).toBe(2); }); it('should add correct children on add button click without source', async () => { @@ -288,8 +288,8 @@ describe('', () => { ); const addItemElement = screen - .getByText('ra.action.add') - .closest('button'); + .getByLabelText('ra.action.add') + .closest('button') as HTMLButtonElement; fireEvent.click(addItemElement); await waitFor(() => { @@ -306,7 +306,7 @@ describe('', () => { '', ]); - expect(screen.queryAllByText('ra.action.remove').length).toBe(1); + expect(screen.queryAllByLabelText('ra.action.remove').length).toBe(1); }); it('should add correct children with default value on add button click without source', async () => { @@ -327,8 +327,8 @@ describe('', () => { ); const addItemElement = screen - .getByText('ra.action.add') - .closest('button'); + .getByLabelText('ra.action.add') + .closest('button') as HTMLButtonElement; fireEvent.click(addItemElement); await waitFor(() => { @@ -345,7 +345,7 @@ describe('', () => { '5', ]); - expect(screen.queryAllByText('ra.action.remove').length).toBe(1); + expect(screen.queryAllByLabelText('ra.action.remove').length).toBe(1); }); it('should add correct children with default value after removing one', async () => { @@ -371,12 +371,11 @@ describe('', () => { ); - const removeFirstButton = getByText( - (screen.queryAllByLabelText('Email')[0] as HTMLElement).closest( - 'li' - ), + const removeFirstButton = getByLabelText( + // @ts-ignore + screen.queryAllByLabelText('Email')[0].closest('li'), 'ra.action.remove' - ).closest('button'); + ).closest('button') as HTMLButtonElement; fireEvent.click(removeFirstButton); await waitFor(() => { @@ -384,8 +383,8 @@ describe('', () => { }); const addItemElement = screen - .getByText('ra.action.add') - .closest('button'); + .getByLabelText('ra.action.add') + .closest('button') as HTMLButtonElement; fireEvent.click(addItemElement); await waitFor(() => { @@ -396,15 +395,15 @@ describe('', () => { expect( screen .queryAllByLabelText('Email') - .map(inputElement => inputElement.value) + .map(inputElement => (inputElement as HTMLInputElement).value) ).toEqual(['default@marmelab.com']); expect( screen .queryAllByLabelText('Name') - .map(inputElement => inputElement.value) + .map(inputElement => (inputElement as HTMLInputElement).value) ).toEqual(['']); - expect(screen.queryAllByText('ra.action.remove').length).toBe(1); + expect(screen.queryAllByLabelText('ra.action.remove').length).toBe(1); }); it('should remove children row on remove button click', async () => { @@ -432,10 +431,11 @@ describe('', () => { })) ).toEqual(emails); - const removeFirstButton = getByText( + const removeFirstButton = getByLabelText( + // @ts-ignore inputElements[0].closest('li'), 'ra.action.remove' - ).closest('button'); + ).closest('button') as HTMLButtonElement; fireEvent.click(removeFirstButton); await waitFor(() => { @@ -548,7 +548,7 @@ describe('', () => { ); - expect(screen.queryAllByText('ra.action.remove').length).toBe(0); + expect(screen.queryAllByLabelText('ra.action.remove').length).toBe(0); expect( screen.queryAllByText('Custom Remove Button').length ).toBeGreaterThan(0); @@ -731,7 +731,7 @@ describe('', () => { ); const addItemElement = screen - .getByText('ra.action.add') + .getByLabelText('ra.action.add') .closest('button') as HTMLButtonElement; fireEvent.click(addItemElement); diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.stories.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.stories.tsx index dd4ef577a3b..66fac16d258 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.stories.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.stories.tsx @@ -34,7 +34,7 @@ export const Basic = () => ( - + @@ -64,7 +64,7 @@ export const GetItemLabel = () => ( - + `item #${index}`} > @@ -77,12 +77,12 @@ export const GetItemLabel = () => ( ); -export const GetItemLabelEmpty = () => ( +export const FullWidth = () => ( - - ''}> + + @@ -96,7 +96,7 @@ export const Inline = () => ( - + @@ -107,11 +107,56 @@ export const Inline = () => ( ); +export const DisableAdd = () => ( + + + + + + + + + + + + +); + +export const DisableRemove = () => ( + + + + + + + + + + + + +); + +export const DisableReordering = () => ( + + + + + + + + + + + + +); + export const Sx = () => ( - + { disableRemove, disableReordering, inline, - getItemLabel = DefaultLabelFn, + getItemLabel = false, + fullWidth, sx, } = props; const { append, fields, move, remove } = useArrayInput(props); @@ -121,43 +122,50 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => { ); return fields ? ( - - {fields.map((member, index) => ( - - {children} - - ))} + +
        + {fields.map((member, index) => ( + + {children} + + ))} +
      {!disabled && !disableAdd && ( -
    • - - {cloneElement(addButton, { - className: clsx( - 'button-add', - `button-add-${source}` - ), - onClick: handleAddButtonClick( - addButton.props.onClick - ), - })} - -
    • +
      + {cloneElement(addButton, { + className: clsx( + 'button-add', + `button-add-${source}` + ), + onClick: handleAddButtonClick( + addButton.props.onClick + ), + })} +
      )}
      @@ -178,6 +186,7 @@ SimpleFormIterator.propTypes = { fields: PropTypes.array, fieldState: PropTypes.object, formState: PropTypes.object, + fullWidth: PropTypes.bool, inline: PropTypes.bool, record: PropTypes.object, source: PropTypes.string, @@ -188,6 +197,8 @@ SimpleFormIterator.propTypes = { TransitionProps: PropTypes.shape({}), }; +type GetItemLabelFunc = (index: number) => string | ReactElement; + export interface SimpleFormIteratorProps extends Partial { addButton?: ReactElement; children?: ReactNode; @@ -196,7 +207,8 @@ export interface SimpleFormIteratorProps extends Partial { disableAdd?: boolean; disableRemove?: boolean | DisableRemoveFunction; disableReordering?: boolean; - getItemLabel?: (index: number) => string; + fullWidth?: boolean; + getItemLabel?: boolean | GetItemLabelFunc; inline?: boolean; meta?: { // the type defined in FieldArrayRenderProps says error is boolean, which is wrong. @@ -211,13 +223,17 @@ export interface SimpleFormIteratorProps extends Partial { sx?: SxProps; } -const Root = styled('ul', { +const Root = styled('div', { name: SimpleFormIteratorPrefix, overridesResolver: (props, styles) => styles.root, })(({ theme }) => ({ - padding: 0, - marginBottom: 0, - '& > li:last-child': { + '& > ul': { + padding: 0, + marginTop: 0, + marginBottom: 0, + }, + '& > ul > li:last-child': { + // hide the last separator borderBottom: 'none', }, [`& .${SimpleFormIteratorClasses.line}`]: { @@ -227,31 +243,36 @@ const Root = styled('ul', { [theme.breakpoints.down('sm')]: { display: 'block' }, }, [`& .${SimpleFormIteratorClasses.index}`]: { - [theme.breakpoints.down('md')]: { display: 'none' }, - marginRight: theme.spacing(1), - }, - [`& .${SimpleFormIteratorClasses.indexContainer}`]: { display: 'flex', - paddingTop: '1em', + alignItems: 'top', marginRight: theme.spacing(1), - alignItems: 'center', + marginTop: theme.spacing(1), + [theme.breakpoints.down('md')]: { display: 'none' }, }, [`& .${SimpleFormIteratorClasses.form}`]: { alignItems: 'flex-start', display: 'flex', flexDirection: 'column', + }, + [`&.fullwidth > ul > li > .${SimpleFormIteratorClasses.form}`]: { flex: 2, }, [`& .${SimpleFormIteratorClasses.inline}`]: { flexDirection: 'row', - gap: '1em', + columnGap: '1em', + flexWrap: 'wrap', }, [`& .${SimpleFormIteratorClasses.action}`]: { - paddingTop: '0.5em', + marginTop: theme.spacing(0.5), + visibility: 'hidden', + '@media(hover:none)': { + visibility: 'visible', + }, }, - [`& .${SimpleFormIteratorClasses.leftIcon}`]: { - marginRight: theme.spacing(1), + [`& .${SimpleFormIteratorClasses.add}`]: { + borderBottom: 'none', + }, + [`& .${SimpleFormIteratorClasses.line}:hover > .${SimpleFormIteratorClasses.action}`]: { + visibility: 'visible', }, })); - -const DefaultLabelFn = index => index + 1; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx index bbec7309dd2..747ace0564d 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx @@ -72,32 +72,22 @@ export const SimpleFormIteratorItem = React.forwardRef( [index, total, reOrder, remove] ); + const label = + typeof getItemLabel === 'function' + ? getItemLabel(index) + : getItemLabel; + return (
    • -
      -
      - - {getItemLabel(index)} - - {!disabled && - !disableReordering && - cloneElement(reOrderButtons, { - index, - max: total, - reOrder, - className: clsx( - 'button-reorder', - `button-reorder-${source}-${index}` - ), - })} -
      -
      + {label} + + )}
      - {!disabled && !disableRemoveField(record) && ( + {!disabled && ( - {cloneElement(removeButton, { - onClick: handleRemoveButtonClick( - removeButton.props.onClick, - index - ), - className: clsx( - 'button-remove', - `button-remove-${source}-${index}` - ), - })} + {!disableReordering && + cloneElement(reOrderButtons, { + index, + max: total, + reOrder, + className: clsx( + 'button-reorder', + `button-reorder-${source}-${index}` + ), + })} + + {!disableRemoveField(record) && + cloneElement(removeButton, { + onClick: handleRemoveButtonClick( + removeButton.props.onClick, + index + ), + className: clsx( + 'button-remove', + `button-remove-${source}-${index}` + ), + })} )}
    • @@ -145,12 +147,14 @@ export const SimpleFormIteratorItem = React.forwardRef( export type DisableRemoveFunction = (record: RaRecord) => boolean; +type GetItemLabelFunc = (index: number) => string | ReactElement; + export type SimpleFormIteratorItemProps = Partial & { children?: ReactNode; disabled?: boolean; disableRemove?: boolean | DisableRemoveFunction; disableReordering?: boolean; - getItemLabel?: (index: number) => string; + getItemLabel?: boolean | GetItemLabelFunc; index: number; inline?: boolean; member: string; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/useSimpleFormIteratorStyles.ts b/packages/ra-ui-materialui/src/input/ArrayInput/useSimpleFormIteratorStyles.ts index 3ce441503ca..369f0964665 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/useSimpleFormIteratorStyles.ts +++ b/packages/ra-ui-materialui/src/input/ArrayInput/useSimpleFormIteratorStyles.ts @@ -1,11 +1,11 @@ export const SimpleFormIteratorPrefix = 'RaSimpleFormIterator'; export const SimpleFormIteratorClasses = { - line: `${SimpleFormIteratorPrefix}-line`, + action: `${SimpleFormIteratorPrefix}-action`, + add: `${SimpleFormIteratorPrefix}-add`, + form: `${SimpleFormIteratorPrefix}-form`, index: `${SimpleFormIteratorPrefix}-index`, inline: `${SimpleFormIteratorPrefix}-inline`, - indexContainer: `${SimpleFormIteratorPrefix}-indexContainer`, - form: `${SimpleFormIteratorPrefix}-form`, - action: `${SimpleFormIteratorPrefix}-action`, - leftIcon: `${SimpleFormIteratorPrefix}-leftIcon`, + line: `${SimpleFormIteratorPrefix}-line`, + list: `${SimpleFormIteratorPrefix}-list`, };