Extract common upload add routine

This commit is contained in:
Sam Becker 2025-06-18 09:54:30 -05:00
parent b115b98ea5
commit 5704597a4f
3 changed files with 121 additions and 95 deletions

View File

@ -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<typeof PathLoaderButton>) {
return (
<PathLoaderButton
{...props}
icon={<BiImageAdd
size={18}
className="translate-x-[1px] translate-y-[1px]"
/>}
>
{children || 'Add'}
</PathLoaderButton>
);
}

View File

@ -8,13 +8,14 @@ import clsx from 'clsx/lite';
import ResponsiveDate from '@/components/ResponsiveDate'; import ResponsiveDate from '@/components/ResponsiveDate';
import Spinner from '@/components/Spinner'; import Spinner from '@/components/Spinner';
import { FaRegCircleCheck } from 'react-icons/fa6'; import { FaRegCircleCheck } from 'react-icons/fa6';
import AddButton from './AddButton';
import { pathForAdminUploadUrl } from '@/app/paths'; import { pathForAdminUploadUrl } from '@/app/paths';
import DeleteBlobButton from './DeleteUploadButton'; import DeleteBlobButton from './DeleteUploadButton';
import { Dispatch, SetStateAction, useEffect, useRef } from 'react'; import { Dispatch, SetStateAction, useEffect, useRef } from 'react';
import { isElementEntirelyInViewport } from '@/utility/dom'; import { isElementEntirelyInViewport } from '@/utility/dom';
import FieldSetWithStatus from '@/components/FieldSetWithStatus'; import FieldSetWithStatus from '@/components/FieldSetWithStatus';
import EditButton from './EditButton'; import EditButton from './EditButton';
import LoaderButton from '@/components/primitives/LoaderButton';
import { BiImageAdd } from 'react-icons/bi';
export default function AdminUploadsTableRow({ export default function AdminUploadsTableRow({
url, url,
@ -55,6 +56,8 @@ export default function AdminUploadsTableRow({
} }
}, [status]); }, [status]);
const isRowLoading = isAdding || isDeleting || isComplete || Boolean(status);
return ( return (
<div <div
ref={ref} ref={ref}
@ -97,7 +100,7 @@ export default function AdminUploadsTableRow({
}} }}
placeholder="Title (optional)" placeholder="Title (optional)"
tabIndex={tabIndex} tabIndex={tabIndex}
readOnly={isAdding || isDeleting || isComplete || Boolean(status)} readOnly={isRowLoading}
hideLabel hideLabel
/> />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -112,14 +115,20 @@ export default function AdminUploadsTableRow({
/>} />}
</> </>
: <> : <>
<AddButton <LoaderButton
path={pathForAdminUploadUrl(url)} icon={<BiImageAdd
disabled={isDeleting} size={18}
className="translate-x-[1px] translate-y-[1px]"
/>}
disabled={isRowLoading}
tooltip="Add directly" tooltip="Add directly"
hideText="never" hideText="never"
/> >
Add
</LoaderButton>
<EditButton <EditButton
path={pathForAdminUploadUrl(url)} path={pathForAdminUploadUrl(url)}
disabled={isRowLoading}
tooltip="Review photo details" tooltip="Review photo details"
hideText="always" hideText="always"
/> />
@ -133,7 +142,7 @@ export default function AdminUploadsTableRow({
setUrlAddStatuses?.(statuses => statuses setUrlAddStatuses?.(statuses => statuses
.filter(({ url: urlToRemove }) => urlToRemove !== url)); .filter(({ url: urlToRemove }) => urlToRemove !== url));
}} }}
isLoading={isDeleting} disabled={isRowLoading}
tooltip="Delete upload" tooltip="Delete upload"
/> />
</>} </>}

View File

@ -85,23 +85,108 @@ export const createPhotoAction = async (formData: FormData) =>
} }
}); });
export const addUploadsAction = async ({ // Helper function for:
uploadUrls, // - addUploadAction
uploadTitles, // - addUploadsAction
const addUpload = async ({
url,
title,
tags, tags,
favorite, favorite,
hidden, hidden,
takenAtLocal, takenAtLocal,
takenAtNaiveLocal, takenAtNaiveLocal,
shouldRevalidateAllKeysAndPaths = true, onStreamUpdate,
}: { onFinish,
uploadUrls: string[] }:{
uploadTitles: string[] url: string
title?: string
tags?: string tags?: string
favorite?: string favorite?: string
hidden?: string hidden?: string
takenAtLocal: string takenAtLocal: string
takenAtNaiveLocal: 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<PhotoFormData> = {
...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<typeof addUpload>[0] & {
uploadUrls: string[]
uploadTitles: string[]
shouldRevalidateAllKeysAndPaths?: boolean shouldRevalidateAllKeysAndPaths?: boolean
}) => }) =>
runAuthenticatedAdminServerAction(async () => { runAuthenticatedAdminServerAction(async () => {
@ -128,71 +213,23 @@ export const addUploadsAction = async ({
try { try {
for (const [index, url] of uploadUrls.entries()) { for (const [index, url] of uploadUrls.entries()) {
currentUploadUrl = url; currentUploadUrl = url;
const title = uploadTitles[index];
progress = 0; progress = 0;
const title = uploadTitles[index];
streamUpdate('Parsing EXIF data'); streamUpdate('Parsing EXIF data');
const { await addUpload({
formDataFromExif, url,
imageResizedBase64, title,
shouldStripGpsData, tags,
fileBytes, favorite,
} = await extractImageDataFromBlobPath(url, { hidden,
includeInitialPhotoFields: true, takenAtLocal,
generateBlurData: BLUR_ENABLED, takenAtNaiveLocal,
generateResizedImage: AI_TEXT_GENERATION_ENABLED, onStreamUpdate: streamUpdate,
}); onFinish: () => {
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<PhotoFormData> = {
...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);
addedUploadUrls.push(url); addedUploadUrls.push(url);
// Re-submit with updated url },
streamUpdate(subheadFinal, 'added'); });
}
}
}; };
} catch (error: any) { } catch (error: any) {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len