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 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 (
<div
ref={ref}
@ -97,7 +100,7 @@ export default function AdminUploadsTableRow({
}}
placeholder="Title (optional)"
tabIndex={tabIndex}
readOnly={isAdding || isDeleting || isComplete || Boolean(status)}
readOnly={isRowLoading}
hideLabel
/>
<div className="flex items-center gap-2">
@ -112,14 +115,20 @@ export default function AdminUploadsTableRow({
/>}
</>
: <>
<AddButton
path={pathForAdminUploadUrl(url)}
disabled={isDeleting}
<LoaderButton
icon={<BiImageAdd
size={18}
className="translate-x-[1px] translate-y-[1px]"
/>}
disabled={isRowLoading}
tooltip="Add directly"
hideText="never"
/>
>
Add
</LoaderButton>
<EditButton
path={pathForAdminUploadUrl(url)}
disabled={isRowLoading}
tooltip="Review photo details"
hideText="always"
/>
@ -133,7 +142,7 @@ export default function AdminUploadsTableRow({
setUrlAddStatuses?.(statuses => statuses
.filter(({ url: urlToRemove }) => urlToRemove !== url));
}}
isLoading={isDeleting}
disabled={isRowLoading}
tooltip="Delete upload"
/>
</>}

View File

@ -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<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
}) =>
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<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);
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