From 5704597a4fab93fba536960b30a443268419a305 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 18 Jun 2025 09:54:30 -0500 Subject: [PATCH] Extract common upload add routine --- src/admin/AddButton.tsx | 20 ---- src/admin/AdminUploadsTableRow.tsx | 23 ++-- src/photo/actions.ts | 173 +++++++++++++++++------------ 3 files changed, 121 insertions(+), 95 deletions(-) delete mode 100644 src/admin/AddButton.tsx diff --git a/src/admin/AddButton.tsx b/src/admin/AddButton.tsx deleted file mode 100644 index 11f154f3..00000000 --- a/src/admin/AddButton.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { BiImageAdd } from 'react-icons/bi'; -import PathLoaderButton from '@/components/primitives/PathLoaderButton'; -import { ComponentProps } from 'react'; - -export default function AddButton({ - children, - ...props -}: ComponentProps) { - return ( - } - > - {children || 'Add'} - - ); -} diff --git a/src/admin/AdminUploadsTableRow.tsx b/src/admin/AdminUploadsTableRow.tsx index 9ec13ae3..503dcd23 100644 --- a/src/admin/AdminUploadsTableRow.tsx +++ b/src/admin/AdminUploadsTableRow.tsx @@ -8,13 +8,14 @@ import clsx from 'clsx/lite'; import ResponsiveDate from '@/components/ResponsiveDate'; import Spinner from '@/components/Spinner'; import { FaRegCircleCheck } from 'react-icons/fa6'; -import AddButton from './AddButton'; import { pathForAdminUploadUrl } from '@/app/paths'; import DeleteBlobButton from './DeleteUploadButton'; import { Dispatch, SetStateAction, useEffect, useRef } from 'react'; import { isElementEntirelyInViewport } from '@/utility/dom'; import FieldSetWithStatus from '@/components/FieldSetWithStatus'; import EditButton from './EditButton'; +import LoaderButton from '@/components/primitives/LoaderButton'; +import { BiImageAdd } from 'react-icons/bi'; export default function AdminUploadsTableRow({ url, @@ -55,6 +56,8 @@ export default function AdminUploadsTableRow({ } }, [status]); + const isRowLoading = isAdding || isDeleting || isComplete || Boolean(status); + return (
@@ -112,14 +115,20 @@ export default function AdminUploadsTableRow({ />} : <> - } + disabled={isRowLoading} tooltip="Add directly" hideText="never" - /> + > + Add + @@ -133,7 +142,7 @@ export default function AdminUploadsTableRow({ setUrlAddStatuses?.(statuses => statuses .filter(({ url: urlToRemove }) => urlToRemove !== url)); }} - isLoading={isDeleting} + disabled={isRowLoading} tooltip="Delete upload" /> } diff --git a/src/photo/actions.ts b/src/photo/actions.ts index f515303a..179e6cc7 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -85,23 +85,108 @@ export const createPhotoAction = async (formData: FormData) => } }); -export const addUploadsAction = async ({ - uploadUrls, - uploadTitles, +// Helper function for: +// - addUploadAction +// - addUploadsAction +const addUpload = async ({ + url, + title, tags, favorite, hidden, takenAtLocal, takenAtNaiveLocal, - shouldRevalidateAllKeysAndPaths = true, -}: { - uploadUrls: string[] - uploadTitles: string[] + onStreamUpdate, + onFinish, +}:{ + url: string + title?: string tags?: string favorite?: string hidden?: string takenAtLocal: string takenAtNaiveLocal: string + onStreamUpdate: ( + statusMessage: string, + status?: UrlAddStatus['status'], + ) => void + onFinish?: (url: string) => void +}) => { + const { + formDataFromExif, + imageResizedBase64, + shouldStripGpsData, + fileBytes, + } = await extractImageDataFromBlobPath(url, { + includeInitialPhotoFields: true, + generateBlurData: BLUR_ENABLED, + generateResizedImage: AI_TEXT_GENERATION_ENABLED, + }); + + if (formDataFromExif) { + if (AI_TEXT_GENERATION_ENABLED) { + onStreamUpdate('Generating AI text'); + } + + const { + title: aiTitle, + caption, + tags: aiTags, + semanticDescription, + } = await generateAiImageQueries( + imageResizedBase64, + Boolean(title) + ? AI_TEXT_AUTO_GENERATED_FIELDS + .filter(field => field !== 'title') + : AI_TEXT_AUTO_GENERATED_FIELDS, + title, + ); + + const form: Partial = { + ...formDataFromExif, + title: title || aiTitle, + caption, + tags: tags || aiTags, + hidden, + favorite, + semanticDescription, + takenAt: formDataFromExif.takenAt || takenAtLocal, + takenAtNaive: formDataFromExif.takenAtNaive || takenAtNaiveLocal, + }; + + onStreamUpdate('Transferring to photo storage'); + + const updatedUrl = await convertUploadToPhoto({ + urlOrigin: url, + fileBytes, + shouldStripGpsData, + }); + if (updatedUrl) { + const subheadFinal = 'Adding to database'; + onStreamUpdate(subheadFinal); + const photo = + await convertFormDataToPhotoDbInsertAndLookupRecipeTitle(form); + photo.url = updatedUrl; + await insertPhoto(photo); + onFinish?.(url); + // Re-submit with updated url + onStreamUpdate(subheadFinal, 'added'); + } + } +}; + +export const addUploadsAction = async ({ + uploadUrls, + uploadTitles, + shouldRevalidateAllKeysAndPaths = true, + tags, + favorite, + hidden, + takenAtLocal, + takenAtNaiveLocal, +}: Parameters[0] & { + uploadUrls: string[] + uploadTitles: string[] shouldRevalidateAllKeysAndPaths?: boolean }) => runAuthenticatedAdminServerAction(async () => { @@ -128,71 +213,23 @@ export const addUploadsAction = async ({ try { for (const [index, url] of uploadUrls.entries()) { currentUploadUrl = url; - const title = uploadTitles[index]; progress = 0; + const title = uploadTitles[index]; streamUpdate('Parsing EXIF data'); - const { - formDataFromExif, - imageResizedBase64, - shouldStripGpsData, - fileBytes, - } = await extractImageDataFromBlobPath(url, { - includeInitialPhotoFields: true, - generateBlurData: BLUR_ENABLED, - generateResizedImage: AI_TEXT_GENERATION_ENABLED, - }); - - if (formDataFromExif) { - if (AI_TEXT_GENERATION_ENABLED) { - streamUpdate('Generating AI text'); - } - - const { - title: aiTitle, - caption, - tags: aiTags, - semanticDescription, - } = await generateAiImageQueries( - imageResizedBase64, - Boolean(title) - ? AI_TEXT_AUTO_GENERATED_FIELDS - .filter(field => field !== 'title') - : AI_TEXT_AUTO_GENERATED_FIELDS, - title, - ); - - const form: Partial = { - ...formDataFromExif, - title: title || aiTitle, - caption, - tags: tags || aiTags, - hidden, - favorite, - semanticDescription, - takenAt: formDataFromExif.takenAt || takenAtLocal, - takenAtNaive: formDataFromExif.takenAtNaive || takenAtNaiveLocal, - }; - - streamUpdate('Transferring to photo storage'); - - const updatedUrl = await convertUploadToPhoto({ - urlOrigin: url, - fileBytes, - shouldStripGpsData, - }); - if (updatedUrl) { - const subheadFinal = 'Adding to database'; - streamUpdate(subheadFinal); - const photo = - await convertFormDataToPhotoDbInsertAndLookupRecipeTitle(form); - photo.url = updatedUrl; - await insertPhoto(photo); + await addUpload({ + url, + title, + tags, + favorite, + hidden, + takenAtLocal, + takenAtNaiveLocal, + onStreamUpdate: streamUpdate, + onFinish: () => { addedUploadUrls.push(url); - // Re-submit with updated url - streamUpdate(subheadFinal, 'added'); - } - } + }, + }); }; } catch (error: any) { // eslint-disable-next-line max-len