From c8ff6046d536a3ddd92f588d601070c7c75d2832 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 17 Oct 2023 08:49:03 +0100 Subject: [PATCH 1/2] Refactored HrmForm to save contact via redux --- plugin-hrm-form/src/components/HrmForm.tsx | 28 ++++-- .../components/tabbedForms/TabbedForms.tsx | 92 +++++++++++-------- plugin-hrm-form/src/types/types.ts | 14 ++- 3 files changed, 83 insertions(+), 51 deletions(-) diff --git a/plugin-hrm-form/src/components/HrmForm.tsx b/plugin-hrm-form/src/components/HrmForm.tsx index 5cee0a4814..8f126b0b8e 100644 --- a/plugin-hrm-form/src/components/HrmForm.tsx +++ b/plugin-hrm-form/src/components/HrmForm.tsx @@ -15,8 +15,8 @@ */ /* eslint-disable react/prop-types */ -import React from 'react'; -import { connect } from 'react-redux'; +import React, { Dispatch } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; import { CaseLayout } from '../styles/case'; import CallTypeButtons from './callTypeButtons'; @@ -24,11 +24,13 @@ import TabbedForms from './tabbedForms'; import Case from './case'; import CSAMReport from './CSAMReport/CSAMReport'; import { RootState } from '../states'; -import type { CustomITask, Case as CaseForm } from '../types/types'; +import type { CustomITask, Case as CaseForm, Contact } from '../types/types'; import { newContactCSAMApi } from './CSAMReport/csamReportApi'; -import { completeTask, submitContactForm } from '../services/formSubmissionHelpers'; +import { completeTask } from '../services/formSubmissionHelpers'; import findContactByTaskSid from '../states/contacts/findContactByTaskSid'; import { namespace, routingBase } from '../states/storeNamespaces'; +import { ContactMetadata } from '../states/contacts/types'; +import { submitContactFormAsyncAction } from '../states/contacts/saveContact'; type OwnProps = { task: CustomITask; @@ -36,14 +38,14 @@ type OwnProps = { }; // eslint-disable-next-line no-use-before-define -type Props = OwnProps & ReturnType; +type Props = OwnProps & ConnectedProps; -const HrmForm: React.FC = ({ routing, task, featureFlags, savedContact, metadata }) => { +const HrmForm: React.FC = ({ routing, task, featureFlags, savedContact, metadata, finaliseContact }) => { if (!routing) return null; const { route } = routing; const onNewCaseSaved = async (caseForm: CaseForm) => { - await submitContactForm(task, savedContact, metadata, caseForm); + await finaliseContact(savedContact, metadata, caseForm); await completeTask(task); }; @@ -52,6 +54,7 @@ const HrmForm: React.FC = ({ routing, task, featureFlags, savedContact, m return ( @@ -82,4 +85,13 @@ const mapStateToProps = (state: RootState, { task }: OwnProps) => { return { routing: routingState.tasks[task.taskSid], savedContact, metadata }; }; -export default connect(mapStateToProps, null)(HrmForm); +const mapDispatchToProps = (dispatch: Dispatch, { task }: OwnProps) => { + return { + finaliseContact: (contact: Contact, metadata: ContactMetadata, caseForm: CaseForm) => + dispatch(submitContactFormAsyncAction(task, contact, metadata, caseForm)), + }; +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +export default connector(HrmForm); diff --git a/plugin-hrm-form/src/components/tabbedForms/TabbedForms.tsx b/plugin-hrm-form/src/components/tabbedForms/TabbedForms.tsx index 72a9eed10e..0c3fc09779 100644 --- a/plugin-hrm-form/src/components/tabbedForms/TabbedForms.tsx +++ b/plugin-hrm-form/src/components/tabbedForms/TabbedForms.tsx @@ -17,7 +17,7 @@ /* eslint-disable react/no-multi-comp */ /* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable react/prop-types */ -import React from 'react'; +import React, { Dispatch } from 'react'; import SearchIcon from '@material-ui/icons/Search'; import { FormProvider, useForm } from 'react-hook-form'; import { connect, ConnectedProps } from 'react-redux'; @@ -30,8 +30,8 @@ import { RootState } from '../../states'; import { removeOfflineContact } from '../../services/formSubmissionHelpers'; import { changeRoute } from '../../states/routing/actions'; import { emptyCategories } from '../../states/contacts/reducer'; -import { NewCaseSubroutes, TabbedFormSubroutes } from '../../states/routing/types'; -import { ContactRawJson, CustomITask, isOfflineContactTask, Contact } from '../../types/types'; +import { AppRoutes, NewCaseSubroutes, TabbedFormSubroutes } from '../../states/routing/types'; +import { ContactRawJson, CustomITask, isOfflineContactTask, Contact, isOfflineContact } from '../../types/types'; import { Box, Row, StyledTabs, TabbedFormsContainer, TabbedFormTabContainer } from '../../styles/HrmStyles'; import FormTab from '../common/forms/FormTab'; import Search from '../search'; @@ -46,14 +46,15 @@ import CSAMReportButton from './CSAMReportButton'; import CSAMAttachments from './CSAMAttachments'; import { forExistingContact } from '../../states/contacts/issueCategorizationStateApi'; import { newCSAMReportActionForContact } from '../../states/csam-report/actions'; -import { CSAMReportTypes } from '../../states/csam-report/types'; +import { CSAMReportType, CSAMReportTypes } from '../../states/csam-report/types'; // Ensure ww import any custom components that might be used in a form import '../contact/ResourceReferralList'; -import { updateDraft } from '../../states/contacts/existingContacts'; +import { ContactDraftChanges, updateDraft } from '../../states/contacts/existingContacts'; import { getUnsavedContact } from '../../states/contacts/getUnsavedContact'; import asyncDispatch from '../../states/asyncDispatch'; import { updateContactInHrmAsyncAction } from '../../states/contacts/saveContact'; import { configurationBase, contactFormsBase, namespace, routingBase } from '../../states/storeNamespaces'; +import { setEditContactPageOpen } from '../../states/contacts/actions'; // eslint-disable-next-line react/display-name const mapTabsComponents = (errors: any) => (t: TabbedFormSubroutes) => { @@ -77,10 +78,10 @@ const mapTabsComponents = (errors: any) => (t: TabbedFormSubroutes) => { const isEmptyCallType = callType => [null, undefined, ''].includes(callType); -const mapTabsToIndex = (task: CustomITask, contactForm: Partial): TabbedFormSubroutes[] => { +const mapTabsToIndex = (contact: Contact, contactForm: Partial): TabbedFormSubroutes[] => { const isCallerType = contactForm.callType === callTypes.caller; - if (isOfflineContactTask(task)) { + if (isOfflineContact(contact)) { if (isNonDataCallType(contactForm.callType)) return ['contactlessTask']; return isCallerType @@ -97,6 +98,7 @@ const mapTabsToIndex = (task: CustomITask, contactForm: Partial) type OwnProps = { task: CustomITask; + contactId: string; csamReportEnabled: boolean; csamClcReportEnabled: boolean; }; @@ -105,9 +107,7 @@ type OwnProps = { type Props = OwnProps & ConnectedProps; const TabbedForms: React.FC = ({ - dispatch, routing, - task, savedContact, draftContact, updatedContact, @@ -116,6 +116,14 @@ const TabbedForms: React.FC = ({ csamClcReportEnabled, editContactFormOpen, isCallTypeCaller, + updateDraftForm, + newCSAMReport, + saveDraft, + clearCallType, + openCSAMReport, + backToCallTypeSelect, + navigateToTab, + task, }) => { const methods = useForm({ shouldFocusError: false, @@ -146,35 +154,32 @@ const TabbedForms: React.FC = ({ if (!currentDefinitionVersion) return null; - const taskId = task.taskSid; const isCallerType = updatedContact.rawJson.callType === callTypes.caller; const onSelectSearchResult = (searchResult: Contact) => { const selectedIsCaller = searchResult.rawJson.callType === callTypes.caller; if (isCallerType && selectedIsCaller && isCallTypeCaller) { - dispatch( - updateDraft(savedContact.id, { rawJson: { callerInformation: searchResult.rawJson.callerInformation } }), - ); - dispatch(changeRoute({ route: 'tabbed-forms', subroute: 'callerInformation' }, taskId)); + updateDraftForm({ callerInformation: searchResult.rawJson.callerInformation }); + navigateToTab('callerInformation'); } else { - dispatch(updateDraft(savedContact.id, { rawJson: { childInformation: searchResult.rawJson.childInformation } })); - dispatch(changeRoute({ route: 'tabbed-forms', subroute: 'childInformation' }, taskId)); + updateDraftForm({ childInformation: searchResult.rawJson.childInformation }); + navigateToTab('childInformation'); } }; const handleBackButton = async () => { if (!hasTaskControl(task)) return; - await asyncDispatch(dispatch)(updateContactInHrmAsyncAction(savedContact, { rawJson: { callType: '' } }, taskId)); - dispatch(changeRoute({ route: 'select-call-type' }, taskId)); + await clearCallType(savedContact); + backToCallTypeSelect(); }; - const tabsToIndex = mapTabsToIndex(task, getUnsavedContact(savedContact, draftContact).rawJson); + const tabsToIndex = mapTabsToIndex(savedContact, getUnsavedContact(savedContact, draftContact).rawJson); const tabs = tabsToIndex.map(mapTabsComponents(methods.errors)); const handleTabsChange = async (t: number) => { const tab = tabsToIndex[t]; - await asyncDispatch(dispatch)(updateContactInHrmAsyncAction(savedContact, draftContact, taskId)); - dispatch(changeRoute({ route: 'tabbed-forms', subroute: tab, autoFocus: false }, taskId)); + await saveDraft(savedContact, draftContact); + navigateToTab(tab); }; const { subroute, autoFocus } = routing; @@ -191,7 +196,7 @@ const TabbedForms: React.FC = ({ } const optionalButtons = - isOfflineContactTask(task) && subroute === 'contactlessTask' + isOfflineContact(savedContact) && subroute === 'contactlessTask' ? [ { label: 'CancelOfflineContact', @@ -216,12 +221,12 @@ const TabbedForms: React.FC = ({ csamClcReportEnabled={csamClcReportEnabled} csamReportEnabled={csamReportEnabled} handleChildCSAMType={() => { - dispatch(newCSAMReportActionForContact(savedContact.id, CSAMReportTypes.CHILD, true)); - dispatch(changeRoute({ route: 'csam-report', subroute: 'form', previousRoute: routing }, taskId)); + newCSAMReport(CSAMReportTypes.CHILD); + openCSAMReport(routing); }} handleCounsellorCSAMType={() => { - dispatch(newCSAMReportActionForContact(savedContact.id, CSAMReportTypes.COUNSELLOR, true)); - dispatch(changeRoute({ route: 'csam-report', subroute: 'form', previousRoute: routing }, taskId)); + newCSAMReport(CSAMReportTypes.COUNSELLOR); + openCSAMReport(routing); }} /> @@ -270,10 +275,8 @@ const TabbedForms: React.FC = ({ initialValues={callerInformation} display={subroute === 'callerInformation'} autoFocus={autoFocus} - updateFormActionDispatcher={dispatch => values => - dispatch( - updateDraft(savedContact.id, { rawJson: { callerInformation: values.callerInformation } }), - )} + updateFormActionDispatcher={() => values => + updateDraftForm({ callerInformation: values.callerInformation })} contactId={savedContact.id} /> @@ -328,7 +331,7 @@ const TabbedForms: React.FC = ({ contactId={savedContact.id} task={task} nextTab={() => handleTabsChange(tabIndex + 1)} - saveUpdates={() => asyncDispatch(dispatch)(updateContactInHrmAsyncAction(savedContact, draftContact))} + saveUpdates={() => saveDraft(savedContact, draftContact)} // TODO: move this two functions to a separate file to centralize "handle task completions" showNextButton={tabIndex !== 0 && tabIndex < tabs.length - 1} showSubmitButton={showSubmitButton} @@ -344,12 +347,9 @@ const TabbedForms: React.FC = ({ TabbedForms.displayName = 'TabbedForms'; -const mapStateToProps = (state: RootState, ownProps: OwnProps) => { - const routing = state[namespace][routingBase].tasks[ownProps.task.taskSid]; - const { savedContact, draftContact, metadata } = - Object.values(state[namespace][contactFormsBase].existingContacts).find( - cs => cs.savedContact.taskId === ownProps.task.taskSid, - ) ?? {}; +const mapStateToProps = (state: RootState, { task, contactId }: OwnProps) => { + const routing = state[namespace][routingBase].tasks[task.taskSid]; + const { savedContact, draftContact, metadata } = state[namespace][contactFormsBase].existingContacts[contactId] || {}; const editContactFormOpen = state[namespace][contactFormsBase].editingContact; const { currentDefinitionVersion } = state[namespace][configurationBase]; const { isCallTypeCaller } = state[namespace][contactFormsBase]; @@ -365,7 +365,23 @@ const mapStateToProps = (state: RootState, ownProps: OwnProps) => { }; }; -const connector = connect(mapStateToProps); +const mapDispatchToProps = (dispatch: Dispatch, { contactId, task }: OwnProps) => ({ + updateDraftForm: (form: Partial) => dispatch(updateDraft(contactId, { rawJson: form })), + saveDraft: (savedContact: Contact, draftContact: ContactDraftChanges) => + asyncDispatch(dispatch)(updateContactInHrmAsyncAction(savedContact, draftContact, task.taskSid)), + clearCallType: (savedContact: Contact) => + asyncDispatch(dispatch)(updateContactInHrmAsyncAction(savedContact, { rawJson: { callType: '' } }, task.taskSid)), + newCSAMReport: (csamReportType: CSAMReportType) => + dispatch(newCSAMReportActionForContact(contactId, csamReportType, true)), + openCSAMReport: (previousRoute: AppRoutes) => + dispatch(changeRoute({ route: 'csam-report', subroute: 'form', previousRoute }, task.taskSid)), + navigateToTab: (tab: TabbedFormSubroutes) => + dispatch(changeRoute({ route: 'tabbed-forms', subroute: tab, autoFocus: false }, task.taskSid)), + backToCallTypeSelect: () => dispatch(changeRoute({ route: 'select-call-type' }, task.taskSid)), + setModalLayout: () => dispatch(setEditContactPageOpen()), +}); + +const connector = connect(mapStateToProps, mapDispatchToProps); const connected = connector(TabbedForms); export default connected; diff --git a/plugin-hrm-form/src/types/types.ts b/plugin-hrm-form/src/types/types.ts index 5807d7744e..0b54af8a15 100644 --- a/plugin-hrm-form/src/types/types.ts +++ b/plugin-hrm-form/src/types/types.ts @@ -15,12 +15,12 @@ */ /* eslint-disable import/no-unused-modules */ -import { ITask } from '@twilio/flex-ui'; -import { CallTypes, DefinitionVersionId } from 'hrm-form-definitions'; +import type { ITask } from '@twilio/flex-ui'; +import type { CallTypes, DefinitionVersionId } from 'hrm-form-definitions'; -import { DateFilterValue } from '../components/caseList/filters/dateFilters'; -import { ChannelTypes } from '../states/DomainConstants'; -import { ResourceReferral } from '../states/contacts/resourceReferral'; +import type { DateFilterValue } from '../components/caseList/filters/dateFilters'; +import type { ChannelTypes } from '../states/DomainConstants'; +import type { ResourceReferral } from '../states/contacts/resourceReferral'; export type EntryInfo = { id: string; @@ -325,6 +325,10 @@ export function isOfflineContactTask(task: CustomITask): task is OfflineContactT return Boolean(task.taskSid?.startsWith('offline-contact-task-')); } +export function isOfflineContact(contact: Contact): boolean { + return Boolean(contact?.taskId?.startsWith('offline-contact-task-')); +} + /** * Checks if the task is issued by someone else to avoid showing certain things in the UI. This is done by checking isInMyBehalf task attribute (attached while creating offline contacts) */ From 9ead56a483721b896ce7baa19a96125913a3fedb Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 17 Oct 2023 09:14:03 +0100 Subject: [PATCH 2/2] Removed redundant dispatcher drilling from TabbedForms & ContactDetails --- .../src/components/contact/ContactDetails.tsx | 18 +++++++++--------- .../contact/ContactDetailsSectionForm.tsx | 16 ++++------------ .../src/components/tabbedForms/TabbedForms.tsx | 13 +++---------- 3 files changed, 16 insertions(+), 31 deletions(-) diff --git a/plugin-hrm-form/src/components/contact/ContactDetails.tsx b/plugin-hrm-form/src/components/contact/ContactDetails.tsx index 11de9b9e7e..a6b8d2e1f1 100644 --- a/plugin-hrm-form/src/components/contact/ContactDetails.tsx +++ b/plugin-hrm-form/src/components/contact/ContactDetails.tsx @@ -36,6 +36,7 @@ import CSAMReport from '../CSAMReport/CSAMReport'; import { existingContactCSAMApi } from '../CSAMReport/csamReportApi'; import { getAseloFeatureFlags } from '../../hrmConfig'; import { configurationBase, contactFormsBase, csamReportBase, namespace } from '../../states/storeNamespaces'; +import { ContactRawJson } from '../../types/types'; type OwnProps = { contactId: string; @@ -58,6 +59,7 @@ const ContactDetails: React.FC = ({ draftContact, enableEditing = true, draftCsamReport, + updateDraftForm, // eslint-disable-next-line sonarjs/cognitive-complexity }) => { const version = savedContact?.rawJson.definitionVersion; @@ -98,14 +100,11 @@ const ContactDetails: React.FC = ({ initialValues={section.getFormValues(definitionVersion, draftContact)[formPath]} display={true} autoFocus={true} - updateFormActionDispatcher={dispatch => values => - dispatch( - updateDraft(contactId, { - rawJson: { - [formPath]: values[formPath], - }, - }), - )} + updateForm={values => + updateDraftForm({ + [formPath]: values[formPath], + }) + } contactId={contactId} /> @@ -149,9 +148,10 @@ const ContactDetails: React.FC = ({ ); }; -const mapDispatchToProps = (dispatch: Dispatch<{ type: string } & Record>) => ({ +const mapDispatchToProps = (dispatch: Dispatch<{ type: string } & Record>, { contactId }: OwnProps) => ({ updateDefinitionVersion: (version: string, definitionVersion: DefinitionVersion) => dispatch(ConfigActions.updateDefinitionVersion(version, definitionVersion)), + updateDraftForm: (form: Partial) => dispatch(updateDraft(contactId, { rawJson: form })), }); const mapStateToProps = (state: RootState, { contactId }: OwnProps) => ({ diff --git a/plugin-hrm-form/src/components/contact/ContactDetailsSectionForm.tsx b/plugin-hrm-form/src/components/contact/ContactDetailsSectionForm.tsx index 819af3a626..aa5da52f3b 100644 --- a/plugin-hrm-form/src/components/contact/ContactDetailsSectionForm.tsx +++ b/plugin-hrm-form/src/components/contact/ContactDetailsSectionForm.tsx @@ -15,8 +15,7 @@ */ /* eslint-disable react/prop-types */ -import React, { Dispatch } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import React from 'react'; import type { FormDefinition, LayoutDefinition } from 'hrm-form-definitions'; import { useFormContext } from 'react-hook-form'; @@ -43,13 +42,13 @@ type OwnProps = { | ContactRawJson['caseInformation']; autoFocus?: boolean; extraChildrenRight?: React.ReactNode; - updateFormActionDispatcher?: (dispatch: Dispatch) => (values: any) => void; + updateForm?: (values: any) => void; contactId?: string; taskSid?: string; }; // eslint-disable-next-line no-use-before-define -type Props = OwnProps & ConnectedProps; +type Props = OwnProps; const ContactDetailsSectionForm: React.FC = ({ display, @@ -105,11 +104,4 @@ const ContactDetailsSectionForm: React.FC = ({ ContactDetailsSectionForm.displayName = 'TabbedFormTab'; -const mapDispatchToProps = (dispatch, ownProps: OwnProps) => ({ - updateForm: ownProps.updateFormActionDispatcher(dispatch), -}); - -const connector = connect(null, mapDispatchToProps); -const connected = connector(ContactDetailsSectionForm); - -export default connected; +export default ContactDetailsSectionForm; diff --git a/plugin-hrm-form/src/components/tabbedForms/TabbedForms.tsx b/plugin-hrm-form/src/components/tabbedForms/TabbedForms.tsx index 0c3fc09779..a20323a9e9 100644 --- a/plugin-hrm-form/src/components/tabbedForms/TabbedForms.tsx +++ b/plugin-hrm-form/src/components/tabbedForms/TabbedForms.tsx @@ -275,8 +275,7 @@ const TabbedForms: React.FC = ({ initialValues={callerInformation} display={subroute === 'callerInformation'} autoFocus={autoFocus} - updateFormActionDispatcher={() => values => - updateDraftForm({ callerInformation: values.callerInformation })} + updateForm={values => updateDraftForm({ callerInformation: values.callerInformation })} contactId={savedContact.id} /> @@ -291,10 +290,7 @@ const TabbedForms: React.FC = ({ initialValues={childInformation} display={subroute === 'childInformation'} autoFocus={autoFocus} - updateFormActionDispatcher={dispatch => values => - dispatch( - updateDraft(savedContact.id, { rawJson: { childInformation: values.childInformation } }), - )} + updateForm={values => updateDraftForm({ childInformation: values.childInformation })} contactId={savedContact.id} /> @@ -315,10 +311,7 @@ const TabbedForms: React.FC = ({ display={subroute === 'caseInformation'} autoFocus={autoFocus} extraChildrenRight={csamAttachments} - updateFormActionDispatcher={dispatch => values => - dispatch( - updateDraft(savedContact.id, { rawJson: { caseInformation: values.caseInformation } }), - )} + updateForm={values => updateDraftForm({ caseInformation: values.caseInformation })} contactId={savedContact.id} />