Make batch edits more discoverable

This commit is contained in:
Sam Becker 2025-03-17 20:43:57 -05:00
parent ed0fe9cae9
commit 34a8f2f362
6 changed files with 51 additions and 72 deletions

View File

@ -11,13 +11,12 @@ import {
PATH_GRID_INFERRED, PATH_GRID_INFERRED,
} from '@/app/paths'; } from '@/app/paths';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
import { IoArrowDown, IoArrowUp } from 'react-icons/io5'; import { IoArrowDown, IoArrowUp, IoCloseSharp } from 'react-icons/io5';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import AdminAppInfoIcon from './AdminAppInfoIcon'; import AdminAppInfoIcon from './AdminAppInfoIcon';
import { signOutAction } from '@/auth/actions'; import { signOutAction } from '@/auth/actions';
import { ComponentProps } from 'react'; import { ComponentProps } from 'react';
import useIsKeyBeingPressed from '@/utility/useIsKeyBeingPressed'; import useIsKeyBeingPressed from '@/utility/useIsKeyBeingPressed';
import IconSelectMultiple from '@/components/icons/IconSelectMultiple';
import IconPhoto from '@/components/icons/IconPhoto'; import IconPhoto from '@/components/icons/IconPhoto';
import IconUpload from '@/components/icons/IconUpload'; import IconUpload from '@/components/icons/IconUpload';
import IconRecipe from '@/components/icons/IconRecipe'; import IconRecipe from '@/components/icons/IconRecipe';
@ -25,6 +24,7 @@ import IconTag from '@/components/icons/IconTag';
import IconFolder from '@/components/icons/IconFolder'; import IconFolder from '@/components/icons/IconFolder';
import IconSignOut from '@/components/icons/IconSignOut'; import IconSignOut from '@/components/icons/IconSignOut';
import IconLock from '@/components/icons/IconLock'; import IconLock from '@/components/icons/IconLock';
import { IoMdCheckboxOutline } from 'react-icons/io';
export default function AdminAppMenu({ export default function AdminAppMenu({
active, active,
@ -121,9 +121,14 @@ export default function AdminAppMenu({
if (photosCountTotal) { if (photosCountTotal) {
items.push({ items.push({
label: isSelecting label: isSelecting
? 'Exit Select' ? 'Exit Batch Edit'
: 'Edit Multiple', : 'Batch Edit ...',
icon: <IconSelectMultiple {...{ isSelecting }} />, icon: isSelecting
? <IoCloseSharp
size={18}
className="translate-x-[-1px] translate-y-[0.5px]"
/>
: <IoMdCheckboxOutline size={17} className="translate-x-[-0.5px]" />,
href: PATH_GRID_INFERRED, href: PATH_GRID_INFERRED,
action: () => { action: () => {
if (isSelecting) { if (isSelecting) {

View File

@ -51,6 +51,10 @@ export default function AdminBatchEditPanelClient({
false, false,
); );
const isFormDisabled =
isPerformingSelectEdit ||
selectedPhotoIds?.length === 0;
const renderPhotoCTA = () => selectedPhotoIds?.length === 0 const renderPhotoCTA = () => selectedPhotoIds?.length === 0
? <> ? <>
<FaArrowDown /> <FaArrowDown />
@ -105,40 +109,37 @@ export default function AdminBatchEditPanelClient({
</LoaderButton> </LoaderButton>
</> </>
: <> : <>
{(selectedPhotoIds?.length ?? 0) > 0 && <DeletePhotosButton
<> photoIds={selectedPhotoIds}
<DeletePhotosButton disabled={isFormDisabled}
photoIds={selectedPhotoIds} onClick={() => setIsPerformingSelectEdit?.(true)}
disabled={isPerformingSelectEdit} onDelete={resetForm}
onClick={() => setIsPerformingSelectEdit?.(true)} onFinish={() => setIsPerformingSelectEdit?.(false)}
onDelete={resetForm} />
onFinish={() => setIsPerformingSelectEdit?.(false)} <LoaderButton
/> icon={<IconFavs />}
<LoaderButton disabled={isFormDisabled}
icon={<IconFavs />} confirmText={`Are you sure you want to favorite ${photosText}?`}
disabled={isPerformingSelectEdit} onClick={() => {
confirmText={`Are you sure you want to favorite ${photosText}?`} setIsPerformingSelectEdit?.(true);
onClick={() => { tagMultiplePhotosAction(
setIsPerformingSelectEdit?.(true); TAG_FAVS,
tagMultiplePhotosAction( selectedPhotoIds ?? [],
TAG_FAVS, )
selectedPhotoIds ?? [], .then(() => {
) toastSuccess(`${photosText} favorited`);
.then(() => { resetForm();
toastSuccess(`${photosText} favorited`); })
resetForm(); .finally(() => setIsPerformingSelectEdit?.(false));
}) }}
.finally(() => setIsPerformingSelectEdit?.(false)); />
}} <LoaderButton
/> onClick={() => setTags('')}
<LoaderButton disabled={isFormDisabled}
onClick={() => setTags('')} icon={<IconTag size={15} className="translate-y-[1.5px]" />}
disabled={isPerformingSelectEdit} >
icon={<IconTag size={15} className="translate-y-[1.5px]" />} Tag ...
> </LoaderButton>
Tag ...
</LoaderButton>
</>}
<LoaderButton <LoaderButton
icon={<IoCloseSharp size={19} />} icon={<IoCloseSharp size={19} />}
onClick={() => setSelectedPhotoIds?.(undefined)} onClick={() => setSelectedPhotoIds?.(undefined)}

View File

@ -18,8 +18,9 @@ export default function DeleteButton({
'active:bg-red-100/50! dark:active:bg-red-950/50!', 'active:bg-red-100/50! dark:active:bg-red-950/50!',
'disabled:text-red-500/60! dark:disabled:text-red-500/60!', 'disabled:text-red-500/60! dark:disabled:text-red-500/60!',
'disabled:bg-red-100/50! dark:disabled:bg-red-950/50!', 'disabled:bg-red-100/50! dark:disabled:bg-red-950/50!',
'border-red-200! hover:border-red-300!', 'border-red-200! disabled:border-red-200! hover:border-red-300!',
'dark:border-red-900/75! dark:hover:border-red-900!', // eslint-disable-next-line max-len
'dark:border-red-900/75! dark:disabled:border-red-900/75! dark:hover:border-red-900!',
className, className,
)} )}
/> />

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import Checkbox from './primitives/Checkbox'; import SimpleCheckbox from './primitives/SimpleCheckbox';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
import Spinner from './Spinner'; import Spinner from './Spinner';
@ -45,7 +45,7 @@ export default function SelectTileOverlay({
className="m-[1px]" className="m-[1px]"
/> />
: null : null
: <Checkbox : <SimpleCheckbox
className={clsx( className={clsx(
'text-white', 'text-white',
// Required to prevent Safari jitter // Required to prevent Safari jitter

View File

@ -1,28 +0,0 @@
import clsx from 'clsx/lite';
import { IconBaseProps } from 'react-icons';
import { ImCheckboxUnchecked } from 'react-icons/im';
import { IoCloseSharp } from 'react-icons/io5';
export default function IconSelectMultiple({
isSelecting,
className,
...props
}: IconBaseProps & { isSelecting: boolean }) {
return isSelecting
? <IoCloseSharp {...{
...props,
className: clsx(
'text-[18px] translate-x-[-1px] translate-y-[0.5px]',
className,
),
}} />
: <ImCheckboxUnchecked
{...{
...props,
className: clsx(
'translate-x-[-0.5px] translate-y-[0.5px] text-[0.75rem]',
className,
),
}}
/>;
}

View File

@ -4,7 +4,7 @@ import { ImCheckboxUnchecked, ImCheckboxChecked } from 'react-icons/im';
const ICON_CLASS_NAME = 'text-[1rem]'; const ICON_CLASS_NAME = 'text-[1rem]';
export default function Checkbox(props: { export default function SimpleCheckbox(props: {
children?: ReactNode children?: ReactNode
} & InputHTMLAttributes<HTMLInputElement>) { } & InputHTMLAttributes<HTMLInputElement>) {
const { const {