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:
@@ -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"""
|
||||
|
||||
106
frontend/src/components/common/SingleRecordStatusUpdateModal.tsx
Normal file
106
frontend/src/components/common/SingleRecordStatusUpdateModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
rowActions: [],
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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<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
|
||||
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}
|
||||
/>
|
||||
<ImageQueueModal
|
||||
isOpen={isQueueModalOpen}
|
||||
@@ -418,6 +456,25 @@ export default function Images() {
|
||||
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) */}
|
||||
{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">
|
||||
|
||||
@@ -1032,6 +1032,13 @@ export async function fetchContentImages(): Promise<ContentImagesResponse> {
|
||||
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> {
|
||||
return fetchAPI('/v1/writer/images/generate_images/', {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user