Skip to content

Commit

Permalink
[MDS-6204] - condition review assignment (#3355)
Browse files Browse the repository at this point in the history
* Create functionality to assign a user to a condition_category.

* fix rebase conflicts in PermitConditions.

* update snapshots

* added new resources to test_expected_auth.py

* added be tests and updated permitConditionCategorySlice.spec.ts

* enhance mocks and update snap

* updating loading of assigned_review_user in permit_condition_category.py
  • Loading branch information
matbusby-fw authored Dec 20, 2024
1 parent d2c74f0 commit 9e6145a
Show file tree
Hide file tree
Showing 42 changed files with 1,945 additions and 584 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ALTER TABLE permit_condition_category
ADD COLUMN user_sub VARCHAR,
ADD CONSTRAINT fk_permit_condition_category_user FOREIGN KEY (user_sub)
REFERENCES "user" (sub) ON DELETE SET NULL ON UPDATE CASCADE;

COMMENT ON COLUMN permit_condition_category.user_sub IS 'The user assigned to this permit condition category to review it';

ALTER TABLE permit_condition_category_version
ADD COLUMN user_sub VARCHAR,
ADD CONSTRAINT fk_permit_condition_category_version_user FOREIGN KEY (user_sub)
REFERENCES "user" (sub) ON DELETE SET NULL ON UPDATE CASCADE;
11 changes: 6 additions & 5 deletions services/common/src/components/forms/RenderLargeSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,18 @@ const RenderLargeSelect = (props) => (
}
>
<Select
loading={props.loading}
virtual={false}
showSearch
disabled={props.disabled}
id={props.id}
dropdownMatchSelectWidth
showSearch
style={{ width: "100%" }}
defaultActiveFirstOption={false}
placeholder={props.placeholder}
notFoundContent="Not Found"
dropdownMatchSelectWidth
backfill
style={{ width: "100%" }}
options={props.dataSource}
placeholder={props.placeholder}
filterOption={() => true}
onSearch={props.handleSearch}
onSelect={props.handleSelect}
Expand All @@ -75,7 +77,6 @@ const RenderLargeSelect = (props) => (
props.handleFocus();
props.input.onFocus(event);
}}
disabled={props.disabled}
/>
</Form.Item>
);
Expand Down
16 changes: 14 additions & 2 deletions services/common/src/components/forms/RenderSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface SelectProps extends BaseInputProps {
data: IOption[];
onSelect?: (value, option) => void;
allowClear?: boolean;
onSearch?: (value) => void;
enableGetPopupContainer?: boolean;
}

