mirror of https://github.com/stashapp/stash.git
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>
This commit is contained in:
parent
53489106a6
commit
d4d45d5a06
|
@ -6,6 +6,7 @@
|
|||
* Added not equals/greater than/less than modifiers for resolution criteria. ([#1568](https://github.com/stashapp/stash/pull/1568))
|
||||
|
||||
### 🎨 Improvements
|
||||
* Improve Studio UI. ([#1629](https://github.com/stashapp/stash/pull/1629))
|
||||
* Improve link styling and ensure links open in a new tab. ([#1622](https://github.com/stashapp/stash/pull/1622))
|
||||
* Added zh-CN language option. ([#1620](https://github.com/stashapp/stash/pull/1620))
|
||||
* Moved scraping settings into the Scraping settings page. ([#1548](https://github.com/stashapp/stash/pull/1548))
|
||||
|
|
|
@ -259,7 +259,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
|||
new ScrapeResult<string>(props.gallery.details, props.scraped.details)
|
||||
);
|
||||
|
||||
const [createStudio] = useStudioCreate({ name: "" });
|
||||
const [createStudio] = useStudioCreate();
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
const [createTag] = useTagCreate();
|
||||
|
||||
|
|
|
@ -321,7 +321,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
|||
new ScrapeResult<string>(props.scene.cover_image, props.scraped.image)
|
||||
);
|
||||
|
||||
const [createStudio] = useStudioCreate({ name: "" });
|
||||
const [createStudio] = useStudioCreate();
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
const [createMovie] = useMovieCreate();
|
||||
const [createTag] = useTagCreate();
|
||||
|
|
|
@ -428,7 +428,7 @@ export const StudioSelect: React.FC<
|
|||
IFilterProps & { excludeIds?: string[] }
|
||||
> = (props) => {
|
||||
const { data, loading } = useAllStudiosForFilter();
|
||||
const [createStudio] = useStudioCreate({ name: "" });
|
||||
const [createStudio] = useStudioCreate();
|
||||
|
||||
const exclude = props.excludeIds ?? [];
|
||||
const studios = (data?.allStudios ?? []).filter(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Button, Table, Tabs, Tab } from "react-bootstrap";
|
||||
import { Tabs, Tab } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useHistory, Link } from "react-router-dom";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import cx from "classnames";
|
||||
import Mousetrap from "mousetrap";
|
||||
|
@ -13,21 +13,21 @@ import {
|
|||
useStudioDestroy,
|
||||
mutateMetadataAutoTag,
|
||||
} from "src/core/StashService";
|
||||
import { ImageUtils, TableUtils } from "src/utils";
|
||||
import { ImageUtils } from "src/utils";
|
||||
import {
|
||||
Icon,
|
||||
DetailsEditNavbar,
|
||||
Modal,
|
||||
LoadingIndicator,
|
||||
StudioSelect,
|
||||
ErrorMessage,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { StudioScenesPanel } from "./StudioScenesPanel";
|
||||
import { StudioGalleriesPanel } from "./StudioGalleriesPanel";
|
||||
import { StudioImagesPanel } from "./StudioImagesPanel";
|
||||
import { StudioChildrenPanel } from "./StudioChildrenPanel";
|
||||
import { StudioPerformersPanel } from "./StudioPerformersPanel";
|
||||
import { StudioEditPanel } from "./StudioEditPanel";
|
||||
import { StudioDetailsPanel } from "./StudioDetailsPanel";
|
||||
|
||||
interface IStudioParams {
|
||||
id?: string;
|
||||
|
@ -45,83 +45,23 @@ export const Studio: React.FC = () => {
|
|||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Editing studio state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [name, setName] = useState<string>();
|
||||
const [url, setUrl] = useState<string>();
|
||||
const [parentStudioId, setParentStudioId] = useState<string>();
|
||||
const [rating, setRating] = useState<number | undefined>(undefined);
|
||||
const [details, setDetails] = useState<string>();
|
||||
const [stashIDs, setStashIDs] = useState<GQL.StashIdInput[]>([]);
|
||||
|
||||
// Studio state
|
||||
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||
const [image, setImage] = useState<string | null>();
|
||||
|
||||
const { data, error, loading } = useFindStudio(id);
|
||||
const { data, error } = useFindStudio(id);
|
||||
const studio = data?.findStudio;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [updateStudio] = useStudioUpdate();
|
||||
const [createStudio] = useStudioCreate(
|
||||
getStudioInput() as GQL.StudioCreateInput
|
||||
);
|
||||
const [deleteStudio] = useStudioDestroy(
|
||||
getStudioInput() as GQL.StudioDestroyInput
|
||||
);
|
||||
|
||||
function updateStudioEditState(state: Partial<GQL.StudioDataFragment>) {
|
||||
setName(state.name);
|
||||
setUrl(state.url ?? undefined);
|
||||
setParentStudioId(state?.parent_studio?.id ?? undefined);
|
||||
setRating(state.rating ?? undefined);
|
||||
setDetails(state.details ?? undefined);
|
||||
setStashIDs(state.stash_ids ?? []);
|
||||
}
|
||||
|
||||
function updateStudioData(studioData: Partial<GQL.StudioDataFragment>) {
|
||||
setImage(undefined);
|
||||
updateStudioEditState(studioData);
|
||||
setImagePreview(studioData.image_path ?? undefined);
|
||||
setStudio(studioData);
|
||||
setRating(studioData.rating ?? undefined);
|
||||
}
|
||||
const [createStudio] = useStudioCreate();
|
||||
const [deleteStudio] = useStudioDestroy({ id });
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
Mousetrap.bind("s s", () => onSave());
|
||||
}
|
||||
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("d d", () => onDelete());
|
||||
|
||||
// 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 () => {
|
||||
if (isEditing) {
|
||||
Mousetrap.unbind("s s");
|
||||
}
|
||||
|
||||
Mousetrap.unbind("e");
|
||||
Mousetrap.unbind("d d");
|
||||
};
|
||||
|
@ -130,58 +70,36 @@ export const Studio: React.FC = () => {
|
|||
useEffect(() => {
|
||||
if (data && data.findStudio) {
|
||||
setImage(undefined);
|
||||
updateStudioEditState(data.findStudio);
|
||||
setImagePreview(data.findStudio.image_path ?? undefined);
|
||||
setStudio(data.findStudio);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setImagePreview(imageData);
|
||||
setImage(imageData);
|
||||
}
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
|
||||
if (!isNew && !isEditing) {
|
||||
if (!data?.findStudio || loading || !studio.id) return <LoadingIndicator />;
|
||||
if (error) return <div>{error.message}</div>;
|
||||
}
|
||||
|
||||
function getStudioInput() {
|
||||
const input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput> = {
|
||||
name,
|
||||
url,
|
||||
image,
|
||||
details,
|
||||
parent_id: parentStudioId ?? null,
|
||||
rating: rating ?? null,
|
||||
stash_ids: stashIDs.map((s) => ({
|
||||
stash_id: s.stash_id,
|
||||
endpoint: s.endpoint,
|
||||
})),
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
(input as GQL.StudioUpdateInput).id = id;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
async function onSave(
|
||||
input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput>
|
||||
) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!isNew) {
|
||||
const result = await updateStudio({
|
||||
variables: {
|
||||
input: getStudioInput() as GQL.StudioUpdateInput,
|
||||
input: input as GQL.StudioUpdateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.studioUpdate) {
|
||||
updateStudioData(result.data.studioUpdate);
|
||||
setIsEditing(false);
|
||||
}
|
||||
} else {
|
||||
const result = await createStudio();
|
||||
const result = await createStudio({
|
||||
variables: {
|
||||
input: input as GQL.StudioCreateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.studioCreate?.id) {
|
||||
history.push(`/studios/${result.data.studioCreate.id}`);
|
||||
setIsEditing(false);
|
||||
|
@ -189,11 +107,13 @@ export const Studio: React.FC = () => {
|
|||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onAutoTag() {
|
||||
if (!studio.id) return;
|
||||
if (!studio?.id) return;
|
||||
try {
|
||||
await mutateMetadataAutoTag({ studios: [studio.id] });
|
||||
Toast.success({
|
||||
|
@ -215,19 +135,6 @@ export const Studio: React.FC = () => {
|
|||
history.push(`/studios`);
|
||||
}
|
||||
|
||||
const removeStashID = (stashID: GQL.StashIdInput) => {
|
||||
setStashIDs(
|
||||
stashIDs.filter(
|
||||
(s) =>
|
||||
!(s.endpoint === stashID.endpoint && s.stash_id === stashID.stash_id)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
function renderDeleteAlert() {
|
||||
return (
|
||||
<Modal
|
||||
|
@ -245,7 +152,7 @@ export const Studio: React.FC = () => {
|
|||
id="dialogs.delete_confirm"
|
||||
values={{
|
||||
entityName:
|
||||
name ??
|
||||
studio?.name ??
|
||||
intl.formatMessage({ id: "studio" }).toLocaleLowerCase(),
|
||||
}}
|
||||
/>
|
||||
|
@ -254,64 +161,25 @@ export const Studio: React.FC = () => {
|
|||
);
|
||||
}
|
||||
|
||||
function renderStashIDs() {
|
||||
if (!studio.stash_ids?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>StashIDs</td>
|
||||
<td>
|
||||
<ul className="pl-0">
|
||||
{stashIDs.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">
|
||||
{isEditing && (
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function onToggleEdit() {
|
||||
setIsEditing(!isEditing);
|
||||
updateStudioData(studio);
|
||||
}
|
||||
|
||||
function onClearImage() {
|
||||
setImage(null);
|
||||
setImagePreview(
|
||||
studio.image_path ? `${studio.image_path}&default=true` : undefined
|
||||
);
|
||||
function renderImage() {
|
||||
let studioImage = studio?.image_path;
|
||||
if (isEditing) {
|
||||
if (image === null) {
|
||||
studioImage = `${studioImage}&default=true`;
|
||||
} else if (image) {
|
||||
studioImage = image;
|
||||
}
|
||||
}
|
||||
|
||||
if (studioImage) {
|
||||
return (
|
||||
<img className="logo" alt={studio?.name ?? ""} src={studioImage} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const activeTabKey =
|
||||
|
@ -328,28 +196,10 @@ export const Studio: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
function renderStudio() {
|
||||
if (isEditing || !parentStudioId) {
|
||||
return (
|
||||
<StudioSelect
|
||||
onSelect={(items) =>
|
||||
setParentStudioId(items.length > 0 ? items[0]?.id : undefined)
|
||||
}
|
||||
ids={parentStudioId ? [parentStudioId] : []}
|
||||
isDisabled={!isEditing}
|
||||
excludeIds={studio.id ? [studio.id] : []}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (studio.parent_studio) {
|
||||
return (
|
||||
<Link to={`/studios/${studio.parent_studio.id}`}>
|
||||
{studio.parent_studio.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isLoading) return <LoadingIndicator />;
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
if (!studio?.id && !isNew)
|
||||
return <ErrorMessage error={`No studio found with id ${id}.`} />;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
|
@ -370,66 +220,36 @@ export const Studio: React.FC = () => {
|
|||
<div className="text-center">
|
||||
{imageEncoding ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : imagePreview ? (
|
||||
<img className="logo" alt={name} src={imagePreview} />
|
||||
) : (
|
||||
""
|
||||
renderImage()
|
||||
)}
|
||||
</div>
|
||||
<Table>
|
||||
<tbody>
|
||||
{TableUtils.renderInputGroup({
|
||||
title: intl.formatMessage({ id: "name" }),
|
||||
value: name ?? "",
|
||||
isEditing: !!isEditing,
|
||||
onChange: setName,
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: intl.formatMessage({ id: "url" }),
|
||||
value: url,
|
||||
isEditing: !!isEditing,
|
||||
onChange: setUrl,
|
||||
})}
|
||||
{TableUtils.renderTextArea({
|
||||
title: intl.formatMessage({ id: "details" }),
|
||||
value: details,
|
||||
isEditing: !!isEditing,
|
||||
onChange: setDetails,
|
||||
})}
|
||||
<tr>
|
||||
<td>{intl.formatMessage({ id: "parent_studios" })}</td>
|
||||
<td>{renderStudio()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{intl.formatMessage({ id: "rating" })}:</td>
|
||||
<td>
|
||||
<RatingStars
|
||||
value={rating}
|
||||
disabled={!isEditing}
|
||||
onSetRating={(value) => setRating(value ?? NaN)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{renderStashIDs()}
|
||||
</tbody>
|
||||
</Table>
|
||||
<DetailsEditNavbar
|
||||
objectName={name ?? "studio"}
|
||||
isNew={isNew}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onToggleEdit}
|
||||
onSave={onSave}
|
||||
onImageChange={onImageChangeHandler}
|
||||
onImageChangeURL={onImageLoad}
|
||||
onClearImage={() => {
|
||||
onClearImage();
|
||||
}}
|
||||
onAutoTag={onAutoTag}
|
||||
onDelete={onDelete}
|
||||
acceptSVG
|
||||
/>
|
||||
{!isEditing && !isNew && studio ? (
|
||||
<>
|
||||
<StudioDetailsPanel studio={studio} />
|
||||
<DetailsEditNavbar
|
||||
objectName={studio.name ?? intl.formatMessage({ id: "studio" })}
|
||||
isNew={isNew}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onToggleEdit}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
onClearImage={() => {}}
|
||||
onAutoTag={onAutoTag}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<StudioEditPanel
|
||||
studio={studio ?? ({} as Partial<GQL.Studio>)}
|
||||
onSubmit={onSave}
|
||||
onCancel={onToggleEdit}
|
||||
onDelete={onDelete}
|
||||
onImageChange={setImage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!isNew && (
|
||||
{studio?.id && (
|
||||
<div className="col col-md-8">
|
||||
<Tabs
|
||||
id="studio-tabs"
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
|
||||
interface IStudioDetailsPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
}
|
||||
|
||||
export const StudioDetailsPanel: React.FC<IStudioDetailsPanel> = ({
|
||||
studio,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
function renderRatingField() {
|
||||
if (!studio.rating) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<dt>{intl.formatMessage({ id: "rating" })}</dt>
|
||||
<dd>
|
||||
<RatingStars value={studio.rating} disabled />
|
||||
</dd>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="studio-details">
|
||||
<div>
|
||||
<h2>{studio.name}</h2>
|
||||
</div>
|
||||
|
||||
<dl className="details-list">
|
||||
<URLField
|
||||
id="url"
|
||||
value={studio.url}
|
||||
url={TextUtils.sanitiseURL(studio.url ?? "")}
|
||||
/>
|
||||
|
||||
<TextField id="details" value={studio.details} />
|
||||
|
||||
<URLField
|
||||
id="parent_studios"
|
||||
value={studio.parent_studio?.name}
|
||||
url={`/studios/${studio.parent_studio?.id}`}
|
||||
trusted
|
||||
target="_self"
|
||||
/>
|
||||
|
||||
{renderRatingField()}
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,305 @@
|
|||
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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -598,9 +598,8 @@ export const studioMutationImpactedQueries = [
|
|||
GQL.AllStudiosForFilterDocument,
|
||||
];
|
||||
|
||||
export const useStudioCreate = (input: GQL.StudioCreateInput) =>
|
||||
export const useStudioCreate = () =>
|
||||
GQL.useStudioCreateMutation({
|
||||
variables: { input },
|
||||
refetchQueries: getQueryNames([GQL.AllStudiosForFilterDocument]),
|
||||
update: deleteCache([
|
||||
GQL.FindStudiosDocument,
|
||||
|
|
|
@ -43,6 +43,9 @@ interface IURLField {
|
|||
value?: string | null;
|
||||
url?: string | null;
|
||||
truncate?: boolean | null;
|
||||
target?: string;
|
||||
// use for internal links
|
||||
trusted?: boolean;
|
||||
}
|
||||
|
||||
export const URLField: React.FC<IURLField> = ({
|
||||
|
@ -53,6 +56,8 @@ export const URLField: React.FC<IURLField> = ({
|
|||
abbr,
|
||||
truncate,
|
||||
children,
|
||||
target,
|
||||
trusted,
|
||||
}) => {
|
||||
if (!value && !children) {
|
||||
return null;
|
||||
|
@ -62,12 +67,14 @@ export const URLField: React.FC<IURLField> = ({
|
|||
<>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</>
|
||||
);
|
||||
|
||||
const rel = !trusted ? "noopener noreferrer" : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
|
||||
<dd>
|
||||
{url ? (
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
<a href={url} target={target || "_blank"} rel={rel}>
|
||||
{value ? (
|
||||
truncate ? (
|
||||
<TruncatedText text={value} />
|
||||
|
|
Loading…
Reference in New Issue