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 ipnut
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
// 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
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>
);
128 changes: 128 additions & 0 deletions packages/ra-ui-materialui/src/form/SimpleFormConfigurable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as React from 'react';
import {
useResourceContext,
usePreference,
useStore,
useTranslate,
} from 'ra-core';

import { Configurable } from '../preferences';
import { SimpleForm, SimpleFormProps } from './SimpleForm';
import { SimpleFormEditor } from './SimpleFormEditor';

export const SimpleFormConfigurable = ({
preferenceKey,
omit,
...props
}: SimpleFormConfigurableProps) => {
const translate = useTranslate();
const resource = useResourceContext(props);
const finalPreferenceKey = preferenceKey || `${resource}.simpleForm`;

const [availableInputs, setAvailableInputs] = useStore<
SimpleFormConfigurableColumn[]
>(`preferences.${finalPreferenceKey}.availableInputs`, []);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setOmit] = useStore<string[]>(
`preferences.${finalPreferenceKey}.omit`,
omit
);

React.useEffect(() => {
// first render, or the preference have been cleared
const inputs = React.Children.map(props.children, (child, index) =>
React.isValidElement(child)
? {
index: String(index),
source: child.props.source,
label:
child.props.source || child.props.label
? child.props.label
: translate(
'ra.configurable.simpleForm.unlabeled',
{
input: index,
_: `Unlabeled input #%{input}`,
}
),
}
: null
).filter(column => column != null);
if (JSON.stringify(inputs) !== JSON.stringify(availableInputs)) {
setAvailableInputs(inputs);
setOmit(omit);
}
}, [availableInputs]); // eslint-disable-line react-hooks/exhaustive-deps

return (
<Configurable
editor={<SimpleFormEditor />}
preferenceKey={finalPreferenceKey}
sx={{
display: 'block',
'&.RaConfigurable-editMode': {
margin: '2px',
Copy link
Member Author

Choose a reason for hiding this comment

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

MAde necessary because the outline is invisible for elements used as children of Paper, which has overflow:hidden

},
}}
>
<SimpleFormWithPreferences {...props} />
</Configurable>
);
};

export interface SimpleFormConfigurableProps extends SimpleFormProps {
/**
* Key to use to store the user's preferences for this SimpleForm.
*
* Set to '[resource].simpleForm' by default. Pass a custom key if you need
* to display more than one SimpleFormConfigurable per resource.
*/
preferenceKey?: string;
/**
* columns to hide by default
*
* @example
* // by default, hide the id and author columns
* // users can choose to show show them in configuration mode
* const PostEdit = () => (
* <Edit>
* <SimpleFormConfigurable omit={['id', 'author']}>
* <TextInput source="id" />
* <TextInput source="title" />
* <TextInput source="author" />
* <TextInput source="year" />
* </SimpleFormConfigurable>
* </Edit>
* );
*/
omit?: string[];
}

export interface SimpleFormConfigurableColumn {
index: string;
source: string;
label?: string;
}

/**
* This SimpleForm filters its children depending on preferences
*/
const SimpleFormWithPreferences = ({ children, ...props }: SimpleFormProps) => {
const [availableInputs] = usePreference('availableInputs', []);
const [omit] = usePreference('omit', []);
const [inputs] = usePreference(
'inputs',
availableInputs
.filter(input => !omit?.includes(input.source))
.map(input => input.index)
);
const childrenArray = React.Children.toArray(children);
return (
<SimpleForm {...props}>
{inputs === undefined
? children
: inputs.map(index => childrenArray[index])}
</SimpleForm>
);
};
Loading