Finalize sync/cleanup ux
This commit is contained in:
parent
aec9748d9a
commit
787f638cd7
95
src/admin/AdminOutdatedClient.tsx
Normal file
95
src/admin/AdminOutdatedClient.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
'use client';
|
||||
|
||||
import { OUTDATED_THRESHOLD, Photo } from '@/photo';
|
||||
import AdminPhotosTable from '@/admin/AdminPhotosTable';
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import IconGrSync from '@/site/IconGrSync';
|
||||
import Banner from '@/components/Banner';
|
||||
import AdminChildPage from '@/components/AdminChildPage';
|
||||
import { PATH_ADMIN_PHOTOS } from '@/site/paths';
|
||||
import { useState } from 'react';
|
||||
import { syncPhotosAction } from '@/photo/actions';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const UPDATE_BATCH_SIZE = 4;
|
||||
|
||||
export default function AdminOutdatedClient({
|
||||
photos,
|
||||
hasAiTextGeneration,
|
||||
}: {
|
||||
photos: Photo[]
|
||||
hasAiTextGeneration: boolean
|
||||
}) {
|
||||
const [photoIdsSyncing, setPhotoIdsSyncing] = useState<string[]>([]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<AdminChildPage
|
||||
backLabel="Photos"
|
||||
backPath={PATH_ADMIN_PHOTOS}
|
||||
breadcrumb={<>
|
||||
<span className="hidden sm:inline-block">
|
||||
Outdated ({photos.length})
|
||||
</span>
|
||||
<span className="sm:hidden">
|
||||
Outdated
|
||||
</span>
|
||||
</>}
|
||||
accessory={<LoaderButton
|
||||
icon={<IconGrSync className="translate-y-[1px]" />}
|
||||
hideTextOnMobile={false}
|
||||
className="primary"
|
||||
onClick={async () => {
|
||||
// eslint-disable-next-line max-len
|
||||
if (window.confirm(`Are you sure you want to sync the oldest ${UPDATE_BATCH_SIZE} photos?`)) {
|
||||
const photosToSync = photos
|
||||
.slice(0, UPDATE_BATCH_SIZE)
|
||||
.map(photo => photo.id);
|
||||
setPhotoIdsSyncing(photosToSync);
|
||||
syncPhotosAction(photosToSync)
|
||||
.finally(() => {
|
||||
setPhotoIdsSyncing([]);
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="hidden sm:inline-block">
|
||||
Sync {UPDATE_BATCH_SIZE} Oldest Photos
|
||||
</span>
|
||||
<span className="sm:hidden">
|
||||
Sync {UPDATE_BATCH_SIZE} Oldest
|
||||
</span>
|
||||
</LoaderButton>}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<Banner>
|
||||
<div className="space-y-1.5">
|
||||
{photos.length}
|
||||
{' '}
|
||||
{photos.length === 1 ? 'photo' : 'photos'}
|
||||
{' ('}uploaded before
|
||||
{' '}
|
||||
{new Date(OUTDATED_THRESHOLD).toLocaleDateString()}{')'}
|
||||
{' '}
|
||||
may have: missing EXIF fields, inaccurate blur data,
|
||||
{' '}
|
||||
undesired privacy settings
|
||||
{hasAiTextGeneration && ', missing AI-generated text'}
|
||||
</div>
|
||||
</Banner>
|
||||
<div className="space-y-4">
|
||||
<AdminPhotosTable
|
||||
photos={photos}
|
||||
photoIdsSyncing={photoIdsSyncing}
|
||||
hasAiTextGeneration={hasAiTextGeneration}
|
||||
canEdit={false}
|
||||
canDelete={false}
|
||||
showUpdatedAt
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AdminChildPage>
|
||||
);
|
||||
}
|
||||
@ -12,7 +12,7 @@ import { PATH_ADMIN_OUTDATED } from '@/site/paths';
|
||||
import { Photo } from '@/photo';
|
||||
import { StorageListResponse } from '@/services/storage';
|
||||
import { useState } from 'react';
|
||||
import { FaRegClock } from 'react-icons/fa6';
|
||||
import { LiaBroomSolid } from 'react-icons/lia';
|
||||
|
||||
export default function AdminPhotosClient({
|
||||
photos,
|
||||
@ -48,7 +48,7 @@ export default function AdminPhotosClient({
|
||||
</div>
|
||||
{photosCountOutdated > 0 && <PathLoaderButton
|
||||
path={PATH_ADMIN_OUTDATED}
|
||||
icon={<FaRegClock size={15} className="translate-y-[1px]" />}
|
||||
icon={<LiaBroomSolid size={18} className="translate-y-[-1px]" />}
|
||||
title={`${photosCountOutdated} Outdated Photos`}
|
||||
className={clsx(
|
||||
isUploading && 'hidden md:inline-flex',
|
||||
|
||||
@ -12,10 +12,7 @@ import PhotoDate from '@/photo/PhotoDate';
|
||||
import FormWithConfirm from '@/components/FormWithConfirm';
|
||||
import EditButton from './EditButton';
|
||||
import DeleteButton from './DeleteButton';
|
||||
import {
|
||||
deletePhotoFormAction,
|
||||
syncPhotoAction,
|
||||
} from '@/photo/actions';
|
||||
import { deletePhotoFormAction } from '@/photo/actions';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
|
||||
import PhotoSyncButton from './PhotoSyncButton';
|
||||
@ -24,21 +21,28 @@ export default function AdminPhotosTable({
|
||||
photos,
|
||||
onLastPhotoVisible,
|
||||
revalidatePhoto,
|
||||
photoIdsSyncing = [],
|
||||
hasAiTextGeneration,
|
||||
showCreatedAt,
|
||||
showUpdatedAt,
|
||||
canEdit = true,
|
||||
canDelete = true,
|
||||
}: {
|
||||
photos: Photo[],
|
||||
onLastPhotoVisible?: () => void
|
||||
revalidatePhoto?: RevalidatePhoto
|
||||
hasAiTextGeneration?: boolean
|
||||
showCreatedAt?: boolean
|
||||
photoIdsSyncing?: string[]
|
||||
hasAiTextGeneration: boolean
|
||||
showUpdatedAt?: boolean
|
||||
canEdit?: boolean
|
||||
canDelete?: boolean
|
||||
}) {
|
||||
const { invalidateSwr } = useAppState();
|
||||
|
||||
const opacityForPhotoId = (photoId: string) =>
|
||||
photoIdsSyncing.length > 0 && !photoIdsSyncing.includes(photoId)
|
||||
? 'opacity-40'
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<AdminTable>
|
||||
{photos.map((photo, index) =>
|
||||
@ -48,8 +52,12 @@ export default function AdminPhotosTable({
|
||||
onVisible={index === photos.length - 1
|
||||
? onLastPhotoVisible
|
||||
: undefined}
|
||||
className={opacityForPhotoId(photo.id)}
|
||||
/>
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<div className={clsx(
|
||||
'flex flex-col lg:flex-row',
|
||||
opacityForPhotoId(photo.id),
|
||||
)}>
|
||||
<Link
|
||||
key={photo.id}
|
||||
href={pathForPhoto({ photo })}
|
||||
@ -81,9 +89,10 @@ export default function AdminPhotosTable({
|
||||
'lg:w-[50%] uppercase',
|
||||
'text-dim',
|
||||
)}>
|
||||
{showCreatedAt
|
||||
? <PhotoDate {...{ photo, dateType: 'createdAt' }} />
|
||||
: <PhotoDate {...{ photo }} />}
|
||||
<PhotoDate {...{
|
||||
photo,
|
||||
dateType: showUpdatedAt ? 'updatedAt' : undefined,
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(
|
||||
@ -93,11 +102,13 @@ export default function AdminPhotosTable({
|
||||
{canEdit &&
|
||||
<EditButton path={pathForAdminPhotoEdit(photo)} />}
|
||||
<PhotoSyncButton
|
||||
action={syncPhotoAction}
|
||||
photoId={photo.id}
|
||||
photoTitle={titleForPhoto(photo)}
|
||||
formData={{ photoId: photo.id }}
|
||||
onFormSubmit={invalidateSwr}
|
||||
onSyncComplete={invalidateSwr}
|
||||
isSyncingExternal={photoIdsSyncing.includes(photo.id)}
|
||||
hasAiTextGeneration={hasAiTextGeneration}
|
||||
disabled={photoIdsSyncing.length > 0}
|
||||
className={opacityForPhotoId(photo.id)}
|
||||
shouldConfirm
|
||||
shouldToast
|
||||
/>
|
||||
|
||||
37
src/admin/ExifSyncButton.tsx
Normal file
37
src/admin/ExifSyncButton.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
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 ExifSyncButton({
|
||||
action,
|
||||
label,
|
||||
onFormSubmit,
|
||||
photoUrl,
|
||||
className,
|
||||
}: {
|
||||
action: (formData: FormData) => void
|
||||
label?: string
|
||||
photoUrl?: string
|
||||
} & ComponentProps<typeof SubmitButtonWithStatus>) {
|
||||
return (
|
||||
<FormWithConfirm
|
||||
action={action}
|
||||
className={className}
|
||||
>
|
||||
<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]',
|
||||
label && 'sm:translate-x-[-0.5px]',
|
||||
)} />}
|
||||
onFormSubmit={onFormSubmit}
|
||||
>
|
||||
{label}
|
||||
</SubmitButtonWithStatus>
|
||||
</FormWithConfirm>
|
||||
);
|
||||
}
|
||||
@ -1,61 +1,61 @@
|
||||
import FormWithConfirm from '@/components/FormWithConfirm';
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import { syncPhotoAction } from '@/photo/actions';
|
||||
import IconGrSync from '@/site/IconGrSync';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { ComponentProps } from 'react';
|
||||
import { toastSuccess } from '@/toast';
|
||||
import { ComponentProps, useState } from 'react';
|
||||
|
||||
export default function PhotoSyncButton({
|
||||
action,
|
||||
label,
|
||||
onFormSubmit,
|
||||
formData: { photoId, photoUrl } = {},
|
||||
photoId,
|
||||
photoTitle,
|
||||
onSyncComplete,
|
||||
className,
|
||||
isSyncingExternal,
|
||||
hasAiTextGeneration,
|
||||
disabled,
|
||||
shouldConfirm,
|
||||
shouldToast,
|
||||
}: {
|
||||
action: (formData: FormData) => void
|
||||
label?: string
|
||||
formData?: {
|
||||
photoId?: string
|
||||
photoUrl?: string
|
||||
}
|
||||
photoId: string
|
||||
photoTitle?: string
|
||||
onSyncComplete?: () => void
|
||||
isSyncingExternal?: boolean
|
||||
hasAiTextGeneration?: boolean
|
||||
shouldConfirm?: boolean
|
||||
shouldToast?: boolean
|
||||
} & ComponentProps<typeof SubmitButtonWithStatus>) {
|
||||
} & ComponentProps<typeof LoaderButton>) {
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
const confirmText = ['Overwrite'];
|
||||
if (photoTitle) { confirmText.push(`"${photoTitle}"`); }
|
||||
confirmText.push('data from original file?');
|
||||
if (hasAiTextGeneration) { confirmText.push(
|
||||
'AI text will be generated for undefined fields.'); }
|
||||
confirmText.push('This action cannot be undone.');
|
||||
|
||||
return (
|
||||
<FormWithConfirm
|
||||
action={action}
|
||||
confirmText={shouldConfirm ? confirmText.join(' ') : undefined}
|
||||
>
|
||||
{photoId &&
|
||||
<input name="photoId" value={photoId} hidden readOnly />}
|
||||
{photoUrl &&
|
||||
<input name="photoUrl" value={photoUrl} hidden readOnly />}
|
||||
<SubmitButtonWithStatus
|
||||
<LoaderButton
|
||||
title="Update photo from original file"
|
||||
className={className}
|
||||
icon={<IconGrSync
|
||||
className={clsx(
|
||||
'translate-y-[0.5px] translate-x-[0.5px]',
|
||||
label && 'sm:translate-x-[-0.5px]',
|
||||
)} />}
|
||||
onFormSubmitToastMessage={shouldToast
|
||||
? photoTitle
|
||||
className="translate-y-[0.5px] translate-x-[0.5px]"
|
||||
/>}
|
||||
onClick={() => {
|
||||
if (!shouldConfirm || window.confirm(confirmText.join(' '))) {
|
||||
setIsSyncing(true);
|
||||
syncPhotoAction(photoId)
|
||||
.then(() => {
|
||||
onSyncComplete?.();
|
||||
if (shouldToast) {
|
||||
toastSuccess(photoTitle
|
||||
? `"${photoTitle}" data synced`
|
||||
: 'Data synced'
|
||||
: undefined}
|
||||
onFormSubmit={onFormSubmit}
|
||||
>
|
||||
{label}
|
||||
</SubmitButtonWithStatus>
|
||||
</FormWithConfirm>
|
||||
: 'Data synced');
|
||||
}
|
||||
})
|
||||
.finally(() => setIsSyncing(false));
|
||||
}
|
||||
}}
|
||||
isLoading={isSyncing || isSyncingExternal}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,65 +1,20 @@
|
||||
import { AI_TEXT_GENERATION_ENABLED } from '@/site/config';
|
||||
import { getPhotos } from '@/photo/db/query';
|
||||
import AdminPhotosTable from '@/admin/AdminPhotosTable';
|
||||
import { OUTDATED_THRESHOLD } from '@/photo';
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import IconGrSync from '@/site/IconGrSync';
|
||||
import Banner from '@/components/Banner';
|
||||
import AdminChildPage from '@/components/AdminChildPage';
|
||||
import { PATH_ADMIN_PHOTOS } from '@/site/paths';
|
||||
import AdminOutdatedClient from '@/admin/AdminOutdatedClient';
|
||||
import { AI_TEXT_GENERATION_ENABLED } from '@/site/config';
|
||||
|
||||
const UPDATE_BATCH_SIZE = 5;
|
||||
|
||||
export default async function AdminPhotosPage() {
|
||||
export default async function AdminOutdatedPage() {
|
||||
const photos = await getPhotos({
|
||||
hidden: 'include',
|
||||
sortBy: 'createdAtAsc',
|
||||
takenBefore: OUTDATED_THRESHOLD,
|
||||
updatedBefore: OUTDATED_THRESHOLD,
|
||||
limit: 1_000,
|
||||
}).catch(() => []);
|
||||
|
||||
return (
|
||||
<AdminChildPage
|
||||
backLabel="Photos"
|
||||
backPath={PATH_ADMIN_PHOTOS}
|
||||
breadcrumb={`Outdated (${photos.length})`}
|
||||
accessory={<LoaderButton
|
||||
icon={<IconGrSync className="translate-y-[1px]" />}
|
||||
hideTextOnMobile={false}
|
||||
className="primary"
|
||||
>
|
||||
<span className="hidden sm:inline-block">
|
||||
Sync Oldest {UPDATE_BATCH_SIZE} Photos
|
||||
</span>
|
||||
<span className="sm:hidden">
|
||||
Sync Oldest
|
||||
</span>
|
||||
</LoaderButton>}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<Banner>
|
||||
<div className="space-y-1.5">
|
||||
These photos {'('}uploaded before
|
||||
{' '}
|
||||
{new Date(OUTDATED_THRESHOLD).toLocaleDateString()}{')'}
|
||||
{' '}
|
||||
may have: missing EXIF fields, inaccurate blur data,
|
||||
{' '}
|
||||
undesired privacy settings,
|
||||
{' '}
|
||||
and missing AI-generated text.
|
||||
</div>
|
||||
</Banner>
|
||||
<div className="space-y-4">
|
||||
<AdminPhotosTable
|
||||
photos={photos}
|
||||
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED}
|
||||
canEdit={false}
|
||||
canDelete={false}
|
||||
showCreatedAt
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AdminChildPage>
|
||||
<AdminOutdatedClient {...{
|
||||
photos,
|
||||
hasAiTextGeneration: AI_TEXT_GENERATION_ENABLED,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ export default async function AdminPhotosPage() {
|
||||
.catch(() => 0),
|
||||
getPhotosMetaCached({
|
||||
hidden: 'include',
|
||||
takenBefore: OUTDATED_THRESHOLD,
|
||||
updatedBefore: OUTDATED_THRESHOLD,
|
||||
})
|
||||
.then(({ count }) => count)
|
||||
.catch(() => 0),
|
||||
|
||||
@ -6,11 +6,13 @@ export default function FormWithConfirm({
|
||||
action,
|
||||
confirmText,
|
||||
onSubmit,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
action: (formData: FormData) => void
|
||||
confirmText?: string
|
||||
onSubmit?: () => void
|
||||
className?: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
@ -24,6 +26,7 @@ export default function FormWithConfirm({
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
|
||||
@ -4,13 +4,18 @@ import { clsx } from 'clsx/lite';
|
||||
export default function ResponsiveDate({
|
||||
date,
|
||||
className,
|
||||
titleLabel,
|
||||
}: {
|
||||
date: Date
|
||||
className?: string
|
||||
titleLabel?: string
|
||||
}) {
|
||||
const title = titleLabel
|
||||
? `${titleLabel}: ${formatDate(date).toLocaleUpperCase()}`
|
||||
: formatDate(date).toLocaleUpperCase();
|
||||
return (
|
||||
<span
|
||||
title={formatDate(date).toLocaleUpperCase()}
|
||||
title={title}
|
||||
className={clsx(className, 'uppercase')}
|
||||
>
|
||||
{/* Small */}
|
||||
|
||||
@ -6,13 +6,6 @@ import { clsx } from 'clsx/lite';
|
||||
import { toastSuccess } from '@/toast';
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
|
||||
interface Props extends ComponentProps<typeof LoaderButton> {
|
||||
onFormStatusChange?: (pending: boolean) => void
|
||||
onFormSubmitToastMessage?: string
|
||||
onFormSubmit?: () => void
|
||||
primary?: boolean
|
||||
}
|
||||
|
||||
export default function SubmitButtonWithStatus({
|
||||
icon,
|
||||
styleAs,
|
||||
@ -26,7 +19,12 @@ export default function SubmitButtonWithStatus({
|
||||
primary,
|
||||
type: _type,
|
||||
...buttonProps
|
||||
}: Props) {
|
||||
}: {
|
||||
onFormStatusChange?: (pending: boolean) => void
|
||||
onFormSubmitToastMessage?: string
|
||||
onFormSubmit?: () => void
|
||||
primary?: boolean
|
||||
} & ComponentProps<typeof LoaderButton>) {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
const pendingPrevious = useRef(pending);
|
||||
|
||||
@ -9,19 +9,38 @@ export default function PhotoDate({
|
||||
}: {
|
||||
photo: Photo
|
||||
className?: string
|
||||
dateType?: 'takenAt' | 'createdAt'
|
||||
dateType?: 'takenAt' | 'createdAt' | 'updatedAt'
|
||||
}) {
|
||||
const date = useMemo(() => {
|
||||
const date = new Date(dateType === 'takenAt'
|
||||
? photo.takenAt
|
||||
: photo.createdAt);
|
||||
: dateType === 'createdAt'
|
||||
? photo.createdAt
|
||||
: photo.updatedAt);
|
||||
return isNaN(date.getTime()) ? new Date() : date;
|
||||
}, [
|
||||
dateType,
|
||||
photo.createdAt,
|
||||
photo.takenAt,
|
||||
photo.createdAt,
|
||||
photo.updatedAt,
|
||||
]);
|
||||
|
||||
const getTitleLabel = () => {
|
||||
switch (dateType) {
|
||||
case 'takenAt':
|
||||
return 'TAKEN';
|
||||
case 'createdAt':
|
||||
return 'CREATED';
|
||||
case 'updatedAt':
|
||||
return 'UPDATED';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDate {...{ date, className }} />
|
||||
<ResponsiveDate {...{
|
||||
date,
|
||||
className,
|
||||
titleLabel: getTitleLabel(),
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import { getExifDataAction } from './actions';
|
||||
import { TagsWithMeta } from '@/tag';
|
||||
import AiButton from './ai/AiButton';
|
||||
import usePhotoFormParent from './form/usePhotoFormParent';
|
||||
import PhotoSyncButton from '@/admin/PhotoSyncButton';
|
||||
import ExifSyncButton from '@/admin/ExifSyncButton';
|
||||
|
||||
export default function PhotoEditPageClient({
|
||||
photo,
|
||||
@ -68,10 +68,10 @@ export default function PhotoEditPageClient({
|
||||
<div className="flex gap-2">
|
||||
{hasAiTextGeneration &&
|
||||
<AiButton {...{ aiContent, shouldConfirm: hasTextContent }} />}
|
||||
<PhotoSyncButton
|
||||
<ExifSyncButton
|
||||
action={action}
|
||||
label="EXIF"
|
||||
formData={{ photoUrl: photo.url }}
|
||||
photoUrl={photo.url}
|
||||
/>
|
||||
</div>}
|
||||
isLoading={pending}
|
||||
|
||||
@ -43,7 +43,7 @@ export default function PhotoSmall({
|
||||
'active:brightness-75',
|
||||
selected && 'brightness-50',
|
||||
'min-w-[50px]',
|
||||
'rounded-[0.15rem] overflow-hidden',
|
||||
'rounded-[3px] overflow-hidden',
|
||||
'border border-gray-200 dark:border-gray-800',
|
||||
)}
|
||||
prefetch={prefetch}
|
||||
|
||||
@ -291,9 +291,8 @@ export const getExifDataAction = async (
|
||||
// - strip GPS data if necessary
|
||||
// - update blur data (or destroy if blur is disabled)
|
||||
// - generate AI text data, if enabled, and auto-generated fields are empty
|
||||
export const syncPhotoAction = async (formData: FormData) =>
|
||||
export const syncPhotoAction = async (photoId: string) =>
|
||||
runAuthenticatedAdminServerAction(async () => {
|
||||
const photoId = formData.get('photoId') as string | undefined;
|
||||
const photo = await getPhoto(photoId ?? '', true);
|
||||
|
||||
if (photo) {
|
||||
@ -347,6 +346,14 @@ export const syncPhotoAction = async (formData: FormData) =>
|
||||
}
|
||||
});
|
||||
|
||||
export const syncPhotosAction = async (photoIds: string[]) =>
|
||||
runAuthenticatedAdminServerAction(async () => {
|
||||
for (const photoId of photoIds) {
|
||||
await syncPhotoAction(photoId);
|
||||
}
|
||||
revalidateAllKeysAndPaths();
|
||||
});
|
||||
|
||||
export const clearCacheAction = async () =>
|
||||
runAuthenticatedAdminServerAction(revalidateAllKeysAndPaths);
|
||||
|
||||
|
||||
@ -62,7 +62,8 @@ const getPhotosCacheKeyForOption = (
|
||||
return value ? `${option}-${createLensKey(value)}` : null;
|
||||
}
|
||||
case 'takenBefore':
|
||||
case 'takenAfterInclusive': {
|
||||
case 'takenAfterInclusive':
|
||||
case 'updatedBefore': {
|
||||
const value = options[option];
|
||||
return value ? `${option}-${value.toISOString()}` : null;
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ export type GetPhotosOptions = {
|
||||
focal?: number
|
||||
takenBefore?: Date
|
||||
takenAfterInclusive?: Date
|
||||
updatedBefore?: Date
|
||||
hidden?: 'exclude' | 'include' | 'only'
|
||||
};
|
||||
|
||||
@ -30,6 +31,7 @@ export const getWheresFromOptions = (
|
||||
hidden = 'exclude',
|
||||
takenBefore,
|
||||
takenAfterInclusive,
|
||||
updatedBefore,
|
||||
query,
|
||||
tag,
|
||||
camera,
|
||||
@ -59,6 +61,10 @@ export const getWheresFromOptions = (
|
||||
wheres.push(`taken_at >= $${valuesIndex++}`);
|
||||
wheresValues.push(takenAfterInclusive.toISOString());
|
||||
}
|
||||
if (updatedBefore) {
|
||||
wheres.push(`updated_at < $${valuesIndex++}`);
|
||||
wheresValues.push(updatedBefore.toISOString());
|
||||
}
|
||||
if (query) {
|
||||
// eslint-disable-next-line max-len
|
||||
wheres.push(`CONCAT(title, ' ', caption, ' ', semantic_description) ILIKE $${valuesIndex++}`);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user