diff --git a/src/features/Courses/CourseDetailTable/__test__/columns.test.jsx b/src/features/Courses/CourseDetailTable/__test__/columns.test.jsx index 2d1cbc0a..107f1363 100644 --- a/src/features/Courses/CourseDetailTable/__test__/columns.test.jsx +++ b/src/features/Courses/CourseDetailTable/__test__/columns.test.jsx @@ -7,6 +7,7 @@ import { MemoryRouter, Route } from 'react-router-dom'; import { renderWithProviders } from 'test-utils'; import { columns } from 'features/Courses/CourseDetailTable/columns'; +import { RequestStatus } from 'features/constants'; describe('columns', () => { test('returns an array of columns with correct properties', () => { @@ -126,6 +127,11 @@ describe('columns', () => { const Component = () => columns[1].Cell(values); const mockStore = { + courses: { + newClass: { + status: RequestStatus.INITIAL, + }, + }, classes: { table: { data: [ @@ -180,6 +186,11 @@ describe('columns', () => { const ComponentNoInstructor = () => columns[1].Cell(values); const mockStore = { + courses: { + newClass: { + status: RequestStatus.INITIAL, + }, + }, classes: { table: { data: [ @@ -223,6 +234,10 @@ describe('columns', () => { }); test('Should render the dropdown menu', () => { + jest.mock('@edx/frontend-platform/logging', () => ({ + logError: jest.fn(), + })); + const values = { row: { values: { instructors: ['Sam Sepiol'] }, @@ -234,11 +249,17 @@ describe('columns', () => { const Component = () => columns[8].Cell(values); const mockStore = { + courses: { + newClass: { + status: RequestStatus.INITIAL, + }, + }, classes: { table: { data: [ { masterCourseName: 'Demo MasterCourse 1', + classId: 'cxx-demo-id', className: 'Demo Class 1', startDate: '09/21/24', endDate: null, @@ -270,5 +291,11 @@ describe('columns', () => { expect(component.getByText('View class content')).toBeInTheDocument(); expect(component.getByText('Manage Instructors')).toBeInTheDocument(); expect(component.getByText('Edit Class')).toBeInTheDocument(); + expect(component.getByText('Delete Class')).toBeInTheDocument(); + + const deleteButton = component.getByText('Delete Class'); + + fireEvent.click(deleteButton); + expect(screen.getByText(/This action will permanently delete this class/i)).toBeInTheDocument(); }); }); diff --git a/src/features/Courses/CourseDetailTable/columns.jsx b/src/features/Courses/CourseDetailTable/columns.jsx index 7abc52ce..d0020f0c 100644 --- a/src/features/Courses/CourseDetailTable/columns.jsx +++ b/src/features/Courses/CourseDetailTable/columns.jsx @@ -1,8 +1,13 @@ /* eslint-disable react/prop-types */ -import React from 'react'; +import React, { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { useParams, Link } from 'react-router-dom'; import { - Dropdown, useToggle, IconButton, Icon, + Dropdown, + useToggle, + IconButton, + Icon, + Toast, } from '@edx/paragon'; import { Badge } from 'react-paragon-topaz'; import { MoreHoriz } from '@edx/paragon/icons'; @@ -11,6 +16,14 @@ import { getConfig } from '@edx/frontend-platform'; import { formatUTCDate, setAssignStaffRole } from 'helpers'; import AddClass from 'features/Courses/AddClass'; +import DeleteModal from 'features/Common/DeleteModal'; + +import { RequestStatus, initialPage } from 'features/constants'; + +import { deleteClass } from 'features/Courses/data/thunks'; +import { fetchClassesData } from 'features/Classes/data/thunks'; + +import { resetClassState } from 'features/Courses/data/slice'; import 'assets/global.scss'; @@ -106,60 +119,108 @@ const columns = [ maxStudents, } = row.original; - const [isOpenModal, openModal, closeModal] = useToggle(false); + const initialDeletionClassState = { + isModalOpen: false, + isRequestComplete: false, + }; + + const dispatch = useDispatch(); + const institution = useSelector((state) => state.main.selectedInstitution); + const deletionState = useSelector((state) => state.courses.newClass.status); + const toastMessage = useSelector((state) => state.courses.notificationMessage); + + const [isOpenEditModal, openModal, closeModal] = useToggle(false); + const [deletionClassState, setDeletionState] = useState(initialDeletionClassState); + + const handleResetDeletion = () => { + setDeletionState(initialDeletionClassState); + dispatch(resetClassState()); + }; + + const handleDeleteClass = async (rowClassId) => { + try { + await dispatch(deleteClass(rowClassId)); + await dispatch(fetchClassesData(institution.id, initialPage, masterCourseName)); + } finally { + setDeletionState({ + isModalOpen: false, + isRequestComplete: true, + }); + } + }; return ( - - - - setAssignStaffRole(`${getConfig().LEARNING_MICROFRONTEND_URL}/course/${classId}/home`, classId)} - className="text-truncate text-decoration-none custom-text-black" - > - - View class content - - - + + + + setAssignStaffRole(`${getConfig().LEARNING_MICROFRONTEND_URL}/course/${classId}/home`, classId)} className="text-truncate text-decoration-none custom-text-black" > - - Manage Instructors - - - - - Edit Class - - - - + + View class content + + + + + Manage Instructors + + + + + Edit Class + + setDeletionState({ ...deletionClassState, isModalOpen: true })} className="text-danger"> + + Delete Class + + + { handleDeleteClass(classId); }} + title="Delete this class" + textModal="This action will permanently delete this class and cannot be undone. Booked seat in this class will not be affected by this action." + /> + + + + {toastMessage} + + ); }, }, diff --git a/src/features/Courses/CoursesDetailPage/__test__/index.test.jsx b/src/features/Courses/CoursesDetailPage/__test__/index.test.jsx index 60745ab6..1257440c 100644 --- a/src/features/Courses/CoursesDetailPage/__test__/index.test.jsx +++ b/src/features/Courses/CoursesDetailPage/__test__/index.test.jsx @@ -3,6 +3,8 @@ import { waitFor } from '@testing-library/react'; import { MemoryRouter, Route } from 'react-router-dom'; import '@testing-library/jest-dom/extend-expect'; +import { RequestStatus } from 'features/constants'; + import { renderWithProviders } from 'test-utils'; import CoursesDetailPage from 'features/Courses/CoursesDetailPage'; @@ -25,6 +27,9 @@ const mockStore = { }, }, courses: { + newClass: { + status: RequestStatus.INITIAL, + }, table: { data: [ { diff --git a/src/features/Courses/data/api.js b/src/features/Courses/data/api.js index 3619d6af..a1d42869 100644 --- a/src/features/Courses/data/api.js +++ b/src/features/Courses/data/api.js @@ -19,7 +19,32 @@ function handleEditClass(data) { ); } +/** + * Deletes a class with the specified classId. + * + * This function constructs the API URL for deleting a class and + * sends a DELETE request using an authenticated HTTP client. + * + * @param {string} classId - The ID of the class to be deleted. + * @returns {Promise} - A promise that resolves with the response of the DELETE request. + */ +function handleDeleteClass(classId) { + const apiV2BaseUrl = getConfig().COURSE_OPERATIONS_API_V2_BASE_URL; + const data = JSON.stringify({ class_id: classId }); + + return getAuthenticatedHttpClient().delete( + `${apiV2BaseUrl}/classes/`, + { + headers: { + 'Content-Type': 'application/json', + }, + data, + }, + ); +} + export { handleNewClass, handleEditClass, + handleDeleteClass, }; diff --git a/src/features/Courses/data/slice.js b/src/features/Courses/data/slice.js index 41cb97a0..1f6bc2ca 100644 --- a/src/features/Courses/data/slice.js +++ b/src/features/Courses/data/slice.js @@ -50,6 +50,10 @@ export const coursesSlice = createSlice({ updateFilters: (state, { payload }) => { state.filters = payload; }, + resetClassState: (state) => { + state.newClass.status = RequestStatus.INITIAL; + state.notificationMessage = null; + }, newClassRequest: (state) => { state.newClass.status = RequestStatus.LOADING; }, @@ -77,6 +81,7 @@ export const { newClassRequest, newClassSuccess, newClassFailed, + resetClassState, updateNotificationMsg, } = coursesSlice.actions; diff --git a/src/features/Courses/data/thunks.js b/src/features/Courses/data/thunks.js index c25f37d8..11e17777 100644 --- a/src/features/Courses/data/thunks.js +++ b/src/features/Courses/data/thunks.js @@ -12,7 +12,7 @@ import { } from 'features/Courses/data/slice'; import { getCoursesByInstitution } from 'features/Common/data/api'; import { initialPage } from 'features/constants'; -import { handleNewClass, handleEditClass } from 'features/Courses/data/api'; +import { handleNewClass, handleEditClass, handleDeleteClass } from 'features/Courses/data/api'; import { assignInstructors } from 'features/Instructors/data'; function fetchCoursesData(id, currentPage, filtersData) { @@ -77,7 +77,23 @@ function editClass(classData) { }; } +function deleteClass(classId) { + return async (dispatch) => { + dispatch(newClassRequest()); + try { + const response = await handleDeleteClass(classId); + dispatch(newClassSuccess(response.data)); + dispatch(updateNotificationMsg('Class Deleted successfully')); + } catch (error) { + dispatch(newClassFailed()); + logError(error); + dispatch(updateNotificationMsg('Class could not be deleted')); + } + }; +} + export { + deleteClass, fetchCoursesData, fetchCoursesOptionsData, addClass,