Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for reordering items in SimpleFormIterator #6433

Merged
merged 3 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -35,6 +35,8 @@ export interface TranslationMessages extends StringMap {
open_menu: string;
close_menu: string;
update: 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 = ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this component is useful (see next comment)

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}
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -26,6 +26,8 @@ export * from './DeleteButton';
export * from './DeleteWithConfirmButton';
export * from './DeleteWithUndoButton';

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
183 changes: 137 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', () => {
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
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 Expand Up @@ -581,4 +626,50 @@ describe('<SimpleFormIterator />', () => {
fireEvent.click(getByText('Custom Remove Button'));
expect(onClick).toHaveBeenCalled();
});

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(
<ThemeProvider theme={theme}>
<SaveContextProvider value={saveContextValue}>
<SideEffectContextProvider value={sideEffectValue}>
<SimpleForm
record={{ id: 'whatever', emails: [{ email: '' }] }}
>
<ArrayInput source="emails">
<SimpleFormIterator
removeButton={
<button>Custom Remove Button</button>
}
>
<TextInput source="email" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</SideEffectContextProvider>
</SaveContextProvider>
</ThemeProvider>
);

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