From 6326db0a18fa64b9204c2ee2f1162faa5432e7a1 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 16 Jun 2024 12:39:23 -0500 Subject: [PATCH] Batch upload adding action --- src/admin/AdminAddAllUploads.tsx | 82 +++++++++++++++++++++++--------- src/admin/AdminUploadsClient.tsx | 4 +- src/photo/actions.ts | 36 ++++++-------- src/site/globals.css | 4 +- 4 files changed, 78 insertions(+), 48 deletions(-) diff --git a/src/admin/AdminAddAllUploads.tsx b/src/admin/AdminAddAllUploads.tsx index 78c16120..0fc89731 100644 --- a/src/admin/AdminAddAllUploads.tsx +++ b/src/admin/AdminAddAllUploads.tsx @@ -15,24 +15,27 @@ import { generateLocalNaivePostgresString, generateLocalPostgresString, } from '@/utility/date'; +import sleep from '@/utility/sleep'; import { readStreamableValue } from 'ai/rsc'; import { clsx } from 'clsx/lite'; import { useRouter } from 'next/navigation'; -import { useRef, useState } from 'react'; -import { BiImageAdd } from 'react-icons/bi'; +import { Dispatch, SetStateAction, useRef, useState } from 'react'; +import { BiCheckCircle, BiImageAdd } from 'react-icons/bi'; + +const UPLOAD_BATCH_SIZE = 4; export default function AdminAddAllUploads({ - storageUrlCount, + storageUrls, uniqueTags, isAdding, setIsAdding, - onUploadAdded, + setAddedUploadUrls, }: { - storageUrlCount: number + storageUrls: string[] uniqueTags?: TagsWithMeta isAdding: boolean setIsAdding: (isAdding: boolean) => void - onUploadAdded?: (addedUploadUrls: string[]) => void + setAddedUploadUrls?: Dispatch> }) { const divRef = useRef(null); @@ -42,9 +45,41 @@ export default function AdminAddAllUploads({ const [tags, setTags] = useState(''); const [actionErrorMessage, setActionErrorMessage] = useState(''); const [tagErrorMessage, setTagErrorMessage] = useState(''); + const [isAddingComplete, setIsAddingComplete] = useState(false); const router = useRouter(); + const addedUploadUrls = useRef([]); + const addUploadUrls = async (uploadUrls: string[]) => { + try { + const stream = await addAllUploadsAction({ + uploadUrls, + tags: showTags ? tags : undefined, + takenAtLocal: generateLocalPostgresString(), + takenAtNaiveLocal: generateLocalNaivePostgresString(), + }); + for await (const data of readStreamableValue(stream)) { + setButtonText(addedUploadUrls.current.length === 0 + ? `Adding ${storageUrls.length} uploads` + : `Adding ${addedUploadUrls.current.length} of ${storageUrls.length}` + ); + setButtonSubheadText(data?.subhead ?? ''); + setAddedUploadUrls?.(current => { + const urls = data?.addedUploadUrls.split(',') ?? []; + const updatedUrls = current + .filter(url => !urls.includes(url)) + .concat(urls); + addedUploadUrls.current = updatedUrls; + return updatedUrls; + }); + } + } catch (e: any) { + setIsAdding(false); + setButtonText('Try Again'); + setActionErrorMessage(e); + } + }; + return ( <> {actionErrorMessage && @@ -58,7 +93,7 @@ export default function AdminAddAllUploads({ )}> {showTags ? tagErrorMessage || 'Add tags to all uploads' - : `Found ${storageUrlCount} uploads`} + : `Found ${storageUrls.length} uploads`} } + disabled={Boolean(tagErrorMessage) || isAddingComplete} + icon={isAddingComplete + ? + : + } onClick={async () => { - if (confirm( - `Are you sure you want to add all ${storageUrlCount} uploads?` - )) { + // eslint-disable-next-line max-len + if (confirm(`Are you sure you want to add all ${storageUrls.length} uploads?`)) { setIsAdding(true); + let uploadsToAdd = storageUrls.slice(); try { - const stream = await addAllUploadsAction({ - tags: showTags ? tags : undefined, - takenAtLocal: generateLocalPostgresString(), - takenAtNaiveLocal: generateLocalNaivePostgresString(), - }); - for await (const data of readStreamableValue(stream)) { - setButtonText(data?.headline ?? ''); - setButtonSubheadText(data?.subhead ?? ''); - onUploadAdded?.(data?.addedUploadUrls.split(',') ?? []); + while (uploadsToAdd.length > 0) { + await addUploadUrls( + uploadsToAdd.splice(0, UPLOAD_BATCH_SIZE), + ); } - router.push(PATH_ADMIN_PHOTOS); + setButtonText('Complete'); + setButtonSubheadText('All uploads added'); + setIsAdding(false); + setIsAddingComplete(true); + await sleep(1000).then(() => + router.push(PATH_ADMIN_PHOTOS)); } catch (e: any) { setIsAdding(false); setButtonText('Try Again'); diff --git a/src/admin/AdminUploadsClient.tsx b/src/admin/AdminUploadsClient.tsx index b8d509cc..8ac65884 100644 --- a/src/admin/AdminUploadsClient.tsx +++ b/src/admin/AdminUploadsClient.tsx @@ -21,11 +21,11 @@ export default function AdminUploadsClient({
{urls.length > 1 && url)} uniqueTags={uniqueTags} isAdding={isAdding} setIsAdding={setIsAdding} - onUploadAdded={setAddedUploadUrls} + setAddedUploadUrls={setAddedUploadUrls} />}
diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 93f2229f..83460245 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -43,7 +43,6 @@ import { AI_TEXT_GENERATION_ENABLED, BLUR_ENABLED, } from '@/site/config'; -import { getStorageUploadUrlsNoStore } from '@/services/storage/cache'; import { generateAiImageQueries } from './ai/server'; import { createStreamableValue } from 'ai/rsc'; import { convertUploadToPhoto } from './storage'; @@ -71,38 +70,29 @@ export const createPhotoAction = async (formData: FormData) => }); export const addAllUploadsAction = async ({ + uploadUrls, tags, takenAtLocal, takenAtNaiveLocal, - limit, }: { + uploadUrls: string[] tags?: string takenAtLocal: string takenAtNaiveLocal: string - limit?: number }) => runAuthenticatedAdminServerAction(async () => { - const uploadUrls = (await getStorageUploadUrlsNoStore()) - .slice(0, limit); const uploadTotal = uploadUrls.length; const addedUploadUrls: string[] = []; - const stream = createStreamableValue<{ - headline: string, - subhead?: string, - addedUploadUrls: string, - }, string>({ - headline: `Adding ${uploadTotal} Photos...`, + const stream = createStreamableValue({ + subhead: '', addedUploadUrls: '', }); (async () => { try { - for (const [index, { url }] of uploadUrls.entries()) { - const headline = `Adding ${index + 1} of ${uploadTotal}`; - + for (const url of uploadUrls) { stream.update({ - headline, subhead: 'Parsing EXIF data', addedUploadUrls: addedUploadUrls.join(','), }); @@ -121,7 +111,6 @@ export const addAllUploadsAction = async ({ if (photoFormExif) { if (AI_TEXT_GENERATION_ENABLED) { stream.update({ - headline, subhead: 'Generating AI text', addedUploadUrls: addedUploadUrls.join(','), }); @@ -148,7 +137,6 @@ export const addAllUploadsAction = async ({ }; stream.update({ - headline, subhead: 'Moving upload to photo storage', addedUploadUrls: addedUploadUrls.join(','), }); @@ -159,24 +147,28 @@ export const addAllUploadsAction = async ({ fileBytes, ); if (updatedUrl) { + const subhead = 'Adding to database'; stream.update({ - headline, - subhead: 'Adding to database', + subhead, addedUploadUrls: addedUploadUrls.join(','), }); const photo = convertFormDataToPhotoDbInsert(form); photo.url = updatedUrl; await insertPhoto(photo); addedUploadUrls.push(url); + // Re-submit with updated url + stream.update({ + subhead, + addedUploadUrls: addedUploadUrls.join(','), + }); } } - } + }; } catch (error: any) { // eslint-disable-next-line max-len stream.error(`${error.message} (${addedUploadUrls.length} of ${uploadTotal} photos successfully added)`); - } finally { - revalidateAllKeysAndPaths(); } + revalidateAllKeysAndPaths(); stream.done(); })(); diff --git a/src/site/globals.css b/src/site/globals.css index c4b8cf43..992b8dd7 100644 --- a/src/site/globals.css +++ b/src/site/globals.css @@ -97,8 +97,8 @@ @apply text-invert bg-gray-900 dark:bg-gray-100 - disabled:text-gray-300 disabled:dark:text-gray-700 - disabled:bg-white disabled:dark:bg-black + disabled:text-dim + disabled:bg-gray-100 dark:disabled:bg-gray-900 disabled:border-gray-200 disabled:dark:border-gray-700 border-gray-900 dark:border-gray-100 active:bg-gray-700 active:border-gray-700