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,