export const RenderSelect: FC<SelectProps> = ({
Expand All @@ -24,11 +26,14 @@ export const RenderSelect: FC<SelectProps> = ({
input,
placeholder = "Please select",
data = [],
onSelect = () => { },
onSelect = () => {},
allowClear = true,
disabled = false,
required = false,
showOptional = true,
loading = false,
enableGetPopupContainer = true,
onSearch,
}) => {
const [isDirty, setIsDirty] = useState(meta.touched);
return (
Expand Down Expand Up @@ -60,18 +65,22 @@ export const RenderSelect: FC<SelectProps> = ({
getValueProps={() => input.value !== "" && { value: input.value }}
>
<Select
loading={loading}
virtual={false}
data-cy={input.name}
disabled={disabled}
allowClear={allowClear}
dropdownMatchSelectWidth
getPopupContainer={(trigger) => trigger.parentNode}
getPopupContainer={
enableGetPopupContainer ? (trigger) => trigger.parentNode : undefined
}
showSearch
dropdownStyle={{ zIndex: 100000, position: "relative" }}
placeholder={placeholder}
optionFilterProp="children"
filterOption={caseInsensitiveLabelFilter}
id={id}
onSearch={onSearch}
onChange={(changeValue) => {
setIsDirty(true);
input.onChange(changeValue);
Expand All @@ -81,6 +90,9 @@ export const RenderSelect: FC<SelectProps> = ({
setIsDirty(true);
}
}}
onFocus={(event) => {
input.onFocus(event);
}}
onSelect={onSelect}
options={data}
/>
Expand Down
3 changes: 3 additions & 0 deletions services/common/src/components/forms/RenderSubmitButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ interface RenderSubmitButtonProps {
buttonProps?: ButtonProps & React.RefAttributes<HTMLElement>;
disableOnClean?: boolean;
iconButton?: boolean;
icon?: ReactNode;
}

const RenderSubmitButton: FC<RenderSubmitButtonProps> = ({
buttonText = "Save Changes",
buttonProps,
disableOnClean = true,
iconButton = false,
icon
}) => {
const { formName, isEditMode } = useContext(FormContext);
const submitting = useSelector(isSubmitting(formName));
Expand All @@ -31,6 +33,7 @@ const RenderSubmitButton: FC<RenderSubmitButtonProps> = ({
disabled={disabled}
loading={submitting}
htmlType="submit"
icon={icon}
aria-label="Submit"
{...buttonProps}
>
Expand Down
9 changes: 4 additions & 5 deletions services/common/src/constants/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,18 +86,16 @@ export const PERMIT_CONDITIONS = (mineGuid, permitGuid, permitAmendmentGuid) =>
export const PERMIT_CONDITION = (mineGuid, permitGuid, permitAmendmentGuid, permitConditionGuid) =>
`/mines/${mineGuid}/permits/${permitGuid}/amendments/${permitAmendmentGuid}/conditions/${permitConditionGuid}`;

export const PERMIT_AMENDMENT_CONDITION_CATEGORIES = (
mineGuid,
permitGuid,
permitAmendmentGuid,
) =>
export const PERMIT_AMENDMENT_CONDITION_CATEGORIES = (mineGuid, permitGuid, permitAmendmentGuid) =>
`/mines/${mineGuid}/permits/${permitGuid}/amendments/${permitAmendmentGuid}/condition-categories`;

export const STANDARD_PERMIT_CONDITIONS = (noticeOfWorkType) =>
`/mines/permits/standard-conditions/${noticeOfWorkType}`;
export const STANDARD_PERMIT_CONDITION = (permitConditionGuid) =>
`/mines/permits/standard-conditions/${permitConditionGuid}`;

export const PERMIT_AMENDMENT_CONDITION_ASSIGN_REVIEWER = "mines/permits/condition-category/assign-review-user";

export const PERMIT_SERVICE_EXTRACTION = `/mines/permits/condition-extraction`;
export const POLL_PERMIT_SERVICE_EXTRACTION = (taskId: string) =>
`/mines/permits/condition-extraction/${taskId}`;
Expand Down Expand Up @@ -390,3 +388,4 @@ export const APP_HELP = (helpKey: string, params?: { system?: string; help_guid?

// User
export const USER_PROFILE = () => "/users/profile";
export const USER_SEARCH = (searchTerm: string) => `/users?search_term=${searchTerm}`;
1 change: 1 addition & 0 deletions services/common/src/constants/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum FORM {
ARCHIVE_DOCUMENT = "ARCHIVE_DOCUMENT",
EDIT_HELP_GUIDE = "EDIT_HELP_GUIDE",
INLINE_EDIT_PERMIT_CONDITION_CATEGORY = "INLINE_EDIT_PERMIT_CONDITION_CATEGORY",
PERMIT_CONDITION_REVIEW_ASSIGNMENT = "PERMIT_CONDITION_REVIEW_ASSIGNMENT",
UPDATE_MAJOR_MINE_APPLICATION = "UPDATE_MAJOR_MINE_APPLICATION",
UPDATE_IRT = "UPDATE_IRT"
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IMineReportPermitRequirement } from "@mds/common/interfaces";
import { IMineReportPermitRequirement, IUser } from "@mds/common/interfaces";


export interface IBoundingBox {
Expand Down Expand Up @@ -33,5 +33,6 @@ export interface IPermitConditionCategory {
description: string;
display_order: number;
step: string;
assigned_review_user?: IUser
conditions?: IPermitCondition[]
}
2 changes: 1 addition & 1 deletion services/common/src/redux/reducers/partiesReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as actionTypes from "@mds/common/constants/actionTypes";
import { PARTIES } from "@mds/common/constants/reducerTypes";
import { createItemMap, createItemIdsArray } from "../utils/helpers";
import { RootState } from "@mds/common/redux/rootState";
import { IParty, ItemMap, IPartyAppt, IPageData, IAddPartyFormState, IOption } from "@mds/common";
import { IParty, ItemMap, IPartyAppt, IPageData, IAddPartyFormState, IOption } from "@mds/common/interfaces";

/**
* @file partiesReducer.js
Expand Down
140 changes: 126 additions & 14 deletions services/common/src/redux/slices/permitConditionCategorySlice.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
import { searchConditionCategories, searchConditionCategoriesReducer, getConditionCategories } from "./permitConditionCategorySlice";
import {
searchConditionCategories,
searchConditionCategoriesReducer,
getConditionCategories,
unassignReviewer,
assignReviewer,
} from "./permitConditionCategorySlice";
import { ENVIRONMENT } from "@mds/common/constants";
import CustomAxios from "@mds/common/redux/customAxios";
import { configureStore } from "@reduxjs/toolkit";
import { notification } from "antd";

const showLoadingMock = jest.fn().mockReturnValue({ type: "SHOW_LOADING", payload: { show: true } });
const hideLoadingMock = jest.fn().mockReturnValue({ type: "HIDE_LOADING", payload: { show: false } });
const showLoadingMock = jest
.fn()
.mockReturnValue({ type: "SHOW_LOADING", payload: { show: true } });
const hideLoadingMock = jest
.fn()
.mockReturnValue({ type: "HIDE_LOADING", payload: { show: false } });
const notificationSuccessMock = jest.fn();

jest.mock("@mds/common/redux/customAxios");
jest.mock("react-redux-loading-bar", () => ({
showLoading: () => showLoadingMock,
hideLoading: () => hideLoadingMock,
}));
jest.mock("antd", () => ({
notification: {
success: jest.fn(),
},
}));

describe("permitConditionCategorySlice", () => {
let store;
Expand All @@ -19,7 +36,7 @@ describe("permitConditionCategorySlice", () => {
store = configureStore({
reducer: {
searchConditionCategories: searchConditionCategoriesReducer,
}
},
});
});

Expand All @@ -32,20 +49,20 @@ describe("permitConditionCategorySlice", () => {
data: {
records: [
{ code: "TEST1", description: "Test Category 1" },
{ code: "TEST2", description: "Test Category 2" }
]
}
{ code: "TEST2", description: "Test Category 2" },
],
},
};

it("should fetch condition categories successfully", async () => {
(CustomAxios as jest.Mock).mockImplementation(() => ({
get: jest.fn().mockResolvedValue(mockResponse)
get: jest.fn().mockResolvedValue(mockResponse),
}));

const payload = {
query: "test",
exclude: ["excluded1"],
limit: 10
limit: 10,
};

await store.dispatch(searchConditionCategories(payload));
Expand All @@ -56,14 +73,16 @@ describe("permitConditionCategorySlice", () => {
expect(showLoadingMock).toHaveBeenCalledTimes(1);
expect(hideLoadingMock).toHaveBeenCalledTimes(1);

expect(getConditionCategories({ searchConditionCategories: state })).toEqual(mockResponse.data.records);
expect(getConditionCategories({ searchConditionCategories: state })).toEqual(
mockResponse.data.records
);
expect(CustomAxios).toHaveBeenCalledWith({ errorToastMessage: "default" });
});

it("should handle API error", async () => {
const error = new Error("API Error");
(CustomAxios as jest.Mock).mockImplementation(() => ({
get: jest.fn().mockRejectedValue(error)
get: jest.fn().mockRejectedValue(error),
}));

await store.dispatch(searchConditionCategories({}));
Expand All @@ -75,13 +94,13 @@ describe("permitConditionCategorySlice", () => {
it("should construct correct URL with query parameters", async () => {
const getMock = jest.fn().mockResolvedValue(mockResponse);
(CustomAxios as jest.Mock).mockImplementation(() => ({
get: getMock
get: getMock,
}));

const payload = {
query: "test",
exclude: ["exc1", "exc2"],
limit: 5
limit: 5,
};

await store.dispatch(searchConditionCategories(payload));
Expand All @@ -96,4 +115,97 @@ describe("permitConditionCategorySlice", () => {
expect(getMock.mock.calls[0][0]).toContain("limit=5");
});
});
});

describe("assignReviewer", () => {
const mockResponse = {
data: {
assigned_review_user: { display_name: "Test User" },
description: "Test Condition",
},
};

it("should successfully assign a reviewer", async () => {
(CustomAxios as jest.Mock).mockImplementation(() => ({
post: jest.fn().mockResolvedValue(mockResponse),
}));

const payload = {
assigned_review_user: "user1",
condition_category_code: "code1",
};

await store.dispatch(assignReviewer(payload));

// Verify loading state management
expect(showLoadingMock).toHaveBeenCalledTimes(1);
expect(hideLoadingMock).toHaveBeenCalledTimes(1);

// Verify success notification
expect(notification.success).toHaveBeenCalledWith({
message: `Successfully assigned ${mockResponse.data.assigned_review_user.display_name} to review ${mockResponse.data.description}`,
duration: 10,
});

expect(CustomAxios).toHaveBeenCalledWith({ errorToastMessage: "default" });
});

it("should handle API error when assigning a reviewer", async () => {
const error = new Error("API Error");
(CustomAxios as jest.Mock).mockImplementation(() => ({
post: jest.fn().mockRejectedValue(error),
}));

const payload = {
assigned_review_user: "user1",
condition_category_code: "code1",
};

await store.dispatch(assignReviewer(payload));

expect(notificationSuccessMock).not.toHaveBeenCalled();
});
});

describe("unassignReviewer", () => {
const mockResponse = {
data: {
description: "Test Condition",
},
};

it("should successfully unassign a reviewer", async () => {
(CustomAxios as jest.Mock).mockImplementation(() => ({
put: jest.fn().mockResolvedValue(mockResponse),
}));

const payload = {
condition_category_code: "code1",
};

await store.dispatch(unassignReviewer(payload));

// Verify success notification
expect(notification.success).toHaveBeenCalledWith({
message: `Successfully unassigned user from ${mockResponse.data.description}`,
duration: 10,
});

expect(CustomAxios).toHaveBeenCalledWith({ errorToastMessage: "default" });
});

it("should handle API error when unassigning a reviewer", async () => {
const error = new Error("API Error");
(CustomAxios as jest.Mock).mockImplementation(() => ({
put: jest.fn().mockRejectedValue(error),
}));

const payload = {
condition_category_code: "code1",
};

await store.dispatch(unassignReviewer(payload));

expect(notificationSuccessMock).not.toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 9e6145a

Please sign in to comment.