Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved list view for scenes, galleries and performers #4368

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e990e7e
initial push
cj12312021 Dec 15, 2023
6b41c42
stylecheet cleanup
cj12312021 Dec 15, 2023
000c529
some clean up
cj12312021 Dec 15, 2023
1e1d61a
More columns
cj12312021 Dec 15, 2023
76278b5
globalized play message and updated column order
cj12312021 Dec 17, 2023
50c8d3f
fixed table height to allow sticky header
cj12312021 Dec 17, 2023
2e71bc8
Refactor column selection
InfiniteStash Dec 18, 2023
8db9e24
updated config.ts with new ui type
cj12312021 Dec 18, 2023
be08c95
config.ts updates not needed
cj12312021 Dec 18, 2023
69896ff
prettier
cj12312021 Dec 18, 2023
90c89b8
added full support for performers page
cj12312021 Dec 19, 2023
7319459
improvements
cj12312021 Dec 19, 2023
1005eb0
prettier
cj12312021 Dec 19, 2023
799e3ad
improved favortie button
cj12312021 Dec 19, 2023
31ecd9b
some improvements and fixes
cj12312021 Dec 21, 2023
51e591e
Specify disabled lint rule
cj12312021 Dec 21, 2023
167bfd9
removed some uses of !important
cj12312021 Dec 21, 2023
be82944
improve sticky header and it's border
cj12312021 Jan 4, 2024
a7c1e01
minizied use of important
cj12312021 Jan 7, 2024
c556aff
Merge remote-tracking branch 'origin/develop' into improved-list-view…
cj12312021 Jan 9, 2024
c714f64
fixes some elipsed fields not expanding on hover
cj12312021 Jan 16, 2024
7c1bf15
Refactor
WithoutPants Jan 16, 2024
05bb88e
Move table-list styles to List directory
WithoutPants Jan 16, 2024
986edbe
Add className to ListTable
WithoutPants Jan 16, 2024
afe6733
Use comma list hover style for ellips-data hover
WithoutPants Jan 16, 2024
2005d95
Add gallery list table
WithoutPants Jan 16, 2024
40b28a6
Fixes anchor list style used in too many places
cj12312021 Jan 16, 2024
3ff2462
Avoid ignoring linter rules
WithoutPants Jan 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 7 additions & 37 deletions ui/v2.5/src/components/Galleries/GalleryList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useState } from "react";
import { useIntl } from "react-intl";
import cloneDeep from "lodash-es/cloneDeep";
import { Table } from "react-bootstrap";
import { Link, useHistory } from "react-router-dom";
import { useHistory } from "react-router-dom";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
import {
Expand All @@ -18,7 +17,7 @@ import GalleryWallCard from "./GalleryWallCard";
import { EditGalleriesDialog } from "./EditGalleriesDialog";
import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog";
import { ExportDialog } from "../Shared/ExportDialog";
import { galleryTitle } from "src/core/galleries";
import { GalleryListTable } from "./GalleryListTable";

const GalleryItemList = makeItemList({
filterMode: GQL.FilterMode.Galleries,
Expand Down Expand Up @@ -152,40 +151,11 @@ export const GalleryList: React.FC<IGalleryList> = ({
}
if (filter.displayMode === DisplayMode.List) {
return (
<Table className="col col-sm-6 mx-auto">
<thead>
<tr>
<th>{intl.formatMessage({ id: "actions.preview" })}</th>
<th className="d-none d-sm-none">
{intl.formatMessage({ id: "title" })}
</th>
</tr>
</thead>
<tbody>
{result.data.findGalleries.galleries.map((gallery) => (
<tr key={gallery.id}>
<td>
<Link to={`/galleries/${gallery.id}`}>
{gallery.cover ? (
<img
loading="lazy"
alt={gallery.title ?? ""}
className="w-100 w-sm-auto"
src={`${gallery.cover.paths.thumbnail}`}
/>
) : undefined}
</Link>
</td>
<td className="d-none d-sm-block">
<Link to={`/galleries/${gallery.id}`}>
{galleryTitle(gallery)} ({gallery.image_count}{" "}
{gallery.image_count === 1 ? "image" : "images"})
</Link>
</td>
</tr>
))}
</tbody>
</Table>
<GalleryListTable
galleries={result.data.findGalleries.galleries}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
}
if (filter.displayMode === DisplayMode.Wall) {
Expand Down
256 changes: 256 additions & 0 deletions ui/v2.5/src/components/Galleries/GalleryListTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import React from "react";
import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import NavUtils from "src/utils/navigation";
import { useIntl } from "react-intl";
import { objectTitle } from "src/core/files";
import { galleryTitle } from "src/core/galleries";
import { RatingSystem } from "../Shared/Rating/RatingSystem";
import { useGalleryUpdate } from "src/core/StashService";
import { IColumn, ListTable } from "../List/ListTable";
import { useTableColumns } from "src/hooks/useTableColumns";

interface IGalleryListTableProps {
galleries: GQL.SlimGalleryDataFragment[];
selectedIds: Set<string>;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
}

const TABLE_NAME = "galleries";

export const GalleryListTable: React.FC<IGalleryListTableProps> = (
props: IGalleryListTableProps
) => {
const intl = useIntl();

const [updateGallery] = useGalleryUpdate();

function setRating(v: number | null, galleryId: string) {
if (galleryId) {
updateGallery({
variables: {
input: {
id: galleryId,
rating100: v,
},
},
});
}
}

const CoverImageCell = (gallery: GQL.SlimGalleryDataFragment) => {
const title = galleryTitle(gallery);

return (
<Link to={`/galleries/${gallery.id}`}>
{gallery.cover ? (
<img
loading="lazy"
alt={title}
className="image-thumbnail"
src={`${gallery.cover.paths.thumbnail}`}
/>
) : undefined}
</Link>
);
};

const TitleCell = (gallery: GQL.SlimGalleryDataFragment) => {
const title = galleryTitle(gallery);

return (
<Link to={`/galleries/${gallery.id}`}>
<span className="ellips-data">{title}</span>
</Link>
);
};

const DateCell = (gallery: GQL.SlimGalleryDataFragment) => (
<>{gallery.date}</>
);

const RatingCell = (gallery: GQL.SlimGalleryDataFragment) => (
<RatingSystem
value={gallery.rating100}
onSetRating={(value) => setRating(value, gallery.id)}
/>
);

const ImagesCell = (gallery: GQL.SlimGalleryDataFragment) => {
return (
<Link to={NavUtils.makeGalleryImagesUrl(gallery)}>
<span>{gallery.image_count}</span>
</Link>
);
};

const TagCell = (gallery: GQL.SlimGalleryDataFragment) => (
<ul className="comma-list">
{gallery.tags.map((tag) => (
<li key={tag.id}>
<Link to={NavUtils.makeTagGalleriesUrl(tag)}>
<span>{tag.name}</span>
</Link>
</li>
))}
</ul>
);

const PerformersCell = (gallery: GQL.SlimGalleryDataFragment) => (
<ul className="comma-list">
{gallery.performers.map((performer) => (
<li key={performer.id}>
<Link to={NavUtils.makePerformerGalleriesUrl(performer)}>
<span>{performer.name}</span>
</Link>
</li>
))}
</ul>
);

const StudioCell = (gallery: GQL.SlimGalleryDataFragment) => {
if (gallery.studio) {
return (
<Link
to={NavUtils.makeStudioGalleriesUrl(gallery.studio)}
title={gallery.studio.name}
>
<span className="ellips-data">{gallery.studio.name}</span>
</Link>
);
}
};

const SceneCell = (gallery: GQL.SlimGalleryDataFragment) => (
<ul className="comma-list">
{gallery.scenes.map((galleryScene) => (
<li key={galleryScene.id}>
<Link to={`/scenes/${galleryScene.id}`}>
<span className="ellips-data">{objectTitle(galleryScene)}</span>
</Link>
</li>
))}
</ul>
);

interface IColumnSpec {
value: string;
label: string;
defaultShow?: boolean;
mandatory?: boolean;
render?: (
gallery: GQL.SlimGalleryDataFragment,
index: number
) => React.ReactNode;
}

const allColumns: IColumnSpec[] = [
{
value: "cover_image",
label: intl.formatMessage({ id: "cover_image" }),
defaultShow: true,
render: CoverImageCell,
},
{
value: "title",
label: intl.formatMessage({ id: "title" }),
defaultShow: true,
mandatory: true,
render: TitleCell,
},
{
value: "date",
label: intl.formatMessage({ id: "date" }),
defaultShow: true,
render: DateCell,
},
{
value: "rating",
label: intl.formatMessage({ id: "rating" }),
defaultShow: true,
render: RatingCell,
},
{
value: "code",
label: intl.formatMessage({ id: "scene_code" }),
render: (s) => <>{s.code}</>,
},
{
value: "images",
label: intl.formatMessage({ id: "images" }),
defaultShow: true,
render: ImagesCell,
},
{
value: "tags",
label: intl.formatMessage({ id: "tags" }),
defaultShow: true,
render: TagCell,
},
{
value: "performers",
label: intl.formatMessage({ id: "performers" }),
defaultShow: true,
render: PerformersCell,
},
{
value: "studio",
label: intl.formatMessage({ id: "studio" }),
defaultShow: true,
render: StudioCell,
},
{
value: "scenes",
label: intl.formatMessage({ id: "scenes" }),
defaultShow: true,
render: SceneCell,
},
{
value: "photographer",
label: intl.formatMessage({ id: "photographer" }),
render: (s) => <>{s.photographer}</>,
},
];

const defaultColumns = allColumns
.filter((col) => col.defaultShow)
.map((col) => col.value);

const { selectedColumns, saveColumns } = useTableColumns(
TABLE_NAME,
defaultColumns
);

const columnRenderFuncs: Record<
string,
(gallery: GQL.SlimGalleryDataFragment, index: number) => React.ReactNode
> = {};
allColumns.forEach((col) => {
if (col.render) {
columnRenderFuncs[col.value] = col.render;
}
});

function renderCell(
column: IColumn,
gallery: GQL.SlimGalleryDataFragment,
index: number
) {
const render = columnRenderFuncs[column.value];

if (render) return render(gallery, index);
}

return (
<ListTable
className="gallery-table"
items={props.galleries}
allColumns={allColumns}
columns={selectedColumns}
setColumns={(c) => saveColumns(c)}
selectedIds={props.selectedIds}
onSelectChange={props.onSelectChange}
renderCell={renderCell}
/>
);
};
Loading
Loading