Form-related fixes, improvements and refactoring (#4283)

* Fix another validateDOMNesting error
* Fix React.forwardRef error
* Fix encoding_image intl message
* Return null instead of undefined from RatingSystem
* DurationInput tweaks
* DateInput tweaks, remove unused utils functions
* Refactor and deduplicate edit form rendering
* Improve/fix yup validation
This commit is contained in:
DingDongSoLong4
2023-11-20 03:42:26 +02:00
committed by GitHub
parent 65b416a2d9
commit 959f2531fd
55 changed files with 1419 additions and 2217 deletions

View File

@@ -6,21 +6,22 @@ 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 { URLListInput } from "src/components/Shared/URLField";
import { useToast } from "src/hooks/Toast";
import FormUtils from "src/utils/form";
import { useFormik } from "formik";
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";
import { DateInput } from "src/components/Shared/DateInput";
import { yupDateString, yupUniqueStringList } from "src/utils/yup";
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;
@@ -49,7 +50,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
title: yup.string().ensure(),
urls: yupUniqueStringList("urls"),
date: yupDateString(intl),
rating100: yup.number().nullable().defined(),
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(),
@@ -70,8 +71,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
const formik = useFormik<InputValues>({
initialValues,
enableReinitialize: true,
validationSchema: schema,
onSubmit: (values) => onSave(values),
validate: yupFormikValidate(schema),
onSubmit: (values) => onSave(schema.cast(values)),
});
function setRating(v: number) {
@@ -128,36 +129,80 @@ export const ImageEditPanel: React.FC<IProps> = ({
setIsLoading(false);
}
function renderTextField(field: string, title: string, placeholder?: string) {
return (
<Form.Group controlId={field} as={Row}>
{FormUtils.renderLabel({
title,
})}
<Col xs={9}>
<Form.Control
className="text-input"
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>
);
}
if (isLoading) return <LoadingIndicator />;
const urlsErrors = Array.isArray(formik.errors.urls)
? formik.errors.urls[0]
: formik.errors.urls;
const urlsErrorMsg = urlsErrors
? intl.formatMessage({ id: "validation.urls_must_be_unique" })
: undefined;
const urlsErrorIdx = urlsErrors?.split(" ").map((e) => parseInt(e));
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);
}
return (
<div id="image-edit-details">
@@ -167,8 +212,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
/>
<Form noValidate onSubmit={formik.handleSubmit}>
<div className="form-container row px-3 pt-3">
<div className="col edit-buttons mb-3 pl-0">
<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"
@@ -185,110 +230,21 @@ export const ImageEditPanel: React.FC<IProps> = ({
<FormattedMessage id="actions.delete" />
</Button>
</div>
</div>
<div className="form-container row px-3">
<div className="col-12 col-lg-6 col-xl-12">
{renderTextField("title", intl.formatMessage({ id: "title" }))}
<Form.Group controlId="urls" as={Row}>
<Col xs={3} className="pr-0 url-label">
<Form.Label className="col-form-label">
<FormattedMessage id="urls" />
</Form.Label>
</Col>
<Col xs={9}>
<URLListInput
value={formik.values.urls ?? []}
setValue={(value) => formik.setFieldValue("urls", value)}
errors={urlsErrorMsg}
errorIdx={urlsErrorIdx}
/>
</Col>
</Form.Group>
<Form.Group controlId="date" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "date" }),
})}
<Col xs={9}>
<DateInput
value={formik.values.date}
onValueChange={(value) => formik.setFieldValue("date", value)}
error={formik.errors.date}
/>
</Col>
</Form.Group>
<Form.Group controlId="rating" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "rating" }),
})}
<Col xs={9}>
<RatingSystem
value={formik.values.rating100 ?? undefined}
onSetRating={(value) =>
formik.setFieldValue("rating100", value ?? null)
}
/>
</Col>
</Form.Group>
<Form.Group controlId="studio" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "studio" }),
})}
<Col xs={9}>
<StudioSelect
onSelect={(items) =>
formik.setFieldValue(
"studio_id",
items.length > 0 ? items[0]?.id : null
)
}
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
/>
</Col>
</Form.Group>
</Row>
<Row className="form-container px-3">
<Col lg={7} xl={12}>
{renderInputField("title")}
<Form.Group controlId="performers" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "performers" }),
labelProps: {
column: true,
sm: 3,
xl: 12,
},
})}
<Col sm={9} xl={12}>
<PerformerSelect
isMulti
onSelect={onSetPerformers}
values={performers}
/>
</Col>
</Form.Group>
{renderURLListField("urls", "validation.urls_must_be_unique")}
<Form.Group controlId="tags" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "tags" }),
labelProps: {
column: true,
sm: 3,
xl: 12,
},
})}
<Col sm={9} xl={12}>
<TagSelect
isMulti
onSelect={(items) =>
formik.setFieldValue(
"tag_ids",
items.map((item) => item.id)
)
}
ids={formik.values.tag_ids}
hoverPlacement="right"
/>
</Col>
</Form.Group>
</div>
</div>
{renderDateField("date")}
{renderRatingField("rating100", "rating")}
{renderStudioField()}
{renderPerformersField()}
{renderTagsField()}
</Col>
</Row>
</Form>
</div>
);