diff --git a/cypress/integration/edit.js b/cypress/integration/edit.js index c16a2fac369..64a882113fb 100644 --- a/cypress/integration/edit.js +++ b/cypress/integration/edit.js @@ -68,6 +68,41 @@ describe('Edit Page', () => { expect(el).to.have.value(date) ); }); + + it('should change reference list correctly when changing filter', () => { + const EditPostTagsPage = editPageFactory('/#/posts/13'); + EditPostTagsPage.navigate(); + EditPostTagsPage.gotoTab(3); + + // Music is selected by default + cy.get( + EditPostTagsPage.elements.input('tags', 'reference-array-input') + ) + .get(`div[role=button]`) + .contains('Music') + .should('exist'); + + EditPostTagsPage.clickInput('change-filter'); + + // Music should not be selected anymore after filter reset + cy.get( + EditPostTagsPage.elements.input('tags', 'reference-array-input') + ) + .get(`div[role=button]`) + .contains('Music') + .should('not.exist'); + + EditPostTagsPage.clickInput('tags', 'reference-array-input'); + + // Music should not be visible in the list after filter reset + cy.get('div[role=listbox]') + .contains('Music') + .should('not.exist'); + + cy.get('div[role=listbox]') + .contains('Photo') + .should('exist'); + }); }); it('should fill form correctly even when switching from one form type to another', () => { diff --git a/cypress/support/EditPage.js b/cypress/support/EditPage.js index 61c896d106c..c6084bfed06 100644 --- a/cypress/support/EditPage.js +++ b/cypress/support/EditPage.js @@ -6,6 +6,9 @@ export default url => ({ if (type === 'rich-text-input') { return `.ra-input-${name} .ql-editor`; } + if (type === 'reference-array-input') { + return `.ra-input div[role=combobox]`; + } return `.edit-page [name='${name}']`; }, inputs: `.ra-input`, @@ -37,8 +40,8 @@ export default url => ({ } }, - clickInput(name) { - cy.get(this.elements.input(name)).click(); + clickInput(name, type = 'input') { + cy.get(this.elements.input(name, type)).click(); }, gotoTab(index) { diff --git a/examples/simple/src/posts/PostEdit.js b/examples/simple/src/posts/PostEdit.js index c97042246b9..7350e37e0fe 100644 --- a/examples/simple/src/posts/PostEdit.js +++ b/examples/simple/src/posts/PostEdit.js @@ -2,7 +2,6 @@ import RichTextInput from 'ra-input-rich-text'; import React from 'react'; import { TopToolbar, - AutocompleteArrayInput, AutocompleteInput, ArrayInput, BooleanInput, @@ -18,7 +17,6 @@ import { ImageField, ImageInput, NumberInput, - ReferenceArrayInput, ReferenceManyField, ReferenceInput, SelectInput, @@ -32,6 +30,7 @@ import { FormDataConsumer, } from 'react-admin'; // eslint-disable-line import/no-unresolved import PostTitle from './PostTitle'; +import TagReferenceInput from './TagReferenceInput'; const EditActions = ({ basePath, data, hasShow }) => ( @@ -121,13 +120,11 @@ const PostEdit = ({ permissions, ...props }) => ( /> - - - + label="Tags" + /> diff --git a/examples/simple/src/posts/TagReferenceInput.js b/examples/simple/src/posts/TagReferenceInput.js new file mode 100644 index 00000000000..f529879ac90 --- /dev/null +++ b/examples/simple/src/posts/TagReferenceInput.js @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { useForm } from 'react-final-form'; +import { AutocompleteArrayInput, ReferenceArrayInput } from 'react-admin'; +import Button from '@material-ui/core/Button'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles({ + button: { + margin: '0 24px', + position: 'relative', + }, + input: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + width: '50%', + }, +}); + +const TagReferenceInput = ({ ...props }) => { + const classes = useStyles(); + const { change } = useForm(); + const [filter, setFilter] = useState(true); + + const handleAddFilter = () => { + setFilter(!filter); + change('tags', []); + }; + + return ( +
+ + + + +
+ ); +}; + +export default TagReferenceInput; diff --git a/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx b/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx index d7b4d6e56d0..2b38056cadf 100644 --- a/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx +++ b/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx @@ -27,7 +27,8 @@ describe('', () => { input={{ value: [1, 2] }} > {children} - + , + { admin: { resources: { tags: { data: {} } } } } ); expect(queryByText('true')).not.toBeNull(); @@ -43,7 +44,8 @@ describe('', () => { input={{ value: [1, 2] }} > {children} - + , + { admin: { resources: { tags: { data: {} } } } } ); expect(queryByText('true')).not.toBeNull(); }); @@ -110,6 +112,7 @@ describe('', () => { , { admin: { + resources: { tags: { data: {} } }, references: { possibleValues: { 'posts@tag_ids': { error: 'boom' }, @@ -234,6 +237,7 @@ describe('', () => { , { admin: { + resources: { tags: { data: { 5: {}, 6: {} } } }, references: { possibleValues: { 'posts@tag_ids': [], @@ -288,7 +292,8 @@ describe('', () => { const { dispatch } = renderWithRedux( {children} - + , + { admin: { resources: { tags: { data: {} } } } } ); expect(dispatch.mock.calls[0][0]).toEqual({ type: CRUD_GET_MATCHING, @@ -425,7 +430,8 @@ describe('', () => { input={{ value: [5, 6] }} > {children} - + , + { admin: { resources: { tags: { data: { 5: {}, 6: {} } } } } } ); await wait(() => { expect(dispatch).toHaveBeenCalledWith({ @@ -449,7 +455,8 @@ describe('', () => { input={{ value: [5] }} > {children} - + , + { admin: { resources: { tags: { data: { 5: {} } } } } } ); fireEvent.click(getByLabelText('Filter')); @@ -477,7 +484,8 @@ describe('', () => { input={{ value: [5] }} > {children} - + , + { admin: { resources: { tags: { data: { 5: {} } } } } } ); rerender( @@ -614,9 +622,9 @@ describe('', () => { input={{ value: [5, 6] }} > {children} - + , + { admin: { resources: { tags: { data: { 5: {}, 6: {} } } } } } ); - await wait(); expect(dispatch).toHaveBeenCalledWith({ type: CRUD_GET_MANY, diff --git a/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts b/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts index e8da163c4f3..0aabe0c8b10 100644 --- a/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts +++ b/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts @@ -56,13 +56,43 @@ const useReferenceArrayInputController = ({ // optimization: we fetch selected items only once. When the user selects more items, // as we already have the past selected items in the store, we don't fetch them. useEffect(() => { + // Only fetch new ids const newIdsToFetch = difference(input.value, inputValue.current); - if (newIdsToFetch.length > 0) { + // Only get from store ids selected and already fetched + const newIdsToGetFromStore = difference(input.value, newIdsToFetch); + /* + input.value (current) + +------------------------+ + | ********************** | + | ********************** | inputValue.current (old) + | ********** +-----------------------+ + | ********** | ooooooooo | | + | ********** | ooooooooo | | + | ********** | ooooooooo | | + | ********** | ooooooooo | | + +---|--------|------|----+ | + | | | | + | | | | + | +------|----------------+ + | | + newIdsToFetch newIdsToGetFromStore + */ + // Change states each time input values changes to avoid keeping previous values no more selected + if (!isEqual(idsToFetch, newIdsToFetch)) { setIdsToFetch(newIdsToFetch); - setIdsToGetFromStore(inputValue.current || []); } + if (!isEqual(idsToGetFromStore, newIdsToGetFromStore)) { + setIdsToGetFromStore(newIdsToGetFromStore); + } + inputValue.current = input.value; - }, [input.value, setIdsToFetch]); + }, [ + idsToFetch, + idsToGetFromStore, + input.value, + setIdsToFetch, + setIdsToGetFromStore, + ]); const [pagination, setPagination] = useState({ page: 1, perPage }); const [sort, setSort] = useState(defaultSort);