diff --git a/src/admin/AdminBatchEditPanelClient.tsx b/src/admin/AdminBatchEditPanelClient.tsx index 68e81392..0bd5ac8b 100644 --- a/src/admin/AdminBatchEditPanelClient.tsx +++ b/src/admin/AdminBatchEditPanelClient.tsx @@ -12,6 +12,7 @@ import { Tags } from '@/tag'; import { usePathname } from 'next/navigation'; import { PATH_GRID_INFERRED } from '@/site/paths'; import PhotoTagFieldset from './PhotoTagFieldset'; +import { tagMultiplePhotosAction } from '@/photo/actions'; export default function AdminBatchEditPanelClient({ uniqueTags, @@ -26,6 +27,8 @@ export default function AdminBatchEditPanelClient({ setSelectedPhotoIds, } = useAppState(); + const [isLoading, setIsLoading] = useState(false); + const [tags, setTags] = useState(); const [tagErrorMessage, setTagErrorMessage] = useState(''); const isTagging = tags !== undefined; @@ -44,6 +47,7 @@ export default function AdminBatchEditPanelClient({ setTags(undefined); setTagErrorMessage(''); }} + disabled={isLoading} > Cancel @@ -51,7 +55,15 @@ 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(tagErrorMessage)} + onClick={() => { + setIsLoading(true); + tagMultiplePhotosAction( + tags, + selectedPhotoIds ?? [], + ) + .finally(() => setIsLoading(false)); + }} + disabled={!tags || Boolean(tagErrorMessage) || isLoading} primary > Apply Tags @@ -60,10 +72,13 @@ export default function AdminBatchEditPanelClient({ : <> {(selectedPhotoIds?.length ?? 0) > 0 && <> - setTags('')}> + setTags('')} + isLoading={isLoading} + > Tag ... - + } } diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 17557dc8..9240bbf5 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -8,6 +8,7 @@ import { renamePhotoTagGlobally, getPhoto, getPhotos, + addTagsToPhotos, } from '@/photo/db/query'; import { GetPhotosOptions, areOptionsSensitive } from './db'; import { @@ -47,6 +48,7 @@ import { generateAiImageQueries } from './ai/server'; import { createStreamableValue } from 'ai/rsc'; import { convertUploadToPhoto } from './storage'; import { UrlAddStatus } from '@/admin/AdminUploadsClient'; +import { convertStringToArray } from '@/utility/string'; // Private actions @@ -203,6 +205,18 @@ export const updatePhotoAction = async (formData: FormData) => redirect(PATH_ADMIN_PHOTOS); }); +export const tagMultiplePhotosAction = ( + tags: string, + photoIds: string[], +) => + runAuthenticatedAdminServerAction(async () => { + await addTagsToPhotos( + convertStringToArray(tags, false) ?? [], + photoIds, + ); + revalidateAllKeysAndPaths(); + }); + export const toggleFavoritePhotoAction = async ( photoId: string, shouldRedirect?: boolean, diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index be2c644e..1f9cacaa 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -244,17 +244,19 @@ export const renamePhotoTagGlobally = (tag: string, updatedTag: string) => `, 'renamePhotoTagGlobally'); export const addTagsToPhotos = (tags: string[], photoIds: string[]) => - safelyQueryPhotos(() => sql` + safelyQueryPhotos(() => query(` UPDATE photos SET tags = ( SELECT array_agg(DISTINCT elem) FROM unnest( - array_cat(tags, ARRAY${convertArrayToPostgresString(tags, 'brackets')}) + array_cat(tags, $1) ) AS elem ) - WHERE id IN ${convertArrayToPostgresString(photoIds, 'brackets')} - LIMIT ${photoIds.length} - `, 'addTagsToPhotos'); + WHERE id = ANY($2) + `, [ + convertArrayToPostgresString(tags), + convertArrayToPostgresString(photoIds), + ]), 'addTagsToPhotos'); export const deletePhoto = (id: string) => safelyQueryPhotos(() => sql` diff --git a/src/services/postgres.ts b/src/services/postgres.ts index 86a8e1fa..27d1fd3c 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -43,11 +43,13 @@ export const sql = ( export const convertArrayToPostgresString = ( array?: string[], - type: 'braces' | 'brackets' = 'braces', + type: 'braces' | 'brackets' | 'parentheses' = 'braces', ) => array ? type === 'braces' ? `{${array.join(',')}}` - : `[${array.map(i => `'${i}'`).join(',')}]` + : type === 'brackets' + ? `[${array.map(i => `'${i}'`).join(',')}]` + : `(${array.map(i => `'${i}'`).join(',')})` : null; const isTemplateStringsArray = (