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

[MDS-6204] - condition review assignment #3355

Merged
merged 7 commits into from
Dec 20, 2024
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
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
Loading