Propagate recipe titles on photo create/update

This commit is contained in:
Sam Becker 2025-03-10 09:57:57 -05:00
parent f6d8e452f0
commit 81b127468f
4 changed files with 80 additions and 42 deletions

View File

@ -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),

View File

@ -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);

View File

@ -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`

View File

@ -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,
);
}
};