How to use on reference input to related filed in react-admin? - react-admin

I'm using react-admin.
I have 3 resources: schools, teachers and classes
Each school has its teachers and its classes
Each class has a teacher from its school
In creations of class one of the inputs is a teacher, it is need to be a reference type but not to all the teachers only for those who belong to the school of this class.
How should I support it?
How to pass the school_id to the reference input?
Thank you!

This is explained in the documentation: https://marmelab.com/react-admin/Inputs.html#referenceinput
In summary:
/ you can filter the query used to populate the possible values. Use the
// `filter` prop for that.
<ReferenceInput
source="teacher_id"
reference="teachers"
filter={{ school_id: values.school_id }}
>
<SelectInput optionText="name" />
</ReferenceInput>
You may wonder how you can get this school_id: https://marmelab.com/react-admin/Inputs.html#linking-two-inputs
import { useFormState } from 'react-final-form';
const TeacherInput = () => {
const { values } = useFormState();
return (
<ReferenceInput
source="teacher_id"
reference="teachers"
filter={{ school_id: school_id: values.school_id }}
>
<SelectInput optionText="name" />
</ReferenceInput>
);
}

Related

How to create a custom record action button inside a List component with React-Admin?

I'm a totally newbie with React and React-Admin. IMHO, I'm trying to achieve something simple that many people must have already done but I cannot find any kind of tutorial anywhere.
I'd like to add another button to the list of action buttons (show/edit) within each row in a <List> component. This button would archive the record.
My last try looks like the code below.
import React from 'react';
import {
Datagrid,
EmailField,
List,
TextField,
ShowButton,
EditButton,
DeleteButton,
CloneButton,
} from 'react-admin';
import { makeStyles } from '#material-ui/core/styles';
import ArchiveIcon from '#material-ui/icons/Archive';
const useRowActionToolbarStyles = makeStyles({
toolbar: {
alignItems: 'center',
float: 'right',
width: '160px',
marginTop: -1,
marginBottom: -1,
},
icon_action_button: {
minWidth: '40px;'
},
});
const ArchiveButton = props => {
const transform = data => ({
...data,
archived: true
});
return <CloneButton {...props} transform={transform} />;
}
const RowActionToolbar = (props) => {
const classes = useRowActionToolbarStyles();
return (
<div className={classes.toolbar}>
<ShowButton label="" basePath={props.basePath} record={props.record} className={classes.icon_action_button}/>
<EditButton label="" basePath={props.basePath} record={props.record} className={classes.icon_action_button}/>
<ArchiveButton {...props} basePath={props.basePath} label="" icon={<ArchiveIcon/>} record={props.record} className={classes.icon_action_button} />
<DeleteButton basePath={props.basePath} label="" record={props.record} className={classes.icon_action_button}/>
</div>
);
};
export const UserList = props => {
return (
<List
{...props}
sort={{ field: 'first_name', order: 'ASC' }}
>
<Datagrid>
<TextField source="first_name"/>
<TextField source="last_name"/>
<EmailField source="email"/>
<RowActionToolbar/>
</Datagrid>
</List>
)
};
Obviously, this code does not work because the <CloneButton> component get rid of the id the record. Moreover, except if I did something wrong - which is totally possible -, it makes a GET request to a create endpoint.
I'm using different routes in my dataProvider (The back end is using Django and Django rest framework). I want to send a PATCH to the detail endpoint, like the <Edit> component does.
I also tried with a <SaveButton>, but it fails too.
Uncaught TypeError: Cannot read property 'save' of undefined
at useSaveContext (SaveContext.js:23)
I guess the <SaveButton> must be within a <SimpleForm>?
I'd like the save behaviour of the <DeleteButton>, i.e. update the record from the list, display the notification that the record has been archived (with the Undo link), send the request to the back end, refresh the list.
Any guidance, directions would be very appreciated.
I don't know that this is a full answer, but felt like more than a comment...
You are trying to archive the existing record, not create a whole new record, right? CloneButton is supposed to be used to create a new record with a new ID (which is why your ID is going away), so you don't want to us it here. note that I've never used CloneButton. it is not fully documented so I could be wrong about its use.
I am thinking that you should use the useRecordContext hook within your Archive button to pull in all of the record's data, including the id; read this little section: https://marmelab.com/react-admin/Architecture.html#context-pull-dont-push
And I don't think transform is what you're looking for here. You will need to use one of the dataProvider hooks, i'm assuming useUpdate: https://marmelab.com/react-admin/Actions.html#useupdate
//first create component
const MyButton = (props: any) => {
const [sendEmailLoading, setSendEmailLoading] =
React.useState<boolean>(false);
const record = useRecordContext(props);
const sendEmail = (id: Identifier) => {
setSendEmailLoading(true)
dataProvider.sendEmail(
"notifications", { id: id })
.then(({ data }: any) => {
if (data && data.status == "success")
notify('Email send success', { type: 'success' });
setSendEmailLoading(false);
refresh();
});
};
return (
<ButtonMUI color='primary' size="small" onClick={() => {
sendEmail(record.id) }}>
{
!record.publish &&(
!sendEmailLoading ? (
translate('resources.notifications.buttons.send')
) : (
<CircularProgress size={25} thickness={2} />
)
)
}
</ButtonMUI>
)
}
//and second add to datagrid list
<Datagrid>
<NumberField source="id" />
<TextFieldRA source="subject" />
<DateField source="date" />
<BooleanField source="publish" />
{/* <EditButton /> */}
<ShowButton />
<MyButton />
</Datagrid>

