mirror of https://github.com/stashapp/stash.git
Bulk update scenes (#92)
* Add bulk update functionality * Restore multiselect fixes from previous branch * Prevent unsetting of studios/tags * Detect when slice fields are omitted and ignore
This commit is contained in:
parent
70ce01c604
commit
0655223c38
|
@ -26,6 +26,34 @@ mutation SceneUpdate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation BulkSceneUpdate(
|
||||||
|
$ids: [ID!] = [],
|
||||||
|
$title: String,
|
||||||
|
$details: String,
|
||||||
|
$url: String,
|
||||||
|
$date: String,
|
||||||
|
$rating: Int,
|
||||||
|
$studio_id: ID,
|
||||||
|
$gallery_id: ID,
|
||||||
|
$performer_ids: [ID!],
|
||||||
|
$tag_ids: [ID!]) {
|
||||||
|
|
||||||
|
bulkSceneUpdate(input: {
|
||||||
|
ids: $ids,
|
||||||
|
title: $title,
|
||||||
|
details: $details,
|
||||||
|
url: $url,
|
||||||
|
date: $date,
|
||||||
|
rating: $rating,
|
||||||
|
studio_id: $studio_id,
|
||||||
|
gallery_id: $gallery_id,
|
||||||
|
performer_ids: $performer_ids,
|
||||||
|
tag_ids: $tag_ids
|
||||||
|
}) {
|
||||||
|
...SceneData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mutation SceneDestroy($id: ID!, $delete_file: Boolean, $delete_generated : Boolean) {
|
mutation SceneDestroy($id: ID!, $delete_file: Boolean, $delete_generated : Boolean) {
|
||||||
sceneDestroy(input: {id: $id, delete_file: $delete_file, delete_generated: $delete_generated})
|
sceneDestroy(input: {id: $id, delete_file: $delete_file, delete_generated: $delete_generated})
|
||||||
}
|
}
|
|
@ -77,6 +77,7 @@ type Query {
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
sceneUpdate(input: SceneUpdateInput!): Scene
|
sceneUpdate(input: SceneUpdateInput!): Scene
|
||||||
|
bulkSceneUpdate(input: BulkSceneUpdateInput!): [Scene!]
|
||||||
sceneDestroy(input: SceneDestroyInput!): Boolean!
|
sceneDestroy(input: SceneDestroyInput!): Boolean!
|
||||||
|
|
||||||
sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker
|
sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker
|
||||||
|
|
|
@ -53,6 +53,20 @@ input SceneUpdateInput {
|
||||||
tag_ids: [ID!]
|
tag_ids: [ID!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input BulkSceneUpdateInput {
|
||||||
|
clientMutationId: String
|
||||||
|
ids: [ID!]
|
||||||
|
title: String
|
||||||
|
details: String
|
||||||
|
url: String
|
||||||
|
date: String
|
||||||
|
rating: Int
|
||||||
|
studio_id: ID
|
||||||
|
gallery_id: ID
|
||||||
|
performer_ids: [ID!]
|
||||||
|
tag_ids: [ID!]
|
||||||
|
}
|
||||||
|
|
||||||
input SceneDestroyInput {
|
input SceneDestroyInput {
|
||||||
id: ID!
|
id: ID!
|
||||||
delete_file: Boolean
|
delete_file: Boolean
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/99designs/gqlgen/graphql"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scraper"
|
"github.com/stashapp/stash/pkg/scraper"
|
||||||
)
|
)
|
||||||
|
@ -165,3 +167,13 @@ func (r *queryResolver) ScrapeFreeones(ctx context.Context, performer_name strin
|
||||||
func (r *queryResolver) ScrapeFreeonesPerformerList(ctx context.Context, query string) ([]string, error) {
|
func (r *queryResolver) ScrapeFreeonesPerformerList(ctx context.Context, query string) ([]string, error) {
|
||||||
return scraper.GetPerformerNames(query)
|
return scraper.GetPerformerNames(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wasFieldIncluded returns true if the given field was included in the request.
|
||||||
|
// Slices are unmarshalled to empty slices even if the field was omitted. This
|
||||||
|
// method determines if it was omitted altogether.
|
||||||
|
func wasFieldIncluded(ctx context.Context, field string) bool {
|
||||||
|
rctx := graphql.GetRequestContext(ctx)
|
||||||
|
|
||||||
|
_, ret := rctx.Variables[field]
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
|
@ -119,6 +119,121 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp
|
||||||
return scene, nil
|
return scene, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.BulkSceneUpdateInput) ([]*models.Scene, error) {
|
||||||
|
// Populate scene from the input
|
||||||
|
updatedTime := time.Now()
|
||||||
|
|
||||||
|
// Start the transaction and save the scene marker
|
||||||
|
tx := database.DB.MustBeginTx(ctx, nil)
|
||||||
|
qb := models.NewSceneQueryBuilder()
|
||||||
|
jqb := models.NewJoinsQueryBuilder()
|
||||||
|
|
||||||
|
updatedScene := models.ScenePartial{
|
||||||
|
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||||
|
}
|
||||||
|
if input.Title != nil {
|
||||||
|
updatedScene.Title = &sql.NullString{String: *input.Title, Valid: true}
|
||||||
|
}
|
||||||
|
if input.Details != nil {
|
||||||
|
updatedScene.Details = &sql.NullString{String: *input.Details, Valid: true}
|
||||||
|
}
|
||||||
|
if input.URL != nil {
|
||||||
|
updatedScene.URL = &sql.NullString{String: *input.URL, Valid: true}
|
||||||
|
}
|
||||||
|
if input.Date != nil {
|
||||||
|
updatedScene.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
|
||||||
|
}
|
||||||
|
if input.Rating != nil {
|
||||||
|
// a rating of 0 means unset the rating
|
||||||
|
if *input.Rating == 0 {
|
||||||
|
updatedScene.Rating = &sql.NullInt64{Int64: 0, Valid: false}
|
||||||
|
} else {
|
||||||
|
updatedScene.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if input.StudioID != nil {
|
||||||
|
// empty string means unset the studio
|
||||||
|
if *input.StudioID == "" {
|
||||||
|
updatedScene.StudioID = &sql.NullInt64{Int64: 0, Valid: false}
|
||||||
|
} else {
|
||||||
|
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||||
|
updatedScene.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := []*models.Scene{}
|
||||||
|
|
||||||
|
for _, sceneIDStr := range input.Ids {
|
||||||
|
sceneID, _ := strconv.Atoi(sceneIDStr)
|
||||||
|
updatedScene.ID = sceneID
|
||||||
|
|
||||||
|
scene, err := qb.Update(updatedScene, tx)
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, scene)
|
||||||
|
|
||||||
|
if input.GalleryID != nil {
|
||||||
|
// Save the gallery
|
||||||
|
galleryID, _ := strconv.Atoi(*input.GalleryID)
|
||||||
|
updatedGallery := models.Gallery{
|
||||||
|
ID: galleryID,
|
||||||
|
SceneID: sql.NullInt64{Int64: int64(sceneID), Valid: true},
|
||||||
|
UpdatedAt: models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||||
|
}
|
||||||
|
gqb := models.NewGalleryQueryBuilder()
|
||||||
|
_, err := gqb.Update(updatedGallery, tx)
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the performers
|
||||||
|
if wasFieldIncluded(ctx, "performer_ids") {
|
||||||
|
var performerJoins []models.PerformersScenes
|
||||||
|
for _, pid := range input.PerformerIds {
|
||||||
|
performerID, _ := strconv.Atoi(pid)
|
||||||
|
performerJoin := models.PerformersScenes{
|
||||||
|
PerformerID: performerID,
|
||||||
|
SceneID: sceneID,
|
||||||
|
}
|
||||||
|
performerJoins = append(performerJoins, performerJoin)
|
||||||
|
}
|
||||||
|
if err := jqb.UpdatePerformersScenes(sceneID, performerJoins, tx); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the tags
|
||||||
|
if wasFieldIncluded(ctx, "tag_ids") {
|
||||||
|
var tagJoins []models.ScenesTags
|
||||||
|
for _, tid := range input.TagIds {
|
||||||
|
tagID, _ := strconv.Atoi(tid)
|
||||||
|
tagJoin := models.ScenesTags{
|
||||||
|
SceneID: sceneID,
|
||||||
|
TagID: tagID,
|
||||||
|
}
|
||||||
|
tagJoins = append(tagJoins, tagJoin)
|
||||||
|
}
|
||||||
|
if err := jqb.UpdateScenesTags(sceneID, tagJoins, tx); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
|
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
|
||||||
qb := models.NewSceneQueryBuilder()
|
qb := models.NewSceneQueryBuilder()
|
||||||
tx := database.DB.MustBeginTx(ctx, nil)
|
tx := database.DB.MustBeginTx(ctx, nil)
|
||||||
|
|
|
@ -25,6 +25,8 @@ interface IListFilterProps {
|
||||||
onChangeDisplayMode: (displayMode: DisplayMode) => void;
|
onChangeDisplayMode: (displayMode: DisplayMode) => void;
|
||||||
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
|
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
|
||||||
onRemoveCriterion: (criterion: Criterion) => void;
|
onRemoveCriterion: (criterion: Criterion) => void;
|
||||||
|
onSelectAll?: () => void;
|
||||||
|
onSelectNone?: () => void;
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,6 +136,39 @@ export const ListFilter: FunctionComponent<IListFilterProps> = (props: IListFilt
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSelectAll() {
|
||||||
|
if (props.onSelectAll) {
|
||||||
|
props.onSelectAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectNone() {
|
||||||
|
if (props.onSelectNone) {
|
||||||
|
props.onSelectNone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectAll() {
|
||||||
|
if (props.onSelectAll) {
|
||||||
|
return <Button onClick={() => onSelectAll()} text="Select All"/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectNone() {
|
||||||
|
if (props.onSelectNone) {
|
||||||
|
return <Button onClick={() => onSelectNone()} text="Select None"/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectAllNone() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderSelectAll()}
|
||||||
|
{renderSelectNone()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -176,6 +211,10 @@ export const ListFilter: FunctionComponent<IListFilterProps> = (props: IListFilt
|
||||||
<ButtonGroup className="filter-item">
|
<ButtonGroup className="filter-item">
|
||||||
{renderDisplayModeOptions()}
|
{renderDisplayModeOptions()}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<ButtonGroup className="filter-item">
|
||||||
|
{renderSelectAllNone()}
|
||||||
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
<div style={{display: "flex", justifyContent: "center", margin: "10px auto"}}>
|
<div style={{display: "flex", justifyContent: "center", margin: "10px auto"}}>
|
||||||
{renderFilterTags()}
|
{renderFilterTags()}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Card,
|
Card,
|
||||||
|
Checkbox,
|
||||||
Divider,
|
Divider,
|
||||||
Elevation,
|
Elevation,
|
||||||
H4,
|
H4,
|
||||||
|
@ -19,12 +20,15 @@ import { SceneHelpers } from "./helpers";
|
||||||
|
|
||||||
interface ISceneCardProps {
|
interface ISceneCardProps {
|
||||||
scene: GQL.SlimSceneDataFragment;
|
scene: GQL.SlimSceneDataFragment;
|
||||||
|
selected: boolean | undefined;
|
||||||
|
onSelectedChanged: (selected : boolean, shiftKey : boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneCard: FunctionComponent<ISceneCardProps> = (props: ISceneCardProps) => {
|
export const SceneCard: FunctionComponent<ISceneCardProps> = (props: ISceneCardProps) => {
|
||||||
const [previewPath, setPreviewPath] = useState<string | undefined>(undefined);
|
const [previewPath, setPreviewPath] = useState<string | undefined>(undefined);
|
||||||
const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: false});
|
const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: false});
|
||||||
|
|
||||||
|
|
||||||
function maybeRenderRatingBanner() {
|
function maybeRenderRatingBanner() {
|
||||||
if (!props.scene.rating) { return; }
|
if (!props.scene.rating) { return; }
|
||||||
return (
|
return (
|
||||||
|
@ -115,6 +119,8 @@ export const SceneCard: FunctionComponent<ISceneCardProps> = (props: ISceneCardP
|
||||||
setPreviewPath("");
|
setPreviewPath("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shiftKey = false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="grid-item"
|
className="grid-item"
|
||||||
|
@ -122,6 +128,12 @@ export const SceneCard: FunctionComponent<ISceneCardProps> = (props: ISceneCardP
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
|
<Checkbox
|
||||||
|
className="card-select"
|
||||||
|
checked={props.selected}
|
||||||
|
onChange={() => props.onSelectedChanged(!props.selected, shiftKey)}
|
||||||
|
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => { shiftKey = event.shiftKey; event.stopPropagation(); } }
|
||||||
|
/>
|
||||||
<Link to={`/scenes/${props.scene.id}`} className="image previewable">
|
<Link to={`/scenes/${props.scene.id}`} className="image previewable">
|
||||||
{maybeRenderRatingBanner()}
|
{maybeRenderRatingBanner()}
|
||||||
<video className="preview" loop={true} poster={props.scene.paths.screenshot || ""} ref={videoHoverHook.videoEl}>
|
<video className="preview" loop={true} poster={props.scene.paths.screenshot || ""} ref={videoHoverHook.videoEl}>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { FunctionComponent } from "react";
|
import React, { FunctionComponent } from "react";
|
||||||
import { QueryHookResult } from "react-apollo-hooks";
|
import { QueryHookResult } from "react-apollo-hooks";
|
||||||
import { FindScenesQuery, FindScenesVariables } from "../../core/generated-graphql";
|
import { FindScenesQuery, FindScenesVariables, SlimSceneDataFragment } from "../../core/generated-graphql";
|
||||||
import { ListHook } from "../../hooks/ListHook";
|
import { ListHook } from "../../hooks/ListHook";
|
||||||
import { IBaseProps } from "../../models/base-props";
|
import { IBaseProps } from "../../models/base-props";
|
||||||
import { ListFilterModel } from "../../models/list-filter/filter";
|
import { ListFilterModel } from "../../models/list-filter/filter";
|
||||||
|
@ -9,6 +9,7 @@ import { DisplayMode, FilterMode } from "../../models/list-filter/types";
|
||||||
import { WallPanel } from "../Wall/WallPanel";
|
import { WallPanel } from "../Wall/WallPanel";
|
||||||
import { SceneCard } from "./SceneCard";
|
import { SceneCard } from "./SceneCard";
|
||||||
import { SceneListTable } from "./SceneListTable";
|
import { SceneListTable } from "./SceneListTable";
|
||||||
|
import { SceneSelectedOptions } from "./SceneSelectedOptions";
|
||||||
|
|
||||||
interface ISceneListProps extends IBaseProps {}
|
interface ISceneListProps extends IBaseProps {}
|
||||||
|
|
||||||
|
@ -17,14 +18,50 @@ export const SceneList: FunctionComponent<ISceneListProps> = (props: ISceneListP
|
||||||
filterMode: FilterMode.Scenes,
|
filterMode: FilterMode.Scenes,
|
||||||
props,
|
props,
|
||||||
renderContent,
|
renderContent,
|
||||||
|
renderSelectedOptions
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderContent(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, filter: ListFilterModel) {
|
function renderSelectedOptions(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, selectedIds: Set<string>) {
|
||||||
|
// find the selected items from the ids
|
||||||
|
if (!result.data || !result.data.findScenes) { return undefined; }
|
||||||
|
|
||||||
|
var scenes = result.data.findScenes.scenes;
|
||||||
|
|
||||||
|
var selectedScenes : SlimSceneDataFragment[] = [];
|
||||||
|
selectedIds.forEach((id) => {
|
||||||
|
var scene = scenes.find((scene) => {
|
||||||
|
return scene.id === id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scene) {
|
||||||
|
selectedScenes.push(scene);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SceneSelectedOptions selected={selectedScenes} onScenesUpdated={() => { return; }}/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSceneCard(scene : SlimSceneDataFragment, selectedIds: Set<string>) {
|
||||||
|
return (
|
||||||
|
<SceneCard
|
||||||
|
key={scene.id}
|
||||||
|
scene={scene}
|
||||||
|
selected={selectedIds.has(scene.id)}
|
||||||
|
onSelectedChanged={(selected: boolean, shiftKey: boolean) => listData.onSelectChange(scene.id, selected, shiftKey)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContent(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, filter: ListFilterModel, selectedIds: Set<string>) {
|
||||||
if (!result.data || !result.data.findScenes) { return; }
|
if (!result.data || !result.data.findScenes) { return; }
|
||||||
if (filter.displayMode === DisplayMode.Grid) {
|
if (filter.displayMode === DisplayMode.Grid) {
|
||||||
return (
|
return (
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
{result.data.findScenes.scenes.map((scene) => (<SceneCard key={scene.id} scene={scene} />))}
|
{result.data.findScenes.scenes.map((scene) => renderSceneCard(scene, selectedIds))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (filter.displayMode === DisplayMode.List) {
|
} else if (filter.displayMode === DisplayMode.List) {
|
||||||
|
|
|
@ -0,0 +1,308 @@
|
||||||
|
import _ from "lodash";
|
||||||
|
import {
|
||||||
|
AnchorButton,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
ControlGroup,
|
||||||
|
FormGroup,
|
||||||
|
HTMLSelect,
|
||||||
|
InputGroup,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Popover,
|
||||||
|
Spinner,
|
||||||
|
Tag,
|
||||||
|
} from "@blueprintjs/core";
|
||||||
|
import React, { FunctionComponent, SyntheticEvent, useEffect, useRef, useState } from "react";
|
||||||
|
import { FilterSelect } from "../select/FilterSelect";
|
||||||
|
import { FilterMultiSelect } from "../select/FilterMultiSelect";
|
||||||
|
import { StashService } from "../../core/StashService";
|
||||||
|
import * as GQL from "../../core/generated-graphql";
|
||||||
|
import { ErrorUtils } from "../../utils/errors";
|
||||||
|
import { ToastUtils } from "../../utils/toasts";
|
||||||
|
|
||||||
|
interface IListOperationProps {
|
||||||
|
selected: GQL.SlimSceneDataFragment[],
|
||||||
|
onScenesUpdated: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SceneSelectedOptions: FunctionComponent<IListOperationProps> = (props: IListOperationProps) => {
|
||||||
|
const [rating, setRating] = useState<string>("");
|
||||||
|
const [studioId, setStudioId] = useState<string | undefined>(undefined);
|
||||||
|
const [performerIds, setPerformerIds] = useState<string[] | undefined>(undefined);
|
||||||
|
const [tagIds, setTagIds] = useState<string[] | undefined>(undefined);
|
||||||
|
|
||||||
|
const updateScenes = StashService.useBulkSceneUpdate(getSceneInput());
|
||||||
|
|
||||||
|
// Network state
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
function getSceneInput() : GQL.BulkSceneUpdateInput {
|
||||||
|
// need to determine what we are actually setting on each scene
|
||||||
|
var aggregateRating = getRating(props.selected);
|
||||||
|
var aggregateStudioId = getStudioId(props.selected);
|
||||||
|
var aggregatePerformerIds = getPerformerIds(props.selected);
|
||||||
|
var aggregateTagIds = getTagIds(props.selected);
|
||||||
|
|
||||||
|
var sceneInput : GQL.BulkSceneUpdateInput = {
|
||||||
|
ids: props.selected.map((scene) => {
|
||||||
|
return scene.id;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// if rating is undefined
|
||||||
|
if (rating === "") {
|
||||||
|
// and all scenes have the same rating, then we are unsetting the rating.
|
||||||
|
if(aggregateRating) {
|
||||||
|
// an undefined rating is ignored in the server, so set it to 0 instead
|
||||||
|
sceneInput.rating = 0;
|
||||||
|
}
|
||||||
|
// otherwise not setting the rating
|
||||||
|
} else {
|
||||||
|
// if rating is set, then we are setting the rating for all
|
||||||
|
sceneInput.rating = Number.parseInt(rating);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if studioId is undefined
|
||||||
|
if (studioId === undefined) {
|
||||||
|
// and all scenes have the same studioId,
|
||||||
|
// then unset the studioId, otherwise ignoring studioId
|
||||||
|
if (aggregateStudioId) {
|
||||||
|
// an undefined studio_id is ignored in the server, so set it to empty string instead
|
||||||
|
sceneInput.studio_id = "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if studioId is set, then we are setting it
|
||||||
|
sceneInput.studio_id = studioId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if performerIds are empty
|
||||||
|
if (!performerIds || performerIds.length === 0) {
|
||||||
|
// and all scenes have the same ids,
|
||||||
|
if (aggregatePerformerIds.length > 0) {
|
||||||
|
// then unset the performerIds, otherwise ignore
|
||||||
|
sceneInput.performer_ids = performerIds;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if performerIds non-empty, then we are setting them
|
||||||
|
sceneInput.performer_ids = performerIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if tagIds non-empty, then we are setting them
|
||||||
|
if (!tagIds || tagIds.length === 0) {
|
||||||
|
// and all scenes have the same ids,
|
||||||
|
if (aggregateTagIds.length > 0) {
|
||||||
|
// then unset the tagIds, otherwise ignore
|
||||||
|
sceneInput.tag_ids = tagIds;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if tagIds non-empty, then we are setting them
|
||||||
|
sceneInput.tag_ids = tagIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sceneInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSave() {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await updateScenes();
|
||||||
|
ToastUtils.success("Updated scenes");
|
||||||
|
} catch (e) {
|
||||||
|
ErrorUtils.handle(e);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
props.onScenesUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRating(state: GQL.SlimSceneDataFragment[]) {
|
||||||
|
var ret : number | undefined;
|
||||||
|
var first = true;
|
||||||
|
|
||||||
|
state.forEach((scene : GQL.SlimSceneDataFragment) => {
|
||||||
|
if (first) {
|
||||||
|
ret = scene.rating;
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
if (ret !== scene.rating) {
|
||||||
|
ret = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStudioId(state: GQL.SlimSceneDataFragment[]) {
|
||||||
|
var ret : string | undefined;
|
||||||
|
var first = true;
|
||||||
|
|
||||||
|
state.forEach((scene : GQL.SlimSceneDataFragment) => {
|
||||||
|
if (first) {
|
||||||
|
ret = scene.studio ? scene.studio.id : undefined;
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
var studioId = scene.studio ? scene.studio.id : undefined;
|
||||||
|
if (ret != studioId) {
|
||||||
|
ret = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toId(object : any) {
|
||||||
|
return object.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPerformerIds(state: GQL.SlimSceneDataFragment[]) {
|
||||||
|
var ret : string[] = [];
|
||||||
|
var first = true;
|
||||||
|
|
||||||
|
state.forEach((scene : GQL.SlimSceneDataFragment) => {
|
||||||
|
if (first) {
|
||||||
|
ret = !!scene.performers ? scene.performers.map(toId).sort() : [];
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
const perfIds = !!scene.performers ? scene.performers.map(toId).sort() : [];
|
||||||
|
|
||||||
|
if (!_.isEqual(ret, perfIds)) {
|
||||||
|
ret = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTagIds(state: GQL.SlimSceneDataFragment[]) {
|
||||||
|
var ret : string[] = [];
|
||||||
|
var first = true;
|
||||||
|
|
||||||
|
state.forEach((scene : GQL.SlimSceneDataFragment) => {
|
||||||
|
if (first) {
|
||||||
|
ret = !!scene.tags ? scene.tags.map(toId).sort() : [];
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
const tIds = !!scene.tags ? scene.tags.map(toId).sort() : [];
|
||||||
|
|
||||||
|
if (!_.isEqual(ret, tIds)) {
|
||||||
|
ret = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateScenesEditState(state: GQL.SlimSceneDataFragment[]) {
|
||||||
|
function toId(object : any) {
|
||||||
|
return object.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rating : string = "";
|
||||||
|
var studioId : string | undefined;
|
||||||
|
var performerIds : string[] = [];
|
||||||
|
var tagIds : string[] = [];
|
||||||
|
var first = true;
|
||||||
|
|
||||||
|
state.forEach((scene : GQL.SlimSceneDataFragment) => {
|
||||||
|
var thisRating = scene.rating ? scene.rating.toString() : "";
|
||||||
|
var thisStudio = scene.studio ? scene.studio.id : undefined;
|
||||||
|
|
||||||
|
if (first) {
|
||||||
|
rating = thisRating;
|
||||||
|
studioId = thisStudio;
|
||||||
|
performerIds = !!scene.performers ? scene.performers.map(toId).sort() : [];
|
||||||
|
tagIds = !!scene.tags ? scene.tags.map(toId).sort() : [];
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
if (rating !== thisRating) {
|
||||||
|
rating = "";
|
||||||
|
}
|
||||||
|
if (studioId != thisStudio) {
|
||||||
|
studioId = undefined;
|
||||||
|
}
|
||||||
|
const perfIds = !!scene.performers ? scene.performers.map(toId).sort() : [];
|
||||||
|
const tIds = !!scene.tags ? scene.tags.map(toId).sort() : [];
|
||||||
|
|
||||||
|
if (!_.isEqual(performerIds, perfIds)) {
|
||||||
|
performerIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isEqual(tagIds, tIds)) {
|
||||||
|
tagIds = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRating(rating);
|
||||||
|
setStudioId(studioId);
|
||||||
|
setPerformerIds(performerIds);
|
||||||
|
setTagIds(tagIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateScenesEditState(props.selected);
|
||||||
|
}, [props.selected]);
|
||||||
|
|
||||||
|
function renderMultiSelect(type: "performers" | "tags", initialIds: string[] | undefined) {
|
||||||
|
return (
|
||||||
|
<FilterMultiSelect
|
||||||
|
type={type}
|
||||||
|
onUpdate={(items) => {
|
||||||
|
const ids = items.map((i) => i.id);
|
||||||
|
switch (type) {
|
||||||
|
case "performers": setPerformerIds(ids); break;
|
||||||
|
case "tags": setTagIds(ids); break;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
initialIds={initialIds}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
|
||||||
|
<div className="operation-container">
|
||||||
|
<FormGroup className="operation-item" label="Rating">
|
||||||
|
<HTMLSelect
|
||||||
|
options={["", 1, 2, 3, 4, 5]}
|
||||||
|
onChange={(event) => setRating(event.target.value)}
|
||||||
|
value={rating}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup className="operation-item" label="Studio">
|
||||||
|
<FilterSelect
|
||||||
|
type="studios"
|
||||||
|
onSelectItem={(item : any) => setStudioId(item ? item.id : undefined)}
|
||||||
|
initialId={studioId}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup className="operation-item" label="Performers">
|
||||||
|
{renderMultiSelect("performers", performerIds)}
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup className="operation-item" label="Tags">
|
||||||
|
{renderMultiSelect("tags", tagIds)}
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<ButtonGroup className="operation-item">
|
||||||
|
<Button
|
||||||
|
intent="primary"
|
||||||
|
onClick={() => onSave()}>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return render();
|
||||||
|
};
|
|
@ -24,13 +24,21 @@ interface IProps extends HTMLInputProps, Partial<IMultiSelectProps<ValidTypes>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilterMultiSelect: React.FunctionComponent<IProps> = (props: IProps) => {
|
export const FilterMultiSelect: React.FunctionComponent<IProps> = (props: IProps) => {
|
||||||
let items: ValidTypes[];
|
let MultiSelectImpl = getMultiSelectImpl();
|
||||||
let InternalMultiSelect: new (props: IMultiSelectProps<any>) => MultiSelect<any>;
|
let InternalMultiSelect = MultiSelectImpl.getInternalMultiSelect();
|
||||||
var createNewFunc = undefined;
|
const data = MultiSelectImpl.getData();
|
||||||
|
|
||||||
|
const [selectedItems, setSelectedItems] = React.useState<ValidTypes[]>([]);
|
||||||
|
const [items, setItems] = React.useState<ValidTypes[]>([]);
|
||||||
const [newTagName, setNewTagName] = React.useState<string>("");
|
const [newTagName, setNewTagName] = React.useState<string>("");
|
||||||
const createTag = StashService.useTagCreate(getTagInput() as GQL.TagCreateInput);
|
const createTag = StashService.useTagCreate(getTagInput() as GQL.TagCreateInput);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!!data) {
|
||||||
|
MultiSelectImpl.translateData();
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
function getTagInput() {
|
function getTagInput() {
|
||||||
const tagInput: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { name: newTagName };
|
const tagInput: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { name: newTagName };
|
||||||
return tagInput;
|
return tagInput;
|
||||||
|
@ -42,6 +50,8 @@ export const FilterMultiSelect: React.FunctionComponent<IProps> = (props: IProps
|
||||||
try {
|
try {
|
||||||
created = await createTag();
|
created = await createTag();
|
||||||
|
|
||||||
|
items.push(created.data.tagCreate);
|
||||||
|
setItems(items.slice());
|
||||||
addSelectedItem(created.data.tagCreate);
|
addSelectedItem(created.data.tagCreate);
|
||||||
|
|
||||||
ToastUtils.success("Created tag");
|
ToastUtils.success("Created tag");
|
||||||
|
@ -76,43 +86,50 @@ export const FilterMultiSelect: React.FunctionComponent<IProps> = (props: IProps
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!!props.initialIds && !!items) {
|
||||||
|
const initialItems = items.filter((item) => props.initialIds!.includes(item.id));
|
||||||
|
setSelectedItems(initialItems);
|
||||||
|
}
|
||||||
|
}, [props.initialIds, items]);
|
||||||
|
|
||||||
|
function getMultiSelectImpl() {
|
||||||
|
let getInternalMultiSelect: () => new (props: IMultiSelectProps<any>) => MultiSelect<any>;
|
||||||
|
let getData: () => GQL.AllPerformersForFilterQuery | GQL.AllStudiosForFilterQuery | GQL.AllTagsForFilterQuery | undefined;
|
||||||
|
let translateData: () => void;
|
||||||
|
let createNewObject: ((query : string) => void) | undefined = undefined;
|
||||||
|
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
case "performers": {
|
case "performers": {
|
||||||
const { data } = StashService.useAllPerformersForFilter();
|
getInternalMultiSelect = () => { return InternalPerformerMultiSelect; };
|
||||||
items = !!data && !!data.allPerformers ? data.allPerformers : [];
|
getData = () => { const { data } = StashService.useAllPerformersForFilter(); return data; }
|
||||||
InternalMultiSelect = InternalPerformerMultiSelect;
|
translateData = () => { let perfData = data as GQL.AllPerformersForFilterQuery; setItems(!!perfData && !!perfData.allPerformers ? perfData.allPerformers : []); };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "studios": {
|
case "studios": {
|
||||||
const { data } = StashService.useAllStudiosForFilter();
|
getInternalMultiSelect = () => { return InternalStudioMultiSelect; };
|
||||||
items = !!data && !!data.allStudios ? data.allStudios : [];
|
getData = () => { const { data } = StashService.useAllStudiosForFilter(); return data; }
|
||||||
InternalMultiSelect = InternalStudioMultiSelect;
|
translateData = () => { let studioData = data as GQL.AllStudiosForFilterQuery; setItems(!!studioData && !!studioData.allStudios ? studioData.allStudios : []); };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "tags": {
|
case "tags": {
|
||||||
const { data } = StashService.useAllTagsForFilter();
|
getInternalMultiSelect = () => { return InternalTagMultiSelect; };
|
||||||
items = !!data && !!data.allTags ? data.allTags : [];
|
getData = () => { const { data } = StashService.useAllTagsForFilter(); return data; }
|
||||||
InternalMultiSelect = InternalTagMultiSelect;
|
translateData = () => { let tagData = data as GQL.AllTagsForFilterQuery; setItems(!!tagData && !!tagData.allTags ? tagData.allTags : []); };
|
||||||
createNewFunc = createNewTag;
|
createNewObject = createNewTag;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
console.error("Unhandled case in FilterMultiSelect");
|
throw "Unhandled case in FilterMultiSelect";
|
||||||
return <>Unhandled case in FilterMultiSelect</>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
return {
|
||||||
const [selectedItems, setSelectedItems] = React.useState<ValidTypes[]>([]);
|
getInternalMultiSelect: getInternalMultiSelect,
|
||||||
const [isInitialized, setIsInitialized] = React.useState<boolean>(false);
|
getData: getData,
|
||||||
/* eslint-enable */
|
translateData: translateData,
|
||||||
|
createNewObject: createNewObject
|
||||||
if (!!props.initialIds && selectedItems.length === 0 && !isInitialized) {
|
};
|
||||||
const initialItems = items.filter((item) => props.initialIds!.includes(item.id));
|
|
||||||
if (initialItems.length > 0) {
|
|
||||||
setSelectedItems(initialItems);
|
|
||||||
setIsInitialized(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderItem: ItemRenderer<ValidTypes> = (item, itemProps) => {
|
const renderItem: ItemRenderer<ValidTypes> = (item, itemProps) => {
|
||||||
|
@ -165,7 +182,7 @@ export const FilterMultiSelect: React.FunctionComponent<IProps> = (props: IProps
|
||||||
onItemSelect={onItemSelect}
|
onItemSelect={onItemSelect}
|
||||||
resetOnSelect={true}
|
resetOnSelect={true}
|
||||||
popoverProps={{position: "bottom"}}
|
popoverProps={{position: "bottom"}}
|
||||||
createNewItemFromQuery={createNewFunc}
|
createNewItemFromQuery={MultiSelectImpl.createNewObject}
|
||||||
createNewItemRenderer={createNewRenderer}
|
createNewItemRenderer={createNewRenderer}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -179,6 +179,10 @@ export class StashService {
|
||||||
return GQL.useSceneUpdate({ variables: input });
|
return GQL.useSceneUpdate({ variables: input });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static useBulkSceneUpdate(input: GQL.BulkSceneUpdateInput) {
|
||||||
|
return GQL.useBulkSceneUpdate({ variables: input, refetchQueries: ["FindScenes"] });
|
||||||
|
}
|
||||||
|
|
||||||
public static useSceneDestroy(input: GQL.SceneDestroyInput) {
|
public static useSceneDestroy(input: GQL.SceneDestroyInput) {
|
||||||
return GQL.useSceneDestroy({ variables: input });
|
return GQL.useSceneDestroy({ variables: input });
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,17 +15,22 @@ export interface IListHookData {
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
template: JSX.Element;
|
template: JSX.Element;
|
||||||
options: IListHookOptions;
|
options: IListHookOptions;
|
||||||
|
onSelectChange: (id: string, selected : boolean, shiftKey: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IListHookOptions {
|
export interface IListHookOptions {
|
||||||
filterMode: FilterMode;
|
filterMode: FilterMode;
|
||||||
props: IBaseProps;
|
props: IBaseProps;
|
||||||
renderContent: (result: QueryHookResult<any, any>, filter: ListFilterModel) => JSX.Element | undefined;
|
renderContent: (result: QueryHookResult<any, any>, filter: ListFilterModel, selectedIds: Set<string>) => JSX.Element | undefined;
|
||||||
|
renderSelectedOptions?: (result: QueryHookResult<any, any>, selectedIds: Set<string>) => JSX.Element | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ListHook {
|
export class ListHook {
|
||||||
public static useList(options: IListHookOptions): IListHookData {
|
public static useList(options: IListHookOptions): IListHookData {
|
||||||
const [filter, setFilter] = useState<ListFilterModel>(new ListFilterModel(options.filterMode));
|
const [filter, setFilter] = useState<ListFilterModel>(new ListFilterModel(options.filterMode));
|
||||||
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
|
const [lastClickedId, setLastClickedId] = useState<string | undefined>(undefined);
|
||||||
|
const [totalCount, setTotalCount] = useState<number>(0);
|
||||||
|
|
||||||
// Update the filter when the query parameters change
|
// Update the filter when the query parameters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -39,42 +44,61 @@ export class ListHook {
|
||||||
}, [options.props.location.search]);
|
}, [options.props.location.search]);
|
||||||
|
|
||||||
let result: QueryHookResult<any, any>;
|
let result: QueryHookResult<any, any>;
|
||||||
let totalCount: number;
|
|
||||||
|
let getData: (filter : ListFilterModel) => QueryHookResult<any, any>;
|
||||||
|
let getItems: () => any[];
|
||||||
|
let getCount: () => number;
|
||||||
|
|
||||||
switch (options.filterMode) {
|
switch (options.filterMode) {
|
||||||
case FilterMode.Scenes: {
|
case FilterMode.Scenes: {
|
||||||
result = StashService.useFindScenes(filter);
|
getData = (filter : ListFilterModel) => { return StashService.useFindScenes(filter); }
|
||||||
totalCount = !!result.data && !!result.data.findScenes ? result.data.findScenes.count : 0;
|
getItems = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.scenes : []; }
|
||||||
|
getCount = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.count : 0; }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FilterMode.SceneMarkers: {
|
case FilterMode.SceneMarkers: {
|
||||||
result = StashService.useFindSceneMarkers(filter);
|
getData = (filter : ListFilterModel) => { return StashService.useFindSceneMarkers(filter); }
|
||||||
totalCount = !!result.data && !!result.data.findSceneMarkers ? result.data.findSceneMarkers.count : 0;
|
getItems = () => { return !!result.data && !!result.data.findSceneMarkers ? result.data.findSceneMarkers.scene_markers : []; }
|
||||||
|
getCount = () => { return !!result.data && !!result.data.findSceneMarkers ? result.data.findSceneMarkers.count : 0; }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FilterMode.Galleries: {
|
case FilterMode.Galleries: {
|
||||||
result = StashService.useFindGalleries(filter);
|
getData = (filter : ListFilterModel) => { return StashService.useFindGalleries(filter); }
|
||||||
totalCount = !!result.data && !!result.data.findGalleries ? result.data.findGalleries.count : 0;
|
getItems = () => { return !!result.data && !!result.data.findGalleries ? result.data.findGalleries.galleries : []; }
|
||||||
|
getCount = () => { return !!result.data && !!result.data.findGalleries ? result.data.findGalleries.count : 0; }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FilterMode.Studios: {
|
case FilterMode.Studios: {
|
||||||
result = StashService.useFindStudios(filter);
|
getData = (filter : ListFilterModel) => { return StashService.useFindStudios(filter); }
|
||||||
totalCount = !!result.data && !!result.data.findStudios ? result.data.findStudios.count : 0;
|
getItems = () => { return !!result.data && !!result.data.findStudios ? result.data.findStudios.studios : []; }
|
||||||
|
getCount = () => { return !!result.data && !!result.data.findStudios ? result.data.findStudios.count : 0; }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FilterMode.Performers: {
|
case FilterMode.Performers: {
|
||||||
result = StashService.useFindPerformers(filter);
|
getData = (filter : ListFilterModel) => { return StashService.useFindPerformers(filter); }
|
||||||
totalCount = !!result.data && !!result.data.findPerformers ? result.data.findPerformers.count : 0;
|
getItems = () => { return !!result.data && !!result.data.findPerformers ? result.data.findPerformers.performers : []; }
|
||||||
|
getCount = () => { return !!result.data && !!result.data.findPerformers ? result.data.findPerformers.count : 0; }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
console.error("REMOVE DEFAULT IN LIST HOOK");
|
console.error("REMOVE DEFAULT IN LIST HOOK");
|
||||||
result = StashService.useFindScenes(filter);
|
getData = (filter : ListFilterModel) => { return StashService.useFindScenes(filter); }
|
||||||
totalCount = !!result.data && !!result.data.findScenes ? result.data.findScenes.count : 0;
|
getItems = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.scenes : []; }
|
||||||
|
getCount = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.count : 0; }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result = getData(filter);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTotalCount(getCount());
|
||||||
|
|
||||||
|
// select none when data changes
|
||||||
|
onSelectNone();
|
||||||
|
setLastClickedId(undefined);
|
||||||
|
}, [result.data])
|
||||||
|
|
||||||
// Update the query parameters when the data changes
|
// Update the query parameters when the data changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const location = Object.assign({}, options.props.history.location);
|
const location = Object.assign({}, options.props.history.location);
|
||||||
|
@ -159,6 +183,77 @@ export class ListHook {
|
||||||
setFilter(newFilter);
|
setFilter(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSelectChange(id: string, selected : boolean, shiftKey: boolean) {
|
||||||
|
if (shiftKey) {
|
||||||
|
multiSelect(id, selected);
|
||||||
|
} else {
|
||||||
|
singleSelect(id, selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function singleSelect(id: string, selected: boolean) {
|
||||||
|
setLastClickedId(id);
|
||||||
|
|
||||||
|
const newSelectedIds = _.clone(selectedIds);
|
||||||
|
if (selected) {
|
||||||
|
newSelectedIds.add(id);
|
||||||
|
} else {
|
||||||
|
newSelectedIds.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedIds(newSelectedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
function multiSelect(id: string, selected : boolean) {
|
||||||
|
let startIndex = 0;
|
||||||
|
let thisIndex = -1;
|
||||||
|
|
||||||
|
if (!!lastClickedId) {
|
||||||
|
startIndex = getItems().findIndex((item) => {
|
||||||
|
return item.id === lastClickedId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
thisIndex = getItems().findIndex((item) => {
|
||||||
|
return item.id === id;
|
||||||
|
});
|
||||||
|
|
||||||
|
selectRange(startIndex, thisIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRange(startIndex : number, endIndex : number) {
|
||||||
|
if (startIndex > endIndex) {
|
||||||
|
let tmp = startIndex;
|
||||||
|
startIndex = endIndex;
|
||||||
|
endIndex = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subset = getItems().slice(startIndex, endIndex + 1);
|
||||||
|
const newSelectedIds : Set<string> = new Set();
|
||||||
|
|
||||||
|
subset.forEach((item) => {
|
||||||
|
newSelectedIds.add(item.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedIds(newSelectedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectAll() {
|
||||||
|
const newSelectedIds : Set<string> = new Set();
|
||||||
|
getItems().forEach((item) => {
|
||||||
|
newSelectedIds.add(item.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedIds(newSelectedIds);
|
||||||
|
setLastClickedId(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectNone() {
|
||||||
|
const newSelectedIds : Set<string> = new Set();
|
||||||
|
setSelectedIds(newSelectedIds);
|
||||||
|
setLastClickedId(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
const template = (
|
const template = (
|
||||||
<div>
|
<div>
|
||||||
<ListFilter
|
<ListFilter
|
||||||
|
@ -169,11 +264,14 @@ export class ListHook {
|
||||||
onChangeDisplayMode={onChangeDisplayMode}
|
onChangeDisplayMode={onChangeDisplayMode}
|
||||||
onAddCriterion={onAddCriterion}
|
onAddCriterion={onAddCriterion}
|
||||||
onRemoveCriterion={onRemoveCriterion}
|
onRemoveCriterion={onRemoveCriterion}
|
||||||
|
onSelectAll={onSelectAll}
|
||||||
|
onSelectNone={onSelectNone}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
/>
|
/>
|
||||||
|
{options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined}
|
||||||
{result.loading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
|
{result.loading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
|
||||||
{result.error ? <h1>{result.error.message}</h1> : undefined}
|
{result.error ? <h1>{result.error.message}</h1> : undefined}
|
||||||
{options.renderContent(result, filter)}
|
{options.renderContent(result, filter, selectedIds)}
|
||||||
<Pagination
|
<Pagination
|
||||||
itemsPerPage={filter.itemsPerPage}
|
itemsPerPage={filter.itemsPerPage}
|
||||||
currentPage={filter.currentPage}
|
currentPage={filter.currentPage}
|
||||||
|
@ -183,6 +281,6 @@ export class ListHook {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return { filter, template, options };
|
return { filter, template, options, onSelectChange };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,14 @@ code {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-item label.card-select {
|
||||||
|
position: absolute;
|
||||||
|
padding-left: 15px;
|
||||||
|
margin-top: -12px;
|
||||||
|
z-index: 10;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
video.preview {
|
video.preview {
|
||||||
// height: 225px; // slows down the page
|
// height: 225px; // slows down the page
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -95,7 +103,7 @@ video.preview {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-item {
|
.filter-item, .operation-item {
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +124,7 @@ video.preview {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.filter-container, .operation-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
|
|
Loading…
Reference in New Issue