From 7c7441045d605185e189d5bb274fced4df5432aa Mon Sep 17 00:00:00 2001 From: Markos Gogoulos Date: Mon, 20 Apr 2026 08:30:59 +0300 Subject: [PATCH] wtv --- .../BulkActionCourseCleanupModal.scss | 33 +++ .../BulkActionCourseCleanupModal.tsx | 237 ++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 frontend/src/static/js/components/BulkActionCourseCleanupModal.scss create mode 100644 frontend/src/static/js/components/BulkActionCourseCleanupModal.tsx diff --git a/frontend/src/static/js/components/BulkActionCourseCleanupModal.scss b/frontend/src/static/js/components/BulkActionCourseCleanupModal.scss new file mode 100644 index 00000000..8d1c005f --- /dev/null +++ b/frontend/src/static/js/components/BulkActionCourseCleanupModal.scss @@ -0,0 +1,33 @@ +.course-cleanup-options { + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.course-cleanup-checkbox { + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 13px; + color: #555; + cursor: pointer; + line-height: 1.4; + + .dark_theme & { + color: #ccc; + } + + input[type='checkbox'] { + flex-shrink: 0; + margin-top: 2px; + width: 15px; + height: 15px; + cursor: pointer; + accent-color: var(--default-theme-color, #009933); + } + + span { + flex: 1; + } +} diff --git a/frontend/src/static/js/components/BulkActionCourseCleanupModal.tsx b/frontend/src/static/js/components/BulkActionCourseCleanupModal.tsx new file mode 100644 index 00000000..217983ca --- /dev/null +++ b/frontend/src/static/js/components/BulkActionCourseCleanupModal.tsx @@ -0,0 +1,237 @@ +import React, { useState, useEffect } from 'react'; +import './BulkActionCategoryModal.scss'; +import './BulkActionCourseCleanupModal.scss'; +import { translateString } from '../utils/helpers/'; + +interface Course { + title: string; + uid: string; +} + +interface BulkActionCourseCleanupModalProps { + isOpen: boolean; + selectedMediaIds: string[]; + onCancel: () => void; + onSuccess: (message: string) => void; + onError: (message: string) => void; + csrfToken: string; +} + +export const BulkActionCourseCleanupModal: React.FC = ({ + isOpen, + selectedMediaIds, + onCancel, + onSuccess, + onError, + csrfToken, +}) => { + const hasMediaSelected = selectedMediaIds.length > 0; + const [availableCourses, setAvailableCourses] = useState([]); + const [coursesToCleanup, setCoursesToCleanup] = useState([]); + const [removePermissions, setRemovePermissions] = useState(false); + const [removeTags, setRemoveTags] = useState(false); + const [applyToAll, setApplyToAll] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + + useEffect(() => { + if (isOpen) { + fetchCourses(); + } else { + setAvailableCourses([]); + setCoursesToCleanup([]); + setRemovePermissions(false); + setRemoveTags(false); + setApplyToAll(false); + } + }, [isOpen, selectedMediaIds.join(',')]); + + const fetchCourses = async () => { + setIsLoading(true); + try { + const contributorResponse = await fetch('/api/v1/categories/contributor?lms_courses_only=true'); + if (!contributorResponse.ok) throw new Error('Failed to fetch courses'); + const contributorData = await contributorResponse.json(); + const allContributorCourses: Course[] = contributorData.results || contributorData; + + if (hasMediaSelected) { + const membershipResponse = await fetch('/api/v1/media/user/bulk_actions', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, + body: JSON.stringify({ action: 'category_membership', media_ids: selectedMediaIds }), + }); + if (!membershipResponse.ok) throw new Error('Failed to fetch media categories'); + const membershipData = await membershipResponse.json(); + const mediaCategoryUids = new Set((membershipData.results || []).map((c: Course) => c.uid)); + setAvailableCourses(allContributorCourses.filter((c) => mediaCategoryUids.has(c.uid))); + } else { + setAvailableCourses(allContributorCourses); + } + } catch (error) { + onError(translateString('Failed to load courses')); + } finally { + setIsLoading(false); + } + }; + + const addCourseToCleanup = (course: Course) => { + if (!coursesToCleanup.some((c) => c.uid === course.uid)) { + setCoursesToCleanup((prev) => [...prev, course]); + setAvailableCourses((prev) => prev.filter((c) => c.uid !== course.uid)); + } + }; + + const removeCourseFromCleanup = (course: Course) => { + setCoursesToCleanup((prev) => prev.filter((c) => c.uid !== course.uid)); + setAvailableCourses((prev) => [...prev, course]); + }; + + const handleProceed = async () => { + if (coursesToCleanup.length === 0) return; + setIsProcessing(true); + try { + const response = await fetch('/api/v1/media/user/bulk_actions', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, + body: JSON.stringify({ + action: 'course_cleanup', + media_ids: selectedMediaIds, + category_uids: coursesToCleanup.map((c) => c.uid), + remove_permissions: removePermissions, + remove_tags: removeTags, + apply_to_all: applyToAll, + }), + }); + if (!response.ok) { + const data = await response.json(); + throw new Error(data.detail || 'Failed'); + } + onSuccess(translateString('Course cleanup completed successfully')); + onCancel(); + } catch (error: any) { + onError(error.message || translateString('Course cleanup failed. Please try again.')); + } finally { + setIsProcessing(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+
+

{translateString('Course Cleanup')}

+
+ + {translateString( + 'Cleanup irrelevant course content such as old user permissions and course tags. Add course to the right column and select what to cleanup under the Column. The cleanup can apply to the media you have selected only or to all media in the course, if that option is selected.' + )} + +
+
+ +
+ +
+
+

{translateString('Courses available')}

+ {isLoading ? ( +
{translateString('Loading courses...')}
+ ) : ( +
+ {availableCourses.length === 0 ? ( +
{translateString('No courses available')}
+ ) : ( + availableCourses.map((course) => ( +
addCourseToCleanup(course)} + > + {course.title} + +
+ )) + )} +
+ )} +
+ +
+

{translateString('Courses to cleanup')}

+ {isLoading ? ( +
{translateString('Loading courses...')}
+ ) : ( + <> +
+ {coursesToCleanup.length === 0 ? ( +
{translateString('No courses selected')}
+ ) : ( + coursesToCleanup.map((course) => ( +
+ {course.title} + +
+ )) + )} +
+ +
+ + + {hasMediaSelected && ( + + )} +
+ + )} +
+
+ +
+ + +
+
+
+ ); +};