Files
stash/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx
2025-12-11 11:17:55 +11:00

260 lines
7.5 KiB
TypeScript

import React, { useEffect, useState } 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 { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
import { Button, Form } from "react-bootstrap";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import ImageUtils from "src/utils/image";
import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
import { useFormik } from "formik";
import { Prompt } from "react-router-dom";
import isEqual from "lodash-es/isEqual";
import { useToast } from "src/hooks/Toast";
import { useConfigurationContext } from "src/hooks/Config";
import { handleUnsavedChanges } from "src/utils/navigation";
import { formikUtils } from "src/utils/form";
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
import { Studio, StudioSelect } from "../StudioSelect";
import { useTagsEdit } from "src/hooks/tagsEdit";
import { Icon } from "src/components/Shared/Icon";
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
interface IStudioEditPanel {
studio: Partial<GQL.StudioDataFragment>;
onSubmit: (studio: GQL.StudioCreateInput) => Promise<void>;
onCancel: () => void;
onDelete: () => void;
setImage: (image?: string | null) => void;
setEncodingImage: (loading: boolean) => void;
}
export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
studio,
onSubmit,
onCancel,
onDelete,
setImage,
setEncodingImage,
}) => {
const intl = useIntl();
const Toast = useToast();
const { configuration: stashConfig } = useConfigurationContext();
const isNew = studio.id === undefined;
// Editing state
const [isStashIDSearchOpen, setIsStashIDSearchOpen] = useState(false);
// Network state
const [isLoading, setIsLoading] = useState(false);
const [parentStudio, setParentStudio] = useState<Studio | null>(null);
const schema = yup.object({
name: yup.string().required(),
urls: yup.array(yup.string().required()).defined(),
details: yup.string().ensure(),
parent_id: yup.string().required().nullable(),
aliases: yupUniqueAliases(intl, "name"),
tag_ids: yup.array(yup.string().required()).defined(),
ignore_auto_tag: yup.boolean().defined(),
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
image: yup.string().nullable().optional(),
});
const initialValues = {
id: studio.id,
name: studio.name ?? "",
urls: studio.urls ?? [],
details: studio.details ?? "",
parent_id: studio.parent_studio?.id ?? null,
aliases: studio.aliases ?? [],
tag_ids: (studio.tags ?? []).map((t) => t.id),
ignore_auto_tag: studio.ignore_auto_tag ?? false,
stash_ids: getStashIDs(studio.stash_ids),
};
type InputValues = yup.InferType<typeof schema>;
const formik = useFormik<InputValues>({
initialValues,
enableReinitialize: true,
validate: yupFormikValidate(schema),
onSubmit: (values) => onSave(schema.cast(values)),
});
const { tagsControl } = useTagsEdit(studio.tags, (ids) =>
formik.setFieldValue("tag_ids", ids)
);
function onSetParentStudio(item: Studio | null) {
setParentStudio(item);
formik.setFieldValue("parent_id", item ? item.id : null);
}
const encodingImage = ImageUtils.usePasteImage((imageData) =>
formik.setFieldValue("image", imageData)
);
useEffect(() => {
setParentStudio(
studio.parent_studio
? {
id: studio.parent_studio.id,
name: studio.parent_studio.name,
aliases: [],
}
: null
);
}, [studio.parent_studio]);
useEffect(() => {
setImage(formik.values.image);
}, [formik.values.image, setImage]);
useEffect(() => {
setEncodingImage(encodingImage);
}, [setEncodingImage, encodingImage]);
// set up hotkeys
useEffect(() => {
Mousetrap.bind("s s", () => {
if (formik.dirty) {
formik.submitForm();
}
});
return () => {
Mousetrap.unbind("s s");
};
});
async function onSave(input: InputValues) {
setIsLoading(true);
try {
await onSubmit(input);
formik.resetForm();
} catch (e) {
Toast.error(e);
}
setIsLoading(false);
}
function onImageLoad(imageData: string | null) {
formik.setFieldValue("image", imageData);
}
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
ImageUtils.onImageChange(event, onImageLoad);
}
function onStashIDSelected(item?: GQL.StashIdInput) {
if (!item) return;
formik.setFieldValue(
"stash_ids",
addUpdateStashID(formik.values.stash_ids, item)
);
}
const {
renderField,
renderInputField,
renderStringListField,
renderStashIDsField,
} = formikUtils(intl, formik);
function renderParentStudioField() {
const title = intl.formatMessage({ id: "parent_studio" });
const control = (
<StudioSelect
onSelect={(items) =>
onSetParentStudio(items.length > 0 ? items[0] : null)
}
values={parentStudio ? [parentStudio] : []}
/>
);
return renderField("parent_id", title, control);
}
function renderTagsField() {
const title = intl.formatMessage({ id: "tags" });
return renderField("tag_ids", title, tagsControl());
}
if (isLoading) return <LoadingIndicator />;
return (
<>
{isStashIDSearchOpen && (
<StashBoxIDSearchModal
entityType="studio"
stashBoxes={stashConfig?.general.stashBoxes ?? []}
excludedStashBoxEndpoints={formik.values.stash_ids.map(
(s) => s.endpoint
)}
onSelectItem={(item) => {
onStashIDSelected(item);
setIsStashIDSearchOpen(false);
}}
/>
)}
<Prompt
when={formik.dirty}
message={(location, action) => {
// Check if it's a redirect after studio creation
if (action === "PUSH" && location.pathname.startsWith("/studios/"))
return true;
return handleUnsavedChanges(intl, "studios", studio.id)(location);
}}
/>
<Form noValidate onSubmit={formik.handleSubmit} id="studio-edit">
{renderInputField("name")}
{renderStringListField("aliases")}
{renderStringListField("urls")}
{renderInputField("details", "textarea")}
{renderParentStudioField()}
{renderTagsField()}
{renderStashIDsField(
"stash_ids",
"studios",
"stash_ids",
undefined,
<Button
variant="success"
className="mr-2 py-0"
onClick={() => setIsStashIDSearchOpen(true)}
disabled={!stashConfig?.general.stashBoxes?.length}
title={intl.formatMessage({ id: "actions.add_stash_id" })}
>
<Icon icon={faPlus} />
</Button>
)}
<hr />
{renderInputField("ignore_auto_tag", "checkbox")}
</Form>
<DetailsEditNavbar
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
classNames="col-xl-9 mt-3"
isNew={isNew}
isEditing
onToggleEdit={onCancel}
onSave={formik.handleSubmit}
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
onImageChange={onImageChange}
onImageChangeURL={onImageLoad}
onClearImage={() => onImageLoad(null)}
onDelete={onDelete}
acceptSVG
/>
</>
);
};