mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
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:
@@ -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))
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user