How can I use related Filters in a List component?

I'm trying to use a kind of "related filters" in a <List> component inside my app.The idea is similar to a situation that you have a filter with country names and after you choose one, the second filter, will have the cities names related to the country in the first filter.
I made a custom component like this:
const CityFilter = props => {
return (
<Fragment>
<Country {...props} />
<FormDataConsumer>
{({ formData, ...rest }) => (
<SelectInput
{...props}
label="City"
source="cityId"
optionText="name"
optionValue="id"
choices={getCitiesfor(formData.countryId)}
parse={(v) => parseInt(v)}
{...rest}
/>
)}
</FormDataConsumer>
</Fragment>);
};
and it works good but the issue is that after you close the filter components the <List> still remains filtered.
So, there're any other way to do that? or how can i do to solve the issue?
Thanks in advance.

React-native Formik setFieldValue

Here is a simplified version of my code.
Notice the setFieldValue_ and this.setFieldValue_ = setFieldValue;
This code works fine, I'm able to get the output when submit button is clicked.
I'm actually wondering if this is the right way to do it? If not, can you point me to the right direction? Also what is this method called? (assigning class variable to some function and use it within another function)
class MyComponent extends React.Component {
setFieldValue_;
someFunction() {
this.setFieldValue_("name", value);
}
render() {
return (
<Formik
initialValues={{
something: ""
}}
onSubmit={values => console.log(values)}
>
{({
setFieldValue,
}) => {
this.setFieldValue_ = setFieldValue;
<ThirdPartyCustomComponent onChange={this.someFunction} />
}}
</Formik>
}
}
I would personally have the onChange simply call formik set field value there and then rather than using different functions. Strictly speaking you don't want to set the value like that because every re-render is setting the value again.
I would also recommend looking at custom formik inputs using the useField hook - https://jaredpalmer.com/formik/docs/api/useField. This will allow you to write a small wrapper around your third party component and formik. Noticing you have used a class based component you may want to do some short reading into react hooks before throwing yourself into using useField.
Docs example:
const MyTextField = ({ label, ...props }) => {
const [field, meta, helpers] = useField(props);
return (
<>
<label>
{label}
<input {...field} {...props} />
</label>
{meta.touched && meta.error ? (
<div className='error'>{meta.error}</div>
) : null}
</>
);
};

Get ReferenceManyField Count?

How do you get to display a count of a "ReferenceManyField" component?
Here is the thing, I am listing organizations and I would like to get each of them their users counts.
Fetch organizations:
GET /organizations?filter={}&range=[0,24]&sort=["name","ASC"]
Fetch users for each organizations:
...
GET /users?filter={"uuid_organization":"1117b0a0-6ec7-40e3-9ee6-49d121c27111"}&range=[0,99]&sort=["email","ASC"]
...
<List
{...props}
filters={<OrganizationFilter />}
sort={{ field: 'name', order: 'ASC' }}
perPage={25}
>
<Responsive
xsmall={<MobileGrid />}
medium={
<Datagrid expand={<OrganizationShow />}>
<OrganizationLinkField />
<ReferenceManyField label="Users" reference="users" target="uuid_organization" sort={{ field: 'email', order: 'ASC' }}>
// PRINT: 102 users
</ReferenceManyField>
<EditButton />
</Datagrid>
}
/>
</List>
I am not sure if I should do it this way, it seems inefficient and overkill. What you think?
Thanks in advance.
The approach you chose is the way react-admin has implemented it to get joined data by foreign key and IMHO it is not that much of an overkill because on database level you still need to query the other table.
According to documentation,
there is an extra response property for response format GET_MANY_REFERENCE called total:
GET_MANY_REFERENCE { data: {Record[]}, total: {int} }
The ReferenceManyField field component uses this response format to retrieve data using the current record id as the foreign key (parent id) to the referenced resource.
Digging into react-admin code latest version 2.9.0 I see that ReferenceManyField passes props to it's children and it is up to you how you handle the data (e.g. List, DataGrid) and one of the props is total (count of records) which is by default passed to Pagination component.
You can create your own simple functional component to handle these props if you do not want to use anything else:
import React from 'react';
const UserTotal = props => <div>User count: {props.total}</div>;
export default UserTotal;
And the usage of your reference field:
<ReferenceManyField
label="Users"
reference="users"
target="uuid_organization"
sort={{ field: 'email', order: 'ASC' }}
>
<UserTotal />
</ReferenceManyField>

How to prevent get request from being send if the length of the query field is short in react-admin

I'm using react-admin and trying to create a filter with autocomplete field that will make a query as I type and will only start sending the query when the search criteria length is longer then 2.
I'm currently using shouldRenderSuggestions inside of my AutocompleteInput field but this still send two requests with an empty string in the "nickname" filter, this is the code part:
<AutocompleteInput optionText="nickname" shouldRenderSuggestions={(val) => {
return val.trim().length > 2
}}/>
The thing that happens is when I fill in the first and second letters the GET request is sent but with an empty string in the nickname field,
The string input is for example:"abc":
1ST request:
http://website.loc/clients?filter={"nickname":""}&page=1&perPage=25&range=[0,24]&sort=["id","DESC"]
2ND request:
http://website.loc/clients?filter={"nickname":""}&page=1&perPage=25&range=[0,24]&sort=["id","DESC"]
3RD request:
http://website.loc/clients?filter={"nickname":"abc"}&page=1&perPage=25&range=[0,24]&sort=["id","DESC"]
I want to avoid from sending the first two requests entirely.
The full code of the component:
const PostPagination = props => (
<Pagination rowsPerPageOptions={[]} {...props} />
);
const PostFilter = (props) => (
<Filter {...props}>
<ReferenceInput label="Client"
source="client_id"
reference="clients"
allowEmpty
filterToQuery={searchText => ({ nickname: searchText })}>
<AutocompleteInput optionText="nickname" shouldRenderSuggestions={(val) => {
return val.trim().length > 2
}}/>
</ReferenceInput>
</Filter>
);
const PostsList = props => {
return (
<List {...props} perPage={15}
pagination={<PostPagination/>}
filters={<PostFilter/>}
exporter={false}>
<Datagrid>
<TextField source="nickname" sortable={false}/>
<DateField label="Created" source="created_at" showTime/>
</Datagrid>
</List>
);
};
Edit: same question goes for "search-as-you-type" fields like <TextInput> inside a <Filter> field, I started to ask a new question but realized it will be kind of a duplicate,
This is the code that also sends requests starting from 1 char, in this case there isn't even a shouldRenderSuggestions option to force it to send empty requests
const ClientFilter = (props) => (
<Filter {...props}>
<TextInput label="Search" source="str" alwaysOn/>
</Filter>
);
Live example of code in codesandbox.io
I stumbled upon this issue, too. The best I've come up with so far is a small wrapper component that prevents the ReferenceInput from triggering API requests unless a certain condition is met:
const ConditionalFilter = (props) => {
const { children, condition, setFilter } = props;
const conditionalSetFilter = (val) => {
if (setFilter && condition(val)) setFilter(val);
};
return React.cloneElement(children, { ...props, setFilter: conditionalSetFilter });
};
Used like this:
const condition = val => val.trim().length > 2;
return (
<ReferenceInput
source="…"
reference="…"
shouldRenderSuggestions={condition}
>
<ConditionalFilter condition={condition}>
<AutocompleteInput />
</ConditionalFilter>
</ReferenceInput>
);
Update for react-admin v3: (without the wrapper component, which is no longer necessary/useful)
const condition = (val) => !!val && val.trim().length > 2;
return (
<ReferenceInput
source="…"
reference="…"
filterToQuery={(val) => (condition(val) ? { name: val.trim() } : {})}
>
<AutocompleteInput shouldRenderSuggestions={condition} />
</ReferenceInput>
);