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.
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>
);
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 })
}
}
Related
I'm using sanity cms for an application, and I have some documents stored on db. Which query should I use the create another document in the database from my frontend?
//Create a client file (for example, in scr folder in React) and add where appropriate
//In my case, I have a token and project Id in a.env file
import sanityClient from '#sanity/client';
import imageUrlBulder from '#sanity/image-url';
export const client = sanityClient({
projectId: process.env.REACT_APP_SANITY_PROJECT_ID,
dataset:'production',
apiVersion: '2020-10-21',
useCdn:true,
token: process.env.REACT_APP_SANITY_API_TOKEN,
});
const builder = imageUrlBulder(client);
export const urlFor = (source) => builder.image(source);
//in the other file include these lines
import { client } from '../client';
//these fields should match your schema declaration
const doc = {
_id: "yout_id",
_type: 'your_doc_type',
userName:name,
image: imageUrl,
}
client.createIfNotExists(doc)
.then(() => {
console.log("document created")
})
.catch(console.error);
check this video for more
I'm trying to use the customDataProvider example provided by react-admin's documentation
//dataProvider.js
import simpleRestProvider from 'ra-data-simple-rest'
const dataProvider = simpleRestProvider(process.env.REACT_APP_API);
export const myDataProvider = {
...dataProvider,
update: (resource, params) => {
if (resource !== 'recipes') {
// fallback to the default implementation
return dataProvider.update(resource, params);
}
/**
* For posts update only, convert uploaded image in base 64 and attach it to
* the `picture` sent property, with `src` and `title` attributes.
*/
// Freshly dropped pictures are File objects and must be converted to base64 strings
const newPictures = params.data.pictures.filter(
p => p.rawFile instanceof File
);
const formerPictures = params.data.pictures.filter(
p => !(p.rawFile instanceof File)
);
return Promise.all(newPictures.map(convertFileToBase64))
.then(base64Pictures =>
base64Pictures.map(picture64 => ({
src: picture64,
title: `${params.data.title}`,
}))
)
.then(transformedNewPictures =>
dataProvider.update(resource, {
data: {
...params.data,
pictures: [
...transformedNewPictures,
...formerPictures,
],
},
})
);
},
};
/**
* 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.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file.rawFile);
});
With this in the page / component
//recipe.js snippet
<ImageInput source="pictures" accept="image/*" multiple="true">
<ImageField source="src" title="title"/>
</ImageInput>
but the image title is showing as undefined in the PUT request body
I have also lost the ID of the item from the URL, this also shows as undefined in the PUT URL
Where am I going wrong?
Hosted remix app with supabase as db on netlify. Is there a way to generate pdf document using remix ?
Remix has a feature called Resource Routes which let you create endpoints returning anything.
Using them, you could return a Response with a PDF, how to generate the PDF will depend on what libraries you are using, if you use something like React PDF you could do something like this:
// routes/pdf.tsx
import { renderToStream } from "#react-pdf/renderer";
// this is your PDF document component created with React PDF
import { PDFDocument } from "~/components/pdf";
import type { LoaderFunction } from "remix";
export let loader: LoaderFunction = async ({ request, params }) => {
// you can get any data you need to generate the PDF inside the loader
// however you want, e.g. fetch an API or query a DB or read the FS
let data = await getDataForThePDFSomehow({ request, params });
// render the PDF as a stream so you do it async
let stream = await renderToStream(<PDFDocument {...data} />);
// and transform it to a Buffer to send in the Response
let body: Buffer = await new Promise((resolve, reject) => {
let buffers: Uint8Array[] = [];
stream.on("data", (data) => {
buffers.push(data);
});
stream.on("end", () => {
resolve(Buffer.concat(buffers));
});
stream.on("error", reject);
});
// finally create the Response with the correct Content-Type header for
// a PDF
let headers = new Headers({ "Content-Type": "application/pdf" });
return new Response(body, { status: 200, headers });
}
Now when the user goes to /pdf it will get the PDF file back, you could also use an iframe to show it on the HTML.
If you don't use React PDF, change the render part to use the library you are using, and keep the headers and the Response creation part.
I am using react-admin to display my data in a simple admin panel. I have a “timeAdded” field that I want to display in the <List> but I don’t want it sent in the payload when I <Edit> (My api does not accept patch requests that try and patch the “timeAdded” field).
Is there any way to remove the fields/params I don’t want sent in the payload when using the <Edit> component?
(I can share code if needed)
Yes, you have to change the data before sending it, in your dataProvider. The dataProvider documentation shows how to do that:
import simpleRestProvider from 'ra-data-simple-rest';
const dataProvider = simpleRestProvider('http://path.to.my.api/');
const myDataProvider = {
...dataProvider,
update: (resource, params) => {
if (resource !== 'posts' || !params.data.pictures) {
// fallback to the default implementation
return dataProvider.update(resource, params);
}
/**
* For posts update only, remove timeAdded field
*/
const { data: { timeAdded, ...restData }, ...rest } = params;
return dataProvider.update(resource, { data: restData, ...rest });
}),
};
If modifying the data provider is not possible or harder than the documentation example, I would also recommend checking out the transform feature.
It is described here : https://marmelab.com/react-admin/CreateEdit.html#transform and an example is given here:https://marmelab.com/react-admin/CreateEdit.html#altering-the-form-values-before-submitting
It could look something like this:
export const WhatEver = (props) => {
const transform = (data) => {
if(data.timeAdded) {
delete data.timeAdded;
}
return data
};
return (
<Edit {...props} transform={transform} >
...
</Edit>
);
}
In my Nuxt project I have a file named "apiAccess.js" in the root folder. This file simply exports a bunch of functions that make Ajax calls to the server API. This file is imported in any page that needs access to the server API. I need to send a JWT token with each of these api requests, and I have stored that token in the Vuex store.
I need to access the JWT token from the Vuex store within this "apiAccess.js" file. Unfortuntaely, this.$store is not recognized within this file. How do I access the Vuex store from within this file? Or should I have done something differently?
Here's a snippet from the apiAccessjs file where I try to access the store:
import axios from 'axios'
const client = axios.create({
baseURL: 'http://localhost:3000/api',
json: true,
headers: { Authorization: 'Bearer' + this.$store.state.auth.token }
})
After i readed this post i used this generic structure:
// generic actions file
import {
SET_DATA_CONTEXT,
SET_ITEM_CONTEXT
} from '#/types/mutations'
// PAGEACTIONS
export const getDataContext = api => async function ({ commit }) {
const data = await this[api].get()
commit(SET_DATA_CONTEXT, data)
}
export const getItemContext = api => async function ({ commit }, id) {
const data = await this[api].getById(id)
commit(SET_ITEM_CONTEXT, data)
}
export const createItemContext = api => async function ({}, form) {
await this[api].create(form)
}
export const updateItemContext = api => async function ({}, form) {
await this[api].update(form)
}
export const deleteItemContext = api => async function ({}, id) {
await this[api].delete(id)
}
and for any store i used actions from my generic file:
// any store file
import {
getDataContext,
getItemContext,
createItemContext,
updateItemContext,
deleteItemContext,
setDynamicModal
} from '#/use/store.actions'
const API = '$rasterLayerAPI'
export const state = () => ({
dataContext: [],
itemContext: {},
})
export const actions = {
createItemContext: createItemContext(API),
getDataContext: getDataContext(API),
getItemContext: getItemContext(API),
updateItemContext: updateItemContext(API),
deleteItemContext: deleteItemContext(API),
}
because I had many stores with similar features.
and the same for mutations i used generic mutations functions.