diff --git a/src/admin/AdminPhotosSyncClient.tsx b/src/admin/AdminPhotosSyncClient.tsx index 94479816..1e17068c 100644 --- a/src/admin/AdminPhotosSyncClient.tsx +++ b/src/admin/AdminPhotosSyncClient.tsx @@ -6,14 +6,15 @@ import IconGrSync from '@/components/icons/IconGrSync'; import Note from '@/components/Note'; import AdminChildPage from '@/components/AdminChildPage'; import { PATH_ADMIN_PHOTOS } from '@/app/paths'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { syncPhotosAction } from '@/photo/actions'; import { useRouter } from 'next/navigation'; import ResponsiveText from '@/components/primitives/ResponsiveText'; import { LiaBroomSolid } from 'react-icons/lia'; import ProgressButton from '@/components/primitives/ProgressButton'; +import ErrorNote from '@/components/ErrorNote'; -const UPDATE_BATCH_SIZE_MAX = 4; +const SYNC_BATCH_SIZE_MAX = 3; export default function AdminPhotosSyncClient({ photos, @@ -22,9 +23,14 @@ export default function AdminPhotosSyncClient({ photos: Photo[] hasAiTextGeneration: boolean }) { - const updateBatchSize = Math.min(UPDATE_BATCH_SIZE_MAX, photos.length); + // Use refs for non-reactive while loop state + const photoIdsToSync = useRef(photos.map(photo => photo.id)); + const errorRef = useRef(undefined); + // Use state for updating progress button and error UI const [photoIdsSyncing, setPhotoIdsSyncing] = useState([]); + const [error, setError] = useState(); + const [progress, setProgress] = useState(0); const arePhotoIdsSyncing = photoIdsSyncing.length > 0; @@ -41,36 +47,64 @@ export default function AdminPhotosSyncClient({ primary icon={} hideTextOnMobile={false} + progress={progress} onClick={async () => { - if (window.confirm( - // eslint-disable-next-line max-len - `Are you sure you want to sync the oldest ${updateBatchSize} photos? This action cannot be undone.`, - )) { - const photosToSync = photos - .slice(0, updateBatchSize) - .map(photo => photo.id); - const isFinalBatch = photosToSync.length >= photos.length; - setPhotoIdsSyncing(photosToSync); - syncPhotosAction(photosToSync) - .finally(() => { - if (isFinalBatch) { - router.push(PATH_ADMIN_PHOTOS); - } else { - setPhotoIdsSyncing([]); + if (window.confirm([ + 'Are you sure you want to sync', + photos.length === 1 + ? '1 outdated photo?' + : `all ${photos.length} outdated photos?`, + 'Browser must remain open while syncing.', + 'This action cannot be undone.', + ].join(' '))) { + errorRef.current = undefined; + setError(undefined); + while (photoIdsToSync.current.length > 0) { + const photoIds = photoIdsToSync.current + .slice(0, SYNC_BATCH_SIZE_MAX); + setPhotoIdsSyncing(photoIds); + await syncPhotosAction(photoIds) + .then(() => { + photoIdsToSync.current = photoIdsToSync.current.filter( + id => !photoIds.includes(id), + ); + setProgress( + (photos.length - photoIdsToSync.current.length) / + photos.length, + ); router.refresh(); - } - }); + }) + .catch(e => { + errorRef.current = e; + setError(e); + }); + if (errorRef.current) { break; } + } + if (!errorRef.current) { + router.push(PATH_ADMIN_PHOTOS); + } else { + setProgress(0); + setPhotoIdsSyncing([]); + router.refresh(); + } } }} isLoading={arePhotoIdsSyncing} - disabled={!updateBatchSize} + disabled={photoIdsSyncing.length > 0} > {arePhotoIdsSyncing - ? 'Syncing' + ? 'Syncing ...' : 'Sync All'} } >
+ {error && + + Issue syncing: + + {' '} + {error.message} + } } @@ -81,11 +115,11 @@ export default function AdminPhotosSyncClient({ {' '} {photos.length === 1 ? 'photo' : 'photos'} {' '} - could benefit from being synced + found
- Sync photos to import newer EXIF fields, improve blur data, + Sync to capture newer EXIF fields, improve blur data, {' '} - and generate AI text when configured + and use AI to generate missing text (if configured)
@@ -96,6 +130,7 @@ export default function AdminPhotosSyncClient({ canEdit={false} canDelete={false} dateType="updatedAt" + shouldScrollIntoViewOnExternalSync />
diff --git a/src/admin/AdminPhotosTable.tsx b/src/admin/AdminPhotosTable.tsx index 937a70d4..af2459c3 100644 --- a/src/admin/AdminPhotosTable.tsx +++ b/src/admin/AdminPhotosTable.tsx @@ -28,6 +28,7 @@ export default function AdminPhotosTable({ canEdit = true, canDelete = true, timezone, + shouldScrollIntoViewOnExternalSync, }: { photos: Photo[], onLastPhotoVisible?: () => void @@ -38,6 +39,7 @@ export default function AdminPhotosTable({ canEdit?: boolean canDelete?: boolean timezone?: Timezone + shouldScrollIntoViewOnExternalSync?: boolean }) { const { invalidateSwr } = useAppState(); @@ -70,7 +72,7 @@ export default function AdminPhotosTable({ - {titleForPhoto(photo)} + {titleForPhoto(photo, false)} {photo.hidden && {' '} {canDelete && ) { + const ref = useRef(null); + const [isSyncing, setIsSyncing] = useState(false); const confirmText = ['Overwrite']; @@ -33,10 +39,18 @@ export default function PhotoSyncButton({ 'AI text will be generated for undefined fields.'); } confirmText.push('This action cannot be undone.'); + useScrollIntoView({ + ref, + shouldScrollIntoView: + isSyncingExternal && + shouldScrollIntoViewOnExternalSync, + }); + return ( } diff --git a/src/components/primitives/LoaderButton.tsx b/src/components/primitives/LoaderButton.tsx index d865c945..169d8809 100644 --- a/src/components/primitives/LoaderButton.tsx +++ b/src/components/primitives/LoaderButton.tsx @@ -2,10 +2,16 @@ import Spinner, { SpinnerColor } from '@/components/Spinner'; import { clsx } from 'clsx/lite'; -import { ButtonHTMLAttributes, ComponentProps, ReactNode } from 'react'; +import { + ButtonHTMLAttributes, + ComponentProps, + ReactNode, + RefObject, +} from 'react'; import Tooltip from '../Tooltip'; export default function LoaderButton({ + ref, children, isLoading, icon, @@ -25,6 +31,7 @@ export default function LoaderButton({ tooltipColor, ...rest }: { + ref?: RefObject isLoading?: boolean icon?: ReactNode spinnerColor?: SpinnerColor @@ -41,6 +48,7 @@ export default function LoaderButton({ const button =