Get ReferenceManyField Count? - react-admin

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>

Related

Clear ReferenceArrayInput selection on filter change

I have 2 ReferenceArrayInputs, one of which is wrapped in a FormDataConsumer.
The first one is for category, the second for sub-category. The second one depends on the first one so I conditionally render it.
When I select a value for category, then select a value for subcategory, and finally change the category value again I still see my selection in the subcategory.
<ArrayInput source={`shops.${shopIndex}.categories`}>
<SimpleFormIterator disableReordering>
<ReferenceInput source="categoryId" reference="categories">
<SelectInput optionText="name" />
</ReferenceInput>
<FormDataConsumer>
{({ formData, getSource, scopedFormData, ...rest }) => {
// No category, no render. I can't filter subcategories.
if (!scopedFormData.categoryId) {
return null;
}
return (
<SelectArrayInput
source={getSource('subCategoryIds')}
reference="subCategories"
perPage={500}
filter={{ categories: [scopedFormData.categoryId] }}
>
<ReferenceArrayInput optionText="name" />
</SelectArrayInput>
);
}}
</FormDataConsumer>
</SimpleFormIterator>
</ArrayInput>
Filtering works great, toggling visibility looks great... I just want to clear the subcategory value when changing the category.
I already tried useFormContext() with a setValue and resetValue, but neither worked. I'm not sure if this is a caching issue or simply because it's an array.
Regarding versions, this is 4.1.0 all the way up to the master branch (tried all of them).

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>

To default to the first record returned from the ReferenceInput in SelectInput

Please how can I initialize the selectinput to the first record returned by the ReferenceInput?
For example, person resource returns this
persons: [
{"id":1,"name":"John"},
{"id":2,"name":"Peter"},
{"id":3,"name":"Paul"}
]
I want the selectinput to default to John being the first record from the persons resource
I saw a similar post where someone said one can use defaultValue property of the SelectInput, but it did not work for me.
Thanks
See the code below:
<ReferenceInput
label="RDM/WDM"
source="salesmgr"
reference="persons"
sort={{ field: 'id', order: 'ASC' }}
>
<SelectInput optionText="name"/>
</ReferenceInput>
Add the defaultValue property to ReferenceInput as follow:
<ReferenceInput
label="RDM/WDM"
source="salesmgr"
reference="persons"
sort={{ field: 'id', order: 'ASC' }}
defaultValue={1}
>
<SelectInput optionText="name"/>
</ReferenceInput>

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-Admin | Bulk-Select with ReferenceField

In our application, we're trying to use Datagrid within ReferenceField to create/modify/delete related records, as shown in https://marmelab.com/blog/2018/07/09/react-admin-tutorials-form-for-related-records.html
All the functionality shown in the tutorial works well, except the bulk-actions added in a recent react-admin update. Clicking these checkboxes gives
Uncaught TypeError: _this.props.onToggleItem is not a function
I believe this is because the onToggleItem prop is normally provided by the List component, however in this application, Datagrid doesn't have a parent List component.
Adding a List component between ReferenceManyField and Datagrid allows bulk select/delete to work after some changes to the style, however this causes another issue: the current displayed page (i.e. records 1-10, 11-20, etc) is stored per-resource in the store, and so it is possible to have a situation where the store says we're on page 2, and displays page 2, which is empty because there are only enough related items to fill one page.
Am I missing something here? Or is bulk-select inside ReferenceManyField not possible at the moment?
export const NetworksShow = (props) => (
<Show title={<NetworksTitle />} actions={<NetworksShowActions />} {...props}>
<ReferenceManyField addLabel={false} target="ipid" reference="acl-network">
<List style={{ margin: '0px -24px -16px -24px' }} {...props} actions={<NetworkACLCardActions ipid={props.id}/>} filter={{ ipid: _.has(props, 'id') ? props.id : undefined }}>
<Datagrid hasBulkActions={true}>
<ReferenceField label="Network" source="ipid" reference="networks">
<TextField source="name" />
</ReferenceField>
<TextField label="URL" source="url" />
<BWChip label="Action" source="wb" />
<EditButton />
<DeleteButton />
</Datagrid>
</List>
</ReferenceManyField>
</Show>
);
As a side-effect of https://github.com/marmelab/react-admin/pull/2365, it is now possible to use ReferenceManyField -> List -> Datagrid in the way described in the question.
For example, we're now doing the following:
<ReferenceManyField addLabel={false} target="groupid" reference="users">
<List
style={{ margin: '0px -24px -16px -24px' }}
filter={{ groupid: id }}
{...props}
>
<Datagrid hasBulkActions>
<LinkField label="Name" source="name" />
<LinkField label="Username" source="email" />
<FlexibleBooleanField label="Email" source="allowemail" />
<ACLChip label="User Access" source="aclid" />
</Datagrid>
</List>
</ReferenceManyField>
Bulk actions works with the above, and any issues with pagination are avoided as react-admin now checks and modifies pagination if nothing appears on the current page.
As I've understood from the documentation, Datagrid is just an iterator "dumb component".
It just "shows" things that the parent - usually List (connected component) or in your case ReferenceManyField - element previously has fetched.
Thus I think that BulkActions can only be functional when provided by a List element.
For the second part of your issue, Lists should be used top-level and not within other elements that's why it breaks your pagination.
I implemented "DumbList" which takes data from parent component instead of loading it itself. This solves the problem:
import React from 'react';
import { ListController } from 'ra-core';
import { ListView } from 'ra-ui-materialui/esm/list/List';
export const DumbList = props =>
<ListController {...props}>
{controllerProps => {
let { data } = props
const ids = Object.keys(data || {})
const total = ids.length
return <ListView
{...props}
// This is important, otherwise bulkActionsToolbar ends up on very top of the page
classes={{ card: 'relative' }}
{...Object.assign(controllerProps, { data, ids, total })} />
}}
</ListController>