feat: Enable drag-and-drop reordering in ModerationGroupImagesPage
- Added PUT /api/admin/groups/:groupId/reorder endpoint - Implemented handleReorder in ModerationGroupImagesPage - Uses adminRequest API with proper error handling - Same mobile touch support as ManagementPortalPage
This commit is contained in:
parent
215acaa67f
commit
91d6d06687
|
|
@ -2573,6 +2573,96 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/admin/groups/{groupId}/reorder": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"Admin - Groups Moderation"
|
||||
],
|
||||
"summary": "Reorder images in a group",
|
||||
"description": "Updates the display order of images within a group",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"description": "Group ID",
|
||||
"example": "abc123def456"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Images reordered successfully",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Image order updated successfully"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"updatedImages": {
|
||||
"type": "number",
|
||||
"example": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"xml": {
|
||||
"name": "main"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid imageIds parameter"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"description": "Group not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"imageIds"
|
||||
],
|
||||
"properties": {
|
||||
"imageIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"example": [
|
||||
5,
|
||||
3,
|
||||
1,
|
||||
2,
|
||||
4
|
||||
],
|
||||
"description": "Array of image IDs in new order"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/admin/{groupId}/reorder": {
|
||||
"put": {
|
||||
"tags": [
|
||||
|
|
|
|||
|
|
@ -978,6 +978,120 @@ router.patch('/groups/:groupId/images/:imageId', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
router.put('/groups/:groupId/reorder', async (req, res) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin - Groups Moderation']
|
||||
#swagger.summary = 'Reorder images in a group'
|
||||
#swagger.description = 'Updates the display order of images within a group'
|
||||
#swagger.parameters['groupId'] = {
|
||||
in: 'path',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Group ID',
|
||||
example: 'abc123def456'
|
||||
}
|
||||
#swagger.requestBody = {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['imageIds'],
|
||||
properties: {
|
||||
imageIds: {
|
||||
type: 'array',
|
||||
items: { type: 'integer' },
|
||||
example: [5, 3, 1, 2, 4],
|
||||
description: 'Array of image IDs in new order'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
description: 'Images reordered successfully',
|
||||
schema: {
|
||||
success: true,
|
||||
message: 'Image order updated successfully',
|
||||
data: {
|
||||
updatedImages: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[400] = {
|
||||
description: 'Invalid imageIds parameter'
|
||||
}
|
||||
#swagger.responses[404] = {
|
||||
description: 'Group not found'
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { groupId } = req.params;
|
||||
const { imageIds } = req.body;
|
||||
|
||||
// Validate imageIds
|
||||
if (!imageIds || !Array.isArray(imageIds) || imageIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'imageIds array is required and cannot be empty'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate that all imageIds are numbers
|
||||
const invalidIds = imageIds.filter(id => !Number.isInteger(id) || id <= 0);
|
||||
if (invalidIds.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid image IDs: ${invalidIds.join(', ')}. Image IDs must be positive integers`
|
||||
});
|
||||
}
|
||||
|
||||
// Verify group exists
|
||||
const groupData = await GroupRepository.getGroupById(groupId);
|
||||
if (!groupData) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Group not found',
|
||||
message: `Gruppe mit ID ${groupId} wurde nicht gefunden`
|
||||
});
|
||||
}
|
||||
|
||||
// Execute reorder using GroupRepository
|
||||
const result = await GroupRepository.updateImageOrder(groupId, imageIds);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Image order updated successfully',
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[ADMIN] Error reordering images for group ${req.params.groupId}:`, error.message);
|
||||
|
||||
// Handle specific errors
|
||||
if (error.message.includes('not found')) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Group or images not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('mismatch')) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to reorder images',
|
||||
message: 'Fehler beim Sortieren der Bilder'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/groups/:groupId', async (req, res) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin - Groups Moderation']
|
||||
|
|
|
|||
|
|
@ -239,13 +239,18 @@
|
|||
background: rgba(0,0,0,0.7);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
padding: 8px 12px;
|
||||
font-size: 16px;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
opacity: 1; /* Always visible on mobile */
|
||||
transition: opacity 0.2s, background 0.2s;
|
||||
touch-action: none; /* Prevent scrolling when touching handle */
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
background: rgba(0,0,0,0.9);
|
||||
}
|
||||
|
||||
.image-gallery-card.reorderable:hover .drag-handle {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors
|
||||
} from '@dnd-kit/core';
|
||||
|
|
@ -34,11 +35,17 @@ const ImageGallery = ({
|
|||
imageDescriptions = {},
|
||||
onDescriptionChange = null
|
||||
}) => {
|
||||
// Sensors for drag and drop (touch-friendly)
|
||||
// Sensors for drag and drop (desktop + mobile optimized)
|
||||
const sensors = useSensors(
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 0, // No delay - allow immediate dragging
|
||||
tolerance: 0, // No tolerance - precise control
|
||||
},
|
||||
}),
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8, // Require 8px movement before drag starts
|
||||
distance: 5, // Require 5px movement before drag starts (desktop)
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
// Services
|
||||
import { adminGet } from '../../services/adminApi';
|
||||
import { adminGet, adminRequest } from '../../services/adminApi';
|
||||
import { handleAdminError } from '../../services/adminErrorHandler';
|
||||
import AdminSessionGate from '../AdminAuth/AdminSessionGate.jsx';
|
||||
import { useAdminSession } from '../../contexts/AdminSessionContext.jsx';
|
||||
|
|
@ -14,6 +14,9 @@ import ImageDescriptionManager from '../ComponentUtils/ImageDescriptionManager';
|
|||
import GroupMetadataEditor from '../ComponentUtils/GroupMetadataEditor';
|
||||
import Loading from '../ComponentUtils/LoadingAnimation/Loading';
|
||||
|
||||
// UI
|
||||
import Swal from 'sweetalert2';
|
||||
|
||||
/**
|
||||
* ModerationGroupImagesPage - Admin page for moderating group images
|
||||
*
|
||||
|
|
@ -71,6 +74,35 @@ const ModerationGroupImagesPage = () => {
|
|||
loadGroup();
|
||||
}, [isAuthenticated, loadGroup]);
|
||||
|
||||
const handleReorder = async (newOrder) => {
|
||||
if (!group || !groupId) {
|
||||
console.error('No groupId available for reordering');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const imageIds = newOrder.map(img => img.id);
|
||||
|
||||
// Use admin API
|
||||
await adminRequest(`/api/admin/groups/${groupId}/reorder`, 'PUT', {
|
||||
imageIds: imageIds
|
||||
});
|
||||
|
||||
await Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Gespeichert',
|
||||
text: 'Die neue Reihenfolge wurde gespeichert.',
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
});
|
||||
|
||||
await loadGroup();
|
||||
} catch (error) {
|
||||
console.error('Error reordering images:', error);
|
||||
await handleAdminError(error, 'Reihenfolge speichern');
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) return <Loading />;
|
||||
if (error) return <div className="moderation-error">{error}</div>;
|
||||
|
|
@ -87,6 +119,8 @@ const ModerationGroupImagesPage = () => {
|
|||
groupId={groupId}
|
||||
onRefresh={loadGroup}
|
||||
mode="moderate"
|
||||
enableReordering={true}
|
||||
onReorder={handleReorder}
|
||||
/>
|
||||
|
||||
{/* Group Metadata Editor */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user