Allow users to opt out of global recipe titling
This commit is contained in:
parent
3b39a1b62e
commit
8b6ea0da6d
@ -12,6 +12,7 @@ import {
|
|||||||
getUniqueTags,
|
getUniqueTags,
|
||||||
deletePhotoRecipeGlobally,
|
deletePhotoRecipeGlobally,
|
||||||
renamePhotoRecipeGlobally,
|
renamePhotoRecipeGlobally,
|
||||||
|
getPhotosNeedingRecipeTitleCount,
|
||||||
} from '@/photo/db/query';
|
} from '@/photo/db/query';
|
||||||
import { GetPhotosOptions, areOptionsSensitive } from './db';
|
import { GetPhotosOptions, areOptionsSensitive } from './db';
|
||||||
import {
|
import {
|
||||||
@ -305,6 +306,13 @@ export const renamePhotoTagGloballyAction = async (formData: FormData) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getPhotosNeedingRecipeTitleCountAction = async (
|
||||||
|
recipeData: string,
|
||||||
|
) =>
|
||||||
|
runAuthenticatedAdminServerAction(async () =>
|
||||||
|
await getPhotosNeedingRecipeTitleCount(recipeData),
|
||||||
|
);
|
||||||
|
|
||||||
export const deletePhotoRecipeGloballyAction = async (formData: FormData) =>
|
export const deletePhotoRecipeGloballyAction = async (formData: FormData) =>
|
||||||
runAuthenticatedAdminServerAction(async () => {
|
runAuthenticatedAdminServerAction(async () => {
|
||||||
const recipe = formData.get('recipe') as string;
|
const recipe = formData.get('recipe') as string;
|
||||||
|
|||||||
@ -359,6 +359,14 @@ export const getRecipeTitleForData = async (data: string | object) =>
|
|||||||
.then(({ rows }) => rows[0]?.recipe_title as string | undefined)
|
.then(({ rows }) => rows[0]?.recipe_title as string | undefined)
|
||||||
, 'getRecipeTitleForData');
|
, 'getRecipeTitleForData');
|
||||||
|
|
||||||
|
export const getPhotosNeedingRecipeTitleCount = async (data: string) =>
|
||||||
|
safelyQueryPhotos(() => sql`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM photos
|
||||||
|
WHERE recipe_title IS NULL AND recipe_data = ${data}
|
||||||
|
`.then(({ rows }) => parseInt(rows[0].count, 10))
|
||||||
|
, 'getPhotosNeedingRecipeTitleCount');
|
||||||
|
|
||||||
export const updateAllMatchingRecipeTitles = (
|
export const updateAllMatchingRecipeTitles = (
|
||||||
title: string,
|
title: string,
|
||||||
data: string,
|
data: string,
|
||||||
|
|||||||
34
src/photo/form/ApplyRecipesGloballyCheckbox.tsx
Normal file
34
src/photo/form/ApplyRecipesGloballyCheckbox.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
||||||
|
import { ComponentProps, useEffect, useState } from 'react';
|
||||||
|
import { getPhotosNeedingRecipeTitleCountAction } from '../actions';
|
||||||
|
|
||||||
|
export default function ApplyRecipeTitleGloballyCheckbox({
|
||||||
|
recipeTitle,
|
||||||
|
hasRecipeTitleChanged,
|
||||||
|
recipeData,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<typeof FieldSetWithStatus> & {
|
||||||
|
recipeTitle?: string
|
||||||
|
hasRecipeTitleChanged?: boolean
|
||||||
|
recipeData?: string
|
||||||
|
}) {
|
||||||
|
const [matchingPhotosCount, setMatchingPhotosCount] = useState<number>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (recipeTitle && hasRecipeTitleChanged && recipeData) {
|
||||||
|
setMatchingPhotosCount(undefined);
|
||||||
|
getPhotosNeedingRecipeTitleCountAction(recipeData)
|
||||||
|
.then(setMatchingPhotosCount);
|
||||||
|
} else {
|
||||||
|
setMatchingPhotosCount(0);
|
||||||
|
}
|
||||||
|
}, [recipeTitle, hasRecipeTitleChanged, recipeData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSetWithStatus {...{
|
||||||
|
...props,
|
||||||
|
label: `Apply title to ${matchingPhotosCount} photos`,
|
||||||
|
type: 'checkbox',
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,9 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { ComponentProps, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
FIELDS_WITH_JSON,
|
FIELDS_WITH_JSON,
|
||||||
FORM_METADATA_ENTRIES,
|
FORM_METADATA_ENTRIES,
|
||||||
|
FormFields,
|
||||||
|
FormMeta,
|
||||||
PhotoFormData,
|
PhotoFormData,
|
||||||
convertFormKeysToLabels,
|
convertFormKeysToLabels,
|
||||||
formHasTextContent,
|
formHasTextContent,
|
||||||
@ -29,10 +31,10 @@ import { useAppState } from '@/state/AppState';
|
|||||||
import UpdateBlurDataButton from '../UpdateBlurDataButton';
|
import UpdateBlurDataButton from '../UpdateBlurDataButton';
|
||||||
import { getNextImageUrlForManipulation } from '@/platforms/next-image';
|
import { getNextImageUrlForManipulation } from '@/platforms/next-image';
|
||||||
import { BLUR_ENABLED, IS_PREVIEW } from '@/app/config';
|
import { BLUR_ENABLED, IS_PREVIEW } from '@/app/config';
|
||||||
import { PhotoDbInsert } from '..';
|
|
||||||
import ErrorNote from '@/components/ErrorNote';
|
import ErrorNote from '@/components/ErrorNote';
|
||||||
import { convertRecipesForForm, Recipes } from '@/recipe';
|
import { convertRecipesForForm, Recipes } from '@/recipe';
|
||||||
import deepEqual from 'fast-deep-equal/es6/react';
|
import deepEqual from 'fast-deep-equal/es6/react';
|
||||||
|
import ApplyRecipeTitleGloballyCheckbox from './ApplyRecipesGloballyCheckbox';
|
||||||
|
|
||||||
const THUMBNAIL_SIZE = 300;
|
const THUMBNAIL_SIZE = 300;
|
||||||
|
|
||||||
@ -223,9 +225,9 @@ export default function PhotoForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shouldHideField = (
|
const shouldHideField = (
|
||||||
key: keyof PhotoDbInsert | 'favorite',
|
key: FormFields,
|
||||||
hideIfEmpty?: boolean,
|
hideIfEmpty?: boolean,
|
||||||
shouldHide?: (formData: Partial<PhotoFormData>) => boolean,
|
shouldHide?: FormMeta['shouldHide'],
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
key === 'blurData' &&
|
key === 'blurData' &&
|
||||||
@ -237,7 +239,7 @@ export default function PhotoForm({
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
(hideIfEmpty && !formData[key]) ||
|
(hideIfEmpty && !formData[key]) ||
|
||||||
shouldHide?.(formData)
|
shouldHide?.(formData, changedFormKeys)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -326,25 +328,27 @@ export default function PhotoForm({
|
|||||||
shouldHide,
|
shouldHide,
|
||||||
loadingMessage,
|
loadingMessage,
|
||||||
type,
|
type,
|
||||||
}]) =>
|
}]) => {
|
||||||
!shouldHideField(key, hideIfEmpty, shouldHide) &&
|
if (!shouldHideField(key, hideIfEmpty, shouldHide)) {
|
||||||
<FieldSetWithStatus
|
const fieldProps: ComponentProps<typeof FieldSetWithStatus> = {
|
||||||
key={key}
|
id: key,
|
||||||
id={key}
|
label: label + (
|
||||||
label={label + (
|
|
||||||
key === 'blurData' && shouldDebugImageFallbacks
|
key === 'blurData' && shouldDebugImageFallbacks
|
||||||
? ` (${(formData[key] ?? '').length} chars.)`
|
? ` (${(formData[key] ?? '').length} chars.)`
|
||||||
: ''
|
: ''
|
||||||
)}
|
),
|
||||||
note={note}
|
note,
|
||||||
error={formErrors[key]}
|
error: formErrors[key],
|
||||||
value={formData[key] ?? ''}
|
value: formData[key] ?? '',
|
||||||
isModified={changedFormKeys.includes(key)}
|
isModified: changedFormKeys.includes(key),
|
||||||
onChange={value => {
|
onChange: value => {
|
||||||
const formUpdated = { ...formData, [key]: value };
|
const formUpdated = { ...formData, [key]: value };
|
||||||
setFormData(formUpdated);
|
setFormData(formUpdated);
|
||||||
if (validate) {
|
if (validate) {
|
||||||
setFormErrors({ ...formErrors, [key]: validate(value) });
|
setFormErrors({
|
||||||
|
...formErrors, [key]:
|
||||||
|
validate(value),
|
||||||
|
});
|
||||||
} else if (validateStringMaxLength !== undefined) {
|
} else if (validateStringMaxLength !== undefined) {
|
||||||
setFormErrors({
|
setFormErrors({
|
||||||
...formErrors,
|
...formErrors,
|
||||||
@ -356,26 +360,41 @@ export default function PhotoForm({
|
|||||||
if (key === 'title') {
|
if (key === 'title') {
|
||||||
onTitleChange?.(value.trim());
|
onTitleChange?.(value.trim());
|
||||||
}
|
}
|
||||||
}}
|
},
|
||||||
selectOptions={selectOptions}
|
selectOptions,
|
||||||
selectOptionsDefaultLabel={selectOptionsDefaultLabel}
|
selectOptionsDefaultLabel: selectOptionsDefaultLabel,
|
||||||
tagOptions={tagOptions}
|
tagOptions,
|
||||||
tagOptionsLimit={tagOptionsLimit}
|
tagOptionsLimit,
|
||||||
// eslint-disable-next-line max-len
|
tagOptionsLimitValidationMessage,
|
||||||
tagOptionsLimitValidationMessage={tagOptionsLimitValidationMessage}
|
required,
|
||||||
required={required}
|
readOnly,
|
||||||
readOnly={readOnly}
|
spellCheck,
|
||||||
spellCheck={spellCheck}
|
capitalize,
|
||||||
capitalize={capitalize}
|
placeholder: loadingMessage && !formData[key]
|
||||||
placeholder={loadingMessage && !formData[key]
|
|
||||||
? loadingMessage
|
? loadingMessage
|
||||||
: undefined}
|
: undefined,
|
||||||
loading={
|
loading: (
|
||||||
(loadingMessage && !formData[key] ? true : false) ||
|
(loadingMessage && !formData[key] ? true : false) ||
|
||||||
isFieldGeneratingAi(key)}
|
isFieldGeneratingAi(key)
|
||||||
type={type}
|
),
|
||||||
accessory={accessoryForField(key)}
|
type,
|
||||||
/>)}
|
accessory: accessoryForField(key),
|
||||||
|
};
|
||||||
|
return key === 'applyRecipeTitleGlobally'
|
||||||
|
? <ApplyRecipeTitleGloballyCheckbox
|
||||||
|
key={key}
|
||||||
|
recipeTitle={formData.recipeTitle}
|
||||||
|
recipeData={formData.recipeData}
|
||||||
|
hasRecipeTitleChanged={
|
||||||
|
changedFormKeys.includes('recipeTitle')}
|
||||||
|
{...fieldProps}
|
||||||
|
/>
|
||||||
|
: <FieldSetWithStatus
|
||||||
|
key={key}
|
||||||
|
{...fieldProps}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
})}
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="shouldStripGpsData"
|
name="shouldStripGpsData"
|
||||||
|
|||||||
@ -25,9 +25,13 @@ import { TAG_FAVS, getValidationMessageForTags } from '@/tag';
|
|||||||
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
|
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
|
||||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||||
|
|
||||||
type VirtualFields = 'favorite';
|
type VirtualFields =
|
||||||
|
'favorite' |
|
||||||
|
'applyRecipeTitleGlobally';
|
||||||
|
|
||||||
export type PhotoFormData = Record<keyof PhotoDbInsert | VirtualFields, string>
|
export type FormFields = keyof PhotoDbInsert | VirtualFields;
|
||||||
|
|
||||||
|
export type PhotoFormData = Record<FormFields, string>
|
||||||
|
|
||||||
export type FieldSetType =
|
export type FieldSetType =
|
||||||
'text' |
|
'text' |
|
||||||
@ -42,7 +46,7 @@ export type AnnotatedTag = {
|
|||||||
annotationAria?: string,
|
annotationAria?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormMeta = {
|
export type FormMeta = {
|
||||||
label: string
|
label: string
|
||||||
note?: string
|
note?: string
|
||||||
required?: boolean
|
required?: boolean
|
||||||
@ -52,9 +56,11 @@ type FormMeta = {
|
|||||||
validateStringMaxLength?: number
|
validateStringMaxLength?: number
|
||||||
spellCheck?: boolean
|
spellCheck?: boolean
|
||||||
capitalize?: boolean
|
capitalize?: boolean
|
||||||
hide?: boolean
|
|
||||||
hideIfEmpty?: boolean
|
hideIfEmpty?: boolean
|
||||||
shouldHide?: (formData: Partial<PhotoFormData>) => boolean
|
shouldHide?: (
|
||||||
|
formData: Partial<PhotoFormData>,
|
||||||
|
changedFormKeys?: (keyof PhotoFormData)[],
|
||||||
|
) => boolean
|
||||||
loadingMessage?: string
|
loadingMessage?: string
|
||||||
type?: FieldSetType
|
type?: FieldSetType
|
||||||
selectOptions?: { value: string, label: string }[]
|
selectOptions?: { value: string, label: string }[]
|
||||||
@ -96,7 +102,7 @@ const FORM_METADATA = (
|
|||||||
label: 'semantic description (not visible)',
|
label: 'semantic description (not visible)',
|
||||||
capitalize: true,
|
capitalize: true,
|
||||||
validateStringMaxLength: STRING_MAX_LENGTH_LONG,
|
validateStringMaxLength: STRING_MAX_LENGTH_LONG,
|
||||||
hide: !aiTextGeneration,
|
shouldHide: () => !aiTextGeneration,
|
||||||
},
|
},
|
||||||
id: { label: 'id', readOnly: true, hideIfEmpty: true },
|
id: { label: 'id', readOnly: true, hideIfEmpty: true },
|
||||||
blurData: {
|
blurData: {
|
||||||
@ -124,6 +130,18 @@ const FORM_METADATA = (
|
|||||||
capitalize: false,
|
capitalize: false,
|
||||||
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
||||||
},
|
},
|
||||||
|
applyRecipeTitleGlobally: {
|
||||||
|
label: 'apply recipe title globally',
|
||||||
|
type: 'checkbox',
|
||||||
|
excludeFromInsert: true,
|
||||||
|
shouldHide: ({ make, recipeTitle, recipeData }, changedFormKeys) =>
|
||||||
|
!(
|
||||||
|
make === MAKE_FUJIFILM &&
|
||||||
|
recipeData &&
|
||||||
|
recipeTitle &&
|
||||||
|
changedFormKeys?.includes('recipeTitle')
|
||||||
|
),
|
||||||
|
},
|
||||||
recipeData: {
|
recipeData: {
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
label: 'recipe data',
|
label: 'recipe data',
|
||||||
@ -152,7 +170,7 @@ const FORM_METADATA = (
|
|||||||
iso: { label: 'ISO' },
|
iso: { label: 'ISO' },
|
||||||
exposureTime: { label: 'exposure time' },
|
exposureTime: { label: 'exposure time' },
|
||||||
exposureCompensation: { label: 'exposure compensation' },
|
exposureCompensation: { label: 'exposure compensation' },
|
||||||
locationName: { label: 'location name', hide: true },
|
locationName: { label: 'location name', shouldHide: () => true },
|
||||||
latitude: { label: 'latitude' },
|
latitude: { label: 'latitude' },
|
||||||
longitude: { label: 'longitude' },
|
longitude: { label: 'longitude' },
|
||||||
takenAt: {
|
takenAt: {
|
||||||
@ -180,8 +198,7 @@ export const FIELDS_TO_NOT_OVERWRITE_WITH_NULL_DATA_ON_SYNC =
|
|||||||
export const FORM_METADATA_ENTRIES = (
|
export const FORM_METADATA_ENTRIES = (
|
||||||
...args: Parameters<typeof FORM_METADATA>
|
...args: Parameters<typeof FORM_METADATA>
|
||||||
) =>
|
) =>
|
||||||
(Object.entries(FORM_METADATA(...args)) as [keyof PhotoFormData, FormMeta][])
|
(Object.entries(FORM_METADATA(...args)) as [keyof PhotoFormData, FormMeta][]);
|
||||||
.filter(([_, meta]) => !meta.hide);
|
|
||||||
|
|
||||||
export const convertFormKeysToLabels = (keys: (keyof PhotoFormData)[]) =>
|
export const convertFormKeysToLabels = (keys: (keyof PhotoFormData)[]) =>
|
||||||
keys.map(key => FORM_METADATA()[key].label.toUpperCase());
|
keys.map(key => FORM_METADATA()[key].label.toUpperCase());
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user