mirror of
https://github.com/stashapp/stash.git
synced 2025-12-15 21:03:22 +01:00
* 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>
305 lines
8.6 KiB
TypeScript
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
|
|
/>
|
|
</>
|
|
);
|
|
};
|