From ea47ab97a799cfd2b1e59ce368070bd518beb737 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Wed, 31 Aug 2022 16:52:49 +0200 Subject: [PATCH 01/12] Improve SimpleFormIterator UI --- .../src/button/IconButtonWithTooltip.tsx | 1 - .../src/input/ArrayInput/AddItemButton.tsx | 14 +++-- .../input/ArrayInput/ArrayInput.stories.tsx | 42 ++++++++++++- .../src/input/ArrayInput/ArrayInput.tsx | 23 ++++++- .../src/input/ArrayInput/ReOrderButtons.tsx | 14 +++-- .../src/input/ArrayInput/RemoveItemButton.tsx | 14 +++-- .../input/ArrayInput/SimpleFormIterator.tsx | 17 ++++-- .../ArrayInput/SimpleFormIteratorItem.tsx | 61 +++++++++++-------- 8 files changed, 135 insertions(+), 51 deletions(-) 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.stories.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx index 8d20126de13..4f4a8189e49 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx @@ -49,7 +49,7 @@ const BookEdit = () => { }} > - + @@ -227,3 +227,43 @@ export const Realistic = () => ( /> ); + +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..d9363082350 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,7 +139,7 @@ export const ArrayInput = (props: ArrayInputProps) => { } return ( - { > @@ -177,13 +179,12 @@ export const ArrayInput = (props: ArrayInputProps) => { /> ) : null} - + ); }; ArrayInput.defaultProps = { options: {}, - fullWidth: true, }; export const getArrayInputError = error => { @@ -203,3 +204,19 @@ export interface ArrayInputProps isLoading?: boolean; record?: Partial; } + +const PREFIX = 'RaArrayInput'; + +export const ArrayInputClasses = { + label: `${PREFIX}-label`, +}; + +const Root = styled(FormControl, { + name: PREFIX, + overridesResolver: (props, styles) => styles.root, +})(({ theme }) => ({ + [`& .${ArrayInputClasses.label}`]: { + top: theme.spacing(-0.5), + left: theme.spacing(-1.5), + }, +})); 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.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx index f7305d0bd69..20590f666d5 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx @@ -146,6 +146,11 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => { ))} {!disabled && !disableAdd && (
  • + {fields.length > 0 && ( + + )} {cloneElement(addButton, { className: clsx( @@ -188,6 +193,8 @@ SimpleFormIterator.propTypes = { TransitionProps: PropTypes.shape({}), }; +type GetItemLabelFunc = (index: number) => string | ReactElement; + export interface SimpleFormIteratorProps extends Partial { addButton?: ReactElement; children?: ReactNode; @@ -196,7 +203,7 @@ export interface SimpleFormIteratorProps extends Partial { disableAdd?: boolean; disableRemove?: boolean | DisableRemoveFunction; disableReordering?: boolean; - getItemLabel?: (index: number) => string; + getItemLabel?: boolean | GetItemLabelFunc; inline?: boolean; meta?: { // the type defined in FieldArrayRenderProps says error is boolean, which is wrong. @@ -232,9 +239,9 @@ const Root = styled('ul', { }, [`& .${SimpleFormIteratorClasses.indexContainer}`]: { display: 'flex', - paddingTop: '1em', - marginRight: theme.spacing(1), - alignItems: 'center', + marginTop: theme.spacing(1), + marginRight: theme.spacing(0.5), + alignItems: 'top', }, [`& .${SimpleFormIteratorClasses.form}`]: { alignItems: 'flex-start', @@ -247,7 +254,7 @@ const Root = styled('ul', { gap: '1em', }, [`& .${SimpleFormIteratorClasses.action}`]: { - paddingTop: '0.5em', + marginTop: theme.spacing(0.5), }, [`& .${SimpleFormIteratorClasses.leftIcon}`]: { marginRight: theme.spacing(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..6d88baccbd7 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx @@ -72,32 +72,26 @@ export const SimpleFormIteratorItem = React.forwardRef( [index, total, reOrder, remove] ); + const label = + typeof getItemLabel === 'function' + ? getItemLabel(index) + : getItemLabel; + return (
  • -
    + {label && (
    - {getItemLabel(index)} + {label} - {!disabled && - !disableReordering && - cloneElement(reOrderButtons, { - index, - max: total, - reOrder, - className: clsx( - 'button-reorder', - `button-reorder-${source}-${index}` - ), - })}
    -
    + )}
    - {!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 +151,13 @@ 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; From 9757a8648f32dd6d1a9986f9bf6f4c753391fa0a Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Wed, 31 Aug 2022 17:20:11 +0200 Subject: [PATCH 02/12] Fix tests --- .../src/input/ArrayInput/ArrayInput.spec.tsx | 4 +- .../ArrayInput/SimpleFormIterator.spec.tsx | 38 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) 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..17c81934d0a 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]; 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..c95c0bec0ff 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,7 +240,7 @@ describe('', () => { ); const addItemElement = screen - .getByText('ra.action.add') + .getByLabelText('ra.action.add') .closest('button'); fireEvent.click(addItemElement); @@ -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,7 +288,7 @@ describe('', () => { ); const addItemElement = screen - .getByText('ra.action.add') + .getByLabelText('ra.action.add') .closest('button'); fireEvent.click(addItemElement); @@ -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,7 +327,7 @@ describe('', () => { ); const addItemElement = screen - .getByText('ra.action.add') + .getByLabelText('ra.action.add') .closest('button'); fireEvent.click(addItemElement); @@ -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,7 +371,7 @@ describe('', () => { ); - const removeFirstButton = getByText( + const removeFirstButton = getByLabelText( (screen.queryAllByLabelText('Email')[0] as HTMLElement).closest( 'li' ), @@ -384,7 +384,7 @@ describe('', () => { }); const addItemElement = screen - .getByText('ra.action.add') + .getByLabelText('ra.action.add') .closest('button'); fireEvent.click(addItemElement); @@ -404,7 +404,7 @@ describe('', () => { .map(inputElement => inputElement.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,7 +432,7 @@ describe('', () => { })) ).toEqual(emails); - const removeFirstButton = getByText( + const removeFirstButton = getByLabelText( inputElements[0].closest('li'), 'ra.action.remove' ).closest('button'); @@ -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); From 2c5a128a8bf6c7624b5b1507d4cc266321dd1768 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Wed, 31 Aug 2022 17:42:07 +0200 Subject: [PATCH 03/12] Fix e2e test --- cypress/integration/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/create.js b/cypress/integration/create.js index a430d8edefc..b01b8f48adc 100644 --- a/cypress/integration/create.js +++ b/cypress/integration/create.js @@ -42,7 +42,7 @@ describe('Create Page', () => { const backlinksContainer = cy .get(CreatePage.elements.input('backlinks.0.date')) .parents('.ra-input-backlinks'); - backlinksContainer.contains('Remove').click(); + backlinksContainer.get('.button-remove').click(); CreatePage.setValues([ { type: 'input', From 93acc075d7dc1905ab349781df096360b4315591 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Wed, 31 Aug 2022 17:43:08 +0200 Subject: [PATCH 04/12] Fix style for embedded ArrayInputs --- packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx | 2 +- .../src/input/ArrayInput/SimpleFormIterator.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx index d9363082350..d936410296a 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx @@ -216,7 +216,7 @@ const Root = styled(FormControl, { overridesResolver: (props, styles) => styles.root, })(({ theme }) => ({ [`& .${ArrayInputClasses.label}`]: { - top: theme.spacing(-0.5), + top: theme.spacing(-2), left: theme.spacing(-1.5), }, })); diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx index 20590f666d5..e69e984c311 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx @@ -223,6 +223,7 @@ const Root = styled('ul', { overridesResolver: (props, styles) => styles.root, })(({ theme }) => ({ padding: 0, + marginTop: 0, marginBottom: 0, '& > li:last-child': { borderBottom: 'none', From eb011263e6fcfa0c790aa095662598fa029d07f9 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Wed, 31 Aug 2022 18:00:38 +0200 Subject: [PATCH 05/12] Tweak input margin --- .../src/input/ArrayInput/ArrayInput.stories.tsx | 2 +- packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 4f4a8189e49..17b9f915a85 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx @@ -209,7 +209,7 @@ export const Realistic = () => ( - + styles.root, })(({ theme }) => ({ + marginTop: theme.spacing(2), [`& .${ArrayInputClasses.label}`]: { top: theme.spacing(-2), left: theme.spacing(-1.5), From 37d188ff8d7d5347f2d83eb18ec7a65d182463e6 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Thu, 1 Sep 2022 00:54:03 +0200 Subject: [PATCH 06/12] Improve design --- examples/simple/src/posts/PostEdit.tsx | 5 +- .../src/input/ArrayInput/ArrayInput.spec.tsx | 18 +- .../input/ArrayInput/ArrayInput.stories.tsx | 158 +++++++++++++++--- .../src/input/ArrayInput/ArrayInput.tsx | 17 +- .../ArrayInput/SimpleFormIterator.spec.tsx | 22 +-- .../ArrayInput/SimpleFormIterator.stories.tsx | 59 ++++++- .../input/ArrayInput/SimpleFormIterator.tsx | 120 +++++++------ .../ArrayInput/useSimpleFormIteratorStyles.ts | 3 +- 8 files changed, 291 insertions(+), 111 deletions(-) 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/input/ArrayInput/ArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx index 17c81934d0a..977c7723621 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx @@ -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 17b9f915a85..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,6 +50,7 @@ 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} /> + + + + + + + @@ -251,6 +358,7 @@ export const ActionsLeft = () => ( }, '& .RaSimpleFormIterator-action': { order: 1, + visibility: 'visible', }, '& .RaSimpleFormIterator-form': { order: 2, diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx index 202253a05e2..2a3b6ceaeda 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx @@ -142,7 +142,12 @@ export const ArrayInput = (props: ArrayInputProps) => { @@ -208,6 +213,7 @@ export interface ArrayInputProps const PREFIX = 'RaArrayInput'; export const ArrayInputClasses = { + root: `${PREFIX}-root`, label: `${PREFIX}-label`, }; @@ -215,9 +221,14 @@ const Root = styled(FormControl, { name: PREFIX, overridesResolver: (props, styles) => styles.root, })(({ theme }) => ({ - marginTop: theme.spacing(2), + marginTop: 0, [`& .${ArrayInputClasses.label}`]: { - top: theme.spacing(-2), + 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/SimpleFormIterator.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx index c95c0bec0ff..e08247c92ee 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx @@ -241,7 +241,7 @@ describe('', () => { const addItemElement = screen .getByLabelText('ra.action.add') - .closest('button'); + .closest('button') as HTMLButtonElement; fireEvent.click(addItemElement); await waitFor(() => { @@ -289,7 +289,7 @@ describe('', () => { const addItemElement = screen .getByLabelText('ra.action.add') - .closest('button'); + .closest('button') as HTMLButtonElement; fireEvent.click(addItemElement); await waitFor(() => { @@ -328,7 +328,7 @@ describe('', () => { const addItemElement = screen .getByLabelText('ra.action.add') - .closest('button'); + .closest('button') as HTMLButtonElement; fireEvent.click(addItemElement); await waitFor(() => { @@ -372,11 +372,10 @@ describe('', () => { ); const removeFirstButton = getByLabelText( - (screen.queryAllByLabelText('Email')[0] as HTMLElement).closest( - 'li' - ), + // @ts-ignore + screen.queryAllByLabelText('Email')[0].closest('li'), 'ra.action.remove' - ).closest('button'); + ).closest('button') as HTMLButtonElement; fireEvent.click(removeFirstButton); await waitFor(() => { @@ -385,7 +384,7 @@ describe('', () => { const addItemElement = screen .getByLabelText('ra.action.add') - .closest('button'); + .closest('button') as HTMLButtonElement; fireEvent.click(addItemElement); await waitFor(() => { @@ -396,12 +395,12 @@ 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.queryAllByLabelText('ra.action.remove').length).toBe(1); @@ -433,9 +432,10 @@ describe('', () => { ).toEqual(emails); const removeFirstButton = getByLabelText( + // @ts-ignore inputElements[0].closest('li'), 'ra.action.remove' - ).closest('button'); + ).closest('button') as HTMLButtonElement; fireEvent.click(removeFirstButton); await waitFor(() => { 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,48 +122,50 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => { ); return fields ? ( - - {fields.map((member, index) => ( - - {children} - - ))} + +
      + {fields.map((member, index) => ( + + {children} + + ))} +
    {!disabled && !disableAdd && ( -
  • - {fields.length > 0 && ( - - )} - - {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 + ), + })} +
    )}
    @@ -183,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, @@ -203,6 +207,7 @@ export interface SimpleFormIteratorProps extends Partial { disableAdd?: boolean; disableRemove?: boolean | DisableRemoveFunction; disableReordering?: boolean; + fullWidth?: boolean; getItemLabel?: boolean | GetItemLabelFunc; inline?: boolean; meta?: { @@ -218,14 +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, - marginTop: 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}`]: { @@ -248,18 +256,26 @@ const Root = styled('ul', { 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}`]: { 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/useSimpleFormIteratorStyles.ts b/packages/ra-ui-materialui/src/input/ArrayInput/useSimpleFormIteratorStyles.ts index 3ce441503ca..0a18a496f85 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/useSimpleFormIteratorStyles.ts +++ b/packages/ra-ui-materialui/src/input/ArrayInput/useSimpleFormIteratorStyles.ts @@ -1,11 +1,12 @@ export const SimpleFormIteratorPrefix = 'RaSimpleFormIterator'; export const SimpleFormIteratorClasses = { + list: `${SimpleFormIteratorPrefix}-list`, line: `${SimpleFormIteratorPrefix}-line`, + add: `${SimpleFormIteratorPrefix}-add`, index: `${SimpleFormIteratorPrefix}-index`, inline: `${SimpleFormIteratorPrefix}-inline`, indexContainer: `${SimpleFormIteratorPrefix}-indexContainer`, form: `${SimpleFormIteratorPrefix}-form`, action: `${SimpleFormIteratorPrefix}-action`, - leftIcon: `${SimpleFormIteratorPrefix}-leftIcon`, }; From 5423a0381d16e70bb5b782ab6f957683caf6dce0 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Thu, 1 Sep 2022 10:19:39 +0200 Subject: [PATCH 07/12] UI tweaks --- cypress/integration/create.js | 2 +- docs/ArrayInput.md | 8 +-- docs/SimpleFormIterator.md | 54 +++++++++++++----- docs/img/array-input-block.png | Bin 0 -> 33601 bytes docs/img/array-input-item-label.png | Bin 0 -> 70314 bytes docs/img/array-input.gif | Bin 886974 -> 559572 bytes docs/img/simple-form-iterator-inline.png | Bin 143652 -> 72181 bytes docs/img/simple-form-iterator-not-inline.png | Bin 0 -> 138250 bytes .../input/ArrayInput/ArrayInput.stories.tsx | 2 +- .../input/ArrayInput/SimpleFormIterator.tsx | 9 +-- .../ArrayInput/SimpleFormIteratorItem.tsx | 15 ++--- .../ArrayInput/useSimpleFormIteratorStyles.ts | 9 ++- 12 files changed, 59 insertions(+), 40 deletions(-) create mode 100644 docs/img/array-input-block.png create mode 100644 docs/img/array-input-item-label.png create mode 100644 docs/img/simple-form-iterator-not-inline.png diff --git a/cypress/integration/create.js b/cypress/integration/create.js index b01b8f48adc..d6a58541165 100644 --- a/cypress/integration/create.js +++ b/cypress/integration/create.js @@ -42,7 +42,7 @@ describe('Create Page', () => { const backlinksContainer = cy .get(CreatePage.elements.input('backlinks.0.date')) .parents('.ra-input-backlinks'); - backlinksContainer.get('.button-remove').click(); + 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..0a86317bb9b 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 this example, 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 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 | @@ -200,39 +222,43 @@ When true, the up and down buttons aren't rendered, so the user cannot reorder r ## `getItemLabel` -Callback to render the label displayed in each row. `` calls this function with the current row index as an argument. +`` 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 - `item #${index}`}> + `#${index + 1}`}> ``` -Use a function returning an empty string to disable the line labels: +![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. ```jsx - ''}> + ``` -## `inline` +![Inline form iterator](./img/simple-form-iterator-inline.png) -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. +Without this prop, `` will render one input per line. ```jsx - + ``` -![Inline form iterator](./img/simple-form-iterator-inline.png) +![Not Inline form iterator](./img/simple-form-iterator-not-inline.png) ## `removeButton` @@ -293,16 +319,16 @@ const OrderEdit = () => ( ## `sx` -You can override the style of the root element (a `