Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CHI-1976 case timeline rework #1933

Merged
merged 11 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/global-qa-release-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,6 @@ jobs:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
slack-message: "`[Flex]` Release from ${{ github.ref_type }} `${{ github.ref_name }}` requested by `${{ github.triggering_actor }}` completed with SHA ${{ github.sha }}. Release tag is ${{ steps.create_pre_release.outputs.generated-pre-release-tag }} :rocket:."
slack-message: "`[Flex]` Release from ${{ github.ref_type }} `${{ github.ref_name }}` requested by `${{ github.triggering_actor }}` completed with SHA ${{ github.sha }}. Release tag is `${{ steps.create_pre_release.outputs.generated-pre-release-tag }}` :rocket:."
env:
SLACK_BOT_TOKEN: ${{ env.GITHUB_ACTIONS_SLACK_BOT_TOKEN }}
4 changes: 3 additions & 1 deletion plugin-hrm-form/src/components/NavigableContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type FocusTarget = 'back' | 'close';
type OwnProps = {
task: CustomITask | StandaloneITask;
titleCode: string;
titleValues?: Record<string, string>;
onGoBack?: () => void;
onCloseModal?: () => void;
focusPriority: FocusTarget[];
Expand Down Expand Up @@ -79,6 +80,7 @@ const NavigableContainer: React.FC<Props> = ({
closeModal,
onCloseModal = () => closeModal(),
titleCode,
titleValues = {},
hasHistory,
isModal,
focusPriority = ['back', 'close'],
Expand Down Expand Up @@ -115,7 +117,7 @@ const NavigableContainer: React.FC<Props> = ({
)}
</Box>
<NavigableContainerTitle data-testid="NavigableContainer-Title">
<Template code={titleCode} />
<Template code={titleCode} {...titleValues} />
</NavigableContainerTitle>
{isModal && (
<HeaderCloseButton
Expand Down
6 changes: 6 additions & 0 deletions plugin-hrm-form/src/components/case/Case.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import selectContactByTaskSid from '../../states/contacts/selectContactByTaskSid
import selectCurrentRouteCaseState from '../../states/case/selectCurrentRouteCase';
import { selectCounselorsHash } from '../../states/configuration/selectCounselorsHash';
import { selectCurrentDefinitionVersion, selectDefinitionVersions } from '../../states/configuration/selectDefinitions';
import FullTimelineView from './timeline/FullTimelineView';

export const isStandaloneITask = (task): task is StandaloneITask => {
return task && task.taskSid === 'standalone-task-sid';
Expand Down Expand Up @@ -316,6 +317,11 @@ const Case: React.FC<Props> = ({
/>
);
}

if (routing.subroute === 'timeline') {
return <FullTimelineView task={task} />;
}

return loading || !definitionVersion ? (
<CenteredContainer>
<CircularProgress size={50} />
Expand Down
104 changes: 47 additions & 57 deletions plugin-hrm-form/src/components/case/CaseHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,41 @@ import { Template } from '@twilio/flex-ui';
import { connect, ConnectedProps } from 'react-redux';
import { DefinitionVersion } from 'hrm-form-definitions';

import { CaseContainer } from '../../styles/case';
import { BottomButtonBar, Box, Flex, SaveAndEndButton, StyledNextStepButton } from '../../styles/HrmStyles';
import { CaseContainer, CaseDetailsBorder, ViewButton } from '../../styles/case';
import { BottomButtonBar, Box, SaveAndEndButton, StyledNextStepButton } from '../../styles/HrmStyles';
import CaseDetailsComponent from './CaseDetails';
import Timeline from './Timeline';
import Timeline from './timeline/Timeline';
import CaseSection from './CaseSection';
import { PermissionActions, PermissionActionType } from '../../permissions';
import {
getPermissionsForCase,
getPermissionsForContact,
PermissionActions,
PermissionActionType,
} from '../../permissions';
import { AppRoutes, CaseItemAction, CaseSectionSubroute, NewCaseSubroutes } from '../../states/routing/types';
AppRoutes,
CaseItemAction,
CaseRoute,
CaseSectionSubroute,
NewCaseSubroutes,
} from '../../states/routing/types';
import CaseSummary from './CaseSummary';
import { RootState } from '../../states';
import { CaseDetails } from '../../states/case/types';
import { Case, Contact, CustomITask, EntryInfo, StandaloneITask } from '../../types/types';
import * as RoutingActions from '../../states/routing/actions';
import { newCloseModalAction } from '../../states/routing/actions';
import InformationRow from './InformationRow';
import TimelineInformationRow from './TimelineInformationRow';
import IncidentInformationRow from './IncidentInformationRow';
import DocumentInformationRow from './DocumentInformationRow';
import { householdSectionApi } from '../../states/case/sections/household';
import { perpetratorSectionApi } from '../../states/case/sections/perpetrator';
import { getAseloFeatureFlags } from '../../hrmConfig';
import NavigableContainer from '../NavigableContainer';
import ConnectToCaseButton from './ConnectToCaseButton';
import { isStandaloneITask } from './Case';
import selectContactByTaskSid from '../../states/contacts/selectContactByTaskSid';
import asyncDispatch from '../../states/asyncDispatch';
import { connectToCaseAsyncAction } from '../../states/contacts/saveContact';
import { BannerContainer, Text } from '../caseMergingBanners/styles';
import InfoIcon from '../caseMergingBanners/InfoIcon';
import { selectCurrentTopmostRouteForTask } from '../../states/routing/getRoute';
import selectCurrentRouteCaseState from '../../states/case/selectCurrentRouteCase';
import CaseCreatedBanner from '../caseMergingBanners/CaseCreatedBanner';
import AddToCaseBanner from '../caseMergingBanners/AddToCaseBanner';
import { selectCaseActivityCount } from '../../states/case/timeline';

export type CaseHomeProps = {
task: CustomITask | StandaloneITask;
Expand All @@ -69,26 +69,27 @@ export type CaseHomeProps = {
// eslint-disable-next-line no-use-before-define
type Props = CaseHomeProps & ConnectedProps<typeof connector>;

const MAX_ACTIVITIES_IN_TIMELINE_SECTION = 5;

const CaseHome: React.FC<Props> = ({
definitionVersion,
task,
openModal,
closeModal,
connectCaseToTaskContact,
handleClose,
handleSaveAndEnd,
caseDetails,
can,
connectedCaseState,
taskContact,
isCreating,
hasMoreActivities,
// eslint-disable-next-line sonarjs/cognitive-complexity
}) => {
if (!connectedCaseState) return null; // narrow type before deconstructing
const caseId = connectedCaseState.connectedCase.id;
const {
enable_upload_documents: enableUploadDocuments,
enable_case_merging: enableCaseMerging,
enable_separate_timeline_view: enableSeparateTimelineView,
} = getAseloFeatureFlags();

const onViewCaseItemClick = (targetSubroute: CaseSectionSubroute) => (id: string) => {
Expand All @@ -99,6 +100,10 @@ const CaseHome: React.FC<Props> = ({
openModal({ route: 'case', subroute: targetSubroute, action: CaseItemAction.Add, caseId });
};

const onViewFullTimelineClick = () => {
openModal({ route: 'case', subroute: 'timeline', caseId, page: 0 });
};

const onPrintCase = () => {
openModal({ route: 'case', subroute: 'case-print-view', caseId });
};
Expand Down Expand Up @@ -126,21 +131,6 @@ const CaseHome: React.FC<Props> = ({
followUpDate,
} = caseDetails;
const statusLabel = definitionVersion.caseStatus[status]?.label ?? status;
const isConnectedToTaskContact = taskContact && taskContact.caseId === id;

const { connectedCase } = connectedCaseState;

const { can: canForCase } = getPermissionsForCase(connectedCase.twilioWorkerId, connectedCase.status);
const { can: canForContact } = getPermissionsForContact(taskContact?.twilioWorkerId);

const showConnectToCaseButton = Boolean(
taskContact &&
!taskContact.caseId &&
!isConnectedToTaskContact &&
connectedCase.connectedContacts?.length &&
canForCase(PermissionActions.UPDATE_CASE_CONTACTS) &&
canForContact(PermissionActions.ADD_CONTACT_TO_CASE),
);

const itemRowRenderer = (itemTypeName: string, viewSubroute: CaseSectionSubroute, items: EntryInfo[]) => {
const itemRows = () => {
Expand Down Expand Up @@ -181,7 +171,7 @@ const CaseHome: React.FC<Props> = ({
<>
{incidents.map((item, index) => {
return (
<TimelineInformationRow
<IncidentInformationRow
key={`incident-${index}`}
onClickView={() => onViewCaseItemClick(NewCaseSubroutes.Incident)(item.id)}
definition={caseForms.IncidentForm}
Expand Down Expand Up @@ -228,27 +218,7 @@ const CaseHome: React.FC<Props> = ({
borderBottom: isCreating ? '1px solid #e5e5e5' : 'none',
}}
>
{showConnectToCaseButton && (
<BannerContainer color="yellow" style={{ paddingTop: '12px', paddingBottom: '12px' }}>
<Flex width="100%" justifyContent="space-between">
<Flex alignItems="center">
<InfoIcon color="#fed44b" />
<Text>
<Template code="CaseMerging-AddContactToCase" />
</Text>
</Flex>
<ConnectToCaseButton
caseId={connectedCase.id.toString()}
isConnectedToTaskContact={isConnectedToTaskContact}
onClickConnectToTaskContact={() => {
connectCaseToTaskContact(taskContact, connectedCaseState.connectedCase);
closeModal();
}}
color="black"
/>
</Flex>
</BannerContainer>
)}
<AddToCaseBanner task={task} />

{isCreating && (
<Box marginBottom="14px" width="100%">
Expand Down Expand Up @@ -278,8 +248,22 @@ const CaseHome: React.FC<Props> = ({
<Box margin="25px 0 0 0">
<CaseSummary task={task} />
</Box>
<Box margin="25px 0 0 0">
<Timeline taskSid={task.taskSid} can={can} />
<Box margin="25px 0 0 0" style={{ textAlign: 'center' }}>
<CaseDetailsBorder>
<Timeline
taskSid={task.taskSid}
page={0}
pageSize={enableSeparateTimelineView ? 5 : Number.MAX_SAFE_INTEGER}
titleCode={
hasMoreActivities && enableSeparateTimelineView ? 'Case-Timeline-RecentTitle' : 'Case-Timeline-Title'
}
/>
{hasMoreActivities && (
<ViewButton style={{ marginTop: '10px' }} withDivider={false} onClick={onViewFullTimelineClick}>
<Template code="Case-Timeline-OpenFullTimelineButton" />
</ViewButton>
)}
</CaseDetailsBorder>
</Box>
<Box margin="25px 0 0 0">
<CaseSection
Expand Down Expand Up @@ -348,10 +332,16 @@ CaseHome.displayName = 'CaseHome';
const mapStateToProps = (state: RootState, { task }: CaseHomeProps) => {
const connectedCaseState = selectCurrentRouteCaseState(state, task.taskSid);
const taskContact = isStandaloneITask(task) ? undefined : selectContactByTaskSid(state, task.taskSid)?.savedContact;
const routing = selectCurrentTopmostRouteForTask(state, task.taskSid);
const routing = selectCurrentTopmostRouteForTask(state, task.taskSid) as CaseRoute;
const isCreating = routing.route === 'case' && routing.isCreating;
const activityCount = routing.route === 'case' ? selectCaseActivityCount(state, routing.caseId) : 0;

return { isCreating, connectedCaseState, taskContact };
return {
isCreating,
connectedCaseState,
taskContact,
hasMoreActivities: activityCount > MAX_ACTIVITIES_IN_TIMELINE_SECTION,
};
};

const mapDispatchToProps = (dispatch: Dispatch<any>, { task }: CaseHomeProps) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import React from 'react';
import { Template } from '@twilio/flex-ui';
import type { FormDefinition, LayoutDefinition } from 'hrm-form-definitions';

import { TimelineRow, TimelineText, TimelineLabel, ViewButton, RowItemContainer } from '../../styles/case';
import { RowItemContainer, TimelineLabel, TimelineRow, TimelineText, ViewButton } from '../../styles/case';
import { Box, HiddenText } from '../../styles/HrmStyles';
import { formatValue } from '../common/forms/helpers';
import type { Incident } from '../../types/types';
Expand All @@ -35,7 +35,7 @@ type OwnProps = {
const RowItem: React.FC = ({ children }) => <RowItemContainer style={{ flex: 1 }}>{children}</RowItemContainer>;
RowItem.displayName = 'RowItem';

const TimelineInformationRow: React.FC<OwnProps> = ({ definition, values, layoutDefinition, onClickView }) => {
const IncidentInformationRow: React.FC<OwnProps> = ({ definition, values, layoutDefinition, onClickView }) => {
return (
<TimelineRow>
{layoutDefinition.previewFields.map((name, index) => {
Expand Down Expand Up @@ -66,6 +66,6 @@ const TimelineInformationRow: React.FC<OwnProps> = ({ definition, values, layout
);
};

TimelineInformationRow.displayName = 'TimelineInformationRow';
IncidentInformationRow.displayName = 'IncidentInformationRow';

export default TimelineInformationRow;
export default IncidentInformationRow;
96 changes: 96 additions & 0 deletions plugin-hrm-form/src/components/case/timeline/FullTimelineView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* 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/.
*/

import React, { Dispatch } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { Template } from '@twilio/flex-ui';

import NavigableContainer from '../../NavigableContainer';
import { CaseLayout } from '../../../styles/case';
import { CustomITask, StandaloneITask } from '../../../types/types';
import AddToCaseBanner from '../../caseMergingBanners/AddToCaseBanner';
import Timeline from './Timeline';
import { RootState } from '../../../states';
import { selectCurrentTopmostRouteForTask } from '../../../states/routing/getRoute';
import { CaseTimelineRoute, ChangeRouteMode } from '../../../states/routing/types';
import { selectCaseActivityCount } from '../../../states/case/timeline';
import Pagination from '../../pagination';
import { changeRoute } from '../../../states/routing/actions';

type MyProps = {
task: CustomITask | StandaloneITask;
};

const TIMELINE_PAGE_SIZE = 25;

const mapStateToProps = (state: RootState, { task }: MyProps) => {
const { caseId, page = 0 } = selectCurrentTopmostRouteForTask(state, task.taskSid) as CaseTimelineRoute;
return {
page,
activityCount: selectCaseActivityCount(state, caseId),
caseId,
};
};

const mapDispatchToProps = (dispatch: Dispatch<any>, { task }: MyProps) => {
return {
changePage: (caseId: string) => (page: number) =>
dispatch(
changeRoute({ route: 'case', subroute: 'timeline', caseId, page }, task.taskSid, ChangeRouteMode.Replace),
),
};
};

const connector = connect(mapStateToProps, mapDispatchToProps);

type Props = MyProps & ConnectedProps<typeof connector>;

const FullTimelineView: React.FC<Props> = ({ task, page, activityCount, changePage, caseId }: Props) => {
const changePageForThisCase = changePage(caseId);

return (
<CaseLayout>
<NavigableContainer
task={task}
titleCode="Case-Timeline-ModalTitle"
titleValues={{ caseId }}
style={{ textAlign: 'center' }}
>
<AddToCaseBanner task={task} />
<Timeline taskSid={task.taskSid} pageSize={TIMELINE_PAGE_SIZE} page={page} titleCode="Case-Timeline-Title" />
<p style={{ marginTop: '10px', fontStyle: 'italic' }}>
<Template
code="Case-Timeline-PaginationDescription"
from={page * TIMELINE_PAGE_SIZE + 1}
to={Math.min((page + 1) * TIMELINE_PAGE_SIZE, activityCount)}
total={activityCount}
/>
</p>
{activityCount > TIMELINE_PAGE_SIZE && (
<Pagination
pagesCount={Math.ceil(activityCount / TIMELINE_PAGE_SIZE)}
page={page}
handleChangePage={changePageForThisCase}
/>
)}
</NavigableContainer>
</CaseLayout>
);
};

FullTimelineView.displayName = 'FullTimelineView';

export default connector(FullTimelineView);
Loading
Loading