This commit is contained in:
Markos Gogoulos
2026-04-18 15:07:51 +03:00
parent 89c4fe1b2b
commit d7fc552230
22 changed files with 168 additions and 23 deletions
@@ -6,12 +6,14 @@ import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
interface BulkActionsDropdownProps {
selectedCount: number;
onActionSelect: (action: string) => void;
hasContributorCourses?: boolean;
}
interface BulkAction {
value: string;
label: string;
enabled: boolean;
allowsNoSelection?: boolean;
}
interface BulkActionGroup {
@@ -19,7 +21,7 @@ interface BulkActionGroup {
actions: BulkAction[];
}
export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ selectedCount, onActionSelect }) => {
export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ selectedCount, onActionSelect, hasContributorCourses = false }) => {
const isLmsMode = inEmbeddedApp();
const BULK_ACTION_GROUPS: BulkActionGroup[] = [
@@ -56,18 +58,24 @@ export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ select
{ value: 'change-owner', label: translateString('Change Owner'), enabled: true },
{ value: 'copy-media', label: translateString('Copy Media'), enabled: true },
{ value: 'delete-media', label: translateString('Delete Media'), enabled: true },
...(isLmsMode && hasContributorCourses
? [{ value: 'course-cleanup', label: translateString('Course Cleanup'), enabled: true, allowsNoSelection: true }]
: []),
],
},
];
const noSelection = selectedCount === 0;
const allActions = BULK_ACTION_GROUPS.flatMap((g) => g.actions);
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const value = event.target.value;
if (!value) return;
if (noSelection) {
const actionDef = allActions.find((a) => a.value === value);
if (noSelection && !actionDef?.allowsNoSelection) {
event.target.value = '';
return;
}
@@ -95,7 +103,7 @@ export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ select
{BULK_ACTION_GROUPS.map((group) => (
<optgroup key={group.label} label={group.label}>
{group.actions.map((action) => (
<option key={action.value} value={action.value} disabled={noSelection || !action.enabled}>
<option key={action.value} value={action.value} disabled={(!action.allowsNoSelection && noSelection) || !action.enabled}>
{action.label}
</option>
))}
@@ -7,6 +7,7 @@ import { BulkActionChangeOwnerModal } from './BulkActionChangeOwnerModal';
import { BulkActionPublishStateModal } from './BulkActionPublishStateModal';
import { BulkActionCategoryModal } from './BulkActionCategoryModal';
import { BulkActionTagModal } from './BulkActionTagModal';
import { BulkActionCourseCleanupModal } from './BulkActionCourseCleanupModal';
/**
* Renders all bulk action modals
@@ -58,6 +59,12 @@ export function BulkActionsModals({
onTagModalSuccess,
onTagModalError,
// Course cleanup modal props
showCourseCleanupModal,
onCourseCleanupModalCancel,
onCourseCleanupModalSuccess,
onCourseCleanupModalError,
// Common props
csrfToken,
@@ -131,6 +138,15 @@ export function BulkActionsModals({
csrfToken={csrfToken}
/>
<BulkActionCourseCleanupModal
isOpen={showCourseCleanupModal}
selectedMediaIds={selectedMediaIds}
onCancel={onCourseCleanupModalCancel}
onSuccess={onCourseCleanupModalSuccess}
onError={onCourseCleanupModalError}
csrfToken={csrfToken}
/>
{showNotification && (
<div
style={{
@@ -193,6 +209,11 @@ BulkActionsModals.propTypes = {
onTagModalSuccess: PropTypes.func.isRequired,
onTagModalError: PropTypes.func.isRequired,
showCourseCleanupModal: PropTypes.bool.isRequired,
onCourseCleanupModalCancel: PropTypes.func.isRequired,
onCourseCleanupModalSuccess: PropTypes.func.isRequired,
onCourseCleanupModalError: PropTypes.func.isRequired,
csrfToken: PropTypes.string.isRequired,
showNotification: PropTypes.bool.isRequired,
@@ -22,6 +22,7 @@ interface MediaListWrapperProps {
onSelectAll?: () => void;
onDeselectAll?: () => void;
showAddMediaButton?: boolean;
hasContributorCourses?: boolean;
}
export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
@@ -38,6 +39,7 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
onSelectAll = () => {},
onDeselectAll = () => {},
showAddMediaButton = false,
hasContributorCourses = false,
}) => {
const [popupContentRef, PopupContent, PopupTrigger] = usePopup() as [any, any, any];
@@ -63,7 +65,7 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
return (
<div className="bulk-actions-container">
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<BulkActionsDropdown selectedCount={selectedCount} onActionSelect={onBulkAction} />
<BulkActionsDropdown selectedCount={selectedCount} onActionSelect={onBulkAction} hasContributorCourses={hasContributorCourses} />
<SelectAllCheckbox
totalCount={totalCount}
selectedCount={selectedCount}
@@ -463,6 +463,7 @@ class ProfileMediaPage extends Page {
onSelectAll={this.props.bulkActions.handleSelectAll}
onDeselectAll={this.props.bulkActions.handleDeselectAll}
showAddMediaButton={!isSelectMediaMode && isMediaAuthor}
hasContributorCourses={this.props.bulkActions.hasContributorCourses}
>
<ProfileMediaFilters
hidden={this.state.hiddenFilters}
@@ -530,6 +531,10 @@ class ProfileMediaPage extends Page {
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
onTagModalError={this.props.bulkActions.handleTagModalError}
showCourseCleanupModal={this.props.bulkActions.showCourseCleanupModal}
onCourseCleanupModalCancel={this.props.bulkActions.handleCourseCleanupModalCancel}
onCourseCleanupModalSuccess={this.props.bulkActions.handleCourseCleanupModalSuccess}
onCourseCleanupModalError={this.props.bulkActions.handleCourseCleanupModalError}
/>
) : null,
];
@@ -486,6 +486,7 @@ class ProfileSharedByMePage extends Page {
onBulkAction={this.props.bulkActions.handleBulkAction}
onSelectAll={this.props.bulkActions.handleSelectAll}
onDeselectAll={this.props.bulkActions.handleDeselectAll}
hasContributorCourses={this.props.bulkActions.hasContributorCourses}
>
<ProfileMediaFilters
hidden={this.state.hiddenFilters}
@@ -554,6 +555,10 @@ class ProfileSharedByMePage extends Page {
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
onTagModalError={this.props.bulkActions.handleTagModalError}
showCourseCleanupModal={this.props.bulkActions.showCourseCleanupModal}
onCourseCleanupModalCancel={this.props.bulkActions.handleCourseCleanupModalCancel}
onCourseCleanupModalSuccess={this.props.bulkActions.handleCourseCleanupModalSuccess}
onCourseCleanupModalError={this.props.bulkActions.handleCourseCleanupModalError}
/>
) : null,
];
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { translateString } from '../helpers';
import { inEmbeddedApp } from '../helpers/embeddedApp';
/**
* Custom hook for managing bulk actions on media items
@@ -22,6 +23,20 @@ export function useBulkActions() {
const [showPublishStateModal, setShowPublishStateModal] = useState(false);
const [showCategoryModal, setShowCategoryModal] = useState(false);
const [showTagModal, setShowTagModal] = useState(false);
const [showCourseCleanupModal, setShowCourseCleanupModal] = useState(false);
const [hasContributorCourses, setHasContributorCourses] = useState(false);
useEffect(() => {
if (!inEmbeddedApp()) return;
fetch('/api/v1/categories/contributor?lms_courses_only=true')
.then((r) => r.ok ? r.json() : null)
.then((data) => {
if (!data) return;
const courses = data.results || data;
setHasContributorCourses(Array.isArray(courses) && courses.length > 0);
})
.catch(() => {});
}, []);
// Get CSRF token from cookies
const getCsrfToken = () => {
@@ -95,6 +110,11 @@ export function useBulkActions() {
const handleBulkAction = (action) => {
const selectedCount = selectedMedia.size;
if (action === 'course-cleanup') {
setShowCourseCleanupModal(true);
return;
}
if (selectedCount === 0) {
return;
}
@@ -500,6 +520,22 @@ export function useBulkActions() {
setShowTagModal(false);
};
// Course cleanup modal handlers
const handleCourseCleanupModalCancel = () => {
setShowCourseCleanupModal(false);
};
const handleCourseCleanupModalSuccess = (message) => {
showNotificationMessage(message);
clearSelectionAndRefresh();
setShowCourseCleanupModal(false);
};
const handleCourseCleanupModalError = (message) => {
showNotificationMessage(message, 'error');
setShowCourseCleanupModal(false);
};
return {
// State
selectedMedia,
@@ -517,6 +553,8 @@ export function useBulkActions() {
showPublishStateModal,
showCategoryModal,
showTagModal,
showCourseCleanupModal,
hasContributorCourses,
// Handlers
handleMediaSelection,
@@ -544,6 +582,9 @@ export function useBulkActions() {
handleTagModalCancel,
handleTagModalSuccess,
handleTagModalError,
handleCourseCleanupModalCancel,
handleCourseCleanupModalSuccess,
handleCourseCleanupModalError,
// Utility
getCsrfToken,