Skip to content

Instantly share code, notes, and snippets.

@MartinCura
Last active April 24, 2024 19:36
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MartinCura/a5a76241f528b9f718ab872623df1e97 to your computer and use it in GitHub Desktop.
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
/**
* 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 react-admin@3.19.0
*/
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>
);
};
@abdifardin
Copy link

@MartinCura
May If its an alternative solution for react admin enterprise package

@MartinCura
Copy link
Author

@MartinCura May If its an alternative solution for react admin enterprise package

Yes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment