diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index d8a43c00171..c62b5b7833a 100644 --- a/ui/v2.5/src/components/Galleries/GalleryCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryCard.tsx @@ -5,7 +5,7 @@ import * as GQL from "src/core/generated-graphql"; import { GridCard } from "../Shared/GridCard"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; -import { GalleryLink } from "../Shared/TagLink"; +import { SceneLink, TagLink } from "../Shared/TagLink"; import { TruncatedText } from "../Shared/TruncatedText"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; import { PopoverCountButton } from "../Shared/PopoverCountButton"; @@ -31,7 +31,7 @@ export const GalleryCard: React.FC = (props) => { if (props.gallery.scenes.length === 0) return; const popoverContent = props.gallery.scenes.map((scene) => ( - + )); return ( @@ -52,7 +52,7 @@ export const GalleryCard: React.FC = (props) => { if (props.gallery.tags.length <= 0) return; const popoverContent = props.gallery.tags.map((tag) => ( - + )); return ( diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx index 791d24a99c0..83ffe2bc3d3 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; -import { GalleryLink } from "src/components/Shared/TagLink"; +import { TagLink } from "src/components/Shared/TagLink"; import { TruncatedText } from "src/components/Shared/TruncatedText"; import { PerformerCard } from "src/components/Performers/PerformerCard"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; @@ -34,7 +34,7 @@ export const GalleryDetailPanel: React.FC = ({ function renderTags() { if (gallery.tags.length === 0) return; const tags = gallery.tags.map((tag) => ( - + )); return ( <> diff --git a/ui/v2.5/src/components/Images/ImageCard.tsx b/ui/v2.5/src/components/Images/ImageCard.tsx index 02007ab937e..5f8c57a53bf 100644 --- a/ui/v2.5/src/components/Images/ImageCard.tsx +++ b/ui/v2.5/src/components/Images/ImageCard.tsx @@ -3,7 +3,7 @@ import { Button, ButtonGroup } from "react-bootstrap"; import cx from "classnames"; import * as GQL from "src/core/generated-graphql"; import { Icon } from "src/components/Shared/Icon"; -import { ImageLink } from "src/components/Shared/TagLink"; +import { GalleryLink, TagLink } from "src/components/Shared/TagLink"; import { HoverPopover } from "src/components/Shared/HoverPopover"; import { SweatDrops } from "src/components/Shared/SweatDrops"; import { PerformerPopoverButton } from "src/components/Shared/PerformerPopoverButton"; @@ -41,7 +41,7 @@ export const ImageCard: React.FC = ( if (props.image.tags.length <= 0) return; const popoverContent = props.image.tags.map((tag) => ( - + )); return ( @@ -83,7 +83,7 @@ export const ImageCard: React.FC = ( if (props.image.galleries.length <= 0) return; const popoverContent = props.image.galleries.map((gallery) => ( - + )); return ( diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx index f4752444a86..417d425cc7b 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from "react"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; -import { ImageLink } from "src/components/Shared/TagLink"; +import { GalleryLink, TagLink } from "src/components/Shared/TagLink"; import { TruncatedText } from "src/components/Shared/TruncatedText"; import { PerformerCard } from "src/components/Performers/PerformerCard"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; @@ -24,7 +24,7 @@ export const ImageDetailPanel: React.FC = (props) => { function renderTags() { if (props.image.tags.length === 0) return; const tags = props.image.tags.map((tag) => ( - + )); return ( <> @@ -67,8 +67,8 @@ export const ImageDetailPanel: React.FC = (props) => { function renderGalleries() { if (props.image.galleries.length === 0) return; - const tags = props.image.galleries.map((gallery) => ( - + const galleries = props.image.galleries.map((gallery) => ( + )); return ( <> @@ -78,7 +78,7 @@ export const ImageDetailPanel: React.FC = (props) => { values={{ count: props.image.galleries.length }} /> - {tags} + {galleries} ); } diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index 4e2dd102af5..fab6acad865 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -9,7 +9,7 @@ import { CountryFlag } from "../Shared/CountryFlag"; import { SweatDrops } from "../Shared/SweatDrops"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; -import { PerformerLink } from "../Shared/TagLink"; +import { TagLink } from "../Shared/TagLink"; import { Button, ButtonGroup } from "react-bootstrap"; import { Criterion, @@ -168,7 +168,7 @@ export const PerformerCard: React.FC = ({ if (performer.tags.length <= 0) return; const popoverContent = performer.tags.map((tag) => ( - + )); return ( diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index a9d18e9f03c..b9c9c2855e7 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useIntl } from "react-intl"; -import { PerformerLink } from "src/components/Shared/TagLink"; +import { TagLink } from "src/components/Shared/TagLink"; import * as GQL from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; import { getStashboxBase } from "src/utils/stashbox"; @@ -29,7 +29,7 @@ export const PerformerDetailsPanel: React.FC = ({ return (
    {(performer.tags ?? []).map((tag) => ( - + ))}
); diff --git a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx index 3610d204408..ac7ce554b2a 100644 --- a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx +++ b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx @@ -18,7 +18,12 @@ import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { ErrorMessage } from "../Shared/ErrorMessage"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; -import { GalleryLink, SceneLink } from "../Shared/TagLink"; +import { + GalleryLink, + MovieLink, + SceneMarkerLink, + TagLink, +} from "../Shared/TagLink"; import { SweatDrops } from "../Shared/SweatDrops"; import { Pagination } from "src/components/List/Pagination"; import TextUtils from "src/utils/text"; @@ -187,7 +192,7 @@ export const SceneDuplicateChecker: React.FC = () => { if (scene.tags.length <= 0) return; const popoverContent = scene.tags.map((tag) => ( - + )); return ( @@ -221,7 +226,7 @@ export const SceneDuplicateChecker: React.FC = () => { src={sceneMovie.movie.front_image_path ?? ""} /> - { if (scene.scene_markers.length <= 0) return; const popoverContent = scene.scene_markers.map((marker) => { - const markerPopover = { ...marker, scene: { id: scene.id } }; - return ; + const markerWithScene = { ...marker, scene: { id: scene.id } }; + return ; }); return ( diff --git a/ui/v2.5/src/components/Scenes/SceneCard.tsx b/ui/v2.5/src/components/Scenes/SceneCard.tsx index 9d31aceb250..0672ae4a61f 100644 --- a/ui/v2.5/src/components/Scenes/SceneCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneCard.tsx @@ -4,7 +4,12 @@ import { Link, useHistory } from "react-router-dom"; import cx from "classnames"; import * as GQL from "src/core/generated-graphql"; import { Icon } from "../Shared/Icon"; -import { SceneLink, GalleryLink } from "../Shared/TagLink"; +import { + GalleryLink, + TagLink, + MovieLink, + SceneMarkerLink, +} from "../Shared/TagLink"; import { HoverPopover } from "../Shared/HoverPopover"; import { SweatDrops } from "../Shared/SweatDrops"; import { TruncatedText } from "../Shared/TruncatedText"; @@ -181,7 +186,7 @@ export const SceneCard: React.FC = ( if (props.scene.tags.length <= 0) return; const popoverContent = props.scene.tags.map((tag) => ( - + )); return ( @@ -219,7 +224,7 @@ export const SceneCard: React.FC = ( src={sceneMovie.movie.front_image_path ?? ""} /> - = ( if (props.scene.scene_markers.length <= 0) return; const popoverContent = props.scene.scene_markers.map((marker) => { - const markerPopover = { ...marker, scene: { id: props.scene.id } }; - return ; + const markerWithScene = { ...marker, scene: { id: props.scene.id } }; + return ; }); return ( diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx index 41a05c9de06..e5277596054 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; -import { DetailsLink } from "src/components/Shared/TagLink"; +import { TagLink } from "src/components/Shared/TagLink"; import { TruncatedText } from "src/components/Shared/TruncatedText"; import { PerformerCard } from "src/components/Performers/PerformerCard"; import { sortPerformers } from "src/core/performers"; @@ -37,7 +37,7 @@ export const SceneDetailPanel: React.FC = (props) => { function renderTags() { if (props.scene.tags.length === 0) return; const tags = props.scene.tags.map((tag) => ( - + )); return ( <> diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index 495df4f5e2d..09d0014e4bf 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -767,12 +767,12 @@ export const TagSelect: React.FC< }; } - const id = (optionProps.data as Option & { __isNew__: boolean }).__isNew__ - ? "" - : optionProps.data.value; + const id = optionProps.data.value; + const hide = (optionProps.data as Option & { __isNew__: boolean }) + .__isNew__; return ( - + ); diff --git a/ui/v2.5/src/components/Shared/TagLink.tsx b/ui/v2.5/src/components/Shared/TagLink.tsx index 81686110939..21086b15b85 100644 --- a/ui/v2.5/src/components/Shared/TagLink.tsx +++ b/ui/v2.5/src/components/Shared/TagLink.tsx @@ -1,16 +1,10 @@ import { Badge, OverlayTrigger, Tooltip } from "react-bootstrap"; -import React from "react"; +import React, { useMemo } from "react"; import { Link } from "react-router-dom"; import cx from "classnames"; -import { - PerformerDataFragment, - TagDataFragment, - MovieDataFragment, - SceneDataFragment, -} from "src/core/generated-graphql"; -import NavUtils from "src/utils/navigation"; +import NavUtils, { INamedObject } from "src/utils/navigation"; import TextUtils from "src/utils/text"; -import { objectTitle } from "src/core/files"; +import { IFile, IObjectWithTitleFiles, objectTitle } from "src/core/files"; import { galleryTitle } from "src/core/galleries"; import * as GQL from "src/core/generated-graphql"; import { TagPopover } from "../Tags/TagPopover"; @@ -19,191 +13,232 @@ import { Placement } from "react-bootstrap/esm/Overlay"; import { faFolderTree } from "@fortawesome/free-solid-svg-icons"; import { Icon } from "../Shared/Icon"; -interface IFile { - path: string; -} -interface IGallery { - id: string; - files: IFile[]; - folder?: GQL.Maybe; - title: GQL.Maybe; -} - type SceneMarkerFragment = Pick & { scene: Pick; primary_tag: Pick; }; -interface IProps { - tag?: Partial; - linkType?: "performer" | "scene" | "gallery" | "image" | "details"; - performer?: Partial; - marker?: SceneMarkerFragment; - movie?: Partial; - scene?: Partial>; - gallery?: Partial; - className?: string; - hoverPlacement?: Placement; - showHierarchyIcon?: boolean; -} - interface ICommonLinkProps { - id: string; link: string; - title: string; className?: string; - hoverPlacement?: Placement; - showHierarchyIcon?: boolean; } const CommonLinkComponent: React.FC = ({ - id, link, - title, className, - hoverPlacement, - showHierarchyIcon = false, + children, }) => { return ( - - - {title} - {showHierarchyIcon && ( - - Explore tag hierarchy - - } - > - - | - - - - )} - - + {children} ); }; -function getLinkAndTitle(props: IProps) { - let id: string = ""; - let link: string = "#"; - let title: string = ""; - if (props.tag) { - id = props.tag.id || ""; - switch (props.linkType) { - case "scene": - case undefined: - link = NavUtils.makeTagScenesUrl(props.tag); - break; - case "performer": - link = NavUtils.makeTagPerformersUrl(props.tag); - break; +interface IPerformerLinkProps { + performer: INamedObject; + linkType?: "scene" | "gallery" | "image"; + className?: string; +} + +export const PerformerLink: React.FC = ({ + performer, + linkType = "scene", + className, +}) => { + const link = useMemo(() => { + switch (linkType) { case "gallery": - link = NavUtils.makeTagGalleriesUrl(props.tag); - break; + return NavUtils.makePerformerGalleriesUrl(performer); case "image": - link = NavUtils.makeTagImagesUrl(props.tag); - break; - case "details": - link = NavUtils.makeTagUrl(id); - break; + return NavUtils.makePerformerImagesUrl(performer); + case "scene": + default: + return NavUtils.makePerformerScenesUrl(performer); } - title = props.tag.name || ""; - } else if (props.performer) { - link = NavUtils.makePerformerScenesUrl(props.performer); - title = props.performer.name || ""; - } else if (props.movie) { - link = NavUtils.makeMovieScenesUrl(props.movie); - title = props.movie.name || ""; - } else if (props.marker) { - link = NavUtils.makeSceneMarkerUrl(props.marker); - title = `${markerTitle(props.marker)} - ${TextUtils.secondsToTimestamp( - props.marker.seconds || 0 - )}`; - } else if (props.gallery) { - link = `/galleries/${props.gallery.id}`; - title = galleryTitle(props.gallery); - } else if (props.scene) { - link = `/scenes/${props.scene.id}`; - title = objectTitle(props.scene); - } - - return { id, link, title }; + }, [performer, linkType]); + + const title = performer.name || ""; + + return ( + + {title} + + ); +}; + +interface IMovieLinkProps { + movie: INamedObject; + linkType?: "scene"; + className?: string; } -export const PerformerLink: React.FC = (props: IProps) => { - const { id, link, title } = getLinkAndTitle(props); +export const MovieLink: React.FC = ({ + movie, + linkType = "scene", + className, +}) => { + const link = useMemo(() => { + switch (linkType) { + case "scene": + return NavUtils.makeMovieScenesUrl(movie); + } + }, [movie, linkType]); + + const title = movie.name || ""; return ( - + + {title} + ); }; -export const SceneLink: React.FC = (props: IProps) => { - const { id, link, title } = getLinkAndTitle(props); +interface ISceneMarkerLinkProps { + marker: SceneMarkerFragment; + linkType?: "scene"; + className?: string; +} + +export const SceneMarkerLink: React.FC = ({ + marker, + linkType = "scene", + className, +}) => { + const link = useMemo(() => { + switch (linkType) { + case "scene": + return NavUtils.makeSceneMarkerUrl(marker); + } + }, [marker, linkType]); + + const title = `${markerTitle(marker)} - ${TextUtils.secondsToTimestamp( + marker.seconds || 0 + )}`; return ( - + + {title} + ); }; -export const GalleryLink: React.FC = (props: IProps) => { - const { id, link, title } = getLinkAndTitle(props); +interface IObjectWithIDTitleFiles extends IObjectWithTitleFiles { + id: string; +} + +interface ISceneLinkProps { + scene: IObjectWithIDTitleFiles; + linkType?: "details"; + className?: string; +} + +export const SceneLink: React.FC = ({ + scene, + linkType = "details", + className, +}) => { + const link = useMemo(() => { + switch (linkType) { + case "details": + return `/scenes/${scene.id}`; + } + }, [scene, linkType]); + + const title = objectTitle(scene); return ( - + + {title} + ); }; -export const ImageLink: React.FC = (props: IProps) => { - const { id, link, title } = getLinkAndTitle(props); +interface IGallery extends IObjectWithIDTitleFiles { + folder?: GQL.Maybe; +} + +interface IGalleryLinkProps { + gallery: IGallery; + linkType?: "details"; + className?: string; +} + +export const GalleryLink: React.FC = ({ + gallery, + linkType = "details", + className, +}) => { + const link = useMemo(() => { + switch (linkType) { + case "details": + return `/galleries/${gallery.id}`; + } + }, [gallery, linkType]); + + const title = galleryTitle(gallery); return ( - + + {title} + ); }; -export const DetailsLink: React.FC = (props: IProps) => { - const { id, link, title } = getLinkAndTitle(props); +interface ITagLinkProps { + tag: INamedObject; + linkType?: "scene" | "gallery" | "image" | "details" | "performer"; + className?: string; + hoverPlacement?: Placement; + showHierarchyIcon?: boolean; +} + +export const TagLink: React.FC = ({ + tag, + linkType = "scene", + className, + hoverPlacement, + showHierarchyIcon = false, +}) => { + const link = useMemo(() => { + switch (linkType) { + case "scene": + return NavUtils.makeTagScenesUrl(tag); + case "performer": + return NavUtils.makeTagPerformersUrl(tag); + case "gallery": + return NavUtils.makeTagGalleriesUrl(tag); + case "image": + return NavUtils.makeTagImagesUrl(tag); + case "details": + return NavUtils.makeTagUrl(tag.id ?? ""); + } + }, [tag, linkType]); + + const title = tag.name || ""; return ( - + + + + {title} + {showHierarchyIcon && ( + + Explore tag hierarchy + + } + > + + | + + + + )} + + + ); }; diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx index 1dde0fe61ce..327bb3ff507 100644 --- a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from "react-intl"; import { sortPerformers } from "src/core/performers"; import { Icon } from "src/components/Shared/Icon"; import { OperationButton } from "src/components/Shared/OperationButton"; -import { PerformerLink, SceneLink } from "src/components/Shared/TagLink"; +import { PerformerLink, TagLink } from "src/components/Shared/TagLink"; import { TruncatedText } from "src/components/Shared/TruncatedText"; import { parsePath, prepareQueryString } from "src/components/Tagger/utils"; import { ScenePreview } from "src/components/Scenes/SceneCard"; @@ -64,7 +64,7 @@ const TaggerSceneDetails: React.FC = ({ scene }) => {
{scene.tags.map((tag) => ( - + ))}
diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx index f0d61dad3bc..f17ccd6ec9f 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { DetailsLink } from "src/components/Shared/TagLink"; +import { TagLink } from "src/components/Shared/TagLink"; import { DetailItem } from "src/components/Shared/DetailItem"; import * as GQL from "src/core/generated-graphql"; @@ -17,7 +17,7 @@ export const TagDetailsPanel: React.FC = ({ tag, fullWidth }) => { return ( <> {tag.parents.map((p) => ( - = ({ tag, fullWidth }) => { return ( <> {tag.children.map((c) => ( - = ({ id }) => { - const { data, loading, error } = useFindTag(id ?? ""); + const { data, loading, error } = useFindTag(id); if (loading) return ( @@ -35,8 +34,15 @@ export const TagPopoverCard: React.FC = ({ id }) => { ); }; +interface ITagPopoverProps { + id: string; + hide?: boolean; + placement?: Placement; +} + export const TagPopover: React.FC = ({ id, + hide, children, placement = "top", }) => { @@ -45,7 +51,7 @@ export const TagPopover: React.FC = ({ const showTagCardOnHover = (config?.ui as IUIConfig)?.showTagCardOnHover ?? true; - if (!id || !showTagCardOnHover) { + if (hide || !showTagCardOnHover) { return <>{children}; } @@ -60,7 +66,3 @@ export const TagPopover: React.FC = ({ ); }; - -interface ITagPopoverCardProps { - id?: string; -} diff --git a/ui/v2.5/src/core/files.ts b/ui/v2.5/src/core/files.ts index 1c2505840c3..52bac6ec036 100644 --- a/ui/v2.5/src/core/files.ts +++ b/ui/v2.5/src/core/files.ts @@ -1,16 +1,16 @@ import TextUtils from "src/utils/text"; import * as GQL from "src/core/generated-graphql"; -interface IFile { +export interface IFile { path: string; } interface IObjectWithFiles { - files: IFile[]; + files?: IFile[]; } -interface IObjectWithTitleFiles extends IObjectWithFiles { - title: GQL.Maybe; +export interface IObjectWithTitleFiles extends IObjectWithFiles { + title?: GQL.Maybe; } export function objectTitle(s: Partial) { diff --git a/ui/v2.5/src/utils/navigation.ts b/ui/v2.5/src/utils/navigation.ts index ddefaeec75f..df7b5b5fc53 100644 --- a/ui/v2.5/src/utils/navigation.ts +++ b/ui/v2.5/src/utils/navigation.ts @@ -73,8 +73,13 @@ const makePerformerImagesUrl = ( return `/images?${filter.makeQueryParameters()}`; }; +export interface INamedObject { + id?: string; + name?: string; +} + const makePerformerGalleriesUrl = ( - performer: Partial, + performer: INamedObject, extraPerformer?: ILabeledId, extraCriteria?: Criterion[] ) => {