From d0847d1ebfd04085df7aa6d747efb2cd64cc6fcc Mon Sep 17 00:00:00 2001 From: DingDongSoLong4 <99329275+DingDongSoLong4@users.noreply.github.com> Date: Wed, 31 May 2023 02:39:22 +0200 Subject: [PATCH] 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 --- .../Galleries/GalleryDetails/Gallery.tsx | 18 +++++ .../GalleryDetails/GalleryCreate.tsx | 32 +++++++- .../GalleryDetails/GalleryEditPanel.tsx | 60 +++------------ .../components/Images/ImageDetails/Image.tsx | 14 ++++ .../Images/ImageDetails/ImageEditPanel.tsx | 29 +++----- .../components/Movies/MovieDetails/Movie.tsx | 43 ++++++----- .../Movies/MovieDetails/MovieCreate.tsx | 22 +++--- .../Movies/MovieDetails/MovieEditPanel.tsx | 29 +++++--- .../Performers/PerformerDetails/Performer.tsx | 57 +++++++++----- .../PerformerDetails/PerformerCreate.tsx | 29 +++++++- .../PerformerDetails/PerformerEditPanel.tsx | 74 ++++++------------- .../components/Scenes/SceneDetails/Scene.tsx | 18 +++++ .../Scenes/SceneDetails/SceneCreate.tsx | 29 +++++++- .../Scenes/SceneDetails/SceneEditPanel.tsx | 69 ++++++----------- .../Studios/StudioDetails/Studio.tsx | 43 ++++++----- .../Studios/StudioDetails/StudioCreate.tsx | 18 +++-- .../Studios/StudioDetails/StudioEditPanel.tsx | 39 +++++++--- .../src/components/Tags/TagDetails/Tag.tsx | 59 ++++++++------- .../components/Tags/TagDetails/TagCreate.tsx | 41 +++++----- .../Tags/TagDetails/TagEditPanel.tsx | 39 +++++++--- ui/v2.5/src/core/StashService.ts | 6 +- 21 files changed, 436 insertions(+), 332 deletions(-) diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index 057642b1a..f7d50da29 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -64,6 +64,23 @@ export const GalleryPage: React.FC = ({ 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 = ({ gallery }) => { setIsDeleteAlertOpen(true)} /> diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx index 62e80e23e..d6519bcd2 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx @@ -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 (
@@ -20,7 +43,12 @@ const GalleryCreate: React.FC = () => { values={{ entityType: intl.formatMessage({ id: "gallery" }) }} /> - {}} /> + {}} + />
); diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index d560127dc..27f3fdb78 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -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; isVisible: boolean; + onSubmit: (input: GQL.GalleryCreateInput) => Promise; onDelete: () => void; } export const GalleryEditPanel: React.FC = ({ 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 = ({ // 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 = ({ 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 = ({ 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(), - } - ), - }); - formik.resetForm(); - } - } + await onSubmit(input); + formik.resetForm(); } catch (e) { Toast.error(e); } diff --git a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx index b9485767c..c52ae22d7 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx @@ -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(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 = () => { setIsDeleteAlertOpen(true)} /> diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx index 96ace1609..9d7e55e9f 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx @@ -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; onDelete: () => void; } export const ImageEditPanel: React.FC = ({ image, isVisible, + onSubmit, onDelete, }) => { const intl = useIntl(); @@ -41,8 +42,6 @@ export const ImageEditPanel: React.FC = ({ 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 = ({ 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 = ({ async function onSave(input: InputValues) { setIsLoading(true); try { - const result = await updateImage({ - variables: { - input: { - id: image.id, - ...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(); - } + formik.resetForm(); } catch (e) { Toast.error(e); } diff --git a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx index 4e723858c..fc04df94b 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx @@ -83,7 +83,7 @@ const MoviePage: React.FC = ({ movie }) => { // set up hotkeys useEffect(() => { - Mousetrap.bind("e", () => setIsEditing(true)); + Mousetrap.bind("e", () => toggleEditing()); Mousetrap.bind("d d", () => { onDelete(); }); @@ -95,22 +95,21 @@ const MoviePage: React.FC = ({ movie }) => { }); async function onSave(input: GQL.MovieCreateInput) { - try { - const result = await updateMovie({ - variables: { - input: { - id: movie.id, - ...input, - }, + await updateMovie({ + variables: { + input: { + id: movie.id, + ...input, }, - }); - 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 = ({ 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 = ({ movie }) => { objectName={movie.name} isNew={false} isEditing={isEditing} - onToggleEdit={onToggleEdit} + onToggleEdit={() => toggleEditing()} onSave={() => {}} onImageChange={() => {}} onDelete={onDelete} @@ -249,7 +252,7 @@ const MoviePage: React.FC = ({ movie }) => { toggleEditing()} onDelete={onDelete} setFrontImage={setFrontImage} setBackImage={setBackImage} diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx index 36b6ea5bd..973fe89bd 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx @@ -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, + const result = await createMovie({ + variables: input, + }); + if (result.data?.movieCreate?.id) { + history.push(`/movies/${result.data.movieCreate.id}`); + Toast.success({ + content: intl.formatMessage( + { id: "toast.created_entity" }, + { entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase() } + ), }); - if (result.data?.movieCreate?.id) { - history.push(`/movies/${result.data.movieCreate.id}`); - } - } catch (e) { - Toast.error(e); } } diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx index c2c51794c..60f4465ed 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx @@ -28,7 +28,7 @@ import { DateInput } from "src/components/Shared/DateInput"; interface IMovieEditPanel { movie: Partial; - onSubmit: (movie: GQL.MovieCreateInput) => void; + onSubmit: (movie: GQL.MovieCreateInput) => Promise; onCancel: () => void; onDelete: () => void; setFrontImage: (image?: string | null) => void; @@ -103,7 +103,7 @@ export const MovieEditPanel: React.FC = ({ 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 = ({ 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 = ({ } } + 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 = ({ 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} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index 197556c8b..a024124c8 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -68,15 +68,17 @@ const PerformerPage: React.FC = ({ performer }) => { const activeImage = useMemo(() => { const performerImage = performer.image_path; - if (image === null && performerImage) { - const performerImageURL = new URL(performerImage); - performerImageURL.searchParams.set("default", "true"); - return performerImageURL.toString(); - } else if (image) { - return image; + if (isEditing) { + if (image === null && performerImage) { + const performerImageURL = new URL(performerImage); + performerImageURL.searchParams.set("default", "true"); + return performerImageURL.toString(); + } 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ performer }) => { setIsEditing(false)} + onSubmit={onSave} + onCancel={() => toggleEditing()} setImage={setImage} setEncodingImage={setEncodingImage} /> diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx index 2f3e32b2e..26b3f88f0 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx @@ -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(); const [encodingImage, setEncodingImage] = useState(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 = () => { diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index 03f2dd128..5c2d26d3d 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -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; isVisible: boolean; + onSubmit: (performer: GQL.PerformerCreateInput) => Promise; onCancel?: () => void; setImage: (image?: string | null) => void; setEncodingImage: (loading: boolean) => void; @@ -65,12 +64,12 @@ interface IPerformerDetails { export const PerformerEditPanel: React.FC = ({ 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 = ({ // Network state const [isLoading, setIsLoading] = useState(false); - const [updatePerformer] = usePerformerUpdate(); - const [createPerformer] = usePerformerCreate(); - const Scrapers = useListPerformerScrapers(); const [queryableScrapers, setQueryableScrapers] = useState([]); @@ -454,61 +450,35 @@ export const PerformerEditPanel: React.FC = ({ 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 = ({ } 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 = ({ return (
{!isNew && onCancel ? ( - ) : null} diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 5d6bb3690..b3ef0b423 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -169,6 +169,23 @@ const ScenePage: React.FC = ({ }; }); + 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 = ({ setIsDeleteAlertOpen(true)} /> diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneCreate.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneCreate.tsx index 4e289c52d..81181272f 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneCreate.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneCreate.tsx @@ -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(); @@ -53,6 +57,23 @@ const SceneCreate: React.FC = () => { return ; } + 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 (
@@ -64,10 +85,10 @@ const SceneCreate: React.FC = () => {
diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index d3e81aac0..840621e33 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -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; - fileID?: string; initialCoverImage?: string; isNew?: boolean; isVisible: boolean; + onSubmit: (input: GQL.SceneCreateInput) => Promise; onDelete?: () => void; } export const SceneEditPanel: React.FC = ({ 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 = ({ const [scrapedScene, setScrapedScene] = useState(); const [endpoint, setEndpoint] = useState(); - const [coverImagePreview, setCoverImagePreview] = useState(); - - 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 = ({ // 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 = ({ 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 = ({ 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 = ({ 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(), - } - ), - }); - 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}`); - } - } + await onSubmit(input); + formik.resetForm(); } catch (e) { Toast.error(e); } @@ -318,7 +293,6 @@ export const SceneEditPanel: React.FC = ({ 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 = ({ if (updatedScene.image) { // image is a base64 string formik.setFieldValue("cover_image", updatedScene.image); - setCoverImagePreview(updatedScene.image); } if (updatedScene.remote_site_id && endpoint) { diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index 8ae80d93f..049bfa076 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -69,7 +69,7 @@ const StudioPage: React.FC = ({ studio }) => { // set up hotkeys useEffect(() => { - Mousetrap.bind("e", () => setIsEditing(true)); + Mousetrap.bind("e", () => toggleEditing()); Mousetrap.bind("d d", () => { onDelete(); }); @@ -83,21 +83,21 @@ const StudioPage: React.FC = ({ studio }) => { }); async function onSave(input: GQL.StudioCreateInput) { - try { - const result = await updateStudio({ - variables: { - input: { - id: studio.id, - ...input, - }, + await updateStudio({ + variables: { + input: { + id: studio.id, + ...input, }, - }); - 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 = ({ 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 = ({ 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 = ({ studio }) => { toggleEditing()} onDelete={onDelete} setImage={setImage} setEncodingImage={setEncodingImage} diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioCreate.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioCreate.tsx index 44ceb259a..250c85d44 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioCreate.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioCreate.tsx @@ -27,15 +27,17 @@ const StudioCreate: React.FC = () => { const [createStudio] = useStudioCreate(); async function onSave(input: GQL.StudioCreateInput) { - try { - const result = await createStudio({ - variables: { input }, + const result = await createStudio({ + variables: { input }, + }); + if (result.data?.studioCreate?.id) { + history.push(`/studios/${result.data.studioCreate.id}`); + Toast.success({ + content: intl.formatMessage( + { id: "toast.created_entity" }, + { entity: intl.formatMessage({ id: "studio" }).toLocaleLowerCase() } + ), }); - if (result.data?.studioCreate?.id) { - history.push(`/studios/${result.data.studioCreate.id}`); - } - } catch (e) { - Toast.error(e); } } diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx index c2a469a70..8eb919483 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx @@ -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; - onSubmit: (studio: GQL.StudioCreateInput) => void; + onSubmit: (studio: GQL.StudioCreateInput) => Promise; onCancel: () => void; onDelete: () => void; setImage: (image?: string | null) => void; @@ -37,10 +39,14 @@ export const StudioEditPanel: React.FC = ({ 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 = ({ }); const initialValues = { + id: studio.id, name: studio.name ?? "", url: studio.url ?? "", details: studio.details ?? "", @@ -89,7 +96,7 @@ export const StudioEditPanel: React.FC = ({ 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 = ({ 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 = ({ : undefined; const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e)); + if (isLoading) return ; + return ( <> = ({ 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} diff --git a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx index 07f7db40b..3e7cc7d09 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx @@ -88,7 +88,7 @@ const TagPage: React.FC = ({ tag }) => { // set up hotkeys useEffect(() => { - Mousetrap.bind("e", () => setIsEditing(true)); + Mousetrap.bind("e", () => toggleEditing()); Mousetrap.bind("d d", () => { onDelete(); }); @@ -106,30 +106,31 @@ const TagPage: React.FC = ({ tag }) => { }); async function onSave(input: GQL.TagCreateInput) { - try { - const oldRelations = { - parents: tag.parents ?? [], - children: tag.children ?? [], - }; - const result = await updateTag({ - variables: { - input: { - id: tag.id, - ...input, - }, + const oldRelations = { + parents: tag.parents ?? [], + children: tag.children ?? [], + }; + const result = await updateTag({ + variables: { + input: { + id: tag.id, + ...input, }, + }, + }); + if (result.data?.tagUpdate) { + toggleEditing(false); + const updated = result.data.tagUpdate; + tagRelationHook(updated, oldRelations, { + parents: updated.parents, + children: updated.children, + }); + Toast.success({ + content: intl.formatMessage( + { id: "toast.updated_entity" }, + { entity: intl.formatMessage({ id: "tag" }).toLocaleLowerCase() } + ), }); - if (result.data?.tagUpdate) { - setIsEditing(false); - const updated = result.data.tagUpdate; - tagRelationHook(updated, oldRelations, { - parents: updated.parents, - children: updated.children, - }); - return updated.id; - } - } catch (e) { - Toast.error(e); } } @@ -190,8 +191,12 @@ const TagPage: React.FC = ({ 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 = ({ tag }) => { objectName={tag.name} isNew={false} isEditing={isEditing} - onToggleEdit={onToggleEdit} + onToggleEdit={() => toggleEditing()} onSave={() => {}} onImageChange={() => {}} onClearImage={() => {}} @@ -297,7 +302,7 @@ const TagPage: React.FC = ({ tag }) => { toggleEditing()} onDelete={onDelete} setImage={setImage} setEncodingImage={setEncodingImage} diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx index 428d339b0..4b4c6dfc1 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx @@ -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,24 +26,26 @@ const TagCreate: React.FC = () => { const [createTag] = useTagCreate(); async function onSave(input: GQL.TagCreateInput) { - try { - const oldRelations = { - parents: [], - children: [], - }; - const result = await createTag({ - variables: { input }, + const oldRelations = { + parents: [], + children: [], + }; + const result = await createTag({ + variables: { input }, + }); + if (result.data?.tagCreate?.id) { + const created = result.data.tagCreate; + tagRelationHook(created, oldRelations, { + parents: created.parents, + children: created.children, + }); + history.push(`/tags/${created.id}`); + Toast.success({ + content: intl.formatMessage( + { id: "toast.created_entity" }, + { entity: intl.formatMessage({ id: "tag" }).toLocaleLowerCase() } + ), }); - if (result.data?.tagCreate?.id) { - const created = result.data.tagCreate; - tagRelationHook(created, oldRelations, { - parents: created.parents, - children: created.children, - }); - history.push(`/tags/${result.data.tagCreate.id}`); - } - } catch (e) { - Toast.error(e); } } diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx index 2c2eca35d..8ad847fc9 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx @@ -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; - // returns id - onSubmit: (tag: GQL.TagCreateInput) => void; + onSubmit: (tag: GQL.TagCreateInput) => Promise; onCancel: () => void; onDelete: () => void; setImage: (image?: string | null) => void; @@ -32,9 +33,13 @@ export const TagEditPanel: React.FC = ({ 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 = ({ 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 = ({ : undefined; const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e)); + if (isLoading) return ; + const isEditing = true; // TODO: CSS class @@ -275,7 +292,7 @@ export const TagEditPanel: React.FC = ({ 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} diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index df4ce69ea..10bb49d3a 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -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 } });