- Fix admin video upload missing media_type/mime_type and video processing (#203) - Fix Gallery-Premium Select All using atomic callbacks instead of stale closure loop (#220) - Add photo dimension repair endpoint and admin UI (#180) - Add E2E tests for all three fixes
This commit is contained in:
@@ -215,6 +215,8 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
|
||||
useCanvasRendering,
|
||||
isSelectionMode,
|
||||
onPhotoSelect: handlePhotoSelect,
|
||||
onSelectAll: selectAll,
|
||||
onDeselectAll: deselectAll,
|
||||
eventName,
|
||||
eventLogo,
|
||||
eventDate,
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface BaseGalleryLayoutProps {
|
||||
selectedPhotos?: Set<number>;
|
||||
isSelectionMode?: boolean;
|
||||
onPhotoSelect?: (photoId: number) => void;
|
||||
onSelectAll?: () => void;
|
||||
onDeselectAll?: () => void;
|
||||
eventName?: string;
|
||||
eventLogo?: string | null;
|
||||
eventDate?: string | null;
|
||||
|
||||
@@ -165,6 +165,8 @@ export const GalleryPremiumLayout: React.FC<GalleryPremiumLayoutProps> = ({
|
||||
selectedPhotos = new Set(),
|
||||
isSelectionMode = false,
|
||||
onPhotoSelect,
|
||||
onSelectAll,
|
||||
onDeselectAll,
|
||||
eventName,
|
||||
eventDate,
|
||||
allowDownloads = true,
|
||||
@@ -287,17 +289,11 @@ export const GalleryPremiumLayout: React.FC<GalleryPremiumLayoutProps> = ({
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
if (selectedPhotos.size === filteredPhotos.length) {
|
||||
// Deselect all
|
||||
filteredPhotos.forEach(p => onPhotoSelect?.(p.id));
|
||||
onDeselectAll?.();
|
||||
} else {
|
||||
// Select all
|
||||
filteredPhotos.forEach(p => {
|
||||
if (!selectedPhotos.has(p.id)) {
|
||||
onPhotoSelect?.(p.id);
|
||||
}
|
||||
});
|
||||
onSelectAll?.();
|
||||
}
|
||||
}, [selectedPhotos, filteredPhotos, onPhotoSelect]);
|
||||
}, [selectedPhotos, filteredPhotos, onSelectAll, onDeselectAll]);
|
||||
|
||||
const handleDownloadSelected = useCallback(async () => {
|
||||
if (selectedPhotos.size === 0) return;
|
||||
|
||||
@@ -7,9 +7,12 @@ import {
|
||||
Clock,
|
||||
HardDrive,
|
||||
Activity,
|
||||
Ruler,
|
||||
} from 'lucide-react';
|
||||
import { Button, Card, Input } from '../../../components/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '../../../config/api';
|
||||
import { settingsService } from '../../../services/settings.service';
|
||||
import { useStatusTab } from '../hooks/useStatusTab';
|
||||
import { UpdateNotificationSettings } from '../components/UpdateNotificationSettings';
|
||||
@@ -53,6 +56,27 @@ export const StatusTab: React.FC<StatusTabProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { storageInfo, systemStatus } = useStatusTab(isActive);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: dimensionStatus } = useQuery({
|
||||
queryKey: ['photo-dimension-status'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/admin/photos/repair-dimensions/status');
|
||||
return res.data;
|
||||
},
|
||||
enabled: isActive,
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
|
||||
const repairMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await api.post('/admin/photos/repair-dimensions');
|
||||
return res.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['photo-dimension-status'] });
|
||||
},
|
||||
});
|
||||
|
||||
// Sync soft limit from storage info
|
||||
useEffect(() => {
|
||||
@@ -528,6 +552,61 @@ export const StatusTab: React.FC<StatusTabProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Photo Dimensions */}
|
||||
{dimensionStatus && (
|
||||
<Card padding="md">
|
||||
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4 flex items-center gap-2">
|
||||
<Ruler className="w-5 h-5" />
|
||||
{t('settings.photoDimensions.title')}
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
{t('settings.photoDimensions.description')}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div className="bg-neutral-50 dark:bg-neutral-800 rounded-lg p-3 text-center">
|
||||
<p className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{dimensionStatus.total}</p>
|
||||
<p className="text-xs text-neutral-600 dark:text-neutral-400">{t('settings.photoDimensions.totalPhotos')}</p>
|
||||
</div>
|
||||
<div className="bg-neutral-50 dark:bg-neutral-800 rounded-lg p-3 text-center">
|
||||
<p className="text-2xl font-bold text-green-600 dark:text-green-400">{dimensionStatus.withDimensions}</p>
|
||||
<p className="text-xs text-neutral-600 dark:text-neutral-400">{t('settings.photoDimensions.withDimensions')}</p>
|
||||
</div>
|
||||
<div className={`rounded-lg p-3 text-center ${Number(dimensionStatus.withoutDimensions) > 0 ? 'bg-amber-50 dark:bg-amber-900/30' : 'bg-neutral-50 dark:bg-neutral-800'}`}>
|
||||
<p className={`text-2xl font-bold ${Number(dimensionStatus.withoutDimensions) > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-neutral-900 dark:text-neutral-100'}`}>{dimensionStatus.withoutDimensions}</p>
|
||||
<p className="text-xs text-neutral-600 dark:text-neutral-400">{t('settings.photoDimensions.missingDimensions')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dimensionStatus.lastResult && (
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
{t('settings.photoDimensions.resultSuccess', {
|
||||
success: dimensionStatus.lastResult.success,
|
||||
failed: dimensionStatus.lastResult.failed,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => repairMutation.mutate()}
|
||||
isLoading={repairMutation.isPending || dimensionStatus.isRunning}
|
||||
disabled={Number(dimensionStatus.withoutDimensions) === 0 || dimensionStatus.isRunning}
|
||||
leftIcon={<Ruler className="w-4 h-4" />}
|
||||
>
|
||||
{dimensionStatus.isRunning
|
||||
? t('settings.photoDimensions.repairing')
|
||||
: Number(dimensionStatus.withoutDimensions) === 0
|
||||
? t('settings.photoDimensions.noneToRepair')
|
||||
: t('settings.photoDimensions.repairButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Update Notification Settings */}
|
||||
<UpdateNotificationSettings />
|
||||
|
||||
|
||||
@@ -1250,6 +1250,19 @@
|
||||
"failed": "Fehlgeschlagen",
|
||||
"lastUpdate": "Letzte Aktualisierung"
|
||||
},
|
||||
"photoDimensions": {
|
||||
"title": "Foto-Abmessungen",
|
||||
"totalPhotos": "Fotos gesamt",
|
||||
"withDimensions": "Mit Abmessungen",
|
||||
"missingDimensions": "Fehlende Abmessungen",
|
||||
"repairButton": "Abmessungen reparieren",
|
||||
"repairing": "Repariere...",
|
||||
"alreadyRunning": "Reparatur läuft bereits",
|
||||
"started": "Reparatur von {{count}} Fotos gestartet",
|
||||
"noneToRepair": "Alle Fotos haben bereits Abmessungen",
|
||||
"resultSuccess": "Letzte Reparatur: {{success}} aktualisiert, {{failed}} fehlgeschlagen",
|
||||
"description": "Fehlende Breite/Höhe für Fotos ergänzen, die vor der Dimensionsverfolgung hochgeladen wurden. Erforderlich für Masonry- und Mosaik-Layouts."
|
||||
},
|
||||
"updateNotifications": {
|
||||
"title": "Update-Benachrichtigungen",
|
||||
"description": "Erhalten Sie E-Mail-Benachrichtigungen, wenn neue Versionen von PicPeak verfügbar sind.",
|
||||
|
||||
@@ -799,6 +799,19 @@
|
||||
"failed": "Failed",
|
||||
"lastUpdate": "Last update"
|
||||
},
|
||||
"photoDimensions": {
|
||||
"title": "Photo Dimensions",
|
||||
"totalPhotos": "Total Photos",
|
||||
"withDimensions": "With Dimensions",
|
||||
"missingDimensions": "Missing Dimensions",
|
||||
"repairButton": "Repair Dimensions",
|
||||
"repairing": "Repairing...",
|
||||
"alreadyRunning": "Repair is already running",
|
||||
"started": "Started repairing {{count}} photos",
|
||||
"noneToRepair": "All photos already have dimensions",
|
||||
"resultSuccess": "Last repair: {{success}} updated, {{failed}} failed",
|
||||
"description": "Backfill missing width/height for photos uploaded before dimension tracking was added. Required for Masonry and Mosaic layouts."
|
||||
},
|
||||
"updateNotifications": {
|
||||
"title": "Update Notifications",
|
||||
"description": "Receive email notifications when new versions of PicPeak are available.",
|
||||
|
||||
Reference in New Issue
Block a user