Add Icons to tags if they have parent/child tags (#3931)

* Add Icons to tags if they have parent/child tags
* Refactor TagLink
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
elkorol 2023-09-20 05:08:00 +01:00 committed by GitHub
parent 36e9ed7a6c
commit 636b0a3167
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 421 additions and 137 deletions

View File

@ -13,12 +13,10 @@ fragment SceneMarkerData on SceneMarker {
primary_tag {
id
name
aliases
}
tags {
id
name
aliases
}
}

View File

@ -3,4 +3,6 @@ fragment SlimTagData on Tag {
name
aliases
image_path
parent_count
child_count
}

View File

@ -15,6 +15,9 @@ type Tag {
performer_count(depth: Int): Int! # Resolver
parents: [Tag!]!
children: [Tag!]!
parent_count: Int! # Resolver
child_count: Int! # Resolver
}
input TagCreateInput {

View File

@ -113,3 +113,25 @@ func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string,
imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj).GetTagImageURL(hasImage)
return &imagePath, nil
}
func (r *tagResolver) ParentCount(ctx context.Context, obj *models.Tag) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.CountByParentTagID(ctx, obj.ID)
return err
}); err != nil {
return ret, err
}
return ret, nil
}
func (r *tagResolver) ChildCount(ctx context.Context, obj *models.Tag) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.CountByChildTagID(ctx, obj.ID)
return err
}); err != nil {
return ret, err
}
return ret, nil
}

View File

