mirror of
https://github.com/stashapp/stash.git
synced 2025-12-07 17:02:38 +01:00
Fix performer image display again and refactoring (#3782)
* Fix the fix for displayed performer image sticking after save * Refactor for consistency * Fully extract entity create/update logic from edit pages * Fix submit hotkeys * Refactor scene cover preview * Fix atoi error on new scene page
This commit is contained in:
parent
fc53380310
commit
d0847d1ebf
21 changed files with 436 additions and 332 deletions
|
|
@ -64,6 +64,23 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
|
|||
|
||||
const [organizedLoading, setOrganizedLoading] = useState(false);
|
||||
|
||||
async function onSave(input: GQL.GalleryCreateInput) {
|
||||
await updateGallery({
|
||||
variables: {
|
||||
input: {
|
||||
id: gallery.id,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{ entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const onOrganizedClick = async () => {
|
||||
try {
|
||||
setOrganizedLoading(true);
|
||||
|
|
@ -242,6 +259,7 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
|
|||
<GalleryEditPanel
|
||||
isVisible={activeTabKey === "gallery-edit-panel"}
|
||||
gallery={gallery}
|
||||
onSubmit={onSave}
|
||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,39 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useGalleryCreate } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { GalleryEditPanel } from "./GalleryEditPanel";
|
||||
|
||||
const GalleryCreate: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const location = useLocation();
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
const gallery = {
|
||||
title: query.get("q") ?? undefined,
|
||||
};
|
||||
|
||||
const [createGallery] = useGalleryCreate();
|
||||
|
||||
async function onSave(input: GQL.GalleryCreateInput) {
|
||||
const result = await createGallery({
|
||||
variables: { input },
|
||||
});
|
||||
if (result.data?.galleryCreate) {
|
||||
history.push(`/galleries/${result.data.galleryCreate.id}`);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.created_entity" },
|
||||
{ entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row new-view">
|
||||
<div className="col-md-6">
|
||||
|
|
@ -20,7 +43,12 @@ const GalleryCreate: React.FC = () => {
|
|||
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
||||
/>
|
||||
</h2>
|
||||
<GalleryEditPanel gallery={gallery} isVisible onDelete={() => {}} />
|
||||
<GalleryEditPanel
|
||||
gallery={gallery}
|
||||
isVisible
|
||||
onSubmit={onSave}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useHistory, Prompt } from "react-router-dom";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
|
|
@ -15,8 +15,6 @@ import * as yup from "yup";
|
|||
import {
|
||||
queryScrapeGallery,
|
||||
queryScrapeGalleryURL,
|
||||
useGalleryCreate,
|
||||
useGalleryUpdate,
|
||||
useListGalleryScrapers,
|
||||
mutateReloadScrapers,
|
||||
} from "src/core/StashService";
|
||||
|
|
@ -44,17 +42,18 @@ import { DateInput } from "src/components/Shared/DateInput";
|
|||
interface IProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
isVisible: boolean;
|
||||
onSubmit: (input: GQL.GalleryCreateInput) => Promise<void>;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export const GalleryEditPanel: React.FC<IProps> = ({
|
||||
gallery,
|
||||
isVisible,
|
||||
onSubmit,
|
||||
onDelete,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
const [scenes, setScenes] = useState<{ id: string; title: string }[]>(
|
||||
(gallery?.scenes ?? []).map((s) => ({
|
||||
id: s.id,
|
||||
|
|
@ -74,9 +73,6 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [createGallery] = useGalleryCreate();
|
||||
const [updateGallery] = useGalleryUpdate();
|
||||
|
||||
const titleRequired =
|
||||
isNew || (gallery?.files?.length === 0 && !gallery?.folder);
|
||||
|
||||
|
|
@ -151,7 +147,9 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
Mousetrap.bind("s s", () => {
|
||||
formik.handleSubmit();
|
||||
if (formik.dirty) {
|
||||
formik.submitForm();
|
||||
}
|
||||
});
|
||||
Mousetrap.bind("d d", () => {
|
||||
onDelete();
|
||||
|
|
@ -174,51 +172,11 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
setQueryableScrapers(newQueryableScrapers);
|
||||
}, [Scrapers]);
|
||||
|
||||
async function onSave(input: GQL.GalleryCreateInput) {
|
||||
async function onSave(input: InputValues) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (isNew) {
|
||||
const result = await createGallery({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
if (result.data?.galleryCreate) {
|
||||
history.push(`/galleries/${result.data.galleryCreate.id}`);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.created_entity" },
|
||||
{
|
||||
entity: intl
|
||||
.formatMessage({ id: "gallery" })
|
||||
.toLocaleLowerCase(),
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const result = await updateGallery({
|
||||
variables: {
|
||||
input: {
|
||||
id: gallery.id!,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.galleryUpdate) {
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{
|
||||
entity: intl
|
||||
.formatMessage({ id: "gallery" })
|
||||
.toLocaleLowerCase(),
|
||||
}
|
||||
),
|
||||
});
|
||||
await onSubmit(input);
|
||||
formik.resetForm();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { Icon } from "src/components/Shared/Icon";
|
|||
import { Counter } from "src/components/Shared/Counter";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import * as Mousetrap from "mousetrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
|
||||
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
|
||||
import { ImageFileInfoPanel } from "./ImageFileInfoPanel";
|
||||
|
|
@ -51,6 +52,18 @@ export const Image: React.FC = () => {
|
|||
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
async function onSave(input: GQL.ImageUpdateInput) {
|
||||
await updateImage({
|
||||
variables: { input },
|
||||
});
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{ entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function onRescan() {
|
||||
if (!image || !image.visual_files.length) {
|
||||
return;
|
||||
|
|
@ -225,6 +238,7 @@ export const Image: React.FC = () => {
|
|||
<ImageEditPanel
|
||||
isVisible={activeTabKey === "image-edit-panel"}
|
||||
image={image}
|
||||
onSubmit={onSave}
|
||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||
import Mousetrap from "mousetrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import * as yup from "yup";
|
||||
import { useImageUpdate } from "src/core/StashService";
|
||||
import {
|
||||
PerformerSelect,
|
||||
TagSelect,
|
||||
|
|
@ -25,12 +24,14 @@ import { DateInput } from "src/components/Shared/DateInput";
|
|||
interface IProps {
|
||||
image: GQL.ImageDataFragment;
|
||||
isVisible: boolean;
|
||||
onSubmit: (input: GQL.ImageUpdateInput) => Promise<void>;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export const ImageEditPanel: React.FC<IProps> = ({
|
||||
image,
|
||||
isVisible,
|
||||
onSubmit,
|
||||
onDelete,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
|
@ -41,8 +42,6 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
const [updateImage] = useImageUpdate();
|
||||
|
||||
const schema = yup.object({
|
||||
title: yup.string().ensure(),
|
||||
url: yup.string().ensure(),
|
||||
|
|
@ -97,7 +96,9 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
Mousetrap.bind("s s", () => {
|
||||
formik.handleSubmit();
|
||||
if (formik.dirty) {
|
||||
formik.submitForm();
|
||||
}
|
||||
});
|
||||
Mousetrap.bind("d d", () => {
|
||||
onDelete();
|
||||
|
|
@ -113,23 +114,11 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
async function onSave(input: InputValues) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await updateImage({
|
||||
variables: {
|
||||
input: {
|
||||
await onSubmit({
|
||||
id: image.id,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.imageUpdate) {
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{ entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
formik.resetForm();
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("e", () => toggleEditing());
|
||||
Mousetrap.bind("d d", () => {
|
||||
onDelete();
|
||||
});
|
||||
|
|
@ -95,8 +95,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||
});
|
||||
|
||||
async function onSave(input: GQL.MovieCreateInput) {
|
||||
try {
|
||||
const result = await updateMovie({
|
||||
await updateMovie({
|
||||
variables: {
|
||||
input: {
|
||||
id: movie.id,
|
||||
|
|
@ -104,13 +103,13 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.movieUpdate) {
|
||||
setIsEditing(false);
|
||||
history.push(`/movies/${result.data.movieUpdate.id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
toggleEditing(false);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{ entity: intl.formatMessage({ id: "movie" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function onDelete() {
|
||||
|
|
@ -124,8 +123,12 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||
history.push(`/movies`);
|
||||
}
|
||||
|
||||
function onToggleEdit() {
|
||||
setIsEditing(!isEditing);
|
||||
function toggleEditing(value?: boolean) {
|
||||
if (value !== undefined) {
|
||||
setIsEditing(value);
|
||||
} else {
|
||||
setIsEditing((e) => !e);
|
||||
}
|
||||
setFrontImage(undefined);
|
||||
setBackImage(undefined);
|
||||
}
|
||||
|
|
@ -239,7 +242,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||
objectName={movie.name}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onToggleEdit}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
onDelete={onDelete}
|
||||
|
|
@ -249,7 +252,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||
<MovieEditPanel
|
||||
movie={movie}
|
||||
onSubmit={onSave}
|
||||
onCancel={onToggleEdit}
|
||||
onCancel={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
setFrontImage={setFrontImage}
|
||||
setBackImage={setBackImage}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@ import React, { useMemo, useState } from "react";
|
|||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useMovieCreate } from "src/core/StashService";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { useIntl } from "react-intl";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { MovieEditPanel } from "./MovieEditPanel";
|
||||
|
||||
const MovieCreate: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const location = useLocation();
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
const movie = {
|
||||
name: query.get("q") ?? undefined,
|
||||
|
|
@ -24,15 +26,17 @@ const MovieCreate: React.FC = () => {
|
|||
const [createMovie] = useMovieCreate();
|
||||
|
||||
async function onSave(input: GQL.MovieCreateInput) {
|
||||
try {
|
||||
const result = await createMovie({
|
||||
variables: input,
|
||||
});
|
||||
if (result.data?.movieCreate?.id) {
|
||||
history.push(`/movies/${result.data.movieCreate.id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.created_entity" },
|
||||
{ entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { DateInput } from "src/components/Shared/DateInput";
|
|||
|
||||
interface IMovieEditPanel {
|
||||
movie: Partial<GQL.MovieDataFragment>;
|
||||
onSubmit: (movie: GQL.MovieCreateInput) => void;
|
||||
onSubmit: (movie: GQL.MovieCreateInput) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
setFrontImage: (image?: string | null) => void;
|
||||
|
|
@ -103,7 +103,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validationSchema: schema,
|
||||
onSubmit: (values) => onSubmit(values),
|
||||
onSubmit: (values) => onSave(values),
|
||||
});
|
||||
|
||||
function setRating(v: number) {
|
||||
|
|
@ -116,19 +116,17 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
setRating
|
||||
);
|
||||
|
||||
function onCancelEditing() {
|
||||
setFrontImage(undefined);
|
||||
setBackImage(undefined);
|
||||
onCancel?.();
|
||||
}
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
// Mousetrap.bind("u", (e) => {
|
||||
// setStudioFocus()
|
||||
// e.preventDefault();
|
||||
// });
|
||||
Mousetrap.bind("s s", () => formik.handleSubmit());
|
||||
Mousetrap.bind("s s", () => {
|
||||
if (formik.dirty) {
|
||||
formik.submitForm();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
// Mousetrap.unbind("u");
|
||||
|
|
@ -182,6 +180,17 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
}
|
||||
}
|
||||
|
||||
async function onSave(input: InputValues) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onSubmit(input);
|
||||
formik.resetForm();
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
async function onScrapeMovieURL() {
|
||||
const { url } = formik.values;
|
||||
if (!url) return;
|
||||
|
|
@ -488,7 +497,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
objectName={movie?.name ?? intl.formatMessage({ id: "movie" })}
|
||||
isNew={isNew}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onCancelEditing}
|
||||
onToggleEdit={onCancel}
|
||||
onSave={formik.handleSubmit}
|
||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||
onImageChange={onFrontImageChange}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
|
||||
const activeImage = useMemo(() => {
|
||||
const performerImage = performer.image_path;
|
||||
if (isEditing) {
|
||||
if (image === null && performerImage) {
|
||||
const performerImageURL = new URL(performerImage);
|
||||
performerImageURL.searchParams.set("default", "true");
|
||||
|
|
@ -75,8 +76,9 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
} else if (image) {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
return performerImage;
|
||||
}, [image, performer.image_path]);
|
||||
}, [image, isEditing, performer.image_path]);
|
||||
|
||||
const lightboxImages = useMemo(
|
||||
() => [{ paths: { thumbnail: activeImage, image: activeImage } }],
|
||||
|
|
@ -122,15 +124,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
setRating
|
||||
);
|
||||
|
||||
// reset image if performer changed
|
||||
useEffect(() => {
|
||||
setImage(undefined);
|
||||
}, [performer]);
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("a", () => setActiveTabKey("details"));
|
||||
Mousetrap.bind("e", () => setIsEditing(!isEditing));
|
||||
Mousetrap.bind("e", () => toggleEditing());
|
||||
Mousetrap.bind("c", () => setActiveTabKey("scenes"));
|
||||
Mousetrap.bind("g", () => setActiveTabKey("galleries"));
|
||||
Mousetrap.bind("m", () => setActiveTabKey("movies"));
|
||||
|
|
@ -147,6 +144,24 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
};
|
||||
});
|
||||
|
||||
async function onSave(input: GQL.PerformerCreateInput) {
|
||||
await updatePerformer({
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
toggleEditing(false);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{ entity: intl.formatMessage({ id: "performer" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function onDelete() {
|
||||
try {
|
||||
await deletePerformer({ variables: { id: performer.id } });
|
||||
|
|
@ -158,6 +173,15 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
history.push("/performers");
|
||||
}
|
||||
|
||||
function toggleEditing(value?: boolean) {
|
||||
if (value !== undefined) {
|
||||
setIsEditing(value);
|
||||
} else {
|
||||
setIsEditing((e) => !e);
|
||||
}
|
||||
setImage(undefined);
|
||||
}
|
||||
|
||||
function renderImage() {
|
||||
if (activeImage) {
|
||||
return (
|
||||
|
|
@ -175,9 +199,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
objectName={
|
||||
performer?.name ?? intl.formatMessage({ id: "performer" })
|
||||
}
|
||||
onToggleEdit={() => {
|
||||
setIsEditing(!isEditing);
|
||||
}}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
onAutoTag={onAutoTag}
|
||||
isNew={false}
|
||||
|
|
@ -297,7 +319,8 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible={isEditing}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => toggleEditing()}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,16 @@ import React, { useMemo, useState } from "react";
|
|||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { usePerformerCreate } from "src/core/StashService";
|
||||
|
||||
const PerformerCreate: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
|
|
@ -14,7 +21,24 @@ const PerformerCreate: React.FC = () => {
|
|||
name: query.get("q") ?? undefined,
|
||||
};
|
||||
|
||||
const intl = useIntl();
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
|
||||
async function onSave(input: GQL.PerformerCreateInput) {
|
||||
const result = await createPerformer({
|
||||
variables: { input },
|
||||
});
|
||||
if (result.data?.performerCreate) {
|
||||
history.push(`/performers/${result.data.performerCreate.id}`);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.created_entity" },
|
||||
{
|
||||
entity: intl.formatMessage({ id: "performer" }).toLocaleLowerCase(),
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderPerformerImage() {
|
||||
if (encodingImage) {
|
||||
|
|
@ -46,6 +70,7 @@ const PerformerCreate: React.FC = () => {
|
|||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible
|
||||
onSubmit={onSave}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import {
|
|||
useListPerformerScrapers,
|
||||
queryScrapePerformer,
|
||||
mutateReloadScrapers,
|
||||
usePerformerUpdate,
|
||||
usePerformerCreate,
|
||||
useTagCreate,
|
||||
queryScrapePerformerURL,
|
||||
} from "src/core/StashService";
|
||||
|
|
@ -24,7 +22,7 @@ import ImageUtils from "src/utils/image";
|
|||
import { getStashIDs } from "src/utils/stashIds";
|
||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { Prompt, useHistory } from "react-router-dom";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { useFormik } from "formik";
|
||||
import {
|
||||
genderToString,
|
||||
|
|
@ -57,6 +55,7 @@ const isScraper = (
|
|||
interface IPerformerDetails {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
isVisible: boolean;
|
||||
onSubmit: (performer: GQL.PerformerCreateInput) => Promise<void>;
|
||||
onCancel?: () => void;
|
||||
setImage: (image?: string | null) => void;
|
||||
setEncodingImage: (loading: boolean) => void;
|
||||
|
|
@ -65,12 +64,12 @@ interface IPerformerDetails {
|
|||
export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
performer,
|
||||
isVisible,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
setImage,
|
||||
setEncodingImage,
|
||||
}) => {
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
|
||||
const isNew = performer.id === undefined;
|
||||
|
||||
|
|
@ -82,9 +81,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [updatePerformer] = usePerformerUpdate();
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
|
||||
const Scrapers = useListPerformerScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||
|
||||
|
|
@ -454,61 +450,35 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
function valuesToInput(input: InputValues): GQL.PerformerCreateInput {
|
||||
return {
|
||||
...input,
|
||||
gender: input.gender || null,
|
||||
height_cm: input.height_cm || null,
|
||||
weight: input.weight || null,
|
||||
penis_length: input.penis_length || null,
|
||||
circumcised: input.circumcised || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function onSave(input: InputValues) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (isNew) {
|
||||
const result = await createPerformer({
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
gender: input.gender || null,
|
||||
height_cm: input.height_cm || null,
|
||||
weight: input.weight || null,
|
||||
penis_length: input.penis_length || null,
|
||||
circumcised: input.circumcised || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.performerCreate) {
|
||||
history.push(`/performers/${result.data.performerCreate.id}`);
|
||||
}
|
||||
} else {
|
||||
await updatePerformer({
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id!,
|
||||
...input,
|
||||
gender: input.gender || null,
|
||||
height_cm: input.height_cm || null,
|
||||
weight: input.weight || null,
|
||||
penis_length: input.penis_length || null,
|
||||
circumcised: input.circumcised || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
await onSubmit(valuesToInput(input));
|
||||
formik.resetForm();
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!isNew && onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
function onCancelEditing() {
|
||||
setImage(undefined);
|
||||
onCancel?.();
|
||||
}
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
Mousetrap.bind("s s", () => {
|
||||
onSave?.(formik.values);
|
||||
if (formik.dirty) {
|
||||
formik.submitForm();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
|
@ -699,9 +669,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
}
|
||||
|
||||
const currentPerformer = {
|
||||
...formik.values,
|
||||
gender: formik.values.gender || null,
|
||||
circumcised: formik.values.circumcised || null,
|
||||
...valuesToInput(formik.values),
|
||||
image: formik.values.image ?? performer.image_path,
|
||||
};
|
||||
|
||||
|
|
@ -729,7 +697,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
return (
|
||||
<div className={cx("details-edit", "col-xl-9", classNames)}>
|
||||
{!isNew && onCancel ? (
|
||||
<Button className="mr-2" variant="primary" onClick={onCancelEditing}>
|
||||
<Button className="mr-2" variant="primary" onClick={onCancel}>
|
||||
<FormattedMessage id="actions.cancel" />
|
||||
</Button>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -169,6 +169,23 @@ const ScenePage: React.FC<IProps> = ({
|
|||
};
|
||||
});
|
||||
|
||||
async function onSave(input: GQL.SceneCreateInput) {
|
||||
await updateScene({
|
||||
variables: {
|
||||
input: {
|
||||
id: scene.id,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{ entity: intl.formatMessage({ id: "scene" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const onOrganizedClick = async () => {
|
||||
try {
|
||||
setOrganizedLoading(true);
|
||||
|
|
@ -461,6 +478,7 @@ const ScenePage: React.FC<IProps> = ({
|
|||
<SceneEditPanel
|
||||
isVisible={activeTabKey === "scene-edit-panel"}
|
||||
scene={scene}
|
||||
onSubmit={onSave}
|
||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { SceneEditPanel } from "./SceneEditPanel";
|
||||
import { useFindScene } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { mutateCreateScene, useFindScene } from "src/core/StashService";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
|
||||
const SceneCreate: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const location = useLocation();
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
|
||||
// create scene from provided scene id if applicable
|
||||
const { data, loading } = useFindScene(query.get("from_scene_id") ?? "");
|
||||
const { data, loading } = useFindScene(query.get("from_scene_id") ?? "new");
|
||||
const [loadingCoverImage, setLoadingCoverImage] = useState(false);
|
||||
const [coverImage, setCoverImage] = useState<string>();
|
||||
|
||||
|
|
@ -53,6 +57,23 @@ const SceneCreate: React.FC = () => {
|
|||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
async function onSave(input: GQL.SceneCreateInput) {
|
||||
const fileID = query.get("file_id") ?? undefined;
|
||||
const result = await mutateCreateScene({
|
||||
...input,
|
||||
file_ids: fileID ? [fileID] : undefined,
|
||||
});
|
||||
if (result.data?.sceneCreate?.id) {
|
||||
history.push(`/scenes/${result.data.sceneCreate.id}`);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.created_entity" },
|
||||
{ entity: intl.formatMessage({ id: "scene" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row new-view justify-content-center" id="create-scene-page">
|
||||
<div className="col-md-8">
|
||||
|
|
@ -64,10 +85,10 @@ const SceneCreate: React.FC = () => {
|
|||
</h2>
|
||||
<SceneEditPanel
|
||||
scene={scene}
|
||||
fileID={query.get("file_id") ?? undefined}
|
||||
initialCoverImage={coverImage}
|
||||
isVisible
|
||||
isNew
|
||||
onSubmit={onSave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,10 +16,8 @@ import {
|
|||
queryScrapeScene,
|
||||
queryScrapeSceneURL,
|
||||
useListSceneScrapers,
|
||||
useSceneUpdate,
|
||||
mutateReloadScrapers,
|
||||
queryScrapeSceneQueryFragment,
|
||||
mutateCreateScene,
|
||||
} from "src/core/StashService";
|
||||
import {
|
||||
PerformerSelect,
|
||||
|
|
@ -37,7 +35,7 @@ import ImageUtils from "src/utils/image";
|
|||
import FormUtils from "src/utils/form";
|
||||
import { getStashIDs } from "src/utils/stashIds";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt, useHistory } from "react-router-dom";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||
import { SceneMovieTable } from "./SceneMovieTable";
|
||||
|
|
@ -59,24 +57,23 @@ const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
|||
|
||||
interface IProps {
|
||||
scene: Partial<GQL.SceneDataFragment>;
|
||||
fileID?: string;
|
||||
initialCoverImage?: string;
|
||||
isNew?: boolean;
|
||||
isVisible: boolean;
|
||||
onSubmit: (input: GQL.SceneCreateInput) => Promise<void>;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export const SceneEditPanel: React.FC<IProps> = ({
|
||||
scene,
|
||||
fileID,
|
||||
initialCoverImage,
|
||||
isNew = false,
|
||||
isVisible,
|
||||
onSubmit,
|
||||
onDelete,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
|
||||
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
|
||||
[]
|
||||
|
|
@ -92,14 +89,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
const [scrapedScene, setScrapedScene] = useState<GQL.ScrapedScene | null>();
|
||||
const [endpoint, setEndpoint] = useState<string>();
|
||||
|
||||
const [coverImagePreview, setCoverImagePreview] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
setCoverImagePreview(
|
||||
initialCoverImage ?? scene.paths?.screenshot ?? undefined
|
||||
);
|
||||
}, [scene.paths?.screenshot, initialCoverImage]);
|
||||
|
||||
useEffect(() => {
|
||||
setGalleries(
|
||||
scene.galleries?.map((g) => ({
|
||||
|
|
@ -114,8 +103,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [updateScene] = useSceneUpdate();
|
||||
|
||||
const schema = yup.object({
|
||||
title: yup.string().ensure(),
|
||||
code: yup.string().ensure(),
|
||||
|
|
@ -183,6 +170,19 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
onSubmit: (values) => onSave(values),
|
||||
});
|
||||
|
||||
const coverImagePreview = useMemo(() => {
|
||||
const sceneImage = scene.paths?.screenshot;
|
||||
const formImage = formik.values.cover_image;
|
||||
if (formImage === null && sceneImage) {
|
||||
const sceneImageURL = new URL(sceneImage);
|
||||
sceneImageURL.searchParams.set("default", "true");
|
||||
return sceneImageURL.toString();
|
||||
} else if (formImage) {
|
||||
return formImage;
|
||||
}
|
||||
return sceneImage;
|
||||
}, [formik.values.cover_image, scene.paths?.screenshot]);
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating100", v);
|
||||
}
|
||||
|
|
@ -209,7 +209,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
Mousetrap.bind("s s", () => {
|
||||
formik.handleSubmit();
|
||||
if (formik.dirty) {
|
||||
formik.submitForm();
|
||||
}
|
||||
});
|
||||
Mousetrap.bind("d d", () => {
|
||||
if (onDelete) {
|
||||
|
|
@ -259,35 +261,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
async function onSave(input: InputValues) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (!isNew) {
|
||||
const result = await updateScene({
|
||||
variables: {
|
||||
input: {
|
||||
id: scene.id!,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.sceneUpdate) {
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{
|
||||
entity: intl.formatMessage({ id: "scene" }).toLocaleLowerCase(),
|
||||
}
|
||||
),
|
||||
});
|
||||
await onSubmit(input);
|
||||
formik.resetForm();
|
||||
}
|
||||
} else {
|
||||
const result = await mutateCreateScene({
|
||||
...input,
|
||||
file_ids: fileID ? [fileID] : undefined,
|
||||
});
|
||||
if (result.data?.sceneCreate?.id) {
|
||||
history.push(`/scenes/${result.data?.sceneCreate.id}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
|
|
@ -318,7 +293,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setCoverImagePreview(imageData);
|
||||
formik.setFieldValue("cover_image", imageData);
|
||||
}
|
||||
|
||||
|
|
@ -619,7 +593,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
if (updatedScene.image) {
|
||||
// image is a base64 string
|
||||
formik.setFieldValue("cover_image", updatedScene.image);
|
||||
setCoverImagePreview(updatedScene.image);
|
||||
}
|
||||
|
||||
if (updatedScene.remote_site_id && endpoint) {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("e", () => toggleEditing());
|
||||
Mousetrap.bind("d d", () => {
|
||||
onDelete();
|
||||
});
|
||||
|
|
@ -83,8 +83,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||
});
|
||||
|
||||
async function onSave(input: GQL.StudioCreateInput) {
|
||||
try {
|
||||
const result = await updateStudio({
|
||||
await updateStudio({
|
||||
variables: {
|
||||
input: {
|
||||
id: studio.id,
|
||||
|
|
@ -92,12 +91,13 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.studioUpdate) {
|
||||
setIsEditing(false);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
toggleEditing(false);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{ entity: intl.formatMessage({ id: "studio" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function onAutoTag() {
|
||||
|
|
@ -149,8 +149,13 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||
);
|
||||
}
|
||||
|
||||
function onToggleEdit() {
|
||||
setIsEditing(!isEditing);
|
||||
function toggleEditing(value?: boolean) {
|
||||
if (value !== undefined) {
|
||||
setIsEditing(value);
|
||||
} else {
|
||||
setIsEditing((e) => !e);
|
||||
}
|
||||
setImage(undefined);
|
||||
}
|
||||
|
||||
function renderImage() {
|
||||
|
|
@ -213,7 +218,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||
objectName={studio.name ?? intl.formatMessage({ id: "studio" })}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onToggleEdit}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
onClearImage={() => {}}
|
||||
|
|
@ -225,7 +230,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||
<StudioEditPanel
|
||||
studio={studio}
|
||||
onSubmit={onSave}
|
||||
onCancel={onToggleEdit}
|
||||
onCancel={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
|
|
|
|||
|
|
@ -27,15 +27,17 @@ const StudioCreate: React.FC = () => {
|
|||
const [createStudio] = useStudioCreate();
|
||||
|
||||
async function onSave(input: GQL.StudioCreateInput) {
|
||||
try {
|
||||
const result = await createStudio({
|
||||
variables: { input },
|
||||
});
|
||||
if (result.data?.studioCreate?.id) {
|
||||
history.push(`/studios/${result.data.studioCreate.id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.created_entity" },
|
||||
{ entity: intl.formatMessage({ id: "studio" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import * as yup from "yup";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { StudioSelect } from "src/components/Shared/Select";
|
||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||
import { Button, Form, Col, Row } from "react-bootstrap";
|
||||
|
|
@ -18,10 +19,11 @@ import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
|||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
|
||||
interface IStudioEditPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
onSubmit: (studio: GQL.StudioCreateInput) => void;
|
||||
onSubmit: (studio: GQL.StudioCreateInput) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
setImage: (image?: string | null) => void;
|
||||
|
|
@ -37,10 +39,14 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
setEncodingImage,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const isNew = studio.id === undefined;
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const schema = yup.object({
|
||||
name: yup.string().required(),
|
||||
url: yup.string().ensure(),
|
||||
|
|
@ -73,6 +79,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
});
|
||||
|
||||
const initialValues = {
|
||||
id: studio.id,
|
||||
name: studio.name ?? "",
|
||||
url: studio.url ?? "",
|
||||
details: studio.details ?? "",
|
||||
|
|
@ -89,7 +96,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validationSchema: schema,
|
||||
onSubmit: (values) => onSubmit(values),
|
||||
onSubmit: (values) => onSave(values),
|
||||
});
|
||||
|
||||
const encodingImage = ImageUtils.usePasteImage((imageData) =>
|
||||
|
|
@ -114,20 +121,30 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
setRating
|
||||
);
|
||||
|
||||
function onCancelEditing() {
|
||||
setImage(undefined);
|
||||
onCancel?.();
|
||||
}
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("s s", () => formik.handleSubmit());
|
||||
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);
|
||||
}
|
||||
|
|
@ -200,6 +217,8 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
: undefined;
|
||||
const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e));
|
||||
|
||||
if (isLoading) return <LoadingIndicator />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Prompt
|
||||
|
|
@ -331,7 +350,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
|
||||
isNew={isNew}
|
||||
isEditing
|
||||
onToggleEdit={onCancelEditing}
|
||||
onToggleEdit={onCancel}
|
||||
onSave={formik.handleSubmit}
|
||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||
onImageChange={onImageChange}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("e", () => toggleEditing());
|
||||
Mousetrap.bind("d d", () => {
|
||||
onDelete();
|
||||
});
|
||||
|
|
@ -106,7 +106,6 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||
});
|
||||
|
||||
async function onSave(input: GQL.TagCreateInput) {
|
||||
try {
|
||||
const oldRelations = {
|
||||
parents: tag.parents ?? [],
|
||||
children: tag.children ?? [],
|
||||
|
|
@ -120,16 +119,18 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||
},
|
||||
});
|
||||
if (result.data?.tagUpdate) {
|
||||
setIsEditing(false);
|
||||
toggleEditing(false);
|
||||
const updated = result.data.tagUpdate;
|
||||
tagRelationHook(updated, oldRelations, {
|
||||
parents: updated.parents,
|
||||
children: updated.children,
|
||||
});
|
||||
return updated.id;
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{ entity: intl.formatMessage({ id: "tag" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -190,8 +191,12 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||
);
|
||||
}
|
||||
|
||||
function onToggleEdit() {
|
||||
setIsEditing(!isEditing);
|
||||
function toggleEditing(value?: boolean) {
|
||||
if (value !== undefined) {
|
||||
setIsEditing(value);
|
||||
} else {
|
||||
setIsEditing((e) => !e);
|
||||
}
|
||||
setImage(undefined);
|
||||
}
|
||||
|
||||
|
|
@ -283,7 +288,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||
objectName={tag.name}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onToggleEdit}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
onClearImage={() => {}}
|
||||
|
|
@ -297,7 +302,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||
<TagEditPanel
|
||||
tag={tag}
|
||||
onSubmit={onSave}
|
||||
onCancel={onToggleEdit}
|
||||
onCancel={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useMemo, useState } from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useTagCreate } from "src/core/StashService";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
|
|
@ -9,10 +9,11 @@ import { tagRelationHook } from "src/core/tags";
|
|||
import { TagEditPanel } from "./TagEditPanel";
|
||||
|
||||
const TagCreate: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const Toast = useToast();
|
||||
|
||||
const location = useLocation();
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
const tag = {
|
||||
name: query.get("q") ?? undefined,
|
||||
|
|
@ -25,7 +26,6 @@ const TagCreate: React.FC = () => {
|
|||
const [createTag] = useTagCreate();
|
||||
|
||||
async function onSave(input: GQL.TagCreateInput) {
|
||||
try {
|
||||
const oldRelations = {
|
||||
parents: [],
|
||||
children: [],
|
||||
|
|
@ -39,10 +39,13 @@ const TagCreate: React.FC = () => {
|
|||
parents: created.parents,
|
||||
children: created.children,
|
||||
});
|
||||
history.push(`/tags/${result.data.tagCreate.id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
history.push(`/tags/${created.id}`);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.created_entity" },
|
||||
{ entity: intl.formatMessage({ id: "tag" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import * as yup from "yup";
|
||||
|
|
@ -10,13 +10,14 @@ import ImageUtils from "src/utils/image";
|
|||
import { useFormik } from "formik";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { StringListInput } from "src/components/Shared/StringListInput";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
|
||||
interface ITagEditPanel {
|
||||
tag: Partial<GQL.TagDataFragment>;
|
||||
// returns id
|
||||
onSubmit: (tag: GQL.TagCreateInput) => void;
|
||||
onSubmit: (tag: GQL.TagCreateInput) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
setImage: (image?: string | null) => void;
|
||||
|
|
@ -32,9 +33,13 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
setEncodingImage,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const isNew = tag.id === undefined;
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const labelXS = 3;
|
||||
const labelXL = 3;
|
||||
const fieldXS = 9;
|
||||
|
|
@ -84,23 +89,33 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
initialValues,
|
||||
validationSchema: schema,
|
||||
enableReinitialize: true,
|
||||
onSubmit: (values) => onSubmit(values),
|
||||
onSubmit: (values) => onSave(values),
|
||||
});
|
||||
|
||||
function onCancelEditing() {
|
||||
setImage(undefined);
|
||||
onCancel?.();
|
||||
}
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("s s", () => formik.handleSubmit());
|
||||
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);
|
||||
}
|
||||
|
||||
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -127,6 +142,8 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
: undefined;
|
||||
const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e));
|
||||
|
||||
if (isLoading) return <LoadingIndicator />;
|
||||
|
||||
const isEditing = true;
|
||||
|
||||
// TODO: CSS class
|
||||
|
|
@ -275,7 +292,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
objectName={tag?.name ?? intl.formatMessage({ id: "tag" })}
|
||||
isNew={isNew}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onCancelEditing}
|
||||
onToggleEdit={onCancel}
|
||||
onSave={formik.handleSubmit}
|
||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||
onImageChange={onImageChange}
|
||||
|
|
|
|||
|
|
@ -222,8 +222,10 @@ export const useFindGallery = (id: string) => {
|
|||
const skip = id === "new";
|
||||
return GQL.useFindGalleryQuery({ variables: { id }, skip });
|
||||
};
|
||||
export const useFindScene = (id: string) =>
|
||||
GQL.useFindSceneQuery({ variables: { id } });
|
||||
export const useFindScene = (id: string) => {
|
||||
const skip = id === "new";
|
||||
return GQL.useFindSceneQuery({ variables: { id }, skip });
|
||||
};
|
||||
export const useSceneStreams = (id: string) =>
|
||||
GQL.useSceneStreamsQuery({ variables: { id } });
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue