React-admin image upload data provider showing undefined - react-admin

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?

Related

Updating useState array from callback

I am building a React Native (Expo) app that scans for Bluetooth devices. The Bluetooth API exposes a callback for when devices are detected, which I use to put non-duplicate devices into an array:
const DeviceListView = () => {
const [deviceList, setDeviceList] = useState([]);
const startScanning = () => {
manager.startDeviceScan(null, null, (error, device) => {
// Add to device list if not already in list
if(!deviceList.some(d => d.device.id == device.id)){
console.log(`Adding ${device.id} to list`);
const newDevice = {
device: device,
...etc...
};
setDeviceList(old => [...old, newDevice]);
}
});
}
// map deviceList to components
componentList = deviceList.map(...);
return <View> {componentList} </View>
}
The problem is that the callback is called many many times faster than setDeviceList updates, so the duplicate checking doesn't work (if I log deviceList, it's just empty).
If I use an additional, separate regular (non-useState) array, the duplicate checking works, but the state doesn't update consistently:
const DeviceListView = () => {
const [deviceList, setDeviceList] = useState([]);
var deviceList2 = [];
const startScanning = () => {
manager.startDeviceScan(null, null, (error, device) => {
// Add to device list if not already in list
if(!deviceList2.some(d => d.device.id == device.id)){
console.log(`Adding ${device.id} to list`);
const newDevice = {
device: device,
...etc...
};
deviceList2.push(newDevice);
setDeviceList(old => [...old, newDevice]);
}
});
}
// map deviceList to components
componentList = deviceList.map(...);
return <View> {componentList} </View>
}
This code almost works, but the deviceList state doesn't update correctly: it shows the first couple of devices but then doesn't update again unless some other component causes a re-render.
What do I need to do to make this work as expected?
I would suggest wrap your duplicate check within the state set function itself, and then return the same device list if no new devices have been found. This offloads race condition handling to the underlying react implementation itself, which I've found to be good enough for most cases.
Thus it would look something like this:
const DeviceListView = () => {
const [deviceList, setDeviceList] = useState([]);
const startScanning = () => {
manager.startDeviceScan(null, null, (error, device) => {
// Add to device list if not already in list
setDeviceList(old => {
if(!old.some(d => d.device.id == device.id)){
console.log(`Adding ${device.id} to list`);
const newDevice = {
device: device,
// ...etc...
};
return [...old, newDevice]
}
return old
});
});
}
// map deviceList to components
componentList = deviceList.map(...);
return <View> {componentList} </View>
}
Since old is unchanged if no new unique devices are found it will also skip next re-render according to the docs ( which is a neat optimisation :) )
This is the preferred way to implement state updates that are dependant on previous state according to the docs
https://reactjs.org/docs/hooks-reference.html#functional-updates
convert your callback to promise so that until you get completed device list, checkout below code (PS. not tested, please change as you need)
const [deviceList, setDeviceList] = useState([]);
const [scanning, setScanning] = useState(false);
useEffect(() => {
if(scanning) {
setDeviceList([]);
startScanning();
}
}, [scanning]);
const subscription = manager.onStateChange(state => {
if (state === "PoweredOn" && scanning === false) {
setCanScan(true);
subscription.remove();
}
}, true);
const fetchScannedDevices = () => {
return new Promise((resolve, reject) => {
manager.startDeviceScan(null, null, (error, device) => {
// Add to device list if not already in list
if (!deviceList.some(d => d.device.id == device.id)) {
console.log(`Adding ${device.id} to list`);
const newDevice = {
device: device,
// ...etc...
};
resolve(newDevice);
}
if (error) {
reject({});
}
});
});
};
const startScanning = async () => {
try {
const newDevice = await fetchScannedDevices();
setDeviceList(old => [...old, newDevice]);
} catch (e) {
//
}
};
const handleScan = () => {
setScanning(true);
};
// map deviceList to components
componentList = deviceList.map(() => {});
return (
<View>
<Button
onPress={() => handleScan()}>
Scan
</Button>
<View>{componentList}</View>
</View>
);
};

How to tell if a Shopify store is using Online Store 2.0 theme?

