Rebuild image edit using formik (#1669)

* Rebuild image edit using formik
* Prompt on page leave when changes are not saved
* Only enables save when changes are made
* Wrap in <Form onSubmit> (not sure if this does anything)
This commit is contained in:
gitgiggety
2021-08-26 03:24:49 +02:00
committed by GitHub
parent 2e83405841
commit 50217f6318
2 changed files with 161 additions and 101 deletions

View File

@@ -13,7 +13,7 @@
### 🎨 Improvements ### 🎨 Improvements
* Move Play Selected Scenes, and Add/Remove Gallery Image buttons to button toolbar. ([#1673](https://github.com/stashapp/stash/pull/1673)) * Move Play Selected Scenes, and Add/Remove Gallery Image buttons to button toolbar. ([#1673](https://github.com/stashapp/stash/pull/1673))
* Add image and gallery counts to tag list view. ([#1672](https://github.com/stashapp/stash/pull/1672)) * Add image and gallery counts to tag list view. ([#1672](https://github.com/stashapp/stash/pull/1672))
* Prompt when leaving gallery edit page with unsaved changes. ([#1654](https://github.com/stashapp/stash/pull/1654)) * Prompt when leaving gallery and image edit pages with unsaved changes. ([#1654](https://github.com/stashapp/stash/pull/1654), [#1669](https://github.com/stashapp/stash/pull/1669))
* Show largest duplicates first in scene duplicate checker. ([#1639](https://github.com/stashapp/stash/pull/1639)) * Show largest duplicates first in scene duplicate checker. ([#1639](https://github.com/stashapp/stash/pull/1639))
* Added checkboxes to scene list view. ([#1642](https://github.com/stashapp/stash/pull/1642)) * Added checkboxes to scene list view. ([#1642](https://github.com/stashapp/stash/pull/1642))
* Added keyboard shortcuts for scene queue navigation. ([#1635](https://github.com/stashapp/stash/pull/1635)) * Added keyboard shortcuts for scene queue navigation. ([#1635](https://github.com/stashapp/stash/pull/1635))

View File

@@ -3,6 +3,7 @@ import { Button, Form, Col, Row } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import * as yup from "yup";
import { useImageUpdate } from "src/core/StashService"; import { useImageUpdate } from "src/core/StashService";
import { import {
PerformerSelect, PerformerSelect,
@@ -12,6 +13,8 @@ import {
} from "src/components/Shared"; } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { FormUtils } from "src/utils"; import { FormUtils } from "src/utils";
import { useFormik } from "formik";
import { Prompt } from "react-router";
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
interface IProps { interface IProps {
@@ -27,25 +30,44 @@ export const ImageEditPanel: React.FC<IProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const Toast = useToast(); const Toast = useToast();
const [title, setTitle] = useState<string>(image?.title ?? "");
const [rating, setRating] = useState<number>(image.rating ?? NaN);
const [studioId, setStudioId] = useState<string | undefined>(
image.studio?.id ?? undefined
);
const [performerIds, setPerformerIds] = useState<string[]>(
image.performers.map((p) => p.id)
);
const [tagIds, setTagIds] = useState<string[]>(image.tags.map((t) => t.id));
// Network state // Network state
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [updateImage] = useImageUpdate(); const [updateImage] = useImageUpdate();
const schema = yup.object({
title: yup.string().optional().nullable(),
rating: 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(),
});
const initialValues = {
title: image.title ?? "",
rating: image.rating ?? null,
studio_id: image.studio?.id,
performer_ids: (image.performers ?? []).map((p) => p.id),
tag_ids: (image.tags ?? []).map((t) => t.id),
};
type InputValues = typeof initialValues;
const formik = useFormik({
initialValues,
validationSchema: schema,
onSubmit: (values) => onSave(getImageInput(values)),
});
function setRating(v: number) {
formik.setFieldValue("rating", v);
}
useEffect(() => { useEffect(() => {
if (isVisible) { if (isVisible) {
Mousetrap.bind("s s", () => { Mousetrap.bind("s s", () => {
onSave(); formik.handleSubmit();
}); });
Mousetrap.bind("d d", () => { Mousetrap.bind("d d", () => {
onDelete(); onDelete();
@@ -84,23 +106,19 @@ export const ImageEditPanel: React.FC<IProps> = ({
} }
}); });
function getImageInput(): GQL.ImageUpdateInput { function getImageInput(input: InputValues): GQL.ImageUpdateInput {
return { return {
id: image.id, id: image.id,
title, ...input,
rating: rating ?? null,
studio_id: studioId ?? null,
performer_ids: performerIds,
tag_ids: tagIds,
}; };
} }
async function onSave() { async function onSave(input: GQL.ImageUpdateInput) {
setIsLoading(true); setIsLoading(true);
try { try {
const result = await updateImage({ const result = await updateImage({
variables: { variables: {
input: getImageInput(), input,
}, },
}); });
if (result.data?.imageUpdate) { if (result.data?.imageUpdate) {
@@ -110,6 +128,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
{ entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() } { entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() }
), ),
}); });
formik.resetForm({ values: formik.values });
} }
} catch (e) { } catch (e) {
Toast.error(e); Toast.error(e);
@@ -117,13 +136,45 @@ export const ImageEditPanel: React.FC<IProps> = ({
setIsLoading(false); setIsLoading(false);
} }
function renderTextField(field: string, title: string, placeholder?: string) {
return (
<Form.Group controlId={title} 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 />; if (isLoading) return <LoadingIndicator />;
return ( return (
<div id="image-edit-details"> <div id="image-edit-details">
<Prompt
when={formik.dirty}
message={intl.formatMessage({ id: "dialogs.unsaved_changes" })}
/>
<Form noValidate onSubmit={formik.handleSubmit}>
<div className="form-container row px-3 pt-3"> <div className="form-container row px-3 pt-3">
<div className="col edit-buttons mb-3 pl-0"> <div className="col edit-buttons mb-3 pl-0">
<Button className="edit-button" variant="primary" onClick={onSave}> <Button
className="edit-button"
variant="primary"
disabled={!formik.dirty}
onClick={() => formik.submitForm()}
>
<FormattedMessage id="actions.save" /> <FormattedMessage id="actions.save" />
</Button> </Button>
<Button <Button
@@ -137,20 +188,17 @@ export const ImageEditPanel: React.FC<IProps> = ({
</div> </div>
<div className="form-container row px-3"> <div className="form-container row px-3">
<div className="col-12 col-lg-6 col-xl-12"> <div className="col-12 col-lg-6 col-xl-12">
{FormUtils.renderInputGroup({ {renderTextField("title", intl.formatMessage({ id: "title" }))}
title: intl.formatMessage({ id: "title" }),
value: title,
onChange: setTitle,
isEditing: true,
})}
<Form.Group controlId="rating" as={Row}> <Form.Group controlId="rating" as={Row}>
{FormUtils.renderLabel({ {FormUtils.renderLabel({
title: intl.formatMessage({ id: "rating" }), title: intl.formatMessage({ id: "rating" }),
})} })}
<Col xs={9}> <Col xs={9}>
<RatingStars <RatingStars
value={rating} value={formik.values.rating ?? undefined}
onSetRating={(value) => setRating(value ?? NaN)} onSetRating={(value) =>
formik.setFieldValue("rating", value ?? null)
}
/> />
</Col> </Col>
</Form.Group> </Form.Group>
@@ -162,9 +210,12 @@ export const ImageEditPanel: React.FC<IProps> = ({
<Col xs={9}> <Col xs={9}>
<StudioSelect <StudioSelect
onSelect={(items) => onSelect={(items) =>
setStudioId(items.length > 0 ? items[0]?.id : undefined) formik.setFieldValue(
"studio_id",
items.length > 0 ? items[0]?.id : null
)
} }
ids={studioId ? [studioId] : []} ids={formik.values.studio_id ? [formik.values.studio_id] : []}
/> />
</Col> </Col>
</Form.Group> </Form.Group>
@@ -182,9 +233,12 @@ export const ImageEditPanel: React.FC<IProps> = ({
<PerformerSelect <PerformerSelect
isMulti isMulti
onSelect={(items) => onSelect={(items) =>
setPerformerIds(items.map((item) => item.id)) formik.setFieldValue(
"performer_ids",
items.map((item) => item.id)
)
} }
ids={performerIds} ids={formik.values.performer_ids}
/> />
</Col> </Col>
</Form.Group> </Form.Group>
@@ -201,13 +255,19 @@ export const ImageEditPanel: React.FC<IProps> = ({
<Col sm={9} xl={12}> <Col sm={9} xl={12}>
<TagSelect <TagSelect
isMulti isMulti
onSelect={(items) => setTagIds(items.map((item) => item.id))} onSelect={(items) =>
ids={tagIds} formik.setFieldValue(
"tag_ids",
items.map((item) => item.id)
)
}
ids={formik.values.tag_ids}
/> />
</Col> </Col>
</Form.Group> </Form.Group>
</div> </div>
</div> </div>
</Form>
</div> </div>
); );
}; };