Tiamon
Tiamon

Reputation: 237

React-Admin <ImageInput> to upload images to rails api

I am trying to upload images from react-admin to rails api backend using active storage.

In the documentation of react-admin it says: "Note that the image upload returns a 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" I am trying to send it as a multi-part form.

I have been reading here and there but I can not find what I want, at least a roadmap of how I should proceed.

Upvotes: 3

Views: 7582

Answers (2)

Robert-Jan Kuyper
Robert-Jan Kuyper

Reputation: 3306

When using files, use a multi-part form in the react front-end and for example multer in your API backend.

In react-admin you should create a custom dataProvider and extend either the default or built a custom one. Per implementation you should handle the file/files upload. For uploading a file or files from your custom dataprovider in react-admin:

// dataProvider.js
// this is only the implementation for a create

case "CREATE":
  const formData = new FormData();

  for ( const param in params.data ) {

    // 1 file
    if (param === 'file') {
      formData.append('file', params.data[param].rawFile);
      continue
    }

    // when using multiple files
    if (param === 'files') {
      params.data[param].forEach(file => {
        formData.append('files', file.rawFile);
      });
      continue
    }

    formData.append(param, params.data[param]);
  }

  return httpClient(`myendpoint.com/upload`, {
    method: "POST",
    body: formData,
  }).then(({ json }) => ({ data: json });

From there you pick it up in your API using multer, that supports multi-part forms out-of-the-box. When using nestjs that could look like:

import {
  Controller,
  Post,
  Header,
  UseInterceptors,
  UploadedFile,
} from "@nestjs/common";
import { FileInterceptor } from '@nestjs/platform-express'

@Controller("upload")
export class UploadController {

  @Post()
  @Header("Content-Type", "application/json")
  // multer extracts file from the request body 
  @UseInterceptors(FileInterceptor('file'))
  async uploadFile(
    @UploadedFile() file : Record<any, any>
  ) {
    console.log({ file })
  }
}

Upvotes: 1

Gildas Garcia
Gildas Garcia

Reputation: 7066

You can actually find an example in the dataProvider section of the documentation.

You have to decorate your dataProvider to enable the data upload. Here is the example of transforming the images into base64 strings before posting the resource:

// in addUploadFeature.js
/**
 * Convert a `File` object returned by the upload input into a base 64 string.
 * That's not the most optimized way to store images in production, but it's
 * enough to illustrate the idea of data provider decoration.
 */
const convertFileToBase64 = file => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file.rawFile);

    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
});

/**
 * For posts update only, convert uploaded image in base 64 and attach it to
 * the `picture` sent property, with `src` and `title` attributes.
 */
const addUploadFeature = requestHandler => (type, resource, params) => {
    if (type === 'UPDATE' && resource === 'posts') {
        // notice that following condition can be true only when `<ImageInput source="pictures" />` component has parameter `multiple={true}`
        // if parameter `multiple` is false, then data.pictures is not an array, but single object
        if (params.data.pictures && params.data.pictures.length) {
            // only freshly dropped pictures are instance of File
            const formerPictures = params.data.pictures.filter(p => !(p.rawFile instanceof File));
            const newPictures = params.data.pictures.filter(p => p.rawFile instanceof File);

            return Promise.all(newPictures.map(convertFileToBase64))
                .then(base64Pictures => base64Pictures.map((picture64, index) => ({
                    src: picture64,
                    title: `${newPictures[index].title}`,
                })))
                .then(transformedNewPictures => requestHandler(type, resource, {
                    ...params,
                    data: {
                        ...params.data,
                        pictures: [...transformedNewPictures, ...formerPictures],
                    },
                }));
        }
    }
    // for other request types and resources, fall back to the default request handler
    return requestHandler(type, resource, params);
};

export default addUploadFeature;

You can then apply this on your dataProvider:

// in dataProvider.js
import simpleRestProvider from 'ra-data-simple-rest';
import addUploadFeature from './addUploadFeature';

const dataProvider = simpleRestProvider('http://path.to.my.api/');
const uploadCapableDataProvider = addUploadFeature(dataProvider);

export default uploadCapableDataProvider;

Finally, you can use it in your admin as usual:

// in App.js
import { Admin, Resource } from 'react-admin';

import dataProvider from './dataProvider';
import PostList from './posts/PostList';

const App = () => (
    <Admin dataProvider={uploadCapableDataProvider}>
        <Resource name="posts" list={PostList} />
    </Admin>
);

Upvotes: 4

Related Questions