I'm building a Shopify app
I want to know if a majority of potential clients are using Online Store 2.0 themes or not.
Is there a way to tell this by looking only at their websites? (i.e. checking if some script is loaded in network tab that only loads for online store 2.0 themes)
On the Shopify Product reviews sample app, they have this endpoint, which returns whether the current theme supports app blocks (only 2.0 themes support app blocks)
/**
* This REST endpoint is resposible for returning whether the store's current main theme supports app blocks.
*/
router.get(
"/api/store/themes/main",
verifyRequest({ authRoute: "/online/auth" }),
async (ctx) => {
const session = await Shopify.Utils.loadCurrentSession(ctx.req, ctx.res);
const clients = {
rest: new Shopify.Clients.Rest(session.shop, session.accessToken),
graphQL: createClient(session.shop, session.accessToken),
};
// Check if App Blocks are supported
// -----------------------------------
// Specify the name of the template we want our app to integrate with
const APP_BLOCK_TEMPLATES = ["product"];
// Use `client.get` to request list of themes on store
const {
body: { themes },
} = await clients.rest.get({
path: "themes",
});
// Find the published theme
const publishedTheme = themes.find((theme) => theme.role === "main");
// Get list of assets contained within the published theme
const {
body: { assets },
} = await clients.rest.get({
path: `themes/${publishedTheme.id}/assets`,
});
// Check if template JSON files exist for the template specified in APP_BLOCK_TEMPLATES
const templateJSONFiles = assets.filter((file) => {
return APP_BLOCK_TEMPLATES.some(
(template) => file.key === `templates/${template}.json`
);
});
// Get bodies of template JSONs
const templateJSONAssetContents = await Promise.all(
templateJSONFiles.map(async (file) => {
const {
body: { asset },
} = await clients.rest.get({
path: `themes/${publishedTheme.id}/assets`,
query: { "asset[key]": file.key },
});
return asset;
})
);
// Find what section is set as 'main' for each template JSON's body
const templateMainSections = templateJSONAssetContents
.map((asset, index) => {
const json = JSON.parse(asset.value);
const main = json.sections.main && json.sections.main.type;
return assets.find((file) => file.key === `sections/${main}.liquid`);
})
.filter((value) => value);
// Request the content of each section and check if it has a schema that contains a
// block of type '#app'
const sectionsWithAppBlock = (
await Promise.all(
templateMainSections.map(async (file, index) => {
let acceptsAppBlock = false;
const {
body: { asset },
} = await clients.rest.get({
path: `themes/${publishedTheme.id}/assets`,
query: { "asset[key]": file.key },
});
const match = asset.value.match(
/\{\%\s+schema\s+\%\}([\s\S]*?)\{\%\s+endschema\s+\%\}/m
);
const schema = JSON.parse(match[1]);
if (schema && schema.blocks) {
acceptsAppBlock = schema.blocks.some((b) => b.type === "#app");
}
return acceptsAppBlock ? file : null;
})
)
).filter((value) => value);
/**
* Fetch one published product that's later used to build the editor preview url
*/
const product = await getFirstPublishedProduct(clients.graphQL);
const editorUrl = `https://${session.shop}/admin/themes/${
publishedTheme.id
}/editor?previewPath=${encodeURIComponent(
`/products/${product?.handle}`
)}`;
/**
* This is where we check if the theme supports apps blocks.
* To do so, we check if the main-product section supports blocks of type #app
*/
const supportsSe = templateJSONFiles.length > 0;
const supportsAppBlocks = supportsSe && sectionsWithAppBlock.length > 0;
ctx.body = {
theme: publishedTheme,
supportsSe,
supportsAppBlocks,
/**
* Check if each of the sample app's app blocks have been added to the product.json template
*/
containsAverageRatingAppBlock: containsAppBlock(
templateJSONAssetContents[0]?.value,
"average-rating",
process.env.THEME_APP_EXTENSION_UUID
),
containsProductReviewsAppBlock: containsAppBlock(
templateJSONAssetContents[0]?.value,
"product-reviews",
process.env.THEME_APP_EXTENSION_UUID
),
editorUrl,
};
ctx.res.statusCode = 200;
}
);

react-native-storage returning undefined from local storage

I am having some difficulties on executing local storage operations...
"react-native": "0.64",
"react-native-storage": "^1.0.1"
I'm using react-native-storage, as pointed in title, and I have created two simple methods for handling Writing and Reading:
import Storage from 'react-native-storage';
import AsyncStorage from '#react-native-community/async-storage';
const storage = new Storage({
size: 1000,
storageBackend: AsyncStorage,
defaultExpires: null,
enableCache: true,
sync: {
return: 'No data.'
}
});
const saveToLocalStorage = (key: any, data: any) => {
storage.save({
key,
data,
expires: null
})
}
const getFromLocalStorage = (key: any) => {
storage.load({
key,
autoSync: true
})
.then(data => {
return { data }
})
.catch(err => { });
}
export { saveToLocalStorage, getFromLocalStorage }
As you can see, it's pretty much the code example from https://www.npmjs.com/package/react-native-permissions.
At the App.tsx file, I do the following:
useEffect(() => {
saveToLocalStorage('test', 'test data');
const test = getFromLocalStorage('test');
}, [])
which returns undefined.
But if in the method getFromLocalStorage I replace
.then(data => {
return { data }
})
for
.then(data => console.warn(data));
the result is the image from bellow:
In short:
If the function returns the object from the storage, it brings undefined.
If the function returns a console.log from the storage, it brings what I've written on it.
because return { data } is not a valid expression for async functions
just use AsyncStorage, react-native-storage is not needed unless you develop for both mobile and web
useEffect(() => {
await AsyncStorage.setItem('test', 'myValue');
const value = await AsyncStorage.getItem('test');
console.log(value);
}, [])

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

