mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Optimize Tag List Page Performance (#6398)
This commit is contained in:
@@ -57,3 +57,31 @@ fragment SelectTagData on Tag {
|
|||||||
updated_at
|
updated_at
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Optimized fragment for tag list page - excludes expensive recursive *_count_all fields
|
||||||
|
fragment TagListData on Tag {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
sort_name
|
||||||
|
description
|
||||||
|
aliases
|
||||||
|
ignore_auto_tag
|
||||||
|
favorite
|
||||||
|
image_path
|
||||||
|
# Direct counts only - no recursive depth queries
|
||||||
|
scene_count
|
||||||
|
scene_marker_count
|
||||||
|
image_count
|
||||||
|
gallery_count
|
||||||
|
performer_count
|
||||||
|
studio_count
|
||||||
|
group_count
|
||||||
|
|
||||||
|
parents {
|
||||||
|
...SlimTagData
|
||||||
|
}
|
||||||
|
|
||||||
|
children {
|
||||||
|
...SlimTagData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,3 +25,13 @@ query FindTagsForSelect(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Optimized query for tag list page - uses TagListData fragment without recursive counts
|
||||||
|
query FindTagsForList($filter: FindFilterType, $tag_filter: TagFilterType) {
|
||||||
|
findTags(filter: $filter, tag_filter: $tag_filter) {
|
||||||
|
count
|
||||||
|
tags {
|
||||||
|
...TagListData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function Tags(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
selected: GQL.TagDataFragment[];
|
selected: (GQL.TagDataFragment | GQL.TagListDataFragment)[];
|
||||||
onClose: (applied: boolean) => void;
|
onClose: (applied: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ export const EditTagsDialog: React.FC<IListOperationProps> = (
|
|||||||
let updateChildTagIds: string[] = [];
|
let updateChildTagIds: string[] = [];
|
||||||
let first = true;
|
let first = true;
|
||||||
|
|
||||||
state.forEach((tag: GQL.TagDataFragment) => {
|
state.forEach((tag: GQL.TagDataFragment | GQL.TagListDataFragment) => {
|
||||||
getAggregateStateObject(updateState, tag, tagFields, first);
|
getAggregateStateObject(updateState, tag, tagFields, first);
|
||||||
|
|
||||||
const thisParents = (tag.parents ?? []).map((t) => t.id).sort();
|
const thisParents = (tag.parents ?? []).map((t) => t.id).sort();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import cx from "classnames";
|
|||||||
import { useTagUpdate } from "src/core/StashService";
|
import { useTagUpdate } from "src/core/StashService";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tag: GQL.TagDataFragment;
|
tag: GQL.TagDataFragment | GQL.TagListDataFragment;
|
||||||
cardWidth?: number;
|
cardWidth?: number;
|
||||||
zoomIndex: number;
|
zoomIndex: number;
|
||||||
selecting?: boolean;
|
selecting?: boolean;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import { TagCard } from "./TagCard";
|
import { TagCard } from "./TagCard";
|
||||||
|
|
||||||
interface ITagCardGrid {
|
interface ITagCardGrid {
|
||||||
tags: GQL.TagDataFragment[];
|
tags: (GQL.TagDataFragment | GQL.TagListDataFragment)[];
|
||||||
selectedIds: Set<string>;
|
selectedIds: Set<string>;
|
||||||
zoomIndex: number;
|
zoomIndex: number;
|
||||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { Button } from "react-bootstrap";
|
|||||||
import { Link, useHistory } from "react-router-dom";
|
import { Link, useHistory } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
queryFindTags,
|
queryFindTagsForList,
|
||||||
mutateMetadataAutoTag,
|
mutateMetadataAutoTag,
|
||||||
useFindTags,
|
useFindTagsForList,
|
||||||
useTagDestroy,
|
useTagDestroy,
|
||||||
useTagsDestroy,
|
useTagsDestroy,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
@@ -27,11 +27,11 @@ import { TagCardGrid } from "./TagCardGrid";
|
|||||||
import { EditTagsDialog } from "./EditTagsDialog";
|
import { EditTagsDialog } from "./EditTagsDialog";
|
||||||
import { View } from "../List/views";
|
import { View } from "../List/views";
|
||||||
|
|
||||||
function getItems(result: GQL.FindTagsQueryResult) {
|
function getItems(result: GQL.FindTagsForListQueryResult) {
|
||||||
return result?.data?.findTags?.tags ?? [];
|
return result?.data?.findTags?.tags ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCount(result: GQL.FindTagsQueryResult) {
|
function getCount(result: GQL.FindTagsForListQueryResult) {
|
||||||
return result?.data?.findTags?.count ?? 0;
|
return result?.data?.findTags?.count ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ interface ITagList {
|
|||||||
export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
|
export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [deletingTag, setDeletingTag] =
|
const [deletingTag, setDeletingTag] =
|
||||||
useState<Partial<GQL.TagDataFragment> | null>(null);
|
useState<Partial<GQL.TagListDataFragment> | null>(null);
|
||||||
|
|
||||||
const filterMode = GQL.FilterMode.Tags;
|
const filterMode = GQL.FilterMode.Tags;
|
||||||
const view = View.Tags;
|
const view = View.Tags;
|
||||||
@@ -79,7 +79,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
function addKeybinds(
|
function addKeybinds(
|
||||||
result: GQL.FindTagsQueryResult,
|
result: GQL.FindTagsForListQueryResult,
|
||||||
filter: ListFilterModel
|
filter: ListFilterModel
|
||||||
) {
|
) {
|
||||||
Mousetrap.bind("p r", () => {
|
Mousetrap.bind("p r", () => {
|
||||||
@@ -92,7 +92,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function viewRandom(
|
async function viewRandom(
|
||||||
result: GQL.FindTagsQueryResult,
|
result: GQL.FindTagsForListQueryResult,
|
||||||
filter: ListFilterModel
|
filter: ListFilterModel
|
||||||
) {
|
) {
|
||||||
// query for a random tag
|
// query for a random tag
|
||||||
@@ -103,7 +103,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
|
|||||||
const filterCopy = cloneDeep(filter);
|
const filterCopy = cloneDeep(filter);
|
||||||
filterCopy.itemsPerPage = 1;
|
filterCopy.itemsPerPage = 1;
|
||||||
filterCopy.currentPage = index + 1;
|
filterCopy.currentPage = index + 1;
|
||||||
const singleResult = await queryFindTags(filterCopy);
|
const singleResult = await queryFindTagsForList(filterCopy);
|
||||||
if (singleResult.data.findTags.tags.length === 1) {
|
if (singleResult.data.findTags.tags.length === 1) {
|
||||||
const { id } = singleResult.data.findTags.tags[0];
|
const { id } = singleResult.data.findTags.tags[0];
|
||||||
// navigate to the tag page
|
// navigate to the tag page
|
||||||
@@ -122,7 +122,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
|
|||||||
setIsExportDialogOpen(true);
|
setIsExportDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAutoTag(tag: GQL.TagDataFragment) {
|
async function onAutoTag(tag: GQL.TagListDataFragment) {
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
try {
|
try {
|
||||||
await mutateMetadataAutoTag({ tags: [tag.id] });
|
await mutateMetadataAutoTag({ tags: [tag.id] });
|
||||||
@@ -139,7 +139,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
|
|||||||
children: deletingTag?.children ?? [],
|
children: deletingTag?.children ?? [],
|
||||||
};
|
};
|
||||||
await deleteTag();
|
await deleteTag();
|
||||||
tagRelationHook(deletingTag as GQL.TagDataFragment, oldRelations, {
|
tagRelationHook(deletingTag as GQL.TagListDataFragment, oldRelations, {
|
||||||
parents: [],
|
parents: [],
|
||||||
children: [],
|
children: [],
|
||||||
});
|
});
|
||||||
@@ -160,7 +160,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderContent(
|
function renderContent(
|
||||||
result: GQL.FindTagsQueryResult,
|
result: GQL.FindTagsForListQueryResult,
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
selectedIds: Set<string>,
|
selectedIds: Set<string>,
|
||||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
|
||||||
@@ -324,14 +324,14 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderEditDialog(
|
function renderEditDialog(
|
||||||
selectedTags: GQL.TagDataFragment[],
|
selectedTags: GQL.TagListDataFragment[],
|
||||||
onClose: (confirmed: boolean) => void
|
onClose: (confirmed: boolean) => void
|
||||||
) {
|
) {
|
||||||
return <EditTagsDialog selected={selectedTags} onClose={onClose} />;
|
return <EditTagsDialog selected={selectedTags} onClose={onClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDeleteDialog(
|
function renderDeleteDialog(
|
||||||
selectedTags: GQL.TagDataFragment[],
|
selectedTags: GQL.TagListDataFragment[],
|
||||||
onClose: (confirmed: boolean) => void
|
onClose: (confirmed: boolean) => void
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@@ -357,7 +357,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
|
|||||||
return (
|
return (
|
||||||
<ItemListContext
|
<ItemListContext
|
||||||
filterMode={filterMode}
|
filterMode={filterMode}
|
||||||
useResult={useFindTags}
|
useResult={useFindTagsForList}
|
||||||
getItems={getItems}
|
getItems={getItems}
|
||||||
getCount={getCount}
|
getCount={getCount}
|
||||||
alterQuery={alterQuery}
|
alterQuery={alterQuery}
|
||||||
|
|||||||
@@ -429,6 +429,16 @@ export const useFindTags = (filter?: ListFilterModel) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Optimized query for tag list page - excludes expensive recursive *_count_all fields
|
||||||
|
export const useFindTagsForList = (filter?: ListFilterModel) =>
|
||||||
|
GQL.useFindTagsForListQuery({
|
||||||
|
skip: filter === undefined,
|
||||||
|
variables: {
|
||||||
|
filter: filter?.makeFindFilter(),
|
||||||
|
tag_filter: filter?.makeFilter(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const queryFindTags = (filter: ListFilterModel) =>
|
export const queryFindTags = (filter: ListFilterModel) =>
|
||||||
client.query<GQL.FindTagsQuery>({
|
client.query<GQL.FindTagsQuery>({
|
||||||
query: GQL.FindTagsDocument,
|
query: GQL.FindTagsDocument,
|
||||||
@@ -438,6 +448,16 @@ export const queryFindTags = (filter: ListFilterModel) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Optimized query for tag list page
|
||||||
|
export const queryFindTagsForList = (filter: ListFilterModel) =>
|
||||||
|
client.query<GQL.FindTagsForListQuery>({
|
||||||
|
query: GQL.FindTagsForListDocument,
|
||||||
|
variables: {
|
||||||
|
filter: filter.makeFindFilter(),
|
||||||
|
tag_filter: filter.makeFilter(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const queryFindTagsByIDForSelect = (tagIDs: string[]) =>
|
export const queryFindTagsByIDForSelect = (tagIDs: string[]) =>
|
||||||
client.query<GQL.FindTagsForSelectQuery>({
|
client.query<GQL.FindTagsForSelectQuery>({
|
||||||
query: GQL.FindTagsForSelectDocument,
|
query: GQL.FindTagsForSelectDocument,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ interface ITagRelationTuple {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const tagRelationHook = (
|
export const tagRelationHook = (
|
||||||
tag: GQL.SlimTagDataFragment | GQL.TagDataFragment,
|
tag: GQL.SlimTagDataFragment | GQL.TagDataFragment | GQL.TagListDataFragment,
|
||||||
old: ITagRelationTuple,
|
old: ITagRelationTuple,
|
||||||
updated: ITagRelationTuple
|
updated: ITagRelationTuple
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user