diff --git a/ui/v2.5/src/components/Galleries/Galleries.tsx b/ui/v2.5/src/components/Galleries/Galleries.tsx index b4aef94a5..c7c064154 100644 --- a/ui/v2.5/src/components/Galleries/Galleries.tsx +++ b/ui/v2.5/src/components/Galleries/Galleries.tsx @@ -1,7 +1,8 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; import { PersistanceLevel } from "src/hooks/ListHook"; -import { Gallery } from "./GalleryDetails/Gallery"; +import Gallery from "./GalleryDetails/Gallery"; +import GalleryCreate from "./GalleryDetails/GalleryCreate"; import { GalleryList } from "./GalleryList"; const Galleries = () => ( @@ -13,6 +14,7 @@ const Galleries = () => ( )} /> + ); diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index b438d54a7..15ffa2d31 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -2,6 +2,7 @@ import { Tab, Nav, Dropdown } from "react-bootstrap"; import React, { useEffect, useState } from "react"; import { useParams, useHistory, Link } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; +import * as GQL from "src/core/generated-graphql"; import { mutateMetadataScan, useFindGallery, @@ -9,7 +10,7 @@ import { } from "src/core/StashService"; import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared"; import { TextUtils } from "src/utils"; -import * as Mousetrap from "mousetrap"; +import Mousetrap from "mousetrap"; import { useToast } from "src/hooks"; import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton"; import { GalleryEditPanel } from "./GalleryEditPanel"; @@ -20,27 +21,26 @@ import { GalleryAddPanel } from "./GalleryAddPanel"; import { GalleryFileInfoPanel } from "./GalleryFileInfoPanel"; import { GalleryScenesPanel } from "./GalleryScenesPanel"; +interface IProps { + gallery: GQL.GalleryDataFragment; +} + interface IGalleryParams { - id?: string; tab?: string; } -export const Gallery: React.FC = () => { - const { tab = "images", id = "new" } = useParams(); +export const GalleryPage: React.FC = ({ gallery }) => { + const { tab = "images" } = useParams(); const history = useHistory(); const Toast = useToast(); const intl = useIntl(); - const isNew = id === "new"; - - 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"; const setActiveRightTabKey = (newTab: string | null) => { if (tab !== newTab) { const tabParam = newTab === "images" ? "" : `/${newTab}`; - history.replace(`/galleries/${id}${tabParam}`); + history.replace(`/galleries/${gallery.id}${tabParam}`); } }; @@ -54,8 +54,8 @@ export const Gallery: React.FC = () => { await updateGallery({ variables: { input: { - id: gallery?.id ?? "", - organized: !gallery?.organized, + id: gallery.id, + organized: !gallery.organized, }, }, }); @@ -118,7 +118,7 @@ export const Gallery: React.FC = () => { - {gallery?.path ? ( + {gallery.path ? ( { }; }); - if (loading) { - return ; - } - - if (error) return ; - - if (isNew) - return ( -
-
-

- -

- setIsDeleteAlertOpen(true)} - /> -
-
- ); - - if (!gallery) - return ; - return (
{maybeRenderDeleteDialog()} @@ -323,3 +294,17 @@ export const Gallery: React.FC = () => {
); }; + +const GalleryLoader: React.FC = () => { + const { id } = useParams<{ id?: string }>(); + const { data, loading, error } = useFindGallery(id ?? ""); + + if (loading) return ; + if (error) return ; + if (!data?.findGallery) + return ; + + return ; +}; + +export default GalleryLoader; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx index a25908a3c..e256251e0 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx @@ -11,7 +11,7 @@ import { useIntl } from "react-intl"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; interface IGalleryAddProps { - gallery: Partial; + gallery: GQL.GalleryDataFragment; } export const GalleryAddPanel: React.FC = ({ gallery }) => { @@ -20,7 +20,7 @@ export const GalleryAddPanel: React.FC = ({ gallery }) => { function filterHook(filter: ListFilterModel) { const galleryValue = { - id: gallery.id!, + id: gallery.id, label: gallery.title ?? TextUtils.fileNameFromPath(gallery.path ?? ""), }; // if galleries is already present, then we modify it, otherwise add diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx new file mode 100644 index 000000000..e7a0407bc --- /dev/null +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import { GalleryEditPanel } from "./GalleryEditPanel"; + +const GalleryCreate: React.FC = () => { + const intl = useIntl(); + + return ( +
+
+

+ +

+ {}} + /> +
+
+ ); +}; + +export default GalleryCreate; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx index 878137d0f..bc2c3b1e7 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx @@ -9,23 +9,25 @@ import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; import { sortPerformers } from "src/core/performers"; interface IGalleryDetailProps { - gallery: Partial; + gallery: GQL.GalleryDataFragment; } -export const GalleryDetailPanel: React.FC = (props) => { +export const GalleryDetailPanel: React.FC = ({ + gallery, +}) => { function renderDetails() { - if (!props.gallery.details || props.gallery.details === "") return; + if (!gallery.details) return; return ( <>
Details
-

{props.gallery.details}

+

{gallery.details}

); } function renderTags() { - if (!props.gallery.tags || props.gallery.tags.length === 0) return; - const tags = props.gallery.tags.map((tag) => ( + if (gallery.tags.length === 0) return; + const tags = gallery.tags.map((tag) => ( )); return ( @@ -37,14 +39,13 @@ export const GalleryDetailPanel: React.FC = (props) => { } function renderPerformers() { - if (!props.gallery.performers || props.gallery.performers.length === 0) - return; - const performers = sortPerformers(props.gallery.performers); + if (gallery.performers.length === 0) return; + const performers = sortPerformers(gallery.performers); const cards = performers.map((performer) => ( )); @@ -59,9 +60,8 @@ export const GalleryDetailPanel: React.FC = (props) => { } // filename should use entire row if there is no studio - const galleryDetailsWidth = props.gallery.studio ? "col-9" : "col-12"; - const title = - props.gallery.title ?? TextUtils.fileNameFromPath(props.gallery.path ?? ""); + const galleryDetailsWidth = gallery.studio ? "col-9" : "col-12"; + const title = gallery.title ?? TextUtils.fileNameFromPath(gallery.path ?? ""); return ( <> @@ -70,29 +70,29 @@ export const GalleryDetailPanel: React.FC = (props) => {

- {props.gallery.date ? ( + {gallery.date ? (
) : undefined} - {props.gallery.rating ? ( + {gallery.rating ? (
- Rating: + Rating:
) : ( "" )} - {props.gallery.studio && ( + {gallery.studio && (
- + {`${props.gallery.studio.name} diff --git a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx index 8d65b5561..77f7066be 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx @@ -1,17 +1,16 @@ import React, { useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import cx from "classnames"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; import { useFindMovie, useMovieUpdate, - useMovieCreate, useMovieDestroy, } from "src/core/StashService"; import { useParams, useHistory } from "react-router-dom"; import { DetailsEditNavbar, + ErrorMessage, LoadingIndicator, Modal, } from "src/components/Shared"; @@ -20,19 +19,17 @@ import { MovieScenesPanel } from "./MovieScenesPanel"; import { MovieDetailsPanel } from "./MovieDetailsPanel"; import { MovieEditPanel } from "./MovieEditPanel"; -interface IMovieParams { - id?: string; +interface IProps { + movie: GQL.MovieDataFragment; } -export const Movie: React.FC = () => { +const MoviePage: React.FC = ({ movie }) => { const intl = useIntl(); const history = useHistory(); const Toast = useToast(); - const { id = "new" } = useParams(); - const isNew = id === "new"; // Editing state - const [isEditing, setIsEditing] = useState(isNew); + const [isEditing, setIsEditing] = useState(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); // Editing movie state @@ -44,14 +41,10 @@ export const Movie: React.FC = () => { ); const [encodingImage, setEncodingImage] = useState(false); - // Network state - const { data, error, loading } = useFindMovie(id); - const movie = data?.findMovie; - - const [isLoading, setIsLoading] = useState(false); - const [updateMovie] = useMovieUpdate(); - const [createMovie] = useMovieCreate(); - const [deleteMovie] = useMovieDestroy({ id }); + const [updateMovie, { loading: updating }] = useMovieUpdate(); + const [deleteMovie, { loading: deleting }] = useMovieDestroy({ + id: movie.id, + }); // set up hotkeys useEffect(() => { @@ -66,23 +59,14 @@ export const Movie: React.FC = () => { const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding); - if (!isNew && !isEditing) { - if (!data || !data.findMovie || loading) return ; - if (error) { - return <>{error!.message}; - } - } - function getMovieInput( input: Partial ) { const ret: Partial = { ...input, + id: movie.id, }; - if (!isNew) { - (ret as GQL.MovieUpdateInput).id = id; - } return ret; } @@ -90,42 +74,25 @@ export const Movie: React.FC = () => { input: Partial ) { try { - setIsLoading(true); - - if (!isNew) { - const result = await updateMovie({ - variables: { - input: getMovieInput(input) as GQL.MovieUpdateInput, - }, - }); - if (result.data?.movieUpdate) { - setIsEditing(false); - history.push(`/movies/${result.data.movieUpdate.id}`); - } - } else { - const result = await createMovie({ - variables: getMovieInput(input) as GQL.MovieCreateInput, - }); - if (result.data?.movieCreate?.id) { - history.push(`/movies/${result.data.movieCreate.id}`); - setIsEditing(false); - } + const result = await updateMovie({ + variables: { + input: getMovieInput(input) as GQL.MovieUpdateInput, + }, + }); + if (result.data?.movieUpdate) { + setIsEditing(false); + history.push(`/movies/${result.data.movieUpdate.id}`); } } catch (e) { Toast.error(e); - } finally { - setIsLoading(false); } } async function onDelete() { try { - setIsLoading(true); await deleteMovie(); } catch (e) { Toast.error(e); - } finally { - setIsLoading(false); } // redirect to movies page @@ -155,7 +122,7 @@ export const Movie: React.FC = () => { id="dialogs.delete_confirm" values={{ entityName: - movie?.name ?? + movie.name ?? intl.formatMessage({ id: "movie" }).toLocaleLowerCase(), }} /> @@ -165,7 +132,7 @@ export const Movie: React.FC = () => { } function renderFrontImage() { - let image = movie?.front_image_path; + let image = movie.front_image_path; if (isEditing) { if (frontImage === null) { image = `${image}&default=true`; @@ -184,7 +151,7 @@ export const Movie: React.FC = () => { } function renderBackImage() { - let image = movie?.back_image_path; + let image = movie.back_image_path; if (isEditing) { if (backImage === null) { image = undefined; @@ -202,16 +169,12 @@ export const Movie: React.FC = () => { } } - if (isLoading) return ; + if (updating || deleting) return ; // TODO: CSS class return (
-
+
{encodingImage ? ( @@ -223,13 +186,13 @@ export const Movie: React.FC = () => { )}
- {!isEditing && movie ? ( + {!isEditing ? ( <> {/* HACK - this is also rendered in the MovieEditPanel */} {}} @@ -239,7 +202,7 @@ export const Movie: React.FC = () => { ) : ( { )}
- {!isNew && movie && ( -
- -
- )} +
+ +
{renderDeleteAlert()}
); }; + +const MovieLoader: React.FC = () => { + const { id } = useParams<{ id?: string }>(); + const { data, loading, error } = useFindMovie(id ?? ""); + + if (loading) return ; + if (error) return ; + if (!data?.findMovie) + return ; + + return ; +}; + +export default MovieLoader; diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx new file mode 100644 index 000000000..30f8aba19 --- /dev/null +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx @@ -0,0 +1,99 @@ +import React, { useState } from "react"; +import * as GQL from "src/core/generated-graphql"; +import { useMovieCreate } from "src/core/StashService"; +import { useHistory } from "react-router-dom"; +import { LoadingIndicator } from "src/components/Shared"; +import { useToast } from "src/hooks"; +import { MovieEditPanel } from "./MovieEditPanel"; + +export const MovieCreate: React.FC = () => { + const history = useHistory(); + const Toast = useToast(); + + // Editing movie state + const [frontImage, setFrontImage] = useState( + undefined + ); + const [backImage, setBackImage] = useState( + undefined + ); + const [encodingImage, setEncodingImage] = useState(false); + + const [createMovie] = useMovieCreate(); + + const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding); + + function getMovieInput( + input: Partial + ) { + const ret: Partial = { + ...input, + }; + + return ret; + } + + async function onSave( + input: Partial + ) { + try { + const result = await createMovie({ + variables: getMovieInput(input) as GQL.MovieCreateInput, + }); + if (result.data?.movieCreate?.id) { + history.push(`/movies/${result.data.movieCreate.id}`); + } + } catch (e) { + Toast.error(e); + } + } + + function renderFrontImage() { + if (frontImage) { + return ( +
+ Front Cover +
+ ); + } + } + + function renderBackImage() { + if (backImage) { + return ( +
+ Back Cover +
+ ); + } + } + + // TODO: CSS class + return ( +
+
+
+ {encodingImage ? ( + + ) : ( +
+ {renderFrontImage()} + {renderBackImage()} +
+ )} +
+ + history.push("/movies")} + onDelete={() => {}} + setFrontImage={setFrontImage} + setBackImage={setBackImage} + onImageEncoding={onImageEncoding} + /> +
+
+ ); +}; + +export default MovieCreate; diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx index 2635e99fb..1542c79b8 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx @@ -6,7 +6,7 @@ import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; import { TextField, URLField } from "src/utils/field"; interface IMovieDetailsPanel { - movie: Partial; + movie: GQL.MovieDataFragment; } export const MovieDetailsPanel: React.FC = ({ movie }) => { diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx index 981d52751..97ddcc267 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx @@ -386,7 +386,12 @@ export const MovieEditPanel: React.FC = ({ { + // Check if it's a redirect after movie creation + if (action === "PUSH" && location.pathname.startsWith("/movies/")) + return true; + return intl.formatMessage({ id: "dialogs.unsaved_changes" }); + }} />
diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx index 873007fdb..1750bc5f9 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx @@ -5,12 +5,12 @@ import { ListFilterModel } from "src/models/list-filter/filter"; import { SceneList } from "src/components/Scenes/SceneList"; interface IMovieScenesPanel { - movie: Partial; + movie: GQL.MovieDataFragment; } export const MovieScenesPanel: React.FC = ({ movie }) => { function filterHook(filter: ListFilterModel) { - const movieValue = { id: movie.id!, label: movie.name! }; + const movieValue = { id: movie.id, label: movie.name }; // if movie is already present, then we modify it, otherwise add let movieCriterion = filter.criteria.find((c) => { return c.criterionOption.type === "movies"; diff --git a/ui/v2.5/src/components/Movies/Movies.tsx b/ui/v2.5/src/components/Movies/Movies.tsx index 6649d63f0..a07e33903 100644 --- a/ui/v2.5/src/components/Movies/Movies.tsx +++ b/ui/v2.5/src/components/Movies/Movies.tsx @@ -1,11 +1,13 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; -import { Movie } from "./MovieDetails/Movie"; +import Movie from "./MovieDetails/Movie"; +import MovieCreate from "./MovieDetails/MovieCreate"; import { MovieList } from "./MovieList"; const Movies = () => ( + ); diff --git a/ui/v2.5/src/components/Movies/styles.scss b/ui/v2.5/src/components/Movies/styles.scss index 3452b265f..773b04392 100644 --- a/ui/v2.5/src/components/Movies/styles.scss +++ b/ui/v2.5/src/components/Movies/styles.scss @@ -1,7 +1,3 @@ -.movie-details { - max-width: 1200px; -} - .movie-card { width: 240px; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index cc40c8ff0..341dc13e8 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -27,23 +27,21 @@ import { PerformerMoviesPanel } from "./PerformerMoviesPanel"; import { PerformerImagesPanel } from "./PerformerImagesPanel"; import { PerformerEditPanel } from "./PerformerEditPanel"; +interface IProps { + performer: GQL.PerformerDataFragment; +} interface IPerformerParams { - id?: string; tab?: string; } -export const Performer: React.FC = () => { +const PerformerPage: React.FC = ({ performer }) => { const Toast = useToast(); const history = useHistory(); const intl = useIntl(); - const { tab = "details", id = "new" } = useParams(); - const isNew = id === "new"; + const { tab = "details" } = useParams(); - // Performer state const [imagePreview, setImagePreview] = useState(); const [imageEncoding, setImageEncoding] = 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 @@ -51,7 +49,7 @@ export const Performer: React.FC = () => { const activeImage = imagePreview === undefined ? performer.image_path ?? "" - : imagePreview ?? (isNew ? "" : `${performer.image_path}&default=true`); + : imagePreview ?? `${performer.image_path}&default=true`; const lightboxImages = useMemo( () => [{ paths: { thumbnail: activeImage, image: activeImage } }], [activeImage] @@ -61,12 +59,8 @@ export const Performer: React.FC = () => { images: lightboxImages, }); - // Network state - const [loading, setIsLoading] = useState(false); - const isLoading = performerLoading || loading; - const [updatePerformer] = usePerformerUpdate(); - const [deletePerformer] = usePerformerDestroy(); + const [deletePerformer, { loading: isDestroying }] = usePerformerDestroy(); const activeTabKey = tab === "scenes" || @@ -80,7 +74,7 @@ export const Performer: React.FC = () => { const setActiveTabKey = (newTab: string | null) => { if (tab !== newTab) { const tabParam = newTab === "details" ? "" : `/${newTab}`; - history.replace(`/performers/${id}${tabParam}`); + history.replace(`/performers/${performer.id}${tabParam}`); } }; @@ -131,19 +125,12 @@ export const Performer: React.FC = () => { }; }); - if (isLoading) return ; - if (error) return ; - if (!performer.id && !isNew) - return ; - async function onDelete() { - setIsLoading(true); try { - await deletePerformer({ variables: { id } }); + await deletePerformer({ variables: { id: performer.id } }); } catch (e) { Toast.error(e); } - setIsLoading(false); // redirect to performers page history.push("/performers"); @@ -175,7 +162,7 @@ export const Performer: React.FC = () => { { ); - function renderPerformerImage() { - if (imageEncoding) { - return ; - } - if (activeImage) { - return Performer; - } - } - - if (isNew) + if (isDestroying) return ( -
-
- {renderPerformerImage()} -
-
-

Create Performer

- -
-
+ ); - if (!performer.id) { - return ; - } - return (
@@ -370,3 +331,17 @@ export const Performer: React.FC = () => {
); }; + +const PerformerLoader: React.FC = () => { + const { id } = useParams<{ id?: string }>(); + const { data, loading, error } = useFindPerformer(id ?? ""); + + if (loading) return ; + if (error) return ; + if (!data?.findPerformer) + return ; + + return ; +}; + +export default PerformerLoader; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx new file mode 100644 index 000000000..33b60f92b --- /dev/null +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx @@ -0,0 +1,42 @@ +import React, { useState } from "react"; +import { LoadingIndicator } from "src/components/Shared"; +import { PerformerEditPanel } from "./PerformerEditPanel"; + +const PerformerCreate: React.FC = () => { + const [imagePreview, setImagePreview] = useState(); + const [imageEncoding, setImageEncoding] = useState(false); + + const activeImage = imagePreview ?? ""; + + const onImageChange = (image?: string | null) => setImagePreview(image); + const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding); + + function renderPerformerImage() { + if (imageEncoding) { + return ; + } + if (activeImage) { + return Performer; + } + } + + return ( +
+
+ {renderPerformerImage()} +
+
+

Create Performer

+ +
+
+ ); +}; + +export default PerformerCreate; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index 4afb3c172..03223a9c3 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -7,7 +7,7 @@ import { TextField, URLField } from "src/utils/field"; import { genderToString } from "src/utils/gender"; interface IPerformerDetails { - performer: Partial; + performer: GQL.PerformerDataFragment; } export const PerformerDetailsPanel: React.FC = ({ @@ -17,7 +17,7 @@ export const PerformerDetailsPanel: React.FC = ({ const intl = useIntl(); function renderTagsField() { - if (!performer.tags?.length) { + if (!performer.tags.length) { return; } @@ -38,7 +38,7 @@ export const PerformerDetailsPanel: React.FC = ({ } function renderStashIDs() { - if (!performer.stash_ids?.length) { + if (!performer.stash_ids.length) { return; } diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx index 7d1810bac..e7f4f020c 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx @@ -4,7 +4,7 @@ import { GalleryList } from "src/components/Galleries/GalleryList"; import { performerFilterHook } from "src/core/performers"; interface IPerformerDetailsProps { - performer: Partial; + performer: GQL.PerformerDataFragment; } export const PerformerGalleriesPanel: React.FC = ({ diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerImagesPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerImagesPanel.tsx index cb242c580..6e22700ad 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerImagesPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerImagesPanel.tsx @@ -4,7 +4,7 @@ import { ImageList } from "src/components/Images/ImageList"; import { performerFilterHook } from "src/core/performers"; interface IPerformerImagesPanel { - performer: Partial; + performer: GQL.PerformerDataFragment; } export const PerformerImagesPanel: React.FC = ({ diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerMoviesPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerMoviesPanel.tsx index 4176bfb3d..f3facc01b 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerMoviesPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerMoviesPanel.tsx @@ -4,7 +4,7 @@ import { MovieList } from "src/components/Movies/MovieList"; import { performerFilterHook } from "src/core/performers"; interface IPerformerDetailsProps { - performer: Partial; + performer: GQL.PerformerDataFragment; } export const PerformerMoviesPanel: React.FC = ({ diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerOperationsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerOperationsPanel.tsx index 24c710f26..66c658054 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerOperationsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerOperationsPanel.tsx @@ -6,7 +6,7 @@ import { mutateMetadataAutoTag } from "src/core/StashService"; import { useToast } from "src/hooks"; interface IPerformerOperationsProps { - performer: Partial; + performer: GQL.PerformerDataFragment; } export const PerformerOperationsPanel: React.FC = ({ @@ -15,9 +15,6 @@ export const PerformerOperationsPanel: React.FC = ({ const Toast = useToast(); async function onAutoTag() { - if (!performer?.id) { - return; - } try { await mutateMetadataAutoTag({ performers: [performer.id] }); Toast.success({ content: "Started auto tagging" }); diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx index 64464cffa..6cc07b390 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx @@ -4,7 +4,7 @@ import { SceneList } from "src/components/Scenes/SceneList"; import { performerFilterHook } from "src/core/performers"; interface IPerformerDetailsProps { - performer: Partial; + performer: GQL.PerformerDataFragment; } export const PerformerScenesPanel: React.FC = ({ diff --git a/ui/v2.5/src/components/Performers/Performers.tsx b/ui/v2.5/src/components/Performers/Performers.tsx index 53aa517c8..f221feba4 100644 --- a/ui/v2.5/src/components/Performers/Performers.tsx +++ b/ui/v2.5/src/components/Performers/Performers.tsx @@ -1,7 +1,8 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; import { PersistanceLevel } from "src/hooks/ListHook"; -import { Performer } from "./PerformerDetails/Performer"; +import Performer from "./PerformerDetails/Performer"; +import PerformerCreate from "./PerformerDetails/PerformerCreate"; import { PerformerList } from "./PerformerList"; const Performers = () => ( @@ -13,6 +14,7 @@ const Performers = () => ( )} /> + ); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 0e2d14220..413f981e5 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -38,12 +38,12 @@ import { SceneGenerateDialog } from "../SceneGenerateDialog"; import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel"; import { OrganizedButton } from "./OrganizedButton"; -interface ISceneParams { - id?: string; +interface IProps { + scene: GQL.SceneDataFragment; + refetch: () => void; } -export const Scene: React.FC = () => { - const { id = "new" } = useParams(); +const ScenePage: React.FC = ({ scene, refetch }) => { const location = useLocation(); const history = useHistory(); const Toast = useToast(); @@ -53,18 +53,16 @@ export const Scene: React.FC = () => { const [timestamp, setTimestamp] = useState(getInitialTimestamp()); const [collapsed, setCollapsed] = useState(false); - const { data, error, loading, refetch } = useFindScene(id); - const scene = data?.findScene; const { data: sceneStreams, error: streamableError, loading: streamableLoading, - } = useSceneStreams(id); + } = useSceneStreams(scene.id); const [oLoading, setOLoading] = useState(false); - const [incrementO] = useSceneIncrementO(scene?.id ?? "0"); - const [decrementO] = useSceneDecrementO(scene?.id ?? "0"); - const [resetO] = useSceneResetO(scene?.id ?? "0"); + const [incrementO] = useSceneIncrementO(scene.id); + const [decrementO] = useSceneDecrementO(scene.id); + const [resetO] = useSceneResetO(scene.id); const [organizedLoading, setOrganizedLoading] = useState(false); @@ -85,7 +83,7 @@ export const Scene: React.FC = () => { const queryParams = queryString.parse(location.search); const autoplay = queryParams?.autoplay === "true"; - const currentQueueIndex = queueScenes.findIndex((s) => s.id === id); + const currentQueueIndex = queueScenes.findIndex((s) => s.id === scene.id); async function getQueueFilterScenes(filter: ListFilterModel) { const query = await queryFindScenes(filter); @@ -113,7 +111,7 @@ export const Scene: React.FC = () => { useEffect(() => { setRerenderPlayer(true); - }, [id]); + }, [scene.id]); useEffect(() => { setSceneQueue(SceneQueue.fromQueryParameters(location.search)); @@ -142,8 +140,8 @@ export const Scene: React.FC = () => { await updateScene({ variables: { input: { - id: scene?.id ?? "", - organized: !scene?.organized, + id: scene.id, + organized: !scene.organized, }, }, }); @@ -192,10 +190,6 @@ export const Scene: React.FC = () => { } async function onRescan() { - if (!scene) { - return; - } - await mutateMetadataScan({ paths: [scene.path], }); @@ -214,10 +208,6 @@ export const Scene: React.FC = () => { } async function onGenerateScreenshot(at?: number) { - if (!scene) { - return; - } - await generateScreenshot({ variables: { id: scene.id, @@ -323,7 +313,7 @@ export const Scene: React.FC = () => { } function maybeRenderDeleteDialog() { - if (isDeleteAlertOpen && scene) { + if (isDeleteAlertOpen) { return ( ); @@ -331,7 +321,7 @@ export const Scene: React.FC = () => { } function maybeRenderSceneGenerateDialog() { - if (isGenerateDialogOpen && scene) { + if (isGenerateDialogOpen) { return ( { } } - function renderOperations() { - return ( - - - - - - onRescan()} - > - - - setIsGenerateDialogOpen(true)} - > - - - - onGenerateScreenshot(JWUtils.getPlayer().getPosition()) - } - > - - - onGenerateScreenshot()} - > - - - setIsDeleteAlertOpen(true)} - > - - - - - ); - } - - function renderTabs() { - if (!scene) { - return; - } - - return ( - k && setActiveTabKey(k)} + const renderOperations = () => ( + + -
- -
+ +
+ + onRescan()} + > + + + setIsGenerateDialogOpen(true)} + > + + + + onGenerateScreenshot(JWUtils.getPlayer().getPosition()) + } + > + + + onGenerateScreenshot()} + > + + + setIsDeleteAlertOpen(true)} + > + + + +
+ ); - - - - - - playScene(sceneID)} - onNext={onQueueNext} - onPrevious={onQueuePrevious} - onRandom={onQueueRandom} - start={queueStart} - hasMoreScenes={queueHasMoreScenes()} - onLessScenes={() => onQueueLessScenes()} - onMoreScenes={() => onQueueMoreScenes()} - /> - - - - - - - - {scene.galleries.length === 1 && ( - - - + const renderTabs = () => ( + k && setActiveTabKey(k)} + > +
+ +
+ + + + + + + playScene(sceneID)} + onNext={onQueueNext} + onPrevious={onQueuePrevious} + onRandom={onQueueRandom} + start={queueStart} + hasMoreScenes={queueHasMoreScenes()} + onLessScenes={() => onQueueLessScenes()} + onMoreScenes={() => onQueueMoreScenes()} + /> + + + + + + + + {scene.galleries.length === 1 && ( + + - - + )} + {scene.galleries.length > 1 && ( + + - - setIsDeleteAlertOpen(true)} - onUpdate={() => refetch()} - /> - - -
- ); - } + )} + + + + + + + + setIsDeleteAlertOpen(true)} + onUpdate={() => refetch()} + /> + +
+
+ ); // set up hotkeys useEffect(() => { @@ -582,10 +561,8 @@ export const Scene: React.FC = () => { return collapsed ? ">" : "<"; } - if (loading || streamableLoading) return ; - if (error) return ; + if (streamableLoading) return ; if (streamableError) return ; - if (!scene) return ; return (
@@ -638,3 +615,17 @@ export const Scene: React.FC = () => {
); }; + +const SceneLoader: React.FC = () => { + const { id } = useParams<{ id?: string }>(); + const { data, loading, error, refetch } = useFindScene(id ?? ""); + + if (loading) return ; + if (error) return ; + if (!data?.findScene) + return ; + + return ; +}; + +export default SceneLoader; diff --git a/ui/v2.5/src/components/Scenes/Scenes.tsx b/ui/v2.5/src/components/Scenes/Scenes.tsx index 0f301f7f5..d485d3a16 100644 --- a/ui/v2.5/src/components/Scenes/Scenes.tsx +++ b/ui/v2.5/src/components/Scenes/Scenes.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; import { PersistanceLevel } from "src/hooks/ListHook"; -import { Scene } from "./SceneDetails/Scene"; +import Scene from "./SceneDetails/Scene"; import { SceneList } from "./SceneList"; import { SceneMarkerList } from "./SceneMarkerList"; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index 984e60c5c..fd528b16d 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -2,14 +2,12 @@ import { Tabs, Tab } from "react-bootstrap"; import React, { useEffect, useState } from "react"; import { useParams, useHistory } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; -import cx from "classnames"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; import { useFindStudio, useStudioUpdate, - useStudioCreate, useStudioDestroy, mutateMetadataAutoTag, } from "src/core/StashService"; @@ -30,32 +28,29 @@ import { StudioEditPanel } from "./StudioEditPanel"; import { StudioDetailsPanel } from "./StudioDetailsPanel"; import { StudioMoviesPanel } from "./StudioMoviesPanel"; +interface IProps { + studio: GQL.StudioDataFragment; +} + interface IStudioParams { - id?: string; tab?: string; } -export const Studio: React.FC = () => { +const StudioPage: React.FC = ({ studio }) => { const history = useHistory(); const Toast = useToast(); const intl = useIntl(); - const { tab = "details", id = "new" } = useParams(); - const isNew = id === "new"; + const { tab = "details" } = useParams(); // Editing state - const [isEditing, setIsEditing] = useState(isNew); + const [isEditing, setIsEditing] = useState(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); // Studio state const [image, setImage] = useState(); - const { data, loading: studioLoading, error } = useFindStudio(id); - const studio = data?.findStudio; - - const [isLoading, setIsLoading] = useState(false); const [updateStudio] = useStudioUpdate(); - const [createStudio] = useStudioCreate(); - const [deleteStudio] = useStudioDestroy({ id }); + const [deleteStudio] = useStudioDestroy({ id: studio.id }); // set up hotkeys useEffect(() => { @@ -68,53 +63,29 @@ export const Studio: React.FC = () => { }; }); - useEffect(() => { - if (data && data.findStudio) { - setImage(undefined); - } - }, [data]); - function onImageLoad(imageData: string) { setImage(imageData); } const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing); - async function onSave( - input: Partial - ) { + async function onSave(input: Partial) { try { - setIsLoading(true); - - if (!isNew) { - const result = await updateStudio({ - variables: { - input: input as GQL.StudioUpdateInput, - }, - }); - if (result.data?.studioUpdate) { - setIsEditing(false); - } - } else { - const result = await createStudio({ - variables: { - input: input as GQL.StudioCreateInput, - }, - }); - if (result.data?.studioCreate?.id) { - history.push(`/studios/${result.data.studioCreate.id}`); - setIsEditing(false); - } + const result = await updateStudio({ + variables: { + input: input as GQL.StudioUpdateInput, + }, + }); + if (result.data?.studioUpdate) { + setIsEditing(false); } } catch (e) { Toast.error(e); - } finally { - setIsLoading(false); } } async function onAutoTag() { - if (!studio?.id) return; + if (!studio.id) return; try { await mutateMetadataAutoTag({ studios: [studio.id] }); Toast.success({ @@ -153,7 +124,7 @@ export const Studio: React.FC = () => { id="dialogs.delete_confirm" values={{ entityName: - studio?.name ?? + studio.name ?? intl.formatMessage({ id: "studio" }).toLocaleLowerCase(), }} /> @@ -167,7 +138,7 @@ export const Studio: React.FC = () => { } function renderImage() { - let studioImage = studio?.image_path; + let studioImage = studio.image_path; if (isEditing) { if (image === null) { studioImage = `${studioImage}&default=true`; @@ -177,9 +148,7 @@ export const Studio: React.FC = () => { } if (studioImage) { - return ( - {studio?.name - ); + return {studio.name}; } } @@ -194,31 +163,13 @@ export const Studio: React.FC = () => { const setActiveTabKey = (newTab: string | null) => { if (tab !== newTab) { const tabParam = newTab === "scenes" ? "" : `/${newTab}`; - history.replace(`/studios/${id}${tabParam}`); + history.replace(`/studios/${studio.id}${tabParam}`); } }; - if (isLoading || studioLoading) return ; - if (error) return ; - if (!studio?.id && !isNew) - return ; - return (
-
- {isNew && ( -

- {intl.formatMessage( - { id: "actions.add_entity" }, - { entityType: intl.formatMessage({ id: "studio" }) } - )} -

- )} +
{imageEncoding ? ( @@ -226,12 +177,12 @@ export const Studio: React.FC = () => { renderImage() )}
- {!isEditing && !isNew && studio ? ( + {!isEditing ? ( <> {}} @@ -243,7 +194,7 @@ export const Studio: React.FC = () => { ) : ( )} + studio={studio} onSubmit={onSave} onCancel={onToggleEdit} onDelete={onDelete} @@ -251,46 +202,58 @@ export const Studio: React.FC = () => { /> )}
- {studio?.id && ( -
- + + + + + - - - - - - - - - - - - - - - - - - - -
- )} + + + + + + + + + + + + + + + +
{renderDeleteAlert()}
); }; + +const StudioLoader: React.FC = () => { + const { id } = useParams<{ id?: string }>(); + const { data, loading, error } = useFindStudio(id ?? ""); + + if (loading) return ; + if (error) return ; + if (!data?.findStudio) + return ; + + return ; +}; + +export default StudioLoader; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx index a8ef45547..cd4eddec0 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx @@ -5,7 +5,7 @@ import { ListFilterModel } from "src/models/list-filter/filter"; import { StudioList } from "../StudioList"; interface IStudioChildrenPanel { - studio: Partial; + studio: GQL.StudioDataFragment; } export const StudioChildrenPanel: React.FC = ({ diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioCreate.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioCreate.tsx new file mode 100644 index 000000000..080eb7242 --- /dev/null +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioCreate.tsx @@ -0,0 +1,79 @@ +import React, { useState } from "react"; +import { useHistory } from "react-router-dom"; +import { useIntl } from "react-intl"; + +import * as GQL from "src/core/generated-graphql"; +import { useStudioCreate } from "src/core/StashService"; +import { ImageUtils } from "src/utils"; +import { LoadingIndicator } from "src/components/Shared"; +import { useToast } from "src/hooks"; +import { StudioEditPanel } from "./StudioEditPanel"; + +const StudioCreate: React.FC = () => { + const history = useHistory(); + const Toast = useToast(); + const intl = useIntl(); + + // Studio state + const [image, setImage] = useState(); + + const [createStudio] = useStudioCreate(); + + function onImageLoad(imageData: string) { + setImage(imageData); + } + + const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true); + + async function onSave( + input: Partial + ) { + try { + const result = await createStudio({ + variables: { + input: input as GQL.StudioCreateInput, + }, + }); + if (result.data?.studioCreate?.id) { + history.push(`/studios/${result.data.studioCreate.id}`); + } + } catch (e) { + Toast.error(e); + } + } + + function renderImage() { + if (image) { + return ; + } + } + + return ( +
+
+

+ {intl.formatMessage( + { id: "actions.add_entity" }, + { entityType: intl.formatMessage({ id: "studio" }) } + )} +

+
+ {imageEncoding ? ( + + ) : ( + renderImage() + )} +
+ history.push("/studios")} + onDelete={() => {}} + /> +
+
+ ); +}; + +export default StudioCreate; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx index 838d2ebe7..c05e7eca4 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx @@ -7,7 +7,7 @@ import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; import { TextField, URLField } from "src/utils/field"; interface IStudioDetailsPanel { - studio: Partial; + studio: GQL.StudioDataFragment; } export const StudioDetailsPanel: React.FC = ({ @@ -31,7 +31,7 @@ export const StudioDetailsPanel: React.FC = ({ } function renderTagsList() { - if (!studio?.aliases?.length) { + if (!studio.aliases?.length) { return; } diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx index 1f6f227ae..fa793fa12 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx @@ -211,7 +211,12 @@ export const StudioEditPanel: React.FC = ({ <> { + // Check if it's a redirect after studio creation + if (action === "PUSH" && location.pathname.startsWith("/studios/")) + return true; + return intl.formatMessage({ id: "dialogs.unsaved_changes" }); + }} /> diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx index 278a3a162..f71a28600 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx @@ -4,7 +4,7 @@ import { GalleryList } from "src/components/Galleries/GalleryList"; import { studioFilterHook } from "src/core/studios"; interface IStudioGalleriesPanel { - studio: Partial; + studio: GQL.StudioDataFragment; } export const StudioGalleriesPanel: React.FC = ({ diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioImagesPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioImagesPanel.tsx index 429e39c5e..c1b2fb11f 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioImagesPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioImagesPanel.tsx @@ -4,7 +4,7 @@ import { studioFilterHook } from "src/core/studios"; import { ImageList } from "src/components/Images/ImageList"; interface IStudioImagesPanel { - studio: Partial; + studio: GQL.StudioDataFragment; } export const StudioImagesPanel: React.FC = ({ studio }) => { diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioMoviesPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioMoviesPanel.tsx index 36d1844d7..b8510138c 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioMoviesPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioMoviesPanel.tsx @@ -4,7 +4,7 @@ import { MovieList } from "src/components/Movies/MovieList"; import { studioFilterHook } from "src/core/studios"; interface IStudioMoviesPanel { - studio: Partial; + studio: GQL.StudioDataFragment; } export const StudioMoviesPanel: React.FC = ({ studio }) => { diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx index f1ead8a87..32ebf1dae 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx @@ -5,7 +5,7 @@ import { PerformerList } from "src/components/Performers/PerformerList"; import { StudiosCriterion } from "src/models/list-filter/criteria/studios"; interface IStudioPerformersPanel { - studio: Partial; + studio: GQL.StudioDataFragment; } export const StudioPerformersPanel: React.FC = ({ diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx index aefd7614b..0bd2ddfec 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx @@ -4,7 +4,7 @@ import { SceneList } from "src/components/Scenes/SceneList"; import { studioFilterHook } from "src/core/studios"; interface IStudioScenesPanel { - studio: Partial; + studio: GQL.StudioDataFragment; } export const StudioScenesPanel: React.FC = ({ studio }) => { diff --git a/ui/v2.5/src/components/Studios/Studios.tsx b/ui/v2.5/src/components/Studios/Studios.tsx index e466319e9..bf93485bf 100644 --- a/ui/v2.5/src/components/Studios/Studios.tsx +++ b/ui/v2.5/src/components/Studios/Studios.tsx @@ -1,11 +1,13 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; -import { Studio } from "./StudioDetails/Studio"; +import Studio from "./StudioDetails/Studio"; +import StudioCreate from "./StudioDetails/StudioCreate"; import { StudioList } from "./StudioList"; const Studios = () => ( + ); diff --git a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx index f97878254..7bf1f74af 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx @@ -2,20 +2,19 @@ import { Tabs, Tab, Dropdown } from "react-bootstrap"; import React, { useEffect, useState } from "react"; import { useParams, useHistory } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; -import cx from "classnames"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; import { useFindTag, useTagUpdate, - useTagCreate, useTagDestroy, mutateMetadataAutoTag, } from "src/core/StashService"; import { ImageUtils } from "src/utils"; import { DetailsEditNavbar, + ErrorMessage, Modal, LoadingIndicator, Icon, @@ -31,33 +30,30 @@ import { TagDetailsPanel } from "./TagDetailsPanel"; import { TagEditPanel } from "./TagEditPanel"; import { TagMergeModal } from "./TagMergeDialog"; +interface IProps { + tag: GQL.TagDataFragment; +} + interface ITabParams { - id?: string; tab?: string; } -export const Tag: React.FC = () => { +const TagPage: React.FC = ({ tag }) => { const history = useHistory(); const Toast = useToast(); const intl = useIntl(); - const { tab = "scenes", id = "new" } = useParams(); - const isNew = id === "new"; + const { tab = "scenes" } = useParams(); // Editing state - const [isEditing, setIsEditing] = useState(isNew); + const [isEditing, setIsEditing] = useState(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); const [mergeType, setMergeType] = useState<"from" | "into" | undefined>(); // Editing tag state const [image, setImage] = useState(); - // Tag state - const { data, error, loading } = useFindTag(id); - const tag = data?.findTag; - const [updateTag] = useTagUpdate(); - const [createTag] = useTagCreate(); - const [deleteTag] = useTagDestroy({ id }); + const [deleteTag] = useTagDestroy({ id: tag.id }); const activeTabKey = tab === "markers" || @@ -69,7 +65,7 @@ export const Tag: React.FC = () => { const setActiveTabKey = (newTab: string | null) => { if (tab !== newTab) { const tabParam = newTab === "scenes" ? "" : `/${newTab}`; - history.replace(`/tags/${id}${tabParam}`); + history.replace(`/tags/${tag.id}${tabParam}`); } }; @@ -88,35 +84,21 @@ export const Tag: React.FC = () => { }; }); - useEffect(() => { - if (data && data.findTag) { - setImage(undefined); - } - }, [data]); - function onImageLoad(imageData: string) { setImage(imageData); } const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing); - if (!isNew && !isEditing) { - if (!data?.findTag || loading) return ; - if (error) return
{error.message}
; - } - function getTagInput( input: Partial ) { const ret: Partial = { ...input, image, + id: tag.id, }; - if (!isNew) { - (ret as GQL.TagUpdateInput).id = id; - } - return ret; } @@ -125,39 +107,22 @@ export const Tag: React.FC = () => { ) { try { const oldRelations = { - parents: tag?.parents ?? [], - children: tag?.children ?? [], + parents: tag.parents ?? [], + children: tag.children ?? [], }; - if (!isNew) { - const result = await updateTag({ - variables: { - input: getTagInput(input) as GQL.TagUpdateInput, - }, + const result = await updateTag({ + variables: { + input: getTagInput(input) as GQL.TagUpdateInput, + }, + }); + if (result.data?.tagUpdate) { + setIsEditing(false); + const updated = result.data.tagUpdate; + tagRelationHook(updated, oldRelations, { + parents: updated.parents, + children: updated.children, }); - if (result.data?.tagUpdate) { - setIsEditing(false); - const updated = result.data.tagUpdate; - tagRelationHook(updated, oldRelations, { - parents: updated.parents, - children: updated.children, - }); - return updated.id; - } - } else { - const result = await createTag({ - variables: { - input: getTagInput(input) as GQL.TagCreateInput, - }, - }); - if (result.data?.tagCreate?.id) { - setIsEditing(false); - const created = result.data.tagCreate; - tagRelationHook(created, oldRelations, { - parents: created.parents, - children: created.children, - }); - return created.id; - } + return updated.id; } } catch (e) { Toast.error(e); @@ -165,7 +130,7 @@ export const Tag: React.FC = () => { } async function onAutoTag() { - if (!tag?.id) return; + if (!tag.id) return; try { await mutateMetadataAutoTag({ tags: [tag.id] }); Toast.success({ @@ -179,8 +144,8 @@ export const Tag: React.FC = () => { async function onDelete() { try { const oldRelations = { - parents: tag?.parents ?? [], - children: tag?.children ?? [], + parents: tag.parents ?? [], + children: tag.children ?? [], }; await deleteTag(); tagRelationHook(tag as GQL.TagDataFragment, oldRelations, { @@ -212,7 +177,7 @@ export const Tag: React.FC = () => { id="dialogs.delete_confirm" values={{ entityName: - tag?.name ?? + tag.name ?? intl.formatMessage({ id: "tag" }).toLocaleLowerCase(), }} /> @@ -227,7 +192,7 @@ export const Tag: React.FC = () => { } function renderImage() { - let tagImage = tag?.image_path; + let tagImage = tag.image_path; if (isEditing) { if (image === null) { tagImage = `${tagImage}&default=true`; @@ -237,7 +202,7 @@ export const Tag: React.FC = () => { } if (tagImage) { - return {tag?.name; + return {tag.name}; } } @@ -284,27 +249,22 @@ export const Tag: React.FC = () => { return (
-
+
{imageEncoding ? ( ) : ( renderImage() )} - {!isNew && tag &&

{tag.name}

} +

{tag.name}

- {!isEditing && !isNew && tag ? ( + {!isEditing ? ( <> {/* HACK - this is also rendered in the TagEditPanel */} {}} @@ -317,7 +277,7 @@ export const Tag: React.FC = () => { ) : ( { /> )}
- {!isNew && tag && ( -
- + + + + + + + + - - - - - - - - - - - - - - - - -
- )} + + + + + + + + + +
{renderDeleteAlert()} {renderMergeDialog()}
); }; + +const TagLoader: React.FC = () => { + const { id } = useParams<{ id?: string }>(); + const { data, loading, error } = useFindTag(id ?? ""); + + if (loading) return ; + if (error) return ; + if (!data?.findTag) + return ; + + return ; +}; + +export default TagLoader; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx new file mode 100644 index 000000000..7773280de --- /dev/null +++ b/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx @@ -0,0 +1,91 @@ +import React, { useState } from "react"; +import { useHistory } from "react-router-dom"; + +import * as GQL from "src/core/generated-graphql"; +import { useTagCreate } from "src/core/StashService"; +import { ImageUtils } from "src/utils"; +import { LoadingIndicator } from "src/components/Shared"; +import { useToast } from "src/hooks"; +import { tagRelationHook } from "src/core/tags"; +import { TagEditPanel } from "./TagEditPanel"; + +const TagCreate: React.FC = () => { + const history = useHistory(); + const Toast = useToast(); + + // Editing tag state + const [image, setImage] = useState(); + + const [createTag] = useTagCreate(); + + function onImageLoad(imageData: string) { + setImage(imageData); + } + + const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true); + + function getTagInput( + input: Partial + ) { + const ret: Partial = { + ...input, + image, + }; + + return ret; + } + + async function onSave( + input: Partial + ) { + try { + const oldRelations = { + parents: [], + children: [], + }; + const result = await createTag({ + variables: { + input: getTagInput(input) as GQL.TagCreateInput, + }, + }); + if (result.data?.tagCreate?.id) { + const created = result.data.tagCreate; + tagRelationHook(created, oldRelations, { + parents: created.parents, + children: created.children, + }); + return created.id; + } + } catch (e) { + Toast.error(e); + } + } + + function renderImage() { + if (image) { + return ; + } + } + + return ( +
+
+
+ {imageEncoding ? ( + + ) : ( + renderImage() + )} +
+ history.push("/tags")} + onDelete={() => {}} + setImage={setImage} + /> +
+
+ ); +}; + +export default TagCreate; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx index c02570b85..9852130af 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx @@ -5,12 +5,12 @@ import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; interface ITagDetails { - tag: Partial; + tag: GQL.TagDataFragment; } export const TagDetailsPanel: React.FC = ({ tag }) => { function renderAliasesField() { - if (!tag.aliases?.length) { + if (!tag.aliases.length) { return; } diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx index faf1b4320..575729eb2 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx @@ -8,12 +8,12 @@ import { import { SceneMarkerList } from "src/components/Scenes/SceneMarkerList"; interface ITagMarkersPanel { - tag: Partial; + tag: GQL.TagDataFragment; } export const TagMarkersPanel: React.FC = ({ tag }) => { function filterHook(filter: ListFilterModel) { - const tagValue = { id: tag.id!, label: tag.name! }; + const tagValue = { id: tag.id, label: tag.name }; // if tag is already present, then we modify it, otherwise add let tagCriterion = filter.criteria.find((c) => { return c.criterionOption.type === "tags"; diff --git a/ui/v2.5/src/components/Tags/Tags.tsx b/ui/v2.5/src/components/Tags/Tags.tsx index 9c80d44c2..b9ffe7d31 100644 --- a/ui/v2.5/src/components/Tags/Tags.tsx +++ b/ui/v2.5/src/components/Tags/Tags.tsx @@ -1,11 +1,13 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; -import { Tag } from "./TagDetails/Tag"; +import Tag from "./TagDetails/Tag"; +import TagCreate from "./TagDetails/TagCreate"; import { TagList } from "./TagList"; const Tags = () => ( + ); diff --git a/ui/v2.5/src/core/performers.ts b/ui/v2.5/src/core/performers.ts index 8f745d97d..6dadec8d1 100644 --- a/ui/v2.5/src/core/performers.ts +++ b/ui/v2.5/src/core/performers.ts @@ -2,11 +2,12 @@ import { PerformersCriterion } from "src/models/list-filter/criteria/performers" import * as GQL from "src/core/generated-graphql"; import { ListFilterModel } from "src/models/list-filter/filter"; -export const performerFilterHook = ( - performer: Partial -) => { +export const performerFilterHook = (performer: GQL.PerformerDataFragment) => { return (filter: ListFilterModel) => { - const performerValue = { id: performer.id!, label: performer.name! }; + const performerValue = { + id: performer.id, + label: performer.name ?? `Performer ${performer.id}`, + }; // if performers is already present, then we modify it, otherwise add let performerCriterion = filter.criteria.find((c) => { return c.criterionOption.type === "performers"; diff --git a/ui/v2.5/src/core/studios.ts b/ui/v2.5/src/core/studios.ts index f967987aa..1be512ede 100644 --- a/ui/v2.5/src/core/studios.ts +++ b/ui/v2.5/src/core/studios.ts @@ -2,9 +2,9 @@ import * as GQL from "src/core/generated-graphql"; import { StudiosCriterion } from "src/models/list-filter/criteria/studios"; import { ListFilterModel } from "src/models/list-filter/filter"; -export const studioFilterHook = (studio: Partial) => { +export const studioFilterHook = (studio: GQL.StudioDataFragment) => { return (filter: ListFilterModel) => { - const studioValue = { id: studio.id!, label: studio.name! }; + const studioValue = { id: studio.id, label: studio.name }; // if studio is already present, then we modify it, otherwise add let studioCriterion = filter.criteria.find((c) => { return c.criterionOption.type === "studios";