Last active
August 13, 2024 09:44
-
-
Save MartinCura/a5a76241f528b9f718ab872623df1e97 to your computer and use it in GitHub Desktop.
react-admin (w/ hasura backend) components for many-to-many (M2M) relationship management
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* I couldn't find online any way of managing entities with a M2M relationship in react-admin with hasura backend, | |
* so here are mine. | |
* Example of what you can do: to the Edit view of your Movie resource, add DeletableRelatedResourceList | |
* to list and delete associated actors, and M2MRelatedResourceSelectInput to add new associated actors | |
* (with an intermediary table movie_actors). | |
* | |
* Done with [email protected] | |
*/ | |
import React from 'react'; | |
import { useApolloClient, useMutation } from '@apollo/client'; | |
import DeleteIcon from '@mui/icons-material/Delete'; | |
import { MenuItem, Select } from '@mui/material'; | |
import { | |
Datagrid, | |
ArrayField, | |
TextField, | |
Button, | |
useNotify, | |
useRefresh, | |
useRecordContext, | |
ReferenceArrayInput, | |
} from 'react-admin'; | |
// const deleteMovieActorMut = gql` | |
// mutation removeActorFromMovie($relationId: Int!) { | |
// delete_movie_actors_by_pk(id: $relationId) { | |
// id | |
// } | |
// } | |
// `; | |
// const addMovieActorMut = gql` | |
// mutation addActorToMovie( | |
// $resourceId: Int! | |
// $relatedResourceId: Int! | |
// ) { | |
// insert_movie_actors( | |
// objects: [ | |
// { movie_id: $resourceId, actor_id: $relatedResourceId } | |
// ] | |
// ) { | |
// returning { | |
// id | |
// } | |
// } | |
// } | |
// `; | |
/** | |
* List of a related resource in a many-to-many relationship where | |
* rows can be deleted and the intermediary table's row is removed. | |
* @param {string} props.relationship Name of the (array) relationship in hasura | |
* @param {string} props.relatedResource Name of the related resource in snake_case singular, | |
* e.g. actor | |
* @param {gql} props.deleteRelationMut GQL mutation to delete the relationship, which should | |
* receive the ID of the relationship as $relationId | |
* @param {string} props.label A label for the list | |
* | |
* Example: | |
* <DeletableRelatedResourceList | |
* relationship="movie__actors" | |
* relatedResource="actor" | |
* deleteRelationMut={deleteMovieActorMut} | |
* label="Actors" | |
* /> | |
*/ | |
export const DeletableM2MRelatedResourceList = ({ | |
relationship, | |
relatedResource, | |
deleteRelationMut, | |
label, | |
className, | |
}) => { | |
const apolloClient = useApolloClient(); | |
const refresh = useRefresh(); | |
const notify = useNotify(); | |
const [removeRelation, { loading }] = useMutation(deleteRelationMut, { | |
client: apolloClient, | |
onCompleted: () => { | |
refresh(); | |
}, | |
onError: () => { | |
notify('Error removing relation.'); | |
}, | |
}); | |
const RemoveButton = ({ record }) => ( | |
<Button | |
disabled={loading} | |
onClick={() => | |
removeRelation({ variables: { relationId: record.id } }) | |
} | |
> | |
<DeleteIcon /> | |
</Button> | |
); | |
return ( | |
<ArrayField | |
className={className} | |
source={relationship} | |
fieldKey={`${relatedResource}.id`} | |
label={label} | |
> | |
<Datagrid> | |
<TextField source={`${relatedResource}.id`} label="ID" /> | |
<TextField source={`${relatedResource}.name`} label="Name" /> | |
<RemoveButton /> | |
</Datagrid> | |
</ArrayField> | |
); | |
}; | |
/** | |
* Select input for a many-to-many relationship where selecting a related resource | |
* adds the appropriate row in the intermediary table. | |
* @param {string} props.relationship Name of the (array) relationship in hasura | |
* @param {string} props.source Name of the source resource | |
* @param {string} props.relatedResource Name of the related resource in snake_case singular, e.g. actor | |
* @param {string} props.addRelationMut GQL mutation to add the relationship, which should receive | |
* the ID of the source resource as $resourceId and the ID of the related resource as $relatedResourceId | |
* | |
* Example: | |
* <M2MRelatedResourceSelectInput | |
* relationship="movie__actors" | |
* resource="movie" | |
* relatedResource="actor" | |
* addRelationMut={addMovieActorMut} | |
* /> | |
*/ | |
export const M2MRelatedResourceSelectInput = ({ | |
relationship, | |
resource, | |
relatedResource, | |
addRelationMut, | |
...rest | |
}) => { | |
const record = useRecordContext(); | |
const apolloClient = useApolloClient(); | |
const refresh = useRefresh(); | |
const notify = useNotify(); | |
const [addRelation, { loading }] = useMutation(addRelationMut, { | |
client: apolloClient, | |
onCompleted: () => { | |
refresh(); | |
}, | |
onError: () => { | |
notify('Error adding relation.'); | |
}, | |
}); | |
const RelatedSelect = ({ choices }) => { | |
// don't show related resources already in this relationship | |
const filteredChoices = choices.filter( | |
relatedResource => | |
!relatedResource[relationship].some( | |
relation => relation[resource].id === record.id | |
) | |
); | |
const noneSelected = filteredChoices.length === choices.length; | |
return ( | |
<Select | |
onChange={event => | |
addRelation({ | |
variables: { | |
resourceId: record.id, | |
relatedResourceId: event.target.value, | |
}, | |
}) | |
} | |
value="" | |
disabled={loading || filteredChoices.length === 0} | |
style={{ width: '250px', margin: !noneSelected && '15px 0' }} | |
> | |
{filteredChoices.map(choice => ( | |
<MenuItem value={choice.id} key={choice.id}> | |
{`#${choice.id} ${choice.name}`} | |
</MenuItem> | |
))} | |
</Select> | |
); | |
}; | |
return ( | |
<ReferenceArrayInput | |
{...rest} | |
source={relationship} | |
reference={`${relatedResource}s`} | |
format={relations => | |
relations.map(relation => relation[relatedResource].id) | |
} | |
> | |
<RelatedSelect /> | |
</ReferenceArrayInput> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Yes