mirror of
https://github.com/stashapp/stash.git
synced 2025-12-15 21:03:22 +01:00
parent
6b59b9643c
commit
9ede271c05
39 changed files with 632 additions and 651 deletions
|
|
@ -21,6 +21,7 @@ import {
|
|||
useSystemStatus,
|
||||
} from "src/core/StashService";
|
||||
import flattenMessages from "./utils/flattenMessages";
|
||||
import * as yup from "yup";
|
||||
import Mousetrap from "mousetrap";
|
||||
import MousetrapPause from "mousetrap-pause";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
|
|
@ -126,7 +127,18 @@ export const App: React.FC = () => {
|
|||
}
|
||||
);
|
||||
|
||||
setMessages(flattenMessages(mergedMessages));
|
||||
const newMessages = flattenMessages(mergedMessages) as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
|
||||
yup.setLocale({
|
||||
mixed: {
|
||||
required: newMessages["validation.required"],
|
||||
},
|
||||
});
|
||||
|
||||
setMessages(newMessages);
|
||||
};
|
||||
|
||||
setLocale();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
|||
import { galleryTitle } from "src/core/galleries";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
|
||||
interface IProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
|
|
@ -79,44 +80,50 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
isNew || (gallery?.files?.length === 0 && !gallery?.folder);
|
||||
|
||||
const schema = yup.object({
|
||||
title: titleRequired
|
||||
? yup.string().required()
|
||||
: yup.string().optional().nullable(),
|
||||
details: yup.string().optional().nullable(),
|
||||
url: yup.string().optional().nullable(),
|
||||
date: yup.string().optional().nullable(),
|
||||
rating100: yup.number().optional().nullable(),
|
||||
studio_id: yup.string().optional().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
scene_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
title: titleRequired ? yup.string().required() : yup.string().ensure(),
|
||||
url: yup.string().ensure(),
|
||||
date: yup
|
||||
.string()
|
||||
.ensure()
|
||||
.test({
|
||||
name: "date",
|
||||
test: (value) => {
|
||||
if (!value) return true;
|
||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||
if (Number.isNaN(Date.parse(value))) return false;
|
||||
return true;
|
||||
},
|
||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||
}),
|
||||
rating100: yup.number().nullable().defined(),
|
||||
studio_id: yup.string().required().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).defined(),
|
||||
tag_ids: yup.array(yup.string().required()).defined(),
|
||||
scene_ids: yup.array(yup.string().required()).defined(),
|
||||
details: yup.string().ensure(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
title: gallery?.title ?? "",
|
||||
details: gallery?.details ?? "",
|
||||
url: gallery?.url ?? "",
|
||||
date: gallery?.date ?? "",
|
||||
rating100: gallery?.rating100 ?? null,
|
||||
studio_id: gallery?.studio?.id,
|
||||
studio_id: gallery?.studio?.id ?? null,
|
||||
performer_ids: (gallery?.performers ?? []).map((p) => p.id),
|
||||
tag_ids: (gallery?.tags ?? []).map((t) => t.id),
|
||||
scene_ids: (gallery?.scenes ?? []).map((s) => s.id),
|
||||
details: gallery?.details ?? "",
|
||||
};
|
||||
|
||||
type InputValues = typeof initialValues;
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const formik = useFormik({
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validationSchema: schema,
|
||||
onSubmit: (values) => onSave(getGalleryInput(values)),
|
||||
onSubmit: (values) => onSave(values),
|
||||
});
|
||||
|
||||
// always dirty if creating a new gallery with a title
|
||||
if (isNew && gallery?.title) {
|
||||
formik.dirty = true;
|
||||
}
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating100", v);
|
||||
}
|
||||
|
|
@ -166,24 +173,13 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
setQueryableScrapers(newQueryableScrapers);
|
||||
}, [Scrapers]);
|
||||
|
||||
function getGalleryInput(
|
||||
input: InputValues
|
||||
): GQL.GalleryCreateInput | GQL.GalleryUpdateInput {
|
||||
return {
|
||||
id: isNew ? undefined : gallery?.id ?? "",
|
||||
...input,
|
||||
};
|
||||
}
|
||||
|
||||
async function onSave(
|
||||
input: GQL.GalleryCreateInput | GQL.GalleryUpdateInput
|
||||
) {
|
||||
async function onSave(input: GQL.GalleryCreateInput) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (isNew) {
|
||||
const result = await createGallery({
|
||||
variables: {
|
||||
input: input as GQL.GalleryCreateInput,
|
||||
input,
|
||||
},
|
||||
});
|
||||
if (result.data?.galleryCreate) {
|
||||
|
|
@ -202,7 +198,10 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
} else {
|
||||
const result = await updateGallery({
|
||||
variables: {
|
||||
input: input as GQL.GalleryUpdateInput,
|
||||
input: {
|
||||
id: gallery.id!,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.galleryUpdate) {
|
||||
|
|
@ -216,7 +215,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
}
|
||||
),
|
||||
});
|
||||
formik.resetForm({ values: formik.values });
|
||||
formik.resetForm();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -271,7 +270,10 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const currentGallery = getGalleryInput(formik.values);
|
||||
const currentGallery = {
|
||||
id: gallery.id!,
|
||||
...formik.values,
|
||||
};
|
||||
|
||||
return (
|
||||
<GalleryScrapeDialog
|
||||
|
|
@ -384,7 +386,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
|
||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
||||
return (
|
||||
<Form.Group controlId={title} as={Row}>
|
||||
<Form.Group controlId={field} as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title,
|
||||
})}
|
||||
|
|
@ -419,7 +421,9 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
<Button
|
||||
className="edit-button"
|
||||
variant="primary"
|
||||
disabled={!formik.dirty}
|
||||
disabled={
|
||||
(!isNew && !formik.dirty) || !isEqual(formik.errors, {})
|
||||
}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
<FormattedMessage id="actions.save" />
|
||||
|
|
@ -561,8 +565,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
<Form.Control
|
||||
as="textarea"
|
||||
className="gallery-description text-input"
|
||||
onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
formik.setFieldValue("details", newValue.currentTarget.value)
|
||||
onChange={(e) =>
|
||||
formik.setFieldValue("details", e.currentTarget.value)
|
||||
}
|
||||
value={formik.values.details}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { Prompt } from "react-router-dom";
|
|||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
|
||||
interface IProps {
|
||||
image: GQL.ImageDataFragment;
|
||||
|
|
@ -42,31 +43,44 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
const [updateImage] = useImageUpdate();
|
||||
|
||||
const schema = yup.object({
|
||||
title: yup.string().optional().nullable(),
|
||||
rating100: yup.number().optional().nullable(),
|
||||
url: yup.string().optional().nullable(),
|
||||
date: yup.string().optional().nullable(),
|
||||
studio_id: yup.string().optional().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
title: yup.string().ensure(),
|
||||
url: yup.string().ensure(),
|
||||
date: yup
|
||||
.string()
|
||||
.ensure()
|
||||
.test({
|
||||
name: "date",
|
||||
test: (value) => {
|
||||
if (!value) return true;
|
||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||
if (Number.isNaN(Date.parse(value))) return false;
|
||||
return true;
|
||||
},
|
||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||
}),
|
||||
rating100: yup.number().nullable().defined(),
|
||||
studio_id: yup.string().required().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).defined(),
|
||||
tag_ids: yup.array(yup.string().required()).defined(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
title: image.title ?? "",
|
||||
rating100: image.rating100 ?? null,
|
||||
url: image?.url ?? "",
|
||||
date: image?.date ?? "",
|
||||
studio_id: image.studio?.id,
|
||||
rating100: image.rating100 ?? null,
|
||||
studio_id: image.studio?.id ?? null,
|
||||
performer_ids: (image.performers ?? []).map((p) => p.id),
|
||||
tag_ids: (image.tags ?? []).map((t) => t.id),
|
||||
};
|
||||
|
||||
type InputValues = typeof initialValues;
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const formik = useFormik({
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validationSchema: schema,
|
||||
onSubmit: (values) => onSave(getImageInput(values)),
|
||||
onSubmit: (values) => onSave(values),
|
||||
});
|
||||
|
||||
function setRating(v: number) {
|
||||
|
|
@ -95,19 +109,15 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
}
|
||||
});
|
||||
|
||||
function getImageInput(input: InputValues): GQL.ImageUpdateInput {
|
||||
return {
|
||||
id: image.id,
|
||||
...input,
|
||||
};
|
||||
}
|
||||
|
||||
async function onSave(input: GQL.ImageUpdateInput) {
|
||||
async function onSave(input: InputValues) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await updateImage({
|
||||
variables: {
|
||||
input,
|
||||
input: {
|
||||
id: image.id,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.imageUpdate) {
|
||||
|
|
@ -117,7 +127,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
{ entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
formik.resetForm({ values: formik.values });
|
||||
formik.resetForm();
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
|
|
@ -127,7 +137,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
|
||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
||||
return (
|
||||
<Form.Group controlId={title} as={Row}>
|
||||
<Form.Group controlId={field} as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title,
|
||||
})}
|
||||
|
|
@ -161,7 +171,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
<Button
|
||||
className="edit-button"
|
||||
variant="primary"
|
||||
disabled={!formik.dirty}
|
||||
disabled={!formik.dirty || !isEqual(formik.errors, {})}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
<FormattedMessage id="actions.save" />
|
||||
|
|
|
|||
|
|
@ -33,12 +33,8 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Editing movie state
|
||||
const [frontImage, setFrontImage] = useState<string | undefined | null>(
|
||||
undefined
|
||||
);
|
||||
const [backImage, setBackImage] = useState<string | undefined | null>(
|
||||
undefined
|
||||
);
|
||||
const [frontImage, setFrontImage] = useState<string | null>();
|
||||
const [backImage, setBackImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
const [updateMovie, { loading: updating }] = useMovieUpdate();
|
||||
|
|
@ -59,26 +55,14 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||
};
|
||||
});
|
||||
|
||||
const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding);
|
||||
|
||||
function getMovieInput(
|
||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||
) {
|
||||
const ret: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
||||
...input,
|
||||
id: movie.id,
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async function onSave(
|
||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||
) {
|
||||
async function onSave(input: GQL.MovieCreateInput) {
|
||||
try {
|
||||
const result = await updateMovie({
|
||||
variables: {
|
||||
input: getMovieInput(input) as GQL.MovieUpdateInput,
|
||||
input: {
|
||||
id: movie.id,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.movieUpdate) {
|
||||
|
|
@ -214,7 +198,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||
onDelete={onDelete}
|
||||
setFrontImage={setFrontImage}
|
||||
setBackImage={setBackImage}
|
||||
onImageEncoding={onImageEncoding}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,24 +23,10 @@ const MovieCreate: React.FC = () => {
|
|||
|
||||
const [createMovie] = useMovieCreate();
|
||||
|
||||
const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding);
|
||||
|
||||
function getMovieInput(
|
||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||
) {
|
||||
const ret: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
||||
...input,
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async function onSave(
|
||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||
) {
|
||||
async function onSave(input: GQL.MovieCreateInput) {
|
||||
try {
|
||||
const result = await createMovie({
|
||||
variables: getMovieInput(input) as GQL.MovieCreateInput,
|
||||
variables: input,
|
||||
});
|
||||
if (result.data?.movieCreate?.id) {
|
||||
history.push(`/movies/${result.data.movieCreate.id}`);
|
||||
|
|
@ -92,7 +78,7 @@ const MovieCreate: React.FC = () => {
|
|||
onDelete={() => {}}
|
||||
setFrontImage={setFrontImage}
|
||||
setBackImage={setBackImage}
|
||||
onImageEncoding={onImageEncoding}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,17 +23,16 @@ import { Prompt } from "react-router-dom";
|
|||
import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
|
||||
interface IMovieEditPanel {
|
||||
movie: Partial<GQL.MovieDataFragment>;
|
||||
onSubmit: (
|
||||
movie: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||
) => void;
|
||||
onSubmit: (movie: GQL.MovieCreateInput) => void;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
setFrontImage: (image?: string | null) => void;
|
||||
setBackImage: (image?: string | null) => void;
|
||||
onImageEncoding: (loading?: boolean) => void;
|
||||
setEncodingImage: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
|
|
@ -43,7 +42,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
onDelete,
|
||||
setFrontImage,
|
||||
setBackImage,
|
||||
onImageEncoding,
|
||||
setEncodingImage,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
|
@ -61,59 +60,51 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
|
||||
const schema = yup.object({
|
||||
name: yup.string().required(),
|
||||
aliases: yup.string().optional().nullable(),
|
||||
duration: yup.string().optional().nullable(),
|
||||
aliases: yup.string().ensure(),
|
||||
duration: yup.number().nullable().defined(),
|
||||
date: yup
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.matches(/^\d{4}-\d{2}-\d{2}$/),
|
||||
rating100: yup.number().optional().nullable(),
|
||||
studio_id: yup.string().optional().nullable(),
|
||||
director: yup.string().optional().nullable(),
|
||||
synopsis: yup.string().optional().nullable(),
|
||||
url: yup.string().optional().nullable(),
|
||||
front_image: yup.string().optional().nullable(),
|
||||
back_image: yup.string().optional().nullable(),
|
||||
.ensure()
|
||||
.test({
|
||||
name: "date",
|
||||
test: (value) => {
|
||||
if (!value) return true;
|
||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||
if (Number.isNaN(Date.parse(value))) return false;
|
||||
return true;
|
||||
},
|
||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||
}),
|
||||
studio_id: yup.string().required().nullable(),
|
||||
director: yup.string().ensure(),
|
||||
rating100: yup.number().nullable().defined(),
|
||||
url: yup.string().ensure(),
|
||||
synopsis: yup.string().ensure(),
|
||||
front_image: yup.string().nullable().optional(),
|
||||
back_image: yup.string().nullable().optional(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
name: movie?.name,
|
||||
aliases: movie?.aliases,
|
||||
duration: movie?.duration,
|
||||
date: movie?.date,
|
||||
name: movie?.name ?? "",
|
||||
aliases: movie?.aliases ?? "",
|
||||
duration: movie?.duration ?? null,
|
||||
date: movie?.date ?? "",
|
||||
studio_id: movie?.studio?.id ?? null,
|
||||
director: movie?.director ?? "",
|
||||
rating100: movie?.rating100 ?? null,
|
||||
studio_id: movie?.studio?.id,
|
||||
director: movie?.director,
|
||||
synopsis: movie?.synopsis,
|
||||
url: movie?.url,
|
||||
front_image: undefined,
|
||||
back_image: undefined,
|
||||
url: movie?.url ?? "",
|
||||
synopsis: movie?.synopsis ?? "",
|
||||
};
|
||||
|
||||
type InputValues = typeof initialValues;
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const formik = useFormik({
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validationSchema: schema,
|
||||
onSubmit: (values) => onSubmit(getMovieInput(values)),
|
||||
onSubmit: (values) => onSubmit(values),
|
||||
});
|
||||
|
||||
const encodingImage = ImageUtils.usePasteImage(showImageAlert);
|
||||
|
||||
useEffect(() => {
|
||||
setFrontImage(formik.values.front_image);
|
||||
}, [formik.values.front_image, setFrontImage]);
|
||||
|
||||
useEffect(() => {
|
||||
setBackImage(formik.values.back_image);
|
||||
}, [formik.values.back_image, setBackImage]);
|
||||
|
||||
useEffect(
|
||||
() => onImageEncoding(encodingImage),
|
||||
[onImageEncoding, encodingImage]
|
||||
);
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating100", v);
|
||||
}
|
||||
|
|
@ -138,35 +129,6 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
};
|
||||
});
|
||||
|
||||
function showImageAlert(imageData: string) {
|
||||
setImageClipboard(imageData);
|
||||
setIsImageAlertOpen(true);
|
||||
}
|
||||
|
||||
function setImageFromClipboard(isFrontImage: boolean) {
|
||||
if (isFrontImage) {
|
||||
formik.setFieldValue("front_image", imageClipboard);
|
||||
} else {
|
||||
formik.setFieldValue("back_image", imageClipboard);
|
||||
}
|
||||
|
||||
setImageClipboard(undefined);
|
||||
setIsImageAlertOpen(false);
|
||||
}
|
||||
|
||||
function getMovieInput(values: InputValues) {
|
||||
const input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
||||
...values,
|
||||
rating100: values.rating100 ?? null,
|
||||
studio_id: values.studio_id ?? null,
|
||||
};
|
||||
|
||||
if (movie && movie.id) {
|
||||
(input as GQL.MovieUpdateInput).id = movie.id;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
function updateMovieEditStateFromScraper(
|
||||
state: Partial<GQL.ScrapedMovieDataFragment>
|
||||
) {
|
||||
|
|
@ -175,39 +137,42 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
}
|
||||
|
||||
if (state.aliases) {
|
||||
formik.setFieldValue("aliases", state.aliases ?? undefined);
|
||||
formik.setFieldValue("aliases", state.aliases);
|
||||
}
|
||||
|
||||
if (state.duration) {
|
||||
formik.setFieldValue(
|
||||
"duration",
|
||||
DurationUtils.stringToSeconds(state.duration) ?? undefined
|
||||
DurationUtils.stringToSeconds(state.duration)
|
||||
);
|
||||
}
|
||||
|
||||
if (state.date) {
|
||||
formik.setFieldValue("date", state.date ?? undefined);
|
||||
formik.setFieldValue("date", state.date);
|
||||
}
|
||||
|
||||
if (state.studio && state.studio.stored_id) {
|
||||
formik.setFieldValue("studio_id", state.studio.stored_id ?? undefined);
|
||||
formik.setFieldValue("studio_id", state.studio.stored_id);
|
||||
}
|
||||
|
||||
if (state.director) {
|
||||
formik.setFieldValue("director", state.director ?? undefined);
|
||||
formik.setFieldValue("director", state.director);
|
||||
}
|
||||
if (state.synopsis) {
|
||||
formik.setFieldValue("synopsis", state.synopsis ?? undefined);
|
||||
formik.setFieldValue("synopsis", state.synopsis);
|
||||
}
|
||||
if (state.url) {
|
||||
formik.setFieldValue("url", state.url ?? undefined);
|
||||
formik.setFieldValue("url", state.url);
|
||||
}
|
||||
|
||||
const imageStr = (state as GQL.ScrapedMovieDataFragment).front_image;
|
||||
formik.setFieldValue("front_image", imageStr ?? undefined);
|
||||
|
||||
const backImageStr = (state as GQL.ScrapedMovieDataFragment).back_image;
|
||||
formik.setFieldValue("back_image", backImageStr ?? undefined);
|
||||
if (state.front_image) {
|
||||
// image is a base64 string
|
||||
formik.setFieldValue("front_image", state.front_image);
|
||||
}
|
||||
if (state.back_image) {
|
||||
// image is a base64 string
|
||||
formik.setFieldValue("back_image", state.back_image);
|
||||
}
|
||||
}
|
||||
|
||||
async function onScrapeMovieURL() {
|
||||
|
|
@ -248,7 +213,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const currentMovie = getMovieInput(formik.values);
|
||||
const currentMovie = {
|
||||
id: movie.id!,
|
||||
...formik.values,
|
||||
};
|
||||
|
||||
// Get image paths for scrape gui
|
||||
currentMovie.front_image = movie?.front_image_path;
|
||||
|
|
@ -272,16 +240,50 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
setScrapedMovie(undefined);
|
||||
}
|
||||
|
||||
const encodingImage = ImageUtils.usePasteImage(showImageAlert);
|
||||
|
||||
useEffect(() => {
|
||||
setFrontImage(formik.values.front_image);
|
||||
}, [formik.values.front_image, setFrontImage]);
|
||||
|
||||
useEffect(() => {
|
||||
setBackImage(formik.values.back_image);
|
||||
}, [formik.values.back_image, setBackImage]);
|
||||
|
||||
useEffect(() => {
|
||||
setEncodingImage(encodingImage);
|
||||
}, [setEncodingImage, encodingImage]);
|
||||
|
||||
function onFrontImageLoad(imageData: string | null) {
|
||||
formik.setFieldValue("front_image", imageData);
|
||||
}
|
||||
|
||||
function onFrontImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, (data) =>
|
||||
formik.setFieldValue("front_image", data)
|
||||
);
|
||||
ImageUtils.onImageChange(event, onFrontImageLoad);
|
||||
}
|
||||
|
||||
function onBackImageLoad(imageData: string | null) {
|
||||
formik.setFieldValue("back_image", imageData);
|
||||
}
|
||||
|
||||
function onBackImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, (data) =>
|
||||
formik.setFieldValue("back_image", data)
|
||||
);
|
||||
ImageUtils.onImageChange(event, onBackImageLoad);
|
||||
}
|
||||
|
||||
function showImageAlert(imageData: string) {
|
||||
setImageClipboard(imageData);
|
||||
setIsImageAlertOpen(true);
|
||||
}
|
||||
|
||||
function setImageFromClipboard(isFrontImage: boolean) {
|
||||
if (isFrontImage) {
|
||||
formik.setFieldValue("front_image", imageClipboard);
|
||||
} else {
|
||||
formik.setFieldValue("back_image", imageClipboard);
|
||||
}
|
||||
|
||||
setImageClipboard(undefined);
|
||||
setIsImageAlertOpen(false);
|
||||
}
|
||||
|
||||
function renderImageAlert() {
|
||||
|
|
@ -325,7 +327,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
|
||||
const isEditing = true;
|
||||
|
||||
function renderTextField(field: string, title: string) {
|
||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
||||
return (
|
||||
<Form.Group controlId={field} as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
|
|
@ -334,10 +336,13 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
<Col xs={9}>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
placeholder={title}
|
||||
placeholder={placeholder ?? title}
|
||||
{...formik.getFieldProps(field)}
|
||||
isInvalid={!!formik.getFieldMeta(field).error}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formik.getFieldMeta(field).error}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
);
|
||||
|
|
@ -392,14 +397,18 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
<Col xs={9}>
|
||||
<DurationInput
|
||||
numericValue={formik.values.duration ?? undefined}
|
||||
onValueChange={(valueAsNumber: number) => {
|
||||
formik.setFieldValue("duration", valueAsNumber);
|
||||
onValueChange={(valueAsNumber) => {
|
||||
formik.setFieldValue("duration", valueAsNumber ?? null);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField("date", intl.formatMessage({ id: "date" }))}
|
||||
{renderTextField(
|
||||
"date",
|
||||
intl.formatMessage({ id: "date" }),
|
||||
"YYYY-MM-DD"
|
||||
)}
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
|
|
@ -410,7 +419,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
onSelect={(items) =>
|
||||
formik.setFieldValue(
|
||||
"studio_id",
|
||||
items.length > 0 ? items[0]?.id : undefined
|
||||
items.length > 0 ? items[0]?.id : null
|
||||
)
|
||||
}
|
||||
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
||||
|
|
@ -466,18 +475,14 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
isNew={isNew}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onCancel}
|
||||
onSave={() => formik.handleSubmit()}
|
||||
saveDisabled={!formik.dirty}
|
||||
onSave={formik.handleSubmit}
|
||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||
onImageChange={onFrontImageChange}
|
||||
onImageChangeURL={(i) => formik.setFieldValue("front_image", i)}
|
||||
onClearImage={() => {
|
||||
formik.setFieldValue("front_image", null);
|
||||
}}
|
||||
onImageChangeURL={onFrontImageLoad}
|
||||
onClearImage={() => onFrontImageLoad(null)}
|
||||
onBackImageChange={onBackImageChange}
|
||||
onBackImageChangeURL={(i) => formik.setFieldValue("back_image", i)}
|
||||
onClearBackImage={() => {
|
||||
formik.setFieldValue("back_image", null);
|
||||
}}
|
||||
onBackImageChangeURL={onBackImageLoad}
|
||||
onClearBackImage={() => onBackImageLoad(null)}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
<Form.Control
|
||||
as="select"
|
||||
className="input-control"
|
||||
value={genderToString(updateInput.gender ?? undefined)}
|
||||
value={genderToString(updateInput.gender)}
|
||||
onChange={(event) =>
|
||||
setUpdateField({
|
||||
gender: stringToGender(event.currentTarget.value),
|
||||
|
|
|
|||
|
|
@ -54,17 +54,17 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
const abbreviateCounter =
|
||||
(configuration?.ui as IUIConfig)?.abbreviateCounters ?? false;
|
||||
|
||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
// if undefined then get the existing image
|
||||
// if null then get the default (no) image
|
||||
// otherwise get the set image
|
||||
const activeImage =
|
||||
imagePreview === undefined
|
||||
image === undefined
|
||||
? performer.image_path ?? ""
|
||||
: imagePreview ?? `${performer.image_path}&default=true`;
|
||||
: image ?? `${performer.image_path}&default=true`;
|
||||
const lightboxImages = useMemo(
|
||||
() => [{ paths: { thumbnail: activeImage, image: activeImage } }],
|
||||
[activeImage]
|
||||
|
|
@ -91,10 +91,6 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const onImageChange = (image?: string | null) => setImagePreview(image);
|
||||
|
||||
const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding);
|
||||
|
||||
async function onAutoTag() {
|
||||
try {
|
||||
await mutateMetadataAutoTag({ performers: [performer.id] });
|
||||
|
|
@ -254,11 +250,9 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible={isEditing}
|
||||
onImageChange={onImageChange}
|
||||
onImageEncoding={onImageEncoding}
|
||||
onCancelEditing={() => {
|
||||
setIsEditing(false);
|
||||
}}
|
||||
onCancelEditing={() => setIsEditing(false)}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
|
@ -393,7 +387,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
</Helmet>
|
||||
|
||||
<div className="performer-image-container col-md-4 text-center">
|
||||
{imageEncoding ? (
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
<Button variant="link" onClick={() => showLightbox()}>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { PerformerEditPanel } from "./PerformerEditPanel";
|
|||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const PerformerCreate: React.FC = () => {
|
||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
const location = useLocation();
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
|
|
@ -14,21 +14,17 @@ const PerformerCreate: React.FC = () => {
|
|||
name: query.get("q") ?? undefined,
|
||||
};
|
||||
|
||||
const activeImage = imagePreview ?? "";
|
||||
const intl = useIntl();
|
||||
|
||||
const onImageChange = (image?: string | null) => setImagePreview(image);
|
||||
const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding);
|
||||
|
||||
function renderPerformerImage() {
|
||||
if (imageEncoding) {
|
||||
if (encodingImage) {
|
||||
return <LoadingIndicator message="Encoding image..." />;
|
||||
}
|
||||
if (activeImage) {
|
||||
if (image) {
|
||||
return (
|
||||
<img
|
||||
className="performer"
|
||||
src={activeImage}
|
||||
src={image}
|
||||
alt={intl.formatMessage({ id: "performer" })}
|
||||
/>
|
||||
);
|
||||
|
|
@ -50,8 +46,8 @@ const PerformerCreate: React.FC = () => {
|
|||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible
|
||||
onImageChange={onImageChange}
|
||||
onImageEncoding={onImageEncoding}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ import { useToast } from "src/hooks/Toast";
|
|||
import { Prompt, useHistory } from "react-router-dom";
|
||||
import { useFormik } from "formik";
|
||||
import {
|
||||
genderStrings,
|
||||
genderToString,
|
||||
stringGenderMap,
|
||||
stringToGender,
|
||||
} from "src/utils/gender";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
|
@ -42,6 +42,7 @@ import {
|
|||
faTrashAlt,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { StringListInput } from "src/components/Shared/StringListInput";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
|
||||
const isScraper = (
|
||||
scraper: GQL.Scraper | GQL.StashBox
|
||||
|
|
@ -50,24 +51,24 @@ const isScraper = (
|
|||
interface IPerformerDetails {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
isVisible: boolean;
|
||||
onImageChange?: (image?: string | null) => void;
|
||||
onImageEncoding?: (loading?: boolean) => void;
|
||||
onCancelEditing?: () => void;
|
||||
setImage: (image?: string | null) => void;
|
||||
setEncodingImage: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
performer,
|
||||
isVisible,
|
||||
onImageChange,
|
||||
onImageEncoding,
|
||||
onCancelEditing,
|
||||
setImage,
|
||||
setEncodingImage,
|
||||
}) => {
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
|
||||
const isNew = performer.id === undefined;
|
||||
|
||||
// Editing stat
|
||||
// Editing state
|
||||
const [scraper, setScraper] = useState<GQL.Scraper | IStashBox>();
|
||||
const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>();
|
||||
const [isScraperModalOpen, setIsScraperModalOpen] = useState<boolean>(false);
|
||||
|
|
@ -81,18 +82,13 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
const Scrapers = useListPerformerScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||
|
||||
const [scrapedPerformer, setScrapedPerformer] = useState<
|
||||
GQL.ScrapedPerformer | undefined
|
||||
>();
|
||||
const [scrapedPerformer, setScrapedPerformer] =
|
||||
useState<GQL.ScrapedPerformer>();
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
const [createTag] = useTagCreate();
|
||||
const intl = useIntl();
|
||||
|
||||
const genderOptions = [""].concat(genderStrings);
|
||||
|
||||
const labelXS = 3;
|
||||
const labelXL = 2;
|
||||
const fieldXS = 9;
|
||||
|
|
@ -100,102 +96,121 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
|
||||
const schema = yup.object({
|
||||
name: yup.string().required(),
|
||||
disambiguation: yup.string().optional(),
|
||||
disambiguation: yup.string().ensure(),
|
||||
alias_list: yup
|
||||
.array(yup.string().required())
|
||||
.optional()
|
||||
.defined()
|
||||
.test({
|
||||
name: "unique",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
test: (value: any) => {
|
||||
return (value ?? []).length === new Set(value).size;
|
||||
test: (value, context) => {
|
||||
if (!value) return true;
|
||||
const aliases = new Set(value);
|
||||
aliases.add(context.parent.name);
|
||||
return value.length + 1 === aliases.size;
|
||||
},
|
||||
message: intl.formatMessage({ id: "dialogs.aliases_must_be_unique" }),
|
||||
message: intl.formatMessage({
|
||||
id: "validation.aliases_must_be_unique",
|
||||
}),
|
||||
}),
|
||||
gender: yup.string().optional().oneOf(genderOptions),
|
||||
birthdate: yup.string().optional(),
|
||||
ethnicity: yup.string().optional(),
|
||||
eye_color: yup.string().optional(),
|
||||
country: yup.string().optional(),
|
||||
height_cm: yup.number().optional(),
|
||||
measurements: yup.string().optional(),
|
||||
fake_tits: yup.string().optional(),
|
||||
career_length: yup.string().optional(),
|
||||
tattoos: yup.string().optional(),
|
||||
piercings: yup.string().optional(),
|
||||
url: yup.string().optional(),
|
||||
twitter: yup.string().optional(),
|
||||
instagram: yup.string().optional(),
|
||||
tag_ids: yup.array(yup.string().required()).optional(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().optional(),
|
||||
image: yup.string().optional().nullable(),
|
||||
details: yup.string().optional(),
|
||||
death_date: yup.string().optional(),
|
||||
hair_color: yup.string().optional(),
|
||||
weight: yup.number().optional(),
|
||||
ignore_auto_tag: yup.boolean().optional(),
|
||||
gender: yup.string<GQL.GenderEnum | "">().ensure(),
|
||||
birthdate: yup
|
||||
.string()
|
||||
.ensure()
|
||||
.test({
|
||||
name: "date",
|
||||
test: (value) => {
|
||||
if (!value) return true;
|
||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||
if (Number.isNaN(Date.parse(value))) return false;
|
||||
return true;
|
||||
},
|
||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||
}),
|
||||
death_date: yup
|
||||
.string()
|
||||
.ensure()
|
||||
.test({
|
||||
name: "date",
|
||||
test: (value) => {
|
||||
if (!value) return true;
|
||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||
if (Number.isNaN(Date.parse(value))) return false;
|
||||
return true;
|
||||
},
|
||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||
}),
|
||||
country: yup.string().ensure(),
|
||||
ethnicity: yup.string().ensure(),
|
||||
hair_color: yup.string().ensure(),
|
||||
eye_color: yup.string().ensure(),
|
||||
height_cm: yup.number().nullable().defined().default(null),
|
||||
weight: yup.number().nullable().defined().default(null),
|
||||
measurements: yup.string().ensure(),
|
||||
fake_tits: yup.string().ensure(),
|
||||
tattoos: yup.string().ensure(),
|
||||
piercings: yup.string().ensure(),
|
||||
career_length: yup.string().ensure(),
|
||||
url: yup.string().ensure(),
|
||||
twitter: yup.string().ensure(),
|
||||
instagram: yup.string().ensure(),
|
||||
details: yup.string().ensure(),
|
||||
tag_ids: yup.array(yup.string().required()).defined(),
|
||||
ignore_auto_tag: yup.boolean().defined(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
||||
image: yup.string().nullable().optional(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
name: performer.name ?? "",
|
||||
disambiguation: performer.disambiguation ?? "",
|
||||
alias_list: performer.alias_list?.slice().sort(),
|
||||
gender: genderToString(performer.gender ?? undefined),
|
||||
alias_list: performer.alias_list ?? [],
|
||||
gender: (performer.gender as GQL.GenderEnum) ?? "",
|
||||
birthdate: performer.birthdate ?? "",
|
||||
ethnicity: performer.ethnicity ?? "",
|
||||
eye_color: performer.eye_color ?? "",
|
||||
death_date: performer.death_date ?? "",
|
||||
country: performer.country ?? "",
|
||||
height_cm: performer.height_cm ?? undefined,
|
||||
ethnicity: performer.ethnicity ?? "",
|
||||
hair_color: performer.hair_color ?? "",
|
||||
eye_color: performer.eye_color ?? "",
|
||||
height_cm: performer.height_cm ?? null,
|
||||
weight: performer.weight ?? null,
|
||||
measurements: performer.measurements ?? "",
|
||||
fake_tits: performer.fake_tits ?? "",
|
||||
career_length: performer.career_length ?? "",
|
||||
tattoos: performer.tattoos ?? "",
|
||||
piercings: performer.piercings ?? "",
|
||||
career_length: performer.career_length ?? "",
|
||||
url: performer.url ?? "",
|
||||
twitter: performer.twitter ?? "",
|
||||
instagram: performer.instagram ?? "",
|
||||
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
||||
stash_ids: performer.stash_ids ?? undefined,
|
||||
image: undefined,
|
||||
details: performer.details ?? "",
|
||||
death_date: performer.death_date ?? "",
|
||||
hair_color: performer.hair_color ?? "",
|
||||
weight: performer.weight ?? undefined,
|
||||
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
||||
ignore_auto_tag: performer.ignore_auto_tag ?? false,
|
||||
stash_ids: getStashIDs(performer.stash_ids),
|
||||
};
|
||||
|
||||
type InputValues = typeof initialValues;
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const formik = useFormik({
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validationSchema: schema,
|
||||
onSubmit: (values) => onSave(values),
|
||||
});
|
||||
|
||||
// always dirty if creating a new performer with a name
|
||||
if (isNew && performer.name) {
|
||||
formik.dirty = true;
|
||||
}
|
||||
|
||||
function translateScrapedGender(scrapedGender?: string) {
|
||||
if (!scrapedGender) {
|
||||
return;
|
||||
}
|
||||
|
||||
let retEnum: GQL.GenderEnum | undefined;
|
||||
|
||||
// try to translate from enum values first
|
||||
const upperGender = scrapedGender?.toUpperCase();
|
||||
const upperGender = scrapedGender.toUpperCase();
|
||||
const asEnum = genderToString(upperGender);
|
||||
if (asEnum) {
|
||||
retEnum = stringToGender(asEnum);
|
||||
return stringToGender(asEnum);
|
||||
} else {
|
||||
// try to match against gender strings
|
||||
const caseInsensitive = true;
|
||||
retEnum = stringToGender(scrapedGender, caseInsensitive);
|
||||
return stringToGender(scrapedGender, caseInsensitive);
|
||||
}
|
||||
|
||||
return genderToString(retEnum);
|
||||
}
|
||||
|
||||
function renderNewTags() {
|
||||
|
|
@ -329,10 +344,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
}
|
||||
if (state.gender) {
|
||||
// gender is a string in the scraper data
|
||||
formik.setFieldValue(
|
||||
"gender",
|
||||
translateScrapedGender(state.gender ?? undefined)
|
||||
);
|
||||
const newGender = translateScrapedGender(state.gender);
|
||||
if (newGender) {
|
||||
formik.setFieldValue("gender", newGender);
|
||||
}
|
||||
}
|
||||
if (state.tags) {
|
||||
// map tags to their ids and filter out those not found
|
||||
|
|
@ -345,15 +360,14 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
// image is a base64 string
|
||||
// #404: don't overwrite image if it has been modified by the user
|
||||
// overwrite if not new since it came from a dialog
|
||||
// overwrite if image was cleared (`null`)
|
||||
// otherwise follow existing behaviour (`undefined`)
|
||||
// overwrite if image is unset
|
||||
if (
|
||||
(!isNew || [null, undefined].includes(formik.values.image)) &&
|
||||
(!isNew || !formik.values.image) &&
|
||||
state.images &&
|
||||
state.images.length > 0
|
||||
) {
|
||||
const imageStr = state.images[0];
|
||||
formik.setFieldValue("image", imageStr ?? undefined);
|
||||
formik.setFieldValue("image", imageStr);
|
||||
}
|
||||
if (state.details) {
|
||||
formik.setFieldValue("details", state.details);
|
||||
|
|
@ -382,31 +396,47 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
}
|
||||
}
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||
|
||||
useEffect(() => {
|
||||
setImage(formik.values.image);
|
||||
}, [formik.values.image, setImage]);
|
||||
|
||||
useEffect(
|
||||
() => setEncodingImage(encodingImage),
|
||||
[setEncodingImage, encodingImage]
|
||||
);
|
||||
|
||||
function onImageLoad(imageData: string | null) {
|
||||
formik.setFieldValue("image", imageData);
|
||||
}
|
||||
|
||||
async function onSave(performerInput: InputValues) {
|
||||
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
async function onSave(input: InputValues) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (isNew) {
|
||||
const input = getCreateValues(performerInput);
|
||||
const result = await createPerformer({
|
||||
variables: {
|
||||
input,
|
||||
input: {
|
||||
...input,
|
||||
gender: input.gender || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.performerCreate) {
|
||||
history.push(`/performers/${result.data.performerCreate.id}`);
|
||||
}
|
||||
} else {
|
||||
const input = getUpdateValues(performerInput);
|
||||
|
||||
await updatePerformer({
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id!,
|
||||
...input,
|
||||
stash_ids: getStashIDs(performerInput?.stash_ids),
|
||||
gender: input.gender || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -440,16 +470,12 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (onImageChange) {
|
||||
onImageChange(formik.values.image);
|
||||
}
|
||||
return () => onImageChange?.();
|
||||
}, [formik.values.image, onImageChange]);
|
||||
setImage(formik.values.image);
|
||||
}, [formik.values.image, setImage]);
|
||||
|
||||
useEffect(
|
||||
() => onImageEncoding?.(imageEncoding),
|
||||
[onImageEncoding, imageEncoding]
|
||||
);
|
||||
useEffect(() => {
|
||||
setEncodingImage(encodingImage);
|
||||
}, [setEncodingImage, encodingImage]);
|
||||
|
||||
useEffect(() => {
|
||||
const newQueryableScrapers = (
|
||||
|
|
@ -463,33 +489,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
|
||||
if (isLoading) return <LoadingIndicator />;
|
||||
|
||||
function getUpdateValues(values: InputValues): GQL.PerformerUpdateInput {
|
||||
return {
|
||||
...values,
|
||||
gender: stringToGender(values.gender) ?? null,
|
||||
height_cm: values.height_cm ? Number(values.height_cm) : null,
|
||||
weight: values.weight ? Number(values.weight) : null,
|
||||
id: performer.id ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function getCreateValues(values: InputValues): GQL.PerformerCreateInput {
|
||||
return {
|
||||
...values,
|
||||
gender: stringToGender(values.gender),
|
||||
height_cm: values.height_cm ? Number(values.height_cm) : null,
|
||||
weight: values.weight ? Number(values.weight) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
function onImageChangeURL(url: string) {
|
||||
formik.setFieldValue("image", url);
|
||||
}
|
||||
|
||||
async function onReloadScrapers() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
|
|
@ -655,9 +654,9 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const currentPerformer: Partial<GQL.PerformerUpdateInput> = {
|
||||
const currentPerformer = {
|
||||
...formik.values,
|
||||
gender: stringToGender(formik.values.gender),
|
||||
gender: formik.values.gender || null,
|
||||
image: formik.values.image ?? performer.image_path,
|
||||
};
|
||||
|
||||
|
|
@ -698,8 +697,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
{renderScraperMenu()}
|
||||
<ImageInput
|
||||
isEditing
|
||||
onImageChange={onImageChangeHandler}
|
||||
onImageURL={onImageChangeURL}
|
||||
onImageChange={onImageChange}
|
||||
onImageURL={onImageLoad}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
|
|
@ -712,7 +711,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
</div>
|
||||
<Button
|
||||
variant="success"
|
||||
disabled={!formik.dirty}
|
||||
disabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
<FormattedMessage id="actions.save" />
|
||||
|
|
@ -843,6 +842,9 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
{...formik.getFieldProps(field)}
|
||||
isInvalid={!!formik.getFieldMeta(field).error}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formik.getFieldMeta(field).error}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
);
|
||||
|
|
@ -902,7 +904,11 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
<StringListInput
|
||||
value={formik.values.alias_list ?? []}
|
||||
setValue={(value) => formik.setFieldValue("alias_list", value)}
|
||||
errors={formik.errors.alias_list}
|
||||
errors={
|
||||
Array.isArray(formik.errors.alias_list)
|
||||
? formik.errors.alias_list[0]
|
||||
: formik.errors.alias_list
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
|
@ -917,9 +923,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
className="input-control"
|
||||
{...formik.getFieldProps("gender")}
|
||||
>
|
||||
{genderOptions.map((opt) => (
|
||||
<option value={opt} key={opt}>
|
||||
{opt}
|
||||
<option value="" key=""></option>
|
||||
{Array.from(stringGenderMap.entries()).map(([name, value]) => (
|
||||
<option value={value} key={value}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
|
|
|
|||
|
|
@ -152,8 +152,8 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
let retEnum: GQL.GenderEnum | undefined;
|
||||
|
||||
// try to translate from enum values first
|
||||
const upperGender = scrapedGender?.toUpperCase();
|
||||
const asEnum = genderToString(upperGender as GQL.GenderEnum);
|
||||
const upperGender = scrapedGender.toUpperCase();
|
||||
const asEnum = genderToString(upperGender);
|
||||
if (asEnum) {
|
||||
retEnum = stringToGender(asEnum);
|
||||
} else {
|
||||
|
|
@ -248,7 +248,7 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
);
|
||||
const [gender, setGender] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
genderToString(props.performer.gender ?? undefined),
|
||||
genderToString(props.performer.gender),
|
||||
translateScrapedGender(props.scraped.gender)
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import { objectTitle } from "src/core/files";
|
|||
import { galleryTitle } from "src/core/galleries";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { lazyComponent } from "src/utils/lazyComponent";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
|
||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||
|
|
@ -115,55 +116,66 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
const [updateScene] = useSceneUpdate();
|
||||
|
||||
const schema = yup.object({
|
||||
title: yup.string().optional().nullable(),
|
||||
code: yup.string().optional().nullable(),
|
||||
details: yup.string().optional().nullable(),
|
||||
director: yup.string().optional().nullable(),
|
||||
url: yup.string().optional().nullable(),
|
||||
date: yup.string().optional().nullable(),
|
||||
rating100: yup.number().optional().nullable(),
|
||||
gallery_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
studio_id: yup.string().optional().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
title: yup.string().ensure(),
|
||||
code: yup.string().ensure(),
|
||||
url: yup.string().ensure(),
|
||||
date: yup
|
||||
.string()
|
||||
.ensure()
|
||||
.test({
|
||||
name: "date",
|
||||
test: (value) => {
|
||||
if (!value) return true;
|
||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||
if (Number.isNaN(Date.parse(value))) return false;
|
||||
return true;
|
||||
},
|
||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||
}),
|
||||
director: yup.string().ensure(),
|
||||
rating100: yup.number().nullable().defined(),
|
||||
gallery_ids: yup.array(yup.string().required()).defined(),
|
||||
studio_id: yup.string().required().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).defined(),
|
||||
movies: yup
|
||||
.array(
|
||||
yup.object({
|
||||
movie_id: yup.string().required(),
|
||||
scene_index: yup.string().optional().nullable(),
|
||||
scene_index: yup.number().nullable().defined(),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.nullable(),
|
||||
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
cover_image: yup.string().optional().nullable(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().optional().nullable(),
|
||||
.defined(),
|
||||
tag_ids: yup.array(yup.string().required()).defined(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
||||
details: yup.string().ensure(),
|
||||
cover_image: yup.string().nullable().optional(),
|
||||
});
|
||||
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
title: scene.title ?? "",
|
||||
code: scene.code ?? "",
|
||||
details: scene.details ?? "",
|
||||
director: scene.director ?? "",
|
||||
url: scene.url ?? "",
|
||||
date: scene.date ?? "",
|
||||
director: scene.director ?? "",
|
||||
rating100: scene.rating100 ?? null,
|
||||
gallery_ids: (scene.galleries ?? []).map((g) => g.id),
|
||||
studio_id: scene.studio?.id,
|
||||
studio_id: scene.studio?.id ?? null,
|
||||
performer_ids: (scene.performers ?? []).map((p) => p.id),
|
||||
movies: (scene.movies ?? []).map((m) => {
|
||||
return { movie_id: m.movie.id, scene_index: m.scene_index };
|
||||
return { movie_id: m.movie.id, scene_index: m.scene_index ?? null };
|
||||
}),
|
||||
tag_ids: (scene.tags ?? []).map((t) => t.id),
|
||||
cover_image: initialCoverImage,
|
||||
stash_ids: getStashIDs(scene.stash_ids),
|
||||
details: scene.details ?? "",
|
||||
cover_image: initialCoverImage,
|
||||
}),
|
||||
[scene, initialCoverImage]
|
||||
);
|
||||
|
||||
type InputValues = typeof initialValues;
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const formik = useFormik({
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validationSchema: schema,
|
||||
|
|
@ -225,15 +237,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
setQueryableScrapers(newQueryableScrapers);
|
||||
}, [Scrapers, stashConfig]);
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
function getSceneInput(input: InputValues): GQL.SceneUpdateInput {
|
||||
return {
|
||||
id: scene.id!,
|
||||
...input,
|
||||
};
|
||||
}
|
||||
|
||||
function setMovieIds(movieIds: string[]) {
|
||||
const existingMovies = formik.values.movies;
|
||||
|
||||
|
|
@ -245,29 +248,23 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
|
||||
return {
|
||||
movie_id: m,
|
||||
scene_index: null,
|
||||
};
|
||||
});
|
||||
|
||||
formik.setFieldValue("movies", newMovies);
|
||||
}
|
||||
|
||||
function getCreateValues(values: InputValues): GQL.SceneCreateInput {
|
||||
return {
|
||||
...values,
|
||||
};
|
||||
}
|
||||
|
||||
async function onSave(input: InputValues) {
|
||||
console.log("onSave", input);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (!isNew) {
|
||||
const updateValues = getSceneInput(input);
|
||||
const result = await updateScene({
|
||||
variables: {
|
||||
input: {
|
||||
...updateValues,
|
||||
id: scene.id!,
|
||||
rating100: input.rating100 ?? null,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -280,20 +277,17 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
}
|
||||
),
|
||||
});
|
||||
formik.resetForm();
|
||||
}
|
||||
} else {
|
||||
const createValues = getCreateValues(input);
|
||||
const result = await mutateCreateScene({
|
||||
...createValues,
|
||||
...input,
|
||||
file_ids: fileID ? [fileID] : undefined,
|
||||
});
|
||||
if (result.data?.sceneCreate?.id) {
|
||||
history.push(`/scenes/${result.data?.sceneCreate.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// clear the cover image so that it doesn't appear dirty
|
||||
formik.resetForm({ values: formik.values });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
|
|
@ -321,6 +315,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setCoverImagePreview(imageData);
|
||||
formik.setFieldValue("cover_image", imageData);
|
||||
|
|
@ -356,17 +352,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const input: GQL.ScrapedSceneInput = {
|
||||
date: fragment.date,
|
||||
code: fragment.code,
|
||||
details: fragment.details,
|
||||
director: fragment.director,
|
||||
remote_site_id: fragment.remote_site_id,
|
||||
title: fragment.title,
|
||||
url: fragment.url,
|
||||
};
|
||||
|
||||
const result = await queryScrapeSceneQueryFragment(s, input);
|
||||
const result = await queryScrapeSceneQueryFragment(s, fragment);
|
||||
if (!result.data || !result.data.scrapeSingleScene?.length) {
|
||||
Toast.success({
|
||||
content: "No scenes found",
|
||||
|
|
@ -414,7 +400,11 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const currentScene = getSceneInput(formik.values);
|
||||
const currentScene = {
|
||||
id: scene.id!,
|
||||
...formik.values,
|
||||
};
|
||||
|
||||
if (!currentScene.cover_image) {
|
||||
currentScene.cover_image = scene.paths?.screenshot;
|
||||
}
|
||||
|
|
@ -671,7 +661,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
|
||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
||||
return (
|
||||
<Form.Group controlId={title} as={Row}>
|
||||
<Form.Group controlId={field} as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title,
|
||||
})}
|
||||
|
|
@ -682,13 +672,16 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
{...formik.getFieldProps(field)}
|
||||
isInvalid={!!formik.getFieldMeta(field).error}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formik.getFieldMeta(field).error}
|
||||
</Form.Control.Feedback>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
const image = useMemo(() => {
|
||||
if (imageEncoding) {
|
||||
if (encodingImage) {
|
||||
return <LoadingIndicator message="Encoding image..." />;
|
||||
}
|
||||
|
||||
|
|
@ -703,7 +696,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
}
|
||||
|
||||
return <div></div>;
|
||||
}, [imageEncoding, coverImagePreview, intl]);
|
||||
}, [encodingImage, coverImagePreview, intl]);
|
||||
|
||||
if (isLoading) return <LoadingIndicator />;
|
||||
|
||||
|
|
@ -722,7 +715,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
<Button
|
||||
className="edit-button"
|
||||
variant="primary"
|
||||
disabled={!isNew && !formik.dirty}
|
||||
disabled={
|
||||
(!isNew && !formik.dirty) || !isEqual(formik.errors, {})
|
||||
}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
<FormattedMessage id="actions.save" />
|
||||
|
|
@ -946,10 +941,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
<Form.Control
|
||||
as="textarea"
|
||||
className="scene-description text-input"
|
||||
onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
formik.setFieldValue("details", newValue.currentTarget.value)
|
||||
onChange={(e) =>
|
||||
formik.setFieldValue("details", e.currentTarget.value)
|
||||
}
|
||||
value={formik.values.details}
|
||||
value={formik.values.details ?? ""}
|
||||
/>
|
||||
</Form.Group>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
useStudioDestroy,
|
||||
mutateMetadataAutoTag,
|
||||
} from "src/core/StashService";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import { Counter } from "src/components/Shared/Counter";
|
||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
|
|
@ -54,8 +53,9 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Studio state
|
||||
// Editing studio state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
const [updateStudio] = useStudioUpdate();
|
||||
const [deleteStudio] = useStudioDestroy({ id: studio.id });
|
||||
|
|
@ -73,17 +73,14 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||
};
|
||||
});
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setImage(imageData);
|
||||
}
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
|
||||
async function onSave(input: Partial<GQL.StudioUpdateInput>) {
|
||||
async function onSave(input: GQL.StudioCreateInput) {
|
||||
try {
|
||||
const result = await updateStudio({
|
||||
variables: {
|
||||
input: input as GQL.StudioUpdateInput,
|
||||
input: {
|
||||
id: studio.id,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.studioUpdate) {
|
||||
|
|
@ -181,7 +178,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||
<div className="row">
|
||||
<div className="studio-details col-md-4">
|
||||
<div className="text-center">
|
||||
{imageEncoding ? (
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
renderImage()
|
||||
|
|
@ -213,7 +210,8 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||
onSubmit={onSave}
|
||||
onCancel={onToggleEdit}
|
||||
onDelete={onDelete}
|
||||
onImageChange={setImage}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useIntl } from "react-intl";
|
|||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useStudioCreate } from "src/core/StashService";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { StudioEditPanel } from "./StudioEditPanel";
|
||||
|
|
@ -21,25 +20,16 @@ const StudioCreate: React.FC = () => {
|
|||
|
||||
const intl = useIntl();
|
||||
|
||||
// Studio state
|
||||
// Editing studio state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
const [createStudio] = useStudioCreate();
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setImage(imageData);
|
||||
}
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
async function onSave(
|
||||
input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput>
|
||||
) {
|
||||
async function onSave(input: GQL.StudioCreateInput) {
|
||||
try {
|
||||
const result = await createStudio({
|
||||
variables: {
|
||||
input: input as GQL.StudioCreateInput,
|
||||
},
|
||||
variables: { input },
|
||||
});
|
||||
if (result.data?.studioCreate?.id) {
|
||||
history.push(`/studios/${result.data.studioCreate.id}`);
|
||||
|
|
@ -65,7 +55,7 @@ const StudioCreate: React.FC = () => {
|
|||
)}
|
||||
</h2>
|
||||
<div className="text-center">
|
||||
{imageEncoding ? (
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
renderImage()
|
||||
|
|
@ -74,9 +64,10 @@ const StudioCreate: React.FC = () => {
|
|||
<StudioEditPanel
|
||||
studio={studio}
|
||||
onSubmit={onSave}
|
||||
onImageChange={setImage}
|
||||
onCancel={() => history.push("/studios")}
|
||||
onDelete={() => {}}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,16 +17,15 @@ import { StringListInput } from "../../Shared/StringListInput";
|
|||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
|
||||
interface IStudioEditPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
onSubmit: (
|
||||
studio: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput>
|
||||
) => void;
|
||||
onSubmit: (studio: GQL.StudioCreateInput) => void;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
onImageChange?: (image?: string | null) => void;
|
||||
onImageEncoding?: (loading?: boolean) => void;
|
||||
setImage: (image?: string | null) => void;
|
||||
setEncodingImage: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
||||
|
|
@ -34,83 +33,77 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
onSubmit,
|
||||
onCancel,
|
||||
onDelete,
|
||||
onImageChange,
|
||||
onImageEncoding,
|
||||
setImage,
|
||||
setEncodingImage,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const isNew = studio.id === undefined;
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
const schema = yup.object({
|
||||
name: yup.string().required(),
|
||||
url: yup.string().optional().nullable(),
|
||||
details: yup.string().optional().nullable(),
|
||||
image: yup.string().optional().nullable(),
|
||||
rating100: yup.number().optional().nullable(),
|
||||
parent_id: yup.string().optional().nullable(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().optional().nullable(),
|
||||
url: yup.string().ensure(),
|
||||
details: yup.string().ensure(),
|
||||
parent_id: yup.string().required().nullable(),
|
||||
rating100: yup.number().nullable().defined(),
|
||||
aliases: yup
|
||||
.array(yup.string().required())
|
||||
.optional()
|
||||
.defined()
|
||||
.test({
|
||||
name: "unique",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
test: (value: any) => {
|
||||
return (value ?? []).length === new Set(value).size;
|
||||
test: (value, context) => {
|
||||
if (!value) return true;
|
||||
const aliases = new Set(value);
|
||||
aliases.add(context.parent.name);
|
||||
return value.length + 1 === aliases.size;
|
||||
},
|
||||
message: "aliases must be unique",
|
||||
message: intl.formatMessage({
|
||||
id: "validation.aliases_must_be_unique",
|
||||
}),
|
||||
}),
|
||||
ignore_auto_tag: yup.boolean().optional(),
|
||||
ignore_auto_tag: yup.boolean().defined(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
||||
image: yup.string().nullable().optional(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
name: studio.name ?? "",
|
||||
url: studio.url ?? "",
|
||||
details: studio.details ?? "",
|
||||
image: undefined,
|
||||
parent_id: studio.parent_studio?.id ?? null,
|
||||
rating100: studio.rating100 ?? null,
|
||||
parent_id: studio.parent_studio?.id,
|
||||
stash_ids: studio.stash_ids ?? undefined,
|
||||
aliases: studio.aliases,
|
||||
aliases: studio.aliases ?? [],
|
||||
ignore_auto_tag: studio.ignore_auto_tag ?? false,
|
||||
stash_ids: getStashIDs(studio.stash_ids),
|
||||
};
|
||||
|
||||
type InputValues = typeof initialValues;
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const formik = useFormik({
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validationSchema: schema,
|
||||
onSubmit: (values) => onSubmit(getStudioInput(values)),
|
||||
onSubmit: (values) => onSubmit(values),
|
||||
});
|
||||
|
||||
// always dirty if creating a new studio with a name
|
||||
if (isNew && studio.name) {
|
||||
formik.dirty = true;
|
||||
}
|
||||
const encodingImage = ImageUtils.usePasteImage((imageData) =>
|
||||
formik.setFieldValue("image", imageData)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setImage(formik.values.image);
|
||||
}, [formik.values.image, setImage]);
|
||||
|
||||
useEffect(
|
||||
() => setEncodingImage(encodingImage),
|
||||
[setEncodingImage, encodingImage]
|
||||
);
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating100", v);
|
||||
}
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
formik.setFieldValue("image", imageData);
|
||||
}
|
||||
|
||||
function getStudioInput(values: InputValues) {
|
||||
const input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput> = {
|
||||
...values,
|
||||
stash_ids: getStashIDs(values.stash_ids),
|
||||
};
|
||||
|
||||
if (studio && studio.id) {
|
||||
(input as GQL.StudioUpdateInput).id = studio.id;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
useRatingKeybinds(
|
||||
true,
|
||||
configuration?.ui?.ratingSystemOptions?.type,
|
||||
|
|
@ -126,24 +119,12 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (onImageChange) {
|
||||
onImageChange(formik.values.image);
|
||||
}
|
||||
return () => onImageChange?.();
|
||||
}, [formik.values.image, onImageChange]);
|
||||
|
||||
useEffect(
|
||||
() => onImageEncoding?.(imageEncoding),
|
||||
[onImageEncoding, imageEncoding]
|
||||
);
|
||||
|
||||
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
function onImageLoad(imageData: string | null) {
|
||||
formik.setFieldValue("image", imageData);
|
||||
}
|
||||
|
||||
function onImageChangeURL(url: string) {
|
||||
formik.setFieldValue("image", url);
|
||||
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
const removeStashID = (stashID: GQL.StashIdInput) => {
|
||||
|
|
@ -306,7 +287,11 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
<StringListInput
|
||||
value={formik.values.aliases ?? []}
|
||||
setValue={(value) => formik.setFieldValue("aliases", value)}
|
||||
errors={formik.errors.aliases}
|
||||
errors={
|
||||
Array.isArray(formik.errors.aliases)
|
||||
? formik.errors.aliases[0]
|
||||
: formik.errors.aliases
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
|
@ -333,13 +318,11 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
isNew={isNew}
|
||||
isEditing
|
||||
onToggleEdit={onCancel}
|
||||
onSave={() => formik.handleSubmit()}
|
||||
saveDisabled={!formik.dirty}
|
||||
onImageChange={onImageChangeHandler}
|
||||
onImageChangeURL={onImageChangeURL}
|
||||
onClearImage={() => {
|
||||
formik.setFieldValue("image", null);
|
||||
}}
|
||||
onSave={formik.handleSubmit}
|
||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||
onImageChange={onImageChange}
|
||||
onImageChangeURL={onImageLoad}
|
||||
onClearImage={() => onImageLoad(null)}
|
||||
onDelete={onDelete}
|
||||
acceptSVG
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
useTagDestroy,
|
||||
mutateMetadataAutoTag,
|
||||
} from "src/core/StashService";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import { Counter } from "src/components/Shared/Counter";
|
||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||
|
|
@ -64,6 +63,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||
|
||||
// Editing tag state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
const [updateTag] = useTagUpdate();
|
||||
const [deleteTag] = useTagDestroy({ id: tag.id });
|
||||
|
|
@ -99,27 +99,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||
};
|
||||
});
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setImage(imageData);
|
||||
}
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
|
||||
function getTagInput(
|
||||
input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>
|
||||
) {
|
||||
const ret: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = {
|
||||
...input,
|
||||
image,
|
||||
id: tag.id,
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async function onSave(
|
||||
input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>
|
||||
) {
|
||||
async function onSave(input: GQL.TagCreateInput) {
|
||||
try {
|
||||
const oldRelations = {
|
||||
parents: tag.parents ?? [],
|
||||
|
|
@ -127,7 +107,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||
};
|
||||
const result = await updateTag({
|
||||
variables: {
|
||||
input: getTagInput(input) as GQL.TagUpdateInput,
|
||||
input: {
|
||||
id: tag.id,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.tagUpdate) {
|
||||
|
|
@ -270,7 +253,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||
<div className="row">
|
||||
<div className="tag-details col-md-4">
|
||||
<div className="text-center logo-container">
|
||||
{imageEncoding ? (
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
renderImage()
|
||||
|
|
@ -303,6 +286,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||
onCancel={onToggleEdit}
|
||||
onDelete={onDelete}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { useHistory, useLocation } from "react-router-dom";
|
|||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useTagCreate } from "src/core/StashService";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { tagRelationHook } from "src/core/tags";
|
||||
|
|
@ -21,38 +20,18 @@ const TagCreate: React.FC = () => {
|
|||
|
||||
// Editing tag state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
const [createTag] = useTagCreate();
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setImage(imageData);
|
||||
}
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
function getTagInput(
|
||||
input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>
|
||||
) {
|
||||
const ret: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = {
|
||||
...input,
|
||||
image,
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async function onSave(
|
||||
input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>
|
||||
) {
|
||||
async function onSave(input: GQL.TagCreateInput) {
|
||||
try {
|
||||
const oldRelations = {
|
||||
parents: [],
|
||||
children: [],
|
||||
};
|
||||
const result = await createTag({
|
||||
variables: {
|
||||
input: getTagInput(input) as GQL.TagCreateInput,
|
||||
},
|
||||
variables: { input },
|
||||
});
|
||||
if (result.data?.tagCreate?.id) {
|
||||
const created = result.data.tagCreate;
|
||||
|
|
@ -77,7 +56,7 @@ const TagCreate: React.FC = () => {
|
|||
<div className="row">
|
||||
<div className="tag-details col-md-8">
|
||||
<div className="text-center logo-container">
|
||||
{imageEncoding ? (
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
renderImage()
|
||||
|
|
@ -89,6 +68,7 @@ const TagCreate: React.FC = () => {
|
|||
onCancel={() => history.push("/tags")}
|
||||
onDelete={() => {}}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,14 +11,16 @@ import { useFormik } from "formik";
|
|||
import { Prompt } from "react-router-dom";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { StringListInput } from "src/components/Shared/StringListInput";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
|
||||
interface ITagEditPanel {
|
||||
tag: Partial<GQL.TagDataFragment>;
|
||||
// returns id
|
||||
onSubmit: (tag: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>) => void;
|
||||
onSubmit: (tag: GQL.TagCreateInput) => void;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
setImage: (image?: string | null) => void;
|
||||
setEncodingImage: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||
|
|
@ -27,6 +29,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
onCancel,
|
||||
onDelete,
|
||||
setImage,
|
||||
setEncodingImage,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
@ -39,46 +42,46 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
|
||||
const schema = yup.object({
|
||||
name: yup.string().required(),
|
||||
description: yup.string().optional().nullable(),
|
||||
aliases: yup
|
||||
.array(yup.string().required())
|
||||
.optional()
|
||||
.defined()
|
||||
.test({
|
||||
name: "unique",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
test: (value: any) => {
|
||||
return (value ?? []).length === new Set(value).size;
|
||||
test: (value, context) => {
|
||||
if (!value) return true;
|
||||
const aliases = new Set(value);
|
||||
aliases.add(context.parent.name);
|
||||
return value.length + 1 === aliases.size;
|
||||
},
|
||||
message: intl.formatMessage({ id: "dialogs.aliases_must_be_unique" }),
|
||||
message: intl.formatMessage({
|
||||
id: "validation.aliases_must_be_unique",
|
||||
}),
|
||||
}),
|
||||
parent_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
child_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
ignore_auto_tag: yup.boolean().optional(),
|
||||
description: yup.string().ensure(),
|
||||
parent_ids: yup.array(yup.string().required()).defined(),
|
||||
child_ids: yup.array(yup.string().required()).defined(),
|
||||
ignore_auto_tag: yup.boolean().defined(),
|
||||
image: yup.string().nullable().optional(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
name: tag?.name,
|
||||
description: tag?.description,
|
||||
aliases: tag?.aliases,
|
||||
name: tag?.name ?? "",
|
||||
aliases: tag?.aliases ?? [],
|
||||
description: tag?.description ?? "",
|
||||
parent_ids: (tag?.parents ?? []).map((t) => t.id),
|
||||
child_ids: (tag?.children ?? []).map((t) => t.id),
|
||||
ignore_auto_tag: tag?.ignore_auto_tag ?? false,
|
||||
};
|
||||
|
||||
type InputValues = typeof initialValues;
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const formik = useFormik({
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
validationSchema: schema,
|
||||
enableReinitialize: true,
|
||||
onSubmit: (values) => onSubmit(getTagInput(values)),
|
||||
onSubmit: (values) => onSubmit(values),
|
||||
});
|
||||
|
||||
// always dirty if creating a new tag with a name
|
||||
if (isNew && tag?.name) {
|
||||
formik.dirty = true;
|
||||
}
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("s s", () => formik.handleSubmit());
|
||||
|
|
@ -88,19 +91,22 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
};
|
||||
});
|
||||
|
||||
function getTagInput(values: InputValues) {
|
||||
const input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = {
|
||||
...values,
|
||||
};
|
||||
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||
|
||||
if (tag && tag.id) {
|
||||
(input as GQL.TagUpdateInput).id = tag.id;
|
||||
}
|
||||
return input;
|
||||
useEffect(() => {
|
||||
setImage(formik.values.image);
|
||||
}, [formik.values.image, setImage]);
|
||||
|
||||
useEffect(() => {
|
||||
setEncodingImage(encodingImage);
|
||||
}, [setEncodingImage, encodingImage]);
|
||||
|
||||
function onImageLoad(imageData: string | null) {
|
||||
formik.setFieldValue("image", imageData);
|
||||
}
|
||||
|
||||
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, setImage);
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
const isEditing = true;
|
||||
|
|
@ -120,7 +126,8 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
<Prompt
|
||||
when={formik.dirty}
|
||||
message={(location, action) => {
|
||||
if (action === "PUSH" && location.pathname.startsWith(`/tags/`)) {
|
||||
// Check if it's a redirect after movie creation
|
||||
if (action === "PUSH" && location.pathname.startsWith("/tags/")) {
|
||||
return true;
|
||||
}
|
||||
return intl.formatMessage({ id: "dialogs.unsaved_changes" });
|
||||
|
|
@ -151,9 +158,13 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<StringListInput
|
||||
value={formik.values.aliases ?? []}
|
||||
value={formik.values.aliases}
|
||||
setValue={(value) => formik.setFieldValue("aliases", value)}
|
||||
errors={formik.errors.aliases}
|
||||
errors={
|
||||
Array.isArray(formik.errors.aliases)
|
||||
? formik.errors.aliases[0]
|
||||
: formik.errors.aliases
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
|
@ -191,9 +202,10 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
)
|
||||
}
|
||||
ids={formik.values.parent_ids}
|
||||
excludeIds={(tag?.id ? [tag.id] : []).concat(
|
||||
...formik.values.child_ids
|
||||
)}
|
||||
excludeIds={[
|
||||
...(tag?.id ? [tag.id] : []),
|
||||
...formik.values.child_ids,
|
||||
]}
|
||||
creatable={false}
|
||||
/>
|
||||
</Col>
|
||||
|
|
@ -218,9 +230,10 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
)
|
||||
}
|
||||
ids={formik.values.child_ids}
|
||||
excludeIds={(tag?.id ? [tag.id] : []).concat(
|
||||
...formik.values.parent_ids
|
||||
)}
|
||||
excludeIds={[
|
||||
...(tag?.id ? [tag.id] : []),
|
||||
...formik.values.parent_ids,
|
||||
]}
|
||||
creatable={false}
|
||||
/>
|
||||
</Col>
|
||||
|
|
@ -248,13 +261,11 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
isNew={isNew}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onCancel}
|
||||
onSave={() => formik.handleSubmit()}
|
||||
saveDisabled={!formik.dirty}
|
||||
onSave={formik.handleSubmit}
|
||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||
onImageChange={onImageChange}
|
||||
onImageChangeURL={setImage}
|
||||
onClearImage={() => {
|
||||
setImage(null);
|
||||
}}
|
||||
onImageChangeURL={onImageLoad}
|
||||
onClearImage={() => onImageLoad(null)}
|
||||
onDelete={onDelete}
|
||||
acceptSVG
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
* Overhauled and improved HLS streaming. ([#3274](https://github.com/stashapp/stash/pull/3274))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
* Fixed Save button being disabled when stting Tag image. ([#3509](https://github.com/stashapp/stash/pull/3509))
|
||||
* Fixed incorrect performer with identical name being matched when scraping from stash-box. ([#3488](https://github.com/stashapp/stash/pull/3488))
|
||||
* Fixed scene cover not being included when submitting file-less scenes to stash-box. ([#3465](https://github.com/stashapp/stash/pull/3465))
|
||||
* Fixed URL not being during stash-box scrape if the Studio URL is not set. ([#3439](https://github.com/stashapp/stash/pull/3439))
|
||||
|
|
|
|||
|
|
@ -646,7 +646,6 @@
|
|||
"details": "Detaily",
|
||||
"developmentVersion": "Vývojářská verze",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "aliasy musí být unikátní",
|
||||
"delete_alert": "Následující {count, plural, one {{singularEntity}} other {{pluralEntity}}} budou permanentně smazány:",
|
||||
"delete_confirm": "Jste si jisti, že chcete smazat {entityName}?",
|
||||
"delete_entity_desc": "{count, plural, one {Jste si jisti, že checete smazat toto {singularEntity}? Pokud není soubor rovněž smazán, tato {singularEntity} bude znovu přidána při příštím skenování.} other {Jste si jisti, že chcete smazat tyto {pluralEntity}? Pokud nejsou soubory rovněž smazány, tyto {pluralEntity} budou znovu přidány při příštím skenování.}}",
|
||||
|
|
@ -696,5 +695,8 @@
|
|||
"marker_image_previews_tooltip": "Animované WebP náhledy markerů, nezbytné pouze tehdy, pokud je Typ náhledu nastaven na \"Animovaný obrázek\".",
|
||||
"marker_screenshots": "Screenshoty markerů"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "aliasy musí být unikátní"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -614,7 +614,6 @@
|
|||
"details": "Detaljer",
|
||||
"developmentVersion": "Udviklingsversion",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "aliaser skal være unikke",
|
||||
"delete_alert": "Følgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} vil blive slettet permanent:",
|
||||
"delete_confirm": "Er du sikker på, at du vil slette {entityName}?",
|
||||
"delete_entity_desc": "{count, plural, one {Er du sikker på, at du vil slette denne {singularEntity}? Medmindre filen også slettes, vil denne {singularEntity} blive tilføjet igen, når scanningen udføres.} andet {Er du sikker på, at du vil slette disse {pluralEntity}? Medmindre filerne også slettes, vil disse {pluralEntity} blive tilføjet igen, når scanningen udføres.}}",
|
||||
|
|
@ -1039,6 +1038,9 @@
|
|||
"type": "Type",
|
||||
"updated_at": "Opdateret Den",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "aliaser skal være unikke"
|
||||
},
|
||||
"videos": "Videoer",
|
||||
"view_all": "Se alle",
|
||||
"weight": "Vægt",
|
||||
|
|
|
|||
|
|
@ -662,7 +662,6 @@
|
|||
"details": "Details",
|
||||
"developmentVersion": "Entwicklungsversion",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "Aliase müssen einzigartig sein",
|
||||
"create_new_entity": "Neues {entity} erstellen",
|
||||
"delete_alert": "Folgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} werden dauerhaft gelöscht:",
|
||||
"delete_confirm": "Möchten Sie {entityName} wirklich löschen?",
|
||||
|
|
@ -1118,6 +1117,9 @@
|
|||
"type": "Typ",
|
||||
"updated_at": "Aktualisiert am",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "Aliase müssen einzigartig sein"
|
||||
},
|
||||
"videos": "Videos",
|
||||
"view_all": "Alle ansehen",
|
||||
"weight": "Gewicht",
|
||||
|
|
|
|||
|
|
@ -699,7 +699,6 @@
|
|||
"details": "Details",
|
||||
"developmentVersion": "Development Version",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "aliases must be unique",
|
||||
"create_new_entity": "Create new {entity}",
|
||||
"delete_alert": "The following {count, plural, one {{singularEntity}} other {{pluralEntity}}} will be deleted permanently:",
|
||||
"delete_confirm": "Are you sure you want to delete {entityName}?",
|
||||
|
|
@ -1164,6 +1163,11 @@
|
|||
"type": "Type",
|
||||
"updated_at": "Updated At",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "aliases must be unique",
|
||||
"date_invalid_form": "${path} must be in YYYY-MM-DD form",
|
||||
"required": "${path} is a required field"
|
||||
},
|
||||
"videos": "Videos",
|
||||
"view_all": "View All",
|
||||
"play_count": "Play Count",
|
||||
|
|
|
|||
|
|
@ -607,7 +607,6 @@
|
|||
"details": "Detalles",
|
||||
"developmentVersion": "Versión de desarrollo",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "Los alias deben ser únicos",
|
||||
"delete_alert": "El/los siguiente/s {count, plural, one {{singularEntity}} other {{pluralEntity}}} se eliminará/n de forma permanente:",
|
||||
"delete_confirm": "¿Estás seguro que deseas eliminar {entityName}?",
|
||||
"delete_entity_desc": "{count, plural, one {¿Estás seguro que deseas eliminar esta {singularEntity}? Hasta que el archivo sea eliminado también, esta {singularEntity} se volverá a añadir cuando se lleve a cabo un escaneo.} other {¿Estás seguro que deseas eliminar {pluralEntity}? Hasta que los archivos sean eliminados del sistema de ficheros también, estas {pluralEntity} se volverán a añadir cuando se lleve a cabo un escaneo.}}",
|
||||
|
|
@ -1017,6 +1016,9 @@
|
|||
"twitter": "Twitter",
|
||||
"updated_at": "Fecha de modificación",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "Los alias deben ser únicos"
|
||||
},
|
||||
"videos": "Vídeos",
|
||||
"weight": "Peso",
|
||||
"years_old": "años"
|
||||
|
|
|
|||
|
|
@ -666,7 +666,6 @@
|
|||
"details": "Detailid",
|
||||
"developmentVersion": "Arendusversioon",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "aliased peavad olema erilised",
|
||||
"create_new_entity": "Loo uus {entity}",
|
||||
"delete_alert": "Järgnev {count, plural, one {{singularEntity}} other {{pluralEntity}}} kustutatakse lõplikult:",
|
||||
"delete_confirm": "Kas oled kindel, et soovid kustutada {entityName}?",
|
||||
|
|
@ -1124,6 +1123,9 @@
|
|||
"type": "Tüüp",
|
||||
"updated_at": "Viimati Uuendatud",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "aliased peavad olema erilised"
|
||||
},
|
||||
"videos": "Videod",
|
||||
"view_all": "Vaata Kõiki",
|
||||
"weight": "Kaal",
|
||||
|
|
|
|||
|
|
@ -614,7 +614,6 @@
|
|||
"details": "Lisätiedot",
|
||||
"developmentVersion": "Kehitysversio",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "Aliaksien pitää olla uniikkeja",
|
||||
"create_new_entity": "Luo uus {entity}",
|
||||
"delete_alert": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} poistetaan pysyvästi:",
|
||||
"delete_confirm": "Haluatko varmasti poistaa {entityName}?",
|
||||
|
|
@ -1028,6 +1027,9 @@
|
|||
"twitter": "Twitter",
|
||||
"updated_at": "Päivitetty",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "Aliaksien pitää olla uniikkeja"
|
||||
},
|
||||
"videos": "Videot",
|
||||
"view_all": "Näytä kaikki",
|
||||
"weight": "Paino",
|
||||
|
|
|
|||
|
|
@ -666,7 +666,6 @@
|
|||
"details": "Détails",
|
||||
"developmentVersion": "Version de développement",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "Les alias doivent être uniques",
|
||||
"create_new_entity": "Créer un nouveau {entity}",
|
||||
"delete_alert": "{count, plural, one {Le {singularEntity} suivant sera supprimé} other {Les {pluralEntity} suivants seront supprimés}} définitivement :",
|
||||
"delete_confirm": "Êtes-vous sûr de vouloir supprimer {entityName} ?",
|
||||
|
|
@ -1124,6 +1123,9 @@
|
|||
"type": "Type",
|
||||
"updated_at": "Actualisé le",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "Les alias doivent être uniques"
|
||||
},
|
||||
"videos": "Vidéos",
|
||||
"view_all": "Tout voir",
|
||||
"weight": "Poids",
|
||||
|
|
|
|||
|
|
@ -668,7 +668,6 @@
|
|||
"details": "Dettagli",
|
||||
"developmentVersion": "Versione Sviluppo",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "gli alias devono essere univoci",
|
||||
"create_new_entity": "Crea nuovo/a {entity}",
|
||||
"delete_alert": "Il seguente/I seguenti {count, plural, one {{singularEntity}} other {{pluralEntity}}} sarà/saranno cancellati permanentemente:",
|
||||
"delete_confirm": "Sei sicuro di voler cancellare {entityName}?",
|
||||
|
|
@ -1126,6 +1125,9 @@
|
|||
"type": "Tipo",
|
||||
"updated_at": "Aggiornato Al",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "gli alias devono essere univoci"
|
||||
},
|
||||
"videos": "Video",
|
||||
"view_all": "Vedi Tutto",
|
||||
"weight": "Peso",
|
||||
|
|
|
|||
|
|
@ -666,7 +666,6 @@
|
|||
"details": "詳細",
|
||||
"developmentVersion": "開発者バージョン",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "別名は一意でなければいけません",
|
||||
"create_new_entity": "{entity}を新規作成する",
|
||||
"delete_alert": "次の{count, plural, one {{singularEntity}} other {{pluralEntity}}}は完全に削除されます:",
|
||||
"delete_confirm": "本当に{entityName}を削除してよろしいですか?",
|
||||
|
|
@ -1124,6 +1123,9 @@
|
|||
"type": "タイプ",
|
||||
"updated_at": "更新日:",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "別名は一意でなければいけません"
|
||||
},
|
||||
"videos": "動画",
|
||||
"view_all": "全て表示",
|
||||
"weight": "幅",
|
||||
|
|
|
|||
|
|
@ -661,7 +661,6 @@
|
|||
"details": "세부사항",
|
||||
"developmentVersion": "개발 버전",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "별칭은 유일해야 합니다",
|
||||
"create_new_entity": "새로운 {entity} 생성",
|
||||
"delete_alert": "다음 {count, plural, one {{singularEntity}} other {{pluralEntity}}}이(가) 영구 삭제될 것입니다:",
|
||||
"delete_confirm": "정말 {entityName}을 삭제하시겠습니까?",
|
||||
|
|
@ -1107,6 +1106,9 @@
|
|||
"type": "유형",
|
||||
"updated_at": "수정 날짜",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "별칭은 유일해야 합니다"
|
||||
},
|
||||
"videos": "비디오",
|
||||
"view_all": "모두 보기",
|
||||
"weight": "몸무게",
|
||||
|
|
|
|||
|
|
@ -590,7 +590,6 @@
|
|||
"details": "Details",
|
||||
"developmentVersion": "Ontwikkelingsversie",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "aliases moeten uniek zijn",
|
||||
"delete_alert": "De volgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} zal permanent verwijderd worden:",
|
||||
"delete_confirm": "Weet je zeker dat je {entityName} wilt verwijderen?",
|
||||
"delete_entity_desc": "{count, plural, one {Weet u zeker dat u deze {singularEntity} wilt verwijderen? Tenzij het bestand ook wordt verwijderd, wordt deze {singularEntity} opnieuw toegevoegd wanneer de scan wordt uitgevoerd.} other {Weet u zeker dat u deze {pluralEntity} wilt verwijderen? Tenzij de bestanden ook worden verwijderd, worden deze {pluralEntity} opnieuw toegevoegd wanneer de scan wordt uitgevoerd.}}",
|
||||
|
|
@ -988,6 +987,9 @@
|
|||
"twitter": "Twitter",
|
||||
"updated_at": "Bijgewerkt op",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "aliases moeten uniek zijn"
|
||||
},
|
||||
"videos": "Video's",
|
||||
"view_all": "Alles weergeven",
|
||||
"weight": "Gewicht",
|
||||
|
|
|
|||
|
|
@ -666,7 +666,6 @@
|
|||
"details": "Szczegóły",
|
||||
"developmentVersion": "Wersja deweloperska",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "aliasy muszą być unikatowe",
|
||||
"create_new_entity": "Dodaj {entity}",
|
||||
"delete_alert": "Następujące elementy {count, plural, one {{singularEntity}} other {{pluralEntity}}} zostaną trwale usunięte:",
|
||||
"delete_confirm": "Czy na pewno chcesz usunąć {entityName}?",
|
||||
|
|
@ -1124,6 +1123,9 @@
|
|||
"type": "Typ",
|
||||
"updated_at": "Zaktualizowano",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "aliasy muszą być unikatowe"
|
||||
},
|
||||
"videos": "Filmy wideo",
|
||||
"view_all": "Pokaż wszystko",
|
||||
"weight": "Waga",
|
||||
|
|
|
|||
|
|
@ -592,7 +592,6 @@
|
|||
"details": "Detalhes",
|
||||
"developmentVersion": "Versão de desenvolvimento",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "apelidos devem ser únicos",
|
||||
"delete_alert": "Os seguintes {count, plural, one {{singularEntity}} other {{pluralEntity}}} serão deletados permanentemente:",
|
||||
"delete_confirm": "Tem certeza de que deseja excluir {entityName}?",
|
||||
"delete_entity_desc": "{count, plural, one {Tem certeza de que deseja excluir este(a) {singularEntity}? A menos que o arquivo também esteja excluído, este(a) {singularEntity} será re-adicionado quando a escaneamento for executado.} other {Tem certeza de que deseja excluir estes(as) {pluralEntity}? A menos que os arquivos também estejam excluídos, estes(as) {pluralEntity} serão re-adicionados quando o escaneamento for executado.}}",
|
||||
|
|
@ -1021,6 +1020,9 @@
|
|||
"type": "Tipo",
|
||||
"updated_at": "Atualizado em",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "apelidos devem ser únicos"
|
||||
},
|
||||
"videos": "Vídeos",
|
||||
"view_all": "Ver todos",
|
||||
"weight": "Peso",
|
||||
|
|
|
|||
|
|
@ -661,7 +661,6 @@
|
|||
"details": "Подробности",
|
||||
"developmentVersion": "Версия разработки",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "псевдонимы должны быть уникальными",
|
||||
"create_new_entity": "Создать новую запись в {entity}",
|
||||
"delete_alert": "Следующие {count, plural, one {{singularEntity}} other {{pluralEntity}}} будут удалены безвозвратно:",
|
||||
"delete_confirm": "Вы уверены что хотите удалить {entityName}?",
|
||||
|
|
@ -1119,6 +1118,9 @@
|
|||
"type": "Тип",
|
||||
"updated_at": "Обновлено",
|
||||
"url": "Ссылка",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "псевдонимы должны быть уникальными"
|
||||
},
|
||||
"videos": "Видео",
|
||||
"view_all": "Показать все",
|
||||
"weight": "Вес",
|
||||
|
|
|
|||
|
|
@ -666,7 +666,6 @@
|
|||
"details": "Beskrivningar",
|
||||
"developmentVersion": "Utvecklingsversion",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "alias måste vara unik",
|
||||
"create_new_entity": "Skapa ny {entity}",
|
||||
"delete_alert": "De följande {count, plural, one {{singularEntity}} andra {{pluralEntity}}} kommer raderas permanent:",
|
||||
"delete_confirm": "Är du säker på att du vill radera {entityName}?",
|
||||
|
|
@ -1124,6 +1123,9 @@
|
|||
"type": "Typ",
|
||||
"updated_at": "Uppdaterad vid",
|
||||
"url": "URL",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "alias måste vara unik"
|
||||
},
|
||||
"videos": "Videor",
|
||||
"view_all": "Visa Allt",
|
||||
"weight": "Vikt",
|
||||
|
|
|
|||
|
|
@ -547,7 +547,6 @@
|
|||
"details": "Ayrıntılar",
|
||||
"developmentVersion": "Geliştirme Sürümü",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "takma adlar benzersiz olmalıdır",
|
||||
"delete_alert": "Bu {count, plural, one {{singularEntity}} other {{pluralEntity}}} kalıcı olarak silinecektir:",
|
||||
"delete_confirm": "Bunu silmek istediğinizden emin misiniz: {entityName}?",
|
||||
"delete_entity_desc": "{count, plural, one {Bunu silmek istediğinizden emin misiniz: {entityName}? Dosyayı bilgisayarınızdan silmediğiniz sürece, yeniden tarama işleminde bu {singularEntity} tekrar veritabanına eklenecektir.} other {Bunları silmek istediğinizden emin misiniz: {pluralEntity}? Dosyayı bilgisayarınızdan silmediğiniz sürece, yeniden tarama işleminde bu {pluralEntity} tekrar veritabanına eklenecektir.}}",
|
||||
|
|
@ -891,6 +890,9 @@
|
|||
"twitter": "Twitter",
|
||||
"updated_at": "Güncellenme Zamanı",
|
||||
"url": "Internet Adresi (URL)",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "takma adlar benzersiz olmalıdır"
|
||||
},
|
||||
"videos": "Videolar",
|
||||
"weight": "Kilo",
|
||||
"years_old": "yaşında"
|
||||
|
|
|
|||
|
|
@ -666,7 +666,6 @@
|
|||
"details": "简介",
|
||||
"developmentVersion": "开发版本",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "别名必须是唯一的",
|
||||
"create_new_entity": "创建新的 {entity}",
|
||||
"delete_alert": "以下 {count, plural, one {{singularEntity}} other {{pluralEntity}}} 会被永久删除:",
|
||||
"delete_confirm": "确定要删除 {entityName} 吗?",
|
||||
|
|
@ -1124,6 +1123,9 @@
|
|||
"type": "类别",
|
||||
"updated_at": "更新时间",
|
||||
"url": "链接",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "别名必须是唯一的"
|
||||
},
|
||||
"videos": "视频",
|
||||
"view_all": "查看全部",
|
||||
"weight": "体重",
|
||||
|
|
|
|||
|
|
@ -666,7 +666,6 @@
|
|||
"details": "細節",
|
||||
"developmentVersion": "開發版本",
|
||||
"dialogs": {
|
||||
"aliases_must_be_unique": "別名不可重複",
|
||||
"create_new_entity": "建立新{entity}",
|
||||
"delete_alert": "以下{count, plural, one {{singularEntity}} other {{pluralEntity}}}將被永久刪除:",
|
||||
"delete_confirm": "你確定要刪除 {entityName} 嗎?",
|
||||
|
|
@ -1124,6 +1123,9 @@
|
|||
"type": "種類",
|
||||
"updated_at": "更新於",
|
||||
"url": "連結",
|
||||
"validation": {
|
||||
"aliases_must_be_unique": "別名不可重複"
|
||||
},
|
||||
"videos": "影片",
|
||||
"view_all": "顯示全部",
|
||||
"weight": "體重",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const stringGenderMap = new Map<string, GQL.GenderEnum>([
|
|||
["Non-Binary", GQL.GenderEnum.NonBinary],
|
||||
]);
|
||||
|
||||
export const genderToString = (value?: GQL.GenderEnum | string) => {
|
||||
export const genderToString = (value?: GQL.GenderEnum | string | null) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue