So i have an input slider, that i want to disable if the loggedInUser doesnt meet the role requirements for that input, for example, if the input requires an accountant level role, and the user does not have that role the input should be disabled, it will still be visible, but i want it disabled.
now normally id solve this by doing disabled={!loggedInUser.isAccountant} this is usually all thats needed. However, i have other inputs where several roles should be able to access this input but that user may not have all of the roles, for example, i may want an accountant and an admin to access this input, but not a partner though i still want the field to be viewable.
so i tried coming up with a function that would take the loggedInUser's user document and loop over the keys on that document and try matching the key to a role thats passed in. if any of the roles match a key on the user document, it should check if (the key/value is a boolean) if the boolean is true, if it is, it should return the true boolean, (i.e., they have that role(s) needed to access this). However no matter what i do, all i get is a returned Promise<Pending> when i try to insert the function into the disabled prop on the component.
here is the input:
<Form.Group className={styles.boolean} controlId="isPaid">
{/* True/False */}
<Form.Check
type="switch"
id="custom-switch"
label="Is Paid User"
checked={formData.isPaidUser}
onChange={(e) =>
setFormData({
...formData,
isPaidUser: !formData.isPaidUser,
})
}
// Problem is here!
disabled={checkRole(loggedInUser, ['isAdmin', 'isAccountant']}
></Form.Check>
</Form.Group>
here is the checkRole function
/**
* #description Check if the user has the role passed in the array, passing in multiple roles will check if the user has any of the roles
* #param {Object} user - The logged in user object
* #param {Array} roles - The roles to check against
* #returns {Boolean} - Returns true if the user has the role, false if not
* #example checkrole({user}, ['isAdmin'])
* #example checkrole({user}, ['isAdmin', 'isPartner'])
*
* #author Austin Howard
* #version 1.0.1
* #since 1.0.0
*
*/
export default async (user, roles) => {
if (!user) return false;
// roles is an array of roles to check against the user object,
// we need to loop over the keys of the user object till we find a key that matches the role passed in the array
// if the user has the role, check the boolean value of the key, if the value is true, return true else return false
// if the user does not have the role, return false
let hasRole = false;
await Object.keys(user).forEach((key) => {
console.log(`key: ${key}`);
if (roles.includes(key)) {
console.log(`user[key]: ${user[key]}`);
if (user[key]) {
console.log(`setting hasRole to true`);
hasRole = true;
}
}
});
return hasRole;
};
I've tried multiple iterations of calling the function, even so far as setting up a self calling async function to encaspulate the checkRole function however no matter what i try, it will not insert the boolean as i need correctly
It doesn't look like an asynchronous operation, try a synchronous method like
function userHasRole(user, roles){
return roles.some(role => user[role]);
}
So after playing around i was able to make this work by using useState and useEffect hooks, that the form input will use to determine whether or not to disable the input.
so it looks a lot like this
const Form () => {
const [disableFormInput, setDisableFormInput] = React.useState(false); //initial
React.useEffect(() => {
if (user) {
// setup a self-invoking function to check if the user has the correct user roles
(async () => {
setCanDelete(await checkRole(loggedInUser, ['isAdmin']));
})();
}
}, [user]);
...,
// function returns true if they have the role, we only want it disabled if they dont have the role.
<input disabled={!disableFormInput} ... />
With the help of a colleague, i've found a better solution, that requires less code, and is more scalable, the above answer, my first answer, is a complete answer it works, but it is a bit hacky, so here is another one.
use Regex testing to return a boolean, so the user has a field role which is a string of different roles that the user has for example admin accountant partner on the loggedInUser Object i was setting booleans for easy use on the front end, based on the role the user had, however if i just use the string value, i can use RegEx testing to test if the user has the requested roles
so the final solution:
<input disabled={!/accountant|admin|partner/.test(loggedInUser.role)} ... />
This will scale a lot easier than the first answer i posted.
Related
I am making a search functionality into react that effectively looks for data from json-server for a match. I don't want to provide a debounced search to the input field, rather I want to trigger the search when "Enter" key is pressed. So i used onKeyPress prop from MUI's textfield, where I provided the logic to send the query to the server.
Please acknowledge my code as mentioned below -
imports...
export default function AppSearchBar ( ) {
// local state for searchTerm
const [ searchTerm, setSearchTerm ] = useState<string>('');
// using redux - action
const {loadedBooks} = useAppSelector(state => state.books);
const {loadedGames} = useAppSelector(state => state.games);
// these 'loadedBooks' and 'loadedGames' are of type boolean and are initially false (coming from my slices) and set to true when their requests are getting fulfilled.
const dispatch = useAppDispatch();
// useCallback
const fetchAllCategories = useCallback(() => {
setTimeout( async () => {
await dispatch(fetchBooksAsync( searchTerm )); // this is async thunks i created to fetch books into my bookSlice.ts file
await dispatch(fetchGamesAsync( searchTerm )); // this is async thunks i created to fetch books into my gameSlice.ts file
}, 500);
}, [ searchTerm , dispatch ]);
// effect when searchTerm mounts
/* useEffect(() => {
fetchAllCategories()
}, [ fetchAllCategories ]); */ // dependency as the function itself.
// I want this useEffect, but un-commenting this is not allowing my "handleSearchOnEnter" to come into the picture at all, but, I want that first load of all cars be automatic, and then when I write something to input, on pressing enter it should search, and finally when I wipe input field, it should return me back all the cards.
const handleSearchOnEnter = ( event : any ) => {
if ( event.key === "Enter" ) {
fetchAllCategories(); // this is wrapped inside of useCallBack and effect is produced using useEffect.
}}
return (
<Fragment>
<TextField
value = {searchTerm}
onChange = {( event : any ) => setSearchTerm(event.target.value)}
onKeyPress = { searchTerm !== "" ? handleSearchOnEnter : undefined } />
</Fragment>
)
}
Now, problem statement -
Whenever I load my window, all Books and Games are not loaded at all (if I remove useEffect() from code). They only loads when I press enter. But, I don't want this behaviour.
If I keep useEffect() - hook, then they behaves like debounce search and onChange of my text input field, they return the searched result.
What I want is as follows -
- Whenever I first loads the window, all products get loaded.
- Whenever I write something into the input field, then it shouldn't call (fetchFiltersAsync() - which is the API call for full-text search on Json-Server) until i press Enter key, only When I press enter, it should call the API and fetch the relevant results.
- After that, when I manually remove the searchedItem from input field (wiping it), all of my data should get returned. (i.e display all cards into UI)
What is Happening? -
Whenever My window loads, all of my data/cards are not getting loaded., until I presses enter key...cool
When I type something into input field, it fetches the searched results without even taking "Enter" (because of open useEffect() into the code)
When I remove a term from the input field, my data is not getting loaded automatically back to like as they were into first visit (all cards visible).
All controls are here only (into the code), I have to do something with searchTerm, so whenever searchTerm is not empty, then my handleSearchOnEnter() function should get called.
I mean, it should produce useEffect automatically, but only when a searchTerm is not being provided. If the searchTerm is being provided, then it should trigger handleOnEnterSearch()
I had the same issue that is described in the second Problem I solved it by adding in my project.
<form onSubmit={onKeyDownHandler}>
<TextField>{"Some Code"}</TextField>
</form>;
Also you can create an useState and giving it new Date will refresh your table better.
const onKeyDownHandler = (e) => {
e.preventDefault();
// if (searchTxt.length >= 3 || !searchTxt.length) {
dispatch(setSearchTxt(searchTxtTemp));
setTriggerSearch(new Date().getTime());
// }
};
But the bad sides of this code is when you remove everything from input you need to press enter again to refresh.
I've searched the internet literally everywhere, and also the docs but I couldn't find a solution that works in my current project. Quick bit of context:
We have a form, which displays additional fields depending on whether you're adding or editing. When adding, we have 2 different fields. When editing, we have 5 different fields.
Now when we are adding, I do not want these remaining three fields to be validated. Because they will be required when you edit your form. This is where the context will come in.
Currently I'm using the validate prop on the <Formik /> component like so:
<Formik
{...}
validate={(values: FormValues) => {
try {
validateYupSchema(values, getVersionsSchema, true, {
mode: "testing"
});
} catch (e) {
return yupToFormErrors(e);
}
return {};
}}
>
The validateYupSchema function accepts these 4 parameters which I've filled in:
values: T, schema: any, sync?: boolean, context?: any
Now in my validationSchema, I'd like to access this context object. Currently googling for the past 6 hours it constantly recommends me this approach:
export const getVersionsSchema = (t: TFunction<Namespace<"en">>) =>
Yup.object().shape({
{...}
optIn: Yup.string().when("mode", (mode, schema) => {
console.log(mode, schema);
return schema.required("test");
}),
{...}
});
So here you see I'm using when and apparently I have to pass the variable as a string name as the first argument, and a function as the second argument with the context variable name and the schema as arguments.
However, when I log mode in this case, it's undefined...
Can anybody point me in the right direction perhaps?
Here's a codesandbox:
https://codesandbox.io/s/condescending-water-3fmvsv?file=/src/App.js
I recently posted a question with regards to sending and receiving data via a websocket. In the interest of not duplicating most of the work, the original question is found here: Socket.io emitting undefined variable React
I have since modified my app slightly so that each id received from the server gets added or removed to a disabled array (if the focus parameter received from the server is true, add the id and if not, remove it). This is so that I can disabled/enable questions based off the focus sent.
The slight adjustments I've made are three fold, all within the App.js file
1. Add disabled array to state
const [disabled, setDisabled] = useState([])
2. Include disabled parameter to the question
<TextArea
cols={50}
helperText="Optional helper text here; if message is more than one line text should wrap (~100 character count maximum)"
id="text2"
invalidText="Invalid error message."
labelText="Text area label"
placeholder="Placeholder text"
rows={4}
onFocus={() => setFocusTrue('text2')}
onBlur={() => setFocusFalse('text2')}
disabled={disabled.indexOf('text2')!==-1}
/>
You will note in the above that the disabled parameter will be set to true if the questionId is included in the disabled array (therefore disabling the question) and false if not
3. Add questionId to disabled state array
useEffect(() => {
socket.on("message", ({ user, id, focus }) => {
console.log(user, "clicked on", id, "with focus", focus)
console.log('adding', id, 'to disabled')
setDisabled(prevDisabled => [...prevDisabled, id]);
console.log("disabled:", disabled)
})
}, [])
For now, I am just adding all the questionIds coming from the server to the disabled array so I can test whether they are indeed getting added. But this is my issue in that the disabled array is always empty (see screenshot below). The id variable exists as can be seen in the console log output that precedes setting the new disabled state, but it is not getting added as expected.
EDIT:
As per the comments, I can see that the disabled array is in fact getting updated, if I console.log(disabled) just before rendering. However, if I apply any sort of logic to the array, I get an error. The below is the modified useEffect which contains some logic (essentially, to add or remove an id to the array)
useEffect(() => {
socket.on("message", ({ user, id, focus }) => {
console.log(user, "clicked on", id, "with focus", focus)
console.log('adding', id, 'to disabled')
if (focus) {
console.log('not my user and focus is true')
setDisabled(prevDisabled => [...prevDisabled, id])
console.log("disabled after adding", disabled)
} else {
let filteredArray = disabled.filter(idToRemove => idToRemove !== id)
setDisabled({disabled: filteredArray});
}
console.log("disabled:", disabled)
})
}, [])
When I click on a textbox, the id of the question gets added to the disabled array as expected, but when I click out of it, I get the following error:
disabled.indexOf is not a function
This referring to disabled={disabled.indexOf('text1')!==-1}
I think your code is fine, if you are trying to console.log the most resent state right after you setting it, it won't work why? setState is asynchronous it might not work real time as you expected.
what you actually want to try is add a useEffect and listen for changes of the disabled state
useEffect(() => {
console.log("disabled", disabled);
}, [disabled]); // since disabled is in the dependency array this hook function will call, every time when the disabled gets updated.
// so you are so sure the disabled is getting updated correctly
or just do a simple console.log(disabled) right before the render and see.
and your modified version of useEffect is incorrect as I see, it should be corrected as
....
const filteredArray = disabled.filter(idToRemove => idToRemove !== id)
setDisabled(filteredArray);
....
I'm still learning React and I'm trying to make a "design review app" where users signup as customers or designers and interact with each other.
I made the auth system and made sure that while signing up every user would get also some attributes in the firebase database.
Therefore, in my DB, I have a 'users/' path where every user is saved by uid.
Now I'm able to render a different dashboard if you're a customer or a designer.
In my customer dashboard, I just want to render a list of designers (and clicking on them go to their projects).
However, I'm having so many problems trying to get this stuff to work!
In the following code, I'm trying to fetch the users from the db and add their uid to an array.
Later I want to use this array and render the users with those uids.
import firebase from "firebase/app";
import "firebase/database";
export default function CustomerContent() {
const[designers, setDesigners] = useState([]);
function printUsers (){
var users = firebase.database().ref('/users/');
users.on('value', (snapshot)=>{
snapshot.forEach((user)=>{
console.log(user.key)
firebase.database().ref('/users/'+user.key).on('value', (snapshot)=>{
var role = snapshot.val().role
console.log(role)
if(role === 'designer'){
const newDesigners = [...designers, user.key];
setDesigners(newDesigners);
}
})
})
})
}
useEffect(() => {
printUsers();
console.log(designers);
}, [])
return (
<div>
designer list
</div>
)
}
Now the problem with this code is that:
it looks like it runs the printUsers functions two times when loading the page
the array is empty, however, if I link the function to a button(just to try it), it seems to add only 1 uid to the array, and always the same (I have no idea what's going on).
ps. the console.log(user.key) and the console.log(role) print the right user-role combination
It's not a stupid question. Here's what I'd change it to (of course you'd remove the console.logs later though). It's hard to know if this will work perfectly without having access to your database to run it, but based on my last react/firebase project, I believe it'll work.
The first thing was that you reference /users/, when you only need /users. I'm not sure if it makes a difference, but I did it the latter way and it worked for me.
Secondly, you're calling firebase more than you need to. You already have the information you need from the first time.
Third, and this is small, but I wouldn't call your function printUsers. You're doing more than just printing them- you're making a call to firebase (async) and you're setting the state, which are much larger things than just print some data to the console.
Lastly, I would store the entire object in your designers piece of state. Who knows what you'll want to display? Probably at least their name, then possibly their location, background, an icon, etc. You'll want all of that to be available in that array, and possibly you'll want to move that array into redux later if you're app is big enough.
I also added some JSX to the bottom that gives a simple output of what you could do with the designers array for the visual aspect of your app.
import firebase from 'firebase/app';
import 'firebase/database';
export default function CustomerContent() {
const [designers, setDesigners] = useState([]);
function printUsers() {
var users = firebase.database().ref('/users');
users.on('value', (snapshot) => {
snapshot.forEach((snap) => {
const userObject = snap.val();
console.log(userObject);
const role = userObject['role'];
console.log(role);
if (role === 'designer') {
const newDesigners = [...designers, userObject];
setDesigners(newDesigners);
}
});
});
}
useEffect(() => {
printUsers();
console.log(designers);
}, []);
return (
<div>
<h2>The designer are...</h2>
<ul>
{designers.map((designerObject) => {
return <li>{designerObject.name}</li>;
})}
</ul>
</div>
);
}
How can I override a value in the Apollo cache?
I have a graphql query to fetch the user. This returns a user with a default currency. This currency can then be override from a select dropdown.
The query fetches paymentCurrencies from an API, then uses a client side resolver to set the first item in the array of paymentCurrencies to be the users currency
query me {
me {
username
currency #client
paymentCurrencies
}
}
When somebody selects a currency from the dropdown menu, I want to over the users currency with whatever they have selected.
I have something like this so far:
const onChange = e => {
const { value } = e.target
client.writeData({ user: { currency: value, username, __typename: "User" } })
}
I get the following error: Error writing result to store for query:
{"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GeneratedClientQuery"},"selectionSet":null}]}
Cannot read property 'selections' of null
Is using writeData is the correct method or should I be using writeQuery or something?
As described in the other answer you probably want a simple query and mutation setup. The client directive is used to extend your schema to hold client-only additional data. From your explanation, it sounds like you explicitly want this data to be syncronised with the server.
const ME_QUERY = gql`
query me {
me {
username
currency
paymentCurrencies
}
}
`;
const CURRENCY_MUTATION = gql`
mutation setCurrency($currency: String) {
setCurrency(currency: $currency) {
me {
username
currency
}
}
}
`;
function MyComponent() {
const { data } = useQuery(ME_QUERY);
const [setCurrency] = useMutation(CURRENCY_MUTATION);
const onChange = e => setCurrency({
variables: { currency: e.currentTarget.value },
});
return (
<>
<h2>{data && data.me.currency}</h2>
<select onChange={onChange}>
{/* your dropdown logic */}
</select>
</>
);
}
You get the idea. Apollo will now automatically update your cache. Make sure your mutation allows to query the updated user object.
For the automatic update to work your user needs to be recognised by the cache. You can do that by either adding an id field and selecting it in both the query and the mutation or implementing a dataIdFromObject function in Apollo Client 2.x that includes the username for __typename === 'User' or by using a type policy in Apollo Client 3.x. Find the docs here.
writeData should be used for changing fields at the root, for example:
{
yourState #client
}
In this case, you should use writeQuery. Additionally, this logic should really be extracted into a (local) mutation you can then call inside your component. When using writeQuery, the basic idea is to grab the existing data, make a copy and then transform it as needed:
const { me } = client.readQuery({ query: ME_QUERY })
const data = {
me: {
...me,
currency: value,
}
}
client.writeQuery({ query: ME_QUERY, data })
You can also use writeFragment to directly modify a single instance of an object in the cache. However, you need the object's cache key for this. Because the cache key is derived from the __typename and an id field, you should make sure the query includes an id field first. This is good practice regardless to ensure your cache can be updated easily (see here for more details). Then you can do something like this:
client.writeFragment({
id: 'User:42',
fragment: gql`
fragment UserCurrency on User {
currency #client
}
`,
data: {
currency: value,
},
})
It depends.
For persistent change (sync to server) you should just change user setting using mutation.
For session aware change - don't use user settings - copy this (from logged user properties) to separate value in global app state (redux/mobx or as in this case apollo local state).
In both cases rare problem can be with updating many components using changed data.
Redux/mobx solves this automatically.
Apollo HOCs won't rerender.
Hooks - updates partially (only the one with useQuery and useMutation pair), others will be updated only when rerendered.
Components build with <Query/> creates internally an observable then they will be updated, too.
Mutation have update and refetchQueries parameters for.
There is also a package for more complex use cases.