Refine photo tag control

This commit is contained in:
Sam Becker 2024-07-20 15:58:09 -05:00
parent 235d73db3f
commit dc8dedd806
6 changed files with 121 additions and 74 deletions

View File

@ -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<SetStateAction<UrlAddStatus[]>>
}) {
const divRef = useRef<HTMLDivElement>(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}
/>
</div>
<div
ref={divRef}
className={showTags && !actionErrorMessage ? undefined : 'hidden'}
>
<FieldSetWithStatus
id="tags"
label="Optional Tags"
tagOptions={convertTagsForForm(uniqueTags)}
value={tags}
onChange={tags => {
setTags(tags);
setTagErrorMessage(getValidationMessageForTags(tags) ?? '');
}}
{showTags && !actionErrorMessage &&
<PhotoTagFieldset
tags={tags}
tagOptions={uniqueTags}
onChange={setTags}
onError={setTagErrorMessage}
readOnly={isAdding}
error={tagErrorMessage}
required={false}
openOnLoad
hideLabel
/>
</div>
/>}
<div className="space-y-2">
<ProgressButton
primary

View File

@ -30,7 +30,7 @@ export default function AdminAppMenu() {
className="text-[18px] translate-y-[-0.5px]"
/>
: <ImCheckboxUnchecked
className="text-[0.75rem]"
className="text-[0.75rem] translate-y-[-0.5px]"
/>,
href: PATH_GRID_INFERRED,
action: () => {

View File

@ -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 (
<AdminBatchEditPanelClient {...{ existingTags }} />
<AdminBatchEditPanelClient {...{ uniqueTags }} />
);
}

View File

@ -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<string>();
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({
? <>
<LoaderButton
className="min-h-[2.5rem]"
onClick={() => setTags(undefined)}
onClick={() => {
setTags(undefined);
setTagErrorMessage('');
}}
>
Cancel
</LoaderButton>
@ -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({
)
? <SiteGrid
className="sticky top-0 z-10 mb-5 -mt-2 pt-2"
contentMain={<Note
color="gray"
className={clsx(
'min-h-[3.5rem]',
'backdrop-blur-lg !border-transparent',
'!text-gray-900 dark:!text-gray-100',
'!bg-gray-100/90 dark:!bg-gray-900/70',
)}
padding={isTagging ? 'tight-cta-right-left' : 'tight-cta-right'}
cta={<div className="flex items-center gap-2.5">
{renderActions()}
</div>}
spaceChildren={false}
hideIcon
>
{isTagging
? <TagInput
name="tags"
value={tags}
options={convertTagsForForm(existingTags)}
onChange={setTags}
placeholder={`Tag ${selectedPhotoIds?.length} ${photosPlural} ...`}
className={clsx(
Boolean(tagValidationMessage) && 'error',
)}
/>
: <div className="text-base">
{renderPhotoText()}
contentMain={<div className="flex flex-col gap-2">
<Note
color="gray"
className={clsx(
'min-h-[3.5rem]',
'backdrop-blur-lg !border-transparent',
'!text-gray-900 dark:!text-gray-100',
'!bg-gray-100/90 dark:!bg-gray-900/70',
)}
padding={isTagging ? 'tight-cta-right-left' : 'tight-cta-right'}
cta={<div className="flex items-center gap-2.5">
{renderActions()}
</div>}
</Note>} />
spaceChildren={false}
hideIcon
>
{isTagging
? <PhotoTagFieldset
tags={tags}
tagOptions={uniqueTags}
placeholder={
`Tag ${selectedPhotoIds?.length} ${photosPlural} ...`
}
onChange={setTags}
onError={setTagErrorMessage}
openOnLoad
hideLabel
/>
: <div className="text-base">
{renderPhotoText()}
</div>}
</Note>
{tagErrorMessage &&
<div className="text-error pl-4">
{tagErrorMessage}
</div>}
</div>} />
: null;
}

View File

@ -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<Omit<
ComponentProps<typeof FieldSetWithStatus>,
'tagOptions'
>>) {
const {
id,
tags,
tagOptions,
onChange,
onError,
openOnLoad,
...rest
} = props;
const ref = useRef<HTMLInputElement>(null);
const [errorMessageLocal, setErrorMessageLocal] = useState('');
useEffect(() => {
if (openOnLoad) {
const timeout = setTimeout(() => {
ref.current?.querySelectorAll('input')[0]?.focus();
}, 100);
return () => clearTimeout(timeout);
}
}, [openOnLoad]);
return (
<div ref={ref}>
<FieldSetWithStatus
{...rest}
inputRef={ref}
id={id ?? 'tags'}
value={tags}
tagOptions={convertTagsForForm(tagOptions)}
onChange={tags => {
onChange(tags);
const validationMessage = getValidationMessageForTags(tags) ?? '';
onError?.(validationMessage);
setErrorMessageLocal(validationMessage);
}}
error={errorMessageLocal}
/>
</div>
);
}

View File

@ -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 &&
<label
className={clsx(
'flex gap-2 items-center select-none',