Refactor admin AI/sync buttons
This commit is contained in:
parent
2e3d92885c
commit
2da60e68c1
@ -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)}
|
||||
|
||||
58
src/admin/PhotoSyncButton.tsx
Normal file
58
src/admin/PhotoSyncButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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'
|
||||
? [
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -69,5 +69,4 @@ export const cleanUpAiTextResponse = (text?: string) => text
|
||||
.replaceAll('\n', ' ')
|
||||
.replaceAll('"', '')
|
||||
.replace(/\.$/, '')
|
||||
.trim()
|
||||
: undefined;
|
||||
|
||||
@ -50,7 +50,7 @@ export default function Footer() {
|
||||
</div>
|
||||
<form action={() => signOutAndRedirectAction()
|
||||
.then(() => setUserEmail?.(undefined))}>
|
||||
<SubmitButtonWithStatus styleAsLink>
|
||||
<SubmitButtonWithStatus styleAs="link">
|
||||
Sign out
|
||||
</SubmitButtonWithStatus>
|
||||
</form>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user