I'm still a novice web developer, so please bear with me if I miss something fundamental !
I'm creating a backoffice for a Strapi backend, using react-admin.
React-admin library uses a 'data provider' to link itself with an API. Luckily someone already wrote a data provider for Strapi. I had no problem with step 1 and 2 of this README, and I can authenticate to Strapi within my React app.
I now want to fetch and display my Strapi data, starting with Users. In order to do that, quoting Step 3 of this readme : 'In controllers I need to set the Content-Range header with the total number of results to build the pagination'.
So far I tried to do this in my User controller, with no success.
What I try to achieve:
First, I'd like it to simply work with the ctx.set('Content-Range', ...) hard-coded in the controller like aforementioned Step 3.
Second, I've thought it would be very dirty to c/p this logic in every controller (not to mention in any future controllers), instead of having some callback function dynamically appending the Content-Range header to any fetchAll request. Ultimately that's what I aim for, because with ~40 Strapi objects to administrate already and plenty more to come, it has to scale.
Technical infos
node -v: 11.13.0
npm -v: 6.7.0
strapi version: 3.0.0-alpha.25.2
uname -r output: Linux 4.14.106-97.85.amzn2.x86_64
DB: mySQL v2.16
So far I've tried accessing the count() method of User model like aforementioned step3, but my controller doesn't look like the example as I'm working with users-permissions plugin.
This is the action I've tried to edit (located in project/plugins/users-permissions/controllers/User.js)
find: async (ctx) => {
let data = await strapi.plugins['users-permissions'].services.user.fetchAll(ctx.query);
data.reduce((acc, user) => {
acc.push(_.omit(user.toJSON ? user.toJSON() : user, ['password', 'resetPasswordToken']));
return acc;
}, []);
// Send 200 `ok`
ctx.send(data);
},
From what I've gathered on Strapi documentation (here and also here), context is a sort of wrapper object. I only worked with Express-generated APIs before, so I understood this snippet as 'use fetchAll method of the User model object, with ctx.query as an argument', but I had no luck logging this ctx.query. And as I can't log stuff, I'm kinda blocked.
In my exploration, I naively tried to log the full ctx object and work from there:
// Send 200 `ok`
ctx.send(data);
strapi.log.info(ctx.query, ' were query');
strapi.log.info(ctx.request, 'were request');
strapi.log.info(ctx.response, 'were response');
strapi.log.info(ctx.res, 'were res');
strapi.log.info(ctx.req, 'were req');
strapi.log.info(ctx, 'is full context')
},
Unfortunately, I fear I miss something obvious, as it gives me no input at all. Making a fetchAll request from my React app with these console.logs print this in my terminal:
[2019-09-19T12:43:03.409Z] info were query
[2019-09-19T12:43:03.410Z] info were request
[2019-09-19T12:43:03.418Z] info were response
[2019-09-19T12:43:03.419Z] info were res
[2019-09-19T12:43:03.419Z] info were req
[2019-09-19T12:43:03.419Z] info is full context
[2019-09-19T12:43:03.435Z] debug GET /users?_sort=id:DESC&_start=0&_limit=10& (74 ms)
While in my frontend I get the good ol' The Content-Range header is missing in the HTTP Response message I'm trying to solve.
After writing this wall of text I realize the logging issue is separated from my original problem, but if I was able to at least log ctx properly, maybe I'd be able to find the solution myself.
Trying to summarize:
Actual problem is, how do I set my Content-Range properly in my strapi controller ? (partially answered cf. edit 3)
Collateral problem n°1: Can't even log ctx object (cf. edit 2)
Collateral problem n°2: Once I figure out the actual problem, is it feasible to address it dynamically (basically some callback function for index/fetchAll routes, in which the model is a variable, on which I'd call the appropriate count() method, and finally append the result to my response header)? I'm not asking for the code here, just if you think it's feasible and/or know a more elegant way.
Thank you for reading through and excuse me if it was confuse; I wasn't sure which infos would be relevant, so I thought the more the better.
/edit1: forgot to mention, in my controller I also tried to log strapi.plugins['users-permissions'].services.user object to see if it actually has a count() method but got no luck with that either. Also tried the original snippet (Step 3 of aforementioned README), but failed as expected as afaik I don't see the User model being imported anywhere (the only import in User.js being lodash)
/edit2: About the logs, my bad, I just misunderstood the documentation. I now do:
ctx.send(data);
strapi.log.info('ctx should be : ', {ctx});
strapi.log.info('ctx.req = ', {...ctx.req});
strapi.log.info('ctx.res = ', {...ctx.res});
strapi.log.info('ctx.request = ', {...ctx.request});
ctrapi.log.info('ctx.response = ', {...ctx.response});
Ctx logs this way; also it seems that it needs the spread operator to display nested objects ({ctx.req} crash the server, {...ctx.req} is okay). Cool, because it narrows the question to what's interesting.
/edit3: As expected, having logs helps big time. I've managed to display my users (although in the dirty way). Couldn't find any count() method, but watching the data object that is passed to ctx.send(), it's equivalent to your typical 'res.data' i.e a pure JSON with my user list. So a simple .length did the trick:
let data = await strapi.plugins['users-permissions'].services.user.fetchAll(ctx.query);
data.reduce((acc, user) => {
acc.push(_.omit(user.toJSON ? user.toJSON() : user, ['password', 'resetPasswordToken']));
return acc;
}, []);
ctx.set('Content-Range', data.length) // <-- it did the trick
// Send 200 `ok`
ctx.send(data);
Now starting to work on the hard part: the dynamic callback function that will do that for any index/fetchAll call. Will update once I figure it out
I'm using React Admin and Strapi together and installed ra-strapi-provider.
A little boring to paste Content-Range header into all of my controllers, so I searched for a better solution. Then I've found middleware concept and created one that fits my needs. It's probably not the best solution, but do its job well:
const _ = require("lodash");
module.exports = strapi => {
return {
// can also be async
initialize() {
strapi.app.use(async (ctx, next) => {
await next();
if (_.isArray(ctx.response.body))
ctx.set("Content-Range", ctx.response.body.length);
});
}
};
};
I hope it helps
For people still landing on this page:
Strapi has been updated from #alpha to #beta. Care, as some of the code in my OP is no longer valid; also some of their documentation is not up to date.
I failed to find a "clever" way to solve this problem; in the end I copy/pasted the ctx.set('Content-Range', data.length) bit in all relevant controllers and it just worked.
If somebody comes with a clever solution for that problem I'll happily accept his answer. With the current Strapi version I don't think it's doable with policies or lifecycle callbacks.
The "quick & easy fix" is still to customize each relevant Strapi controller.
With strapi#beta you don't have direct access to controller's code: you'll first need to "rewrite" one with the help of this doc. Then add the ctx.set('Content-Range', data.length) bit. Test it properly with RA, so for the other controllers, you'll just have to create the folder, name the file, copy/paste your code + "Search & Replace" on model name.
The "longer & cleaner fix" would be to dive into the react-admin source code and refactorize so the lack of "Content-Range" header doesn't break pagination.
You'll now have to maintain your own react-admin fork, so make sure you're already committed into this library and have A LOT of tables to manage through it (so much that customizing every Strapi controller will be too tedious).
Before forking RA, please remember all the stuff you can do with the Strapi backoffice alone (including embedding your custom React app into it) and ensure it will be worth the trouble.
I'm trying to access the local storage data in Edge set by my options page, using my popup script. Is there a current, working example of this available?
I was using localStorage, but it would only update the popup if I reloaded the extension after saving changes in my options page. I want to make it easier on the user by allowing the popup to access it immediately after saving, without reloading. I found the browser.storage.local.get(), but documentation conflicts everywhere I look, and I can't find viable working examples.
I have used, per Edge documentation:
browser.storage.local.get("sample");
But it throws an error requiring a callback function. So then I used:
let sample = browser.storage.local.get("example");
sample.then(ifGood, ifBad);
I get an error regarding property "then".
Then I simply tried adding a callback to the action itself:
sample = browser.storage.local.get("example", callbackFunction);
function callbackFunction(data){
alert(data);
}
The end alert should display a string, but it just displays an empty Object. How do I access the data returned in the callback? I tried callbackFunction(this) as an argument in the get, but it throws an error about the syntax of the get.
I found a work-around using browser.runtime.reload() in the Options page when saving the changes. It still reloads the extension, but it does it without requiring the user to do it manually.
You should use this syntax:
browser.storage.local.get(propertyName | null, callbackFn)
where
callbackFn = function fn(resultObject) {...}
When you pass null you will get whole storage object.
Look for example 1 or example 2 in my Edge extension.
I am trying to use the google cloud console to test a cloud function. Below is a snippet.
exports.requestCreated = functions.firestore
.document('users/{userId}/requests/{requestId}')
.onWrite((change, context) => {
// execute operation
});
I have tried all sorts of combination of JSON data. E.g.
{"userId":"Xl86pqOpF9T2MAn12p24OJAfYJW2","requestId":"abc1234"}
But I keep getting the following statement in logs:
Request created by {userId}
The actual userId is not being read from the JSON data in the console. Can you help?
This is not a problem with the execution of the cloud function. It's a problem with hardcoding the string.
'users/{userId}/requests/{requestId}' is a hardcoded string. Node.js will not automatically replace {userId} with the value of the variable userId.
Following this previous SO post, try something like this using template strings:
`users/${userId}/requests/${requestId}`
Please note it is surrounded by backticks (`), not single quotes (').
This assumes you already have a userId and requestId variables defined. You must restructure your cloud function like this to retrieve that data. Notice that the specific variable values must be extracted from the event variable.
Thank you, Nareddyt. The function is for Firestore, and the way it is written right now checks if a new document is created under the collection requests. I tried replacing the string as you suggested, but as you pointed out, it requires these variables to be defined. I do not quite understand how to restructure the cloud function because the syntax I have used is how event detection is suggested in the Firestore documentation. My function currently works in its entirety, but testing it is a major pain. I have to go through my mobile app and do the whole userflow to test this function. I am new to Node.js and any guidance would be appreciated.
I'm in the process on making a actions on google project and I am wanting to add a carousel to the action.
I understand the code end of things of how to add it to your action, but I'm a bit confused on how you get the links for your images. So for example, I seen in googles tutorial for carousel they have this following code snippet (to get it started):
const IMG_URL_AOG = 'https://developers.google.com/actions/images/badges' +
'/XPM_BADGING_GoogleAssistant_VER.png';
const IMG_URL_GOOGLE_ALLO = 'https://allo.google.com/images/allo-logo.png';
const IMG_URL_GOOGLE_HOME = 'https://lh3.googleusercontent.com' +
'/Nu3a6F80WfixUqf_ec_vgXy_c0-0r4VLJRXjVFF_X_CIilEu8B9fT35qyTEj_PEsKw';
const IMG_URL_GOOGLE_PIXEL = 'https://storage.googleapis.com/madebygoog/v1'
+
'/Pixel/Pixel_ColorPicker/Pixel_Device_Angled_Black-720w.png';
const IMG_URL_MEDIA = 'http://storage.googleapis.com/automotive-
media/album_art.jpg';
const MEDIA_SOURCE = 'http://storage.googleapis.com/automotive-
media/Jazz_In_Paris.mp3';
// Constants for selected item responses
And if you actually type these links in, you will get something like this back. The last two are other media types, but same idea.
Could someone explain how getting the images/ image links works for carousel's with actions on google? Do you have to make a html page for that image or am I just overthinking this and you can take an image link from online and it will work (given the right size of course). I might have missed something in the docs.
Thanks for the help or suggestions!
To maybe provide further context, I'm going off of this doc
firstly let my say that the mdc documentation is difficult for non-pros like me.
I'm using Elixir Phoenix and Brunch.
I import and everything is fine.
import {MDCTab, MDCTabFoundation} from '#material/tabs'; import
{MDCTabBar, MDCTabBarFoundation} from '#material/tabs'; import
{MDCTabBarScroller, MDCTabBarScrollerFoundation} from
'#material/tabs';
I manually instantiate the tab bar in a separate function that I export
export var Tabbable = {
run: function(MDCTabBar, el){
var myDynamicTabBar = window.myDynamicTabBar = new MDCTabBar(document.querySelector('#' + el));
Which is following the documentation like this
const tabBar = new MDCTabBar(document.querySelector('#my-mdc-tab-bar'));
but is slightly different to the documentation's use of the tab bar in their code snippet
var dynamicTabBar = window.dynamicTabBar = new mdc.tabs.MDCTabBar(document.querySelector('#dynamic-tab-bar'));
But, whenever I try to use mdc I get a 'not defined' error. Therefore, I'm not using it :-)
Now, when the user clicks the tab bar I capture that like this:
myDynamicTabBar.listen('MDCTabBar:change', function ({detail: tabs}) {
var nthChildIndex = tabs.activeTabIndex;
updatePanel(nthChildIndex);
});
The subtle difference is that my myDynamicTabBar is MDCTabBar but the documentation's dynamicTabBar is mdc.tabs.MDCTabBar
My tab control works, but it throws an error only visible in the console:
Uncaught Error: Invalid tab component given as activeTab: Tab not
found within this component's tab list
which is likely because I'm not using mdc.tabs? The documentation notes the change event happens on the MDCTabBar.
Therefore, how do I get rid of this annoying error in the console?
And why can I not access the global mdc? I have tried this in my Brunch file
globals: { mdc: "#material"}
But no good.
I'm right behind you on this! I'm frustrated with the docs too :(
You answered your own question in this Elixir thread which is very informative.
I found the real solution in this thread https://github.com/hyperapp/hyperapp/issues/546
MDCTabBar automatically initiates its children. So initiating tabs will result in that error.
The fix is to just initiate MDCTabBar