diff --git a/backend/igny8_core/modules/writer/views.py b/backend/igny8_core/modules/writer/views.py index 9476c3fb..f89b505a 100644 --- a/backend/igny8_core/modules/writer/views.py +++ b/backend/igny8_core/modules/writer/views.py @@ -504,6 +504,28 @@ class ImagesViewSet(SiteSectorModelViewSet): 'type': 'TaskError' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + @action(detail=False, methods=['post'], url_path='bulk_update', url_name='bulk_update') + def bulk_update(self, request): + """Bulk update image status by content_id or image IDs""" + content_id = request.data.get('content_id') + image_ids = request.data.get('ids', []) + status_value = request.data.get('status') + + if not status_value: + return Response({'error': 'No status provided'}, status=status.HTTP_400_BAD_REQUEST) + + queryset = self.get_queryset() + + # Update by content_id if provided, otherwise by image IDs + if content_id: + updated_count = queryset.filter(content_id=content_id).update(status=status_value) + elif image_ids: + updated_count = queryset.filter(id__in=image_ids).update(status=status_value) + else: + return Response({'error': 'Either content_id or ids must be provided'}, status=status.HTTP_400_BAD_REQUEST) + + return Response({'updated_count': updated_count}, status=status.HTTP_200_OK) + @action(detail=False, methods=['get'], url_path='content_images', url_name='content_images') def content_images(self, request): """Get images grouped by content - one row per content with featured and in-article images""" diff --git a/frontend/src/components/common/SingleRecordStatusUpdateModal.tsx b/frontend/src/components/common/SingleRecordStatusUpdateModal.tsx new file mode 100644 index 00000000..7b46d34a --- /dev/null +++ b/frontend/src/components/common/SingleRecordStatusUpdateModal.tsx @@ -0,0 +1,106 @@ +/** + * Single Record Status Update Modal + * Modal for updating status of all images in a single content record + * Similar to BulkStatusUpdateModal but for single records + */ + +import { useState } from 'react'; +import { Modal } from '../ui/modal'; +import Button from '../ui/button/Button'; +import SelectDropdown from '../form/SelectDropdown'; +import Label from '../form/Label'; +import { InfoIcon } from '../../icons'; + +interface SingleRecordStatusUpdateModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: (status: string) => void | Promise; + title: string; + recordName: string; + confirmText?: string; + statusOptions: Array<{ value: string; label: string }>; + isLoading?: boolean; +} + +export default function SingleRecordStatusUpdateModal({ + isOpen, + onClose, + onConfirm, + title, + recordName, + confirmText = 'Update Status', + statusOptions, + isLoading = false, +}: SingleRecordStatusUpdateModalProps) { + const [selectedStatus, setSelectedStatus] = useState(''); + + const handleConfirm = async () => { + if (!selectedStatus) return; + await onConfirm(selectedStatus); + // Reset on success (onClose will be called by parent) + setSelectedStatus(''); + }; + + const handleClose = () => { + setSelectedStatus(''); + onClose(); + }; + + return ( + +
+ {/* Header with icon */} +
+
+ +
+

+ {title} +

+
+ + {/* Message */} +

+ Update status for all images in {recordName} +

