stash/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx
gitgiggety d4d45d5a06
Rebuild Studio page by splitting view and edit (#1629)
* Rebuild Studio page by splitting view and edit
* Fix parent studio id, open studio link in same tab

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-08-11 14:44:18 +10:00

305 lines
8.6 KiB
TypeScript

import React, { useEffect } from "react";
import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import * as yup from "yup";
import Mousetrap from "mousetrap";
import { Icon, StudioSelect, DetailsEditNavbar } from "src/components/Shared";
import { Button, Form, Col, Row } from "react-bootstrap";
import { FormUtils, ImageUtils } from "src/utils";
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
import { useFormik } from "formik";
import { Prompt } from "react-router-dom";
interface IStudioEditPanel {
studio: Partial<GQL.StudioDataFragment>;
onSubmit: (
studio: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput>
) => void;
onCancel: () => void;
onDelete: () => void;
onImageChange?: (image?: string | null) => void;
onImageEncoding?: (loading?: boolean) => void;
}
export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
studio,
onSubmit,
onCancel,
onDelete,
onImageChange,
onImageEncoding,
}) => {
const intl = useIntl();
const isNew = !studio || !studio.id;
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(),
rating: yup.number().optional().nullable(),
parent_id: yup.string().optional().nullable(),
stash_ids: yup.mixed<GQL.StashIdInput>().optional().nullable(),
});
const initialValues = {
name: studio.name ?? "",
url: studio.url ?? "",
details: studio.details ?? "",
image: undefined,
rating: studio.rating ?? null,
parent_id: studio.parent_studio?.id,
stash_ids: studio.stash_ids ?? undefined,
};
type InputValues = typeof initialValues;
const formik = useFormik({
initialValues,
validationSchema: schema,
onSubmit: (values) => onSubmit(getStudioInput(values)),
});
function setRating(v: number) {
formik.setFieldValue("rating", v);
}
function onImageLoad(imageData: string) {
formik.setFieldValue("image", imageData);
}
function getStudioInput(values: InputValues) {
const input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput> = {
...values,
};
if (studio && studio.id) {
(input as GQL.StudioUpdateInput).id = studio.id;
}
return input;
}
// set up hotkeys
useEffect(() => {
Mousetrap.bind("s s", () => formik.handleSubmit());
// numeric keypresses get caught by jwplayer, so blur the element
// if the rating sequence is started
Mousetrap.bind("r", () => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
Mousetrap.bind("0", () => setRating(NaN));
Mousetrap.bind("1", () => setRating(1));
Mousetrap.bind("2", () => setRating(2));
Mousetrap.bind("3", () => setRating(3));
Mousetrap.bind("4", () => setRating(4));
Mousetrap.bind("5", () => setRating(5));
setTimeout(() => {
Mousetrap.unbind("0");
Mousetrap.unbind("1");
Mousetrap.unbind("2");
Mousetrap.unbind("3");
Mousetrap.unbind("4");
Mousetrap.unbind("5");
}, 1000);
});
return () => {
Mousetrap.unbind("s s");
Mousetrap.unbind("e");
};
});
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 onImageChangeURL(url: string) {
formik.setFieldValue("image", url);
}
const removeStashID = (stashID: GQL.StashIdInput) => {
formik.setFieldValue(
"stash_ids",
(formik.values.stash_ids ?? []).filter(
(s) =>
!(s.endpoint === stashID.endpoint && s.stash_id === stashID.stash_id)
)
);
};
function renderStashIDs() {
if (!formik.values.stash_ids?.length) {
return;
}
return (
<Row>
<Form.Label column>StashIDs</Form.Label>
<Col xs={9}>
<ul className="pl-0">
{formik.values.stash_ids.map((stashID) => {
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
const link = base ? (
<a
href={`${base}studios/${stashID.stash_id}`}
target="_blank"
rel="noopener noreferrer"
>
{stashID.stash_id}
</a>
) : (
stashID.stash_id
);
return (
<li key={stashID.stash_id} className="row no-gutters">
<Button
variant="danger"
className="mr-2 py-0"
title={intl.formatMessage(
{ id: "actions.delete_entity" },
{ entityType: intl.formatMessage({ id: "stash_id" }) }
)}
onClick={() => removeStashID(stashID)}
>
<Icon icon="trash-alt" />
</Button>
{link}
</li>
);
})}
</ul>
</Col>
</Row>
);
}
return (
<>
<Prompt
when={formik.dirty}
message="Unsaved changes. Are you sure you want to leave?"
/>
<Form noValidate onSubmit={formik.handleSubmit} id="studio-edit">
<Form.Group controlId="name" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "name" }),
})}
<Col xs={9}>
<Form.Control
className="text-input"
{...formik.getFieldProps("name")}
isInvalid={!!formik.errors.name}
/>
<Form.Control.Feedback type="invalid">
{formik.errors.name}
</Form.Control.Feedback>
</Col>
</Form.Group>
<Form.Group controlId="url" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "url" }),
})}
<Col xs={9}>
<Form.Control
className="text-input"
{...formik.getFieldProps("url")}
isInvalid={!!formik.errors.url}
/>
<Form.Control.Feedback type="invalid">
{formik.errors.url}
</Form.Control.Feedback>
</Col>
</Form.Group>
<Form.Group controlId="details" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "details" }),
})}
<Col xs={9}>
<Form.Control
as="textarea"
className="text-input"
{...formik.getFieldProps("details")}
isInvalid={!!formik.errors.details}
/>
<Form.Control.Feedback type="invalid">
{formik.errors.details}
</Form.Control.Feedback>
</Col>
</Form.Group>
<Form.Group controlId="parent_studio" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "parent_studios" }),
})}
<Col xs={9}>
<StudioSelect
onSelect={(items) =>
formik.setFieldValue(
"parent_id",
items.length > 0 ? items[0]?.id : null
)
}
ids={formik.values.parent_id ? [formik.values.parent_id] : []}
excludeIds={studio.id ? [studio.id] : []}
/>
</Col>
</Form.Group>
<Form.Group controlId="rating" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "rating" }),
})}
<Col xs={9}>
<RatingStars
value={formik.values.rating ?? undefined}
onSetRating={(value) =>
formik.setFieldValue("rating", value ?? null)
}
/>
</Col>
</Form.Group>
{renderStashIDs()}
</Form>
<DetailsEditNavbar
objectName={studio?.name ?? "studio"}
isNew={isNew}
isEditing
onToggleEdit={onCancel}
onSave={() => formik.handleSubmit()}
saveDisabled={!formik.dirty}
onImageChange={onImageChangeHandler}
onImageChangeURL={onImageChangeURL}
onClearImage={() => {
formik.setFieldValue("image", null);
}}
onDelete={onDelete}
acceptSVG
/>
</>
);
};