Add bulk update functionality for image status

- Introduced a new endpoint in the backend to handle bulk updates of image statuses by content ID or image IDs.
- Updated the frontend to include a new row action for updating image status and integrated a modal for status confirmation.
- Enhanced the API service to support bulk status updates and updated the images page to manage status updates effectively.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-12 01:37:41 +00:00
parent 645c6f3f9e
commit 9f20b8e065
6 changed files with 212 additions and 0 deletions

View File

@@ -504,6 +504,28 @@ class ImagesViewSet(SiteSectorModelViewSet):
'type': 'TaskError' 'type': 'TaskError'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) }, 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') @action(detail=False, methods=['get'], url_path='content_images', url_name='content_images')
def content_images(self, request): def content_images(self, request):
"""Get images grouped by content - one row per content with featured and in-article images""" """Get images grouped by content - one row per content with featured and in-article images"""

View File

@@ -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<void>;
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<string>('');
const handleConfirm = async () => {
if (!selectedStatus) return;
await onConfirm(selectedStatus);
// Reset on success (onClose will be called by parent)
setSelectedStatus('');
};
const handleClose = () => {
setSelectedStatus('');
onClose();
};
return (
<Modal
isOpen={isOpen}
onClose={handleClose}
className="max-w-md"
>
<div className="p-6">
{/* Header with icon */}
<div className="flex items-center gap-3 mb-4">
<div className="flex items-center justify-center w-10 h-10 bg-blue-50 rounded-xl dark:bg-blue-500/10">
<InfoIcon className="w-5 h-5 text-blue-500" />
</div>
<h2 className="text-xl font-bold text-gray-800 dark:text-white">
{title}
</h2>
</div>
{/* Message */}
<p className="text-gray-600 dark:text-gray-400 mb-4">
Update status for all images in <span className="font-semibold text-gray-900 dark:text-white">{recordName}</span>
</p>
{/* Status Selector */}
<div className="mb-6">
<Label className="mb-2">
New Status
</Label>
<SelectDropdown
options={statusOptions}
placeholder="Select status"
value={selectedStatus}
onChange={(value) => setSelectedStatus(value || '')}
className="w-full"
/>
</div>
{/* Actions */}
<div className="flex justify-end gap-4">
<Button
variant="outline"
onClick={handleClose}
disabled={isLoading}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleConfirm}
disabled={isLoading || !selectedStatus}
>
{isLoading ? 'Updating...' : confirmText}
</Button>
</div>
</div>
</Modal>
);
}

View File

@@ -309,6 +309,17 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
}, },
], ],
}, },
'/writer/images': {
rowActions: [
{
key: 'update_status',
label: 'Update Status',
icon: <CheckCircleIcon className="w-5 h-5" />,
variant: 'primary',
},
],
bulkActions: [],
},
// Default config (fallback) // Default config (fallback)
default: { default: {
rowActions: [], rowActions: [],

View File

@@ -54,6 +54,7 @@ import { ReactComponent as ChatIcon } from "./chat.svg?react";
import { ReactComponent as MoreDotIcon } from "./moredot.svg?react"; import { ReactComponent as MoreDotIcon } from "./moredot.svg?react";
import { ReactComponent as AlertHexaIcon } from "./alert-hexa.svg?react"; import { ReactComponent as AlertHexaIcon } from "./alert-hexa.svg?react";
import { ReactComponent as ErrorHexaIcon } from "./info-hexa.svg?react"; import { ReactComponent as ErrorHexaIcon } from "./info-hexa.svg?react";
import { ReactComponent as CalendarIcon } from "./calendar.svg?react";
export { export {
ErrorHexaIcon, ErrorHexaIcon,
@@ -112,4 +113,12 @@ export {
ChatIcon, ChatIcon,
AngleLeftIcon, AngleLeftIcon,
AngleRightIcon, 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 };

View File

@@ -11,11 +11,13 @@ import {
ContentImagesResponse, ContentImagesResponse,
fetchImageGenerationSettings, fetchImageGenerationSettings,
generateImages, generateImages,
bulkUpdateImagesStatus,
} from '../../services/api'; } from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer'; import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon, DownloadIcon, BoltIcon } from '../../icons'; import { FileIcon, DownloadIcon, BoltIcon } from '../../icons';
import { createImagesPageConfig } from '../../config/pages/images.config'; import { createImagesPageConfig } from '../../config/pages/images.config';
import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQueueModal'; import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQueueModal';
import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordStatusUpdateModal';
import { useResourceDebug } from '../../hooks/useResourceDebug'; import { useResourceDebug } from '../../hooks/useResourceDebug';
export default function Images() { export default function Images() {
@@ -76,6 +78,12 @@ export default function Images() {
const [imageModel, setImageModel] = useState<string | null>(null); const [imageModel, setImageModel] = useState<string | null>(null);
const [imageProvider, setImageProvider] = useState<string | null>(null); const [imageProvider, setImageProvider] = useState<string | null>(null);
// Status update modal state
const [isStatusModalOpen, setIsStatusModalOpen] = useState(false);
const [statusUpdateContentId, setStatusUpdateContentId] = useState<number | null>(null);
const [statusUpdateRecordName, setStatusUpdateRecordName] = useState<string>('');
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
// Load images - wrapped in useCallback // Load images - wrapped in useCallback
const loadImages = useCallback(async () => { const loadImages = useCallback(async () => {
setLoading(true); setLoading(true);
@@ -175,6 +183,35 @@ export default function Images() {
toast.info(`Bulk action "${action}" for ${ids.length} items`); toast.info(`Bulk action "${action}" for ${ids.length} items`);
}, [toast]); }, [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 // Build image queue structure
const buildImageQueue = useCallback((contentId: number, maxInArticleImages: number) => { const buildImageQueue = useCallback((contentId: number, maxInArticleImages: number) => {
const contentImages = images.find(g => g.content_id === contentId); const contentImages = images.find(g => g.content_id === contentId);
@@ -396,6 +433,7 @@ export default function Images() {
setStatusFilter(''); setStatusFilter('');
setCurrentPage(1); setCurrentPage(1);
}} }}
onRowAction={handleRowAction}
/> />
<ImageQueueModal <ImageQueueModal
isOpen={isQueueModalOpen} isOpen={isQueueModalOpen}
@@ -418,6 +456,25 @@ export default function Images() {
onLog={addAiLog} onLog={addAiLog}
/> />
{/* Status Update Modal */}
<SingleRecordStatusUpdateModal
isOpen={isStatusModalOpen}
onClose={() => {
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) */} {/* AI Function Logs - Display below table (only when Resource Debug is enabled) */}
{resourceDebugEnabled && aiLogs.length > 0 && ( {resourceDebugEnabled && aiLogs.length > 0 && (
<div className="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4"> <div className="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">

View File

@@ -1032,6 +1032,13 @@ export async function fetchContentImages(): Promise<ContentImagesResponse> {
return fetchAPI('/v1/writer/images/content_images/'); 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<any> { export async function generateImages(imageIds: number[], contentId?: number): Promise<any> {
return fetchAPI('/v1/writer/images/generate_images/', { return fetchAPI('/v1/writer/images/generate_images/', {
method: 'POST', method: 'POST',