diff --git a/src/components/cmdk/CommandKClient.tsx b/src/components/cmdk/CommandKClient.tsx index 45eb51c7..704f62d7 100644 --- a/src/components/cmdk/CommandKClient.tsx +++ b/src/components/cmdk/CommandKClient.tsx @@ -63,6 +63,7 @@ import { IoMdCamera } from 'react-icons/io'; import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation'; import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon'; import { formatFocalLength } from '@/focal'; +import { formatRecipe } from '@/recipe'; const DIALOG_TITLE = 'Global Command-K Menu'; const DIALOG_DESCRIPTION = 'For searching photos, views, and settings'; @@ -279,7 +280,7 @@ export default function CommandKClient({ className="translate-x-[-1px]" />, items: recipes.map(({ recipe, count }) => ({ - label: recipe, + label: formatRecipe(recipe), annotation: formatCount(count), annotationAria: formatCountDescriptive(count), path: pathForRecipe(recipe), diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 79315b10..3cdf0e2d 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -37,6 +37,7 @@ import { blurImageFromUrl, convertFormDataToPhotoDbInsertAndLookupRecipeTitle, extractImageDataFromBlobPath, + propagateRecipeTitleIfNecessary, } from './server'; import { TAG_FAVS, isTagFavs } from '@/tag'; import { convertPhotoToPhotoDbInsert, Photo } from '.'; @@ -73,6 +74,7 @@ export const createPhotoAction = async (formData: FormData) => if (updatedUrl) { photo.url = updatedUrl; await insertPhoto(photo); + await propagateRecipeTitleIfNecessary(formData, photo); revalidateAllKeysAndPaths(); redirect(PATH_ADMIN_PHOTOS); } @@ -210,7 +212,10 @@ export const updatePhotoAction = async (formData: FormData) => await updatePhoto(photo) .then(async () => { - if (urlToDelete) { await deleteFile(urlToDelete); } + if (urlToDelete) { + await deleteFile(urlToDelete); + } + await propagateRecipeTitleIfNecessary(formData, photo); }); revalidatePhoto(photo.id); diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index f31f0270..4ee241ed 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -317,6 +317,55 @@ export const getUniqueCameras = async () => }))) , 'getUniqueCameras'); +export const getUniqueRecipes = async () => + safelyQueryPhotos(() => sql` + SELECT DISTINCT recipe_title, COUNT(*) + FROM photos + WHERE hidden IS NOT TRUE AND recipe_title IS NOT NULL + GROUP BY recipe_title + ORDER BY recipe_title ASC + `.then(({ rows }): Recipes => rows + .map(({ recipe_title, count }) => ({ + recipe: recipe_title, + count: parseInt(count, 10), + }))) + , 'getUniqueRecipes'); + +export const getRecipeTitleForData = async (data: string | object) => + // Includes legacy check on pre-stringified JSON + safelyQueryPhotos(() => sql` + SELECT recipe_title FROM photos + WHERE hidden IS NOT TRUE AND + recipe_data = ${typeof data === 'string' ? data : JSON.stringify(data)} + LIMIT 1 + ` + .then(({ rows }) => rows[0]?.recipe_title as string | undefined) + , 'getRecipeTitleForData'); + +export const updateAllMatchingRecipeTitles = ( + title: string, + data: string, +) => + safelyQueryPhotos(() => sql` + UPDATE photos + SET recipe_title = ${title} + WHERE recipe_title IS NULL AND recipe_data = ${data} + `, 'updateAllMatchingRecipeTitles'); + +export const getUniqueFilmSimulations = async () => + safelyQueryPhotos(() => sql` + SELECT DISTINCT film_simulation, COUNT(*) + FROM photos + WHERE hidden IS NOT TRUE AND film_simulation IS NOT NULL + GROUP BY film_simulation + ORDER BY film_simulation ASC + `.then(({ rows }): FilmSimulations => rows + .map(({ film_simulation, count }) => ({ + simulation: film_simulation as FilmSimulation, + count: parseInt(count, 10), + }))) + , 'getUniqueFilmSimulations'); + export const getUniqueLenses = async () => safelyQueryPhotos(() => sql` SELECT DISTINCT lens_make||' '||lens_model as lens, @@ -333,45 +382,7 @@ export const getUniqueLenses = async () => lens: { make, model }, count: parseInt(count, 10), }))) - , 'getUniqueCameras'); - -export const getUniqueFilmSimulations = async () => - safelyQueryPhotos(() => sql` - SELECT DISTINCT film_simulation, COUNT(*) - FROM photos - WHERE hidden IS NOT TRUE AND film_simulation IS NOT NULL - GROUP BY film_simulation - ORDER BY film_simulation ASC - `.then(({ rows }): FilmSimulations => rows - .map(({ film_simulation, count }) => ({ - simulation: film_simulation as FilmSimulation, - count: parseInt(count, 10), - }))) - , 'getUniqueFilmSimulations'); - -export const getUniqueRecipes = async () => - safelyQueryPhotos(() => sql` - SELECT DISTINCT recipe_title, COUNT(*) - FROM photos - WHERE hidden IS NOT TRUE AND recipe_title IS NOT NULL - GROUP BY recipe_title - ORDER BY recipe_title ASC - `.then(({ rows }): Recipes => rows - .map(({ recipe_title, count }) => ({ - recipe: recipe_title, - count: parseInt(count, 10), - }))) - , 'getUniqueRecipes'); - -export const getRecipeTitleForData = async (data: string | object) => - safelyQueryPhotos(() => query( - // eslint-disable-next-line max-len - 'SELECT recipe_title FROM photos WHERE hidden IS NOT TRUE AND recipe_data = $1 LIMIT 1', - // Legacy check on escaped, string-based JSON - [typeof data === 'string' ? data : JSON.stringify(data)], - ) - .then(({ rows }) => rows[0]?.recipe_title as string | undefined) - , 'getRecipeTitleForData'); + , 'getUniqueLenses'); export const getUniqueFocalLengths = async () => safelyQueryPhotos(() => sql` diff --git a/src/photo/server.ts b/src/photo/server.ts index adcdd54e..6a8cc150 100644 --- a/src/photo/server.ts +++ b/src/photo/server.ts @@ -22,7 +22,11 @@ import { FujifilmRecipe, getFujifilmRecipeFromMakerNote, } from '@/platforms/fujifilm/recipe'; -import { getRecipeTitleForData } from './db/query'; +import { + getRecipeTitleForData, + updateAllMatchingRecipeTitles, +} from './db/query'; +import { PhotoDbInsert } from '.'; const IMAGE_WIDTH_RESIZE = 200; const IMAGE_WIDTH_BLUR = 200; @@ -211,3 +215,20 @@ export const convertFormDataToPhotoDbInsertAndLookupRecipeTitle = return photo; }; + +export const propagateRecipeTitleIfNecessary = async ( + formData: FormData, + photo: PhotoDbInsert, +) => { + if ( + // Only propagate recipe title if set by user before lookup + formData.get('recipeTitle') && + photo.recipeTitle && + photo.recipeData + ) { + await updateAllMatchingRecipeTitles( + photo.recipeTitle, + photo.recipeData, + ); + } +};