diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index 8b9b74c42..88eccb37a 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -1,9 +1,8 @@ import { Tab, Nav, Dropdown } from "react-bootstrap"; import React, { useEffect, useState } from "react"; import { useParams, useHistory, Link } from "react-router-dom"; -import * as GQL from "src/core/generated-graphql"; import { useFindGallery } from "src/core/StashService"; -import { LoadingIndicator, Icon } from "src/components/Shared"; +import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared"; import { TextUtils } from "src/utils"; import * as Mousetrap from "mousetrap"; import { GalleryEditPanel } from "./GalleryEditPanel"; @@ -22,8 +21,8 @@ export const Gallery: React.FC = () => { const history = useHistory(); const isNew = id === "new"; - const [gallery, setGallery] = useState>({}); const { data, error, loading } = useFindGallery(id); + const gallery = data?.findGallery; const [activeTabKey, setActiveTabKey] = useState("gallery-details-panel"); const activeRightTabKey = tab === "images" || tab === "add" ? tab : "images"; @@ -36,10 +35,6 @@ export const Gallery: React.FC = () => { const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); - useEffect(() => { - if (data?.findGallery) setGallery(data.findGallery); - }, [data]); - function onDeleteDialogClosed(deleted: boolean) { setIsDeleteAlertOpen(false); if (deleted) { @@ -125,8 +120,8 @@ export const Gallery: React.FC = () => { setGallery(newGallery)} onDelete={() => setIsDeleteAlertOpen(true)} /> @@ -183,27 +178,29 @@ export const Gallery: React.FC = () => { }; }); + if (loading) { + return ; + } + + if (error) return ; + if (isNew) return (

Create Gallery

