Skip to content

Commit

Permalink
Add support for reordering items in SimpleFormIterator
Browse files Browse the repository at this point in the history
  • Loading branch information
djhi committed Jul 13, 2021
1 parent cec6894 commit 9c31131
Show file tree
Hide file tree
Showing 12 changed files with 1,106 additions and 604 deletions.
1,232 changes: 782 additions & 450 deletions docs/Inputs.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/ra-core/src/i18n/TranslationMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export interface TranslationMessages extends StringMap {
close: string;
open_menu: string;
close_menu: string;
move_up: string;
move_down: string;
};
boolean: {
[key: string]: StringMap | string;
Expand Down
2 changes: 2 additions & 0 deletions packages/ra-language-english/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const englishMessages: TranslationMessages = {
open_menu: 'Open menu',
close_menu: 'Close menu',
update: 'Update',
move_up: 'Move up',
move_down: 'Move down',
},
boolean: {
true: 'Yes',
Expand Down
2 changes: 2 additions & 0 deletions packages/ra-language-french/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const frenchMessages: TranslationMessages = {
open_menu: 'Ouvrir le menu',
close_menu: 'Fermer le menu',
update: 'Modifier',
move_up: 'Déplacer vers le haut',
move_down: 'Déplacer vers le bas',
},
boolean: {
true: 'Oui',
Expand Down
51 changes: 51 additions & 0 deletions packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react';
import { MouseEvent } from 'react';
import { IconButton, IconButtonProps, Tooltip } from '@material-ui/core';
import { useTranslate } from 'ra-core';

/**
* An IconButton with a tooltip which ensures the tooltip is closed on click to avoid ghost tooltips
* when the button position changes.
*/
export const IconButtonWithTooltip = ({
label,
onClick,
...props
}: IconButtonWithTooltipProps) => {
const translate = useTranslate();
const [open, setOpen] = React.useState(false);

const handleClose = () => {
setOpen(false);
};

const handleOpen = () => {
setOpen(true);
};

const translatedLabel = translate(label, { _: label });

const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
handleClose();
onClick(event);
};

return (
<Tooltip
title={translatedLabel}
open={open}
onOpen={handleOpen}
onClose={handleClose}
>
<IconButton
aria-label={translatedLabel}
onClick={handleClick}
{...props}
/>
</Tooltip>
);
};

export interface IconButtonWithTooltipProps extends IconButtonProps {
label: string;
}
2 changes: 2 additions & 0 deletions packages/ra-ui-materialui/src/button/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import SortButton, { SortButtonProps } from './SortButton';
import RefreshButton, { RefreshButtonProps } from './RefreshButton';
import RefreshIconButton, { RefreshIconButtonProps } from './RefreshIconButton';

export * from './IconButtonWithTooltip';

export type {
BulkDeleteButtonProps,
BulkDeleteWithConfirmButtonProps,
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-ui-materialui/src/detail/editFieldTypes.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { ReactNode, ReactElement } from 'react';
import SimpleForm from '../form/SimpleForm';
import SimpleFormIterator from '../form/SimpleFormIterator';
import { SimpleFormIterator } from '../form/SimpleFormIterator';
import ArrayInput from '../input/ArrayInput';
import BooleanInput from '../input/BooleanInput';
import DateInput from '../input/DateInput';
Expand Down
137 changes: 91 additions & 46 deletions packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as React from 'react';
import { ArrayInput } from '../input';
import TextInput from '../input/TextInput';
import SimpleForm from './SimpleForm';
import SimpleFormIterator from './SimpleFormIterator';
import { SimpleFormIterator } from './SimpleFormIterator';

const theme = createMuiTheme();

Expand Down Expand Up @@ -266,10 +266,10 @@ describe('<SimpleFormIterator />', () => {

const inputElements = queryAllByLabelText(
'resources.undefined.fields.email'
);
) as HTMLInputElement[];

expect(
inputElements.map((inputElement: HTMLInputElement) => ({
inputElements.map(inputElement => ({
email: inputElement.value,
}))
).toEqual([{ email: '' }, { email: '' }]);
Expand Down Expand Up @@ -305,13 +305,13 @@ describe('<SimpleFormIterator />', () => {
expect(inputElements.length).toBe(1);
});

const inputElements = queryAllByLabelText('CustomLabel');
const inputElements = queryAllByLabelText(
'CustomLabel'
) as HTMLInputElement[];

expect(
inputElements.map(
(inputElement: HTMLInputElement) => inputElement.value
)
).toEqual(['']);
expect(inputElements.map(inputElement => inputElement.value)).toEqual([
'',
]);

expect(queryAllByText('ra.action.remove').length).toBe(1);
});
Expand Down Expand Up @@ -348,13 +348,13 @@ describe('<SimpleFormIterator />', () => {
expect(inputElements.length).toBe(1);
});

const inputElements = queryAllByLabelText('CustomLabel');
const inputElements = queryAllByLabelText(
'CustomLabel'
) as HTMLInputElement[];

expect(
inputElements.map(
(inputElement: HTMLInputElement) => inputElement.value
)
).toEqual(['5']);
expect(inputElements.map(inputElement => inputElement.value)).toEqual([
'5',
]);

expect(queryAllByText('ra.action.remove').length).toBe(1);
});
Expand All @@ -380,10 +380,10 @@ describe('<SimpleFormIterator />', () => {

const inputElements = queryAllByLabelText(
'resources.undefined.fields.email'
);
) as HTMLInputElement[];

expect(
inputElements.map((inputElement: HTMLInputElement) => ({
inputElements.map(inputElement => ({
email: inputElement.value,
}))
).toEqual(emails);
Expand All @@ -397,18 +397,78 @@ describe('<SimpleFormIterator />', () => {
await waitFor(() => {
const inputElements = queryAllByLabelText(
'resources.undefined.fields.email'
);
) as HTMLInputElement[];

expect(
inputElements.map((inputElement: HTMLInputElement) => ({
inputElements.map(inputElement => ({
email: inputElement.value,
}))
).toEqual([{ email: 'bar@foo.com' }]);
});
});

it('should reorder children on reorder buttons click', async () => {
const emails = [{ email: 'foo@bar.com' }, { email: 'bar@foo.com' }];

const { queryAllByLabelText } = renderWithRedux(
<ThemeProvider theme={theme}>
<SaveContextProvider value={saveContextValue}>
<SideEffectContextProvider value={sideEffectValue}>
<SimpleForm record={{ id: 'whatever', emails }}>
<ArrayInput source="emails">
<SimpleFormIterator>
<TextInput source="email" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</SideEffectContextProvider>
</SaveContextProvider>
</ThemeProvider>
);

const inputElements = queryAllByLabelText(
'resources.undefined.fields.email'
) as HTMLInputElement[];

expect(
inputElements.map(inputElement => ({
email: inputElement.value,
}))
).toEqual(emails);

const moveDownFirstButton = queryAllByLabelText('ra.action.move_down');

fireEvent.click(moveDownFirstButton[0]);
await waitFor(() => {
const inputElements = queryAllByLabelText(
'resources.undefined.fields.email'
) as HTMLInputElement[];

expect(
inputElements.map(inputElement => ({
email: inputElement.value,
}))
).toEqual([{ email: 'bar@foo.com' }, { email: 'foo@bar.com' }]);
});

const moveUpButton = queryAllByLabelText('ra.action.move_up');

fireEvent.click(moveUpButton[1]);
await waitFor(() => {
const inputElements = queryAllByLabelText(
'resources.undefined.fields.email'
) as HTMLInputElement[];

expect(
inputElements.map(inputElement => ({
email: inputElement.value,
}))
).toEqual([{ email: 'foo@bar.com' }, { email: 'bar@foo.com' }]);
});
});

it('should not display the default add button if a custom add button is passed', () => {
const { queryAllByText } = renderWithRedux(
const { getByText, queryAllByText } = renderWithRedux(
<SaveContextProvider value={saveContextValue}>
<SideEffectContextProvider value={sideEffectValue}>
<SimpleForm>
Expand All @@ -423,11 +483,13 @@ describe('<SimpleFormIterator />', () => {
</SideEffectContextProvider>
</SaveContextProvider>
);

expect(queryAllByText('ra.action.add').length).toBe(0);
expect(getByText('Custom Add Button')).not.toBeNull();
});

it('should not display the default remove button if a custom remove button is passed', () => {
const { queryAllByText } = renderWithRedux(
const { getByText, queryAllByText } = renderWithRedux(
<ThemeProvider theme={theme}>
<SaveContextProvider value={saveContextValue}>
<SideEffectContextProvider value={sideEffectValue}>
Expand All @@ -450,30 +512,11 @@ describe('<SimpleFormIterator />', () => {
);

expect(queryAllByText('ra.action.remove').length).toBe(0);
expect(getByText('Custom Remove Button')).not.toBeNull();
});

it('should display the custom add button', () => {
const { getByText } = renderWithRedux(
<SaveContextProvider value={saveContextValue}>
<SideEffectContextProvider value={sideEffectValue}>
<SimpleForm>
<ArrayInput source="emails">
<SimpleFormIterator
addButton={<button>Custom Add Button</button>}
>
<TextInput source="email" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</SideEffectContextProvider>
</SaveContextProvider>
);

expect(getByText('Custom Add Button')).not.toBeNull();
});

it('should display the custom remove button', () => {
const { getByText } = renderWithRedux(
it('should not display the default reorder element if a custom reorder element is passed', () => {
const { getByText, queryAllByLabelText } = renderWithRedux(
<ThemeProvider theme={theme}>
<SaveContextProvider value={saveContextValue}>
<SideEffectContextProvider value={sideEffectValue}>
Expand All @@ -482,8 +525,8 @@ describe('<SimpleFormIterator />', () => {
>
<ArrayInput source="emails">
<SimpleFormIterator
removeButton={
<button>Custom Remove Button</button>
reOrderButtons={
<button>Custom reorder Button</button>
}
>
<TextInput source="email" />
Expand All @@ -495,7 +538,9 @@ describe('<SimpleFormIterator />', () => {
</ThemeProvider>
);

expect(getByText('Custom Remove Button')).not.toBeNull();
expect(queryAllByLabelText('ra.action.move_up').length).toBe(0);
expect(queryAllByLabelText('ra.action.move_down').length).toBe(0);
expect(getByText('Custom reorder Button')).not.toBeNull();
});

it('should display custom row label', () => {
Expand Down
Loading

0 comments on commit 9c31131

Please sign in to comment.