From dc8dedd8063f5a4263de8f89acedf76c1df05409 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 20 Jul 2024 15:58:09 -0500 Subject: [PATCH] Refine photo tag control --- src/admin/AdminAddAllUploads.tsx | 43 ++++--------- src/admin/AdminAppMenu.tsx | 2 +- src/admin/AdminBatchEditPanel.tsx | 4 +- src/admin/AdminBatchEditPanelClient.tsx | 82 ++++++++++++++----------- src/admin/PhotoTagFieldset.tsx | 58 +++++++++++++++++ src/components/FieldSetWithStatus.tsx | 6 +- 6 files changed, 121 insertions(+), 74 deletions(-) create mode 100644 src/admin/PhotoTagFieldset.tsx diff --git a/src/admin/AdminAddAllUploads.tsx b/src/admin/AdminAddAllUploads.tsx index 26e28028..db3b26ef 100644 --- a/src/admin/AdminAddAllUploads.tsx +++ b/src/admin/AdminAddAllUploads.tsx @@ -5,11 +5,7 @@ import FieldSetWithStatus from '@/components/FieldSetWithStatus'; import Container from '@/components/Container'; import { addAllUploadsAction } from '@/photo/actions'; import { PATH_ADMIN_PHOTOS } from '@/site/paths'; -import { - Tags, - convertTagsForForm, - getValidationMessageForTags, -} from '@/tag'; +import { Tags } from '@/tag'; import { generateLocalNaivePostgresString, generateLocalPostgresString, @@ -22,6 +18,7 @@ import { Dispatch, SetStateAction, useRef, useState } from 'react'; import { BiCheckCircle, BiImageAdd } from 'react-icons/bi'; import ProgressButton from '@/components/primitives/ProgressButton'; import { UrlAddStatus } from './AdminUploadsClient'; +import PhotoTagFieldset from './PhotoTagFieldset'; const UPLOAD_BATCH_SIZE = 4; @@ -38,8 +35,6 @@ export default function AdminAddAllUploads({ setIsAdding: (isAdding: boolean) => void setUrlAddStatuses: Dispatch> }) { - const divRef = useRef(null); - const [buttonText, setButtonText] = useState('Add All Uploads'); const [showTags, setShowTags] = useState(false); const [tags, setTags] = useState(''); @@ -121,36 +116,20 @@ export default function AdminAddAllUploads({ label="Apply tags" type="checkbox" value={showTags ? 'true' : 'false'} - onChange={value => { - setShowTags(value === 'true'); - if (value === 'true') { - setTimeout(() => - divRef.current?.querySelectorAll('input')[0]?.focus() - , 100); - } - }} + onChange={value => setShowTags(value === 'true')} readOnly={isAdding} /> -
- { - setTags(tags); - setTagErrorMessage(getValidationMessageForTags(tags) ?? ''); - }} + {showTags && !actionErrorMessage && + -
+ />}
: , href: PATH_GRID_INFERRED, action: () => { diff --git a/src/admin/AdminBatchEditPanel.tsx b/src/admin/AdminBatchEditPanel.tsx index 708a2e50..7014a5f4 100644 --- a/src/admin/AdminBatchEditPanel.tsx +++ b/src/admin/AdminBatchEditPanel.tsx @@ -2,8 +2,8 @@ import { getUniqueTagsCached } from '@/photo/cache'; import AdminBatchEditPanelClient from './AdminBatchEditPanelClient'; export default async function AdminBatchEditPanel() { - const existingTags = await getUniqueTagsCached(); + const uniqueTags = await getUniqueTagsCached(); return ( - + ); } diff --git a/src/admin/AdminBatchEditPanelClient.tsx b/src/admin/AdminBatchEditPanelClient.tsx index 214f5813..68e81392 100644 --- a/src/admin/AdminBatchEditPanelClient.tsx +++ b/src/admin/AdminBatchEditPanelClient.tsx @@ -8,15 +8,15 @@ import { clsx } from 'clsx/lite'; import { IoCloseSharp } from 'react-icons/io5'; import DeleteButton from './DeleteButton'; import { useState } from 'react'; -import TagInput from '@/components/TagInput'; -import { convertTagsForForm, getValidationMessageForTags, Tags } from '@/tag'; +import { Tags } from '@/tag'; import { usePathname } from 'next/navigation'; import { PATH_GRID_INFERRED } from '@/site/paths'; +import PhotoTagFieldset from './PhotoTagFieldset'; export default function AdminBatchEditPanelClient({ - existingTags, + uniqueTags, }: { - existingTags: Tags + uniqueTags: Tags }) { const pathname = usePathname(); @@ -27,7 +27,7 @@ export default function AdminBatchEditPanelClient({ } = useAppState(); const [tags, setTags] = useState(); - const tagValidationMessage = getValidationMessageForTags(tags); + const [tagErrorMessage, setTagErrorMessage] = useState(''); const isTagging = tags !== undefined; const photosPlural = selectedPhotoIds?.length === 1 ? 'photo' : 'photos'; @@ -40,7 +40,10 @@ export default function AdminBatchEditPanelClient({ ? <> setTags(undefined)} + onClick={() => { + setTags(undefined); + setTagErrorMessage(''); + }} > Cancel @@ -48,7 +51,7 @@ export default function AdminBatchEditPanelClient({ className="min-h-[2.5rem]" // eslint-disable-next-line max-len confirmText={`Are you sure you want to apply tags to ${selectedPhotoIds?.length} ${photosPlural}? This action cannot be undone.`} - disabled={!tags || Boolean(tagValidationMessage)} + disabled={!tags || Boolean(tagErrorMessage)} primary > Apply Tags @@ -75,35 +78,42 @@ export default function AdminBatchEditPanelClient({ ) ? - {renderActions()} -
} - spaceChildren={false} - hideIcon - > - {isTagging - ? - :
- {renderPhotoText()} + contentMain={
+ + {renderActions()}
} - } /> + spaceChildren={false} + hideIcon + > + {isTagging + ? + :
+ {renderPhotoText()} +
} + + {tagErrorMessage && +
+ {tagErrorMessage} +
} +
} /> : null; } diff --git a/src/admin/PhotoTagFieldset.tsx b/src/admin/PhotoTagFieldset.tsx new file mode 100644 index 00000000..ce932924 --- /dev/null +++ b/src/admin/PhotoTagFieldset.tsx @@ -0,0 +1,58 @@ +'use client'; + +import FieldSetWithStatus from '@/components/FieldSetWithStatus'; +import { convertTagsForForm, getValidationMessageForTags, Tags } from '@/tag'; +import { ComponentProps, useEffect, useRef, useState } from 'react'; + +export default function PhotoTagFieldset(props: { + tags: string + tagOptions?: Tags + onChange: (tags: string) => void + onError?: (error: string) => void + openOnLoad?: boolean +} & Partial, + 'tagOptions' +>>) { + const { + id, + tags, + tagOptions, + onChange, + onError, + openOnLoad, + ...rest + } = props; + + const ref = useRef(null); + + const [errorMessageLocal, setErrorMessageLocal] = useState(''); + + useEffect(() => { + if (openOnLoad) { + const timeout = setTimeout(() => { + ref.current?.querySelectorAll('input')[0]?.focus(); + }, 100); + return () => clearTimeout(timeout); + } + }, [openOnLoad]); + + return ( +
+ { + onChange(tags); + const validationMessage = getValidationMessageForTags(tags) ?? ''; + onError?.(validationMessage); + setErrorMessageLocal(validationMessage); + }} + error={errorMessageLocal} + /> +
+ ); +} diff --git a/src/components/FieldSetWithStatus.tsx b/src/components/FieldSetWithStatus.tsx index 89ad3570..ef763d50 100644 --- a/src/components/FieldSetWithStatus.tsx +++ b/src/components/FieldSetWithStatus.tsx @@ -29,7 +29,7 @@ export default function FieldSetWithStatus({ hideLabel, }: { id: string - label: string + label?: string note?: string error?: string value: string @@ -37,7 +37,7 @@ export default function FieldSetWithStatus({ onChange?: (value: string) => void selectOptions?: { value: string, label: string }[] selectOptionsDefaultLabel?: string - tagOptions?: AnnotatedTag [] + tagOptions?: AnnotatedTag[] placeholder?: string loading?: boolean required?: boolean @@ -55,7 +55,7 @@ export default function FieldSetWithStatus({ 'space-y-1', type === 'checkbox' && 'flex items-center gap-2', )}> - {!hideLabel && + {!hideLabel && label &&