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:
gitgiggety
2021-08-11 06:44:18 +02:00
committed by GitHub
parent 53489106a6
commit d4d45d5a06
9 changed files with 450 additions and 259 deletions

View File

@@ -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"