From bd678925c5227aa2a3ebad76eafe90d6815c8dfa Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Wed, 5 Apr 2023 11:06:35 +0200 Subject: [PATCH 1/3] feat(inventory groups): fixed 2 bugs for add host modal --- .../Modals/AddHostToGroupModal.js | 8 ++++++++ .../Modals/CreateGroupModal.js | 19 ++++++++++++++++--- .../InventoryGroups/Modals/Modal.js | 1 - .../Modals/ModalSchemas/schemes.js | 3 ++- .../InventoryGroups/Modals/SearchInput.js | 16 ++++++++++++---- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/components/InventoryGroups/Modals/AddHostToGroupModal.js b/src/components/InventoryGroups/Modals/AddHostToGroupModal.js index 0d8dc8eb5..9b88f5b5f 100644 --- a/src/components/InventoryGroups/Modals/AddHostToGroupModal.js +++ b/src/components/InventoryGroups/Modals/AddHostToGroupModal.js @@ -9,6 +9,7 @@ import SearchInput from './SearchInput'; import { fetchGroups } from '../../../store/inventory-actions'; import { addHostSchema } from './ModalSchemas/schemes'; import CreateGroupModal from './CreateGroupModal'; +import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; const AddHostToGroupModal = ({ isModalOpen, @@ -16,6 +17,9 @@ const AddHostToGroupModal = ({ modalState, reloadData }) => { + // change is a parf ot DFD api and required to properly disable "add" button + // eslint-disable-next-line no-unused-vars + const { change } = useFormApi(); const dispatch = useDispatch(); //we have to fetch groups to make them available in state useEffect(() => { @@ -70,6 +74,10 @@ const AddHostToGroupModal = ({ isModalOpen={isCreateGroupModalOpen} setIsModalOpen={setIsCreateGroupModalOpen} reloadData={() => console.log('data reloaded')} + //modal before prop tells create group modal that it should + //reopen add host modal when user closes create group modal + modalBefore={true} + setterOfModalBefore={setIsModalOpen} /> )} diff --git a/src/components/InventoryGroups/Modals/CreateGroupModal.js b/src/components/InventoryGroups/Modals/CreateGroupModal.js index 48dd78803..678e20d68 100644 --- a/src/components/InventoryGroups/Modals/CreateGroupModal.js +++ b/src/components/InventoryGroups/Modals/CreateGroupModal.js @@ -13,7 +13,9 @@ import awesomeDebouncePromise from 'awesome-debounce-promise'; const CreateGroupModal = ({ isModalOpen, setIsModalOpen, - reloadData + reloadData, + modalBefore = false, + setterOfModalBefore }) => { const dispatch = useDispatch(); @@ -46,10 +48,19 @@ const CreateGroupModal = ({ return createGroupSchema(d); }, []); + const onClose = () => { + if (modalBefore) { + setIsModalOpen(false); + setterOfModalBefore(true); + } else { + setIsModalOpen(false); + } + }; + return ( setIsModalOpen(false)} + closeModal={onClose} title="Create group" submitLabel="Create" schema={schema} @@ -64,5 +75,7 @@ export default CreateGroupModal; CreateGroupModal.propTypes = { isModalOpen: PropTypes.bool, setIsModalOpen: PropTypes.func, - reloadData: PropTypes.func + reloadData: PropTypes.func, + modalBefore: PropTypes.bool, + setterOfModalBefore: PropTypes.func }; diff --git a/src/components/InventoryGroups/Modals/Modal.js b/src/components/InventoryGroups/Modals/Modal.js index a562a8860..c700496bd 100644 --- a/src/components/InventoryGroups/Modals/Modal.js +++ b/src/components/InventoryGroups/Modals/Modal.js @@ -70,7 +70,6 @@ RepoModal.propTypes = { size: PropTypes.string, additionalMappers: PropTypes.object, titleIconVariant: PropTypes.any, - validatorMapper: PropTypes.object, customFormTemplate: PropTypes.node }; diff --git a/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js b/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js index 02f6c5512..e1954cf5a 100644 --- a/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js +++ b/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js @@ -65,7 +65,8 @@ export const addHostSchema = (systemName) => ({ name: 'group', label: 'Select a group', isRequired: true, - validate: [{ type: validatorTypes.REQUIRED }] + validate: [{ type: validatorTypes.REQUIRED }], + validateOnMount: true }, { component: 'create-group-btn', name: 'create-group-btn' } ] diff --git a/src/components/InventoryGroups/Modals/SearchInput.js b/src/components/InventoryGroups/Modals/SearchInput.js index ee08987c4..4b85d6431 100644 --- a/src/components/InventoryGroups/Modals/SearchInput.js +++ b/src/components/InventoryGroups/Modals/SearchInput.js @@ -7,8 +7,10 @@ import { SelectOption } from '@patternfly/react-core'; import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; +import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api'; -const SearchInput = () => { +const SearchInput = (props) => { + useFieldApi(props); const { change } = useFormApi(); const [isLoading, setIsLoading] = useState(true); //fetch data from the store @@ -36,7 +38,8 @@ const SearchInput = () => { const [isOpen, setIsOpen] = useState(false); const [selected, setSelected] = useState(null); - const [searchTerm, setSearchTerm] = useState(''); + + const [searchTerm, setSearchTerm] = useState(); const onToggle = (isOpen) => { setIsOpen(isOpen); @@ -47,7 +50,10 @@ const SearchInput = () => { setSelected(value); setIsOpen(false); //this is requried to make select component pass the saved data up to the modal - change('group', value); + const found = storeGroups.some(el => el.name === value.name); + if (found || searchTerm === '') { + change('group', value); + } }; const clearSelection = () => { @@ -88,6 +94,7 @@ const SearchInput = () => { isOpen={isOpen} aria-labelledby="typeahead-select-id-1" placeholderText="Type or click to select a group" + noResultsFoundText={isLoading ? 'Loading...' : 'No results found'} isInputValuePersisted={true} maxHeight={'180px'} > @@ -98,7 +105,8 @@ const SearchInput = () => { key={DeviceGroup.ID} value={{ toString: () => DeviceGroup.Name, - groupId: DeviceGroup.ID + groupId: DeviceGroup.ID, + name: DeviceGroup.Name }} {...(DeviceGroup.description && { description: DeviceGroup.description From 9e2e3d6ad49d190acd81efa3523e8203671f706c Mon Sep 17 00:00:00 2001 From: Georgy Karataev Date: Wed, 5 Apr 2023 13:58:00 +0200 Subject: [PATCH 2/3] Use built-in searchable select component --- .../Modals/AddHostToGroupModal.js | 16 +-- .../Modals/ModalSchemas/schemes.js | 17 ++- .../InventoryGroups/Modals/SearchInput.js | 121 ------------------ .../Modals/__tests__/SearchInput.test.js | 26 ---- 4 files changed, 17 insertions(+), 163 deletions(-) delete mode 100644 src/components/InventoryGroups/Modals/SearchInput.js delete mode 100644 src/components/InventoryGroups/Modals/__tests__/SearchInput.test.js diff --git a/src/components/InventoryGroups/Modals/AddHostToGroupModal.js b/src/components/InventoryGroups/Modals/AddHostToGroupModal.js index 9b88f5b5f..98f89dcc2 100644 --- a/src/components/InventoryGroups/Modals/AddHostToGroupModal.js +++ b/src/components/InventoryGroups/Modals/AddHostToGroupModal.js @@ -3,13 +3,11 @@ import PropTypes from 'prop-types'; import Modal from './Modal'; import { addHostToGroup } from '../utils/api'; import apiWithToast from '../utils/apiWithToast'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { CreateGroupButton } from '../SmallComponents/CreateGroupButton'; -import SearchInput from './SearchInput'; import { fetchGroups } from '../../../store/inventory-actions'; import { addHostSchema } from './ModalSchemas/schemes'; import CreateGroupModal from './CreateGroupModal'; -import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; const AddHostToGroupModal = ({ isModalOpen, @@ -17,15 +15,14 @@ const AddHostToGroupModal = ({ modalState, reloadData }) => { - // change is a parf ot DFD api and required to properly disable "add" button - // eslint-disable-next-line no-unused-vars - const { change } = useFormApi(); const dispatch = useDispatch(); //we have to fetch groups to make them available in state useEffect(() => { dispatch(fetchGroups()); - }, []); + + const groups = useSelector(({ groups }) => groups?.data?.results); + const [isCreateGroupModalOpen, setIsCreateGroupModalOpen] = useState(false); const handleAddDevices = (values) => { @@ -52,11 +49,8 @@ const AddHostToGroupModal = ({ closeModal={() => setIsModalOpen(false)} title="Add to group" submitLabel="Add" - schema={addHostSchema(modalState.name)} + schema={addHostSchema(modalState.name, groups)} additionalMappers={{ - 'search-input': { - component: SearchInput - }, 'create-group-btn': { component: CreateGroupButton, closeModal: () => { diff --git a/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js b/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js index e1954cf5a..5d61ece11 100644 --- a/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js +++ b/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js @@ -53,7 +53,7 @@ const createDescription = (systemName) => { //this is a custom schema that is passed via additional mappers to the Modal component //it allows to create custom item types in the modal -export const addHostSchema = (systemName) => ({ +export const addHostSchema = (systemName, groups) => ({ fields: [ { component: componentTypes.PLAIN_TEXT, @@ -61,13 +61,20 @@ export const addHostSchema = (systemName) => ({ label: createDescription(systemName) }, { - component: 'search-input', + component: 'select', name: 'group', label: 'Select a group', + simpleValue: true, + isSearchable: true, // enables typeahead isRequired: true, - validate: [{ type: validatorTypes.REQUIRED }], - validateOnMount: true + isClearable: true, + placeholder: 'Type or click to select a group', + options: (groups || []).map(({ id, name }) => ({ + label: name, + value: id + })), + validate: [{ type: validatorTypes.REQUIRED }] }, - { component: 'create-group-btn', name: 'create-group-btn' } + { component: 'create-group-btn', name: 'create-group-btn', isRequired: true } ] }); diff --git a/src/components/InventoryGroups/Modals/SearchInput.js b/src/components/InventoryGroups/Modals/SearchInput.js deleted file mode 100644 index 4b85d6431..000000000 --- a/src/components/InventoryGroups/Modals/SearchInput.js +++ /dev/null @@ -1,121 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useSelector } from 'react-redux'; -import { - HelperText, - HelperTextItem, - Select, - SelectOption -} from '@patternfly/react-core'; -import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; -import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api'; - -const SearchInput = (props) => { - useFieldApi(props); - const { change } = useFormApi(); - const [isLoading, setIsLoading] = useState(true); - //fetch data from the store - const storeGroups = useSelector(({ groups }) => groups?.data?.results); - - //select options is a constructed array of objects with values for dropdown - const [selectOptions, setSelectOptions] = useState([]); - //when storeGroups is changed - we create selectOptions - useEffect(() => { - setSelectOptions( - (storeGroups || []).reduce((acc, group) => { - acc.push({ - DeviceGroup: { - ID: group.id, - Name: group.name, - UpdatedAt: group.updated_at, - CreatedAt: group.created_at - } - }); - return acc; - }, []) - ); - setIsLoading(false); - }, [storeGroups]); - - const [isOpen, setIsOpen] = useState(false); - const [selected, setSelected] = useState(null); - - const [searchTerm, setSearchTerm] = useState(); - - const onToggle = (isOpen) => { - setIsOpen(isOpen); - }; - - const updateSelection = (value) => { - // Update state when an option has been selected. - setSelected(value); - setIsOpen(false); - //this is requried to make select component pass the saved data up to the modal - const found = storeGroups.some(el => el.name === value.name); - if (found || searchTerm === '') { - change('group', value); - } - }; - - const clearSelection = () => { - setSearchTerm(''); - updateSelection(null); - setIsOpen(false); - }; - - const onSelect = (_event, selection, isPlaceholder) => { - if (isPlaceholder) { - clearSelection(); - } - else { - updateSelection(selection); - } - }; - - return ( - <> - - {!isLoading && !selected && isOpen && selectOptions.length ? ( - - Over {selectOptions.length} results found. Refine your search. - - ) : ( - - Select a group - - )} - - - - ); -}; - -export default SearchInput; diff --git a/src/components/InventoryGroups/Modals/__tests__/SearchInput.test.js b/src/components/InventoryGroups/Modals/__tests__/SearchInput.test.js deleted file mode 100644 index b5d927ab4..000000000 --- a/src/components/InventoryGroups/Modals/__tests__/SearchInput.test.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable camelcase */ - -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import SearchInput from '../SearchInput'; -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; -import groups from '../../../../../cypress/fixtures/groups.json'; - -describe('SearchInput', () => { - let mockStore; - const initialStore = { - groups - }; - beforeEach(() => { - mockStore = configureStore(); - }); - - test('displays select options when the user clicks on the component', async () => { - const store = mockStore(initialStore); - render(); - fireEvent.click(screen.getByRole('textbox', { placeholder: 'Type or click to select a group' })); - const options = await screen.findAllByRole('option'); - expect(options).toHaveLength(1); - }); -}); From a9be12ba4093250f86fd30bbeb28332a5f11763a Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Thu, 6 Apr 2023 12:28:25 +0200 Subject: [PATCH 3/3] feat(inventory groups): fixed 1 more bug --- .../InventoryGroups/Modals/AddHostToGroupModal.js | 6 +++--- src/components/InventoryGroups/Modals/Modal.js | 1 + .../InventoryGroups/Modals/ModalSchemas/schemes.js | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/InventoryGroups/Modals/AddHostToGroupModal.js b/src/components/InventoryGroups/Modals/AddHostToGroupModal.js index 98f89dcc2..36ea5541d 100644 --- a/src/components/InventoryGroups/Modals/AddHostToGroupModal.js +++ b/src/components/InventoryGroups/Modals/AddHostToGroupModal.js @@ -15,6 +15,7 @@ const AddHostToGroupModal = ({ modalState, reloadData }) => { + const dispatch = useDispatch(); //we have to fetch groups to make them available in state useEffect(() => { @@ -24,15 +25,14 @@ const AddHostToGroupModal = ({ const groups = useSelector(({ groups }) => groups?.data?.results); const [isCreateGroupModalOpen, setIsCreateGroupModalOpen] = useState(false); - const handleAddDevices = (values) => { const { group } = values; const statusMessages = { onSuccess: { title: 'Success', - description: `System(s) have been added to ${group.toString()} successfully` + description: `System(s) have been added to ${group.name} successfully` }, - onError: { title: 'Error', description: `Failed to add ${modalState.name} to ${modalState.groupName}` } + onError: { title: 'Error', description: `Failed to add ${modalState.name} to ${group.name}` } }; apiWithToast( diff --git a/src/components/InventoryGroups/Modals/Modal.js b/src/components/InventoryGroups/Modals/Modal.js index c700496bd..71bcd3300 100644 --- a/src/components/InventoryGroups/Modals/Modal.js +++ b/src/components/InventoryGroups/Modals/Modal.js @@ -52,6 +52,7 @@ const RepoModal = ({ closeModal(); }} onCancel={() => closeModal()} + subscription={{ values: true }} /> ); diff --git a/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js b/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js index 5d61ece11..7d52f187e 100644 --- a/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js +++ b/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js @@ -71,7 +71,7 @@ export const addHostSchema = (systemName, groups) => ({ placeholder: 'Type or click to select a group', options: (groups || []).map(({ id, name }) => ({ label: name, - value: id + value: { name, id } })), validate: [{ type: validatorTypes.REQUIRED }] },