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-3207: Replace section specific permissions with generic ones that cover all section types #2761

Merged
merged 3 commits into from
Feb 6, 2025
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
27 changes: 4 additions & 23 deletions plugin-hrm-form/src/___tests__/components/case/CaseHome.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import configureMockStore from 'redux-mock-store';
import each from 'jest-each';

import { mockGetDefinitionsResponse } from '../../mockGetConfig';
import { fetchRules } from '../../../permissions/fetchRules';
import { validateAndSetPermissionRules } from '../../../permissions';
import { mockLocalFetchDefinitions } from '../../mockFetchDefinitions';
import CaseHome, { CaseHomeProps } from '../../../components/case/CaseHome';
import { Case, CustomITask } from '../../../types/types';
Expand All @@ -40,27 +38,10 @@ import { FullCaseSection } from '../../../services/caseSectionService';
import { TaskSID } from '../../../types/twilio';
import { CaseStateEntry } from '../../../states/case/types';

const e2eRules = require('../../../permissions/e2e.json');

jest.mock('../../../permissions/fetchRules', () => {
return {
fetchRules: jest.fn(() => {
throw new Error('fetchRules not mocked!');
}),
};
});

beforeEach(async () => {
const fetchRulesSpy = fetchRules as jest.MockedFunction<typeof fetchRules>;
fetchRulesSpy.mockResolvedValueOnce(e2eRules);
await validateAndSetPermissionRules();

jest.mock('../../../permissions', () => ({
...jest.requireActual('../../../permissions'),
getInitializedCan: jest.fn(() => () => true),
}));
});

