Allow editing recipe data, protect manually configured fuji fields
This commit is contained in:
parent
2b44a5fa04
commit
541c09c551
@ -21,7 +21,7 @@ export default async function UploadPage({ params }: Params) {
|
||||
|
||||
const {
|
||||
blobId,
|
||||
photoFormExif,
|
||||
formDataFromExif,
|
||||
imageResizedBase64: imageThumbnailBase64,
|
||||
shouldStripGpsData,
|
||||
error,
|
||||
@ -32,7 +32,7 @@ export default async function UploadPage({ params }: Params) {
|
||||
});
|
||||
|
||||
const isDataMissing =
|
||||
!photoFormExif ||
|
||||
!formDataFromExif ||
|
||||
(AI_TEXT_GENERATION_ENABLED && !imageThumbnailBase64);
|
||||
|
||||
if (isDataMissing && !error) {
|
||||
@ -50,7 +50,7 @@ export default async function UploadPage({ params }: Params) {
|
||||
!isDataMissing
|
||||
? <UploadPageClient {...{
|
||||
blobId,
|
||||
photoFormExif,
|
||||
formDataFromExif,
|
||||
uniqueTags,
|
||||
hasAiTextGeneration,
|
||||
textFieldsToAutoGenerate,
|
||||
|
||||
@ -23,6 +23,7 @@ export default function FieldSetWithStatus({
|
||||
loading,
|
||||
required,
|
||||
readOnly,
|
||||
spellCheck,
|
||||
capitalize,
|
||||
type = 'text',
|
||||
inputRef,
|
||||
@ -43,6 +44,7 @@ export default function FieldSetWithStatus({
|
||||
loading?: boolean
|
||||
required?: boolean
|
||||
readOnly?: boolean
|
||||
spellCheck?: boolean
|
||||
capitalize?: boolean
|
||||
type?: FieldSetType
|
||||
inputRef?: Ref<HTMLInputElement>
|
||||
@ -140,6 +142,8 @@ export default function FieldSetWithStatus({
|
||||
placeholder={placeholder}
|
||||
onChange={e => onChange?.(e.target.value)}
|
||||
readOnly={readOnly || pending || loading}
|
||||
spellCheck={spellCheck}
|
||||
autoCapitalize={!capitalize ? 'off' : undefined}
|
||||
className={clsx(
|
||||
'w-full h-24 resize-none',
|
||||
Boolean(error) && 'error',
|
||||
@ -156,6 +160,7 @@ export default function FieldSetWithStatus({
|
||||
? e.target.value === 'true' ? 'false' : 'true'
|
||||
: e.target.value)}
|
||||
type={type}
|
||||
spellCheck={spellCheck}
|
||||
autoComplete="off"
|
||||
autoCapitalize={!capitalize ? 'off' : undefined}
|
||||
readOnly={readOnly || pending || loading}
|
||||
|
||||
@ -307,12 +307,16 @@ export default function PhotoLarge({
|
||||
<li>{photo.isoFormatted}</li>
|
||||
<li>{photo.exposureCompensationFormatted ?? '0ev'}</li>
|
||||
</ul>
|
||||
{showSimulation && photo.filmSimulation &&
|
||||
{(
|
||||
(showSimulation && photo.filmSimulation)
|
||||
|| photo.fujifilmRecipe
|
||||
) &&
|
||||
<div className="flex items-center gap-2 *:w-auto">
|
||||
{showSimulation && photo.filmSimulation &&
|
||||
<PhotoFilmSimulation
|
||||
simulation={photo.filmSimulation}
|
||||
prefetch={prefetchRelatedLinks}
|
||||
/>
|
||||
/>}
|
||||
{photo.fujifilmRecipe &&
|
||||
<button
|
||||
ref={refRecipeTrigger}
|
||||
|
||||
@ -12,7 +12,7 @@ import { useMemo } from 'react';
|
||||
|
||||
export default function UploadPageClient({
|
||||
blobId,
|
||||
photoFormExif,
|
||||
formDataFromExif,
|
||||
uniqueTags,
|
||||
hasAiTextGeneration,
|
||||
textFieldsToAutoGenerate,
|
||||
@ -20,7 +20,7 @@ export default function UploadPageClient({
|
||||
shouldStripGpsData,
|
||||
}: {
|
||||
blobId?: string
|
||||
photoFormExif: Partial<PhotoFormData>
|
||||
formDataFromExif: Partial<PhotoFormData>
|
||||
uniqueTags: Tags
|
||||
hasAiTextGeneration?: boolean
|
||||
textFieldsToAutoGenerate?: AiAutoGeneratedField[],
|
||||
@ -41,10 +41,10 @@ export default function UploadPageClient({
|
||||
});
|
||||
|
||||
const initialPhotoForm = useMemo(() => ({
|
||||
...photoFormExif,
|
||||
...formDataFromExif,
|
||||
// Generate missing dates on client to avoid timezone issues
|
||||
...generateTakenAtFields(photoFormExif),
|
||||
}), [photoFormExif]);
|
||||
...generateTakenAtFields(formDataFromExif),
|
||||
}), [formDataFromExif]);
|
||||
|
||||
return (
|
||||
<AdminChildPage
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
} from '@/photo/db/query';
|
||||
import { GetPhotosOptions, areOptionsSensitive } from './db';
|
||||
import {
|
||||
FIELDS_TO_NOT_OVERWRITE_WITH_NULL_DATA_ON_SYNC,
|
||||
PhotoFormData,
|
||||
convertFormDataToPhotoDbInsert,
|
||||
convertPhotoToFormData,
|
||||
@ -115,7 +116,7 @@ export const addAllUploadsAction = async ({
|
||||
streamUpdate('Parsing EXIF data');
|
||||
|
||||
const {
|
||||
photoFormExif,
|
||||
formDataFromExif,
|
||||
imageResizedBase64,
|
||||
shouldStripGpsData,
|
||||
fileBytes,
|
||||
@ -125,7 +126,7 @@ export const addAllUploadsAction = async ({
|
||||
generateResizedImage: AI_TEXT_GENERATION_ENABLED,
|
||||
});
|
||||
|
||||
if (photoFormExif) {
|
||||
if (formDataFromExif) {
|
||||
if (AI_TEXT_GENERATION_ENABLED) {
|
||||
streamUpdate('Generating AI text');
|
||||
}
|
||||
@ -141,13 +142,13 @@ export const addAllUploadsAction = async ({
|
||||
);
|
||||
|
||||
const form: Partial<PhotoFormData> = {
|
||||
...photoFormExif,
|
||||
...formDataFromExif,
|
||||
title,
|
||||
caption,
|
||||
tags: tags || aiTags,
|
||||
semanticDescription,
|
||||
takenAt: photoFormExif.takenAt || takenAtLocal,
|
||||
takenAtNaive: photoFormExif.takenAtNaive || takenAtNaiveLocal,
|
||||
takenAt: formDataFromExif.takenAt || takenAtLocal,
|
||||
takenAtNaive: formDataFromExif.takenAtNaive || takenAtNaiveLocal,
|
||||
};
|
||||
|
||||
streamUpdate('Transferring to photo storage');
|
||||
@ -302,9 +303,9 @@ export const getExifDataAction = async (
|
||||
url: string,
|
||||
): Promise<Partial<PhotoFormData>> =>
|
||||
runAuthenticatedAdminServerAction(async () => {
|
||||
const { photoFormExif } = await extractImageDataFromBlobPath(url);
|
||||
if (photoFormExif) {
|
||||
return photoFormExif;
|
||||
const { formDataFromExif } = await extractImageDataFromBlobPath(url);
|
||||
if (formDataFromExif) {
|
||||
return formDataFromExif;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
@ -322,7 +323,7 @@ export const syncPhotoAction = async (photoId: string) =>
|
||||
|
||||
if (photo) {
|
||||
const {
|
||||
photoFormExif,
|
||||
formDataFromExif,
|
||||
imageResizedBase64,
|
||||
shouldStripGpsData,
|
||||
fileBytes,
|
||||
@ -333,7 +334,7 @@ export const syncPhotoAction = async (photoId: string) =>
|
||||
});
|
||||
|
||||
let urlToDelete: string | undefined;
|
||||
if (photoFormExif) {
|
||||
if (formDataFromExif) {
|
||||
if (photo.url.includes(photo.id) || shouldStripGpsData) {
|
||||
// Anonymize storage url on update if necessary by
|
||||
// re-running image upload transfer logic
|
||||
@ -359,11 +360,18 @@ export const syncPhotoAction = async (photoId: string) =>
|
||||
AI_TEXT_AUTO_GENERATED_FIELDS,
|
||||
);
|
||||
|
||||
const formDataFromPhoto = convertPhotoToFormData(photo);
|
||||
|
||||
// Don't overwrite manually configured fujifilm meta with null data
|
||||
FIELDS_TO_NOT_OVERWRITE_WITH_NULL_DATA_ON_SYNC.forEach(field => {
|
||||
if (!formDataFromExif[field] && formDataFromPhoto[field]) {
|
||||
delete formDataFromExif[field];
|
||||
}
|
||||
});
|
||||
|
||||
const photoFormDbInsert = convertFormDataToPhotoDbInsert({
|
||||
...convertPhotoToFormData(photo),
|
||||
...photoFormExif,
|
||||
// Don't overwrite manually configured film simulations
|
||||
...photo.filmSimulation && { filmSimulation: photo.filmSimulation },
|
||||
...formDataFromPhoto,
|
||||
...formDataFromExif,
|
||||
...!BLUR_ENABLED && { blurData: undefined },
|
||||
...!photo.title && { title: atTitle },
|
||||
...!photo.caption && { caption: aiCaption },
|
||||
|
||||
@ -305,6 +305,7 @@ export default function PhotoForm({
|
||||
readOnly,
|
||||
validate,
|
||||
validateStringMaxLength,
|
||||
spellCheck,
|
||||
capitalize,
|
||||
hideIfEmpty,
|
||||
shouldHide,
|
||||
@ -346,6 +347,7 @@ export default function PhotoForm({
|
||||
tagOptions={tagOptions}
|
||||
required={required}
|
||||
readOnly={readOnly}
|
||||
spellCheck={spellCheck}
|
||||
capitalize={capitalize}
|
||||
placeholder={loadingMessage && !formData[key]
|
||||
? loadingMessage
|
||||
|
||||
@ -50,6 +50,7 @@ type FormMeta = {
|
||||
readOnly?: boolean
|
||||
validate?: (value?: string) => string | undefined
|
||||
validateStringMaxLength?: number
|
||||
spellCheck?: boolean
|
||||
capitalize?: boolean
|
||||
hide?: boolean
|
||||
hideIfEmpty?: boolean
|
||||
@ -59,6 +60,7 @@ type FormMeta = {
|
||||
selectOptions?: { value: string, label: string }[]
|
||||
selectOptionsDefaultLabel?: string
|
||||
tagOptions?: AnnotatedTag[]
|
||||
shouldNotOverwriteWithNullDataOnSync?: boolean
|
||||
};
|
||||
|
||||
const STRING_MAX_LENGTH_SHORT = 255;
|
||||
@ -107,12 +109,26 @@ const FORM_METADATA = (
|
||||
selectOptions: FILM_SIMULATION_FORM_INPUT_OPTIONS,
|
||||
selectOptionsDefaultLabel: 'Unknown',
|
||||
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
||||
shouldNotOverwriteWithNullDataOnSync: true,
|
||||
},
|
||||
fujifilmRecipe: {
|
||||
type: 'textarea',
|
||||
label: 'fujifilm recipe',
|
||||
spellCheck: false,
|
||||
capitalize: false,
|
||||
validate: value => {
|
||||
let validationMessage = undefined;
|
||||
if (value) {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
} catch {
|
||||
validationMessage = 'Invalid JSON';
|
||||
}
|
||||
}
|
||||
return validationMessage;
|
||||
},
|
||||
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
||||
readOnly: true,
|
||||
shouldNotOverwriteWithNullDataOnSync: true,
|
||||
},
|
||||
focalLength: { label: 'focal length' },
|
||||
focalLengthIn35MmFormat: { label: 'focal length 35mm-equivalent' },
|
||||
@ -138,6 +154,11 @@ const FORM_METADATA = (
|
||||
hidden: { label: 'hidden', type: 'checkbox' },
|
||||
});
|
||||
|
||||
export const FIELDS_TO_NOT_OVERWRITE_WITH_NULL_DATA_ON_SYNC =
|
||||
Object.entries(FORM_METADATA())
|
||||
.filter(([_, meta]) => meta.shouldNotOverwriteWithNullDataOnSync)
|
||||
.map(([key]) => key as keyof PhotoFormData);
|
||||
|
||||
export const FORM_METADATA_ENTRIES = (
|
||||
...args: Parameters<typeof FORM_METADATA>
|
||||
) =>
|
||||
@ -175,9 +196,8 @@ export const formHasTextContent = ({
|
||||
|
||||
// CREATE FORM DATA: FROM PHOTO
|
||||
|
||||
export const convertPhotoToFormData = (
|
||||
photo: Photo,
|
||||
): PhotoFormData => {
|
||||
export const convertPhotoToFormData = (photo: Photo): PhotoFormData => {
|
||||
console.log('convertPhotoToFormData', photo);
|
||||
const valueForKey = (key: keyof Photo, value: any) => {
|
||||
switch (key) {
|
||||
case 'tags':
|
||||
|
||||
@ -31,7 +31,7 @@ export const extractImageDataFromBlobPath = async (
|
||||
},
|
||||
): Promise<{
|
||||
blobId?: string
|
||||
photoFormExif?: Partial<PhotoFormData>
|
||||
formDataFromExif?: Partial<PhotoFormData>
|
||||
imageResizedBase64?: string
|
||||
shouldStripGpsData?: boolean
|
||||
fileBytes?: ArrayBuffer
|
||||
@ -108,7 +108,7 @@ export const extractImageDataFromBlobPath = async (
|
||||
return {
|
||||
blobId,
|
||||
...exifData && {
|
||||
photoFormExif: {
|
||||
formDataFromExif: {
|
||||
...includeInitialPhotoFields && {
|
||||
hidden: 'false',
|
||||
favorite: 'false',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user