Refactor admin AI/sync buttons

This commit is contained in:
Sam Becker 2024-05-29 14:13:48 -05:00
parent 2e3d92885c
commit 2da60e68c1
10 changed files with 98 additions and 63 deletions

View File

@ -11,8 +11,6 @@ import { AiOutlineEyeInvisible } from 'react-icons/ai';
import PhotoDate from '@/photo/PhotoDate';
import FormWithConfirm from '@/components/FormWithConfirm';
import EditButton from './EditButton';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import IconGrSync from '@/site/IconGrSync';
import DeleteButton from './DeleteButton';
import {
deletePhotoFormAction,
@ -20,6 +18,7 @@ import {
} from '@/photo/actions';
import { useAppState } from '@/state/AppState';
import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
import PhotoSyncButton from './PhotoSyncButton';
export default function AdminPhotosTable({
photos,
@ -82,25 +81,15 @@ export default function AdminPhotosTable({
'gap-2 sm:gap-3 items-center',
)}>
<EditButton path={pathForAdminPhotoEdit(photo)} />
<FormWithConfirm
<PhotoSyncButton
action={syncPhotoExifDataAction}
confirmText={
'Are you sure you want to overwrite EXIF data ' +
`for "${titleForPhoto(photo)}" from source file? ` +
'This action cannot be undone.'
}
>
<input type="hidden" name="id" value={photo.id} />
<SubmitButtonWithStatus
icon={<IconGrSync
className="translate-x-[1px] translate-y-[0.5px]"
/>}
onFormSubmitToastMessage={`
"${titleForPhoto(photo)}" EXIF data synced
`}
onFormSubmit={invalidateSwr}
/>
</FormWithConfirm>
photoTitle={titleForPhoto(photo)}
formData={{ photoId: photo.id }}
onFormSubmit={invalidateSwr}
includeLabel={false}
shouldConfirm
shouldToast
/>
<FormWithConfirm
action={deletePhotoFormAction}
confirmText={deleteConfirmationTextForPhoto(photo)}

View File

@ -0,0 +1,58 @@
import FormWithConfirm from '@/components/FormWithConfirm';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import IconGrSync from '@/site/IconGrSync';
import { clsx } from 'clsx/lite';
import { ComponentProps } from 'react';
export default function PhotoSyncButton({
action,
includeLabel = true,
onFormSubmit,
formData: { photoId, photoUrl } = {},
photoTitle,
shouldConfirm,
shouldToast,
}: {
action: (formData: FormData) => void
includeLabel?: boolean
formData?: {
photoId?: string
photoUrl?: string
}
photoTitle?: string
shouldConfirm?: boolean
shouldToast?: boolean
} & ComponentProps<typeof SubmitButtonWithStatus>) {
const confirmText =
'Are you sure you want to overwrite EXIF data ' + (photoTitle
? `for "${photoTitle}" from source file? `
: 'from source file? '
) + 'This action cannot be undone.';
return (
<FormWithConfirm
action={action}
confirmText={shouldConfirm ? confirmText : undefined}
>
{photoId &&
<input name="id" value={photoId} hidden readOnly />}
{photoUrl &&
<input name="photoUrl" value={photoUrl} hidden readOnly />}
<SubmitButtonWithStatus
title="Update photo from original file"
icon={<IconGrSync
className={clsx(
'translate-y-[0.5px] translate-x-[0.5px]',
includeLabel && 'sm:translate-x-[-0.5px]',
)} />}
onFormSubmitToastMessage={shouldToast
? photoTitle
? `"${photoTitle}" EXIF data synced`
: 'EXIF data synced'
: undefined}
onFormSubmit={onFormSubmit}
>
{includeLabel ? 'EXIF' : null}
</SubmitButtonWithStatus>
</FormWithConfirm>
);
}

View File

@ -8,8 +8,8 @@ export default function FormWithConfirm({
onSubmit,
children,
}: {
action: (data: FormData) => Promise<void>
confirmText: string
action: (formData: FormData) => void
confirmText?: string
onSubmit?: () => void
children: ReactNode
}) {
@ -17,11 +17,11 @@ export default function FormWithConfirm({
<form
action={action}
onSubmit={e => {
if (!confirm(confirmText)) {
e.preventDefault();
} else {
if (!confirmText || confirm(confirmText)) {
e.currentTarget.requestSubmit();
onSubmit?.();
} else {
e.preventDefault();
}
}}
>

View File

@ -1,16 +1,12 @@
'use client';
import { HTMLProps, useEffect, useRef } from 'react';
import { ComponentProps, useEffect, useRef } from 'react';
import { useFormStatus } from 'react-dom';
import { SpinnerColor } from './Spinner';
import { clsx } from 'clsx/lite';
import { toastSuccess } from '@/toast';
import LoaderButton from '@/components/primitives/LoaderButton';
interface Props extends HTMLProps<HTMLButtonElement> {
icon?: JSX.Element
styleAsLink?: boolean
spinnerColor?: SpinnerColor
interface Props extends ComponentProps<typeof LoaderButton> {
onFormStatusChange?: (pending: boolean) => void
onFormSubmitToastMessage?: string
onFormSubmit?: () => void
@ -19,7 +15,7 @@ interface Props extends HTMLProps<HTMLButtonElement> {
export default function SubmitButtonWithStatus({
icon,
styleAsLink,
styleAs,
spinnerColor,
onFormStatusChange,
onFormSubmitToastMessage,
@ -36,7 +32,7 @@ export default function SubmitButtonWithStatus({
const pendingPrevious = useRef(pending);
useEffect(() => {
if (pending && !pendingPrevious.current) {
if (!pending && pendingPrevious.current) {
if (onFormSubmitToastMessage) {
toastSuccess(onFormSubmitToastMessage);
}
@ -60,7 +56,7 @@ export default function SubmitButtonWithStatus({
)}
icon={icon}
spinnerColor={spinnerColor}
styleAs={styleAsLink ? 'link' : undefined}
styleAs={styleAs}
isLoading={pending}
{...buttonProps}
>

View File

@ -3,12 +3,12 @@ import { clsx } from 'clsx/lite';
import { ButtonHTMLAttributes, ReactNode } from 'react';
export default function LoaderButton(props: {
children?: ReactNode
isLoading?: boolean
icon?: ReactNode
spinnerColor?: SpinnerColor
styleAs?: 'button' | 'link' | 'link-without-hover'
hideTextOnMobile?: boolean
shouldPreventDefault?: boolean
} & ButtonHTMLAttributes<HTMLButtonElement>) {
const {
children,
@ -17,7 +17,9 @@ export default function LoaderButton(props: {
spinnerColor,
styleAs = 'button',
hideTextOnMobile = true,
shouldPreventDefault,
type = 'button',
onClick,
disabled,
className,
...rest
@ -27,6 +29,10 @@ export default function LoaderButton(props: {
<button
{...rest}
type={type}
onClick={e => {
if (shouldPreventDefault) { e.preventDefault(); }
onClick?.(e);
}}
className={clsx(
...(styleAs !== 'button'
? [

View File

@ -59,8 +59,7 @@ export default function PathLoaderButton({
<LoaderButton
icon={icon}
className={className}
onClick={e => {
if (shouldPreventDefault) { e.preventDefault(); }
onClick={() => {
startTransition(() => {
if (shouldReplace) {
router.replace(path, { scroll: shouldScroll });
@ -69,6 +68,7 @@ export default function PathLoaderButton({
}
});
}}
shouldPreventDefault={shouldPreventDefault}
isLoading={shouldShowLoader}
spinnerColor={spinnerColor}
styleAs={styleAs}

View File

@ -3,7 +3,6 @@
import AdminChildPage from '@/components/AdminChildPage';
import { Photo } from '.';
import { PATH_ADMIN_PHOTOS } from '@/site/paths';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import {
PhotoFormData,
convertPhotoToFormData,
@ -11,11 +10,11 @@ import {
import PhotoForm from './form/PhotoForm';
import { useFormState } from 'react-dom';
import { areSimpleObjectsEqual } from '@/utility/object';
import IconGrSync from '@/site/IconGrSync';
import { getExifDataAction } from './actions';
import { TagsWithMeta } from '@/tag';
import AiButton from './ai/AiButton';
import usePhotoFormParent from './form/usePhotoFormParent';
import PhotoSyncButton from '@/admin/PhotoSyncButton';
export default function PhotoEditPageClient({
photo,
@ -69,16 +68,10 @@ export default function PhotoEditPageClient({
<div className="flex gap-2">
{hasAiTextGeneration &&
<AiButton {...{ aiContent, shouldConfirm: hasTextContent }} />}
<form action={action}>
<input name="photoUrl" value={photo.url} hidden readOnly />
<SubmitButtonWithStatus
icon={<IconGrSync
className="translate-y-[-1px] sm:mr-[4px]"
/>}
>
EXIF
</SubmitButtonWithStatus>
</form>
<PhotoSyncButton
action={action}
formData={{ photoUrl: photo.url }}
/>
</div>}
isLoading={pending}
>

View File

@ -1,9 +1,8 @@
import Spinner from '@/components/Spinner';
import { AiContent } from './useAiImageQueries';
import { HiSparkles } from 'react-icons/hi';
import { ALL_AI_AUTO_GENERATED_FIELDS, AiAutoGeneratedField } from '.';
import { useMemo } from 'react';
import { clsx } from 'clsx/lite';
import LoaderButton from '@/components/primitives/LoaderButton';
export default function AiButton({
aiContent,
@ -40,12 +39,9 @@ export default function AiButton({
]);
return (
<button
type="button"
className={clsx(
'flex min-w-[3.25rem] min-h-9 justify-center',
className,
)}
<LoaderButton
icon={<HiSparkles size={16} />}
className={className}
onClick={e => {
if (
!shouldConfirm ||
@ -56,9 +52,7 @@ export default function AiButton({
e.preventDefault();
}
}}
disabled={isLoading}
>
{isLoading ? <Spinner /> : <HiSparkles size={16} />}
</button>
isLoading={isLoading}
/>
);
}

View File

@ -69,5 +69,4 @@ export const cleanUpAiTextResponse = (text?: string) => text
.replaceAll('\n', ' ')
.replaceAll('"', '')
.replace(/\.$/, '')
.trim()
: undefined;

View File

@ -50,7 +50,7 @@ export default function Footer() {
</div>
<form action={() => signOutAndRedirectAction()
.then(() => setUserEmail?.(undefined))}>
<SubmitButtonWithStatus styleAsLink>
<SubmitButtonWithStatus styleAs="link">
Sign out
</SubmitButtonWithStatus>
</form>