jest.mock('../../../permissions', () => ({
getInitializedCan: jest.fn(() => () => true),
PermissionActions: {},
}));
// Called by the <Timeline/> subcomponent
jest.mock('../../../services/CaseService', () => ({
getCaseTimeline: jest.fn(() => Promise.resolve({ activities: [], count: 0 })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ import { newGoBackAction } from '../../../states/routing/actions';
import { RecursivePartial } from '../../RecursivePartial';
import { RootState } from '../../../states';
import { VALID_EMPTY_CASE } from '../../testCases';
import { getInitializedCan } from '../../../permissions';

jest.mock('../../../permissions', () => ({
getInitializedCan: jest.fn(),
PermissionActions: {},
}));

const mockGetInitializedCan = getInitializedCan as jest.MockedFunction<typeof getInitializedCan>;

const { mockFetchImplementation, mockReset, buildBaseURL } = mockLocalFetchDefinitions();

Expand Down Expand Up @@ -139,14 +147,14 @@ describe('Test ViewHousehold', () => {

mockV1 = await loadDefinition(formDefinitionsBaseUrl);
mockGetDefinitionsResponse(getDefinitionVersions, DefinitionVersionId.v1, mockV1);
mockGetInitializedCan.mockReturnValue(() => true);
});

beforeEach(async () => {
ownProps = {
definitionVersion: mockV1,
task: task as StandaloneITask,
sectionApi: householdSectionApi,
canEdit: () => true,
};
});

Expand All @@ -166,10 +174,11 @@ describe('Test ViewHousehold', () => {
expect(store.dispatch).toHaveBeenCalledWith(newGoBackAction(task.taskSid));
});
test('Test no edit permissions', async () => {
mockGetInitializedCan.mockReturnValue(() => false);
render(
<StorelessThemeProvider themeConf={themeConf}>
<Provider store={store}>
<ViewCaseItem {...ownProps} canEdit={() => false} />
<ViewCaseItem {...ownProps} />
</Provider>
</StorelessThemeProvider>,
);
Expand Down
22 changes: 9 additions & 13 deletions plugin-hrm-form/src/components/case/Case.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import AddEditCaseItem, { AddEditCaseItemProps } from './AddEditCaseItem';
import ViewCaseItem from './ViewCaseItem';
import { bindFileUploadCustomHandlers } from './documentUploadHandler';
import { recordBackendError } from '../../fullStory';
import { getInitializedCan, PermissionActions } from '../../permissions';
import { getInitializedCan } from '../../permissions';
import { CenteredContainer } from './styles';
import EditCaseSummary from './EditCaseSummary';
import { documentSectionApi } from '../../states/case/sections/document';
Expand Down Expand Up @@ -183,13 +183,9 @@ const Case: React.FC<Props> = ({
definitionVersion,
};

const renderCaseItemPage = (
sectionApi: CaseSectionApi,
editPermission: typeof PermissionActions[keyof typeof PermissionActions],
extraAddEditProps: Partial<AddEditCaseItemProps> = {},
) => {
const renderCaseItemPage = (sectionApi: CaseSectionApi, extraAddEditProps: Partial<AddEditCaseItemProps> = {}) => {
if (isViewCaseSectionRoute(routing)) {
return <ViewCaseItem {...addScreenProps} sectionApi={sectionApi} canEdit={() => can(editPermission)} />;
return <ViewCaseItem {...addScreenProps} sectionApi={sectionApi} />;
}
return (
<AddEditCaseItem
Expand All @@ -204,17 +200,17 @@ const Case: React.FC<Props> = ({

switch (subroute) {
case NewCaseSubroutes.Note:
return renderCaseItemPage(noteSectionApi, PermissionActions.EDIT_NOTE);
return renderCaseItemPage(noteSectionApi);
case NewCaseSubroutes.Referral:
return renderCaseItemPage(referralSectionApi, PermissionActions.EDIT_REFERRAL);
return renderCaseItemPage(referralSectionApi);
case NewCaseSubroutes.Household:
return renderCaseItemPage(householdSectionApi, PermissionActions.EDIT_HOUSEHOLD);
return renderCaseItemPage(householdSectionApi);
case NewCaseSubroutes.Perpetrator:
return renderCaseItemPage(perpetratorSectionApi, PermissionActions.EDIT_PERPETRATOR);
return renderCaseItemPage(perpetratorSectionApi);
case NewCaseSubroutes.Incident:
return renderCaseItemPage(incidentSectionApi, PermissionActions.EDIT_INCIDENT);
return renderCaseItemPage(incidentSectionApi);
case NewCaseSubroutes.Document:
return renderCaseItemPage(documentSectionApi, PermissionActions.EDIT_DOCUMENT, {
return renderCaseItemPage(documentSectionApi, {
customFormHandlers: bindFileUploadCustomHandlers(connectedCase.id),
reactHookFormOptions: {
shouldUnregister: false,
Expand Down
8 changes: 4 additions & 4 deletions plugin-hrm-form/src/components/case/CaseHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,21 +212,21 @@ const CaseHome: React.FC<Props> = ({
</Box>
<Box margin="25px 0 0 0">
<CaseSection
canAdd={() => can(PermissionActions.ADD_HOUSEHOLD)}
canAdd={() => can(PermissionActions.ADD_CASE_SECTION)}
taskSid={task.taskSid}
sectionType="household"
/>
</Box>
<Box margin="25px 0 0 0">
<CaseSection
canAdd={() => can(PermissionActions.ADD_PERPETRATOR)}
canAdd={() => can(PermissionActions.ADD_CASE_SECTION)}
taskSid={task.taskSid}
sectionType="perpetrator"
/>
</Box>
<Box margin="25px 0 0 0">
<CaseSection
canAdd={() => can(PermissionActions.ADD_INCIDENT)}
canAdd={() => can(PermissionActions.ADD_CASE_SECTION)}
taskSid={task.taskSid}
sectionType="incident"
sectionRenderer={({ sectionTypeSpecificData, sectionType, sectionId }, onClickView) => (
Expand All @@ -243,7 +243,7 @@ const CaseHome: React.FC<Props> = ({
{enableUploadDocuments && (
<Box margin="25px 0 0 0">
<CaseSection
canAdd={() => can(PermissionActions.ADD_DOCUMENT)}
canAdd={() => can(PermissionActions.ADD_CASE_SECTION)}
taskSid={task.taskSid}
sectionType="document"
sectionRenderer={(caseSection, onClickView) => (
Expand Down
76 changes: 24 additions & 52 deletions plugin-hrm-form/src/components/case/ViewCaseItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,91 +16,63 @@

/* eslint-disable react/prop-types */
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { Template } from '@twilio/flex-ui';
import Edit from '@material-ui/icons/Edit';
import { DefinitionVersion, isNonSaveable } from 'hrm-form-definitions';

import { CaseStateEntry } from '../../states/case/types';
import { BottomButtonBar, Box, StyledNextStepButton } from '../../styles';
import { CaseLayout, FullWidthFormTextContainer } from './styles';
import { RootState } from '../../states';
import { SectionEntry, SectionEntryValue } from '../common/forms/SectionEntry';
import ActionHeader from './ActionHeader';
import type { CustomITask, StandaloneITask } from '../../types/types';
import { AppRoutes, CaseItemAction, isViewCaseSectionRoute } from '../../states/routing/types';
import { CaseItemAction, isViewCaseSectionRoute } from '../../states/routing/types';
import * as RoutingActions from '../../states/routing/actions';
import { CaseSectionApi } from '../../states/case/sections/api';
import { FormTargetObject } from '../common/forms/types';
import NavigableContainer from '../NavigableContainer';
import { selectCurrentTopmostRouteForTask } from '../../states/routing/getRoute';
import selectCurrentRouteCaseState from '../../states/case/selectCurrentRouteCase';
import selectCaseItemHistory from '../../states/case/sections/selectCaseItemHistory';
import { CaseSectionTypeSpecificData } from '../../services/caseSectionService';
import { getInitializedCan, PermissionActions } from '../../permissions';

export type ViewCaseItemProps = {
task: CustomITask | StandaloneITask;
definitionVersion: DefinitionVersion;
sectionApi: CaseSectionApi;
includeAddedTime?: boolean;
canEdit: () => boolean;
};

const mapStateToProps = (
state: RootState,
{ task, sectionApi }: ViewCaseItemProps,
): {
caseItemHistory: ReturnType<typeof selectCaseItemHistory>;
currentRoute: AppRoutes;
sections: CaseStateEntry['sections'];
form: CaseSectionTypeSpecificData;
} => {
const { sections } = selectCurrentRouteCaseState(state, task.taskSid) || {};
const currentRoute = selectCurrentTopmostRouteForTask(state, task.taskSid);
if (isViewCaseSectionRoute(currentRoute)) {
return {
caseItemHistory: selectCaseItemHistory(state, currentRoute.caseId, sectionApi, currentRoute.id),
currentRoute,
sections,
form: sectionApi.getSectionItemById(sections, currentRoute.id).sectionTypeSpecificData,
};
}
return {
currentRoute,
form: undefined,
caseItemHistory: undefined,
sections: undefined,
};
};

const mapToDispatchProps = {
changeRoute: RoutingActions.changeRoute,
goBack: RoutingActions.newGoBackAction,
};
const ViewCaseItem: React.FC<ViewCaseItemProps> = ({ task, definitionVersion, sectionApi }) => {
// Hooks
const { sections, connectedCase } = useSelector(
(state: RootState) =>
selectCurrentRouteCaseState(state, task.taskSid) || { sections: undefined, connectedCase: undefined },
);

const connector = connect(mapStateToProps, mapToDispatchProps);
const currentRoute = useSelector((state: RootState) => selectCurrentTopmostRouteForTask(state, task.taskSid));
const caseItemHistory = useSelector((state: RootState) =>
isViewCaseSectionRoute(currentRoute)
? selectCaseItemHistory(state, currentRoute.caseId, sectionApi, currentRoute.id)
: null,
);

type Props = ViewCaseItemProps & ConnectedProps<typeof connector>;
const dispatch = useDispatch();

const ViewCaseItem: React.FC<Props> = ({
task,
currentRoute,
changeRoute,
definitionVersion,
sectionApi,
sections,
canEdit,
caseItemHistory,
form,
}) => {
// Conditional returns allowed from here
if (!isViewCaseSectionRoute(currentRoute) || !sections) {
return null;
}

const canEdit = getInitializedCan()(PermissionActions.EDIT_CASE_SECTION, connectedCase);
const form = sectionApi.getSectionItemById(sections, currentRoute.id).sectionTypeSpecificData;

const { addingCounsellorName, added, updatingCounsellorName, updated } = caseItemHistory;
const formDefinition = sectionApi.getSectionFormDefinition(definitionVersion).filter(fd => !isNonSaveable(fd));

const onEditCaseItemClick = () => {
changeRoute({ ...currentRoute, action: CaseItemAction.Edit }, task.taskSid);
dispatch(RoutingActions.changeRoute({ ...currentRoute, action: CaseItemAction.Edit }, task.taskSid));
};

const targetObject: FormTargetObject = {
Expand Down Expand Up @@ -132,7 +104,7 @@ const ViewCaseItem: React.FC<Props> = ({
</Box>
)}
</Box>
{canEdit() && (
{canEdit && (
<BottomButtonBar>
<Box marginRight="15px">
<StyledNextStepButton
Expand All @@ -154,4 +126,4 @@ const ViewCaseItem: React.FC<Props> = ({

ViewCaseItem.displayName = 'ViewCaseItem';

export default connector(ViewCaseItem);
export default ViewCaseItem;
7 changes: 3 additions & 4 deletions plugin-hrm-form/src/components/case/timeline/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import DialogContent from '@material-ui/core/DialogContent';
import CallTypeIcon from '../../common/icons/CallTypeIcon';
import TimelineIcon, { IconType } from './TimelineIcon';
import { CaseSectionFont, TimelineCallTypeIcon, TimelineDate, TimelineRow, TimelineText, ViewButton } from '../styles';
import { Box, Row } from '../../../styles';
import { Box, Row, colors } from '../../../styles';
import CaseAddButton from '../CaseAddButton';
import { Case, Contact, CustomITask } from '../../../types/types';
import { isCaseSectionTimelineActivity, isContactTimelineActivity } from '../../../states/case/types';
Expand All @@ -34,7 +34,6 @@ import { newOpenModalAction } from '../../../states/routing/actions';
import { RootState } from '../../../states';
import { newGetTimelineAsyncAction, selectTimeline, UITimelineActivity } from '../../../states/case/timeline';
import selectCurrentRouteCaseState from '../../../states/case/selectCurrentRouteCase';
import { colors } from '../../../styles/banners';
import InfoIcon from '../../caseMergingBanners/InfoIcon';
import { selectCurrentTopmostRouteForTask } from '../../../states/routing/getRoute';
import asyncDispatch from '../../../states/asyncDispatch';
Expand Down Expand Up @@ -159,12 +158,12 @@ const Timeline: React.FC<Props> = ({
<CaseAddButton
templateCode="Case-Note"
onClick={handleAddNoteClick}
disabled={!can(PermissionActions.ADD_NOTE, connectedCase)}
disabled={!can(PermissionActions.ADD_CASE_SECTION, connectedCase)}
/>
<CaseAddButton
templateCode="Case-Referral"
onClick={handleAddReferralClick}
disabled={!can(PermissionActions.ADD_REFERRAL, connectedCase)}
disabled={!can(PermissionActions.ADD_CASE_SECTION, connectedCase)}
withDivider
/>
</Box>
Expand Down
2 changes: 2 additions & 0 deletions plugin-hrm-form/src/permissions/e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"closeCase": [["everyone"]],
"reopenCase": [["isSupervisor"]],
"caseStatusTransition": [],
"addCaseSection": [["isSupervisor"],["isCreator", "isCaseOpen"]],
"editCaseSection": [["isSupervisor"],["isCreator", "isCaseOpen"]],
"addNote": [["everyone"]],
"editNote": [["everyone"]],
"addReferral": [["everyone"]],
Expand Down
14 changes: 2 additions & 12 deletions plugin-hrm-form/src/permissions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,8 @@ export const CaseActions = {
CLOSE_CASE: 'closeCase',
REOPEN_CASE: 'reopenCase',
CASE_STATUS_TRANSITION: 'caseStatusTransition',
ADD_NOTE: 'addNote',
EDIT_NOTE: 'editNote',
ADD_REFERRAL: 'addReferral',
EDIT_REFERRAL: 'editReferral',
ADD_HOUSEHOLD: 'addHousehold',
EDIT_HOUSEHOLD: 'editHousehold',
ADD_PERPETRATOR: 'addPerpetrator',
EDIT_PERPETRATOR: 'editPerpetrator',
ADD_INCIDENT: 'addIncident',
EDIT_INCIDENT: 'editIncident',
ADD_DOCUMENT: 'addDocument',
EDIT_DOCUMENT: 'editDocument',
ADD_CASE_SECTION: 'addCaseSection',
EDIT_CASE_SECTION: 'editCaseSection',
EDIT_CASE_OVERVIEW: 'editCaseOverview',
UPDATE_CASE_CONTACTS: 'updateCaseContacts',
} as const;
Expand Down
Loading