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