Improve upload state management

This commit is contained in:
Sam Becker 2025-06-18 21:35:24 -05:00
parent 144e68b965
commit f6bc865225
10 changed files with 63 additions and 25 deletions

View File

@ -119,7 +119,13 @@ export default function RootLayout({
revalidatePath('/admin', 'layout');
}}
/>
<AdminBatchEditPanel />
<AdminBatchEditPanel
onBatchActionComplete={async () => {
'use server';
// Update upload count in admin nav
revalidatePath('/admin', 'layout');
}}
/>
{children}
</div>
</main>

View File

@ -6,8 +6,8 @@ import {
} from '@/utility/date';
import { pathForAdminUploadUrl } from '@/app/paths';
import { useRouter } from 'next/navigation';
import { BiImageAdd } from 'react-icons/bi';
import { ComponentProps, useState } from 'react';
import IconAddUpload from '@/components/icons/IconAddUpload';
export default function AddUploadButton({
url,
@ -30,10 +30,7 @@ export default function AddUploadButton({
return (
<LoaderButton
{...props}
icon={<BiImageAdd
size={18}
className="translate-x-[1px] translate-y-[1px]"
/>}
icon={<IconAddUpload />}
onClick={() => {
onAddStart?.();
setIsAddingLocal(true);

View File

@ -1,9 +1,13 @@
import { getUniqueTagsCached } from '@/photo/cache';
import AdminBatchEditPanelClient from './AdminBatchEditPanelClient';
export default async function AdminBatchEditPanel() {
export default async function AdminBatchEditPanel({
onBatchActionComplete,
}: {
onBatchActionComplete?: () => Promise<void>
}) {
const uniqueTags = await getUniqueTagsCached().catch(() => []);
return (
<AdminBatchEditPanelClient {...{ uniqueTags }} />
<AdminBatchEditPanelClient {...{ uniqueTags, onBatchActionComplete }} />
);
}

View File

@ -14,7 +14,7 @@ import sleep from '@/utility/sleep';
import { readStreamableValue } from 'ai/rsc';
import { useRouter } from 'next/navigation';
import { Dispatch, SetStateAction, useRef, useState } from 'react';
import { BiCheckCircle, BiImageAdd } from 'react-icons/bi';
import { BiCheckCircle } from 'react-icons/bi';
import ProgressButton from '@/components/primitives/ProgressButton';
import { UrlAddStatus } from './AdminUploadsClient';
import PhotoTagFieldset from './PhotoTagFieldset';
@ -23,6 +23,7 @@ import { useAppState } from '@/state/AppState';
import { pluralize } from '@/utility/string';
import FieldsetFavs from '@/photo/form/FieldsetFavs';
import FieldsetHidden from '@/photo/form/FieldsetHidden';
import IconAddUpload from '@/components/icons/IconAddUpload';
const UPLOAD_BATCH_SIZE = 2;
@ -35,6 +36,7 @@ export default function AdminBatchUploadActions({
setUrlAddStatuses,
isDeleting,
setIsDeleting,
onBatchActionComplete,
}: {
uploadUrls: string[]
uploadTitles: string[]
@ -44,6 +46,7 @@ export default function AdminBatchUploadActions({
setUrlAddStatuses: Dispatch<SetStateAction<UrlAddStatus[]>>
isDeleting: boolean
setIsDeleting: Dispatch<SetStateAction<boolean>>
onBatchActionComplete?: () => Promise<void>
}) {
const { updateAdminData } = useAppState();
@ -177,10 +180,7 @@ export default function AdminBatchUploadActions({
}
icon={isAddingComplete
? <BiCheckCircle size={18} className="translate-x-[1px]" />
: <BiImageAdd
size={18}
className="translate-x-[1px] translate-y-[2px]"
/>
: <IconAddUpload />
}
onClick={async () => {
// eslint-disable-next-line max-len
@ -208,6 +208,7 @@ export default function AdminBatchUploadActions({
setAddingProgress(1);
setIsAdding(false);
setIsAddingComplete(true);
await onBatchActionComplete?.();
await sleep(1000).then(() =>
router.push(PATH_ADMIN_PHOTOS));
} catch (e: any) {
@ -225,9 +226,10 @@ export default function AdminBatchUploadActions({
<DeleteUploadButton
urls={uploadUrls}
onDeleteStart={() => setIsDeleting(true)}
onDelete={didFail => {
onDelete={async didFail => {
if (!didFail) {
updateAdminData?.({ uploadsCount: 0 });
await onBatchActionComplete?.();
router.push(PATH_ADMIN_PHOTOS);
} else {
setIsDeleting(false);

View File

@ -2,7 +2,7 @@
import { StorageListItem, StorageListResponse } from '@/platforms/storage';
import AdminBatchUploadActions from './AdminBatchUploadActions';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { Tags } from '@/tag';
import AdminUploadsTable from './AdminUploadsTable';
@ -20,14 +20,19 @@ export default function AdminUploadsClient({
urls: StorageListResponse
uniqueTags?: Tags
}) {
const [isAdding, setIsAdding] = useState(false);
const [urlAddStatuses, setUrlAddStatuses] = useState<UrlAddStatus[]>(urls);
useEffect(() => {
// Overwrite local state when server state changes
setUrlAddStatuses(urls);
}, [urls]);
const uploadUrls = useMemo(() => urlAddStatuses
.map(({ url }) => url), [urlAddStatuses]);
const uploadTitles = useMemo(() => urlAddStatuses
.map(({ draftTitle }) => draftTitle ?? ''), [urlAddStatuses]);
const [isAdding, setIsAdding] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
return (

View File

@ -96,8 +96,8 @@ export default function AdminUploadsTableRow({
'gap-2 sm:gap-3',
'p-2 sm:p-3',
)}>
<div className="flex flex-col gap-3 w-full">
<div className="flex flex-col grow gap-3">
<div className="flex flex-col gap-8 w-full">
<div className="flex flex-col grow gap-2">
<FieldSetWithStatus
label="Title"
value={draftTitle}
@ -133,7 +133,7 @@ export default function AdminUploadsTableRow({
<EditButton
path={pathForAdminUploadUrl(url, draftTitle)}
disabled={isRowLoading}
tooltip="Review photo details"
tooltip="Review EXIF details before adding"
hideText="always"
/>
<DeleteUploadButton

View File

@ -0,0 +1,15 @@
import { clsx } from 'clsx/lite';
import { IconBaseProps } from 'react-icons';
import { BiImageAdd } from 'react-icons/bi';
export default function IconAddUpload({
className,
size,
...props
}: IconBaseProps) {
return <BiImageAdd
{...props}
size={size ?? 18}
className={clsx('translate-x-[1px] translate-y-[1px]', className)}
/>;
}

View File

@ -1,7 +1,7 @@
'use client';
import { uploadPhotoFromClient } from '@/platforms/storage';
import { useRouter } from 'next/navigation';
import { usePathname, useRouter } from 'next/navigation';
import { PATH_ADMIN_UPLOADS, pathForAdminUploadUrl } from '@/app/paths';
import ImageInput from '../components/ImageInput';
import { clsx } from 'clsx/lite';
@ -49,6 +49,8 @@ export default function PhotoUploadWithStatus({
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
// Hide upload panel while button is shown
if (showButton) {
@ -127,9 +129,14 @@ export default function PhotoUploadWithStatus({
if (isLastBlob) {
await onLastUpload?.();
shouldResetUploadStateAfterPending.current = true;
startTransition(() => hasMultipleUploads
? router.push(PATH_ADMIN_UPLOADS)
: router.push(pathForAdminUploadUrl(url)));
if (pathname === PATH_ADMIN_UPLOADS) {
setUploadState?.({ isUploading: false });
router.refresh();
} else {
startTransition(() => hasMultipleUploads
? router.push(PATH_ADMIN_UPLOADS)
: router.push(pathForAdminUploadUrl(url)));
}
}
})
.catch(error => {

View File

@ -47,6 +47,7 @@ import PhotoFilmIcon from '@/film/PhotoFilmIcon';
import FieldsetFavs from './FieldsetFavs';
import FieldsetHidden from './FieldsetHidden';
import { useAppText } from '@/i18n/state/client';
import IconAddUpload from '@/components/icons/IconAddUpload';
const THUMBNAIL_SIZE = 300;
@ -469,12 +470,13 @@ export default function PhotoForm({
Cancel
</Link>
<SubmitButtonWithStatus
icon={type === 'create' && <IconAddUpload />}
disabled={!canFormBeSubmitted}
onFormStatusChange={onFormStatusChange}
onFormSubmit={invalidateSwr}
primary
>
{type === 'create' ? 'Create' : 'Update'}
{type === 'create' ? 'Add' : 'Update'}
</SubmitButtonWithStatus>
<div className={clsx(
'absolute -top-16 -left-2 right-0 bottom-0 -z-10',

View File

@ -58,7 +58,7 @@ export type AppStateContextType = {
setIsPerformingSelectEdit?: Dispatch<SetStateAction<boolean>>
insightsIndicatorStatus?: InsightsIndicatorStatus
// UPLOAD
startUpload?: (onStart?: () => void) => void
startUpload?: () => Promise<boolean>
uploadInputRef?: RefObject<HTMLInputElement | null>
uploadState: UploadState
setUploadState?: (uploadState: Partial<UploadState>) => void