diff --git a/src/components/UI/Form/AutoComplete/AutoComplete.tsx b/src/components/UI/Form/AutoComplete/AutoComplete.tsx index 8c5774ac8..7bff38c80 100644 --- a/src/components/UI/Form/AutoComplete/AutoComplete.tsx +++ b/src/components/UI/Form/AutoComplete/AutoComplete.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import { Chip, FormHelperText, FormControl, Checkbox, Paper } from '@material-ui/core'; @@ -19,7 +19,10 @@ export interface AutocompleteProps { chipIcon?: any; getOptions?: any; validate?: any; + asyncValues?: any; noOptionsText?: any; + onChange?: any; + asyncSearch?: boolean; } export const AutoComplete: React.SFC = ({ @@ -34,13 +37,17 @@ export const AutoComplete: React.SFC = ({ multiple = true, disabled = false, getOptions, + asyncValues, + onChange, + asyncSearch = false, noOptionsText = 'No options available', }) => { const errorText = getIn(errors, field.name); const touchedVal = getIn(touched, field.name); const hasError = dirty && touchedVal && errorText !== undefined; - const [optionValue, setOptionValue] = React.useState([]); - const [open, setOpen] = React.useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [optionValue, setOptionValue] = useState([]); + const [open, setOpen] = useState(false); useEffect(() => { if (options.length > 0) { @@ -67,13 +74,28 @@ export const AutoComplete: React.SFC = ({ options={optionValue} getOptionLabel={(option: any) => (option[optionLabel] ? option[optionLabel] : '')} onChange={(event, value: any) => { + if (asyncSearch) { + const filterValues = asyncValues.value.filter( + (val: any) => val.id !== value[value.length - 1].id + ); + if (filterValues.length === value.length - 2) { + asyncValues.setValue(filterValues); + } else { + asyncValues.setValue([...value]); + } + setSearchTerm(''); + onChange(''); + } setFieldValue(field.name, value); }} + inputValue={asyncSearch ? searchTerm : undefined} value={ multiple - ? optionValue.filter((option: any) => - field.value.map((value: any) => value.id).includes(option.id) - ) + ? asyncSearch + ? asyncValues.value + : optionValue.filter((option: any) => + field.value.map((value: any) => value.id).includes(option.id) + ) : field.value } disabled={disabled} @@ -91,21 +113,46 @@ export const AutoComplete: React.SFC = ({ /> )) } - renderOption={(option, { selected }) => ( - - {multiple ? : ''} - {option[optionLabel]} - - )} - renderInput={(params) => ( - - )} + renderOption={(option, { selected }) => { + return ( + + {multiple ? ( + value.id).includes(option.id) + : selected + } + color="primary" + /> + ) : ( + '' + )} + {option[optionLabel]} + + ); + }} + renderInput={(params: any) => { + const asyncChange = asyncSearch + ? { + onChange: (event: any) => { + setSearchTerm(event.target.value); + return onChange(event.target.value); + }, + } + : null; + return ( + + ); + }} open={open} onOpen={() => { setOpen(true); diff --git a/src/components/UI/SearchDialogBox/SearchDialogBox.tsx b/src/components/UI/SearchDialogBox/SearchDialogBox.tsx index f2880034c..c9e9980b5 100644 --- a/src/components/UI/SearchDialogBox/SearchDialogBox.tsx +++ b/src/components/UI/SearchDialogBox/SearchDialogBox.tsx @@ -11,17 +11,29 @@ export interface SearchDialogBoxProps { options: any; selectedOptions: any; icon?: any; + optionLabel?: string; + onChange?: any; + asyncSearch?: boolean; } -export const SearchDialogBox = (props: any) => { +export const SearchDialogBox = (props: SearchDialogBoxProps) => { const [selectedOptions, setSelectedOptions] = useState>([]); + const [asyncSelectedOptions, setAsyncSelectedOptions] = useState>([]); useEffect(() => { - setSelectedOptions( - props.options.filter((option: any) => props.selectedOptions.includes(option.id)) - ); + if (!props.asyncSearch) { + setSelectedOptions( + props.options.filter((option: any) => props.selectedOptions.includes(option.id)) + ); + } }, [props.selectedOptions, props.options]); + useEffect(() => { + if (props.asyncSearch === true) { + setAsyncSelectedOptions(props.selectedOptions); + } + }, [props.selectedOptions]); + const changeValue = (event: any, value: any) => { setSelectedOptions(value); }; @@ -29,7 +41,13 @@ export const SearchDialogBox = (props: any) => { return ( props.handleOk(selectedOptions.map((option: any) => option.id))} + handleOk={() => + props.handleOk( + props.asyncSearch + ? asyncSelectedOptions.map((option: any) => option.id) + : selectedOptions.map((option: any) => option.id) + ) + } handleCancel={props.handleCancel} titleAlign="left" buttonOk="Save" @@ -37,9 +55,12 @@ export const SearchDialogBox = (props: any) => {
props.onChange(value)} form={{ setFieldValue: changeValue }} textFieldProps={{ label: 'Search', diff --git a/src/containers/Automation/AutomationList/AutomationList.tsx b/src/containers/Automation/AutomationList/AutomationList.tsx index 8aadf939e..2ef5ea02c 100644 --- a/src/containers/Automation/AutomationList/AutomationList.tsx +++ b/src/containers/Automation/AutomationList/AutomationList.tsx @@ -37,12 +37,14 @@ const columnAttributes = { }; const configureIcon = ; -const additionalAction = { - label: 'Configure', - icon: configureIcon, - parameter: 'uuid', - link: '/automation/configure', -}; +const additionalAction = [ + { + label: 'Configure', + icon: configureIcon, + parameter: 'uuid', + link: '/automation/configure', + }, +]; export const AutomationList: React.SFC = (props) => ( = (props) => { ); } - const additionalAction = { - icon: unblockIcon, - parameter: 'id', - dialog: setDialog, - }; + const additionalAction = [ + { + icon: unblockIcon, + parameter: 'id', + dialog: setDialog, + }, + ]; return ( <> - - + + + + + ); describe('', () => { @@ -28,4 +38,19 @@ describe('', () => { // TODO: test delete }); + + test('it should have add contact to group dialog box ', async () => { + setUserRole(['Admin']); + const { getByText, getAllByTestId } = render(wrapper); + + // loading is show initially + expect(getByText('Loading...')).toBeInTheDocument(); + await wait(); + expect(getAllByTestId('additionalButton')[0]).toBeInTheDocument(); + fireEvent.click(getAllByTestId('additionalButton')[0]); + + await wait(); + + expect(getByText('Add contacts to the group')).toBeInTheDocument(); + }); }); diff --git a/src/containers/Group/GroupList/GroupList.tsx b/src/containers/Group/GroupList/GroupList.tsx index 9d648142c..b8a589510 100644 --- a/src/containers/Group/GroupList/GroupList.tsx +++ b/src/containers/Group/GroupList/GroupList.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { GET_GROUPS_COUNT, FILTER_GROUPS, GET_GROUPS } from '../../../graphql/queries/Group'; -import { DELETE_GROUP } from '../../../graphql/mutations/Group'; +import { DELETE_GROUP, UPDATE_GROUP_CONTACTS } from '../../../graphql/mutations/Group'; import styles from './GroupList.module.css'; import { ReactComponent as GroupIcon } from '../../../assets/images/icons/Groups/Dark.svg'; import { ReactComponent as AutomationIcon } from '../../../assets/images/icons/Automations/Selected.svg'; @@ -11,6 +11,9 @@ import { DropdownDialog } from '../../../components/UI/DropdownDialog/DropdownDi import { ADD_AUTOMATION_TO_GROUP } from '../../../graphql/mutations/Automation'; import { setNotification } from '../../../common/notification'; import { displayUserGroups } from '../../../context/role'; +import { ReactComponent as AddContactIcon } from '../../../assets/images/icons/Contact/Add.svg'; +import { SearchDialogBox } from '../../../components/UI/SearchDialogBox/SearchDialogBox'; +import { CONTACT_SEARCH_QUERY, GET_GROUP_CONTACTS } from '../../../graphql/queries/Contact'; import { setVariables } from '../../../common/constants'; export interface GroupListProps {} @@ -43,9 +46,39 @@ const columnAttributes = { export const GroupList: React.SFC = (props) => { const client = useApolloClient(); const [addAutomationDialogShow, setAddAutomationDialogShow] = useState(false); + const [addContactsDialogShow, setAddContactsDialogShow] = useState(false); + const [contactSearchTerm, setContactSearchTerm] = useState(''); const [groupId, setGroupId] = useState(); const [getAutomations, { data: automationData }] = useLazyQuery(GET_AUTOMATIONS); + const [getContacts, { data: contactsData }] = useLazyQuery(CONTACT_SEARCH_QUERY, { + variables: setVariables({ name: contactSearchTerm }, 50), + }); + + const [getGroupContacts, { data: groupContactsData }] = useLazyQuery(GET_GROUP_CONTACTS); + const [updateGroupContacts] = useMutation(UPDATE_GROUP_CONTACTS, { + onCompleted: (data) => { + const numberDeleted = data.updateGroupContacts.numberDeleted; + const numberAdded = data.updateGroupContacts.groupContacts.length; + if (numberDeleted > 0 && numberAdded > 0) { + setNotification( + client, + `${numberDeleted} contact${ + numberDeleted === 1 ? '' : 's were' + } removed and ${numberAdded} contact${numberAdded == 1 ? '' : 's were'} added` + ); + } else if (numberDeleted > 0) { + setNotification( + client, + `${numberDeleted} contact${numberDeleted === 1 ? '' : 's were'} removed` + ); + } else { + setNotification(client, `${numberAdded} contact${numberAdded == 1 ? '' : 's were'} added`); + } + setAddContactsDialogShow(false); + }, + refetchQueries: [{ query: GET_GROUP_CONTACTS, variables: { id: groupId } }], + }); const [addAutomationToGroup] = useMutation(ADD_AUTOMATION_TO_GROUP, { onCompleted: (data: any) => { @@ -54,9 +87,17 @@ export const GroupList: React.SFC = (props) => { }, }); let automationOptions = []; + let contactOptions = []; + let groupContacts: Array = []; if (automationData) { automationOptions = automationData.flows; } + if (contactsData) { + contactOptions = contactsData.contacts; + } + if (groupContactsData) { + groupContacts = groupContactsData.group.group.contacts; + } let dialog = null; @@ -70,6 +111,13 @@ export const GroupList: React.SFC = (props) => { setAddAutomationDialogShow(true); }; + const setContactsDialog = (id: any) => { + getGroupContacts({ variables: { id } }); + getContacts(); + setGroupId(id); + setAddContactsDialogShow(true); + }; + const handleAutomationSubmit = (value: any) => { addAutomationToGroup({ variables: { @@ -92,14 +140,62 @@ export const GroupList: React.SFC = (props) => { ); } - const automationIcon = ; - const additionalAction = { - label: 'Start automation flow', - icon: automationIcon, - parameter: 'id', - dialog: setAutomationDialog, + const handleGroupAdd = (value: any) => { + const selectedContacts = value.filter( + (contact: any) => !groupContacts.map((groupContact: any) => groupContact.id).includes(contact) + ); + const unselectedContacts = groupContacts + .map((groupContact: any) => groupContact.id) + .filter((contact: any) => !value.includes(contact)); + + if (selectedContacts.length === 0 && unselectedContacts.length === 0) { + setAddContactsDialogShow(false); + } else { + updateGroupContacts({ + variables: { + input: { + addContactIds: selectedContacts, + groupId: groupId, + deleteContactIds: unselectedContacts, + }, + }, + }); + } }; + if (addContactsDialogShow) { + dialog = ( + setAddContactsDialogShow(false)} + options={contactOptions} + optionLabel="name" + asyncSearch={true} + selectedOptions={groupContacts} + onChange={(value: any) => { + setContactSearchTerm(value); + }} + /> + ); + } + const addContactIcon = ; + const automationIcon = ; + const additionalAction = [ + { + label: 'Add contacts to group', + icon: addContactIcon, + parameter: 'id', + dialog: setContactsDialog, + }, + { + label: 'Start automation flow', + icon: automationIcon, + parameter: 'id', + dialog: setAutomationDialog, + }, + ]; + const refetchQueries = { query: GET_GROUPS, variables: setVariables(), diff --git a/src/containers/List/List.tsx b/src/containers/List/List.tsx index 07b2d196f..e78c0c6b5 100644 --- a/src/containers/List/List.tsx +++ b/src/containers/List/List.tsx @@ -45,13 +45,13 @@ export interface ListProps { displayListType?: string; cardLink?: any; editSupport?: boolean; - additionalAction?: { + additionalAction?: Array<{ icon: any; parameter: string; link?: string; dialog?: any; label?: string; - } | null; + }>; deleteModifier?: { icon: string; variables: any; @@ -95,7 +95,7 @@ export const List: React.SFC = ({ filters = null, displayListType = 'list', cardLink = null, - additionalAction = null, + additionalAction = [], refetchQueries, backLinkButton, restrictedAction, @@ -311,32 +311,37 @@ export const List: React.SFC = ({ if (id) { return (
- {additionalAction && additionalAction.link ? ( - - - - {additionalAction.icon} - - - - ) : null} + {additionalAction.map((action: any, index: number) => { + if (action.link) { + return ( + + + + {action.icon} + + + + ); + } else if (action.dialog) { + return ( + + action.dialog(additionalActionParameter)} + > + {action.icon} + + + ); + } + })} - {additionalAction && additionalAction.dialog ? ( - - additionalAction.dialog(additionalActionParameter)} - > - {additionalAction.icon} - - - ) : null} {/* do not display edit & delete for staff role in group */} {displayUserGroups || listItem !== 'groups' ? ( <> @@ -358,9 +363,9 @@ export const List: React.SFC = ({ ? restrictedAction(listItem) : { chat: true, edit: true, delete: true }; let action: any; - if (additionalAction) { + if (additionalAction.length > 0) { // check if we are dealing with nested element - const params = additionalAction.parameter.split('.'); + const params = additionalAction[0].parameter.split('.'); if (params.length > 1) { action = listItem[params[0]][params[1]]; } else { diff --git a/src/containers/StaffManagement/StaffManagementList/StaffManagementList.tsx b/src/containers/StaffManagement/StaffManagementList/StaffManagementList.tsx index ba19d6dd3..80af6d81b 100644 --- a/src/containers/StaffManagement/StaffManagementList/StaffManagementList.tsx +++ b/src/containers/StaffManagement/StaffManagementList/StaffManagementList.tsx @@ -59,7 +59,7 @@ export const StaffManagementList: React.SFC = () => { }; const chatIcon = ; - const additionalAction = { icon: chatIcon, parameter: 'contact.id', link: '/chat' }; + const additionalAction = [{ icon: chatIcon, parameter: 'contact.id', link: '/chat' }]; const getRestrictedAction = (param: any) => { let action: any = { chat: true, edit: true, delete: true }; diff --git a/src/graphql/mutations/Group.ts b/src/graphql/mutations/Group.ts index 486e839e0..3c8ce0c6d 100644 --- a/src/graphql/mutations/Group.ts +++ b/src/graphql/mutations/Group.ts @@ -75,6 +75,7 @@ export const UPDATE_GROUP_CONTACTS = gql` id value } + numberDeleted } } `; diff --git a/src/mocks/Contact.tsx b/src/mocks/Contact.tsx index 9e947fb0f..5645ccbfb 100644 --- a/src/mocks/Contact.tsx +++ b/src/mocks/Contact.tsx @@ -3,11 +3,13 @@ import { GET_CONTACT, GET_CONTACT_DETAILS, GET_CONTACT_COUNT, + CONTACT_SEARCH_QUERY, } from '../graphql/queries/Contact'; import { getCurrentUserQuery } from './User'; import { filterTagsQuery } from './Tag'; import { getOrganizationLanguagesQuery, getOrganizationQuery } from '../mocks/Organization'; import { UPDATE_CONTACT } from '../graphql/mutations/Contact'; +import { setVariables } from '../common/constants'; export const contactGroupsQuery = { request: { @@ -133,3 +135,20 @@ export const countGroupContactsQuery = { }, }, }; +export const getContactsQuery = { + request: { + query: CONTACT_SEARCH_QUERY, + variables: setVariables({ name: '' }, 50, 0, 'ASC'), + }, + result: { + data: { + contacts: [ + { + id: '1', + name: 'Glific User', + phone: '9876543211', + }, + ], + }, + }, +}; diff --git a/src/mocks/Group.tsx b/src/mocks/Group.tsx index b88eeb6fb..fd47297fe 100644 --- a/src/mocks/Group.tsx +++ b/src/mocks/Group.tsx @@ -1,3 +1,5 @@ +import { setVariables } from '../common/constants'; +import { GET_GROUP_CONTACTS } from '../graphql/queries/Contact'; import { FILTER_GROUPS, GET_GROUP, @@ -91,18 +93,38 @@ export const countGroupQuery = { export const filterGroupQuery = { request: { query: FILTER_GROUPS, - variables: { filter: { label: '' } }, + variables: setVariables({ label: '' }, 10, 0, 'ASC'), + }, + result: { + data: { + groups: [ + { + id: '1', + label: 'Staff group', + description: 'Group for staff members', + isRestricted: false, + }, + ], + }, + }, +}; + +export const getGroupContactsQuery = { + request: { + query: GET_GROUP_CONTACTS, + variables: { id: '1' }, }, result: { data: { group: { - group: [ - { - id: '1', - label: 'Staff group', - isRestricted: true, - }, - ], + group: { + contacts: [ + { + id: '1', + name: 'Glific User', + }, + ], + }, }, }, },