mirror of
https://github.com/stashapp/stash.git
synced 2026-03-31 10:32:36 +02:00
Replace tag list view with tag list table (#6703)
* Replace tag list view with tag list table Uses same styling as performer list table * Remove "count" suffix from count headers in performer list
This commit is contained in:
parent
b76dd089f5
commit
208c19a81d
3 changed files with 244 additions and 115 deletions
|
|
@ -334,19 +334,19 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
|
|||
},
|
||||
{
|
||||
value: "scene_count",
|
||||
label: intl.formatMessage({ id: "scene_count" }),
|
||||
label: intl.formatMessage({ id: "scenes" }),
|
||||
defaultShow: true,
|
||||
render: SceneCountCell,
|
||||
},
|
||||
{
|
||||
value: "gallery_count",
|
||||
label: intl.formatMessage({ id: "gallery_count" }),
|
||||
label: intl.formatMessage({ id: "galleries" }),
|
||||
defaultShow: true,
|
||||
render: GalleryCountCell,
|
||||
},
|
||||
{
|
||||
value: "image_count",
|
||||
label: intl.formatMessage({ id: "image_count" }),
|
||||
label: intl.formatMessage({ id: "images" }),
|
||||
defaultShow: true,
|
||||
render: ImageCountCell,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,22 +5,17 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
|||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { useFilteredItemList } from "../List/ItemList";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
queryFindTagsForList,
|
||||
mutateMetadataAutoTag,
|
||||
useFindTagsForList,
|
||||
useTagsDestroy,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
import NavUtils from "src/utils/navigation";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
|
||||
import { ExportDialog } from "../Shared/ExportDialog";
|
||||
import { tagRelationHook } from "../../core/tags";
|
||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { TagMergeModal } from "./TagMergeDialog";
|
||||
import { TagCardGrid } from "./TagCardGrid";
|
||||
import { EditTagsDialog } from "./EditTagsDialog";
|
||||
|
|
@ -51,17 +46,16 @@ import { Pagination, PaginationIndex } from "../List/Pagination";
|
|||
import { LoadedContent } from "../List/PagedList";
|
||||
import { SidebarBooleanFilter } from "../List/Filters/BooleanFilter";
|
||||
import { FavoriteTagCriterionOption } from "src/models/list-filter/criteria/favorite";
|
||||
import { TagListTable } from "./TagListTable";
|
||||
|
||||
const TagList: React.FC<{
|
||||
tags: GQL.TagListDataFragment[];
|
||||
filter: ListFilterModel;
|
||||
selectedIds: Set<string>;
|
||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||
onDelete: (tag: GQL.TagListDataFragment) => void;
|
||||
onAutoTag: (tag: GQL.TagListDataFragment) => void;
|
||||
}> = PatchComponent(
|
||||
"TagList",
|
||||
({ tags, filter, selectedIds, onSelectChange, onDelete, onAutoTag }) => {
|
||||
({ tags, filter, selectedIds, onSelectChange }) => {
|
||||
if (tags.length === 0 && filter.displayMode !== DisplayMode.Tagger) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -77,95 +71,13 @@ const TagList: React.FC<{
|
|||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.List) {
|
||||
const tagElements = tags.map((tag) => {
|
||||
return (
|
||||
<div key={tag.id} className="tag-list-row row">
|
||||
<Link to={`/tags/${tag.id}`}>{tag.name}</Link>
|
||||
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="tag-list-button"
|
||||
onClick={() => onAutoTag(tag)}
|
||||
>
|
||||
<FormattedMessage id="actions.auto_tag" />
|
||||
</Button>
|
||||
<Button variant="secondary" className="tag-list-button">
|
||||
<Link
|
||||
to={NavUtils.makeTagScenesUrl(tag)}
|
||||
className="tag-list-anchor"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="countables.scenes"
|
||||
values={{
|
||||
count: tag.scene_count ?? 0,
|
||||
}}
|
||||
/>
|
||||
: <FormattedNumber value={tag.scene_count ?? 0} />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="secondary" className="tag-list-button">
|
||||
<Link
|
||||
to={NavUtils.makeTagImagesUrl(tag)}
|
||||
className="tag-list-anchor"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="countables.images"
|
||||
values={{
|
||||
count: tag.image_count ?? 0,
|
||||
}}
|
||||
/>
|
||||
: <FormattedNumber value={tag.image_count ?? 0} />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="secondary" className="tag-list-button">
|
||||
<Link
|
||||
to={NavUtils.makeTagGalleriesUrl(tag)}
|
||||
className="tag-list-anchor"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="countables.galleries"
|
||||
values={{
|
||||
count: tag.gallery_count ?? 0,
|
||||
}}
|
||||
/>
|
||||
: <FormattedNumber value={tag.gallery_count ?? 0} />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="secondary" className="tag-list-button">
|
||||
<Link
|
||||
to={NavUtils.makeTagSceneMarkersUrl(tag)}
|
||||
className="tag-list-anchor"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="countables.markers"
|
||||
values={{
|
||||
count: tag.scene_marker_count ?? 0,
|
||||
}}
|
||||
/>
|
||||
: <FormattedNumber value={tag.scene_marker_count ?? 0} />
|
||||
</Link>
|
||||
</Button>
|
||||
<span className="tag-list-count">
|
||||
<FormattedMessage id="total" />:{" "}
|
||||
<FormattedNumber
|
||||
value={
|
||||
(tag.scene_count || 0) +
|
||||
(tag.scene_marker_count || 0) +
|
||||
(tag.image_count || 0) +
|
||||
(tag.gallery_count || 0)
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
<Button variant="danger" onClick={() => onDelete(tag)}>
|
||||
<Icon icon={faTrashAlt} color="danger" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return <div className="col col-sm-8 m-auto">{tagElements}</div>;
|
||||
return (
|
||||
<TagListTable
|
||||
tags={tags}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={onSelectChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Tagger) {
|
||||
return <TagTagger tags={tags} />;
|
||||
|
|
@ -287,7 +199,6 @@ export const FilteredTagList = PatchComponent(
|
|||
(props: ITagList) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
|
||||
const searchFocus = useFocus();
|
||||
|
||||
|
|
@ -433,16 +344,6 @@ export const FilteredTagList = PatchComponent(
|
|||
);
|
||||
}
|
||||
|
||||
async function onAutoTag(tag: GQL.TagListDataFragment) {
|
||||
if (!tag) return;
|
||||
try {
|
||||
await mutateMetadataAutoTag({ tags: [tag.id] });
|
||||
Toast.success(intl.formatMessage({ id: "toast.started_auto_tagging" }));
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const convertedExtraOperations = extraOperations.map((op) => ({
|
||||
text: op.text,
|
||||
onClick: () => op.onClick(result, filter, selectedIds),
|
||||
|
|
@ -566,8 +467,6 @@ export const FilteredTagList = PatchComponent(
|
|||
tags={items}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={onSelectChange}
|
||||
onDelete={(tag) => onDelete(tag)}
|
||||
onAutoTag={(tag) => onAutoTag(tag)}
|
||||
/>
|
||||
</LoadedContent>
|
||||
|
||||
|
|
|
|||
230
ui/v2.5/src/components/Tags/TagListTable.tsx
Normal file
230
ui/v2.5/src/components/Tags/TagListTable.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import NavUtils from "src/utils/navigation";
|
||||
import { faHeart } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useTagUpdate } from "src/core/StashService";
|
||||
import { useTableColumns } from "src/hooks/useTableColumns";
|
||||
import cx from "classnames";
|
||||
import { IColumn, ListTable } from "../List/ListTable";
|
||||
|
||||
interface ITagListTableProps {
|
||||
tags: GQL.TagListDataFragment[];
|
||||
selectedIds: Set<string>;
|
||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||
}
|
||||
|
||||
const TABLE_NAME = "tags";
|
||||
|
||||
export const TagListTable: React.FC<ITagListTableProps> = (
|
||||
props: ITagListTableProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [updateTag] = useTagUpdate();
|
||||
|
||||
function setFavorite(v: boolean, tagId: string) {
|
||||
if (tagId) {
|
||||
updateTag({
|
||||
variables: {
|
||||
input: {
|
||||
id: tagId,
|
||||
favorite: v,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ImageCell = (tag: GQL.TagListDataFragment) => (
|
||||
<Link to={`/tags/${tag.id}`}>
|
||||
<img
|
||||
loading="lazy"
|
||||
className="image-thumbnail"
|
||||
alt={tag.name ?? ""}
|
||||
src={tag.image_path ?? ""}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const NameCell = (tag: GQL.TagListDataFragment) => (
|
||||
<Link to={`/tags/${tag.id}`}>
|
||||
<div className="ellips-data" title={tag.name}>
|
||||
{tag.name}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const AliasesCell = (tag: GQL.TagListDataFragment) => {
|
||||
let aliases = tag.aliases ? tag.aliases.join(", ") : "";
|
||||
return (
|
||||
<span className="ellips-data" title={aliases}>
|
||||
{aliases}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const FavoriteCell = (tag: GQL.TagListDataFragment) => (
|
||||
<Button
|
||||
className={cx("minimal", tag.favorite ? "favorite" : "not-favorite")}
|
||||
onClick={() => setFavorite(!tag.favorite, tag.id)}
|
||||
>
|
||||
<Icon icon={faHeart} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
const SceneCountCell = (tag: GQL.TagListDataFragment) => (
|
||||
<Link to={NavUtils.makeTagScenesUrl(tag)}>
|
||||
<span>{tag.scene_count}</span>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const GalleryCountCell = (tag: GQL.TagListDataFragment) => (
|
||||
<Link to={NavUtils.makeTagGalleriesUrl(tag)}>
|
||||
<span>{tag.gallery_count}</span>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const ImageCountCell = (tag: GQL.TagListDataFragment) => (
|
||||
<Link to={NavUtils.makeTagImagesUrl(tag)}>
|
||||
<span>{tag.image_count}</span>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const GroupCountCell = (tag: GQL.TagListDataFragment) => (
|
||||
<Link to={NavUtils.makeTagGroupsUrl(tag)}>
|
||||
<span>{tag.group_count}</span>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const StudioCountCell = (tag: GQL.TagListDataFragment) => (
|
||||
<Link to={NavUtils.makeTagStudiosUrl(tag)}>
|
||||
<span>{tag.studio_count}</span>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const PerformerCountCell = (tag: GQL.TagListDataFragment) => (
|
||||
<Link to={NavUtils.makeTagPerformersUrl(tag)}>
|
||||
<span>{tag.performer_count}</span>
|
||||
</Link>
|
||||
);
|
||||
|
||||
interface IColumnSpec {
|
||||
value: string;
|
||||
label: string;
|
||||
defaultShow?: boolean;
|
||||
mandatory?: boolean;
|
||||
render?: (tag: GQL.TagListDataFragment, index: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
const allColumns: IColumnSpec[] = [
|
||||
{
|
||||
value: "image",
|
||||
label: intl.formatMessage({ id: "image" }),
|
||||
defaultShow: true,
|
||||
render: ImageCell,
|
||||
},
|
||||
{
|
||||
value: "name",
|
||||
label: intl.formatMessage({ id: "name" }),
|
||||
mandatory: true,
|
||||
defaultShow: true,
|
||||
render: NameCell,
|
||||
},
|
||||
{
|
||||
value: "aliases",
|
||||
label: intl.formatMessage({ id: "aliases" }),
|
||||
defaultShow: true,
|
||||
render: AliasesCell,
|
||||
},
|
||||
{
|
||||
value: "favourite",
|
||||
label: intl.formatMessage({ id: "favourite" }),
|
||||
defaultShow: true,
|
||||
render: FavoriteCell,
|
||||
},
|
||||
{
|
||||
value: "scene_count",
|
||||
label: intl.formatMessage({ id: "scenes" }),
|
||||
defaultShow: true,
|
||||
render: SceneCountCell,
|
||||
},
|
||||
{
|
||||
value: "gallery_count",
|
||||
label: intl.formatMessage({ id: "galleries" }),
|
||||
defaultShow: true,
|
||||
render: GalleryCountCell,
|
||||
},
|
||||
{
|
||||
value: "image_count",
|
||||
label: intl.formatMessage({ id: "images" }),
|
||||
defaultShow: true,
|
||||
render: ImageCountCell,
|
||||
},
|
||||
{
|
||||
value: "group_count",
|
||||
label: intl.formatMessage({ id: "groups" }),
|
||||
defaultShow: true,
|
||||
render: GroupCountCell,
|
||||
},
|
||||
{
|
||||
value: "performer_count",
|
||||
label: intl.formatMessage({ id: "performers" }),
|
||||
defaultShow: true,
|
||||
render: PerformerCountCell,
|
||||
},
|
||||
{
|
||||
value: "studio_count",
|
||||
label: intl.formatMessage({ id: "studios" }),
|
||||
defaultShow: true,
|
||||
render: StudioCountCell,
|
||||
},
|
||||
];
|
||||
|
||||
const defaultColumns = allColumns
|
||||
.filter((col) => col.defaultShow)
|
||||
.map((col) => col.value);
|
||||
|
||||
const { selectedColumns, saveColumns } = useTableColumns(
|
||||
TABLE_NAME,
|
||||
defaultColumns
|
||||
);
|
||||
|
||||
const columnRenderFuncs: Record<
|
||||
string,
|
||||
(tag: GQL.TagListDataFragment, index: number) => React.ReactNode
|
||||
> = {};
|
||||
allColumns.forEach((col) => {
|
||||
if (col.render) {
|
||||
columnRenderFuncs[col.value] = col.render;
|
||||
}
|
||||
});
|
||||
|
||||
function renderCell(
|
||||
column: IColumn,
|
||||
tag: GQL.TagListDataFragment,
|
||||
index: number
|
||||
) {
|
||||
const render = columnRenderFuncs[column.value];
|
||||
|
||||
if (render) return render(tag, index);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListTable
|
||||
className="tag-table"
|
||||
items={props.tags}
|
||||
allColumns={allColumns}
|
||||
columns={selectedColumns}
|
||||
setColumns={(c) => saveColumns(c)}
|
||||
selectedIds={props.selectedIds}
|
||||
onSelectChange={props.onSelectChange}
|
||||
renderCell={renderCell}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in a new issue