+ + {/* Status Selector */} +
+ + setSelectedStatus(value || '')} + className="w-full" + /> +
+ + {/* Actions */} +
+ + +
+
+
+ ); +} + diff --git a/frontend/src/config/pages/table-actions.config.tsx b/frontend/src/config/pages/table-actions.config.tsx index 8e55597e..18044f01 100644 --- a/frontend/src/config/pages/table-actions.config.tsx +++ b/frontend/src/config/pages/table-actions.config.tsx @@ -309,6 +309,17 @@ const tableActionsConfigs: Record = { }, ], }, + '/writer/images': { + rowActions: [ + { + key: 'update_status', + label: 'Update Status', + icon: , + variant: 'primary', + }, + ], + bulkActions: [], + }, // Default config (fallback) default: { rowActions: [], diff --git a/frontend/src/icons/index.ts b/frontend/src/icons/index.ts index 0b37ab8f..ac0f4ed7 100644 --- a/frontend/src/icons/index.ts +++ b/frontend/src/icons/index.ts @@ -54,6 +54,7 @@ import { ReactComponent as ChatIcon } from "./chat.svg?react"; import { ReactComponent as MoreDotIcon } from "./moredot.svg?react"; import { ReactComponent as AlertHexaIcon } from "./alert-hexa.svg?react"; import { ReactComponent as ErrorHexaIcon } from "./info-hexa.svg?react"; +import { ReactComponent as CalendarIcon } from "./calendar.svg?react"; export { ErrorHexaIcon, @@ -112,4 +113,12 @@ export { ChatIcon, AngleLeftIcon, AngleRightIcon, + CalendarIcon, }; + +// Aliases for commonly used icon names +export { AngleLeftIcon as ArrowLeftIcon }; +export { FileIcon as FileTextIcon }; +export { TimeIcon as ClockIcon }; +export { ErrorIcon as XCircleIcon }; +export { BoxIcon as TagIcon }; diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index 78ebe6b1..0998ae6b 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -11,11 +11,13 @@ import { ContentImagesResponse, fetchImageGenerationSettings, generateImages, + bulkUpdateImagesStatus, } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, DownloadIcon, BoltIcon } from '../../icons'; import { createImagesPageConfig } from '../../config/pages/images.config'; import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQueueModal'; +import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordStatusUpdateModal'; import { useResourceDebug } from '../../hooks/useResourceDebug'; export default function Images() { @@ -76,6 +78,12 @@ export default function Images() { const [imageModel, setImageModel] = useState(null); const [imageProvider, setImageProvider] = useState(null); + // Status update modal state + const [isStatusModalOpen, setIsStatusModalOpen] = useState(false); + const [statusUpdateContentId, setStatusUpdateContentId] = useState(null); + const [statusUpdateRecordName, setStatusUpdateRecordName] = useState(''); + const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); + // Load images - wrapped in useCallback const loadImages = useCallback(async () => { setLoading(true); @@ -175,6 +183,35 @@ export default function Images() { toast.info(`Bulk action "${action}" for ${ids.length} items`); }, [toast]); + // Row action handler + const handleRowAction = useCallback(async (action: string, row: ContentImagesGroup) => { + if (action === 'update_status') { + setStatusUpdateContentId(row.content_id); + setStatusUpdateRecordName(row.content_title || `Content #${row.content_id}`); + setIsStatusModalOpen(true); + } + }, []); + + // Handle status update confirmation + const handleStatusUpdate = useCallback(async (status: string) => { + if (!statusUpdateContentId) return; + + setIsUpdatingStatus(true); + try { + const result = await bulkUpdateImagesStatus(statusUpdateContentId, status); + toast.success(`Successfully updated ${result.updated_count} image(s) status to ${status}`); + setIsStatusModalOpen(false); + setStatusUpdateContentId(null); + setStatusUpdateRecordName(''); + // Reload images to reflect the changes + loadImages(); + } catch (error: any) { + toast.error(`Failed to update status: ${error.message}`); + } finally { + setIsUpdatingStatus(false); + } + }, [statusUpdateContentId, toast, loadImages]); + // Build image queue structure const buildImageQueue = useCallback((contentId: number, maxInArticleImages: number) => { const contentImages = images.find(g => g.content_id === contentId); @@ -396,6 +433,7 @@ export default function Images() { setStatusFilter(''); setCurrentPage(1); }} + onRowAction={handleRowAction} /> + {/* Status Update Modal */} + { + setIsStatusModalOpen(false); + setStatusUpdateContentId(null); + setStatusUpdateRecordName(''); + }} + onConfirm={handleStatusUpdate} + title="Update Image Status" + recordName={statusUpdateRecordName} + statusOptions={[ + { value: 'pending', label: 'Pending' }, + { value: 'generated', label: 'Generated' }, + { value: 'failed', label: 'Failed' }, + ]} + isLoading={isUpdatingStatus} + /> + {/* AI Function Logs - Display below table (only when Resource Debug is enabled) */} {resourceDebugEnabled && aiLogs.length > 0 && (
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 2f0e357e..a035d39b 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1032,6 +1032,13 @@ export async function fetchContentImages(): Promise { return fetchAPI('/v1/writer/images/content_images/'); } +export async function bulkUpdateImagesStatus(contentId: number, status: string): Promise<{ updated_count: number }> { + return fetchAPI(`/v1/writer/images/bulk_update/`, { + method: 'POST', + body: JSON.stringify({ content_id: contentId, status }), + }); +} + export async function generateImages(imageIds: number[], contentId?: number): Promise { return fetchAPI('/v1/writer/images/generate_images/', { method: 'POST',