Files
stash/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx
DingDongSoLong4 d37de0e49b Fix crash on blank aliases/urls (#4344)
* Fix crash on blank alias/url
* Fix StringListInput clear issue
2023-12-12 11:28:00 +11:00

279 lines
6.9 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { Button, Form, Col, Row } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
import * as yup from "yup";
import { TagSelect, StudioSelect } from "src/components/Shared/Select";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { useToast } from "src/hooks/Toast";
import { useFormik } from "formik";
import { Prompt } from "react-router-dom";
import { useRatingKeybinds } from "src/hooks/keybinds";
import { ConfigurationContext } from "src/hooks/Config";
import isEqual from "lodash-es/isEqual";
import {
yupDateString,
yupFormikValidate,
yupUniqueStringList,
} from "src/utils/yup";
import {
Performer,
PerformerSelect,
} from "src/components/Performers/PerformerSelect";
import { formikUtils } from "src/utils/form";
interface IProps {
image: GQL.ImageDataFragment;
isVisible: boolean;
onSubmit: (input: GQL.ImageUpdateInput) => Promise<void>;
onDelete: () => void;
}
export const ImageEditPanel: React.FC<IProps> = ({
image,
isVisible,
onSubmit,
onDelete,
}) => {
const intl = useIntl();
const Toast = useToast();
// Network state
const [isLoading, setIsLoading] = useState(false);
const { configuration } = React.useContext(ConfigurationContext);
const [performers, setPerformers] = useState<Performer[]>([]);
const schema = yup.object({
title: yup.string().ensure(),
code: yup.string().ensure(),
urls: yupUniqueStringList(intl),
date: yupDateString(intl),
details: yup.string().ensure(),
photographer: yup.string().ensure(),
rating100: yup.number().integer().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 ?? "",
code: image.code ?? "",
urls: image?.urls ?? [],
date: image?.date ?? "",
details: image.details ?? "",
photographer: image.photographer ?? "",
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 = yup.InferType<typeof schema>;
const formik = useFormik<InputValues>({
initialValues,
enableReinitialize: true,
validate: yupFormikValidate(schema),
onSubmit: (values) => onSave(schema.cast(values)),
});
function setRating(v: number) {
formik.setFieldValue("rating100", v);
}
function onSetPerformers(items: Performer[]) {
setPerformers(items);
formik.setFieldValue(
"performer_ids",
items.map((item) => item.id)
);
}
useRatingKeybinds(
true,
configuration?.ui?.ratingSystemOptions?.type,
setRating
);
useEffect(() => {
setPerformers(image.performers ?? []);
}, [image.performers]);
useEffect(() => {
if (isVisible) {
Mousetrap.bind("s s", () => {
if (formik.dirty) {
formik.submitForm();
}
});
Mousetrap.bind("d d", () => {
onDelete();
});
return () => {
Mousetrap.unbind("s s");
Mousetrap.unbind("d d");
};
}
});
async function onSave(input: InputValues) {
setIsLoading(true);
try {
await onSubmit({
id: image.id,
...input,
});
formik.resetForm();
} catch (e) {
Toast.error(e);
}
setIsLoading(false);
}
if (isLoading) return <LoadingIndicator />;
const splitProps = {
labelProps: {
column: true,
sm: 3,
},
fieldProps: {
sm: 9,
},
};
const fullWidthProps = {
labelProps: {
column: true,
sm: 3,
xl: 12,
},
fieldProps: {
sm: 9,
xl: 12,
},
};
const {
renderField,
renderInputField,
renderDateField,
renderRatingField,
renderURLListField,
} = formikUtils(intl, formik, splitProps);
function renderStudioField() {
const title = intl.formatMessage({ id: "studio" });
const control = (
<StudioSelect
onSelect={(items) =>
formik.setFieldValue(
"studio_id",
items.length > 0 ? items[0]?.id : null
)
}
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
/>
);
return renderField("studio_id", title, control);
}
function renderPerformersField() {
const title = intl.formatMessage({ id: "performers" });
const control = (
<PerformerSelect isMulti onSelect={onSetPerformers} values={performers} />
);
return renderField("performer_ids", title, control, fullWidthProps);
}
function renderTagsField() {
const title = intl.formatMessage({ id: "tags" });
const control = (
<TagSelect
isMulti
onSelect={(items) =>
formik.setFieldValue(
"tag_ids",
items.map((item) => item.id)
)
}
ids={formik.values.tag_ids}
hoverPlacement="right"
/>
);
return renderField("tag_ids", title, control, fullWidthProps);
}
function renderDetailsField() {
const props = {
labelProps: {
column: true,
sm: 3,
lg: 12,
},
fieldProps: {
sm: 9,
lg: 12,
},
};
return renderInputField("details", "textarea", "details", props);
}
return (
<div id="image-edit-details">
<Prompt
when={formik.dirty}
message={intl.formatMessage({ id: "dialogs.unsaved_changes" })}
/>
<Form noValidate onSubmit={formik.handleSubmit}>
<Row className="form-container edit-buttons-container px-3 pt-3">
<div className="edit-buttons mb-3 pl-0">
<Button
className="edit-button"
variant="primary"
disabled={!formik.dirty || !isEqual(formik.errors, {})}
onClick={() => formik.submitForm()}
>
<FormattedMessage id="actions.save" />
</Button>
<Button
className="edit-button"
variant="danger"
onClick={() => onDelete()}
>
<FormattedMessage id="actions.delete" />
</Button>
</div>
</Row>
<Row className="form-container px-3">
<Col lg={7} xl={12}>
{renderInputField("title")}
{renderInputField("code", "text", "scene_code")}
{renderURLListField("urls")}
{renderDateField("date")}
{renderInputField("photographer")}
{renderRatingField("rating100", "rating")}
{renderStudioField()}
{renderPerformersField()}
{renderTagsField()}
</Col>
<Col lg={5} xl={12}>
{renderDetailsField()}
</Col>
</Row>
</Form>
</div>
);
};