Display tag counts in photo form
This commit is contained in:
parent
e5efc3614d
commit
1da28079e6
@ -12,7 +12,7 @@ export default async function PhotoEditPage({
|
|||||||
|
|
||||||
if (!photo) { redirect(PATH_ADMIN); }
|
if (!photo) { redirect(PATH_ADMIN); }
|
||||||
|
|
||||||
const uniqueTags = (await getUniqueTagsCached()).map(tag => tag.tag);
|
const uniqueTags = await getUniqueTagsCached();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PhotoEditPageClient {...{ photo, uniqueTags }} />
|
<PhotoEditPageClient {...{ photo, uniqueTags }} />
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export default async function UploadPage({ params: { uploadPath } }: Params) {
|
|||||||
photoFormExif,
|
photoFormExif,
|
||||||
} = await extractExifDataFromBlobPath(uploadPath);
|
} = await extractExifDataFromBlobPath(uploadPath);
|
||||||
|
|
||||||
const uniqueTags = (await getUniqueTagsCached()).map(tag => tag.tag);
|
const uniqueTags = await getUniqueTagsCached();
|
||||||
|
|
||||||
if (!photoFormExif) { redirect(PATH_ADMIN); }
|
if (!photoFormExif) { redirect(PATH_ADMIN); }
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { LegacyRef } from 'react';
|
|||||||
import { useFormStatus } from 'react-dom';
|
import { useFormStatus } from 'react-dom';
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { FieldSetType } from '@/photo/form';
|
import { FieldSetType, AnnotatedTag } from '@/photo/form';
|
||||||
import TagInput from './TagInput';
|
import TagInput from './TagInput';
|
||||||
|
|
||||||
export default function FieldSetWithStatus({
|
export default function FieldSetWithStatus({
|
||||||
@ -16,7 +16,7 @@ export default function FieldSetWithStatus({
|
|||||||
onChange,
|
onChange,
|
||||||
selectOptions,
|
selectOptions,
|
||||||
selectOptionsDefaultLabel,
|
selectOptionsDefaultLabel,
|
||||||
commaSeparatedOptions,
|
tagOptions,
|
||||||
placeholder,
|
placeholder,
|
||||||
loading,
|
loading,
|
||||||
required,
|
required,
|
||||||
@ -33,7 +33,7 @@ export default function FieldSetWithStatus({
|
|||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
selectOptions?: { value: string, label: string }[]
|
selectOptions?: { value: string, label: string }[]
|
||||||
selectOptionsDefaultLabel?: string
|
selectOptionsDefaultLabel?: string
|
||||||
commaSeparatedOptions?: string[]
|
tagOptions?: AnnotatedTag []
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
required?: boolean
|
required?: boolean
|
||||||
@ -91,11 +91,11 @@ export default function FieldSetWithStatus({
|
|||||||
{optionLabel}
|
{optionLabel}
|
||||||
</option>)}
|
</option>)}
|
||||||
</select>
|
</select>
|
||||||
: commaSeparatedOptions
|
: tagOptions
|
||||||
? <TagInput
|
? <TagInput
|
||||||
name={id}
|
name={id}
|
||||||
value={value}
|
value={value}
|
||||||
options={commaSeparatedOptions}
|
options={tagOptions}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className={clsx(Boolean(error) && 'error')}
|
className={clsx(Boolean(error) && 'error')}
|
||||||
readOnly={readOnly || pending}
|
readOnly={readOnly || pending}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { AnnotatedTag } from '@/photo/form';
|
||||||
import { convertStringToArray, parameterize } from '@/utility/string';
|
import { convertStringToArray, parameterize } from '@/utility/string';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
@ -15,7 +16,7 @@ export default function TagInput({
|
|||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
value?: string
|
value?: string
|
||||||
options?: string[]
|
options?: AnnotatedTag[]
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
className?: string
|
className?: string
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
@ -28,6 +29,10 @@ export default function TagInput({
|
|||||||
const [inputText, setInputText] = useState('');
|
const [inputText, setInputText] = useState('');
|
||||||
const [selectedOptionIndex, setSelectedOptionIndex] = useState<number>();
|
const [selectedOptionIndex, setSelectedOptionIndex] = useState<number>();
|
||||||
|
|
||||||
|
const optionValues = useMemo(() =>
|
||||||
|
options.map(({ value }) => value)
|
||||||
|
, [options]);
|
||||||
|
|
||||||
const selectedOptions = useMemo(() =>
|
const selectedOptions = useMemo(() =>
|
||||||
convertStringToArray(value) ?? []
|
convertStringToArray(value) ?? []
|
||||||
, [value]);
|
, [value]);
|
||||||
@ -35,18 +40,21 @@ export default function TagInput({
|
|||||||
const inputTextFormatted = parameterize(inputText);
|
const inputTextFormatted = parameterize(inputText);
|
||||||
const isInputTextUnique =
|
const isInputTextUnique =
|
||||||
inputTextFormatted &&
|
inputTextFormatted &&
|
||||||
!options.includes(inputTextFormatted) &&
|
!optionValues.includes(inputTextFormatted) &&
|
||||||
!selectedOptions.includes(inputTextFormatted);
|
!selectedOptions.includes(inputTextFormatted);
|
||||||
|
|
||||||
const optionsFiltered = (isInputTextUnique
|
const optionsFiltered = useMemo<AnnotatedTag[]>(() =>
|
||||||
? [`${CREATE_LABEL} "${inputTextFormatted}"`]
|
(isInputTextUnique
|
||||||
: []).concat(options
|
? [{ value: `${CREATE_LABEL} "${inputTextFormatted}"` }]
|
||||||
.filter(option =>
|
: []
|
||||||
!selectedOptions.includes(option) &&
|
).concat(options
|
||||||
(
|
.filter(({ value }) =>
|
||||||
!inputTextFormatted ||
|
!selectedOptions.includes(value) &&
|
||||||
option.includes(inputTextFormatted)
|
(
|
||||||
)));
|
!inputTextFormatted ||
|
||||||
|
value.includes(inputTextFormatted)
|
||||||
|
)))
|
||||||
|
, [inputTextFormatted, isInputTextUnique, options, selectedOptions]);
|
||||||
|
|
||||||
const hideMenu = useCallback((shouldBlurInput?: boolean) => {
|
const hideMenu = useCallback((shouldBlurInput?: boolean) => {
|
||||||
setShouldShowMenu(false);
|
setShouldShowMenu(false);
|
||||||
@ -117,7 +125,7 @@ export default function TagInput({
|
|||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
addOption(optionsFiltered[selectedOptionIndex ?? 0]);
|
addOption(optionsFiltered[selectedOptionIndex ?? 0].value);
|
||||||
setInputText('');
|
setInputText('');
|
||||||
break;
|
break;
|
||||||
case ',':
|
case ',':
|
||||||
@ -137,7 +145,12 @@ export default function TagInput({
|
|||||||
break;
|
break;
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
setSelectedOptionIndex(i => {
|
setSelectedOptionIndex(i => {
|
||||||
if (i === undefined || i === 0) {
|
if (
|
||||||
|
document.activeElement === inputRef.current &&
|
||||||
|
optionsFiltered.length > 0
|
||||||
|
) {
|
||||||
|
return optionsFiltered.length - 1;
|
||||||
|
} else if (i === undefined || i === 0) {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
return undefined;
|
return undefined;
|
||||||
} else {
|
} else {
|
||||||
@ -197,8 +210,8 @@ export default function TagInput({
|
|||||||
'cursor-pointer select-none',
|
'cursor-pointer select-none',
|
||||||
'whitespace-nowrap',
|
'whitespace-nowrap',
|
||||||
'px-1.5 py-0.5',
|
'px-1.5 py-0.5',
|
||||||
'bg-gray-100 dark:bg-gray-800',
|
'bg-gray-200/60 dark:bg-gray-800',
|
||||||
'active:bg-gray-50 dark:active:bg-gray-900',
|
'active:bg-gray-200 dark:active:bg-gray-900',
|
||||||
'rounded-sm',
|
'rounded-sm',
|
||||||
)}
|
)}
|
||||||
onClick={() => removeOption(option)}
|
onClick={() => removeOption(option)}
|
||||||
@ -233,27 +246,40 @@ export default function TagInput({
|
|||||||
'text-xl shadow-lg dark:shadow-xl',
|
'text-xl shadow-lg dark:shadow-xl',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{optionsFiltered.map((option, index) =>
|
{optionsFiltered.map(({ value, annotation }, index) =>
|
||||||
<div
|
<div
|
||||||
key={option}
|
key={value}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
'group flex items-center gap-1',
|
||||||
'cursor-pointer select-none',
|
'cursor-pointer select-none',
|
||||||
'px-1 py-1 rounded-sm',
|
'px-1.5 py-1 rounded-sm',
|
||||||
index === 0 && selectedOptionIndex === undefined &&
|
|
||||||
'bg-gray-100 dark:bg-gray-800',
|
|
||||||
'hover:bg-gray-100 dark:hover:bg-gray-800',
|
'hover:bg-gray-100 dark:hover:bg-gray-800',
|
||||||
'active:bg-gray-50 dark:active:bg-gray-900',
|
'active:bg-gray-50 dark:active:bg-gray-900',
|
||||||
'focus:bg-gray-100 dark:focus:bg-gray-800',
|
'focus:bg-gray-100 dark:focus:bg-gray-800',
|
||||||
|
index === 0 && selectedOptionIndex === undefined &&
|
||||||
|
'bg-gray-100 dark:bg-gray-800',
|
||||||
'outline-none',
|
'outline-none',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addOption(option);
|
addOption(value);
|
||||||
setInputText('');
|
setInputText('');
|
||||||
}}
|
}}
|
||||||
onFocus={() => setSelectedOptionIndex(index)}
|
onFocus={() => setSelectedOptionIndex(index)}
|
||||||
>
|
>
|
||||||
{option}
|
<span className="grow min-w-0 truncate">
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
{annotation &&
|
||||||
|
<span className={clsx(
|
||||||
|
'whitespace-nowrap text-dim text-sm',
|
||||||
|
'group-focus:inline-block group-hover:inline-block',
|
||||||
|
index === 0 && selectedOptionIndex === undefined
|
||||||
|
? 'inline-block'
|
||||||
|
: 'hidden',
|
||||||
|
)}>
|
||||||
|
{annotation}
|
||||||
|
</span>}
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,13 +10,14 @@ import { useFormState } from 'react-dom';
|
|||||||
import { areSimpleObjectsEqual } from '@/utility/object';
|
import { areSimpleObjectsEqual } from '@/utility/object';
|
||||||
import IconGrSync from '@/site/IconGrSync';
|
import IconGrSync from '@/site/IconGrSync';
|
||||||
import { getExifDataAction } from './actions';
|
import { getExifDataAction } from './actions';
|
||||||
|
import { Tags } from '@/tag';
|
||||||
|
|
||||||
export default function PhotoEditPageClient({
|
export default function PhotoEditPageClient({
|
||||||
photo,
|
photo,
|
||||||
uniqueTags,
|
uniqueTags,
|
||||||
}: {
|
}: {
|
||||||
photo: Photo
|
photo: Photo
|
||||||
uniqueTags?: string[]
|
uniqueTags?: Tags
|
||||||
}) {
|
}) {
|
||||||
const seedExifData = { url: photo.url };
|
const seedExifData = { url: photo.url };
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
FORM_METADATA_ENTRIES,
|
FORM_METADATA_ENTRIES,
|
||||||
PhotoFormData,
|
PhotoFormData,
|
||||||
convertFormKeysToLabels,
|
convertFormKeysToLabels,
|
||||||
getInitialErrors,
|
getFormErrors,
|
||||||
isFormValid,
|
isFormValid,
|
||||||
} from '.';
|
} from '.';
|
||||||
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
||||||
@ -23,7 +23,7 @@ import { toastSuccess, toastWarning } from '@/toast';
|
|||||||
import { getDimensionsFromSize } from '@/utility/size';
|
import { getDimensionsFromSize } from '@/utility/size';
|
||||||
import ImageBlurFallback from '@/components/ImageBlurFallback';
|
import ImageBlurFallback from '@/components/ImageBlurFallback';
|
||||||
import { BLUR_ENABLED } from '@/site/config';
|
import { BLUR_ENABLED } from '@/site/config';
|
||||||
import { sortTagsWithoutFavs } from '@/tag';
|
import { Tags, sortTagsObjectWithoutFavs } from '@/tag';
|
||||||
|
|
||||||
const THUMBNAIL_SIZE = 300;
|
const THUMBNAIL_SIZE = 300;
|
||||||
|
|
||||||
@ -37,13 +37,13 @@ export default function PhotoForm({
|
|||||||
initialPhotoForm: Partial<PhotoFormData>
|
initialPhotoForm: Partial<PhotoFormData>
|
||||||
updatedExifData?: Partial<PhotoFormData>
|
updatedExifData?: Partial<PhotoFormData>
|
||||||
type?: 'create' | 'edit'
|
type?: 'create' | 'edit'
|
||||||
uniqueTags?: string[]
|
uniqueTags?: Tags
|
||||||
debugBlur?: boolean
|
debugBlur?: boolean
|
||||||
}) {
|
}) {
|
||||||
const [formData, setFormData] =
|
const [formData, setFormData] =
|
||||||
useState<Partial<PhotoFormData>>(initialPhotoForm);
|
useState<Partial<PhotoFormData>>(initialPhotoForm);
|
||||||
const [formErrors, setFormErrors] =
|
const [formErrors, setFormErrors] =
|
||||||
useState(getInitialErrors(initialPhotoForm));
|
useState(getFormErrors(initialPhotoForm));
|
||||||
|
|
||||||
// Update form when EXIF data
|
// Update form when EXIF data
|
||||||
// is refreshed by parent
|
// is refreshed by parent
|
||||||
@ -146,51 +146,57 @@ export default function PhotoForm({
|
|||||||
onSubmit={() => blur()}
|
onSubmit={() => blur()}
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
{FORM_METADATA_ENTRIES.map(([key, {
|
{FORM_METADATA_ENTRIES(
|
||||||
label,
|
sortTagsObjectWithoutFavs(uniqueTags ?? [])
|
||||||
note,
|
.map(({ tag, count }) => ({
|
||||||
required,
|
value: tag,
|
||||||
options,
|
annotation: `× ${count}`,
|
||||||
optionsDefaultLabel,
|
}))
|
||||||
readOnly,
|
)
|
||||||
validate,
|
.map(([key, {
|
||||||
capitalize,
|
label,
|
||||||
hideIfEmpty,
|
note,
|
||||||
hideBasedOnCamera,
|
required,
|
||||||
loadingMessage,
|
selectOptions,
|
||||||
type,
|
selectOptionsDefaultLabel,
|
||||||
}]) =>
|
tagOptions,
|
||||||
(
|
readOnly,
|
||||||
(!hideIfEmpty || formData[key]) &&
|
validate,
|
||||||
!hideBasedOnCamera?.(formData.make)
|
capitalize,
|
||||||
) &&
|
hideIfEmpty,
|
||||||
<FieldSetWithStatus
|
hideBasedOnCamera,
|
||||||
key={key}
|
loadingMessage,
|
||||||
id={key}
|
type,
|
||||||
label={label}
|
}]) =>
|
||||||
note={note}
|
(
|
||||||
error={formErrors[key]}
|
(!hideIfEmpty || formData[key]) &&
|
||||||
value={formData[key] ?? ''}
|
!hideBasedOnCamera?.(formData.make)
|
||||||
onChange={value => {
|
) &&
|
||||||
setFormData({ ...formData, [key]: value });
|
<FieldSetWithStatus
|
||||||
if (validate) {
|
key={key}
|
||||||
setFormErrors({ ...formErrors, [key]: validate(value) });
|
id={key}
|
||||||
}
|
label={label}
|
||||||
}}
|
note={note}
|
||||||
selectOptions={options}
|
error={formErrors[key]}
|
||||||
selectOptionsDefaultLabel={optionsDefaultLabel}
|
value={formData[key] ?? ''}
|
||||||
commaSeparatedOptions={key === 'tags'
|
onChange={value => {
|
||||||
? sortTagsWithoutFavs(uniqueTags ?? [])
|
setFormData({ ...formData, [key]: value });
|
||||||
: undefined}
|
if (validate) {
|
||||||
required={required}
|
setFormErrors({ ...formErrors, [key]: validate(value) });
|
||||||
readOnly={readOnly}
|
}
|
||||||
capitalize={capitalize}
|
}}
|
||||||
placeholder={loadingMessage && !formData[key]
|
selectOptions={selectOptions}
|
||||||
? loadingMessage
|
selectOptionsDefaultLabel={selectOptionsDefaultLabel}
|
||||||
: undefined}
|
tagOptions={tagOptions}
|
||||||
loading={loadingMessage && !formData[key] ? true : false}
|
required={required}
|
||||||
type={type}
|
readOnly={readOnly}
|
||||||
/>)}
|
capitalize={capitalize}
|
||||||
|
placeholder={loadingMessage && !formData[key]
|
||||||
|
? loadingMessage
|
||||||
|
: undefined}
|
||||||
|
loading={loadingMessage && !formData[key] ? true : false}
|
||||||
|
type={type}
|
||||||
|
/>)}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Link
|
<Link
|
||||||
className="button"
|
className="button"
|
||||||
|
|||||||
@ -26,6 +26,8 @@ export type FieldSetType =
|
|||||||
'password' |
|
'password' |
|
||||||
'checkbox';
|
'checkbox';
|
||||||
|
|
||||||
|
export type AnnotatedTag = { value: string, annotation?: string };
|
||||||
|
|
||||||
type FormMeta = {
|
type FormMeta = {
|
||||||
label: string
|
label: string
|
||||||
note?: string
|
note?: string
|
||||||
@ -39,14 +41,18 @@ type FormMeta = {
|
|||||||
hideBasedOnCamera?: (make?: string, mode?: string) => boolean
|
hideBasedOnCamera?: (make?: string, mode?: string) => boolean
|
||||||
loadingMessage?: string
|
loadingMessage?: string
|
||||||
type?: FieldSetType
|
type?: FieldSetType
|
||||||
options?: { value: string, label: string }[]
|
selectOptions?: { value: string, label: string }[]
|
||||||
optionsDefaultLabel?: string
|
selectOptionsDefaultLabel?: string
|
||||||
|
tagOptions?: AnnotatedTag[]
|
||||||
};
|
};
|
||||||
|
|
||||||
const FORM_METADATA: Record<keyof PhotoFormData, FormMeta> = {
|
const FORM_METADATA = (
|
||||||
|
tagOptions?: AnnotatedTag[]
|
||||||
|
): Record<keyof PhotoFormData, FormMeta> => ({
|
||||||
title: { label: 'title', capitalize: true },
|
title: { label: 'title', capitalize: true },
|
||||||
tags: {
|
tags: {
|
||||||
label: 'tags',
|
label: 'tags',
|
||||||
|
tagOptions,
|
||||||
validate: tags => doesTagsStringIncludeFavs(tags)
|
validate: tags => doesTagsStringIncludeFavs(tags)
|
||||||
? `'${TAG_FAVS}' is a reserved tag`
|
? `'${TAG_FAVS}' is a reserved tag`
|
||||||
: undefined,
|
: undefined,
|
||||||
@ -66,8 +72,8 @@ const FORM_METADATA: Record<keyof PhotoFormData, FormMeta> = {
|
|||||||
model: { label: 'camera model' },
|
model: { label: 'camera model' },
|
||||||
filmSimulation: {
|
filmSimulation: {
|
||||||
label: 'fujifilm simulation',
|
label: 'fujifilm simulation',
|
||||||
options: FILM_SIMULATION_FORM_INPUT_OPTIONS,
|
selectOptions: FILM_SIMULATION_FORM_INPUT_OPTIONS,
|
||||||
optionsDefaultLabel: 'Unknown',
|
selectOptionsDefaultLabel: 'Unknown',
|
||||||
hideBasedOnCamera: make => make !== MAKE_FUJIFILM,
|
hideBasedOnCamera: make => make !== MAKE_FUJIFILM,
|
||||||
},
|
},
|
||||||
focalLength: { label: 'focal length' },
|
focalLength: { label: 'focal length' },
|
||||||
@ -84,26 +90,28 @@ const FORM_METADATA: Record<keyof PhotoFormData, FormMeta> = {
|
|||||||
priorityOrder: { label: 'priority order' },
|
priorityOrder: { label: 'priority order' },
|
||||||
favorite: { label: 'favorite', type: 'checkbox', virtual: true },
|
favorite: { label: 'favorite', type: 'checkbox', virtual: true },
|
||||||
hidden: { label: 'hidden', type: 'checkbox' },
|
hidden: { label: 'hidden', type: 'checkbox' },
|
||||||
};
|
});
|
||||||
|
|
||||||
export const FORM_METADATA_ENTRIES =
|
export const FORM_METADATA_ENTRIES = (
|
||||||
(Object.entries(FORM_METADATA) as [keyof PhotoFormData, FormMeta][])
|
...args: Parameters<typeof FORM_METADATA>
|
||||||
|
) =>
|
||||||
|
(Object.entries(FORM_METADATA(...args)) as [keyof PhotoFormData, FormMeta][])
|
||||||
.filter(([_, meta]) => !meta.hide);
|
.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());
|
||||||
|
|
||||||
export const getInitialErrors = (
|
export const getFormErrors = (
|
||||||
formData: Partial<PhotoFormData>
|
formData: Partial<PhotoFormData>
|
||||||
): Partial<Record<keyof PhotoFormData, string>> =>
|
): Partial<Record<keyof PhotoFormData, string>> =>
|
||||||
Object.keys(formData).reduce((acc, key) => ({
|
Object.keys(formData).reduce((acc, key) => ({
|
||||||
...acc,
|
...acc,
|
||||||
[key]: FORM_METADATA_ENTRIES.find(([k]) => k === key)?.[1]
|
[key]: FORM_METADATA_ENTRIES().find(([k]) => k === key)?.[1]
|
||||||
.validate?.(formData[key as keyof PhotoFormData]),
|
.validate?.(formData[key as keyof PhotoFormData]),
|
||||||
}), {});
|
}), {});
|
||||||
|
|
||||||
export const isFormValid = (formData: Partial<PhotoFormData>) =>
|
export const isFormValid = (formData: Partial<PhotoFormData>) =>
|
||||||
FORM_METADATA_ENTRIES.every(
|
FORM_METADATA_ENTRIES().every(
|
||||||
([key, { required, validate }]) =>
|
([key, { required, validate }]) =>
|
||||||
(!required || Boolean(formData[key])) &&
|
(!required || Boolean(formData[key])) &&
|
||||||
(validate?.(formData[key]) === undefined)
|
(validate?.(formData[key]) === undefined)
|
||||||
@ -191,7 +199,7 @@ export const convertFormDataToPhotoDbInsert = (
|
|||||||
if (
|
if (
|
||||||
key.startsWith('$ACTION_ID_') ||
|
key.startsWith('$ACTION_ID_') ||
|
||||||
(photoForm as any)[key] === '' ||
|
(photoForm as any)[key] === '' ||
|
||||||
FORM_METADATA[key as keyof PhotoFormData]?.virtual
|
FORM_METADATA()[key as keyof PhotoFormData]?.virtual
|
||||||
) {
|
) {
|
||||||
delete (photoForm as any)[key];
|
delete (photoForm as any)[key];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,9 @@ export const sortTagsObject = (
|
|||||||
export const sortTagsWithoutFavs = (tags: string[]) =>
|
export const sortTagsWithoutFavs = (tags: string[]) =>
|
||||||
sortTags(tags, TAG_FAVS);
|
sortTags(tags, TAG_FAVS);
|
||||||
|
|
||||||
|
export const sortTagsObjectWithoutFavs = (tags: Tags) =>
|
||||||
|
sortTagsObject(tags, TAG_FAVS);
|
||||||
|
|
||||||
export const descriptionForTaggedPhotos = (
|
export const descriptionForTaggedPhotos = (
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user