From d430c1774c8da9770f2eacafa0c05206875d9b23 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 15 Apr 2025 08:26:33 +0100 Subject: [PATCH 01/17] Remove use of legacy case categories from CaseHome.tsx --- plugin-hrm-form/src/components/case/CaseHome.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin-hrm-form/src/components/case/CaseHome.tsx b/plugin-hrm-form/src/components/case/CaseHome.tsx index d8d14d4477..36ed8da82c 100644 --- a/plugin-hrm-form/src/components/case/CaseHome.tsx +++ b/plugin-hrm-form/src/components/case/CaseHome.tsx @@ -139,7 +139,7 @@ const CaseHome: React.FC = ({ task, handlePrintCase, handleClose, handlePrintCase={handlePrintCase} isOrphanedCase={isOrphanedCase} definitionVersion={definitionVersion} - categories={connectedCase.categories} + categories={firstConnectedContact?.rawJson?.categories ?? {}} /> Date: Fri, 25 Apr 2025 16:04:12 +0100 Subject: [PATCH 02/17] Replace case categories in case print --- plugin-hrm-form/src/___tests__/testCases.ts | 1 - plugin-hrm-form/src/states/case/timeline.ts | 15 +++++++++++++++ plugin-hrm-form/src/types/types.ts | 1 - 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/plugin-hrm-form/src/___tests__/testCases.ts b/plugin-hrm-form/src/___tests__/testCases.ts index ec13a341c3..db70258efe 100644 --- a/plugin-hrm-form/src/___tests__/testCases.ts +++ b/plugin-hrm-form/src/___tests__/testCases.ts @@ -29,7 +29,6 @@ export const VALID_EMPTY_CASE: Case = { helpline: '', twilioWorkerId: 'WK', status: '', - categories: {}, }; export const VALID_EMPTY_CASE_STATE_ENTRY: CaseStateEntry = { diff --git a/plugin-hrm-form/src/states/case/timeline.ts b/plugin-hrm-form/src/states/case/timeline.ts index 410eb8a30b..c2b58335c6 100644 --- a/plugin-hrm-form/src/states/case/timeline.ts +++ b/plugin-hrm-form/src/states/case/timeline.ts @@ -170,3 +170,18 @@ export const selectTimeline = ( // eslint-disable-next-line import/no-unused-modules export const selectTimelineCount = (state: RootState, caseId: string, timelineId: string): number | undefined => state[namespace].connectedCase.cases[caseId]?.timelines?.[timelineId]?.length; + +export const selectTimelineContactCategories = (state: RootState, caseId: string, timelineId: string) => { + const timeline = selectTimeline(state, caseId, timelineId, { offset: 0, limit: 10000 }); + const contactActivities = timeline.filter(isContactTimelineActivity) as TimelineActivity[]; + const timelineCategories: Record = {}; + for (const { activity } of contactActivities) { + const categoriesList = Object.entries(activity.rawJson?.categories ?? {}); + for (const [newCategory, newSubcategories] of categoriesList) { + timelineCategories[newCategory] = Array.from( + new Set([...(timelineCategories[newCategory] ?? []), ...newSubcategories]), + ); + } + } + return timelineCategories; +}; diff --git a/plugin-hrm-form/src/types/types.ts b/plugin-hrm-form/src/types/types.ts index 694d279248..138b88e513 100644 --- a/plugin-hrm-form/src/types/types.ts +++ b/plugin-hrm-form/src/types/types.ts @@ -75,7 +75,6 @@ export type Case = { statusUpdatedAt?: string; statusUpdatedBy?: WorkerSID; previousStatus?: string; - categories: Record; firstContact?: Contact; }; From 852f056d997a7a1d79ad58188c4da303766273ac Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Fri, 25 Apr 2025 16:05:12 +0100 Subject: [PATCH 03/17] Replace case categories in case print --- .../src/components/case/casePrint/CasePrintView.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugin-hrm-form/src/components/case/casePrint/CasePrintView.tsx b/plugin-hrm-form/src/components/case/casePrint/CasePrintView.tsx index c79597da9a..b9158b935e 100644 --- a/plugin-hrm-form/src/components/case/casePrint/CasePrintView.tsx +++ b/plugin-hrm-form/src/components/case/casePrint/CasePrintView.tsx @@ -40,7 +40,11 @@ import { Contact, CustomITask, StandaloneITask } from '../../../types/types'; import { TimelineActivity } from '../../../states/case/types'; import { RootState } from '../../../states'; import selectCurrentRouteCaseState from '../../../states/case/selectCurrentRouteCase'; -import { newGetTimelineAsyncAction, selectTimeline } from '../../../states/case/timeline'; +import { + newGetTimelineAsyncAction, + selectTimeline, + selectTimelineContactCategories, +} from '../../../states/case/timeline'; import { selectDefinitionVersionForCase } from '../../../states/configuration/selectDefinitions'; import { selectCounselorsHash } from '../../../states/configuration/selectCounselorsHash'; import selectCaseHelplineData from '../../../states/case/selectCaseHelplineData'; @@ -69,6 +73,9 @@ const CasePrintView: React.FC = ({ task }) => { limit: MAX_PRINTOUT_CONTACTS, }) as TimelineActivity[], ); + const categories = useSelector((state: RootState) => + selectTimelineContactCategories(state, connectedCase?.id, 'print-contacts'), + ); const sectionTypeNames = Object.keys(definitionVersion.caseSectionTypes).filter( sectionType => definitionVersion.layoutVersion.case.sectionTypes?.[sectionType]?.printFormat !== 'hidden', ); @@ -212,7 +219,7 @@ const CasePrintView: React.FC = ({ task }) => { followUpDate={printedFollowUpDate} childIsAtRisk={connectedCase.info.childIsAtRisk} counselor={counselorsHash[connectedCase.twilioWorkerId]} - categories={connectedCase.categories} + categories={categories} caseManager={office?.manager} chkOnBlob={chkOnBlob} chkOffBlob={chkOffBlob} From 6fd38d5aa1c16b4985ebca9f25b21683a8b89836 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 28 Apr 2025 15:48:00 +0100 Subject: [PATCH 04/17] Replace reliance on case categories with looking up categories on contacts separately --- .../___tests__/components/case/Case.test.tsx | 1 + .../components/caseList/CaseList.test.tsx | 22 ++++-- .../___tests__/search/SearchResults.test.tsx | 8 +++ .../___tests__/states/case/saveCase.test.ts | 3 - .../src/components/case/CaseHome.tsx | 7 +- .../components/caseList/CaseListTableRow.tsx | 58 +++++++-------- .../components/search/CasePreview/index.tsx | 72 ++++++++----------- plugin-hrm-form/src/states/case/saveCase.ts | 1 - plugin-hrm-form/src/states/case/timeline.ts | 3 + 9 files changed, 88 insertions(+), 87 deletions(-) diff --git a/plugin-hrm-form/src/___tests__/components/case/Case.test.tsx b/plugin-hrm-form/src/___tests__/components/case/Case.test.tsx index 4bd776c03d..b0bbb1face 100644 --- a/plugin-hrm-form/src/___tests__/components/case/Case.test.tsx +++ b/plugin-hrm-form/src/___tests__/components/case/Case.test.tsx @@ -52,6 +52,7 @@ jest.mock('../../../states/case/timeline', () => ({ newGetTimelineAsyncAction: jest.fn(), selectTimelineCount: jest.fn(() => 0), selectTimeline: jest.fn(() => []), + selectTimelineContactCategories: jest.fn().mockReturnValue({}), })); const { mockFetchImplementation, mockReset, buildBaseURL } = mockLocalFetchDefinitions(); diff --git a/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx b/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx index d9a5017a99..521d3abf6b 100644 --- a/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx +++ b/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx @@ -39,6 +39,7 @@ import { RecursivePartial } from '../../RecursivePartial'; import { HrmState, RootState } from '../../../states'; import { CaseStateEntry } from '../../../states/case/types'; import { VALID_EMPTY_CONTACT } from '../../testContacts'; +import { newGetTimelineAsyncAction } from '../../../states/case/timeline'; const { mockFetchImplementation, mockReset, buildBaseURL } = mockLocalFetchDefinitions(); const e2eRules = require('../../../permissions/e2e.json'); @@ -51,6 +52,16 @@ jest.mock('../../../permissions/fetchRules', () => { }; }); +jest.mock('../../../states/case/timeline', () => ({ + newGetTimelineAsyncAction: jest.fn(), + selectTimelineContactCategories: jest.fn().mockReturnValue({}), +})); + +jest.mock('../../../states/caseList/listContent', () => ({ + ...jest.requireActual('../../../states/caseList/listContent'), + fetchCaseListAsyncAction: jest.fn(), +})); + beforeEach(async () => { const fetchRulesSpy = fetchRules as jest.MockedFunction; fetchRulesSpy.mockResolvedValueOnce(e2eRules); @@ -117,12 +128,6 @@ const mockedCases: Record = { }; const mockedCaseList = Object.keys(mockedCases); - -jest.mock('../../../states/caseList/listContent', () => ({ - ...jest.requireActual('../../../states/caseList/listContent'), - fetchCaseListAsyncAction: jest.fn(), -})); - expect.extend(toHaveNoViolations); const mockStore = configureMockStore([]); @@ -142,11 +147,16 @@ const blankCaseListState: CaseListState = { let mockV1; const mockFetchCaseListAsyncAction = fetchCaseListAsyncAction as jest.MockedFunction; +const mockNewGetTimelineAsyncAction = newGetTimelineAsyncAction as jest.MockedFunction< + typeof newGetTimelineAsyncAction +>; beforeEach(() => { mockReset(); mockFetchCaseListAsyncAction.mockReset(); mockFetchCaseListAsyncAction.mockReturnValue({ type: 'cases/fetch-list' } as any); + mockNewGetTimelineAsyncAction.mockReset(); + mockNewGetTimelineAsyncAction.mockReturnValue({ type: 'case-action/get-timeline' } as any); }); beforeAll(async () => { diff --git a/plugin-hrm-form/src/___tests__/search/SearchResults.test.tsx b/plugin-hrm-form/src/___tests__/search/SearchResults.test.tsx index f1c60b1e2f..42f87d9624 100644 --- a/plugin-hrm-form/src/___tests__/search/SearchResults.test.tsx +++ b/plugin-hrm-form/src/___tests__/search/SearchResults.test.tsx @@ -92,11 +92,16 @@ describe('Search Results', () => { cases: { case1: { connectedCase: { + id: 'case1', createdAt: new Date(1593469560208).toISOString(), twilioWorkerId: 'worker1', status: 'open', info: null, }, + timelines: { 'print-contacts': [] }, + }, + case2: { + timelines: { 'print-contacts': [] }, }, }, }, @@ -206,6 +211,7 @@ describe('Search Results', () => { count: 1, cases: [ { + id: 'case1', createdAt: '2020-11-23T17:38:42.227Z', updatedAt: '2020-11-23T17:38:42.227Z', helpline: '', @@ -314,6 +320,7 @@ describe('Search Results', () => { households: [{ household: { name: { firstName: 'Maria', lastName: 'Silva' } } }], summary: 'case 1 summary', }, + id: 'case1', }, { createdAt: '2020-11-23T17:38:42.227Z', @@ -323,6 +330,7 @@ describe('Search Results', () => { households: [{ household: { name: { firstName: 'John', lastName: 'Doe' } } }], summary: 'case 2 summary', }, + id: 'case2', }, ], }; diff --git a/plugin-hrm-form/src/___tests__/states/case/saveCase.test.ts b/plugin-hrm-form/src/___tests__/states/case/saveCase.test.ts index 4206be8a4e..2c527848ca 100644 --- a/plugin-hrm-form/src/___tests__/states/case/saveCase.test.ts +++ b/plugin-hrm-form/src/___tests__/states/case/saveCase.test.ts @@ -92,7 +92,6 @@ const mockPayload: Omit = { info: {}, createdAt: '12-05-2023', updatedAt: '12-05-2023', - categories: {}, }; const testStore = (stateChanges: Partial = {}) => @@ -120,7 +119,6 @@ const nonInitialPartialState: RecursivePartial = { info: {}, createdAt: '12-05-2023', updatedAt: '12-05-2023', - categories: {}, }, caseWorkingCopy: { sections: {}, @@ -230,7 +228,6 @@ describe('createCaseAsyncAction', () => { }, connectedCase: { id: '234', - categories: undefined, info: {}, }, references: new Set(), diff --git a/plugin-hrm-form/src/components/case/CaseHome.tsx b/plugin-hrm-form/src/components/case/CaseHome.tsx index 36ed8da82c..a9c93aad21 100644 --- a/plugin-hrm-form/src/components/case/CaseHome.tsx +++ b/plugin-hrm-form/src/components/case/CaseHome.tsx @@ -37,7 +37,7 @@ import { selectCurrentTopmostRouteForTask } from '../../states/routing/getRoute' import selectCurrentRouteCaseState from '../../states/case/selectCurrentRouteCase'; import CaseCreatedBanner from '../caseMergingBanners/CaseCreatedBanner'; import AddToCaseBanner from '../caseMergingBanners/AddToCaseBanner'; -import { selectTimelineCount } from '../../states/case/timeline'; +import { selectTimelineContactCategories, selectTimelineCount } from '../../states/case/timeline'; import { selectDefinitionVersionForCase } from '../../states/configuration/selectDefinitions'; import selectCaseHelplineData from '../../states/case/selectCaseHelplineData'; import { selectCounselorName } from '../../states/configuration/selectCounselorsHash'; @@ -72,6 +72,9 @@ const CaseHome: React.FC = ({ task, handlePrintCase, handleClose, const firstConnectedContact = useSelector( (state: RootState) => selectFirstContactByCaseId(state, routing.caseId)?.savedContact, ); + const timelineCategories = useSelector((state: RootState) => + selectTimelineContactCategories(state, routing.caseId, MAIN_TIMELINE_ID), + ); const activityCount = useSelector((state: RootState) => routing.route === 'case' ? selectTimelineCount(state, routing.caseId, MAIN_TIMELINE_ID) : 0, ); @@ -139,7 +142,7 @@ const CaseHome: React.FC = ({ task, handlePrintCase, handleClose, handlePrintCase={handlePrintCase} isOrphanedCase={isOrphanedCase} definitionVersion={definitionVersion} - categories={firstConnectedContact?.rawJson?.categories ?? {}} + categories={timelineCategories ?? {}} /> () => void; }; -const mapStateToProps = (state: RootState, { caseId }: OwnProps) => { - const caseItem = selectCaseByCaseId(state, caseId)?.connectedCase; - return { - definitionVersions: state[namespace][configurationBase].definitionVersions, - counselorsHash: selectCounselorsHash(state), - caseItem, - firstConnectedContact: selectFirstCaseContact(state, caseItem), - }; -}; - -const mapDispatchToProps = { - updateDefinitionVersion: ConfigActions.updateDefinitionVersion, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); +const CaseListTableRow: React.FC = ({ caseId, handleClickViewCase, ...props }) => { + const dispatch = useDispatch(); + const { connectedCase: caseItem, timelines } = useSelector((state: RootState) => selectCaseByCaseId(state, caseId)); + const firstConnectedContact = useSelector((state: RootState) => selectFirstCaseContact(state, caseItem)); + const counselorsHash = useSelector(selectCounselorsHash); + const definitionVersions = useSelector(selectDefinitionVersions); + const timelineCategories = useSelector((state: RootState) => + selectTimelineContactCategories(state, caseId, CONTACTS_TIMELINE_ID), + ); -type Props = OwnProps & ConnectedProps; - -const CaseListTableRow: React.FC = ({ - caseItem, - firstConnectedContact, - counselorsHash, - handleClickViewCase, - ...props -}) => { - const { updateDefinitionVersion, definitionVersions } = props; const { definitionVersion } = getHrmConfig(); let version = caseItem.info.definitionVersion; if (!Object.values(DefinitionVersionId).includes(version)) { @@ -98,12 +85,18 @@ const CaseListTableRow: React.FC = ({ useEffect(() => { const fetchDefinitionVersions = async () => { const definitionVersion = await getDefinitionVersion(version); - updateDefinitionVersion(version, definitionVersion); + dispatch(updateDefinitionVersion(version, definitionVersion)); }; if (version && !definitionVersions[version]) { fetchDefinitionVersions(); } - }, [definitionVersions, updateDefinitionVersion, version]); + }, [definitionVersions, version, dispatch]); + + useEffect(() => { + if (!timelineCategories) { + dispatch(newGetTimelineAsyncAction(caseId, CONTACTS_TIMELINE_ID, [], true, { offset: 0, limit: 10000 })); + } + }, [timelineCategories, caseId, dispatch]); if (!caseItem) { return null; @@ -123,7 +116,7 @@ const CaseListTableRow: React.FC = ({ const definitionVersion = definitionVersions[version]; - const categories = getContactTags(definitionVersion, caseItem.categories); + const categories = timelineCategories ? getContactTags(definitionVersion, timelineCategories) : []; // Get the status for a case from the value of CaseStatus.json of the current form definitions const getCaseStatusLabel = (caseStatus: string) => { return definitionVersion @@ -190,6 +183,5 @@ const CaseListTableRow: React.FC = ({ }; CaseListTableRow.displayName = 'CaseListTableRow'; -const connected = connector(CaseListTableRow); -export default connected; +export default CaseListTableRow; diff --git a/plugin-hrm-form/src/components/search/CasePreview/index.tsx b/plugin-hrm-form/src/components/search/CasePreview/index.tsx index 6eadab9272..26ec578174 100644 --- a/plugin-hrm-form/src/components/search/CasePreview/index.tsx +++ b/plugin-hrm-form/src/components/search/CasePreview/index.tsx @@ -16,7 +16,7 @@ /* eslint-disable react/prop-types */ import React, { Dispatch, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { connect, ConnectedProps, useDispatch, useSelector } from 'react-redux'; import { Case, Contact, RouterTask } from '../../../types/types'; import CaseHeader from './CaseHeader'; @@ -37,49 +37,39 @@ import selectContactStateByContactId from '../../../states/contacts/selectContac import selectContextContactId from '../../../states/contacts/selectContextContactId'; import { selectFirstCaseContact } from '../../../states/contacts/selectContactByCaseId'; import { contactLabelFromHrmContact } from '../../../states/contacts/contactIdentifier'; +import { selectDefinitionVersions } from '../../../states/configuration/selectDefinitions'; +import { newGetTimelineAsyncAction, selectTimelineContactCategories } from '../../../states/case/timeline'; -type OwnProps = { +type Props = { currentCase: Case; onClickViewCase: () => void; counselorsHash: { [sid: string]: string }; task: RouterTask; }; -const mapStateToProps = (state: RootState, { task, currentCase }: OwnProps) => { - const contactId = selectContextContactId(state, task.taskSid, 'search', 'case-results'); - const taskContact = selectContactStateByContactId(state, contactId)?.savedContact; - const firstConnectedContact = selectFirstCaseContact(state, currentCase); - return { - definitionVersions: state[namespace].configuration.definitionVersions, - taskContact, - firstConnectedContact, - isConnectedToTaskContact: Boolean(taskContact?.caseId && taskContact.caseId === currentCase?.id), - }; -}; +const CONTACTS_TIMELINE_ID = 'print-contacts'; + +const CasePreview: React.FC = ({ currentCase, onClickViewCase, counselorsHash, task }) => { + const contactId = useSelector((state: RootState) => + selectContextContactId(state, task.taskSid, 'search', 'case-results'), + ); + const definitionVersions = useSelector(selectDefinitionVersions); + const firstConnectedContact = useSelector((state: RootState) => selectFirstCaseContact(state, currentCase)); + const taskContact = useSelector((state: RootState) => selectContactStateByContactId(state, contactId)?.savedContact); + const isConnectedToTaskContact = Boolean(taskContact?.caseId && taskContact.caseId === currentCase?.id); + const timelineCategories = useSelector((state: RootState) => + selectTimelineContactCategories(state, currentCase.id.toString(), CONTACTS_TIMELINE_ID), + ); + + const dispatch = useDispatch(); -const mapDispatchToProps = (dispatch: Dispatch, { task, currentCase }: OwnProps) => ({ - connectCaseToTaskContact: async (taskContact: Contact) => { - await asyncDispatch(dispatch)(connectToCaseAsyncAction(taskContact.id, currentCase.id)); - }, - closeModal: () => dispatch(newCloseModalAction(task.taskSid)), -}); - -const connector = connect(mapStateToProps, mapDispatchToProps); - -type Props = OwnProps & ConnectedProps; - -const CasePreview: React.FC = ({ - currentCase, - onClickViewCase, - counselorsHash, - definitionVersions, - taskContact, - firstConnectedContact, - isConnectedToTaskContact, - connectCaseToTaskContact, - closeModal, -}) => { - const { id, createdAt, status, info, twilioWorkerId, categories } = currentCase; + useEffect(() => { + if (!timelineCategories) { + dispatch(newGetTimelineAsyncAction(currentCase.id, CONTACTS_TIMELINE_ID, [], true, { offset: 0, limit: 10000 })); + } + }, [timelineCategories, currentCase.id, dispatch]); + + const { id, createdAt, status, info, twilioWorkerId } = currentCase; const updatedAtObj = getUpdatedDate(currentCase); const followUpDateObj = info.followUpDate ? new Date(info.followUpDate) : undefined; const { definitionVersion: versionId } = info; @@ -133,8 +123,8 @@ const CasePreview: React.FC = ({ isConnectedToTaskContact={isConnectedToTaskContact} showConnectButton={showConnectButton} onClickConnectToTaskContact={() => { - connectCaseToTaskContact(taskContact); - closeModal(); + asyncDispatch(dispatch)(connectToCaseAsyncAction(taskContact.id, currentCase.id)); + dispatch(newCloseModalAction(task.taskSid)); }} /> @@ -144,7 +134,7 @@ const CasePreview: React.FC = ({ )} - + ); @@ -152,6 +142,4 @@ const CasePreview: React.FC = ({ CasePreview.displayName = 'CasePreview'; -const connected = connector(CasePreview); - -export default connected; +export default CasePreview; diff --git a/plugin-hrm-form/src/states/case/saveCase.ts b/plugin-hrm-form/src/states/case/saveCase.ts index d007f02efa..52864ce1fc 100644 --- a/plugin-hrm-form/src/states/case/saveCase.ts +++ b/plugin-hrm-form/src/states/case/saveCase.ts @@ -97,7 +97,6 @@ const updateConnectedCase = (state: HrmState, connectedCase: Case): HrmState => connectedCase: { ...stateCase?.connectedCase, ...connectedCase, - categories: stateCase?.connectedCase?.categories ?? connectedCase.categories, info: { ...(stateCase?.connectedCase?.info || {}), ...restCaseSummary, diff --git a/plugin-hrm-form/src/states/case/timeline.ts b/plugin-hrm-form/src/states/case/timeline.ts index c2b58335c6..60b672c605 100644 --- a/plugin-hrm-form/src/states/case/timeline.ts +++ b/plugin-hrm-form/src/states/case/timeline.ts @@ -173,6 +173,9 @@ export const selectTimelineCount = (state: RootState, caseId: string, timelineId export const selectTimelineContactCategories = (state: RootState, caseId: string, timelineId: string) => { const timeline = selectTimeline(state, caseId, timelineId, { offset: 0, limit: 10000 }); + if (!timeline) { + return undefined; + } const contactActivities = timeline.filter(isContactTimelineActivity) as TimelineActivity[]; const timelineCategories: Record = {}; for (const { activity } of contactActivities) { From 26c96986b7cf1f491834f1c3f81d1ee1f26e28cb Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 29 Apr 2025 10:59:55 +0100 Subject: [PATCH 05/17] Replace firstContact with timeline contactds --- .../___tests__/components/case/Case.test.tsx | 1 + .../components/caseList/CaseList.test.tsx | 31 +++++++------- plugin-hrm-form/src/___tests__/testCases.ts | 1 + .../src/components/case/CaseHome.tsx | 28 +++++-------- .../case/casePrint/CasePrintView.tsx | 4 +- .../components/caseList/CaseListTableRow.tsx | 21 +++++----- .../search/CasePreview/CaseHeader.tsx | 6 +-- .../components/search/CasePreview/index.tsx | 36 ++++++++-------- plugin-hrm-form/src/services/CaseService.ts | 42 ++++--------------- plugin-hrm-form/src/states/case/timeline.ts | 23 +++++++++- .../src/states/contacts/contactIdentifier.ts | 2 +- .../src/states/contacts/reducer.ts | 4 -- .../states/contacts/selectContactByCaseId.ts | 16 +------ plugin-hrm-form/src/types/types.ts | 2 +- 14 files changed, 93 insertions(+), 124 deletions(-) diff --git a/plugin-hrm-form/src/___tests__/components/case/Case.test.tsx b/plugin-hrm-form/src/___tests__/components/case/Case.test.tsx index b0bbb1face..1a87efaa7b 100644 --- a/plugin-hrm-form/src/___tests__/components/case/Case.test.tsx +++ b/plugin-hrm-form/src/___tests__/components/case/Case.test.tsx @@ -53,6 +53,7 @@ jest.mock('../../../states/case/timeline', () => ({ selectTimelineCount: jest.fn(() => 0), selectTimeline: jest.fn(() => []), selectTimelineContactCategories: jest.fn().mockReturnValue({}), + selectCaseLabel: jest.fn().mockReturnValue('first last'), })); const { mockFetchImplementation, mockReset, buildBaseURL } = mockLocalFetchDefinitions(); diff --git a/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx b/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx index 521d3abf6b..a8bfd3143f 100644 --- a/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx +++ b/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx @@ -33,13 +33,13 @@ import { getDefinitionVersions } from '../../../hrmConfig'; import { CaseListState } from '../../../states/caseList/reducer'; import { caseListContentInitialState, fetchCaseListAsyncAction } from '../../../states/caseList/listContent'; import { caseListSettingsInitialState } from '../../../states/caseList/settings'; -import { Contact, ContactRawJson, standaloneTaskSid } from '../../../types/types'; +import { standaloneTaskSid } from '../../../types/types'; import { namespace } from '../../../states/storeNamespaces'; import { RecursivePartial } from '../../RecursivePartial'; import { HrmState, RootState } from '../../../states'; import { CaseStateEntry } from '../../../states/case/types'; import { VALID_EMPTY_CONTACT } from '../../testContacts'; -import { newGetTimelineAsyncAction } from '../../../states/case/timeline'; +import { newGetTimelineAsyncAction, selectCaseLabel } from '../../../states/case/timeline'; const { mockFetchImplementation, mockReset, buildBaseURL } = mockLocalFetchDefinitions(); const e2eRules = require('../../../permissions/e2e.json'); @@ -55,6 +55,7 @@ jest.mock('../../../permissions/fetchRules', () => { jest.mock('../../../states/case/timeline', () => ({ newGetTimelineAsyncAction: jest.fn(), selectTimelineContactCategories: jest.fn().mockReturnValue({}), + selectCaseLabel: jest.fn().mockReturnValue(''), })); jest.mock('../../../states/caseList/listContent', () => ({ @@ -79,6 +80,7 @@ const mockedCases: Record = { availableStatusTransitions: [], connectedCase: { id: '1', + label: '', accountSid: 'AC', twilioWorkerId: 'WK worker 1', createdAt: '2020-07-07T17:38:42.227Z', @@ -88,10 +90,6 @@ const mockedCases: Record = { definitionVersion: DefinitionVersionId.v1, }, helpline: '', - categories: {}, - firstContact: { - id: 'contact-1', - } as Contact, }, timelines: {}, sections: {}, @@ -102,6 +100,7 @@ const mockedCases: Record = { availableStatusTransitions: [], connectedCase: { id: '2', + label: '', accountSid: 'AC', twilioWorkerId: 'WK-worker 2', createdAt: '2020-07-07T17:38:42.227Z', @@ -111,16 +110,6 @@ const mockedCases: Record = { definitionVersion: DefinitionVersionId.v1, }, helpline: '', - firstContact: { - id: 'contact-2', - rawJson: { - childInformation: { - firstName: 'Sonya', - lastName: 'Michels', - }, - } as Partial, - } as Contact, - categories: {}, }, timelines: {}, sections: {}, @@ -209,6 +198,16 @@ test('Should dispatch fetchList actions', async () => { }); test('Should render list if it is populated', async () => { + (selectCaseLabel as jest.MockedFunction).mockImplementation((state, caseId) => { + switch (caseId) { + case '1': + return 'Michael Smith'; + case '2': + return 'Sonya Michels'; + default: + return ''; + } + }); const initialState: RootState = createState({ configuration: { counselors: { diff --git a/plugin-hrm-form/src/___tests__/testCases.ts b/plugin-hrm-form/src/___tests__/testCases.ts index db70258efe..d3e04d4509 100644 --- a/plugin-hrm-form/src/___tests__/testCases.ts +++ b/plugin-hrm-form/src/___tests__/testCases.ts @@ -19,6 +19,7 @@ import { Case } from '../types/types'; import { CaseStateEntry } from '../states/case/types'; export const VALID_EMPTY_CASE: Case = { + label: '', id: '1', accountSid: 'AC', info: { diff --git a/plugin-hrm-form/src/components/case/CaseHome.tsx b/plugin-hrm-form/src/components/case/CaseHome.tsx index a9c93aad21..33e762dcdb 100644 --- a/plugin-hrm-form/src/components/case/CaseHome.tsx +++ b/plugin-hrm-form/src/components/case/CaseHome.tsx @@ -14,7 +14,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React, { useMemo } from 'react'; +import React from 'react'; import { Template } from '@twilio/flex-ui'; import { useDispatch, useSelector } from 'react-redux'; import { DefinitionVersion } from 'hrm-form-definitions'; @@ -37,15 +37,10 @@ import { selectCurrentTopmostRouteForTask } from '../../states/routing/getRoute' import selectCurrentRouteCaseState from '../../states/case/selectCurrentRouteCase'; import CaseCreatedBanner from '../caseMergingBanners/CaseCreatedBanner'; import AddToCaseBanner from '../caseMergingBanners/AddToCaseBanner'; -import { selectTimelineContactCategories, selectTimelineCount } from '../../states/case/timeline'; +import { selectCaseLabel, selectTimelineContactCategories, selectTimelineCount } from '../../states/case/timeline'; import { selectDefinitionVersionForCase } from '../../states/configuration/selectDefinitions'; import selectCaseHelplineData from '../../states/case/selectCaseHelplineData'; import { selectCounselorName } from '../../states/configuration/selectCounselorsHash'; -import { contactLabelFromHrmContact } from '../../states/contacts/contactIdentifier'; -import { - selectContactsByCaseIdInCreatedOrder, - selectFirstContactByCaseId, -} from '../../states/contacts/selectContactByCaseId'; export type CaseHomeProps = { task: CustomITask | StandaloneITask; @@ -68,16 +63,18 @@ const CaseHome: React.FC = ({ task, handlePrintCase, handleClose, ); const routing = useSelector((state: RootState) => selectCurrentTopmostRouteForTask(state, task.taskSid) as CaseRoute); - const caseContacts = useSelector((state: RootState) => selectContactsByCaseIdInCreatedOrder(state, routing.caseId)); - const firstConnectedContact = useSelector( - (state: RootState) => selectFirstContactByCaseId(state, routing.caseId)?.savedContact, - ); const timelineCategories = useSelector((state: RootState) => selectTimelineContactCategories(state, routing.caseId, MAIN_TIMELINE_ID), ); const activityCount = useSelector((state: RootState) => routing.route === 'case' ? selectTimelineCount(state, routing.caseId, MAIN_TIMELINE_ID) : 0, ); + const caseLabel = useSelector((state: RootState) => + selectCaseLabel(state, routing.caseId, MAIN_TIMELINE_ID, { + substituteForId: false, + placeholder: '', + }), + ); const definitionVersion = useSelector((state: RootState) => selectDefinitionVersionForCase(state, connectedCase)); const counselor = useSelector((state: RootState) => selectCounselorName(state, connectedCase?.twilioWorkerId)); @@ -89,12 +86,9 @@ const CaseHome: React.FC = ({ task, handlePrintCase, handleClose, if (!connectedCase) return null; // narrow type before deconstructing const isNewContact = Boolean(taskContact && taskContact.caseId === routing.caseId && !taskContact.finalizedAt); - const isNewCase = caseContacts.length === 1 && taskContact && taskContact.caseId === routing.caseId; - const isOrphanedCase = !firstConnectedContact; - const label = contactLabelFromHrmContact(definitionVersion, firstConnectedContact ?? taskContact, { - placeholder: '', - substituteForId: false, - }); + const isNewCase = taskContact && taskContact.caseId === routing.caseId; + const isOrphanedCase = !timelineCategories; + const label = caseLabel; const hasMoreActivities = activityCount > MAX_ACTIVITIES_IN_TIMELINE_SECTION; const caseId = connectedCase.id; diff --git a/plugin-hrm-form/src/components/case/casePrint/CasePrintView.tsx b/plugin-hrm-form/src/components/case/casePrint/CasePrintView.tsx index b9158b935e..9bfb733e4c 100644 --- a/plugin-hrm-form/src/components/case/casePrint/CasePrintView.tsx +++ b/plugin-hrm-form/src/components/case/casePrint/CasePrintView.tsx @@ -42,6 +42,7 @@ import { RootState } from '../../../states'; import selectCurrentRouteCaseState from '../../../states/case/selectCurrentRouteCase'; import { newGetTimelineAsyncAction, + selectCaseLabel, selectTimeline, selectTimelineContactCategories, } from '../../../states/case/timeline'; @@ -50,7 +51,6 @@ import { selectCounselorsHash } from '../../../states/configuration/selectCounse import selectCaseHelplineData from '../../../states/case/selectCaseHelplineData'; import * as RoutingActions from '../../../states/routing/actions'; import { FullCaseSection } from '../../../services/caseSectionService'; -import { contactLabelFromHrmContact } from '../../../states/contacts/contactIdentifier'; type OwnProps = { task: CustomITask | StandaloneITask; @@ -76,6 +76,7 @@ const CasePrintView: React.FC = ({ task }) => { const categories = useSelector((state: RootState) => selectTimelineContactCategories(state, connectedCase?.id, 'print-contacts'), ); + const caseLabel = useSelector((state: RootState) => selectCaseLabel(state, connectedCase?.id, 'print-contacts')); const sectionTypeNames = Object.keys(definitionVersion.caseSectionTypes).filter( sectionType => definitionVersion.layoutVersion.case.sectionTypes?.[sectionType]?.printFormat !== 'hidden', ); @@ -182,7 +183,6 @@ const CasePrintView: React.FC = ({ task }) => { ? parseISO(connectedCase.info.followUpDate).toLocaleDateString() : ''; - const caseLabel = contactLabelFromHrmContact(definitionVersion, connectedCase.firstContact); const allCsamReports = contactTimeline?.flatMap(({ activity }) => activity?.csamReports ?? []) ?? []; const orderedListSections = Object.entries(definitionVersion.caseSectionTypes) diff --git a/plugin-hrm-form/src/components/caseList/CaseListTableRow.tsx b/plugin-hrm-form/src/components/caseList/CaseListTableRow.tsx index ce7b704fc9..e2cf23c33a 100644 --- a/plugin-hrm-form/src/components/caseList/CaseListTableRow.tsx +++ b/plugin-hrm-form/src/components/caseList/CaseListTableRow.tsx @@ -17,7 +17,7 @@ /* eslint-disable react/prop-types */ import React, { useEffect } from 'react'; import { Template } from '@twilio/flex-ui'; -import { connect, ConnectedProps, useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { DefinitionVersionId } from 'hrm-form-definitions'; import { parseISO } from 'date-fns'; @@ -42,14 +42,15 @@ import { import { formatName, getShortSummary } from '../../utils'; import { getContactTags } from '../../utils/categories'; import CategoryWithTooltip from '../common/CategoryWithTooltip'; -import { contactLabelFromHrmContact } from '../../states/contacts/contactIdentifier'; import { getHrmConfig } from '../../hrmConfig'; -import { configurationBase, namespace } from '../../states/storeNamespaces'; import { selectCaseByCaseId } from '../../states/case/selectCaseStateByCaseId'; -import { selectFirstCaseContact } from '../../states/contacts/selectContactByCaseId'; import { selectCounselorsHash } from '../../states/configuration/selectCounselorsHash'; import { selectDefinitionVersions } from '../../states/configuration/selectDefinitions'; -import { newGetTimelineAsyncAction, selectTimelineContactCategories } from '../../states/case/timeline'; +import { + newGetTimelineAsyncAction, + selectCaseLabel, + selectTimelineContactCategories, +} from '../../states/case/timeline'; const CHAR_LIMIT = 200; const CONTACTS_TIMELINE_ID = 'print-contacts'; @@ -59,15 +60,15 @@ type Props = { handleClickViewCase: (currentCase: Case) => () => void; }; -const CaseListTableRow: React.FC = ({ caseId, handleClickViewCase, ...props }) => { +const CaseListTableRow: React.FC = ({ caseId, handleClickViewCase }) => { const dispatch = useDispatch(); - const { connectedCase: caseItem, timelines } = useSelector((state: RootState) => selectCaseByCaseId(state, caseId)); - const firstConnectedContact = useSelector((state: RootState) => selectFirstCaseContact(state, caseItem)); + const { connectedCase: caseItem } = useSelector((state: RootState) => selectCaseByCaseId(state, caseId)); const counselorsHash = useSelector(selectCounselorsHash); const definitionVersions = useSelector(selectDefinitionVersions); const timelineCategories = useSelector((state: RootState) => selectTimelineContactCategories(state, caseId, CONTACTS_TIMELINE_ID), ); + const caseLabel = useSelector((state: RootState) => selectCaseLabel(state, caseId, CONTACTS_TIMELINE_ID)); const { definitionVersion } = getHrmConfig(); let version = caseItem.info.definitionVersion; @@ -124,8 +125,6 @@ const CaseListTableRow: React.FC = ({ caseId, handleClickViewCase, ...pro : caseStatus; }; - const contactLabel = contactLabelFromHrmContact(definitionVersion, firstConnectedContact); - return ( @@ -143,7 +142,7 @@ const CaseListTableRow: React.FC = ({ caseId, handleClickViewCase, ...pro - {contactLabel} + {caseLabel} {counselor} diff --git a/plugin-hrm-form/src/components/search/CasePreview/CaseHeader.tsx b/plugin-hrm-form/src/components/search/CasePreview/CaseHeader.tsx index 6e0eaea001..974a78f6fb 100644 --- a/plugin-hrm-form/src/components/search/CasePreview/CaseHeader.tsx +++ b/plugin-hrm-form/src/components/search/CasePreview/CaseHeader.tsx @@ -27,7 +27,7 @@ import ConnectToCaseButton from '../../case/ConnectToCaseButton'; type OwnProps = { caseId: string; - contactLabel?: string; + caseLabel?: string; createdAt: string; updatedAt?: string; followUpDate?: Date; @@ -44,7 +44,7 @@ type Props = OwnProps; const CaseHeader: React.FC = ({ caseId, - contactLabel, + caseLabel, createdAt, updatedAt, followUpDate, @@ -71,7 +71,7 @@ const CaseHeader: React.FC = ({ #{caseId} - {isOrphanedCase ? strings['CaseHeader-Voided'] : `${contactLabel}`} + {isOrphanedCase ? strings['CaseHeader-Voided'] : `${caseLabel}`} {showConnectButton && ( diff --git a/plugin-hrm-form/src/components/search/CasePreview/index.tsx b/plugin-hrm-form/src/components/search/CasePreview/index.tsx index 26ec578174..3b1ae0bd4d 100644 --- a/plugin-hrm-form/src/components/search/CasePreview/index.tsx +++ b/plugin-hrm-form/src/components/search/CasePreview/index.tsx @@ -15,10 +15,10 @@ */ /* eslint-disable react/prop-types */ -import React, { Dispatch, useEffect } from 'react'; -import { connect, ConnectedProps, useDispatch, useSelector } from 'react-redux'; +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; -import { Case, Contact, RouterTask } from '../../../types/types'; +import { Case, RouterTask } from '../../../types/types'; import CaseHeader from './CaseHeader'; import { Flex, PreviewWrapper } from '../../../styles'; import getUpdatedDate from '../../../states/getUpdatedDate'; @@ -27,7 +27,6 @@ import { getDefinitionVersion } from '../../../services/ServerlessService'; import { updateDefinitionVersion } from '../../../states/configuration/actions'; import { RootState } from '../../../states'; import TagsAndCounselor from '../TagsAndCounselor'; -import { namespace } from '../../../states/storeNamespaces'; import asyncDispatch from '../../../states/asyncDispatch'; import { connectToCaseAsyncAction } from '../../../states/contacts/saveContact'; import { newCloseModalAction } from '../../../states/routing/actions'; @@ -35,10 +34,12 @@ import { getInitializedCan, PermissionActions } from '../../../permissions'; import { PreviewRow } from '../styles'; import selectContactStateByContactId from '../../../states/contacts/selectContactStateByContactId'; import selectContextContactId from '../../../states/contacts/selectContextContactId'; -import { selectFirstCaseContact } from '../../../states/contacts/selectContactByCaseId'; -import { contactLabelFromHrmContact } from '../../../states/contacts/contactIdentifier'; import { selectDefinitionVersions } from '../../../states/configuration/selectDefinitions'; -import { newGetTimelineAsyncAction, selectTimelineContactCategories } from '../../../states/case/timeline'; +import { + newGetTimelineAsyncAction, + selectCaseLabel, + selectTimelineContactCategories, +} from '../../../states/case/timeline'; type Props = { currentCase: Case; @@ -54,11 +55,16 @@ const CasePreview: React.FC = ({ currentCase, onClickViewCase, counselors selectContextContactId(state, task.taskSid, 'search', 'case-results'), ); const definitionVersions = useSelector(selectDefinitionVersions); - const firstConnectedContact = useSelector((state: RootState) => selectFirstCaseContact(state, currentCase)); const taskContact = useSelector((state: RootState) => selectContactStateByContactId(state, contactId)?.savedContact); const isConnectedToTaskContact = Boolean(taskContact?.caseId && taskContact.caseId === currentCase?.id); const timelineCategories = useSelector((state: RootState) => - selectTimelineContactCategories(state, currentCase.id.toString(), CONTACTS_TIMELINE_ID), + selectTimelineContactCategories(state, currentCase.id, CONTACTS_TIMELINE_ID), + ); + const caseLabel = useSelector((state: RootState) => + selectCaseLabel(state, currentCase.id, CONTACTS_TIMELINE_ID, { + substituteForId: false, + placeholder: '', + }), ); const dispatch = useDispatch(); @@ -73,10 +79,8 @@ const CasePreview: React.FC = ({ currentCase, onClickViewCase, counselors const updatedAtObj = getUpdatedDate(currentCase); const followUpDateObj = info.followUpDate ? new Date(info.followUpDate) : undefined; const { definitionVersion: versionId } = info; - const orphanedCase = !firstConnectedContact; - const { caseInformation } = (firstConnectedContact || {}).rawJson || {}; - const { callSummary } = caseInformation || {}; - const summary = info?.summary || callSummary; + const orphanedCase = !timelineCategories; + const summary = info?.summary; const counselor = counselorsHash[twilioWorkerId]; const can = React.useMemo(() => { @@ -102,17 +106,13 @@ const CasePreview: React.FC = ({ currentCase, onClickViewCase, counselors (!taskContact.caseId || isConnectedToTaskContact), ); } - const contactLabel = contactLabelFromHrmContact(definitionVersion, firstConnectedContact, { - substituteForId: false, - placeholder: '', - }); return ( & { connectedContacts: Contact[] }; - -const convertApiCaseToFlexCase = (apiCase: ApiCase): Case => { - if (!apiCase) { - return apiCase; - } - const { connectedContacts, ...withoutConnectedContacts } = apiCase; - const firstContact = connectedContacts?.[0]; - return { - ...(firstContact ? { firstContact: convertApiContactToFlexContact(firstContact) } : {}), - ...withoutConnectedContacts, - id: apiCase.id.toString(), // coerce to string type, can be removed once API is aligned - }; -}; - export const getCasePayload = (contact: Contact, creatingWorkerSid: string, definitionVersion: DefinitionVersionId) => { const { helpline, rawJson: contactForm } = contact; - const caseRecord = contactForm.contactlessTask?.createdOnBehalfOf + return contactForm.contactlessTask?.createdOnBehalfOf ? { helpline, status: 'open', @@ -57,8 +42,6 @@ export const getCasePayload = (contact: Contact, creatingWorkerSid: string, defi twilioWorkerId: creatingWorkerSid, info: { definitionVersion }, }; - - return caseRecord; }; export async function createCase(contact: Contact, creatingWorkerSid: string, definitionVersion: DefinitionVersionId) { @@ -69,7 +52,7 @@ export async function createCase(contact: Contact, creatingWorkerSid: string, de body: JSON.stringify(caseRecord), }; - return convertApiCaseToFlexCase(await fetchHrmApi('/cases', options)); + return fetchHrmApi('/cases', options); } export async function cancelCase(caseId: Case['id']) { @@ -86,7 +69,7 @@ export async function updateCaseOverview(caseId: Case['id'], body: CaseOverview) body: JSON.stringify(body), }; - return convertApiCaseToFlexCase(await fetchHrmApi(`/cases/${caseId}/overview`, options)); + return fetchHrmApi(`/cases/${caseId}/overview`, options); } export async function updateCaseStatus(caseId: Case['id'], status: Case['status']): Promise { @@ -95,7 +78,7 @@ export async function updateCaseStatus(caseId: Case['id'], status: Case['status' body: JSON.stringify({ status }), }; - return convertApiCaseToFlexCase(await fetchHrmApi(`/cases/${caseId}/status`, options)); + return fetchHrmApi(`/cases/${caseId}/status`, options); } export async function getCase(caseId: Case['id']): Promise { @@ -103,8 +86,7 @@ export async function getCase(caseId: Case['id']): Promise { method: 'GET', returnNullFor404: true, }; - const fromApi: ApiCase = await fetchHrmApi(`/cases/${caseId}`, options); - return convertApiCaseToFlexCase(fromApi); + return fetchHrmApi(`/cases/${caseId}`, options); } export type TimelineResult = { @@ -168,12 +150,7 @@ export async function listCases(queryParams, listCasesPayload): Promise { + const caseLabelFromCase = selectCaseByCaseId(state, caseId)?.connectedCase?.label; + if (caseLabelFromCase) return caseLabelFromCase; + const timeline = selectTimeline(state, caseId, timelineId, { offset: 0, limit: 10000 }); + if (timeline) { + const firstContact = timeline.find(isContactTimelineActivity) as TimelineActivity; + if (firstContact?.activity) { + const def = selectDefinitionVersionForContact(state, firstContact.activity); + return contactLabelFromHrmContact(def, firstContact.activity, contactLabelOptions); + } + } + return undefined; +}; diff --git a/plugin-hrm-form/src/states/contacts/contactIdentifier.ts b/plugin-hrm-form/src/states/contacts/contactIdentifier.ts index b3702725cb..528f4d6bdb 100644 --- a/plugin-hrm-form/src/states/contacts/contactIdentifier.ts +++ b/plugin-hrm-form/src/states/contacts/contactIdentifier.ts @@ -32,7 +32,7 @@ const definitionUsesChildName = (definition: DefinitionVersion) => definition.tabbedForms.ChildInformationTab.find(input => input.name === 'firstName' || input.name === 'lastName'), ); -type ContactLabelOptions = { placeholder?: string; substituteForId?: boolean }; +export type ContactLabelOptions = { placeholder?: string; substituteForId?: boolean }; const contactLabel = ( definition: DefinitionVersion, diff --git a/plugin-hrm-form/src/states/contacts/reducer.ts b/plugin-hrm-form/src/states/contacts/reducer.ts index 7c3bd59747..a875e985c5 100644 --- a/plugin-hrm-form/src/states/contacts/reducer.ts +++ b/plugin-hrm-form/src/states/contacts/reducer.ts @@ -61,7 +61,6 @@ import { saveContactReducer } from './saveContact'; import { ConfigurationState } from '../configuration/reducer'; import { Contact } from '../../types/types'; import { - SEARCH_CASES_SUCCESS, SEARCH_CONTACTS_SUCCESS, SearchCasesSuccessAction, SearchContactsSuccessAction, @@ -253,9 +252,6 @@ export function reduce( case SEARCH_CONTACTS_SUCCESS: { return loadContactListIntoState(state, rootState.configuration, action.searchResult.contacts, `${action.taskId}-search-contact`); } - case SEARCH_CASES_SUCCESS: { - return loadContactListIntoState(state, rootState.configuration, action.searchResult.cases.map(c => c.firstContact).filter(Boolean), `${action.taskId}-search-case`); - } case GET_CASE_TIMELINE_ACTION_FULFILLED: { const { payload: { caseId, timelineResult: { activities } } } = action; const contacts = activities.filter(isContactTimelineActivity).map(({ activity })=> activity); diff --git a/plugin-hrm-form/src/states/contacts/selectContactByCaseId.ts b/plugin-hrm-form/src/states/contacts/selectContactByCaseId.ts index bf95ccb893..d688de1a29 100644 --- a/plugin-hrm-form/src/states/contacts/selectContactByCaseId.ts +++ b/plugin-hrm-form/src/states/contacts/selectContactByCaseId.ts @@ -19,8 +19,7 @@ import { parseISO } from 'date-fns'; import { RootState } from '..'; import { ContactState } from './existingContacts'; import { namespace } from '../storeNamespaces'; -import { Case, Contact } from '../../types/types'; -import selectContactStateByContactId from './selectContactStateByContactId'; +import { Case } from '../../types/types'; export const selectContactsByCaseIdInCreatedOrder = (state: RootState, caseId: Case['id']): ContactState[] => Object.values(state[namespace].activeContacts.existingContacts) @@ -29,16 +28,3 @@ export const selectContactsByCaseIdInCreatedOrder = (state: RootState, caseId: C export const selectFirstContactByCaseId = (state: RootState, caseId: Case['id']): ContactState => selectContactsByCaseIdInCreatedOrder(state, caseId)[0] || null; - -export const selectFirstCaseContact = (state: RootState, parentCase: Case): Contact => { - if (!parentCase.firstContact) return undefined; - const contactState = selectContactStateByContactId(state, parentCase.firstContact.id); - if (contactState) { - if (contactState.savedContact.caseId === parentCase.id) { - return contactState.savedContact; // Contact loaded into state and still connected to case, return this version - } - // If the contact in state is not connected to the case, try to find one that is. - return selectFirstContactByCaseId(state, parentCase.id)?.savedContact; - } - return parentCase.firstContact; // Contact not loaded into state, return this version, could be stale -}; diff --git a/plugin-hrm-form/src/types/types.ts b/plugin-hrm-form/src/types/types.ts index 138b88e513..4a84936de5 100644 --- a/plugin-hrm-form/src/types/types.ts +++ b/plugin-hrm-form/src/types/types.ts @@ -65,6 +65,7 @@ export type CaseInfo = CaseOverview & { export type Case = { accountSid: AccountSID; id: string; + label: string; status: string; helpline: string; twilioWorkerId: WorkerSID; @@ -75,7 +76,6 @@ export type Case = { statusUpdatedAt?: string; statusUpdatedBy?: WorkerSID; previousStatus?: string; - firstContact?: Contact; }; export type TwilioStoredMedia = { From a60d2434ee101da3ce74d7158a1ca7795d53b24b Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 29 Apr 2025 11:03:30 +0100 Subject: [PATCH 06/17] Replace firstContact with timeline contactds --- .../src/___tests__/components/caseList/CaseList.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx b/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx index a8bfd3143f..4a31953cf9 100644 --- a/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx +++ b/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx @@ -93,6 +93,7 @@ const mockedCases: Record = { }, timelines: {}, sections: {}, + outstandingUpdateCount: 0, }, '2': { caseWorkingCopy: undefined, @@ -113,6 +114,7 @@ const mockedCases: Record = { }, timelines: {}, sections: {}, + outstandingUpdateCount: 0, }, }; From c8571e4e59fd5aa4928ff71358e6804c7fe90e55 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 29 Apr 2025 11:20:04 +0100 Subject: [PATCH 07/17] Mock timelines in UI test --- e2e-tests/ui-tests/aselo-service-mocks/hrm/cases.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/e2e-tests/ui-tests/aselo-service-mocks/hrm/cases.ts b/e2e-tests/ui-tests/aselo-service-mocks/hrm/cases.ts index b74cbf96b5..cd456ccf9f 100644 --- a/e2e-tests/ui-tests/aselo-service-mocks/hrm/cases.ts +++ b/e2e-tests/ui-tests/aselo-service-mocks/hrm/cases.ts @@ -102,6 +102,19 @@ const hrmCases = () => { }); }, ); + await page.route( + new URL(path.join(PATH_PREFIX, '*', 'timeline', '**'), context.HRM_BASE_URL).toString(), + async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + activities: [], + count: 0, + }), + }); + }, + ); }, }; }; From bf51164ef6e2420781fd0e3e4380cd6b281c4e2c Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 29 Apr 2025 11:36:19 +0100 Subject: [PATCH 08/17] Mock timelines in UI test --- e2e-tests/ui-tests/aselo-service-mocks/hrm/cases.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-tests/ui-tests/aselo-service-mocks/hrm/cases.ts b/e2e-tests/ui-tests/aselo-service-mocks/hrm/cases.ts index cd456ccf9f..ba58bca354 100644 --- a/e2e-tests/ui-tests/aselo-service-mocks/hrm/cases.ts +++ b/e2e-tests/ui-tests/aselo-service-mocks/hrm/cases.ts @@ -103,7 +103,7 @@ const hrmCases = () => { }, ); await page.route( - new URL(path.join(PATH_PREFIX, '*', 'timeline', '**'), context.HRM_BASE_URL).toString(), + new URL(path.join(PATH_PREFIX, '*', 'timeline**'), context.HRM_BASE_URL).toString(), async (route) => { await route.fulfill({ status: 200, From 69ee72ee3c048b761e83b93d4657a76d266709ea Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 29 Apr 2025 12:23:06 +0100 Subject: [PATCH 09/17] Fix check for new case --- plugin-hrm-form/src/components/case/CaseHome.tsx | 11 +++++++++-- plugin-hrm-form/src/states/case/timeline.ts | 8 ++++++-- plugin-hrm-form/src/states/case/types.ts | 4 ++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/plugin-hrm-form/src/components/case/CaseHome.tsx b/plugin-hrm-form/src/components/case/CaseHome.tsx index 33e762dcdb..5d7681bd4f 100644 --- a/plugin-hrm-form/src/components/case/CaseHome.tsx +++ b/plugin-hrm-form/src/components/case/CaseHome.tsx @@ -41,6 +41,7 @@ import { selectCaseLabel, selectTimelineContactCategories, selectTimelineCount } import { selectDefinitionVersionForCase } from '../../states/configuration/selectDefinitions'; import selectCaseHelplineData from '../../states/case/selectCaseHelplineData'; import { selectCounselorName } from '../../states/configuration/selectCounselorsHash'; +import { isContactIdentifierTimelineActivity } from '../../states/case/types'; export type CaseHomeProps = { task: CustomITask | StandaloneITask; @@ -62,13 +63,17 @@ const CaseHome: React.FC = ({ task, handlePrintCase, handleClose, isStandaloneITask(task) ? undefined : selectContactByTaskSid(state, task.taskSid)?.savedContact, ); const routing = useSelector((state: RootState) => selectCurrentTopmostRouteForTask(state, task.taskSid) as CaseRoute); - const timelineCategories = useSelector((state: RootState) => selectTimelineContactCategories(state, routing.caseId, MAIN_TIMELINE_ID), ); const activityCount = useSelector((state: RootState) => routing.route === 'case' ? selectTimelineCount(state, routing.caseId, MAIN_TIMELINE_ID) : 0, ); + const contactCount = useSelector((state: RootState) => + routing.route === 'case' + ? selectTimelineCount(state, routing.caseId, MAIN_TIMELINE_ID, isContactIdentifierTimelineActivity) + : 0, + ); const caseLabel = useSelector((state: RootState) => selectCaseLabel(state, routing.caseId, MAIN_TIMELINE_ID, { substituteForId: false, @@ -85,7 +90,9 @@ const CaseHome: React.FC = ({ task, handlePrintCase, handleClose, // End Hooks if (!connectedCase) return null; // narrow type before deconstructing - const isNewContact = Boolean(taskContact && taskContact.caseId === routing.caseId && !taskContact.finalizedAt); + const isNewContact = Boolean( + contactCount === 1 && taskContact && taskContact.caseId === routing.caseId && !taskContact.finalizedAt, + ); const isNewCase = taskContact && taskContact.caseId === routing.caseId; const isOrphanedCase = !timelineCategories; const label = caseLabel; diff --git a/plugin-hrm-form/src/states/case/timeline.ts b/plugin-hrm-form/src/states/case/timeline.ts index 552a3f3820..c4f46624ec 100644 --- a/plugin-hrm-form/src/states/case/timeline.ts +++ b/plugin-hrm-form/src/states/case/timeline.ts @@ -170,8 +170,12 @@ export const selectTimeline = ( }; // eslint-disable-next-line import/no-unused-modules -export const selectTimelineCount = (state: RootState, caseId: string, timelineId: string): number | undefined => - state[namespace].connectedCase.cases[caseId]?.timelines?.[timelineId]?.length; +export const selectTimelineCount = ( + state: RootState, + caseId: string, + timelineId: string, + filter: (activity: ContactIdentifierTimelineActivity | CaseSectionIdentifierTimelineActivity) => boolean = () => true, +): number | undefined => state[namespace].connectedCase.cases[caseId]?.timelines?.[timelineId]?.filter(filter)?.length; export const selectTimelineContactCategories = (state: RootState, caseId: string, timelineId: string) => { const timeline = selectTimeline(state, caseId, timelineId, { offset: 0, limit: 10000 }); diff --git a/plugin-hrm-form/src/states/case/types.ts b/plugin-hrm-form/src/states/case/types.ts index cc2a6dfd19..f0acca4856 100644 --- a/plugin-hrm-form/src/states/case/types.ts +++ b/plugin-hrm-form/src/states/case/types.ts @@ -89,6 +89,10 @@ export type ContactIdentifierTimelineActivity = TimelineActivity<{ contactId: Co activityType: 'contact-id'; }; +export const isContactIdentifierTimelineActivity = ( + activity: TimelineActivity, +): activity is CaseSectionIdentifierTimelineActivity => activity.activityType === 'contact-id'; + export type CaseSectionIdentifierTimelineActivity = TimelineActivity<{ sectionType: string; sectionId: string; From 2df4903bc1b96a8062c9cbad3b4a40938a2dd2b1 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 29 Apr 2025 15:26:04 +0100 Subject: [PATCH 10/17] Put case ID string conversion back because API still returns case ids as numbers --- plugin-hrm-form/src/services/CaseService.ts | 27 ++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/plugin-hrm-form/src/services/CaseService.ts b/plugin-hrm-form/src/services/CaseService.ts index 534195367e..4c3834bdd3 100644 --- a/plugin-hrm-form/src/services/CaseService.ts +++ b/plugin-hrm-form/src/services/CaseService.ts @@ -26,6 +26,10 @@ import { getQueryParams } from './PaginationParams'; import { convertApiCaseSectionToCaseSection, FullGenericCaseSection } from './caseSectionService'; import { convertApiContactToFlexContact } from './ContactService'; +const convertApiCaseToFlexCase = (apiCase: Case): Case => ({ + ...apiCase, + id: apiCase.id.toString(), // coerce to string type, can be removed once API is aligned +}); export const getCasePayload = (contact: Contact, creatingWorkerSid: string, definitionVersion: DefinitionVersionId) => { const { helpline, rawJson: contactForm } = contact; @@ -52,7 +56,7 @@ export async function createCase(contact: Contact, creatingWorkerSid: string, de body: JSON.stringify(caseRecord), }; - return fetchHrmApi('/cases', options); + return convertApiCaseToFlexCase(await fetchHrmApi('/cases', options)); } export async function cancelCase(caseId: Case['id']) { @@ -69,7 +73,7 @@ export async function updateCaseOverview(caseId: Case['id'], body: CaseOverview) body: JSON.stringify(body), }; - return fetchHrmApi(`/cases/${caseId}/overview`, options); + return convertApiCaseToFlexCase(await fetchHrmApi(`/cases/${caseId}/overview`, options)); } export async function updateCaseStatus(caseId: Case['id'], status: Case['status']): Promise { @@ -78,7 +82,7 @@ export async function updateCaseStatus(caseId: Case['id'], status: Case['status' body: JSON.stringify({ status }), }; - return fetchHrmApi(`/cases/${caseId}/status`, options); + return convertApiCaseToFlexCase(await fetchHrmApi(`/cases/${caseId}/status`, options)); } export async function getCase(caseId: Case['id']): Promise { @@ -86,7 +90,8 @@ export async function getCase(caseId: Case['id']): Promise { method: 'GET', returnNullFor404: true, }; - return fetchHrmApi(`/cases/${caseId}`, options); + const fromApi: Case = await fetchHrmApi(`/cases/${caseId}`, options); + return convertApiCaseToFlexCase(fromApi); } export type TimelineResult = { @@ -150,7 +155,12 @@ export async function listCases(queryParams, listCasesPayload): Promise Date: Tue, 29 Apr 2025 17:58:46 +0100 Subject: [PATCH 11/17] Remove category filters, improve translation key handling for multi select filter --- .../form-definitions/as/v1/CaseFilters.json | 1 - .../form-definitions/br/v1/CaseFilters.json | 1 - .../form-definitions/ca/v1/CaseFilters.json | 1 - .../form-definitions/cl/v1/CaseFilters.json | 1 - .../form-definitions/co/v1/CaseFilters.json | 2 - .../form-definitions/e2e/v1/CaseFilters.json | 2 - .../form-definitions/et/v1/CaseFilters.json | 2 - .../form-definitions/hu/v1/CaseFilters.json | 2 - .../form-definitions/in/v1/CaseFilters.json | 2 - .../form-definitions/jm/v1/CaseFilters.json | 2 - .../form-definitions/mt/v1/CaseFilters.json | 2 - .../form-definitions/mw/v1/CaseFilters.json | 2 - .../form-definitions/nz/v1/CaseFilters.json | 2 - .../form-definitions/ph/v1/CaseFilters.json | 2 - .../form-definitions/sg/v1/CaseFilters.json | 2 - .../form-definitions/th/v1/CaseFilters.json | 2 - .../form-definitions/tz/v1/CaseFilters.json | 2 - .../form-definitions/ukmh/v1/CaseFilters.json | 2 - .../form-definitions/usch/v1/CaseFilters.json | 2 - .../form-definitions/uscr/v1/CaseFilters.json | 4 +- .../form-definitions/za/v1/CaseFilters.json | 2 - .../form-definitions/zm/v1/CaseFilters.json | 2 - .../form-definitions/zw/v1/CaseFilters.json | 2 - .../components/caseList/CaseList.test.tsx | 1 + .../filters/CaseListFilterProvider.test.tsx | 14 +- .../filters/CaseListFilterProvider.tsx | 25 +- .../caseList/filters/CategoriesFilter.tsx | 334 ------------------ .../caseList/filters/CategorySection.tsx | 184 ---------- .../components/caseList/filters/Filters.tsx | 123 ++----- .../caseList/filters/MultiSelectFilter.tsx | 12 +- .../profileList/filters/ProfileFilters.tsx | 2 +- 31 files changed, 34 insertions(+), 705 deletions(-) delete mode 100644 plugin-hrm-form/src/components/caseList/filters/CategoriesFilter.tsx delete mode 100644 plugin-hrm-form/src/components/caseList/filters/CategorySection.tsx diff --git a/hrm-form-definitions/form-definitions/as/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/as/v1/CaseFilters.json index 3ae9a22f92..fb452ea6f5 100644 --- a/hrm-form-definitions/form-definitions/as/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/as/v1/CaseFilters.json @@ -1,7 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, "operatingArea": { "searchable": true, "type": "multi-select", "position": "left" }, "createdDate": { "component": "generate-created-date-filter", "position": "right" }, diff --git a/hrm-form-definitions/form-definitions/br/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/br/v1/CaseFilters.json index 2f69e35016..ddf48788ed 100644 --- a/hrm-form-definitions/form-definitions/br/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/br/v1/CaseFilters.json @@ -1,7 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, diff --git a/hrm-form-definitions/form-definitions/ca/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/ca/v1/CaseFilters.json index fe57adf319..af52556679 100644 --- a/hrm-form-definitions/form-definitions/ca/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/ca/v1/CaseFilters.json @@ -1,7 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, diff --git a/hrm-form-definitions/form-definitions/cl/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/cl/v1/CaseFilters.json index fe57adf319..af52556679 100644 --- a/hrm-form-definitions/form-definitions/cl/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/cl/v1/CaseFilters.json @@ -1,7 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, diff --git a/hrm-form-definitions/form-definitions/co/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/co/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/co/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/co/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/e2e/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/e2e/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/e2e/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/e2e/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/et/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/et/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/et/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/et/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/hu/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/hu/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/hu/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/hu/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/in/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/in/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/in/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/in/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/jm/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/jm/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/jm/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/jm/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/mt/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/mt/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/mt/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/mt/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/mw/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/mw/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/mw/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/mw/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/nz/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/nz/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/nz/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/nz/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/ph/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/ph/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/ph/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/ph/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/sg/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/sg/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/sg/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/sg/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/th/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/th/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/th/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/th/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/tz/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/tz/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/tz/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/tz/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/ukmh/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/ukmh/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/ukmh/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/ukmh/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/usch/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/usch/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/usch/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/usch/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/uscr/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/uscr/v1/CaseFilters.json index 3ae9a22f92..46de27a179 100644 --- a/hrm-form-definitions/form-definitions/uscr/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/uscr/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, - "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "operatingArea": { "searchable": true, "type": "multi-select", "position": "left" }, + "counselor": { "component": "generate-counselor-filter", "position": "left" }, "operatingArea": { "searchable": true, "type": "multi-select", "position": "left" }, "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, diff --git a/hrm-form-definitions/form-definitions/za/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/za/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/za/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/za/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/zm/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/zm/v1/CaseFilters.json index 2f69e35016..1433b2b1b9 100644 --- a/hrm-form-definitions/form-definitions/zm/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/zm/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/hrm-form-definitions/form-definitions/zw/v1/CaseFilters.json b/hrm-form-definitions/form-definitions/zw/v1/CaseFilters.json index fe57adf319..97ac1ba8df 100644 --- a/hrm-form-definitions/form-definitions/zw/v1/CaseFilters.json +++ b/hrm-form-definitions/form-definitions/zw/v1/CaseFilters.json @@ -1,8 +1,6 @@ { "status": { "component": "generate-status-filter", "position": "left" }, "counselor": { "component": "generate-counselor-filter", "position": "left" }, - "category": { "component": "generate-category-filter", "position": "left" }, - "createdDate": { "component": "generate-created-date-filter", "position": "right" }, "updatedDate": { "component": "generate-updated-date-filter", "position": "right" }, "followUpDate": { "type": "date-input", "allowFutureDates": true, "position": "right" } diff --git a/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx b/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx index 4a31953cf9..55a50234fa 100644 --- a/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx +++ b/plugin-hrm-form/src/___tests__/components/caseList/CaseList.test.tsx @@ -40,6 +40,7 @@ import { HrmState, RootState } from '../../../states'; import { CaseStateEntry } from '../../../states/case/types'; import { VALID_EMPTY_CONTACT } from '../../testContacts'; import { newGetTimelineAsyncAction, selectCaseLabel } from '../../../states/case/timeline'; +import { loadTranslations } from '../../../translations'; const { mockFetchImplementation, mockReset, buildBaseURL } = mockLocalFetchDefinitions(); const e2eRules = require('../../../permissions/e2e.json'); diff --git a/plugin-hrm-form/src/___tests__/components/caseList/filters/CaseListFilterProvider.test.tsx b/plugin-hrm-form/src/___tests__/components/caseList/filters/CaseListFilterProvider.test.tsx index 0d882cfb7d..13e3877c0f 100644 --- a/plugin-hrm-form/src/___tests__/components/caseList/filters/CaseListFilterProvider.test.tsx +++ b/plugin-hrm-form/src/___tests__/components/caseList/filters/CaseListFilterProvider.test.tsx @@ -14,15 +14,13 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React from 'react'; +import * as React from 'react'; import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { StorelessThemeProvider } from '@twilio/flex-ui'; import { getFilterComponent } from '../../../../components/caseList/filters/CaseListFilterProvider'; import { dateFilterOptionsInPast } from '../../../../components/caseList/filters/dateFilters'; -import { Category } from '../../../../components/caseList/filters/CategoriesFilter'; -import { Item } from '../../../../components/caseList/filters/MultiSelectFilter'; const themeConf = {}; @@ -75,7 +73,6 @@ describe('CaseListFilterProvider', () => { dateFilterValues: {}, handleApplyStatusFilter: jest.fn(), handleApplyCounselorFilter: jest.fn(), - handleApplyCategoriesFilter: jest.fn(), handleApplyDateRangeFilter: jest.fn(), }; @@ -106,15 +103,6 @@ describe('CaseListFilterProvider', () => { expect(counselorButton).toBeInTheDocument(); }); - test('renders CategoryFilter component', () => { - const component = getFilterComponent('generate-category-filter', baseProps, filterData); - - render({component}); - - const categoriesButton = screen.getByRole('button', { name: /Categories/i }); - expect(categoriesButton).toBeInTheDocument(); - }); - test('renders CreatedDateFilter component', () => { const component = getFilterComponent('generate-created-date-filter', baseProps, filterData); diff --git a/plugin-hrm-form/src/components/caseList/filters/CaseListFilterProvider.tsx b/plugin-hrm-form/src/components/caseList/filters/CaseListFilterProvider.tsx index e366a12364..091db8e448 100644 --- a/plugin-hrm-form/src/components/caseList/filters/CaseListFilterProvider.tsx +++ b/plugin-hrm-form/src/components/caseList/filters/CaseListFilterProvider.tsx @@ -17,7 +17,6 @@ import React from 'react'; import MultiSelectFilter, { Item } from './MultiSelectFilter'; -import CategoriesFilter, { Category } from './CategoriesFilter'; import DateRangeFilter from './DateRangeFilter'; import { DateFilterValue } from '../../../states/caseList/dateFilters'; import { DateFilter } from './dateFilters'; @@ -32,12 +31,10 @@ type FilterDataProps = { strings: Record; statusValues?: Item[]; counselorValues?: Item[]; - categoriesValues?: Category[]; dateFilterValues?: Record; dateFilters?: DateFilter[]; handleApplyStatusFilter?: (values: Item[]) => void; handleApplyCounselorFilter?: (values: Item[]) => void; - handleApplyCategoriesFilter?: (values: Category[]) => void; handleApplyDateRangeFilter?: (filter: DateFilter) => (filterValue: DateFilterValue | undefined) => void; }; @@ -48,7 +45,7 @@ const StatusFilter: React.FC = props return ( = pr = pr ); }; -// Categories Filter Component -const CategoryFilter: React.FC = props => { - const { strings, categoriesValues = [], handleApplyCategoriesFilter, ...baseProps } = props; - - return ( - - ); -}; - // Created Date Filter Component const CreatedDateFilter: React.FC = props => { const { dateFilters = [], dateFilterValues = {}, handleApplyDateRangeFilter, ...baseProps } = props; @@ -137,7 +117,6 @@ const UpdatedDateFilter: React.FC = const FilterComponents: Record> = { 'generate-status-filter': StatusFilter, 'generate-counselor-filter': CounselorFilter, - 'generate-category-filter': CategoryFilter, 'generate-created-date-filter': CreatedDateFilter, 'generate-updated-date-filter': UpdatedDateFilter, }; diff --git a/plugin-hrm-form/src/components/caseList/filters/CategoriesFilter.tsx b/plugin-hrm-form/src/components/caseList/filters/CategoriesFilter.tsx deleted file mode 100644 index acdecce7d5..0000000000 --- a/plugin-hrm-form/src/components/caseList/filters/CategoriesFilter.tsx +++ /dev/null @@ -1,334 +0,0 @@ -/** - * Copyright (C) 2021-2023 Technology Matters - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - */ - -/* eslint-disable no-unused-expressions */ -/* eslint-disable react/prop-types */ -import React, { useEffect, useState, useRef } from 'react'; -import { useForm } from 'react-hook-form'; -import { Template } from '@twilio/flex-ui'; -import ArrowDropDown from '@material-ui/icons/ArrowDropDown'; -import ArrowDropUp from '@material-ui/icons/ArrowDropUp'; - -import SearchInput from './SearchInput'; -import { - Flex, - Box, - MultiSelectButton, - DialogArrow, - FiltersDialog, - FiltersDialogTitle, - FiltersBottomButtons, - FiltersApplyButton, - FiltersClearButton, - MultiSelectUnorderedList, -} from '../../../styles'; -import CategorySection from './CategorySection'; - -/** - * Due to an issue of ReactHookForms transforming names with double quotes or single quotes, - * we're explicitally replacing these chars with placeholders and vice-versa when needed. - * ex: There's a subcategory named '"Thank you for your assistance"', - * (the double quotes should be part of its name). - * - * TODO: Open an issue on ReactHookForms GitHub, so they can fix or tell us the appropiate way - * to handle this scenario. - */ -const SINGLE_QUOTE_PLACEHOLDER = 'SINGLE_QUOTE_PLACEHOLDER'; -const DOUBLE_QUOTES_PLACEHOLDER = 'DOUBLE_QUOTES_PLACEHOLDER'; - -const addPlaceholdersToDefaultValues = (categories: Category[]): Category[] => { - const addPlaceholders = text => text.replace(/'/g, SINGLE_QUOTE_PLACEHOLDER).replace(/"/g, DOUBLE_QUOTES_PLACEHOLDER); - - return categories.map(({ categoryName, subcategories }) => ({ - categoryName, - subcategories: subcategories.map(subcategory => ({ - ...subcategory, - value: addPlaceholders(subcategory.value), - })), - })); -}; - -const transformToCategories = (values: ReactHookFormValues): Category[] => { - const removePlaceholders = text => - text - .replace(new RegExp(SINGLE_QUOTE_PLACEHOLDER, 'g'), "'") - .replace(new RegExp(DOUBLE_QUOTES_PLACEHOLDER, 'g'), '"'); - - const getSubcategories = category => - Object.keys(category).map(subcategoryName => ({ - value: removePlaceholders(subcategoryName), - label: removePlaceholders(subcategoryName), - checked: category[subcategoryName], - })); - - return Object.keys(values).map(categoryName => ({ - categoryName, - subcategories: getSubcategories(values[categoryName]), - })); -}; - -const transformToValues = (categories: Category[]) => { - const getSubcategories = (category: Category) => - category.subcategories.reduce((acc, subcategory) => ({ ...acc, [subcategory.label]: subcategory.checked }), {}); - return categories.reduce((acc, category) => ({ ...acc, [category.categoryName]: getSubcategories(category) }), {}); -}; - -type Subcategory = { - value: string; - label: string; - checked: boolean; -}; -export type Category = { - categoryName: string; - subcategories: Subcategory[]; -}; - -type ReactHookFormValues = { - [name: string]: boolean; -}; - -type OwnProps = { - name: string; - text: string; - defaultValues: Category[]; - withSearch?: boolean; - openedFilter: string; - searchable?: boolean; - searchDescription?: string; - applyFilter: (values: Category[]) => void; - setOpenedFilter: (name: string) => void; -}; - -// eslint-disable-next-line no-use-before-define -type Props = OwnProps; - -const CategoriesFilter: React.FC = ({ - name, - text, - defaultValues: defaultValuesRaw, - openedFilter, - searchable, - searchDescription, - applyFilter, - setOpenedFilter, - // eslint-disable-next-line sonarjs/cognitive-complexity -}) => { - const [defaultValues, setDefaultValues] = useState(addPlaceholdersToDefaultValues(defaultValuesRaw)); - useEffect(() => setDefaultValues(addPlaceholdersToDefaultValues(defaultValuesRaw)), [defaultValuesRaw]); - - const { register, handleSubmit, reset, getValues, setValue, watch } = useForm({ - defaultValues: transformToValues(defaultValues), - }); - - const [selectedCount, setSelectedCount] = useState(0); - const [searchTerm, setSearchTerm] = useState(''); - const [categories, setCategories] = useState(defaultValues); - - const filterButtonElement = useRef(null); - const firstElement = useRef(null); - const lastElement = useRef(null); - - // Force React Hook Forms to rerender whenever defaultValues changes - useEffect(() => { - const updateSelectedCount = () => { - const count = defaultValues - .flatMap(category => category.subcategories) - .reduce((acc, item) => (item.checked ? acc + 1 : acc), 0); - setSelectedCount(count); - }; - - setCategories(defaultValues); - reset(defaultValues); - updateSelectedCount(); - }, [reset, defaultValues]); - - const isOpened = name === openedFilter; - - // Close dialog on ESC - useEffect(() => { - const closeDialog = event => { - if (event.key === 'Escape') { - // Always reset to defaultValues whenever you open/close the component - reset(defaultValues); - setSearchTerm(''); - setOpenedFilter(null); - - filterButtonElement.current?.focus(); - } - }; - - if (isOpened) { - window.addEventListener('keydown', closeDialog); - } - - return () => window.removeEventListener('keydown', closeDialog); - }, [isOpened, defaultValues, reset, setSearchTerm, setOpenedFilter]); - - const onSubmit = (values: ReactHookFormValues) => { - setOpenedFilter(null); - setSearchTerm(''); - - const categories = transformToCategories(values); - applyFilter(categories); - }; - - const handleClick = () => { - // Always reset to defaultValues whenever you open/close the component - reset(defaultValues); - setSearchTerm(''); - - if (isOpened) { - setOpenedFilter(null); - } else { - setOpenedFilter(name); - } - }; - - const handleClear = () => { - const values = getValues(); - const getSubcategoriesFullName = (category, categoryName) => - Object.keys(category).map(subcategoryName => `${categoryName}.${subcategoryName}`); - - // Mark all values as false - Object.keys(values) - .flatMap(categoryName => getSubcategoriesFullName(values[categoryName], categoryName)) - .forEach(subcategoryFullName => setValue(`${subcategoryFullName}`, false)); - - setSearchTerm(''); - }; - - const handleChangeSearch = event => { - const { value } = event.target; - - setSearchTerm(value); - }; - - const clearSearchTerm = () => setSearchTerm(''); - - const handleTabForLastElement = event => { - if (!event.shiftKey && event.key === 'Tab') { - event.preventDefault(); - - firstElement.current?.focus(); - } - }; - - const handleShiftTabForFirstElement = event => { - if (event.shiftKey && event.key === 'Tab') { - event.preventDefault(); - - lastElement.current?.focus(); - } - }; - - const drawCount = () => (selectedCount === 0 ? '' : ` (${selectedCount})`); - - const highlightLabel = (label: string) => { - if (!searchable || searchTerm.length === 0) { - return {label}; - } - - const startIndex = label.toLowerCase().indexOf(searchTerm.toLowerCase()); - const endIndex = startIndex + searchTerm.length; - - const preffix = label.substring(0, startIndex); - const highlighted = label.substring(startIndex, endIndex); - const suffix = label.substring(endIndex); - - return ( - <> - {preffix} - {highlighted} - {suffix} - - ); - }; - - return ( -
- 0)} - type="button" - onClick={handleClick} - ref={ref => { - filterButtonElement.current = ref; - }} - data-testid="FilterBy-Categories-Button" - > - {text} - {drawCount()} - - {isOpened && } - {!isOpened && } - - - {isOpened && ( - - - Filter by: {text} - {searchable && ( - - )} -
- - {categories.map((category, i) => ( -
  • - -
  • - ))} -
    - - - -