From 6118c6a7a601bf1ba6bc6b9423f1077645d6f6c5 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:29:38 +0200
Subject: [PATCH 01/19] Backport #9656
---
Makefile | 2 +-
docs/Inputs.md | 25 ++-
docs/SimpleFormIterator.md | 29 ++++
docs/TranslatableInputs.md | 2 +-
packages/ra-core/src/form/useInput.ts | 2 +
.../src/RichTextInput.stories.tsx | 13 ++
.../input/ArrayInput/ArrayInput.stories.tsx | 42 ++++-
.../src/input/ArrayInput/ArrayInput.tsx | 1 +
.../ArrayInput/SimpleFormIterator.stories.tsx | 32 ++++
.../input/ArrayInput/SimpleFormIterator.tsx | 1 +
.../input/AutocompleteArrayInput.stories.tsx | 56 +++++++
.../src/input/AutocompleteInput.stories.tsx | 34 +++++
.../src/input/AutocompleteInput.tsx | 6 +
.../src/input/BooleanInput.stories.tsx | 6 +
.../src/input/BooleanInput.tsx | 6 +-
.../src/input/CheckboxGroupInput.stories.tsx | 18 +++
.../src/input/CheckboxGroupInput.tsx | 6 +
.../src/input/CommonInputProps.ts | 2 +
.../src/input/DatagridInput.tsx | 5 +-
.../src/input/DateInput.stories.tsx | 8 +
.../ra-ui-materialui/src/input/DateInput.tsx | 6 +
.../src/input/DateTimeInput.stories.tsx | 16 ++
.../src/input/DateTimeInput.tsx | 6 +
.../src/input/FileInput.stories.tsx | 11 +-
.../ra-ui-materialui/src/input/FileInput.tsx | 24 ++-
.../src/input/ImageInput.stories.tsx | 11 +-
.../ra-ui-materialui/src/input/ImageInput.tsx | 1 -
.../input/NullableBooleanInput.stories.tsx | 8 +
.../src/input/NullableBooleanInput.tsx | 8 +-
.../src/input/NumberInput.stories.tsx | 34 +++++
.../src/input/NumberInput.tsx | 8 +-
.../input/RadioButtonGroupInput.stories.tsx | 12 ++
.../src/input/RadioButtonGroupInput.tsx | 6 +
.../src/input/ReferenceArrayInput.stories.tsx | 2 +-
.../src/input/ResettableTextField.tsx | 7 +-
.../src/input/SelectArrayInput.stories.tsx | 144 ++++++++++++++++++
.../src/input/SelectArrayInput.tsx | 6 +
.../src/input/SelectInput.stories.tsx | 31 ++++
.../src/input/TextInput.stories.tsx | 32 ++++
.../src/input/TimeInput.stories.tsx | 7 +
.../ra-ui-materialui/src/input/TimeInput.tsx | 6 +
41 files changed, 657 insertions(+), 25 deletions(-)
diff --git a/Makefile b/Makefile
index 3e9445aab6b..a8a48cc77cd 100644
--- a/Makefile
+++ b/Makefile
@@ -114,7 +114,7 @@ build-create-react-admin:
@echo "Transpiling create-react-admin files...";
@cd ./packages/create-react-admin && yarn build
-build: build-ra-core build-ra-ui-materialui build-ra-data-fakerest build-ra-data-json-server build-ra-data-localforage build-ra-data-localstorage build-ra-data-simple-rest build-ra-data-graphql build-ra-data-graphql-simple build-ra-i18n-polyglot build-ra-input-rich-text build-data-generator build-ra-language-english build-ra-language-french build-ra-i18n-i18next build-react-admin build-ra-no-code build-create-react-admin ## compile ES6 files to JS
+build: build-ra-core build-ra-data-fakerest build-ra-ui-materialui build-ra-data-json-server build-ra-data-localforage build-ra-data-localstorage build-ra-data-simple-rest build-ra-data-graphql build-ra-data-graphql-simple build-ra-i18n-polyglot build-ra-input-rich-text build-data-generator build-ra-language-english build-ra-language-french build-ra-i18n-i18next build-react-admin build-ra-no-code build-create-react-admin ## compile ES6 files to JS
doc: ## compile doc as html and launch doc web server
@yarn doc
diff --git a/docs/Inputs.md b/docs/Inputs.md
index 33c4f4718fa..e3a8466e6b9 100644
--- a/docs/Inputs.md
+++ b/docs/Inputs.md
@@ -43,7 +43,8 @@ All input components accept the following props:
| `source` | Required | `string` | - | Name of the entity property to use for the input value |
| `className` | Optional | `string` | - | Class name (usually generated by JSS) to customize the look and feel of the field element itself |
| `defaultValue` | Optional | `any` | - | Default value of the input. |
-| `disabled` | Optional | `boolean` | - | If true, the input is disabled. |
+| `readOnly` | Optional | `boolean` | `false` | If true, the input is in read-only mode. |
+| `disabled` | Optional | `boolean` | `false` | If true, the input is disabled. |
| `format` | Optional | `Function` | `value => value == null ? '' : value` | Callback taking the value from the form state, and returning the input value. |
| `fullWidth` | Optional | `boolean` | `true` | If `false`, the input will not expand to fill the form width |
| `helperText` | Optional | `string` | - | Text to be displayed under the input (cannot be used inside a filter) |
@@ -150,21 +151,29 @@ export const PostCreate = () => (
);
```
+## `readOnly`
+
+The `readOnly` prop set to true makes the element not mutable, meaning the user can not edit the control.
+
+```tsx
+
+```
+
+Contrary to disabled controls, read-only controls are still focusable and are submitted with the form.
+
## `disabled`
-If `true`, the input is disabled and the user can't change the value.
+The `disabled` prop set to true makes the element not mutable, focusable, or even submitted with the form.
```tsx
```
-**Tip**: The form framework used by react-admin, react-hook-form, [considers](https://github.com/react-hook-form/react-hook-form/pull/10805) that a `disabled` input shouldn't submit any value. So react-hook-form sets the value of all `disabled` inputs to `undefined`. As a consequence, a form with a `disabled` input is always considered `dirty` (i.e. react-hook-form considers that the form values and the initial record values are different), and it triggers [the `warnWhenUnsavedChanges` feature](./Forms.md#warning-about-unsaved-changes) when leaving the form, even though the user changed nothing. The workaround is to set the `disabled` prop on the underlying input component, as follows:
+Contrary to read-only controls, disabled controls can not receive focus and are not submitted with the form.
-{% raw %}
-```jsx
-
-```
-{% endraw %}
+**Warning:** Note that `disabled` inputs are **not** included in the form values, and hence may trigger `warnWhenUnsavedChanges` if the input previously had a value in the record.
+
+**Tip:** To include the input in the form values, you can use `readOnly` instead of `disabled`.
## `format`
diff --git a/docs/SimpleFormIterator.md b/docs/SimpleFormIterator.md
index fd09517578c..903f9dc410d 100644
--- a/docs/SimpleFormIterator.md
+++ b/docs/SimpleFormIterator.md
@@ -85,6 +85,7 @@ const OrderEdit = () => (
| `inline` | Optional | `boolean` | `false` | When true, inputs are put on the same line |
| `removeButton` | Optional | `ReactElement` | - | Component to render for the remove button |
| `reOrderButtons` | Optional | `ReactElement` | - | Component to render for the up / down button |
+| `disabled` | Optional | `boolean` | `false` | If true, all buttons are disabled. |
| `sx` | Optional | `SxProps` | - | Material UI shortcut for defining custom styles |
## `addButton`
@@ -345,6 +346,34 @@ const OrderEdit = () => (
);
```
+## `readOnly`
+
+The `readOnly` prop set to true makes the children input not mutable, meaning the user can not edit them.
+
+```jsx
+
+
+
+
+
+```
+
+Contrary to disabled controls, read-only controls are still focusable and are submitted with the form.
+
+## `disabled`
+
+The `disabled` prop set to true makes the children input not mutable, focusable, or even submitted with the form.
+
+```jsx
+
+
+
+
+
+```
+
+Contrary to read-only controls, disabled controls can not receive focus and are not submitted with the form.
+
## `sx`
You can override the style of the root element (a `
)}
+ disabled={disabled || readOnly}
+ readOnly={readOnly}
data-testid="selectArray"
size={size}
{...field}
diff --git a/packages/ra-ui-materialui/src/input/SelectInput.stories.tsx b/packages/ra-ui-materialui/src/input/SelectInput.stories.tsx
index 15cee12bfa7..65f30581d5b 100644
--- a/packages/ra-ui-materialui/src/input/SelectInput.stories.tsx
+++ b/packages/ra-ui-materialui/src/input/SelectInput.stories.tsx
@@ -83,6 +83,37 @@ export const Disabled = () => (
]}
disabled
/>
+
+
+);
+
+export const ReadOnly = () => (
+
+
+
);
diff --git a/packages/ra-ui-materialui/src/input/TextInput.stories.tsx b/packages/ra-ui-materialui/src/input/TextInput.stories.tsx
index 0474ff269ec..745fa958b84 100644
--- a/packages/ra-ui-materialui/src/input/TextInput.stories.tsx
+++ b/packages/ra-ui-materialui/src/input/TextInput.stories.tsx
@@ -38,6 +38,38 @@ export const Resettable = () => (
);
+export const Disabled = () => (
+
+
+
+
+
+
+
+
+
+);
+
+export const ReadOnly = () => (
+
+
+
+
+
+
+
+
+
+);
+
export const DefaultValue = () => (
diff --git a/packages/ra-ui-materialui/src/input/TimeInput.stories.tsx b/packages/ra-ui-materialui/src/input/TimeInput.stories.tsx
index 9e9cc17ceca..77cca9595d6 100644
--- a/packages/ra-ui-materialui/src/input/TimeInput.stories.tsx
+++ b/packages/ra-ui-materialui/src/input/TimeInput.stories.tsx
@@ -25,6 +25,13 @@ export const NonFullWidth = () => (
export const Disabled = () => (
+
+
+);
+export const ReadOnly = () => (
+
+
+
);
diff --git a/packages/ra-ui-materialui/src/input/TimeInput.tsx b/packages/ra-ui-materialui/src/input/TimeInput.tsx
index 2342fd8fa03..9c0765e2dc6 100644
--- a/packages/ra-ui-materialui/src/input/TimeInput.tsx
+++ b/packages/ra-ui-materialui/src/input/TimeInput.tsx
@@ -54,6 +54,8 @@ export const TimeInput = ({
onChange,
source,
resource,
+ disabled,
+ readOnly,
parse = parseTime,
validate,
variant,
@@ -68,6 +70,8 @@ export const TimeInput = ({
resource,
source,
validate,
+ readOnly,
+ disabled,
...rest,
});
@@ -85,6 +89,8 @@ export const TimeInput = ({
variant={variant}
margin={margin}
error={invalid}
+ disabled={disabled || readOnly}
+ readOnly={readOnly}
helperText={
renderHelperText ? (
Date: Mon, 24 Jun 2024 15:35:42 +0200
Subject: [PATCH 02/19] Backport #9673
---
yarn.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index 11eac332e55..3776596ad5c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13096,9 +13096,9 @@ __metadata:
linkType: hard
"ip@npm:^1.1.5":
- version: 1.1.5
- resolution: "ip@npm:1.1.5"
- checksum: 877e98d676cd8d0ca01fee8282d11b91fb97be7dd9d0b2d6d98e161db2d4277954f5b55db7cfc8556fe6841cb100d13526a74f50ab0d83d6b130fe8445040175
+ version: 1.1.9
+ resolution: "ip@npm:1.1.9"
+ checksum: 5af58bfe2110c9978acfd77a2ffcdf9d33a6ce1c72f49edbaf16958f7a8eb979b5163e43bb18938caf3aaa55cdacde4e470874c58ca3b4b112ea7a30461a0c27
languageName: node
linkType: hard
From 105dcde859b810cf786124b3a5ad84a1514643b0 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:36:55 +0200
Subject: [PATCH 03/19] Backport #9677
---
.../src/input/ArrayInput/ArrayInput.spec.tsx | 54 +++++++++++++++++++
.../src/input/ArrayInput/ArrayInput.tsx | 7 ++-
2 files changed, 60 insertions(+), 1 deletion(-)
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 d4467c1e0c0..17db8f2c750 100644
--- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx
+++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx
@@ -248,6 +248,60 @@ describe('', () => {
});
});
+ it('should not clear errors of children when unmounted', async () => {
+ let setArrayInputVisible;
+
+ const MyArrayInput = () => {
+ const [visible, setVisible] = React.useState(true);
+
+ setArrayInputVisible = setVisible;
+
+ return visible ? (
+
+
+
+
+
+
+ ) : null;
+ };
+
+ render(
+
+ ({ arr: [{ foo: 'Must be "baz"' }, {}] })}
+ >
+
+
+
+ );
+
+ // change one input to enable the SaveButton (which is disabled when the form is pristine)
+ fireEvent.change(
+ screen.getAllByLabelText('resources.bar.fields.arr.id')[0],
+ {
+ target: { value: '42' },
+ }
+ );
+ fireEvent.click(await screen.findByLabelText('ra.action.save'));
+
+ await screen.findByText('Must be "baz"');
+
+ setArrayInputVisible(false);
+ expect(screen.queryByText('Must be "baz"')).toBeNull();
+
+ // ensure errors are still there after re-mount
+ setArrayInputVisible(true);
+ await screen.findByText('Must be "baz"');
+ });
+
it('should allow to have a helperText', () => {
render(
diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx
index ce0a78f8dec..d7576bd5a7f 100644
--- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx
+++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx
@@ -129,7 +129,12 @@ export const ArrayInput = (props: ArrayInputProps) => {
formGroups.registerField(finalSource, formGroupName);
return () => {
- unregister(finalSource, { keepValue: true });
+ unregister(finalSource, {
+ keepValue: true,
+ keepError: true,
+ keepDirty: true,
+ keepTouched: true,
+ });
formGroups &&
formGroupName != null &&
formGroups.unregisterField(finalSource, formGroupName);
From f9252e753f96733affbd2971b905962557ea3164 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:37:17 +0200
Subject: [PATCH 04/19] Backport #9681
---
packages/ra-ui-materialui/src/input/AutocompleteInput.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
index 9caa4f48195..65a0519fb18 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
@@ -597,6 +597,7 @@ If you provided a React element for the optionText prop, you must also provide t
variant={variant}
className={AutocompleteInputClasses.textField}
{...params}
+ {...TextFieldProps}
InputProps={mergedTextFieldProps}
size={size}
/>
From 178c5130a981fd2446f07374543d81ace8c96308 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:39:22 +0200
Subject: [PATCH 05/19] Backport #9690
---
packages/ra-ui-materialui/src/input/ReferenceInput.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/ra-ui-materialui/src/input/ReferenceInput.tsx b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx
index 804b018f663..0394f49622c 100644
--- a/packages/ra-ui-materialui/src/input/ReferenceInput.tsx
+++ b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx
@@ -66,7 +66,7 @@ import { AutocompleteInput } from './AutocompleteInput';
export const ReferenceInput = (props: ReferenceInputProps) => {
const { children = defaultChildren, ...rest } = props;
- if (props.validate) {
+ if (props.validate && process.env.NODE_ENV !== 'production') {
throw new Error(
' does not accept a validate prop. Set the validate prop on the child instead.'
);
From 84b12042e04856f3b229c68ed7dd0f76fa23ad7c Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:39:52 +0200
Subject: [PATCH 06/19] Backport #9694
---
packages/ra-ui-materialui/src/button/SortButton.tsx | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/packages/ra-ui-materialui/src/button/SortButton.tsx b/packages/ra-ui-materialui/src/button/SortButton.tsx
index d3267958354..74a3dfd269a 100644
--- a/packages/ra-ui-materialui/src/button/SortButton.tsx
+++ b/packages/ra-ui-materialui/src/button/SortButton.tsx
@@ -53,8 +53,14 @@ const SortButton = (props: SortButtonProps) => {
icon = defaultIcon,
sx,
className,
+ resource: resourceProp,
} = props;
- const { resource, sort, setSort } = useListSortContext();
+ const {
+ resource: resourceFromContext,
+ sort,
+ setSort,
+ } = useListSortContext();
+ const resource = resourceProp || resourceFromContext;
const translate = useTranslate();
const translateLabel = useTranslateLabel();
const isXSmall = useMediaQuery((theme: Theme) =>
From cea3bf8241f29089a7f26bcdd57f1839c6a93177 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:40:11 +0200
Subject: [PATCH 07/19] Backport #9697
---
docs/useNotify.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/useNotify.md b/docs/useNotify.md
index 72418c84a50..47faa67d20f 100644
--- a/docs/useNotify.md
+++ b/docs/useNotify.md
@@ -149,6 +149,7 @@ notify('This is an error', { type: 'error' });
When using `useNotify` as a side effect for an `undoable` mutation, you MUST set the `undoable` option to `true`, otherwise the "undo" button will not appear, and the actual update will never occur.
+{% raw %}
```jsx
import * as React from 'react';
import { useNotify, Edit, SimpleForm } from 'react-admin';
@@ -169,6 +170,7 @@ const PostEdit = () => {
);
}
```
+{% endraw %}
## Custom Notification Content
From 12843130fd0f9af09794791c6d32c7c17972f659 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:40:24 +0200
Subject: [PATCH 08/19] Backport #9699
---
docs/AccordionForm.md | 48 +++++++++++++++++++++++--------------------
1 file changed, 26 insertions(+), 22 deletions(-)
diff --git a/docs/AccordionForm.md b/docs/AccordionForm.md
index 277136a8b3e..cdcaf5906e0 100644
--- a/docs/AccordionForm.md
+++ b/docs/AccordionForm.md
@@ -375,15 +375,16 @@ This component renders a [Material UI `` component](https://mui.com/c
Here are all the props you can set on the `` component:
-| Prop | Required | Type | Default | Description |
-| ----------------- | -------- | ----------- | ------- | ------------------------------------------------------------------------------------------------ |
-| `label` | Required | `string` | - | The main label used as the accordion summary. Appears in red when the accordion has errors |
-| `children` | Required | `ReactNode` | - | A list of `` elements |
-| `secondary` | Optional | `string` | - | The secondary label used as the accordion summary |
-| `defaultExpanded` | Optional | `boolean` | `false` | Set to true to have the accordion expanded by default (except if autoClose = true on the parent) |
-| `disabled` | Optional | `boolean` | `false` | If true, the accordion will be displayed in a disabled state. |
-| `square` | Optional | `boolean` | `false` | If true, rounded corners are disabled. |
-| `sx` | Optional | `Object` | - | An object containing the MUI style overrides to apply to the root component. |
+| Prop | Required | Type | Default | Description |
+| ----------------- | -------- | ----------------------- | ------- | ------------------------------------------------------------------------------------------------ |
+| `children` | Required | `ReactNode` | - | A list of `` elements |
+| `defaultExpanded` | Optional | `boolean` | `false` | Set to true to have the accordion expanded by default (except if autoClose = true on the parent) |
+| `disabled` | Optional | `boolean` | `false` | If true, the accordion will be displayed in a disabled state. |
+| `id` | Optional | `string` | - | An id for this Accordion to be used in the [`useFormGroup`](./Upgrade.md#useformgroup-hook-returned-state-has-changed) hook and for CSS classes. |
+| `label` | Required | `string` or `ReactNode` | - | The main label used as the accordion summary. Appears in red when the accordion has errors |
+| `secondary` | Optional | `string` or `ReactNode` | - | The secondary label used as the accordion summary |
+| `square` | Optional | `boolean` | `false` | If true, rounded corners are disabled. |
+| `sx` | Optional | `Object` | - | An object containing the MUI style overrides to apply to the root component. |
```tsx
import {
@@ -413,6 +414,8 @@ const CustomerEdit = () => (
);
```
+**Warning**: To use an `` with the `autoClose` prop and a React node element as a `label`, you **must** specify an `id`.
+
## ``
Renders children (Inputs) inside a Material UI `` element without a Card style. To be used as child of a `` or a `` element.
@@ -430,19 +433,20 @@ Prefer `` to `` to always display a list of imp
Here are all the props you can set on the `` component:
-| Prop | Required | Type | Default | Description |
-| ------------------ | -------- | ----------- | ------- | ------------------------------------------------------------- |
-| `Accordion` | Optional | `Component` | - | The component to use as the accordion. |
-| `AccordionDetails` | Optional | `Component` | - | The component to use as the accordion details. |
-| `AccordionSummary` | Optional | `Component` | - | The component to use as the accordion summary. |
-| `label` | Required | `string` | - | The main label used as the accordion summary. |
-| `children` | Required | `ReactNode` | - | A list of `` elements |
-| `fullWidth` | Optional | `boolean` | `false` | If true, the Accordion takes the entire form width. |
-| `className` | Optional | `string` | - | A class name to style the underlying `` |
-| `secondary` | Optional | `string` | - | The secondary label used as the accordion summary |
-| `defaultExpanded` | Optional | `boolean` | `false` | Set to true to have the accordion expanded by default |
-| `disabled` | Optional | `boolean` | `false` | If true, the accordion will be displayed in a disabled state. |
-| `square` | Optional | `boolean` | `false` | If true, rounded corners are disabled. |
+| Prop | Required | Type | Default | Description |
+| ------------------ | -------- | ----------------------- | ------- | ------------------------------------------------------------- |
+| `Accordion` | Optional | `Component` | - | The component to use as the accordion. |
+| `AccordionDetails` | Optional | `Component` | - | The component to use as the accordion details. |
+| `AccordionSummary` | Optional | `Component` | - | The component to use as the accordion summary. |
+| `children` | Required | `ReactNode` | - | A list of `` elements |
+| `className` | Optional | `string` | - | A class name to style the underlying `` |
+| `defaultExpanded` | Optional | `boolean` | `false` | Set to true to have the accordion expanded by default |
+| `disabled` | Optional | `boolean` | `false` | If true, the accordion will be displayed in a disabled state. |
+| `fullWidth` | Optional | `boolean` | `false` | If true, the Accordion takes the entire form width. |
+| `id` | Optional | `string` | - | An id for this Accordion to be used for CSS classes. |
+| `label` | Required | `string` or `ReactNode` | - | The main label used as the accordion summary. |
+| `secondary` | Optional | `string` or `ReactNode` | - | The secondary label used as the accordion summary |
+| `square` | Optional | `boolean` | `false` | If true, rounded corners are disabled. |
```tsx
import {
From fcf59ed8e84679b01cbe81e002c70fdee0c6488c Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:42:10 +0200
Subject: [PATCH 09/19] Backport #9700
---
docs/FieldsForRelationships.md | 2 ++
docs/ReferenceArrayField.md | 35 +++++++++++-------------
docs/ReferenceField.md | 50 ++++++++++++++++------------------
docs/ReferenceManyField.md | 45 +++++++++---------------------
4 files changed, 55 insertions(+), 77 deletions(-)
diff --git a/docs/FieldsForRelationships.md b/docs/FieldsForRelationships.md
index a0ae4bbb9e4..6664c534bf7 100644
--- a/docs/FieldsForRelationships.md
+++ b/docs/FieldsForRelationships.md
@@ -16,6 +16,8 @@ React-admin handles relationships *regardless of the capacity of the API to mana
React-admin provides helpers to fetch related records, depending on the type of relationship, and how the API implements it.
+
+
## One-To-Many
When one record has many related records, this is called a one-to-many relationship. For instance, if an author has written several books, `authors` has a one-to-many relationship with `books`.
diff --git a/docs/ReferenceArrayField.md b/docs/ReferenceArrayField.md
index 7c28b929399..df5804a09d0 100644
--- a/docs/ReferenceArrayField.md
+++ b/docs/ReferenceArrayField.md
@@ -7,7 +7,15 @@ title: "The ReferenceArrayField Component"
Use `` to display a list of related records, via a one-to-many relationship materialized by an array of foreign keys.
-
+
+
+`` fetches a list of referenced records (using the `dataProvider.getMany()` method), and puts them in a [`ListContext`](./useListContext.md). It then renders each related record, using its [`recordRepresentation`](./Resource.md#recordrepresentation), in a [``](./ChipField.md).
+
+**Tip**: If the relationship is materialized by a foreign key on the referenced resource, use [the `` component](./ReferenceManyField.md) instead.
+
+**Tip**: To edit the records of a one-to-many relationship, use [the `` component](./ReferenceArrayInput.md).
+
+## Usage
For instance, let's consider a model where a `post` has many `tags`, materialized to a `tags_ids` field containing an array of ids:
@@ -35,24 +43,9 @@ A typical `post` record therefore looks like this:
}
```
-In that case, use `` to display the post tags names as follows:
+In that case, use `` to display the post tag names as Chips as follows:
```jsx
-
-```
-
-`` fetches a list of referenced records (using the `dataProvider.getMany()` method), and puts them in a [`ListContext`](./useListContext.md). It then renders each related record, using its [`recordRepresentation`](./Resource.md#recordrepresentation), in a [``](./ChipField.md).
-
-**Tip**: If the relationship is materialized by a foreign key on the referenced resource, use [the `` component](./ReferenceManyField.md) instead.
-
-## Usage
-
-`` expects a `reference` attribute, which specifies the resource to fetch for the related records. It also expects a `source` attribute, which defines the field containing the list of ids to look for in the referenced resource.
-
-For instance, if each post contains a list of tag ids (e.g. `{ id: 1234, title: 'Lorem Ipsum', tag_ids: [1, 23, 4] }`), here is how to fetch the list of tags for each post in a list, and display the `name` for each `tag` in a ``:
-
-```jsx
-import * as React from "react";
import { List, Datagrid, ReferenceArrayField, TextField } from 'react-admin';
export const PostList = () => (
@@ -60,16 +53,20 @@ export const PostList = () => (
-
+
);
```
+
+
+`` expects a `reference` attribute, which specifies the resource to fetch for the related records. It also expects a `source` attribute, which defines the field containing the list of ids to look for in the referenced resource.
+
`` fetches the `tag` resources related to each `post` resource by matching `post.tag_ids` to `tag.id`. By default, it renders one string by related record, via a [``](./SingleFieldList.md) with a [``](./ChipField.md) child using the resource [`recordRepresentation`](./Resource.md#recordrepresentation) as source
-Configure the `` to render related records in a meaningul way. For instance, for the `tags` resource, if you want the `` to display the tag `name`:
+Configure the `` to render related records in a meaningful way. For instance, for the `tags` resource, if you want the `` to display the tag `name`:
```jsx
diff --git a/docs/ReferenceField.md b/docs/ReferenceField.md
index da727062c10..ed82bd81f5d 100644
--- a/docs/ReferenceField.md
+++ b/docs/ReferenceField.md
@@ -7,7 +7,9 @@ title: "The ReferenceField Component"
`` is useful for displaying many-to-one and one-to-one relationships, e.g. the details of a user when rendering a post authored by that user.
-
+
+
+## Usage
For instance, let's consider a model where a `post` has one author from the `users` resource, referenced by a `user_id` field.
@@ -22,18 +24,35 @@ For instance, let's consider a model where a `post` has one author from the `use
└──────────────┘
```
-In that case, use `` to display the post author's id as follows:
+In that case, use `` to display the post author's as follows:
```jsx
-
+import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
+
+export const PostShow = () => (
+
+
+
+
+
+
+
+
+);
```
-`` fetches the data, puts it in a [`RecordContext`](./useRecordContext.md), and renders the [`recordRepresentation`](./Resource.md#recordrepresentation) (the record `id` field by default).
+`` fetches the data, puts it in a [`RecordContext`](./useRecordContext.md), and renders the [`recordRepresentation`](./Resource.md#recordrepresentation) (the record `id` field by default) wrapped in a link to the related user `` page.
+
+
So it's a good idea to configure the `` to render related records in a meaningul way. For instance, for the `users` resource, if you want the `` to display the full name of the author:
```jsx
- `${record.first_name} ${record.last_name}`} />
+ `${record.first_name} ${record.last_name}`}
+/>
```
Alternately, if you pass a child component, `` will render it instead of the `recordRepresentation`. Usual child components for `` are other `` components (e.g. [``](./TextField.md)).
@@ -48,27 +67,6 @@ This component fetches a referenced record (`users` in this example) using the `
It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for performance reasons](#performance). When using several `` in the same page (e.g. in a ``), this allows to call the `dataProvider` once instead of once per row.
-## Usage
-
-Here is how to render both a post and the author name in a show view:
-
-```jsx
-import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
-
-export const PostShow = () => (
-
-
-
-
-
-
-
-
-);
-```
-
-With this configuration, `` wraps the user's name in a link to the related user `` page.
-
## Props
| Prop | Required | Type | Default | Description |
diff --git a/docs/ReferenceManyField.md b/docs/ReferenceManyField.md
index 0ea09616b8a..832edb923d6 100644
--- a/docs/ReferenceManyField.md
+++ b/docs/ReferenceManyField.md
@@ -7,7 +7,15 @@ title: "The ReferenceManyField Component"
`` is useful for displaying a list of related records via a one-to-many relationship, when the foreign key is carried by the referenced resource.
-
+
+
+This component fetches a list of referenced records by a reverse lookup of the current `record.id` in the `target` field of another resource (using the `dataProvider.getManyReference()` REST method), and puts them in a [`ListContext`](./useListContext.md). Its children can then use the data from this context. The most common case is to use [``](./SingleFieldList.md) or [``](./Datagrid.md) as child.
+
+**Tip**: If the relationship is materialized by an array of ids in the initial record, use [the `` component](./ReferenceArrayField.md) instead.
+
+**Tip**: To edit the records of a one-to-many relationship, use [the `` component](./ReferenceManyInput.md).
+
+## Usage
For instance, if an `author` has many `books`, and each book resource exposes an `author_id` field:
@@ -25,12 +33,14 @@ For instance, if an `author` has many `books`, and each book resource exposes an
`` can render the titles of all the books by a given author.
```jsx
+import { Show, SimpleShowLayout, ReferenceManyField, Datagrid, TextField, DateField } from 'react-admin';
+
const AuthorShow = () => (
-
+
@@ -41,42 +51,13 @@ const AuthorShow = () => (
);
```
-This component fetches a list of referenced records by a reverse lookup of the current `record.id` in the `target` field of another resource (using the `dataProvider.getManyReference()` REST method), and puts them in a [`ListContext`](./useListContext.md). Its children can then use the data from this context. The most common case is to use [``](./SingleFieldList.md) or [``](./Datagrid.md) as child.
-
-**Tip**: If the relationship is materialized by an array of ids in the initial record, use [the `` component](./ReferenceArrayField.md) instead.
-
-**Tip**: To edit the records of a one-to-many relationship, use [the `` component](./ReferenceManyInput.md).
-
-## Usage
-
-For instance, here is how to show the title of the books written by a particular author in a show view.
-
-```jsx
-import { Show, SimpleShowLayout, TextField, ReferenceManyField, Datagrid, DateField } from 'react-admin';
-
-export const AuthorShow = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
-```
+
`` accepts a `reference` attribute, which specifies the resource to fetch for the related record. It also accepts a `source` attribute which defines the field containing the value to look for in the `target` field of the referenced resource. By default, this is the `id` of the resource (`authors.id` in the previous example).
You can also use `` in a list, e.g. to display the authors of the comments related to each post in a list by matching `post.id` to `comment.post_id`:
```jsx
-import * as React from "react";
import { List, Datagrid, ChipField, ReferenceManyField, SingleFieldList, TextField } from 'react-admin';
export const PostList = () => (
From a36db79e835433647e4649e00f6d8b5ee956a783 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:42:32 +0200
Subject: [PATCH 10/19] Backport #9709
---
docs/StackedFilters.md | 57 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 57 insertions(+)
diff --git a/docs/StackedFilters.md b/docs/StackedFilters.md
index 65ae010d88e..7b070e4c893 100644
--- a/docs/StackedFilters.md
+++ b/docs/StackedFilters.md
@@ -66,6 +66,8 @@ export const PostListToolbar = () => (
);
```
+You must also update your data provider to support filters with operators. See the [data provider configuration](#data-provider-configuration) section below.
+
## Filters Configuration
Define the filter configuration in the `` prop. The value must be an object defining the operators and UI for each field that can be used as a filter.
@@ -145,6 +147,61 @@ const postListFilters: FiltersConfig = {
};
```
+## Data Provider Configuration
+
+In react-admin, `dataProvider.getList()` accepts a `filter` parameter to filter the records. There is no notion of *operators* in this parameter, as the expected format is an object like `{ field: value }`. As `StackedFilters` needs operators, it uses a convention to concatenate the field name and the operator with an underscore.
+
+For instance, if the Post resource has a `title` field, and you configure `` to allow filtering on this field as a text field, the `dataProvider.getList()` may receive the following `filter` parameter:
+
+- title_eq
+- title_neq
+- title_q
+
+The actual suffixes depend on the type of filter configured in `` (see [filters configuration builders](#filter-configuration-builders) above). Here is an typical call to `dataProvider.getList()` with a posts list using ``:
+
+```jsx
+const { data } = useGetList('posts', {
+ filter: {
+ title_q: 'lorem',
+ date_gte: '2021-01-01',
+ views_eq: 0,
+ tags_inc_any: [1, 2],
+ },
+ pagination: { page: 1, perPage: 10 },
+ sort: { field: 'title', order: 'ASC' },
+});
+```
+
+It's up to your data provider to convert the `filter` parameter into a query that your API understands.
+
+For instance, if your API expects filters as an array of criteria objects (`[{ field, operator, value }]`), `dataProvider.getList()` should convert the `filter` parameter as follows:
+
+```jsx
+const dataProvider = {
+ // ...
+ getList: async (resource, params) => {
+ const { filter } = params;
+ const filterFields = Object.keys(filter);
+ const criteria = [];
+ // eq operator
+ filterFields.filter(field => field.endsWith('_eq')).forEach(field => {
+ criteria.push({ field: field.replace('_eq', ''), operator: 'eq', value: filter[field] });
+ });
+ // neq operator
+ filterFields.filter(field => field.endsWith('_neq')).forEach(field => {
+ criteria.push({ field: field.replace('_neq', ''), operator: 'neq', value: filter[field] });
+ });
+ // q operator
+ filterFields.filter(field => field.endsWith('_q')).forEach(field => {
+ criteria.push({ field: field.replace('_q', ''), operator: 'q', value: filter[field] });
+ });
+ // ...
+ },
+}
+```
+
+Few of the [existing data providers](./DataProviderList.md) implement this convention. this means you'll probably have to adapt your data provider to support the operators used by ``.
+
## Props
| Prop | Required | Type | Default | Description |
From 00716b2c29577c482f74392591e8d1dc5ab52582 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:43:57 +0200
Subject: [PATCH 11/19] Backport #9712
---
.../src/input/AutocompleteArrayInput.spec.tsx | 2 +-
.../src/input/AutocompleteInput.spec.tsx | 491 +++++++++++-------
.../src/input/AutocompleteInput.stories.tsx | 17 +-
.../src/input/AutocompleteInput.tsx | 17 +-
4 files changed, 342 insertions(+), 185 deletions(-)
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.spec.tsx
index 20c0ed399d9..64aabcbd1b8 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.spec.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.spec.tsx
@@ -833,7 +833,7 @@ describe('', () => {
selector: 'input',
})
);
- expect(screen.queryAllByRole('option')).toHaveLength(2);
+ expect(screen.queryAllByRole('option')).toHaveLength(3);
expect(screen.getByText('Technical')).not.toBeNull();
expect(screen.getByText('Programming')).not.toBeNull();
});
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx
index fd8d43a71e6..42a3ca97794 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx
@@ -1052,198 +1052,341 @@ describe('', () => {
});
});
- it('should support creation of a new choice through the onCreate event', async () => {
- const choices = [
- { id: 'ang', name: 'Angular' },
- { id: 'rea', name: 'React' },
- ];
- const handleCreate = filter => {
- const newChoice = {
- id: 'js_fatigue',
- name: filter,
+ describe('onCreate', () => {
+ it('should include an option with the createLabel when the input is empty', async () => {
+ const choices = [
+ { id: 'ang', name: 'Angular' },
+ { id: 'rea', name: 'React' },
+ ];
+ const handleCreate = filter => {
+ const newChoice = {
+ id: 'js_fatigue',
+ name: filter,
+ };
+ choices.push(newChoice);
+ return newChoice;
};
- choices.push(newChoice);
- return newChoice;
- };
- const { rerender } = render(
-
-
-
-
-
- );
+ render(
+
+
+
+
+
+ );
- const input = screen.getByLabelText(
- 'resources.posts.fields.language'
- ) as HTMLInputElement;
- input.focus();
- fireEvent.change(input, { target: { value: 'New Kid On The Block' } });
- fireEvent.click(screen.getByText('ra.action.create_item'));
- await new Promise(resolve => setTimeout(resolve));
- rerender(
-
-
-
-
-
- );
- expect(
- screen.queryByDisplayValue('New Kid On The Block')
- ).not.toBeNull();
- fireEvent.click(screen.getByLabelText('ra.action.clear_input_value'));
- fireEvent.blur(input);
- fireEvent.focus(input);
- expect(screen.queryByText('New Kid On The Block')).not.toBeNull();
- });
+ const input = screen.getByLabelText(
+ 'resources.posts.fields.language'
+ ) as HTMLInputElement;
+ input.focus();
+ fireEvent.change(input, {
+ target: { value: '' },
+ });
- it('should support creation of a new choice through the onCreate event with a promise', async () => {
- const choices = [
- { id: 'ang', name: 'Angular' },
- { id: 'rea', name: 'React' },
- ];
- const handleCreate = filter => {
- return new Promise(resolve => {
+ expect(screen.queryByText('ra.action.create')).not.toBeNull();
+ expect(screen.queryByText('ra.action.create_item')).toBeNull();
+ });
+ it('should include an option with the createItemLabel when the input not empty', async () => {
+ const choices = [
+ { id: 'ang', name: 'Angular' },
+ { id: 'rea', name: 'React' },
+ ];
+ const handleCreate = filter => {
const newChoice = {
id: 'js_fatigue',
name: filter,
};
choices.push(newChoice);
- setTimeout(() => resolve(newChoice), 100);
- });
- };
+ return newChoice;
+ };
- const { rerender } = render(
-
-
-
-
-
- );
+ render(
+
+
+
+
+
+ );
- const input = screen.getByLabelText(
- 'resources.posts.fields.language'
- ) as HTMLInputElement;
- input.focus();
- fireEvent.change(input, { target: { value: 'New Kid On The Block' } });
- fireEvent.click(screen.getByText('ra.action.create_item'));
- await new Promise(resolve => setTimeout(resolve, 100));
- rerender(
-
-
-
-
-
- );
- expect(
- screen.queryByDisplayValue('New Kid On The Block')
- ).not.toBeNull();
- fireEvent.click(screen.getByLabelText('ra.action.clear_input_value'));
- fireEvent.blur(input);
- fireEvent.focus(input);
- expect(screen.queryByText('New Kid On The Block')).not.toBeNull();
- });
+ const input = screen.getByLabelText(
+ 'resources.posts.fields.language'
+ ) as HTMLInputElement;
+ input.focus();
+ fireEvent.change(input, {
+ target: { value: 'foo' },
+ });
+
+ expect(screen.queryByText('ra.action.create')).toBeNull();
+ expect(screen.queryByText('ra.action.create_item')).not.toBeNull();
+ });
+ it('should not include a create option when the input matches an option', async () => {
+ const choices = [
+ { id: 'ang', name: 'Angular' },
+ { id: 'rea', name: 'React' },
+ ];
+ const handleCreate = filter => {
+ const newChoice = {
+ id: 'js_fatigue',
+ name: filter,
+ };
+ choices.push(newChoice);
+ return newChoice;
+ };
- it('should support creation of a new choice through the create element', async () => {
- const choices = [
- { id: 'ang', name: 'Angular' },
- { id: 'rea', name: 'React' },
- ];
- const newChoice = { id: 'js_fatigue', name: 'New Kid On The Block' };
+ render(
+
+
+
+
+
+ );
- const Create = () => {
- const context = useCreateSuggestionContext();
- const handleClick = () => {
+ const input = screen.getByLabelText(
+ 'resources.posts.fields.language'
+ ) as HTMLInputElement;
+ input.focus();
+ fireEvent.change(input, {
+ target: { value: 'React' },
+ });
+ expect(screen.queryByText('ra.action.create')).toBeNull();
+ expect(screen.queryByText('ra.action.create_item')).toBeNull();
+ });
+ it('should allow the creation of a new choice', async () => {
+ const choices = [
+ { id: 'ang', name: 'Angular' },
+ { id: 'rea', name: 'React' },
+ ];
+ const handleCreate = filter => {
+ const newChoice = {
+ id: 'js_fatigue',
+ name: filter,
+ };
choices.push(newChoice);
- context.onCreate(newChoice);
+ return newChoice;
};
- return ;
- };
+ const { rerender } = render(
+
+
+
+
+
+ );
- const { rerender } = render(
-
-
- }
- />
-
-
- );
+ const input = screen.getByLabelText(
+ 'resources.posts.fields.language'
+ ) as HTMLInputElement;
+ input.focus();
+ fireEvent.change(input, {
+ target: { value: 'New Kid On The Block' },
+ });
+ fireEvent.click(screen.getByText('ra.action.create_item'));
+ await new Promise(resolve => setTimeout(resolve));
+ rerender(
+
+
+
+
+
+ );
+ expect(
+ screen.queryByDisplayValue('New Kid On The Block')
+ ).not.toBeNull();
+ fireEvent.click(
+ screen.getByLabelText('ra.action.clear_input_value')
+ );
+ fireEvent.blur(input);
+ fireEvent.focus(input);
+ expect(screen.queryByText('New Kid On The Block')).not.toBeNull();
+ });
- const input = screen.getByLabelText(
- 'resources.posts.fields.language'
- ) as HTMLInputElement;
- fireEvent.change(input, { target: { value: 'New Kid On The Block' } });
- fireEvent.click(screen.getByText('ra.action.create_item'));
- fireEvent.click(screen.getByText('Get the kid'));
- await new Promise(resolve => setTimeout(resolve));
- rerender(
-
-
- }
- />
-
-
- );
- expect(
- screen.queryByDisplayValue('New Kid On The Block')
- ).not.toBeNull();
- fireEvent.click(screen.getByLabelText('ra.action.clear_input_value'));
- fireEvent.blur(input);
- fireEvent.focus(input);
- expect(screen.queryByText('New Kid On The Block')).not.toBeNull();
+ it('should allow the creation of a new choice with a promise', async () => {
+ const choices = [
+ { id: 'ang', name: 'Angular' },
+ { id: 'rea', name: 'React' },
+ ];
+ const handleCreate = filter => {
+ return new Promise(resolve => {
+ const newChoice = {
+ id: 'js_fatigue',
+ name: filter,
+ };
+ choices.push(newChoice);
+ setTimeout(() => resolve(newChoice), 100);
+ });
+ };
+
+ const { rerender } = render(
+
+
+
+
+
+ );
+
+ const input = screen.getByLabelText(
+ 'resources.posts.fields.language'
+ ) as HTMLInputElement;
+ input.focus();
+ fireEvent.change(input, {
+ target: { value: 'New Kid On The Block' },
+ });
+ fireEvent.click(screen.getByText('ra.action.create_item'));
+ await new Promise(resolve => setTimeout(resolve, 100));
+ rerender(
+
+
+
+
+
+ );
+ expect(
+ screen.queryByDisplayValue('New Kid On The Block')
+ ).not.toBeNull();
+ fireEvent.click(
+ screen.getByLabelText('ra.action.clear_input_value')
+ );
+ fireEvent.blur(input);
+ fireEvent.focus(input);
+ expect(screen.queryByText('New Kid On The Block')).not.toBeNull();
+ });
+ });
+ describe('create', () => {
+ it('should allow the creation of a new choice', async () => {
+ const choices = [
+ { id: 'ang', name: 'Angular' },
+ { id: 'rea', name: 'React' },
+ ];
+ const newChoice = {
+ id: 'js_fatigue',
+ name: 'New Kid On The Block',
+ };
+
+ const Create = () => {
+ const context = useCreateSuggestionContext();
+ const handleClick = () => {
+ choices.push(newChoice);
+ context.onCreate(newChoice);
+ };
+
+ return ;
+ };
+
+ const { rerender } = render(
+
+
+ }
+ />
+
+
+ );
+
+ const input = screen.getByLabelText(
+ 'resources.posts.fields.language'
+ ) as HTMLInputElement;
+ fireEvent.change(input, {
+ target: { value: 'New Kid On The Block' },
+ });
+ fireEvent.click(screen.getByText('ra.action.create_item'));
+ fireEvent.click(screen.getByText('Get the kid'));
+ await new Promise(resolve => setTimeout(resolve));
+ rerender(
+
+
+ }
+ />
+
+
+ );
+ expect(
+ screen.queryByDisplayValue('New Kid On The Block')
+ ).not.toBeNull();
+ fireEvent.click(
+ screen.getByLabelText('ra.action.clear_input_value')
+ );
+ fireEvent.blur(input);
+ fireEvent.focus(input);
+ expect(screen.queryByText('New Kid On The Block')).not.toBeNull();
+ });
});
it('should return null when no choice is selected', async () => {
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx
index 1e29cd9cd89..a7044798416 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx
@@ -299,12 +299,23 @@ export const CreateLabel = () => (
{
- if (filter) {
+ const newAuthorName = window.prompt(
+ 'Enter a new author',
+ filter
+ );
+
+ if (newAuthorName) {
const newAuthor = {
id: choicesForCreationSupport.length + 1,
- name: filter,
+ name: newAuthorName,
};
choicesForCreationSupport.push(newAuthor);
return newAuthor;
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
index 65a0519fb18..54ed76562b7 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
@@ -509,13 +509,16 @@ If you provided a React element for the optionText prop, you must also provide t
// add create option if necessary
const { inputValue } = params;
- // FIXME pass the allowCreate: true option to useCreateSuggestions instead
- if (
- (onCreate || create) &&
- inputValue !== '' &&
- !doesQueryMatchSuggestion(filterValue)
- ) {
- filteredOptions = filteredOptions.concat(getCreateItem(inputValue));
+ if (onCreate || create) {
+ if (inputValue === '') {
+ // create option with createLabel
+ filteredOptions = filteredOptions.concat(getCreateItem(''));
+ } else if (!doesQueryMatchSuggestion(filterValue)) {
+ filteredOptions = filteredOptions.concat(
+ // create option with createItemLabel
+ getCreateItem(inputValue)
+ );
+ }
}
return filteredOptions;
From a4d653c51ced71f8ce186a6ec5d19ab358e3c6be Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:44:19 +0200
Subject: [PATCH 12/19] Backport #9716
---
docs/Layout.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/Layout.md b/docs/Layout.md
index 78ed2511c82..c340feb8d0b 100644
--- a/docs/Layout.md
+++ b/docs/Layout.md
@@ -369,7 +369,7 @@ This property accepts the following subclasses:
To override the style of `` using the [application-wide style overrides](./AppTheme.md#theming-individual-components), use the `RaLayout` key.
-**Tip**: If you need to override global styles (like the default font size or family), you should [write a custom theme](./AppTheme.md) rather than override the `` prop. And if you need to tweak the default layout to add a right column or move the menu to the top, you're probably better off [writing your own layout component](./Layout.md#writing-a-layout-from-scratch).
+**Tip**: If you need to override global styles (like the default font size or family), you should [write a custom theme](./AppTheme.md#writing-a-custom-theme) rather than override the `` prop. And if you need to tweak the default layout to add a right column or move the menu to the top, you're probably better off [writing your own layout component](./Layout.md#writing-a-layout-from-scratch).
## Adding A Custom Context
From 3b34f76a2f6404384e2c664125b814e85d5071b9 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 15:44:33 +0200
Subject: [PATCH 13/19] Backport #9717
---
packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx
index 4f55b89f48c..c7a681dbebd 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx
@@ -81,7 +81,7 @@ export const AutocompleteArrayInput = <
{...props}
multiple
- defaultValue={defaultValue ?? []}
+ defaultValue={defaultValue ?? (props.disabled ? undefined : [])}
/>
);
From 398508d0ea34b51f902f70d0c558cd39e4242360 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 16:04:26 +0200
Subject: [PATCH 14/19] Backport #9718
---
.../src/input/AutocompleteArrayInput.spec.tsx | 7 +++++--
packages/ra-ui-materialui/src/input/AutocompleteInput.tsx | 1 -
.../ra-ui-materialui/src/list/filter/FilterButton.spec.tsx | 7 ++++---
3 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.spec.tsx
index 64aabcbd1b8..3f0d228df0d 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.spec.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.spec.tsx
@@ -880,7 +880,9 @@ describe('', () => {
});
expect(screen.getByText('Technical')).not.toBeNull();
expect(screen.getByText('Programming')).not.toBeNull();
- expect(screen.getByText('ra.action.create_item')).not.toBeNull();
+ await waitFor(() => {
+ expect(screen.getByText('ra.action.create_item')).not.toBeNull();
+ });
});
it('should support creation of a new choice through the onCreate event when optionText is a function', async () => {
@@ -1108,8 +1110,9 @@ describe('', () => {
]
);
});
+ screen.getByRole('combobox').blur();
expect(screen.getByDisplayValue('Russian')).not.toBeNull();
- screen.getAllByRole('combobox')[0].focus();
+ screen.getByRole('combobox').focus();
fireEvent.click(await screen.findByText('Victor Hugo'));
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
index 54ed76562b7..88910a97f75 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
@@ -557,7 +557,6 @@ If you provided a React element for the optionText prop, you must also provide t
return (
<>
', () => {
render();
// Open Posts List
- fireEvent.click(await screen.findByText('Posts'));
+ userEvent.click(await screen.findByText('Posts'));
await waitFor(() => {
expect(screen.queryAllByRole('checkbox')).toHaveLength(11);
});
- fireEvent.click(await screen.findByLabelText('Open'));
- fireEvent.click(await screen.findByText('Sint...'));
+ userEvent.click(await screen.findByLabelText('Open'));
+ userEvent.click(await screen.findByText('Sint...'));
await screen.findByLabelText('Add filter');
await waitFor(
From 1fda0977d80fa3d3e62843be5e07e103f24bf15f Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 16:04:44 +0200
Subject: [PATCH 15/19] Backport #9720
---
docs/ReferenceManyToManyInput.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/ReferenceManyToManyInput.md b/docs/ReferenceManyToManyInput.md
index 60d4db34047..89a72d0cbd1 100644
--- a/docs/ReferenceManyToManyInput.md
+++ b/docs/ReferenceManyToManyInput.md
@@ -96,6 +96,7 @@ const BandEdit = () => (
);
```
+**Limitation**: `` cannot be used to filter a list.
## Props
From b0d9ea4f4ce6ee5236ad2070dcefaf927ec97df9 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 16:13:28 +0200
Subject: [PATCH 16/19] Fix ArrayInput tests
---
.../src/input/ArrayInput/ArrayInput.spec.tsx | 34 +++++++++++--------
1 file changed, 20 insertions(+), 14 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 17db8f2c750..996d7d5174e 100644
--- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx
+++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx
@@ -257,7 +257,7 @@ describe('', () => {
setArrayInputVisible = setVisible;
return visible ? (
-
+
@@ -268,18 +268,22 @@ describe('', () => {
render(
- ({ arr: [{ foo: 'Must be "baz"' }, {}] })}
- >
-
-
+
+ ({
+ arr: [{ foo: 'Must be "baz"' }, {}],
+ })}
+ >
+
+
+
);
@@ -295,7 +299,9 @@ describe('', () => {
await screen.findByText('Must be "baz"');
setArrayInputVisible(false);
- expect(screen.queryByText('Must be "baz"')).toBeNull();
+ await waitFor(() => {
+ expect(screen.queryByText('Must be "baz"')).toBeNull();
+ });
// ensure errors are still there after re-mount
setArrayInputVisible(true);
From 02c877a614bb14e5e3be7ef5f5cc83f4442760f8 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 16:30:49 +0200
Subject: [PATCH 17/19] Fix FilterButton tests
---
.../src/list/filter/FilterButton.spec.tsx | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx
index ac00ea226f9..8bd413991dc 100644
--- a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx
+++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx
@@ -245,21 +245,22 @@ describe('', () => {
userEvent.click(await screen.findByText('Sint...'));
await screen.findByLabelText('Add filter');
+ expect(screen.queryAllByText('Close')).toHaveLength(0);
await waitFor(
() => {
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
},
{ timeout: 10000 }
);
- fireEvent.click(screen.getByLabelText('Add filter'));
- fireEvent.click(await screen.findByText('Remove all filters'));
+ userEvent.click(screen.getByLabelText('Add filter'));
+ userEvent.click(await screen.findByText('Remove all filters'));
await waitFor(() => {
expect(screen.getAllByRole('checkbox')).toHaveLength(11);
});
- fireEvent.click(await screen.findByLabelText('Open'));
- fireEvent.click(await screen.findByText('Sint...'));
+ userEvent.click(await screen.findByLabelText('Open'));
+ userEvent.click(await screen.findByText('Sint...'));
await waitFor(
() => {
From f4f47a7619247c19ba4c0760c739b78906b29268 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 16:37:20 +0200
Subject: [PATCH 18/19] Fix FilterButton tests timeout
---
.../src/list/filter/FilterButton.spec.tsx | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx
index 8bd413991dc..7fefe4386db 100644
--- a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx
+++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx
@@ -255,9 +255,12 @@ describe('', () => {
userEvent.click(screen.getByLabelText('Add filter'));
userEvent.click(await screen.findByText('Remove all filters'));
- await waitFor(() => {
- expect(screen.getAllByRole('checkbox')).toHaveLength(11);
- });
+ await waitFor(
+ () => {
+ expect(screen.getAllByRole('checkbox')).toHaveLength(11);
+ },
+ { timeout: 10000 }
+ );
userEvent.click(await screen.findByLabelText('Open'));
userEvent.click(await screen.findByText('Sint...'));
From 25621201d0163db4a274d7f58f5f3bacb8cafe9d Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 24 Jun 2024 16:43:44 +0200
Subject: [PATCH 19/19] Fix FilterButton tests timeout last expect
---
packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx
index 7fefe4386db..ebffd985ea4 100644
--- a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx
+++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx
@@ -269,7 +269,7 @@ describe('', () => {
() => {
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
},
- { timeout: 4000 }
+ { timeout: 10000 }
);
expect(screen.queryByText('Save current query...')).toBeNull();