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 SimpleFormConfigurable component #8395

Merged
merged 14 commits into from
Nov 18, 2022
61 changes: 61 additions & 0 deletions docs/SimpleForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,3 +492,64 @@ export const UserCreate = () => {
}
```
{% endraw %}

## Configurable

You can let end users customize the fields displayed in the `<SimpleForm>` by using the `<SimpleFormConfigurable>` component instead.

![SimpleFormConfigurable](./img/SimpleFormConfigurable.gif)

```diff
import {
Edit,
- SimpleForm,
+ SimpleFormConfigurable,
TextInput,
} from 'react-admin';

const PostEdit = () => (
<Edit>
- <SimpleForm>
+ <SimpleFormConfigurable>
<TextInput source="title" />
<TextInput source="author" />
<TextInput source="year" />
- </SimpleForm>
+ </SimpleFormConfigurable>
</Edit>
);
```

When users enter the configuration mode and select the `<SimpleForm>`, they can show / hide SimpleForm inputs.

By default, `<SimpleFormConfigurable>` renders all child inputs. But you can also omit some of them by passing an `omit` prop containing an array of input sources:

```jsx
// By default, hide the author input
// users can choose to show it in configuration mode
const PostEdit = () => (
<Edit>
<SimpleFormConfigurable omit={['author']}>
<TextInput source="title" />
<TextInput source="author" />
<TextInput source="year" />
</SimpleFormConfigurable>
</Edit>
);
```

If you render more than one `<SimpleFormConfigurable>` in the same page, you must pass a unique `preferenceKey` prop to each one:

```jsx
const PostEdit = () => (
<Edit>
<SimpleFormConfigurable preferenceKey="posts.simpleForm">
<TextInput source="title" />
<TextInput source="author" />
<TextInput source="year" />
</SimpleFormConfigurable>
</Edit>
);
```

`<SimpleFormConfigurable>` accepts the same props as `<SimpleForm>`.
Binary file added docs/img/SimpleFormConfigurable.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions examples/simple/src/comments/CommentCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
Create,
DateInput,
TextInput,
SimpleForm,
SimpleFormConfigurable,
required,
minLength,
} from 'react-admin'; // eslint-disable-line import/no-unresolved
Expand All @@ -15,7 +15,7 @@ const defaultSort = { field: 'title', order: 'ASC' };

const CommentCreate = () => (
<Create redirect={false}>
<SimpleForm>
<SimpleFormConfigurable>
<PostReferenceInput
source="post_id"
reference="posts"
Expand All @@ -26,7 +26,7 @@ const CommentCreate = () => (
<TextInput source="author.name" validate={minLength(10)} />
<DateInput source="created_at" defaultValue={now} />
<TextInput fullWidth source="body" multiline />
</SimpleForm>
</SimpleFormConfigurable>
</Create>
);

Expand Down
6 changes: 3 additions & 3 deletions examples/simple/src/posts/PostCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
ReferenceInput,
SaveButton,
SelectInput,
SimpleForm,
SimpleFormConfigurable,
SimpleFormIterator,
TextInput,
Toolbar,
Expand Down Expand Up @@ -103,7 +103,7 @@ const PostCreate = () => {
const dateDefaultValue = useMemo(() => new Date(), []);
return (
<Create redirect="edit">
<SimpleForm
<SimpleFormConfigurable
toolbar={<PostCreateToolbar />}
defaultValues={defaultValues}
>
Expand Down Expand Up @@ -193,7 +193,7 @@ const PostCreate = () => {
</SimpleFormIterator>
</ArrayInput>
)}
</SimpleForm>
</SimpleFormConfigurable>
</Create>
);
};
Expand Down
8 changes: 3 additions & 5 deletions examples/simple/src/tags/TagCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,19 @@
import * as React from 'react';
import {
Create,
SimpleForm,
TextField,
SimpleFormConfigurable,
TextInput,
required,
TranslatableInputs,
} from 'react-admin';

const TagCreate = () => (
<Create redirect="list">
<SimpleForm>
<TextField source="id" />
<SimpleFormConfigurable>
<TranslatableInputs locales={['en', 'fr']}>
<TextInput source="name" validate={[required()]} />
</TranslatableInputs>
</SimpleForm>
</SimpleFormConfigurable>
</Create>
);

Expand Down
6 changes: 3 additions & 3 deletions examples/simple/src/tags/TagEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from 'react';
import { useParams } from 'react-router';
import {
Edit,
SimpleForm,
SimpleFormConfigurable,
TextField,
TextInput,
required,
Expand All @@ -19,12 +19,12 @@ const TagEdit = () => {
return (
<>
<Edit redirect="list">
<SimpleForm warnWhenUnsavedChanges>
<SimpleFormConfigurable warnWhenUnsavedChanges>
<TextField source="id" />
<TranslatableInputs locales={['en', 'fr']}>
<TextInput source="name" validate={[required()]} />
</TranslatableInputs>
</SimpleForm>
</SimpleFormConfigurable>
</Edit>
<ResourceContextProvider value="posts">
<List
Expand Down
47 changes: 47 additions & 0 deletions packages/ra-ui-materialui/src/form/SimpleFormConfigurable.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import expect from 'expect';

import { Basic, Omit, PreferenceKey } from './SimpleFormConfigurable.stories';

describe('<SimpleFormConfigurable>', () => {
const enterConfigurationMode = async () => {
screen.getByLabelText('Configure mode').click();
await screen.findByText('Inspector');
fireEvent.mouseOver(screen.getAllByDisplayValue('War and Peace')[0]);
await screen.getByTitle('ra.configurable.customize').click();
await screen.findByText('Form');
};
it('should render a form with configurable inputs', async () => {
render(<Basic />);
await enterConfigurationMode();
expect(screen.queryByDisplayValue('Leo Tolstoy')).not.toBeNull();
screen.getAllByLabelText('Author')[0].click();
expect(screen.queryByDisplayValue('Leo Tolstoy')).toBeNull();
screen.getAllByLabelText('Author')[0].click();
expect(screen.queryByDisplayValue('Leo Tolstoy')).not.toBeNull();
});
describe('omit', () => {
it('should not render omitted inputs by default', async () => {
render(<Omit />);
expect(screen.queryByLabelText('Author')).toBeNull();
expect(screen.queryByDisplayValue('Leo Tolstoy')).toBeNull();
await enterConfigurationMode();
screen.getByLabelText('Author').click();
expect(screen.queryByDisplayValue('Leo Tolstoy')).not.toBeNull();
});
});
describe('preferenceKey', () => {
it('should allow two ConfigurableDatagrid not to share the same preferences', async () => {
render(<PreferenceKey />);
expect(screen.queryAllByDisplayValue('War and Peace')).toHaveLength(
2
);
await enterConfigurationMode();
screen.getAllByLabelText('Title')[0].click();
expect(screen.queryAllByDisplayValue('War and Peace')).toHaveLength(
1
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { PreferencesEditorContextProvider, I18nContextProvider } from 'ra-core';
import { ThemeProvider, createTheme, Box, Paper } from '@mui/material';
import { QueryClientProvider, QueryClient } from 'react-query';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import en from 'ra-language-english';

import { Inspector, InspectorButton } from '../preferences';
import { NumberInput, TextInput } from '../input';
import { SimpleFormConfigurable } from './SimpleFormConfigurable';
import { defaultTheme } from '../defaultTheme';

export default { title: 'ra-ui-materialui/forms/SimpleFormConfigurable' };

const data = {
id: 1,
title: 'War and Peace',
author: 'Leo Tolstoy',
year: 1869,
};

const Wrapper = ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
<ThemeProvider theme={createTheme(defaultTheme)}>
<PreferencesEditorContextProvider>
<MemoryRouter>
<Inspector />
<Box display="flex" justifyContent="flex-end">
<InspectorButton />
</Box>
<Paper sx={{ width: 600, m: 2 }}>{children}</Paper>
</MemoryRouter>
</PreferencesEditorContextProvider>
</ThemeProvider>
</QueryClientProvider>
);

export const Basic = () => (
<Wrapper>
<SimpleFormConfigurable record={data} resource="books">
<TextInput source="title" fullWidth />
<TextInput source="author" />
<NumberInput source="year" />
</SimpleFormConfigurable>
</Wrapper>
);

export const Omit = () => (
<Wrapper>
<SimpleFormConfigurable
record={data}
resource="books2"
omit={['author']}
>
<TextInput source="title" fullWidth />
<TextInput source="author" />
<NumberInput source="year" />
</SimpleFormConfigurable>
</Wrapper>
);

export const PreferenceKey = () => (
<Wrapper>
<SimpleFormConfigurable
record={data}
resource="books3"
preferenceKey="pref1"
>
<TextInput source="title" fullWidth />
<TextInput source="author" />
<NumberInput source="year" />
</SimpleFormConfigurable>
<SimpleFormConfigurable
record={data}
resource="books3"
preferenceKey="pref2"
>
<TextInput source="title" fullWidth />
<TextInput source="author" />
<NumberInput source="year" />
</SimpleFormConfigurable>
</Wrapper>
);

const translations = { en };
const i18nProvider = polyglotI18nProvider(locale => translations[locale], 'en');

export const I18N = () => (
<I18nContextProvider value={i18nProvider}>
<Wrapper>
<SimpleFormConfigurable record={data} resource="books">
<TextInput source="title" fullWidth />
<TextInput source="author" />
<NumberInput source="year" />
</SimpleFormConfigurable>
</Wrapper>
</I18nContextProvider>
);
Loading