setGallery(newGallery)} onDelete={() => setIsDeleteAlertOpen(true)} />
); - if (loading || !gallery || !data?.findGallery) { - return ; - } - - if (error) return
{error.message}
; + if (!gallery) + return ; return (
diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index ccdda0bb2..b0c694ec3 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { useHistory } from "react-router-dom"; import { Button, Form, Col, Row } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; import { useGalleryCreate, useGalleryUpdate } from "src/core/StashService"; @@ -13,15 +14,25 @@ import { FormUtils, EditableTextUtils } from "src/utils"; import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; interface IProps { - gallery: Partial; isVisible: boolean; - isNew?: boolean; - onUpdate: (gallery: GQL.GalleryDataFragment) => void; onDelete: () => void; } -export const GalleryEditPanel: React.FC = (props: IProps) => { +interface INewProps { + isNew: true; + gallery: undefined; +} + +interface IExistingProps { + isNew: false; + gallery: GQL.GalleryDataFragment; +} + +export const GalleryEditPanel: React.FC< + IProps & (INewProps | IExistingProps) +> = (props) => { const Toast = useToast(); + const history = useHistory(); const [title, setTitle] = useState(); const [details, setDetails] = useState(); const [url, setUrl] = useState(); @@ -83,15 +94,15 @@ export const GalleryEditPanel: React.FC = (props: IProps) => { } }); - function updateGalleryEditState(state: Partial) { - const perfIds = state.performers?.map((performer) => performer.id); - const tIds = state.tags ? state.tags.map((tag) => tag.id) : undefined; + function updateGalleryEditState(state?: GQL.GalleryDataFragment) { + const perfIds = state?.performers?.map((performer) => performer.id); + const tIds = state?.tags ? state?.tags.map((tag) => tag.id) : undefined; - setTitle(state.title ?? undefined); - setDetails(state.details ?? undefined); - setUrl(state.url ?? undefined); - setDate(state.date ?? undefined); - setRating(state.rating === null ? NaN : state.rating); + setTitle(state?.title ?? undefined); + setDetails(state?.details ?? undefined); + setUrl(state?.url ?? undefined); + setDate(state?.date ?? undefined); + setRating(state?.rating === null ? NaN : state?.rating); setStudioId(state?.studio?.id ?? undefined); setPerformerIds(perfIds); setTagIds(tIds); @@ -104,7 +115,7 @@ export const GalleryEditPanel: React.FC = (props: IProps) => { function getGalleryInput() { return { - id: props.isNew ? undefined : props.gallery.id!, + id: props.isNew ? undefined : props.gallery.id, title, details, url, @@ -122,13 +133,12 @@ export const GalleryEditPanel: React.FC = (props: IProps) => { if (props.isNew) { const result = await createGallery(); if (result.data?.galleryCreate) { - props.onUpdate(result.data.galleryCreate); + history.push(`/galleries/${result.data.galleryCreate.id}`); Toast.success({ content: "Created gallery" }); } } else { const result = await updateGallery(); if (result.data?.galleryUpdate) { - props.onUpdate(result.data.galleryUpdate); Toast.success({ content: "Updated gallery" }); } } diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx index f28a595af..d536eba83 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx @@ -8,7 +8,7 @@ import { showWhenSelected } from "src/hooks/ListHook"; import { useToast } from "src/hooks"; interface IGalleryDetailsProps { - gallery: Partial; + gallery: GQL.GalleryDataFragment; } export const GalleryImagesPanel: React.FC = ({ diff --git a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx index 0b8e65576..0abceb958 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx @@ -1,14 +1,13 @@ import { Tab, Nav, Dropdown } from "react-bootstrap"; import React, { useEffect, useState } from "react"; import { useParams, useHistory, Link } from "react-router-dom"; -import * as GQL from "src/core/generated-graphql"; import { useFindImage, useImageIncrementO, useImageDecrementO, useImageResetO, } from "src/core/StashService"; -import { LoadingIndicator, Icon } from "src/components/Shared"; +import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared"; import { useToast } from "src/hooks"; import { TextUtils } from "src/utils"; import * as Mousetrap from "mousetrap"; @@ -27,8 +26,8 @@ export const Image: React.FC = () => { const history = useHistory(); const Toast = useToast(); - const [image, setImage] = useState(); const { data, error, loading } = useFindImage(id); + const image = data?.findImage; const [oLoading, setOLoading] = useState(false); const [incrementO] = useImageIncrementO(image?.id ?? "0"); const [decrementO] = useImageDecrementO(image?.id ?? "0"); @@ -38,21 +37,10 @@ export const Image: React.FC = () => { const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); - useEffect(() => { - if (data?.findImage) setImage(data.findImage); - }, [data]); - - const updateOCounter = (newValue: number) => { - const modifiedImage = { ...image } as GQL.ImageDataFragment; - modifiedImage.o_counter = newValue; - setImage(modifiedImage); - }; - const onIncrementClick = async () => { try { setOLoading(true); - const result = await incrementO(); - if (result.data) updateOCounter(result.data.imageIncrementO); + await incrementO(); } catch (e) { Toast.error(e); } finally { @@ -63,8 +51,7 @@ export const Image: React.FC = () => { const onDecrementClick = async () => { try { setOLoading(true); - const result = await decrementO(); - if (result.data) updateOCounter(result.data.imageDecrementO); + await decrementO(); } catch (e) { Toast.error(e); } finally { @@ -75,8 +62,7 @@ export const Image: React.FC = () => { const onResetClick = async () => { try { setOLoading(true); - const result = await resetO(); - if (result.data) updateOCounter(result.data.imageResetO); + await resetO(); } catch (e) { Toast.error(e); } finally { @@ -172,7 +158,6 @@ export const Image: React.FC = () => { setImage(newImage)} onDelete={() => setIsDeleteAlertOpen(true)} /> @@ -196,11 +181,15 @@ export const Image: React.FC = () => { }; }); - if (loading || !image || !data?.findImage) { + if (loading) { return ; } - if (error) return
{error.message}
; + if (error) return ; + + if (!image) { + return ; + } return (
diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx index 840ec9727..f937e9b84 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx @@ -15,7 +15,6 @@ import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; interface IProps { image: GQL.ImageDataFragment; isVisible: boolean; - onUpdate: (image: GQL.ImageDataFragment) => void; onDelete: () => void; } @@ -107,7 +106,6 @@ export const ImageEditPanel: React.FC = (props: IProps) => { try { const result = await updateImage(); if (result.data?.imageUpdate) { - props.onUpdate(result.data.imageUpdate); Toast.success({ content: "Updated image" }); } } catch (e) { diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index f0c15660c..636c76e40 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -9,7 +9,12 @@ import { usePerformerCreate, usePerformerDestroy, } from "src/core/StashService"; -import { CountryFlag, Icon, LoadingIndicator } from "src/components/Shared"; +import { + CountryFlag, + ErrorMessage, + Icon, + LoadingIndicator, +} from "src/components/Shared"; import { useToast } from "src/hooks"; import { TextUtils } from "src/utils"; import FsLightbox from "fslightbox-react"; @@ -29,12 +34,11 @@ export const Performer: React.FC = () => { const isNew = id === "new"; // Performer state - const [performer, setPerformer] = useState< - Partial - >({}); const [imagePreview, setImagePreview] = useState(); const [imageEncoding, setImageEncoding] = useState(false); const [lightboxToggle, setLightboxToggle] = useState(false); + const { data, loading: performerLoading, error } = useFindPerformer(id); + const performer = data?.findPerformer || ({} as Partial); // if undefined then get the existing image // if null then get the default (no) image @@ -45,9 +49,9 @@ export const Performer: React.FC = () => { : imagePreview ?? `${performer.image_path}?default=true`; // Network state - const [isLoading, setIsLoading] = useState(false); + const [loading, setIsLoading] = useState(false); + const isLoading = performerLoading || loading; - const { data, error } = useFindPerformer(id); const [updatePerformer] = usePerformerUpdate(); const [createPerformer] = usePerformerCreate(); const [deletePerformer] = usePerformerDestroy(); @@ -63,11 +67,6 @@ export const Performer: React.FC = () => { } }; - useEffect(() => { - setIsLoading(false); - if (data?.findPerformer) setPerformer(data.findPerformer); - }, [data]); - const onImageChange = (image?: string | null) => setImagePreview(image); const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding); @@ -89,10 +88,10 @@ export const Performer: React.FC = () => { }; }); - if ((!isNew && (!data || !data.findPerformer)) || isLoading) - return ; - - if (error) return
{error.message}
; + if (isLoading) return ; + if (error) return ; + if (!performer.id && !isNew) + return ; async function onSave( performerInput: @@ -102,21 +101,18 @@ export const Performer: React.FC = () => { setIsLoading(true); try { if (!isNew) { - const result = await updatePerformer({ + await updatePerformer({ variables: performerInput as GQL.PerformerUpdateInput, }); if (performerInput.image) { // Refetch image to bust browser cache - await fetch(`/performer/${performer.id}/image`, { cache: "reload" }); + await fetch(`/performer/${id}/image`, { cache: "reload" }); } - if (result.data?.performerUpdate) - setPerformer(result.data?.performerUpdate); } else { const result = await createPerformer({ variables: performerInput as GQL.PerformerCreateInput, }); if (result.data?.performerCreate) { - setPerformer(result.data.performerCreate); history.push(`/performers/${result.data.performerCreate.id}`); } } @@ -199,8 +195,7 @@ export const Performer: React.FC = () => { } function setFavorite(v: boolean) { - performer.favorite = v; - onSave(performer); + onSave({ ...performer, favorite: v }); } const renderIcons = () => ( diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index b2f9669ba..d6838c66c 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -12,7 +12,7 @@ import { useSceneGenerateScreenshot, } from "src/core/StashService"; import { GalleryViewer } from "src/components/Galleries/GalleryViewer"; -import { LoadingIndicator, Icon } from "src/components/Shared"; +import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared"; import { useToast } from "src/hooks"; import { ScenePlayer } from "src/components/ScenePlayer"; import { TextUtils, JWUtils } from "src/utils"; @@ -39,8 +39,8 @@ export const Scene: React.FC = () => { const [timestamp, setTimestamp] = useState(getInitialTimestamp()); const [collapsed, setCollapsed] = useState(false); - const [scene, setScene] = useState(); const { data, error, loading } = useFindScene(id); + const scene = data?.findScene; const { data: sceneStreams, error: streamableError, @@ -59,10 +59,6 @@ export const Scene: React.FC = () => { const queryParams = queryString.parse(location.search); const autoplay = queryParams?.autoplay === "true"; - useEffect(() => { - if (data?.findScene) setScene(data.findScene); - }, [data]); - function getInitialTimestamp() { const params = queryString.parse(location.search); const initialTimestamp = params?.t ?? "0"; @@ -72,17 +68,10 @@ export const Scene: React.FC = () => { ); } - const updateOCounter = (newValue: number) => { - const modifiedScene = { ...scene } as GQL.SceneDataFragment; - modifiedScene.o_counter = newValue; - setScene(modifiedScene); - }; - const onIncrementClick = async () => { try { setOLoading(true); - const result = await incrementO(); - if (result.data) updateOCounter(result.data.sceneIncrementO); + await incrementO(); } catch (e) { Toast.error(e); } finally { @@ -93,8 +82,7 @@ export const Scene: React.FC = () => { const onDecrementClick = async () => { try { setOLoading(true); - const result = await decrementO(); - if (result.data) updateOCounter(result.data.sceneDecrementO); + await decrementO(); } catch (e) { Toast.error(e); } finally { @@ -105,8 +93,7 @@ export const Scene: React.FC = () => { const onResetClick = async () => { try { setOLoading(true); - const result = await resetO(); - if (result.data) updateOCounter(result.data.sceneResetO); + await resetO(); } catch (e) { Toast.error(e); } finally { @@ -290,7 +277,6 @@ export const Scene: React.FC = () => { setScene(newScene)} onDelete={() => setIsDeleteAlertOpen(true)} /> @@ -320,12 +306,10 @@ export const Scene: React.FC = () => { return collapsed ? ">" : "<"; } - if (loading || streamableLoading || !scene || !data?.findScene) { - return ; - } - - if (error) return
{error.message}
; - if (streamableError) return
{streamableError.message}
; + if (loading || streamableLoading) return ; + if (error) return ; + if (streamableError) return ; + if (!scene) return ; return (
diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index 3299e4bca..d2cba1629 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -36,7 +36,6 @@ import { SceneScrapeDialog } from "./SceneScrapeDialog"; interface IProps { scene: GQL.SceneDataFragment; isVisible: boolean; - onUpdate: (scene: GQL.SceneDataFragment) => void; onDelete: () => void; } @@ -226,7 +225,6 @@ export const SceneEditPanel: React.FC = (props: IProps) => { try { const result = await updateScene(); if (result.data?.sceneUpdate) { - props.onUpdate(result.data.sceneUpdate); Toast.success({ content: "Updated scene" }); } } catch (e) { diff --git a/ui/v2.5/src/components/Shared/ErrorMessage.tsx b/ui/v2.5/src/components/Shared/ErrorMessage.tsx new file mode 100644 index 000000000..2e40a35df --- /dev/null +++ b/ui/v2.5/src/components/Shared/ErrorMessage.tsx @@ -0,0 +1,13 @@ +import React, { ReactNode } from "react"; + +interface IProps { + error: string | ReactNode; +} + +const ErrorMessage: React.FC = ({ error }) => ( +
+

Error: {error}

+
+); + +export default ErrorMessage; diff --git a/ui/v2.5/src/components/Shared/index.ts b/ui/v2.5/src/components/Shared/index.ts index d6f679769..1645f096b 100644 --- a/ui/v2.5/src/components/Shared/index.ts +++ b/ui/v2.5/src/components/Shared/index.ts @@ -19,3 +19,4 @@ export { default as LoadingIndicator } from "./LoadingIndicator"; export { ImageInput } from "./ImageInput"; export { SweatDrops } from "./SweatDrops"; export { default as CountryFlag } from "./CountryFlag"; +export { default as ErrorMessage } from "./ErrorMessage"; diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss index 015621175..05c11c2ae 100644 --- a/ui/v2.5/src/components/Shared/styles.scss +++ b/ui/v2.5/src/components/Shared/styles.scss @@ -133,3 +133,13 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active { max-width: 32rem; text-align: center; } + +.ErrorMessage { + align-items: center; + height: 20rem; + justify-content: center; + + &-content { + display: inline-block; + } +} diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index f4c99107a..f1d187123 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -309,19 +309,55 @@ export const useBulkSceneUpdate = (input: GQL.BulkSceneUpdateInput) => export const useScenesUpdate = (input: GQL.SceneUpdateInput[]) => GQL.useScenesUpdateMutation({ variables: { input } }); +type SceneOMutation = + | GQL.SceneIncrementOMutation + | GQL.SceneDecrementOMutation + | GQL.SceneResetOMutation; +const updateSceneO = ( + id: string, + cache: ApolloCache, + updatedOCount?: number +) => { + const scene = cache.readQuery< + GQL.FindSceneQuery, + GQL.FindSceneQueryVariables + >({ + query: GQL.FindSceneDocument, + variables: { id }, + }); + if (updatedOCount === undefined || !scene?.findScene) return; + + cache.writeQuery({ + query: GQL.FindSceneDocument, + variables: { id }, + data: { + ...scene, + findScene: { + ...scene.findScene, + o_counter: updatedOCount, + }, + }, + }); +}; + export const useSceneIncrementO = (id: string) => GQL.useSceneIncrementOMutation({ variables: { id }, + update: (cache, data) => + updateSceneO(id, cache, data.data?.sceneIncrementO), }); export const useSceneDecrementO = (id: string) => GQL.useSceneDecrementOMutation({ variables: { id }, + update: (cache, data) => + updateSceneO(id, cache, data.data?.sceneDecrementO), }); export const useSceneResetO = (id: string) => GQL.useSceneResetOMutation({ variables: { id }, + update: (cache, data) => updateSceneO(id, cache, data.data?.sceneResetO), }); export const useSceneDestroy = (input: GQL.SceneDestroyInput) => @@ -372,19 +408,54 @@ export const useImagesDestroy = (input: GQL.ImagesDestroyInput) => update: deleteCache(imageMutationImpactedQueries), }); +type ImageOMutation = + | GQL.ImageIncrementOMutation + | GQL.ImageDecrementOMutation + | GQL.ImageResetOMutation; +const updateImageO = ( + id: string, + cache: ApolloCache, + updatedOCount?: number +) => { + const image = cache.readQuery< + GQL.FindImageQuery, + GQL.FindImageQueryVariables + >({ + query: GQL.FindImageDocument, + variables: { id }, + }); + if (updatedOCount === undefined || !image?.findImage) return; + + cache.writeQuery({ + query: GQL.FindImageDocument, + variables: { id }, + data: { + findImage: { + ...image.findImage, + o_counter: updatedOCount, + }, + }, + }); +}; + export const useImageIncrementO = (id: string) => GQL.useImageIncrementOMutation({ variables: { id }, + update: (cache, data) => + updateImageO(id, cache, data.data?.imageIncrementO), }); export const useImageDecrementO = (id: string) => GQL.useImageDecrementOMutation({ variables: { id }, + update: (cache, data) => + updateImageO(id, cache, data.data?.imageDecrementO), }); export const useImageResetO = (id: string) => GQL.useImageResetOMutation({ variables: { id }, + update: (cache, data) => updateImageO(id, cache, data.data?.imageResetO), }); const galleryMutationImpactedQueries = [