Add modes for performer/tag for bulk scene editing (#412)

This commit is contained in:
WithoutPants
2020-03-23 08:16:11 +11:00
committed by GitHub
parent acb7260824
commit d886012d74
8 changed files with 338 additions and 33 deletions

View File

@@ -39,8 +39,8 @@ mutation BulkSceneUpdate(
$rating: Int,
$studio_id: ID,
$gallery_id: ID,
$performer_ids: [ID!],
$tag_ids: [ID!]) {
$performer_ids: BulkUpdateIds,
$tag_ids: BulkUpdateIds) {
bulkSceneUpdate(input: {
ids: $ids,

View File

@@ -68,6 +68,17 @@ input SceneUpdateInput {
cover_image: String
}
enum BulkUpdateIdMode {
SET
ADD
REMOVE
}
input BulkUpdateIds {
ids: [ID!]
mode: BulkUpdateIdMode!
}
input BulkSceneUpdateInput {
clientMutationId: String
ids: [ID!]
@@ -78,8 +89,8 @@ input BulkSceneUpdateInput {
rating: Int
studio_id: ID
gallery_id: ID
performer_ids: [ID!]
tag_ids: [ID!]
performer_ids: BulkUpdateIds
tag_ids: BulkUpdateIds
}
input SceneDestroyInput {

View File

@@ -269,9 +269,14 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
// Save the performers
if wasFieldIncluded(ctx, "performer_ids") {
performerIDs, err := adjustScenePerformerIDs(tx, sceneID, *input.PerformerIds)
if err != nil {
_ = tx.Rollback()
return nil, err
}
var performerJoins []models.PerformersScenes
for _, pid := range input.PerformerIds {
performerID, _ := strconv.Atoi(pid)
for _, performerID := range performerIDs {
performerJoin := models.PerformersScenes{
PerformerID: performerID,
SceneID: sceneID,
@@ -286,9 +291,14 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
// Save the tags
if wasFieldIncluded(ctx, "tag_ids") {
tagIDs, err := adjustSceneTagIDs(tx, sceneID, *input.TagIds)
if err != nil {
_ = tx.Rollback()
return nil, err
}
var tagJoins []models.ScenesTags
for _, tid := range input.TagIds {
tagID, _ := strconv.Atoi(tid)
for _, tagID := range tagIDs {
tagJoin := models.ScenesTags{
SceneID: sceneID,
TagID: tagID,
@@ -310,6 +320,72 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
return ret, nil
}
func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
for _, idStr := range updateIDs.Ids {
id, _ := strconv.Atoi(idStr)
// look for the id in the list
foundExisting := false
for idx, existingID := range existingIDs {
if existingID == id {
if updateIDs.Mode == models.BulkUpdateIDModeRemove {
// remove from the list
existingIDs = append(existingIDs[:idx], existingIDs[idx+1:]...)
}
foundExisting = true
break
}
}
if !foundExisting && updateIDs.Mode != models.BulkUpdateIDModeRemove {
existingIDs = append(existingIDs, id)
}
}
return existingIDs
}
func adjustScenePerformerIDs(tx *sqlx.Tx, sceneID int, ids models.BulkUpdateIds) ([]int, error) {
var ret []int
jqb := models.NewJoinsQueryBuilder()
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
// adding to the joins
performerJoins, err := jqb.GetScenePerformers(sceneID, tx)
if err != nil {
return nil, err
}
for _, join := range performerJoins {
ret = append(ret, join.PerformerID)
}
}
return adjustIDs(ret, ids), nil
}
func adjustSceneTagIDs(tx *sqlx.Tx, sceneID int, ids models.BulkUpdateIds) ([]int, error) {
var ret []int
jqb := models.NewJoinsQueryBuilder()
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
// adding to the joins
tagJoins, err := jqb.GetSceneTags(sceneID, tx)
if err != nil {
return nil, err
}
for _, join := range tagJoins {
ret = append(ret, join.TagID)
}
}
return adjustIDs(ret, ids), nil
}
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
qb := models.NewSceneQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)

View File

@@ -4,11 +4,11 @@ import _ from "lodash";
import { StashService } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import {
FilterSelect,
StudioSelect,
LoadingIndicator
} from "src/components/Shared";
import { useToast } from "src/hooks";
import MultiSet from "../Shared/MultiSet";
interface IListOperationProps {
selected: GQL.SlimSceneDataFragment[];
@@ -21,7 +21,9 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
const Toast = useToast();
const [rating, setRating] = useState<string>("");
const [studioId, setStudioId] = useState<string>();
const [performerMode, setPerformerMode] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerIds, setPerformerIds] = useState<string[]>();
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [tagIds, setTagIds] = useState<string[]>();
const [updateScenes] = StashService.useBulkSceneUpdate(getSceneInput());
@@ -29,6 +31,13 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
// Network state
const [isLoading, setIsLoading] = useState(true);
function makeBulkUpdateIds(ids: string[], mode: GQL.BulkUpdateIdMode) : GQL.BulkUpdateIds {
return {
mode,
ids
};
}
function getSceneInput(): GQL.BulkSceneUpdateInput {
// need to determine what we are actually setting on each scene
const aggregateRating = getRating(props.selected);
@@ -69,27 +78,27 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
}
// if performerIds are empty
if (!performerIds || performerIds.length === 0) {
if (performerMode === GQL.BulkUpdateIdMode.Set && (!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;
sceneInput.performer_ids = makeBulkUpdateIds(performerIds || [], performerMode);
}
} else {
// if performerIds non-empty, then we are setting them
sceneInput.performer_ids = performerIds;
sceneInput.performer_ids = makeBulkUpdateIds(performerIds || [], performerMode);
}
// if tagIds non-empty, then we are setting them
if (!tagIds || tagIds.length === 0) {
if (tagMode === GQL.BulkUpdateIdMode.Set && (!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;
sceneInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
}
} else {
// if tagIds non-empty, then we are setting them
sceneInput.tag_ids = tagIds;
sceneInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
}
return sceneInput;
@@ -222,21 +231,31 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
setRating(updateRating);
setStudioId(updateStudioID);
setPerformerIds(updatePerformerIds);
setTagIds(updateTagIds);
if (performerMode === GQL.BulkUpdateIdMode.Set) {
setPerformerIds(updatePerformerIds);
}
if (tagMode === GQL.BulkUpdateIdMode.Set) {
setTagIds(updateTagIds);
}
setIsLoading(false);
}, [props.selected]);
}, [props.selected, performerMode, tagMode]);
function renderMultiSelect(
type: "performers" | "tags",
ids: string[] | undefined
) {
let mode = GQL.BulkUpdateIdMode.Add;
switch (type) {
case "performers": mode = performerMode; break;
case "tags": mode = tagMode; break;
}
return (
<FilterSelect
<MultiSet
type={type}
isMulti
isClearable={false}
onSelect={items => {
onUpdate={items => {
const itemIDs = items.map(i => i.id);
switch (type) {
case "performers":
@@ -247,7 +266,14 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
break;
}
}}
onSetMode={(mode) => {
switch (type) {
case "performers": setPerformerMode(mode); break;
case "tags": setTagMode(mode); break;
}
}}
ids={ids ?? []}
mode={mode}
/>
);
}
@@ -264,6 +290,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
<Form.Label>Rating</Form.Label>
<Form.Control
as="select"
className="btn-secondary"
value={rating}
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
setRating(event.currentTarget.value)

View File

@@ -0,0 +1,83 @@
import * as React from "react";
import * as GQL from "src/core/generated-graphql";
import { FilterSelect } from "./Select";
import { Button, InputGroup } from "react-bootstrap";
import { Icon } from "src/components/Shared";
type ValidTypes =
| GQL.SlimPerformerDataFragment
| GQL.Tag
| GQL.SlimStudioDataFragment;
interface IMultiSetProps {
type: "performers" | "studios" | "tags";
ids?: string[];
mode: GQL.BulkUpdateIdMode;
onUpdate: (items: ValidTypes[]) => void;
onSetMode: (mode: GQL.BulkUpdateIdMode) => void;
}
const MultiSet: React.FunctionComponent<IMultiSetProps> = (props: IMultiSetProps) => {
function onUpdate(items: ValidTypes[]) {
props.onUpdate(items);
}
function getModeIcon() {
switch(props.mode) {
case GQL.BulkUpdateIdMode.Set:
return "pencil-alt";
case GQL.BulkUpdateIdMode.Add:
return "plus";
case GQL.BulkUpdateIdMode.Remove:
return "times";
}
}
function getModeText() {
switch(props.mode) {
case GQL.BulkUpdateIdMode.Set:
return "Set";
case GQL.BulkUpdateIdMode.Add:
return "Add";
case GQL.BulkUpdateIdMode.Remove:
return "Remove";
}
}
function nextMode() {
switch(props.mode) {
case GQL.BulkUpdateIdMode.Set:
return GQL.BulkUpdateIdMode.Add;
case GQL.BulkUpdateIdMode.Add:
return GQL.BulkUpdateIdMode.Remove;
case GQL.BulkUpdateIdMode.Remove:
return GQL.BulkUpdateIdMode.Set;
}
}
return (
<InputGroup className="multi-set">
<InputGroup.Prepend>
<Button
size="sm"
variant="secondary"
onClick={() => props.onSetMode(nextMode())}
title={getModeText()}
>
<Icon icon={getModeIcon()} className="fa-fw" />
</Button>
</InputGroup.Prepend>
<FilterSelect
type={props.type}
isMulti
isClearable={false}
onSelect={onUpdate}
ids={props.ids ?? []}
/>
</InputGroup>
);
};
export default MultiSet;

View File

@@ -68,3 +68,10 @@
padding: 0;
}
}
.multi-set > div.input-group-prepend + div {
position: relative;
flex: 1 1;
min-width: 0;
margin-bottom: 0;
}

View File

@@ -8,11 +8,11 @@ import {
} from "@blueprintjs/core";
import React, { FunctionComponent, useEffect, 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";
import { FilterMultiSet } from "../select/FilterMultiSet";
interface IListOperationProps {
selected: GQL.SlimSceneDataFragment[],
@@ -22,7 +22,9 @@ interface IListOperationProps {
export const SceneSelectedOptions: FunctionComponent<IListOperationProps> = (props: IListOperationProps) => {
const [rating, setRating] = useState<string>("");
const [studioId, setStudioId] = useState<string | undefined>(undefined);
const [performerMode, setPerformerMode] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerIds, setPerformerIds] = useState<string[] | undefined>(undefined);
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [tagIds, setTagIds] = useState<string[] | undefined>(undefined);
const updateScenes = StashService.useBulkSceneUpdate(getSceneInput());
@@ -30,6 +32,13 @@ export const SceneSelectedOptions: FunctionComponent<IListOperationProps> = (pro
// Network state
const [isLoading, setIsLoading] = useState(false);
function makeBulkUpdateIds(ids: string[], mode: GQL.BulkUpdateIdMode) : GQL.BulkUpdateIds {
return {
mode,
ids
};
}
function getSceneInput() : GQL.BulkSceneUpdateInput {
// need to determine what we are actually setting on each scene
var aggregateRating = getRating(props.selected);
@@ -70,27 +79,27 @@ export const SceneSelectedOptions: FunctionComponent<IListOperationProps> = (pro
}
// if performerIds are empty
if (!performerIds || performerIds.length === 0) {
if (performerMode == GQL.BulkUpdateIdMode.Set && (!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;
sceneInput.performer_ids = makeBulkUpdateIds(performerIds || [], performerMode);
}
} else {
// if performerIds non-empty, then we are setting them
sceneInput.performer_ids = performerIds;
sceneInput.performer_ids = makeBulkUpdateIds(performerIds || [], performerMode);
}
// if tagIds non-empty, then we are setting them
if (!tagIds || tagIds.length === 0) {
if (tagMode == GQL.BulkUpdateIdMode.Set && (!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;
sceneInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
}
} else {
// if tagIds non-empty, then we are setting them
sceneInput.tag_ids = tagIds;
sceneInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
}
return sceneInput;
@@ -232,17 +241,28 @@ export const SceneSelectedOptions: FunctionComponent<IListOperationProps> = (pro
setRating(rating);
setStudioId(studioId);
setPerformerIds(performerIds);
setTagIds(tagIds);
if (performerMode == GQL.BulkUpdateIdMode.Set) {
setPerformerIds(performerIds);
}
if (tagMode == GQL.BulkUpdateIdMode.Set) {
setTagIds(tagIds);
}
}
useEffect(() => {
updateScenesEditState(props.selected);
}, [props.selected]);
}, [props.selected, performerMode, tagMode]);
function renderMultiSelect(type: "performers" | "tags", initialIds: string[] | undefined) {
let mode = GQL.BulkUpdateIdMode.Add;
switch (type) {
case "performers": mode = performerMode; break;
case "tags": mode = tagMode; break;
}
return (
<FilterMultiSelect
<FilterMultiSet
type={type}
onUpdate={(items) => {
const ids = items.map((i) => i.id);
@@ -251,7 +271,14 @@ export const SceneSelectedOptions: FunctionComponent<IListOperationProps> = (pro
case "tags": setTagIds(ids); break;
}
}}
onSetMode={(mode) => {
switch (type) {
case "performers": setPerformerMode(mode); break;
case "tags": setTagMode(mode); break;
}
}}
initialIds={initialIds}
mode={mode}
/>
);
}

View File

@@ -0,0 +1,74 @@
import * as React from "react";
import { ControlGroup, Button } from "@blueprintjs/core";
import * as GQL from "../../core/generated-graphql";
import { FilterMultiSelect } from "./FilterMultiSelect";
type ValidTypes =
GQL.AllPerformersForFilterAllPerformers |
GQL.AllTagsForFilterAllTags |
GQL.AllMoviesForFilterAllMovies |
GQL.AllStudiosForFilterAllStudios;
interface IFilterMultiSetProps {
type: "performers" | "studios" | "movies" | "tags";
initialIds?: string[];
mode: GQL.BulkUpdateIdMode;
onUpdate: (items: ValidTypes[]) => void;
onSetMode: (mode: GQL.BulkUpdateIdMode) => void;
}
export const FilterMultiSet: React.FunctionComponent<IFilterMultiSetProps> = (props: IFilterMultiSetProps) => {
function onUpdate(items: ValidTypes[]) {
props.onUpdate(items);
}
function getModeIcon() {
switch(props.mode) {
case GQL.BulkUpdateIdMode.Set:
return "edit";
case GQL.BulkUpdateIdMode.Add:
return "plus";
case GQL.BulkUpdateIdMode.Remove:
return "cross";
}
}
function getModeText() {
switch(props.mode) {
case GQL.BulkUpdateIdMode.Set:
return "Set";
case GQL.BulkUpdateIdMode.Add:
return "Add";
case GQL.BulkUpdateIdMode.Remove:
return "Remove";
}
}
function nextMode() {
switch(props.mode) {
case GQL.BulkUpdateIdMode.Set:
return GQL.BulkUpdateIdMode.Add;
case GQL.BulkUpdateIdMode.Add:
return GQL.BulkUpdateIdMode.Remove;
case GQL.BulkUpdateIdMode.Remove:
return GQL.BulkUpdateIdMode.Set;
}
}
return (
<ControlGroup>
<Button
icon={getModeIcon()}
minimal={true}
onClick={() => props.onSetMode(nextMode())}
title={getModeText()}
/>
<FilterMultiSelect
type={props.type}
initialIds={props.initialIds}
onUpdate={onUpdate}
/>
</ControlGroup>
);
};