@ -58,6 +58,48 @@ func (_m *TagReaderWriter) Count(ctx context.Context) (int, error) {
return r0, r1
}
// CountByChildTagID provides a mock function with given fields: ctx, childID
func (_m *TagReaderWriter) CountByChildTagID(ctx context.Context, childID int) (int, error) {
ret := _m.Called(ctx, childID)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, childID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, childID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountByParentTagID provides a mock function with given fields: ctx, parentID
func (_m *TagReaderWriter) CountByParentTagID(ctx context.Context, parentID int) (int, error) {
ret := _m.Called(ctx, parentID)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, parentID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, parentID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: ctx, newTag
func (_m *TagReaderWriter) Create(ctx context.Context, newTag *models.Tag) error {
ret := _m.Called(ctx, newTag)

View File

@ -42,6 +42,8 @@ type TagAutoTagQueryer interface {
// TagCounter provides methods to count tags.
type TagCounter interface {
Count(ctx context.Context) (int, error)
CountByParentTagID(ctx context.Context, parentID int) (int, error)
CountByChildTagID(ctx context.Context, childID int) (int, error)
}
// TagCreator provides methods to create tags.

View File

@ -396,6 +396,20 @@ func (qb *TagStore) FindByChildTagID(ctx context.Context, parentID int) ([]*mode
return qb.queryTags(ctx, query, args)
}
func (qb *TagStore) CountByParentTagID(ctx context.Context, parentID int) (int, error) {
q := dialect.Select(goqu.COUNT("*")).From(goqu.T("tags")).
InnerJoin(goqu.T("tags_relations"), goqu.On(goqu.I("tags_relations.parent_id").Eq(goqu.I("tags.id")))).
Where(goqu.I("tags_relations.child_id").Eq(goqu.V(parentID))) // Pass the parentID here
return count(ctx, q)
}
func (qb *TagStore) CountByChildTagID(ctx context.Context, childID int) (int, error) {
q := dialect.Select(goqu.COUNT("*")).From(goqu.T("tags")).
InnerJoin(goqu.T("tags_relations"), goqu.On(goqu.I("tags_relations.child_id").Eq(goqu.I("tags.id")))).
Where(goqu.I("tags_relations.parent_id").Eq(goqu.V(childID))) // Pass the childID here
return count(ctx, q)
}
func (qb *TagStore) Count(ctx context.Context) (int, error) {
q := dialect.Select(goqu.COUNT("*")).From(qb.table())
return count(ctx, q)

View File

@ -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 { TagLink } 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<IProps> = (props) => {
if (props.gallery.scenes.length === 0) return;
const popoverContent = props.gallery.scenes.map((scene) => (
<TagLink key={scene.id} scene={scene} />
<SceneLink key={scene.id} scene={scene} />
));
return (
@ -52,7 +52,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
if (props.gallery.tags.length <= 0) return;
const popoverContent = props.gallery.tags.map((tag) => (
<TagLink key={tag.id} tag={tag} tagType="gallery" />
<TagLink key={tag.id} tag={tag} linkType="gallery" />
));
return (

View File

@ -34,7 +34,7 @@ export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = ({
function renderTags() {
if (gallery.tags.length === 0) return;
const tags = gallery.tags.map((tag) => (
<TagLink key={tag.id} tag={tag} tagType="gallery" />
<TagLink key={tag.id} tag={tag} linkType="gallery" />
));
return (
<>

View File

@ -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 { TagLink } 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<IImageCardProps> = (
if (props.image.tags.length <= 0) return;
const popoverContent = props.image.tags.map((tag) => (
<TagLink key={tag.id} tag={tag} tagType="image" />
<TagLink key={tag.id} tag={tag} linkType="image" />
));
return (
@ -83,7 +83,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
if (props.image.galleries.length <= 0) return;
const popoverContent = props.image.galleries.map((gallery) => (
<TagLink key={gallery.id} gallery={gallery} />
<GalleryLink key={gallery.id} gallery={gallery} />
));
return (

View File

@ -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 { TagLink } 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<IImageDetailProps> = (props) => {
function renderTags() {
if (props.image.tags.length === 0) return;
const tags = props.image.tags.map((tag) => (
<TagLink key={tag.id} tag={tag} tagType="image" />
<TagLink key={tag.id} tag={tag} linkType="image" />
));
return (
<>
@ -67,8 +67,8 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
function renderGalleries() {
if (props.image.galleries.length === 0) return;
const tags = props.image.galleries.map((gallery) => (
<TagLink key={gallery.id} gallery={gallery} />
const galleries = props.image.galleries.map((gallery) => (
<GalleryLink key={gallery.id} gallery={gallery} />
));
return (
<>
@ -78,7 +78,7 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
values={{ count: props.image.galleries.length }}
/>
</h6>
{tags}
{galleries}
</>
);
}

View File

@ -4,7 +4,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 { TagLink } from "../Shared/TagLink";
import { SceneLink } from "../Shared/TagLink";
import { TruncatedText } from "../Shared/TruncatedText";
import { FormattedMessage } from "react-intl";
import { RatingBanner } from "../Shared/RatingBanner";
@ -36,7 +36,7 @@ export const MovieCard: React.FC<IProps> = (props: IProps) => {
if (props.movie.scenes.length === 0) return;
const popoverContent = props.movie.scenes.map((scene) => (
<TagLink key={scene.id} scene={scene} />
<SceneLink key={scene.id} scene={scene} />
));
return (

View File

@ -168,7 +168,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
if (performer.tags.length <= 0) return;
const popoverContent = performer.tags.map((tag) => (
<TagLink key={tag.id} tagType="performer" tag={tag} />
<TagLink key={tag.id} linkType="performer" tag={tag} />
));
return (

View File

@ -29,7 +29,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
return (
<ul className="pl-0">
{(performer.tags ?? []).map((tag) => (
<TagLink key={tag.id} tagType="performer" tag={tag} />
<TagLink key={tag.id} linkType="performer" tag={tag} />
))}
</ul>
);

View File

@ -19,7 +19,12 @@ import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { ErrorMessage } from "../Shared/ErrorMessage";
import { HoverPopover } from "../Shared/HoverPopover";
import { Icon } from "../Shared/Icon";
import { TagLink } 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";
@ -349,7 +354,7 @@ export const SceneDuplicateChecker: React.FC = () => {
src={sceneMovie.movie.front_image_path ?? ""}
/>
</Link>
<TagLink
<MovieLink
key={sceneMovie.movie.id}
movie={sceneMovie.movie}
className="d-block"
@ -377,8 +382,8 @@ export const SceneDuplicateChecker: React.FC = () => {
if (scene.scene_markers.length <= 0) return;
const popoverContent = scene.scene_markers.map((marker) => {
const markerPopover = { ...marker, scene: { id: scene.id } };
return <TagLink key={marker.id} marker={markerPopover} />;
const markerWithScene = { ...marker, scene: { id: scene.id } };
return <SceneMarkerLink key={marker.id} marker={markerWithScene} />;
});
return (
@ -410,7 +415,7 @@ export const SceneDuplicateChecker: React.FC = () => {
if (scene.galleries.length <= 0) return;
const popoverContent = scene.galleries.map((gallery) => (
<TagLink key={gallery.id} gallery={gallery} />
<GalleryLink key={gallery.id} gallery={gallery} />
));
return (

View File

@ -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 { TagLink } 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";
@ -219,7 +224,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
src={sceneMovie.movie.front_image_path ?? ""}
/>
</Link>
<TagLink
<MovieLink
key={sceneMovie.movie.id}
movie={sceneMovie.movie}
className="d-block"
@ -245,8 +250,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (
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 <TagLink key={marker.id} marker={markerPopover} />;
const markerWithScene = { ...marker, scene: { id: props.scene.id } };
return <SceneMarkerLink key={marker.id} marker={markerWithScene} />;
});
return (
@ -282,7 +287,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
if (props.scene.galleries.length <= 0) return;
const popoverContent = props.scene.galleries.map((gallery) => (
<TagLink key={gallery.id} gallery={gallery} />
<GalleryLink key={gallery.id} gallery={gallery} />
));
return (

View File

@ -18,18 +18,19 @@ export const PrimaryTags: React.FC<IPrimaryTags> = ({
}) => {
if (!sceneMarkers?.length) return <div />;
const primaries: Record<string, GQL.SlimTagDataFragment> = {};
const primaryTags: Record<string, GQL.SceneMarkerDataFragment[]> = {};
const primaryTagNames: Record<string, string> = {};
const markersByTag: Record<string, GQL.SceneMarkerDataFragment[]> = {};
sceneMarkers.forEach((m) => {
if (primaryTags[m.primary_tag.id]) primaryTags[m.primary_tag.id].push(m);
else {
primaryTags[m.primary_tag.id] = [m];
primaries[m.primary_tag.id] = m.primary_tag;
if (primaryTagNames[m.primary_tag.id]) {
markersByTag[m.primary_tag.id].push(m);
} else {
primaryTagNames[m.primary_tag.id] = m.primary_tag.name;
markersByTag[m.primary_tag.id] = [m];
}
});
const primaryCards = Object.keys(primaryTags).map((id) => {
const markers = primaryTags[id].map((marker) => {
const primaryCards = Object.keys(markersByTag).map((id) => {
const markers = markersByTag[id].map((marker) => {
const tags = marker.tags.map((tag) => (
<Badge key={tag.id} variant="secondary" className="tag-item">
{tag.name}
@ -59,7 +60,7 @@ export const PrimaryTags: React.FC<IPrimaryTags> = ({
return (
<Card className="primary-card col-12 col-sm-6 col-xl-6" key={id}>
<h3>{primaries[id].name}</h3>
<h3>{primaryTagNames[id]}</h3>
<Card.Body className="primary-card-body">{markers}</Card.Body>
</Card>
);

View File

@ -6,7 +6,7 @@ import * as GQL from "src/core/generated-graphql";
import { sortPerformers } from "src/core/performers";
import { HoverPopover } from "./HoverPopover";
import { Icon } from "./Icon";
import { TagLink } from "./TagLink";
import { PerformerLink } from "./TagLink";
interface IProps {
performers: Partial<GQL.PerformerDataFragment>[];
@ -26,7 +26,11 @@ export const PerformerPopoverButton: React.FC<IProps> = ({ performers }) => {
src={performer.image_path ?? ""}
/>
</Link>
<TagLink key={performer.id} performer={performer} className="d-block" />
<PerformerLink
key={performer.id}
performer={performer}
className="d-block"
/>
</div>
));

View File

@ -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 (
<TagPopover id={id} placement={props.hoverPlacement}>
<TagPopover id={id} hide={hide} placement={props.hoverPlacement}>
<reactSelectComponents.Option {...thisOptionProps} />
</TagPopover>
);

View File

@ -1,97 +1,252 @@
import { Badge } from "react-bootstrap";
import React from "react";
import { Badge, OverlayTrigger, Tooltip } from "react-bootstrap";
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";
import { markerTitle } from "src/core/markers";
import { Placement } from "react-bootstrap/esm/Overlay";
interface IFile {
path: string;
}
interface IGallery {
id: string;
files: IFile[];
folder?: GQL.Maybe<IFile>;
title: GQL.Maybe<string>;
}
import { faFolderTree } from "@fortawesome/free-solid-svg-icons";
import { Icon } from "../Shared/Icon";
import { FormattedMessage } from "react-intl";
type SceneMarkerFragment = Pick<GQL.SceneMarker, "id" | "title" | "seconds"> & {
scene: Pick<GQL.Scene, "id">;
primary_tag: Pick<GQL.Tag, "id" | "name">;
};
interface IProps {
tag?: Partial<TagDataFragment>;
tagType?: "performer" | "scene" | "gallery" | "image" | "details";
performer?: Partial<PerformerDataFragment>;
marker?: SceneMarkerFragment;
movie?: Partial<MovieDataFragment>;
scene?: Partial<Pick<SceneDataFragment, "id" | "title" | "files">>;
gallery?: Partial<IGallery>;
interface ICommonLinkProps {
link: string;
className?: string;
hoverPlacement?: Placement;
}
export const TagLink: React.FC<IProps> = (props: IProps) => {
let id: string = "";
let link: string = "#";
let title: string = "";
if (props.tag) {
id = props.tag.id || "";
switch (props.tagType) {
case "scene":
case undefined:
link = NavUtils.makeTagScenesUrl(props.tag);
break;
case "performer":
link = NavUtils.makeTagPerformersUrl(props.tag);
break;
case "gallery":
link = NavUtils.makeTagGalleriesUrl(props.tag);
break;
case "image":
link = NavUtils.makeTagImagesUrl(props.tag);
break;
case "details":
link = NavUtils.makeTagUrl(id);
break;
}
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);
}
const CommonLinkComponent: React.FC<ICommonLinkProps> = ({
link,
className,
children,
}) => {
return (
<Badge className={cx("tag-item", props.className)} variant="secondary">
<TagPopover id={id} placement={props.hoverPlacement}>
<Link to={link}>{title}</Link>
</TagPopover>
<Badge className={cx("tag-item", className)} variant="secondary">
<Link to={link}>{children}</Link>
</Badge>
);
};
interface IPerformerLinkProps {
performer: INamedObject;
linkType?: "scene" | "gallery" | "image";
className?: string;
}
export const PerformerLink: React.FC<IPerformerLinkProps> = ({
performer,
linkType = "scene",
className,
}) => {
const link = useMemo(() => {
switch (linkType) {
case "gallery":
return NavUtils.makePerformerGalleriesUrl(performer);
case "image":
return NavUtils.makePerformerImagesUrl(performer);
case "scene":
default:
return NavUtils.makePerformerScenesUrl(performer);
}
}, [performer, linkType]);
const title = performer.name || "";
return (
<CommonLinkComponent link={link} className={className}>
{title}
</CommonLinkComponent>
);
};
interface IMovieLinkProps {
movie: INamedObject;
linkType?: "scene";
className?: string;
}
export const MovieLink: React.FC<IMovieLinkProps> = ({
movie,
linkType = "scene",
className,
}) => {
const link = useMemo(() => {
switch (linkType) {
case "scene":
return NavUtils.makeMovieScenesUrl(movie);
}
}, [movie, linkType]);
const title = movie.name || "";
return (
<CommonLinkComponent link={link} className={className}>
{title}
</CommonLinkComponent>
);
};
interface ISceneMarkerLinkProps {
marker: SceneMarkerFragment;
linkType?: "scene";
className?: string;
}
export const SceneMarkerLink: React.FC<ISceneMarkerLinkProps> = ({
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 (
<CommonLinkComponent link={link} className={className}>
{title}
</CommonLinkComponent>
);
};
interface IObjectWithIDTitleFiles extends IObjectWithTitleFiles {
id: string;
}
interface ISceneLinkProps {
scene: IObjectWithIDTitleFiles;
linkType?: "details";
className?: string;
}
export const SceneLink: React.FC<ISceneLinkProps> = ({
scene,
linkType = "details",
className,
}) => {
const link = useMemo(() => {
switch (linkType) {
case "details":
return `/scenes/${scene.id}`;
}
}, [scene, linkType]);
const title = objectTitle(scene);
return (
<CommonLinkComponent link={link} className={className}>
{title}
</CommonLinkComponent>
);
};
interface IGallery extends IObjectWithIDTitleFiles {
folder?: GQL.Maybe<IFile>;
}
interface IGalleryLinkProps {
gallery: IGallery;
linkType?: "details";
className?: string;
}
export const GalleryLink: React.FC<IGalleryLinkProps> = ({
gallery,
linkType = "details",
className,
}) => {
const link = useMemo(() => {
switch (linkType) {
case "details":
return `/galleries/${gallery.id}`;
}
}, [gallery, linkType]);
const title = galleryTitle(gallery);
return (
<CommonLinkComponent link={link} className={className}>
{title}
</CommonLinkComponent>
);
};
interface ITagLinkProps {
tag: INamedObject;
linkType?: "scene" | "gallery" | "image" | "details" | "performer";
className?: string;
hoverPlacement?: Placement;
showHierarchyIcon?: boolean;
hierarchyTooltipID?: string;
}
export const TagLink: React.FC<ITagLinkProps> = ({
tag,
linkType = "scene",
className,
hoverPlacement,
showHierarchyIcon = false,
hierarchyTooltipID,
}) => {
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 || "";
const tooltip = useMemo(() => {
if (!hierarchyTooltipID) {
return <></>;
}
return (
<Tooltip id="tag-hierarchy-tooltip">
<FormattedMessage id={hierarchyTooltipID} />
</Tooltip>
);
}, [hierarchyTooltipID]);
return (
<CommonLinkComponent link={link} className={className}>
<TagPopover id={tag.id ?? ""} placement={hoverPlacement}>
<Link to={link}>
{title}
{showHierarchyIcon && (
<OverlayTrigger placement="top" overlay={tooltip}>
<span className="icon-wrapper">
<span className="vertical-line">|</span>
<Icon icon={faFolderTree} className="tag-icon" />
</span>
</OverlayTrigger>
)}
</Link>
</TagPopover>
</CommonLinkComponent>
);
};

View File

@ -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 { TagLink } 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";
@ -54,7 +54,7 @@ const TaggerSceneDetails: React.FC<ITaggerSceneDetails> = ({ scene }) => {
src={performer.image_path ?? ""}
/>
</Link>
<TagLink
<PerformerLink
key={performer.id}
performer={performer}
className="d-block"

View File

@ -21,7 +21,9 @@ export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag, fullWidth }) => {
key={p.id}
tag={p}
hoverPlacement="bottom"
tagType="details"
linkType="details"
showHierarchyIcon={p.parent_count !== 0}
hierarchyTooltipID="tag_parent_tooltip"
/>
))}
</>
@ -40,7 +42,9 @@ export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag, fullWidth }) => {
key={c.id}
tag={c}
hoverPlacement="bottom"
tagType="details"
linkType="details"
showHierarchyIcon={c.child_count !== 0}
hierarchyTooltipID="tag_sub_tag_tooltip"
/>
))}
</>

View File

@ -8,13 +8,12 @@ import { ConfigurationContext } from "../../hooks/Config";
import { IUIConfig } from "src/core/config";
import { Placement } from "react-bootstrap/esm/Overlay";
interface ITagPopoverProps {
id?: string;
placement?: Placement;
interface ITagPopoverCardProps {
id: string;
}
export const TagPopoverCard: React.FC<ITagPopoverCardProps> = ({ 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<ITagPopoverCardProps> = ({ id }) => {
);
};
interface ITagPopoverProps {
id: string;
hide?: boolean;
placement?: Placement;
}
export const TagPopover: React.FC<ITagPopoverProps> = ({
id,
hide,
children,
placement = "top",
}) => {
@ -45,7 +51,7 @@ export const TagPopover: React.FC<ITagPopoverProps> = ({
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<ITagPopoverProps> = ({
</HoverPopover>
);
};
interface ITagPopoverCardProps {
id?: string;
}

View File

@ -72,3 +72,21 @@
padding: 0;
}
}
.tag-item {
.icon-wrapper {
color: #202b33;
opacity: 0.5;
padding-left: 6px;
}
}
.tag-item {
.tag-icon {
color: #202b33;
margin: 0;
opacity: 0.5;
padding-left: 3px;
transform: scale(0.7);
}
}

View File

@ -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<string>;
export interface IObjectWithTitleFiles extends IObjectWithFiles {
title?: GQL.Maybe<string>;
}
export function objectTitle(s: Partial<IObjectWithTitleFiles>) {

View File

@ -1319,6 +1319,8 @@
"synopsis": "Synopsis",
"tag": "Tag",
"tag_count": "Tag Count",
"tag_parent_tooltip": "Has parent tags",
"tag_sub_tag_tooltip": "Has sub-tags",
"tags": "Tags",
"tattoos": "Tattoos",
"title": "Title",

View File

@ -73,8 +73,13 @@ const makePerformerImagesUrl = (
return `/images?${filter.makeQueryParameters()}`;
};
export interface INamedObject {
id?: string;
name?: string;
}
const makePerformerGalleriesUrl = (
performer: Partial<GQL.PerformerDataFragment>,
performer: INamedObject,
extraPerformer?: ILabeledId,
extraCriteria?: Criterion<CriterionValue>[]
) => {