React Native - How to see what's stored in AsyncStorage?

I save some items to AsyncStorage in React Native and I am using chrome debugger and iOS simulator.
Without react native, using regular web development localStorage, I was able to see the stored localStorage items under Chrome Debugger > Resources > Local Storage
Any idea how can I view the React Native AsyncStorage stored items?
React Native Debugger has this built in.
Just call showAsyncStorageContentInDev() in the RND console and you'll be able to see a dump of your app's storage.
You can use reactotron i think it has Async Storage explorer ;)
https://github.com/infinitered/reactotron
Following should work,
AsyncStorage.getAllKeys((err, keys) => {
AsyncStorage.multiGet(keys, (error, stores) => {
stores.map((result, i, store) => {
console.log({ [store[i][0]]: store[i][1] });
return true;
});
});
});
I have created a helper method to log all Storage in a single object (more clean to log for example in Reactotron):
import AsyncStorage from '#react-native-community/async-storage';
export function logCurrentStorage() {
AsyncStorage.getAllKeys().then((keyArray) => {
AsyncStorage.multiGet(keyArray).then((keyValArray) => {
let myStorage: any = {};
for (let keyVal of keyValArray) {
myStorage[keyVal[0]] = keyVal[1]
}
console.log('CURRENT STORAGE: ', myStorage);
})
});
}
react native debugger
right click on free space
With bluebird you can do this:
const dumpRaw = () => {
return AsyncStorage.getAllKeys().then(keys => {
return Promise.reduce(keys, (result, key) => {
return AsyncStorage.getItem(key).then(value => {
result[key] = value;
return result;
});
}, {});
});
};
dumpRaw().then(data => console.log(data));
Maybe late, but none of these solutions fit for me.
On android, with Android Studio open file explorer then go to data/data/your_package_name
Inside you should have a folder called database and inside a file RKStorage.
This file is a SQLite3 file so get your favorite SQLite explorer and explore. If you want one this one does the job : DB Browser for SQLite
I did not find Reactotron to have any type of pretty printing enabled and it's also brutally latent so I just wrote a simple function using lodash. You could use underscore too.
Assuming you have a static mapping of all your keys...
const keys = {
key1: 'key1',
key2: 'key2'
}
export function printLocalStorage() {
_.forEach(keys, (k, v) => {
localStore.getAllDataForKey(v).then(tree => {
console.log(k) // Logs key above the object
console.log(tree) // Logs a pretty printed JSON object
})
})
}
It's not performant but it solves the problem.
You can Define function to get all keys by using async and await
getAllkeys = () => {
return new Promise( async (resolve, reject) => {
try {
let keys = await AsyncStorage.getAllKeys();
let items = await AsyncStorage.multiGet(keys)
resolve(items)
} catch (error) {
reject(new Error('Error getting items from AsyncStorage: ' + error.message))
}
});
}
somefunc = async () => {
try {
var items = await getAllkeys();
var someItems = items.filter(function (result, i, item) {
// do filtering stuff
return item;
});
// do something with filtered items
} catch (error) {
// do something with your error
}
}
I have a expo snack that shows this and also performs a "load". So it is useful for doing a dump of the contents and storing it to a file and loading it up later.
Here are they parts.
const keys = await AsyncStorage.getAllKeys();
const stores = await AsyncStorage.multiGet(keys);
const data = stores.reduce(
(acc, row) => ({ ...acc, [row[0]]: row[1] }),
{}
);
// data now contains a JSONable Javascript object that contains all the data
This ammends the data in the AsyncStorage from a JSON string.
// sample is a JSON string
const data = JSON.parse(sample);
const keyValuePairs = Object.entries(data)
.map(([key, value]) => [key, value])
.reduce((acc, row) => [...acc, row], []);
await AsyncStorage.multiSet(keyValuePairs);
import AsyncStorage from "#react-native-async-storage/async-storage";
export const printAsyncStorage = () => {
AsyncStorage.getAllKeys((err, keys) => {
AsyncStorage.multiGet(keys, (error, stores) => {
let asyncStorage = {}
stores.map((result, i, store) => {
asyncStorage[store[i][0]] = store[i][1]
});
console.table(asyncStorage)
});
});
};
enter image description here