React-admin exclude fields when sending PATCH requests - react-admin

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>
);
}

Related

Referencing JSON object fields after assigning to variable in useEffect()

I'm attempting to retrieve a JSON object from an API call, and then set a useState variable to the response for use in my app. I am able to successfully receive the JSON response, but if I try to reference specific fields I get an error saying
null is not an object (evaluating data.type). I understand that this happens because initially the data variable is simply null, but I'm not sure the best way to go about preventing this and I suspect I'm doing something fundamentally wrong.
Here's the function which queries the API and retrieves the response data:
export function searchFlightOffers(postData) {
return getAmadeusToken().then(data => {
const config = {
headers: {
'Authorization': `Bearer ${data.access_token}`
}
}
return axios.post("https://test.api.amadeus.com/v2/shopping/flight-offers", postData, config).then(data => data);
});
}
Then the React Native function which uses it
export default function FlightScreen() {
const [flightResponse, setFlightResponse] = useState(null);
useEffect(() => {
searchFlightOffers(postData).then(data => setFlightResponse(data.data));
}, [])
console.log("TEST: ", flightResponse.type);
Is there a more efficient way I should be doing this?
EDIT: To add hopefully a more clear description of the issue, here's the JSX I'm trying to use this data in:
return (
<ScrollView style={{marginTop: 220}}>
{
flightResponse != null ? flightResponse.map(data => {
return (
<FlightCard
key={data.id}
data={data}
onPress={() => {}}
/>
)
}) : false
}
</ScrollView>
If I add a conditional which checks to see if the flightResponse data is null and then map the data if not then this works just fine. Removing the conditional and just leaving it like this:
return (
<ScrollView style={{marginTop: 220}}>
{
flightResponse.map(data => {
return (
<FlightCard
key={data.id}
data={data}
onPress={() => {}}
/>
)
})
}
</ScrollView>
Leaves me with this error: null is not an object (evaluating 'flightResponse.map') .
While technically the conditional is a solution to my problem there must be a cleaner way to handle this no?
Update: A solution to this problem is to change
const [flightResponse, setFlightResponse] = useState(null);
to
const [flightResponse, setFlightResponse] = useState([]);
and then I can remove the conditional from the JSX.
My apology that I didn't see the additional info you have put there. Apparently you have resolved this issue on your own by adding the check for null, which, to my knowledge, is the correct usage pattern.
You have to check for null because the code in useEffect is not guaranteed to complete (because it is async) before react native executes the code to render the components. By checking for null, you place a guard on this exact situation (and other situations, such as error during fetching data, data itself is empty, etc.).
Original answer (obsolete)
Try rewrite your searchFlightOffers function in async/await style. This way, it is clearer what object is returned. I suspect your current version does not return the data.
The rewrite can be something like this (untested, use with caution).
export const searchFlightOffers = async (postData) => {
try {
const token = await getAmadeusToken();
} catch (err) {
// handle error during token acquisition
console.log(err);
}
const config = {
headers: {
'Authorization': `Bearer ${token.access_token}`
}
}
try {
const flightOfferData = await axios.post(
"https://test.api.amadeus.com/v2/shopping/flight-offers",
postData,
config,
);
return flightOfferData;
} catch (err) {
// handle error during flight offer acquisition
console.log(err);
}
}

(react native chat) Is there any way to use the global variable independently?

Screenshot
In the banner above the chat, there are two buttons that temporarily turn off the banner and always turn it off. When the user presses the always off button, I hope it applies only to the chat room, but maybe because it is a global variable, it affects other chat rooms. Is there a way to use the global variable independently for each chat?
I thought about making it a key value object and using the id value of the chatroom as the key value, but there is no way to get the id value because I have to allocate it as a global variable.
// ChatScreen
...
<InfoBanner postId={postId} errandInfo={errandInfo} />
<View style={styles.contents}>
<GiftedChat
messages={messages}
...
// InfoBanner
let alwaysOption = true
export default InfoBanner = (props) => {
const { postId, errandInfo } = props;
const [bannerVisible, setBannerVisible] = useState(true)
return (
<Banner
visible={alwaysOption && bannerVisible}
style={styles.banner}
actions={[
{
label: 'turn off',
color: 'black',
onPress: () => setBannerVisible(false),
},
{
label: 'always turn off',
color: 'black',
style: {justifyContent: 'center'},
onPress: () => {alwaysOption=false; setBannerVisible(false)},
},
]}
>
...
We could create a custom "hook" that implements a global application cache. We can decide later whether we want to store the chats that should show a banner or not, along with the user ID, on a server or in the phone's local storage.
I will implement a solution using the phone's local storage. It might be useful to store this information in a global cache once it is loaded. I will use Vercel's SWR and react-native-async-storage.
The hook: useChatBanner
import AsyncStorage from '#react-native-async-storage/async-storage';
import useSWR from 'swr'
const keyTemplate = `chat/banner/`
const useChatBanner = (postId) => {
const {data, error, mutate} = useSWR(postId ? `${keyTemplate}${postId}` : null, () => {
const value = await AsyncStorage.getItem(`${keyTemplate}${postId}`).then(response => response === "true" ? true : false)
return value
})
const setBannerVisible = React.useCallback((id, visible) => {
const newData = visible ? "true" : "false"
mutate(newData, false)
AsyncStorage.set(`${keyTemplate}${postId}`, new).then(() => {
mutate(newData)
})
}, [data, mutate])
return {
isVisible: data,
isLoading: !error && !data,
isError: error,
setBannerVisible
}
}
We need to an additional config wrapper component around our main application for SWR.
import { SWRConfig } from "swr"
export default const App = () => {
return <SWRConfig>
// main entry point, navigator code or whatever
</SWRConfig>
}
Then you can use it as follows in your InfoBanner component.
export default InfoBanner = (props) => {
const { postId, errandInfo } = props;
const [bannerVisible, setBannerVisible] = useState(true)
const { isLoading, isVisible } = useChatBanner(postId)
if (isLoading) {
// might be useful to show some loading indicator here
return null
}
return (
<Banner
visible={isVisible && bannerVisible}
...
I have included a setter function in the hook in case you want to allow the user to enable the global flag again.
Using the above, it is possible to hold a boolean flag (or any other data) globally for each postId. Notice that we can implement a similar behavior using the Context API.
If you have a server and an user management, then it is very simple to adapt the above code. Replace the async storage calls with your API and you are good to go. If we manage this using userId's rather then postIds, then the data would be an array and we can filter for postIds to retrieve the correct boolean value.

react-admin: When to first get data pass by a List to a DataGrid

I have a custom component that substitute the DataGrid component, and it uses state, so i want to initiate the state prop with an array of boolean items representing the records.
My problem is where to initiate the state prop of my component since i need to have access to the records coming from the back-end first.
According to the docs, the List component "...passes the data to an iterator view - usually Datagrid..." and looking at the source code it does so in the componentDidMount and the componentWillRecibeProps methods.
But i'm unable to work it out so far.
In a nutshell,
When is the data of DataGrid gets loaded into it?
Any suggestions?
Thank you.
You can modify data coming from back-end before rendering these ways
Probably the most suitable place to modify it is in convertHTTPResponse in dataProvider.js
dataProvider.js:
const convertHTTPResponse = async (response, type, resource, params) => {
let json;
switch (type) {
case GET_LIST:
case GET_MANY:
case GET_MANY_REFERENCE:
json = await response.json();
const total = response.headers.get('x-total-count');
return {
data: json.map(d => /* modify data here */),
total: parseInt(total, 10)
};
<..>
export default (type, resource, params) => {
<..>
return fetch(url, options)
.then(res => convertHTTPResponse(res, type, resource, params));
};
Modify data in your custom component
const ListComponent = (({ classes, ...props }) => (
<List {...props}>
<YourCustomComponent />
</List>
));
const YourCustomComponent = ({ ids, data, basePath }) => {
// modify data here
return //
};

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.
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 })
}
}

How to dynamically set query parameters with AWS AppSync SDK for React-Native

Background: I'm working on building a mobile app with react-native, and am setting up AWS's AppSync for synchronizing the app with cloud data sources.
The challenge: I have a view which shows all items in a list. The list's ID is passed in as a prop to the component. I need to use that list ID to query for the items of that list. I have the query working fine if I hard-code the list ID, but I'm having a hard time figuring out how to dynamically set the list ID for the query when props update.
Here's what I have working (with a hard-coded ID of testList01) in my ListPage component:
const getListItems = id => gql`
query getListItems {
getListItems(listID: ${id}) {
reference_id,
quantity,
}
}
`;
export default graphql(getListItems('testList01'), {
options: {
fetchPolicy: 'cache-and-network',
},
props: props => ({
listItems: props.data ? props.data.getListItems : [],
data: props.data,
}),
})(withNavigationFocus(ListPage));
I would like to be able to dynamically set which list to look up the items for based on a list ID, which is being passed in from props. Specifically, I'm using react-navigation to enter the ListPage, a view where a user can see the items on a List. So here's the code that gets executed when a user clicks on a list name and gets routed to the ListPage component:
handleListSelection(list: Object) {
const { navigation, userLists } = this.props;
navigation.navigate('ListPage', {
listID: list.record_id,
listName: list.list_name,
userLists,
});
}
From my previous (pre-AppSync/GraphQL) implementation, I know that I can access the list ID in ListPage via this.props.navigation.state.params.listID. I would like to be able to use that in my AppSync query, but because the query is created outside the component, I'm unable to access the props, and so am struggling to get the ID.
Got this working using a package called react-apollo-dynamic-query which I found here. The author of that package also links directly to a simple function for doing what I'm trying to do here.
Essentially it just wraps the regular graphql call in a simple way that exposes the props so they can be passed down to the query.
My code now looks likes this (which I have below my definition of the ListPage component, in the same file):
const getListItems = props => {
const listID = props.navigation.state.params.listID;
return gql`
query getListItems {
getListItems(listID: "${listID}") { // Note the variable being wrapped in double quotes
reference_id,
quantity,
}
}
`;
};
const config = {
options: {
fetchPolicy: 'cache-and-network',
},
props: props => ({
listItems: props.data ? props.data.getListItems : [],
}),
};
const MyApolloComponent = graphqlDynamic(getListItems, config)(ListPage);
export default MyApolloComponent;
It should work like this:
const getListItems = (id) => {
return gql`
query getListItems {
getListItems(listID: ${id}) {
reference_id,
quantity,
}
}
`;
}
Call this getListItems like the below
export default graphql(getListItems(id), { //from where ever you want to send the id
options: {
fetchPolicy: '
......
I have not tested this code. Please update if this works. Although I am quite sure that it works.