This commit is contained in:
Markos Gogoulos
2026-02-24 10:54:03 +02:00
parent b39789c2c4
commit dc328cd33c
17 changed files with 52 additions and 33 deletions

View File

@@ -1 +1 @@
VERSION = "7.9e"
VERSION = "7.9f"

View File

@@ -176,6 +176,9 @@ class MediaPublishForm(forms.ModelForm):
if is_embed_mode:
current_queryset = self.fields['category'].queryset
self.fields['category'].queryset = current_queryset.filter(is_lms_course=True)
self.fields['category'].label = 'Course'
self.fields['category'].help_text = 'Media can be part of one or more courses'
self.fields['category'].widget.is_lms_mode = True
self.helper = FormHelper()
self.helper.form_tag = True

View File

@@ -14,6 +14,8 @@ class CategoryModalWidget(forms.SelectMultiple):
js = ('js/category_modal.js',)
def render(self, name, value, attrs=None, renderer=None):
is_lms_mode = getattr(self, 'is_lms_mode', False)
# Get all categories as JSON
categories = []
for opt_value, opt_label in self.choices:
@@ -30,20 +32,24 @@ class CategoryModalWidget(forms.SelectMultiple):
all_categories_json = json.dumps(categories)
selected_ids_json = json.dumps([str(v) for v in (value or [])])
lms_mode_json = json.dumps(is_lms_mode)
search_placeholder = "Search courses..." if is_lms_mode else "Search categories..."
selected_header = "Selected Courses" if is_lms_mode else "Selected Categories"
html = f'''<div class="category-widget" data-name="{name}">
<div class="category-content">
<div class="category-panel">
<input type="text" class="category-search" placeholder="Search categories...">
<input type="text" class="category-search" placeholder="{search_placeholder}">
<div class="category-list scrollable" data-panel="left"></div>
</div>
<div class="category-panel">
<h3>Selected Categories</h3>
<h3>{selected_header}</h3>
<div class="category-list scrollable" data-panel="right"></div>
</div>
</div>
<div class="hidden-inputs"></div>
<script type="application/json" class="category-data">{{"all":{all_categories_json},"selected":{selected_ids_json}}}</script>
<script type="application/json" class="category-data">{{"all":{all_categories_json},"selected":{selected_ids_json},"lms_mode":{lms_mode_json}}}</script>
</div>'''
return mark_safe(html)

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import './BulkActionCategoryModal.scss';
import { translateString } from '../utils/helpers/';
import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
interface Category {
title: string;
@@ -24,6 +25,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
onError,
csrfToken,
}) => {
const isLmsMode = inEmbeddedApp();
const [existingCategories, setExistingCategories] = useState<Category[]>([]);
const [allCategories, setAllCategories] = useState<Category[]>([]);
const [categoriesToAdd, setCategoriesToAdd] = useState<Category[]>([]);
@@ -66,20 +68,27 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
const existingData = await existingResponse.json();
const existing = existingData.results || [];
// Fetch all categories
const allResponse = await fetch('/api/v1/categories');
// Fetch all categories (or LMS courses only in embed mode)
const categoriesUrl = isLmsMode
? '/api/v1/categories/contributor?lms_courses_only=true'
: '/api/v1/categories';
const allResponse = await fetch(categoriesUrl);
if (!allResponse.ok) {
throw new Error(translateString('Failed to fetch all categories'));
throw new Error(isLmsMode ? translateString('Failed to fetch courses') : translateString('Failed to fetch all categories'));
}
const allData = await allResponse.json();
const all = allData.results || allData;
setExistingCategories(existing);
// In LMS mode, filter existing to only show LMS course categories
const allUids = new Set(all.map((c: Category) => c.uid));
const filteredExisting = isLmsMode ? existing.filter((c: Category) => allUids.has(c.uid)) : existing;
setExistingCategories(filteredExisting);
setAllCategories(all);
} catch (error) {
console.error('Error fetching categories:', error);
onError(translateString('Failed to load categories'));
onError(isLmsMode ? translateString('Failed to load courses') : translateString('Failed to load categories'));
} finally {
setIsLoading(false);
}
@@ -126,7 +135,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
});
if (!addResponse.ok) {
throw new Error(translateString('Failed to add categories'));
throw new Error(isLmsMode ? translateString('Failed to add courses') : translateString('Failed to add categories'));
}
}
@@ -147,15 +156,15 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
});
if (!removeResponse.ok) {
throw new Error(translateString('Failed to remove categories'));
throw new Error(isLmsMode ? translateString('Failed to remove courses') : translateString('Failed to remove categories'));
}
}
onSuccess(translateString('Successfully updated categories'));
onSuccess(isLmsMode ? translateString('Successfully updated courses') : translateString('Successfully updated categories'));
onCancel();
} catch (error) {
console.error('Error processing categories:', error);
onError(translateString('Failed to update categories. Please try again.'));
onError(isLmsMode ? translateString('Failed to update courses. Please try again.') : translateString('Failed to update categories. Please try again.'));
} finally {
setIsProcessing(false);
}
@@ -184,7 +193,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
<div className="category-modal-overlay">
<div className="category-modal">
<div className="category-modal-header">
<h2>{translateString('Add / Remove from Categories')}</h2>
<h2>{isLmsMode ? translateString('Share with Course') : translateString('Add / Remove from Categories')}</h2>
<button className="category-modal-close" onClick={onCancel}>
×
</button>
@@ -192,14 +201,14 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
<div className="category-modal-content">
<div className="category-panel">
<h3>{translateString('Categories')}</h3>
<h3>{isLmsMode ? translateString('Courses') : translateString('Categories')}</h3>
{isLoading ? (
<div className="loading-message">{translateString('Loading categories...')}</div>
<div className="loading-message">{isLmsMode ? translateString('Loading courses...') : translateString('Loading categories...')}</div>
) : (
<div className="category-list scrollable">
{leftPanelCategories.length === 0 ? (
<div className="empty-message">{translateString('All categories already added')}</div>
<div className="empty-message">{isLmsMode ? translateString('All courses already added') : translateString('All categories already added')}</div>
) : (
leftPanelCategories.map((category) => (
<div
@@ -227,11 +236,11 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
</h3>
{isLoading ? (
<div className="loading-message">{translateString('Loading categories...')}</div>
<div className="loading-message">{isLmsMode ? translateString('Loading courses...') : translateString('Loading categories...')}</div>
) : (
<div className="category-list scrollable">
{rightPanelCategories.length === 0 ? (
<div className="empty-message">{translateString('No categories')}</div>
<div className="empty-message">{isLmsMode ? translateString('No courses') : translateString('No categories')}</div>
) : (
rightPanelCategories.map((category) => {
const isExisting = existingCategories.some((c) => c.uid === category.uid);
@@ -251,7 +260,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
removeCategoryFromAddList(category);
}
}}
title={isMarkedForRemoval ? translateString('Undo removal') : isExisting ? translateString('Remove category') : translateString('Remove from list')}
title={isMarkedForRemoval ? translateString('Undo removal') : isExisting ? (isLmsMode ? translateString('Remove course') : translateString('Remove category')) : translateString('Remove from list')}
>
{isMarkedForRemoval ? '↺' : '×'}
</button>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import './BulkActionsDropdown.scss';
import { translateString } from '../utils/helpers/';
import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
interface BulkActionsDropdownProps {
selectedCount: number;
@@ -12,7 +13,7 @@ const BULK_ACTIONS = [
{ value: 'add-remove-coeditors', label: translateString('Add / Remove Co-Editors'), enabled: true },
{ value: 'add-remove-coowners', label: translateString('Add / Remove Co-Owners'), enabled: true },
{ value: 'add-remove-playlist', label: translateString('Add to / Remove from Playlist'), enabled: true },
{ value: 'add-remove-category', label: translateString('Add to / Remove from Category'), enabled: true },
{ value: 'add-remove-category', label: inEmbeddedApp() ? translateString('Share with Course') : translateString('Add to / Remove from Category'), enabled: true },
{ value: 'add-remove-tags', label: translateString('Add / Remove Tags'), enabled: true },
{ value: 'enable-comments', label: translateString('Enable Comments'), enabled: true },
{ value: 'disable-comments', label: translateString('Disable Comments'), enabled: true },

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('.category-widget').forEach(w=>{const d=JSON.parse(w.querySelector('.category-data').textContent),all=d.all,sel=new Set(d.selected),name=w.dataset.name,srch=w.querySelector('.category-search'),left=w.querySelector('[data-panel="left"]'),right=w.querySelector('[data-panel="right"]'),inputs=w.querySelector('.hidden-inputs');const upd=()=>{left.innerHTML=all.filter(c=>!sel.has(c.id)&&(!srch.value||c.title.toLowerCase().includes(srch.value.toLowerCase()))).map(c=>`<div class="category-item" data-id="${c.id}"><span>${c.title}</span><button class="add-btn" type="button">+</button></div>`).join('')||'<div class="empty-message">No categories available</div>';right.innerHTML=[...sel].map(id=>all.find(c=>c.id==id)).filter(Boolean).map(c=>`<div class="category-item" data-id="${c.id}"><span>${c.title}</span><button class="remove-btn" type="button">&times;</button></div>`).join('')||'<div class="empty-message">No categories selected</div>';inputs.innerHTML=[...sel].map(id=>`<input type="hidden" name="${name}" value="${id}">`).join('')};srch.oninput=upd;left.onclick=e=>{const item=e.target.closest('.category-item');if(item){sel.add(item.dataset.id);upd()}};right.onclick=e=>{const item=e.target.closest('.category-item');if(item){sel.delete(item.dataset.id);upd()}};upd()})});
document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('.category-widget').forEach(w=>{const d=JSON.parse(w.querySelector('.category-data').textContent),all=d.all,lms=d.lms_mode,sel=new Set(d.selected),name=w.dataset.name,srch=w.querySelector('.category-search'),left=w.querySelector('[data-panel="left"]'),right=w.querySelector('[data-panel="right"]'),inputs=w.querySelector('.hidden-inputs');const upd=()=>{left.innerHTML=all.filter(c=>!sel.has(c.id)&&(!srch.value||c.title.toLowerCase().includes(srch.value.toLowerCase()))).map(c=>`<div class="category-item" data-id="${c.id}"><span>${c.title}</span><button class="add-btn" type="button">+</button></div>`).join('')||(lms?'<div class="empty-message">No courses available</div>':'<div class="empty-message">No categories available</div>');right.innerHTML=[...sel].map(id=>all.find(c=>c.id==id)).filter(Boolean).map(c=>`<div class="category-item" data-id="${c.id}"><span>${c.title}</span><button class="remove-btn" type="button">&times;</button></div>`).join('')||(lms?'<div class="empty-message">No courses selected</div>':'<div class="empty-message">No categories selected</div>');inputs.innerHTML=[...sel].map(id=>`<input type="hidden" name="${name}" value="${id}">`).join('')};srch.oninput=upd;left.onclick=e=>{const item=e.target.closest('.category-item');if(item){sel.add(item.dataset.id);upd()}};right.onclick=e=>{const item=e.target.closest('.category-item');if(item){sel.delete(item.dataset.id);upd()}};upd()})});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long