diff --git a/docs/FileInput.md b/docs/FileInput.md index 4184a7fc4f1..5b98a34ce49 100644 --- a/docs/FileInput.md +++ b/docs/FileInput.md @@ -21,6 +21,7 @@ title: "The FileInput Component" | `labelSingle` | Optional | `string` | 'ra.input.file. upload_single' | Invite displayed in the drop zone if the input accepts one file | | `labelMultiple` | Optional | `string` | 'ra.input.file. upload_several' | Invite displayed in the drop zone if the input accepts several files | | `placeholder` | Optional | `ReactNode` | - | Invite displayed in the drop zone, overrides `labelSingle` and `labelMultiple` | +| `validateFileRemoval` | Optional | `Function` | - | Validate removing items. This is able to cancel to remove the file's list item by onRemove handler when you want to do so by throwing Error. `(file) => void \| Promise` | | `options` | Optional | `Object` | `{}` | Additional options passed to react-dropzone's `useDropzone()` hook. See [the react-dropzone source](https://github.com/react-dropzone/react-dropzone/blob/master/src/index.js) for details . | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -53,6 +54,60 @@ If the default Dropzone label doesn't fit with your need, you can pass a `placeh Note that the file upload returns a [File](https://developer.mozilla.org/en/docs/Web/API/File) object. It is your responsibility to handle it depending on your API behavior. You can for instance encode it in base64, or send it as a multi-part form data. Check [this example](./DataProviders.md#handling-file-uploads) for base64 encoding data by extending the REST Client. +The `validateFileRemoval` handler can interrupt removing visual items in your page. Given if you want to remove immediately file so call your api request in `validateFileRemoval`, then the request fails and throws error, These items don't disapper in the page. + +The `validateFileRemoval` can also be used to confirm the deletion of items to users. The following is an example. + + +```jsx +function Edit(props) { + const [removeImageConfirmEvent, setRemoveFileConfirmEvent] = useState(null); + const [isRemoveImageModalOpen, setIsRemoveImageModalOpen] = useState(false); + return ( + + + { + const promise = new Promise((_resolve, reject) => { + setRemoveFileConfirmEvent({ + fileName: `Image ID: ${file.id}`, + resolve: async (result) => { + await YourApi.deleteImages({ ids: [file.id] }); + return _resolve(result); + }, + reject, + }); + }); + setIsRemoveImageModalOpen(true); + return promise.then((result) => { + // Success Action if you want + }); + }} + /> + { + setIsRemoveImageModalOpen(false); + removeImageConfirmEvent && removeImageConfirmEvent.resolve(); + }} + onCancel={() => { + setIsRemoveImageModalOpen(false); + removeImageConfirmEvent && removeImageConfirmEvent.reject(); + }} + /> + + + ) +} +``` + +This example assumes that it can show some confirm modal has two buttons, to submit and cancel, when clicking a FileInput delete button icon. Then it interrupts to remove items in the page if "YourApi.deleteImages" fails or cancel button is clicked though when succeeding to submit they are removed. + + ## `sx`: CSS API The `` component accepts the usual `className` prop. You can also override many styles of the inner components thanks to the `sx` property (as most Material UI components, see their [documentation about it](https://mui.com/customization/how-to-customize/#overriding-nested-component-styles)). This property accepts the following subclasses: diff --git a/docs/Inputs.md b/docs/Inputs.md index 2d10a0660dc..99372b62719 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -78,7 +78,7 @@ Then you can display a text input to edit the author first name as follows: **Tip**: For compatibility reasons, input components also accept the `defaultValue` prop - which is simply copied as the `initialValue` prop. -## Recipes +## Recipes ### Transforming Input Value to/from Record @@ -124,7 +124,7 @@ const dateParser = v => { Edition forms often contain linked inputs, e.g. country and city (the choices of the latter depending on the value of the former). -React-admin relies on [react-final-form](https://final-form.org/docs/react-final-form/getting-started) for form handling. You can grab the current form values using react-final-form [useFormState](https://final-form.org/docs/react-final-form/api/useFormState) hook. +React-admin relies on [react-final-form](https://final-form.org/docs/react-final-form/getting-started) for form handling. You can grab the current form values using react-final-form [useFormState](https://final-form.org/docs/react-final-form/api/useFormState) hook. ```jsx import * as React from 'react'; diff --git a/packages/ra-ui-materialui/src/input/FileInput.spec.tsx b/packages/ra-ui-materialui/src/input/FileInput.spec.tsx index dc5a30d9e18..c8640d7965d 100644 --- a/packages/ra-ui-materialui/src/input/FileInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/FileInput.spec.tsx @@ -1,7 +1,11 @@ import * as React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { + render, + fireEvent, + waitForElementToBeRemoved, + waitFor, +} from '@testing-library/react'; import { Form } from 'react-final-form'; - import { FileField, ImageField } from '../field'; import { FileInput } from './FileInput'; @@ -247,6 +251,168 @@ describe('', () => { }); }); + describe('should call validateFileRemoval on removal to allow developers to conditionally prevent the removal', () => { + it('normal function', async () => { + const onSubmit = jest.fn(); + + const { getByLabelText, getByTitle } = render( +
( + + { + throw Error('Cancel Removal Action'); + }} + > + + +