mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Add bulk movie update (#2283)
* Add bulk movie edit dialog * Implement common bulk edit functions Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -22,6 +22,12 @@ mutation MovieUpdate($input: MovieUpdateInput!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation BulkMovieUpdate($input: BulkMovieUpdateInput!) {
|
||||||
|
bulkMovieUpdate(input: $input) {
|
||||||
|
...MovieData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mutation MovieDestroy($id: ID!) {
|
mutation MovieDestroy($id: ID!) {
|
||||||
movieDestroy(input: { id: $id })
|
movieDestroy(input: { id: $id })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ type Mutation {
|
|||||||
movieUpdate(input: MovieUpdateInput!): Movie
|
movieUpdate(input: MovieUpdateInput!): Movie
|
||||||
movieDestroy(input: MovieDestroyInput!): Boolean!
|
movieDestroy(input: MovieDestroyInput!): Boolean!
|
||||||
moviesDestroy(ids: [ID!]!): Boolean!
|
moviesDestroy(ids: [ID!]!): Boolean!
|
||||||
|
bulkMovieUpdate(input: BulkMovieUpdateInput!): [Movie!]
|
||||||
|
|
||||||
tagCreate(input: TagCreateInput!): Tag
|
tagCreate(input: TagCreateInput!): Tag
|
||||||
tagUpdate(input: TagUpdateInput!): Tag
|
tagUpdate(input: TagUpdateInput!): Tag
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ input MovieUpdateInput {
|
|||||||
back_image: String
|
back_image: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input BulkMovieUpdateInput {
|
||||||
|
clientMutationId: String
|
||||||
|
ids: [ID!]
|
||||||
|
rating: Int
|
||||||
|
studio_id: ID
|
||||||
|
director: String
|
||||||
|
}
|
||||||
|
|
||||||
input MovieDestroyInput {
|
input MovieDestroyInput {
|
||||||
id: ID!
|
id: ID!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -220,6 +221,71 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
|||||||
return r.getMovie(ctx, movie.ID)
|
return r.getMovie(ctx, movie.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input models.BulkMovieUpdateInput) ([]*models.Movie, error) {
|
||||||
|
movieIDs, err := utils.StringSliceToIntSlice(input.Ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedTime := time.Now()
|
||||||
|
|
||||||
|
translator := changesetTranslator{
|
||||||
|
inputMap: getUpdateInputMap(ctx),
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedMovie := models.MoviePartial{
|
||||||
|
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedMovie.Rating = translator.nullInt64(input.Rating, "rating")
|
||||||
|
updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||||
|
updatedMovie.Director = translator.nullString(input.Director, "director")
|
||||||
|
|
||||||
|
ret := []*models.Movie{}
|
||||||
|
|
||||||
|
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||||
|
qb := repo.Movie()
|
||||||
|
|
||||||
|
for _, movieID := range movieIDs {
|
||||||
|
updatedMovie.ID = movieID
|
||||||
|
|
||||||
|
existing, err := qb.Find(movieID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing == nil {
|
||||||
|
return fmt.Errorf("movie with id %d not found", movieID)
|
||||||
|
}
|
||||||
|
|
||||||
|
movie, err := qb.Update(updatedMovie)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, movie)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var newRet []*models.Movie
|
||||||
|
for _, movie := range ret {
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, plugin.MovieUpdatePost, input, translator.getFields())
|
||||||
|
|
||||||
|
movie, err = r.getMovie(ctx, movie.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newRet = append(newRet, movie)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRet, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) MovieDestroy(ctx context.Context, input models.MovieDestroyInput) (bool, error) {
|
func (r *mutationResolver) MovieDestroy(ctx context.Context, input models.MovieDestroyInput) (bool, error) {
|
||||||
id, err := strconv.Atoi(input.ID)
|
id, err := strconv.Atoi(input.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Added support for bulk-editing movies. ([#2283](https://github.com/stashapp/stash/pull/2283))
|
||||||
* Added support for filtering scenes, images and galleries featuring favourite performers and performer age at time of production. ([#2257](https://github.com/stashapp/stash/pull/2257))
|
* Added support for filtering scenes, images and galleries featuring favourite performers and performer age at time of production. ([#2257](https://github.com/stashapp/stash/pull/2257))
|
||||||
* Added support for filtering scenes with (or without) phash duplicates. ([#2257](https://github.com/stashapp/stash/pull/2257))
|
* Added support for filtering scenes with (or without) phash duplicates. ([#2257](https://github.com/stashapp/stash/pull/2257))
|
||||||
* Added support for sorting scenes by phash. ([#2257](https://github.com/stashapp/stash/pull/2257))
|
* Added support for sorting scenes by phash. ([#2257](https://github.com/stashapp/stash/pull/2257))
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ import { useToast } from "src/hooks";
|
|||||||
import { FormUtils } from "src/utils";
|
import { FormUtils } from "src/utils";
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import MultiSet from "../Shared/MultiSet";
|
||||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
||||||
|
import {
|
||||||
|
getAggregateInputIDs,
|
||||||
|
getAggregateInputValue,
|
||||||
|
getAggregatePerformerIds,
|
||||||
|
getAggregateRating,
|
||||||
|
getAggregateStudioId,
|
||||||
|
getAggregateTagIds,
|
||||||
|
} from "src/utils/bulkUpdate";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
selected: GQL.SlimGalleryDataFragment[];
|
selected: GQL.SlimGalleryDataFragment[];
|
||||||
@@ -42,22 +50,12 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
const checkboxRef = React.createRef<HTMLInputElement>();
|
const checkboxRef = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
function makeBulkUpdateIds(
|
|
||||||
ids: string[],
|
|
||||||
mode: GQL.BulkUpdateIdMode
|
|
||||||
): GQL.BulkUpdateIds {
|
|
||||||
return {
|
|
||||||
mode,
|
|
||||||
ids,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGalleryInput(): GQL.BulkGalleryUpdateInput {
|
function getGalleryInput(): GQL.BulkGalleryUpdateInput {
|
||||||
// need to determine what we are actually setting on each gallery
|
// need to determine what we are actually setting on each gallery
|
||||||
const aggregateRating = getRating(props.selected);
|
const aggregateRating = getAggregateRating(props.selected);
|
||||||
const aggregateStudioId = getStudioId(props.selected);
|
const aggregateStudioId = getAggregateStudioId(props.selected);
|
||||||
const aggregatePerformerIds = getPerformerIds(props.selected);
|
const aggregatePerformerIds = getAggregatePerformerIds(props.selected);
|
||||||
const aggregateTagIds = getTagIds(props.selected);
|
const aggregateTagIds = getAggregateTagIds(props.selected);
|
||||||
|
|
||||||
const galleryInput: GQL.BulkGalleryUpdateInput = {
|
const galleryInput: GQL.BulkGalleryUpdateInput = {
|
||||||
ids: props.selected.map((gallery) => {
|
ids: props.selected.map((gallery) => {
|
||||||
@@ -65,67 +63,22 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// if rating is undefined
|
galleryInput.rating = getAggregateInputValue(rating, aggregateRating);
|
||||||
if (rating === undefined) {
|
galleryInput.studio_id = getAggregateInputValue(
|
||||||
// and all galleries have the same rating, then we are unsetting the rating.
|
studioId,
|
||||||
if (aggregateRating) {
|
aggregateStudioId
|
||||||
// null to unset rating
|
|
||||||
galleryInput.rating = null;
|
|
||||||
}
|
|
||||||
// otherwise not setting the rating
|
|
||||||
} else {
|
|
||||||
// if rating is set, then we are setting the rating for all
|
|
||||||
galleryInput.rating = rating;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if studioId is undefined
|
|
||||||
if (studioId === undefined) {
|
|
||||||
// and all galleries have the same studioId,
|
|
||||||
// then unset the studioId, otherwise ignoring studioId
|
|
||||||
if (aggregateStudioId) {
|
|
||||||
// null to unset studio_id
|
|
||||||
galleryInput.studio_id = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if studioId is set, then we are setting it
|
|
||||||
galleryInput.studio_id = studioId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if performerIds are empty
|
|
||||||
if (
|
|
||||||
performerMode === GQL.BulkUpdateIdMode.Set &&
|
|
||||||
(!performerIds || performerIds.length === 0)
|
|
||||||
) {
|
|
||||||
// and all galleries have the same ids,
|
|
||||||
if (aggregatePerformerIds.length > 0) {
|
|
||||||
// then unset the performerIds, otherwise ignore
|
|
||||||
galleryInput.performer_ids = makeBulkUpdateIds(
|
|
||||||
performerIds || [],
|
|
||||||
performerMode
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if performerIds non-empty, then we are setting them
|
|
||||||
galleryInput.performer_ids = makeBulkUpdateIds(
|
|
||||||
performerIds || [],
|
|
||||||
performerMode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if tagIds non-empty, then we are setting them
|
galleryInput.performer_ids = getAggregateInputIDs(
|
||||||
if (
|
performerMode,
|
||||||
tagMode === GQL.BulkUpdateIdMode.Set &&
|
performerIds,
|
||||||
(!tagIds || tagIds.length === 0)
|
aggregatePerformerIds
|
||||||
) {
|
);
|
||||||
// and all galleries have the same ids,
|
galleryInput.tag_ids = getAggregateInputIDs(
|
||||||
if (aggregateTagIds.length > 0) {
|
tagMode,
|
||||||
// then unset the tagIds, otherwise ignore
|
tagIds,
|
||||||
galleryInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
|
aggregateTagIds
|
||||||
}
|
);
|
||||||
} else {
|
|
||||||
// if tagIds non-empty, then we are setting them
|
|
||||||
galleryInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organized !== undefined) {
|
if (organized !== undefined) {
|
||||||
galleryInput.organized = organized;
|
galleryInput.organized = organized;
|
||||||
@@ -157,85 +110,6 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRating(state: GQL.SlimGalleryDataFragment[]) {
|
|
||||||
let ret: number | undefined;
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((gallery) => {
|
|
||||||
if (first) {
|
|
||||||
ret = gallery.rating ?? undefined;
|
|
||||||
first = false;
|
|
||||||
} else if (ret !== gallery.rating) {
|
|
||||||
ret = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStudioId(state: GQL.SlimGalleryDataFragment[]) {
|
|
||||||
let ret: string | undefined;
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((gallery) => {
|
|
||||||
if (first) {
|
|
||||||
ret = gallery?.studio?.id;
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
const studio = gallery?.studio?.id;
|
|
||||||
if (ret !== studio) {
|
|
||||||
ret = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPerformerIds(state: GQL.SlimGalleryDataFragment[]) {
|
|
||||||
let ret: string[] = [];
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((gallery) => {
|
|
||||||
if (first) {
|
|
||||||
ret = gallery.performers
|
|
||||||
? gallery.performers.map((p) => p.id).sort()
|
|
||||||
: [];
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
const perfIds = gallery.performers
|
|
||||||
? gallery.performers.map((p) => p.id).sort()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!_.isEqual(ret, perfIds)) {
|
|
||||||
ret = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTagIds(state: GQL.SlimGalleryDataFragment[]) {
|
|
||||||
let ret: string[] = [];
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((gallery) => {
|
|
||||||
if (first) {
|
|
||||||
ret = gallery.tags ? gallery.tags.map((t) => t.id).sort() : [];
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
const tIds = gallery.tags ? gallery.tags.map((t) => t.id).sort() : [];
|
|
||||||
|
|
||||||
if (!_.isEqual(ret, tIds)) {
|
|
||||||
ret = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const state = props.selected;
|
const state = props.selected;
|
||||||
let updateRating: number | undefined;
|
let updateRating: number | undefined;
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ import { useToast } from "src/hooks";
|
|||||||
import { FormUtils } from "src/utils";
|
import { FormUtils } from "src/utils";
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import MultiSet from "../Shared/MultiSet";
|
||||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
||||||
|
import {
|
||||||
|
getAggregateInputIDs,
|
||||||
|
getAggregateInputValue,
|
||||||
|
getAggregatePerformerIds,
|
||||||
|
getAggregateRating,
|
||||||
|
getAggregateStudioId,
|
||||||
|
getAggregateTagIds,
|
||||||
|
} from "src/utils/bulkUpdate";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
selected: GQL.SlimImageDataFragment[];
|
selected: GQL.SlimImageDataFragment[];
|
||||||
@@ -42,22 +50,12 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
const checkboxRef = React.createRef<HTMLInputElement>();
|
const checkboxRef = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
function makeBulkUpdateIds(
|
|
||||||
ids: string[],
|
|
||||||
mode: GQL.BulkUpdateIdMode
|
|
||||||
): GQL.BulkUpdateIds {
|
|
||||||
return {
|
|
||||||
mode,
|
|
||||||
ids,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImageInput(): GQL.BulkImageUpdateInput {
|
function getImageInput(): GQL.BulkImageUpdateInput {
|
||||||
// need to determine what we are actually setting on each image
|
// need to determine what we are actually setting on each image
|
||||||
const aggregateRating = getRating(props.selected);
|
const aggregateRating = getAggregateRating(props.selected);
|
||||||
const aggregateStudioId = getStudioId(props.selected);
|
const aggregateStudioId = getAggregateStudioId(props.selected);
|
||||||
const aggregatePerformerIds = getPerformerIds(props.selected);
|
const aggregatePerformerIds = getAggregatePerformerIds(props.selected);
|
||||||
const aggregateTagIds = getTagIds(props.selected);
|
const aggregateTagIds = getAggregateTagIds(props.selected);
|
||||||
|
|
||||||
const imageInput: GQL.BulkImageUpdateInput = {
|
const imageInput: GQL.BulkImageUpdateInput = {
|
||||||
ids: props.selected.map((image) => {
|
ids: props.selected.map((image) => {
|
||||||
@@ -65,67 +63,15 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// if rating is undefined
|
imageInput.rating = getAggregateInputValue(rating, aggregateRating);
|
||||||
if (rating === undefined) {
|
imageInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||||
// and all images have the same rating, then we are unsetting the rating.
|
|
||||||
if (aggregateRating) {
|
|
||||||
// null rating to unset it
|
|
||||||
imageInput.rating = null;
|
|
||||||
}
|
|
||||||
// otherwise not setting the rating
|
|
||||||
} else {
|
|
||||||
// if rating is set, then we are setting the rating for all
|
|
||||||
imageInput.rating = rating;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if studioId is undefined
|
imageInput.performer_ids = getAggregateInputIDs(
|
||||||
if (studioId === undefined) {
|
performerMode,
|
||||||
// and all images have the same studioId,
|
performerIds,
|
||||||
// then unset the studioId, otherwise ignoring studioId
|
aggregatePerformerIds
|
||||||
if (aggregateStudioId) {
|
|
||||||
// null studio_id to unset it
|
|
||||||
imageInput.studio_id = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if studioId is set, then we are setting it
|
|
||||||
imageInput.studio_id = studioId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if performerIds are empty
|
|
||||||
if (
|
|
||||||
performerMode === GQL.BulkUpdateIdMode.Set &&
|
|
||||||
(!performerIds || performerIds.length === 0)
|
|
||||||
) {
|
|
||||||
// and all images have the same ids,
|
|
||||||
if (aggregatePerformerIds.length > 0) {
|
|
||||||
// then unset the performerIds, otherwise ignore
|
|
||||||
imageInput.performer_ids = makeBulkUpdateIds(
|
|
||||||
performerIds || [],
|
|
||||||
performerMode
|
|
||||||
);
|
);
|
||||||
}
|
imageInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
|
||||||
} else {
|
|
||||||
// if performerIds non-empty, then we are setting them
|
|
||||||
imageInput.performer_ids = makeBulkUpdateIds(
|
|
||||||
performerIds || [],
|
|
||||||
performerMode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if tagIds non-empty, then we are setting them
|
|
||||||
if (
|
|
||||||
tagMode === GQL.BulkUpdateIdMode.Set &&
|
|
||||||
(!tagIds || tagIds.length === 0)
|
|
||||||
) {
|
|
||||||
// and all images have the same ids,
|
|
||||||
if (aggregateTagIds.length > 0) {
|
|
||||||
// then unset the tagIds, otherwise ignore
|
|
||||||
imageInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if tagIds non-empty, then we are setting them
|
|
||||||
imageInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organized !== undefined) {
|
if (organized !== undefined) {
|
||||||
imageInput.organized = organized;
|
imageInput.organized = organized;
|
||||||
@@ -155,83 +101,6 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRating(state: GQL.SlimImageDataFragment[]) {
|
|
||||||
let ret: number | undefined;
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((image: GQL.SlimImageDataFragment) => {
|
|
||||||
if (first) {
|
|
||||||
ret = image.rating ?? undefined;
|
|
||||||
first = false;
|
|
||||||
} else if (ret !== image.rating) {
|
|
||||||
ret = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStudioId(state: GQL.SlimImageDataFragment[]) {
|
|
||||||
let ret: string | undefined;
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((image: GQL.SlimImageDataFragment) => {
|
|
||||||
if (first) {
|
|
||||||
ret = image?.studio?.id;
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
const studio = image?.studio?.id;
|
|
||||||
if (ret !== studio) {
|
|
||||||
ret = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPerformerIds(state: GQL.SlimImageDataFragment[]) {
|
|
||||||
let ret: string[] = [];
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((image: GQL.SlimImageDataFragment) => {
|
|
||||||
if (first) {
|
|
||||||
ret = image.performers ? image.performers.map((p) => p.id).sort() : [];
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
const perfIds = image.performers
|
|
||||||
? image.performers.map((p) => p.id).sort()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!_.isEqual(ret, perfIds)) {
|
|
||||||
ret = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTagIds(state: GQL.SlimImageDataFragment[]) {
|
|
||||||
let ret: string[] = [];
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((image: GQL.SlimImageDataFragment) => {
|
|
||||||
if (first) {
|
|
||||||
ret = image.tags ? image.tags.map((t) => t.id).sort() : [];
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
const tIds = image.tags ? image.tags.map((t) => t.id).sort() : [];
|
|
||||||
|
|
||||||
if (!_.isEqual(ret, tIds)) {
|
|
||||||
ret = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const state = props.selected;
|
const state = props.selected;
|
||||||
let updateRating: number | undefined;
|
let updateRating: number | undefined;
|
||||||
|
|||||||
162
ui/v2.5/src/components/Movies/EditMoviesDialog.tsx
Normal file
162
ui/v2.5/src/components/Movies/EditMoviesDialog.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Form, Col, Row } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import { useBulkMovieUpdate } from "src/core/StashService";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { Modal, StudioSelect } from "src/components/Shared";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
import { FormUtils } from "src/utils";
|
||||||
|
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
||||||
|
import {
|
||||||
|
getAggregateInputValue,
|
||||||
|
getAggregateRating,
|
||||||
|
getAggregateStudioId,
|
||||||
|
} from "src/utils/bulkUpdate";
|
||||||
|
|
||||||
|
interface IListOperationProps {
|
||||||
|
selected: GQL.MovieDataFragment[];
|
||||||
|
onClose: (applied: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
||||||
|
props: IListOperationProps
|
||||||
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const Toast = useToast();
|
||||||
|
const [rating, setRating] = useState<number | undefined>();
|
||||||
|
const [studioId, setStudioId] = useState<string | undefined>();
|
||||||
|
const [director, setDirector] = useState<string | undefined>();
|
||||||
|
|
||||||
|
const [updateMovies] = useBulkMovieUpdate(getMovieInput());
|
||||||
|
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
|
function getMovieInput(): GQL.BulkMovieUpdateInput {
|
||||||
|
const aggregateRating = getAggregateRating(props.selected);
|
||||||
|
const aggregateStudioId = getAggregateStudioId(props.selected);
|
||||||
|
|
||||||
|
const movieInput: GQL.BulkMovieUpdateInput = {
|
||||||
|
ids: props.selected.map((movie) => movie.id),
|
||||||
|
director,
|
||||||
|
};
|
||||||
|
|
||||||
|
// if rating is undefined
|
||||||
|
movieInput.rating = getAggregateInputValue(rating, aggregateRating);
|
||||||
|
movieInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||||
|
|
||||||
|
return movieInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSave() {
|
||||||
|
setIsUpdating(true);
|
||||||
|
try {
|
||||||
|
await updateMovies();
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{
|
||||||
|
entity: intl.formatMessage({ id: "movies" }).toLocaleLowerCase(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
props.onClose(true);
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const state = props.selected;
|
||||||
|
let updateRating: number | undefined;
|
||||||
|
let updateStudioId: string | undefined;
|
||||||
|
let updateDirector: string | undefined;
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
state.forEach((movie: GQL.MovieDataFragment) => {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
updateRating = movie.rating ?? undefined;
|
||||||
|
updateStudioId = movie.studio?.id ?? undefined;
|
||||||
|
updateDirector = movie.director ?? undefined;
|
||||||
|
} else {
|
||||||
|
if (movie.rating !== updateRating) {
|
||||||
|
updateRating = undefined;
|
||||||
|
}
|
||||||
|
if (movie.studio?.id !== updateStudioId) {
|
||||||
|
updateStudioId = undefined;
|
||||||
|
}
|
||||||
|
if (movie.director !== updateDirector) {
|
||||||
|
updateDirector = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRating(updateRating);
|
||||||
|
setStudioId(updateStudioId);
|
||||||
|
setDirector(updateDirector);
|
||||||
|
}, [props.selected]);
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show
|
||||||
|
icon="pencil-alt"
|
||||||
|
header="Edit Movies"
|
||||||
|
accept={{
|
||||||
|
onClick: onSave,
|
||||||
|
text: intl.formatMessage({ id: "actions.apply" }),
|
||||||
|
}}
|
||||||
|
cancel={{
|
||||||
|
onClick: () => props.onClose(false),
|
||||||
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
|
variant: "secondary",
|
||||||
|
}}
|
||||||
|
isRunning={isUpdating}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<Form.Group controlId="rating" as={Row}>
|
||||||
|
{FormUtils.renderLabel({
|
||||||
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
|
})}
|
||||||
|
<Col xs={9}>
|
||||||
|
<RatingStars
|
||||||
|
value={rating}
|
||||||
|
onSetRating={(value) => setRating(value)}
|
||||||
|
disabled={isUpdating}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="studio" as={Row}>
|
||||||
|
{FormUtils.renderLabel({
|
||||||
|
title: intl.formatMessage({ id: "studio" }),
|
||||||
|
})}
|
||||||
|
<Col xs={9}>
|
||||||
|
<StudioSelect
|
||||||
|
onSelect={(items) =>
|
||||||
|
setStudioId(items.length > 0 ? items[0]?.id : undefined)
|
||||||
|
}
|
||||||
|
ids={studioId ? [studioId] : []}
|
||||||
|
isDisabled={isUpdating}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="director">
|
||||||
|
<Form.Label>
|
||||||
|
<FormattedMessage id="director" />
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
className="input-control"
|
||||||
|
type="text"
|
||||||
|
value={director}
|
||||||
|
onChange={(event) => setDirector(event.currentTarget.value)}
|
||||||
|
placeholder={intl.formatMessage({ id: "director" })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return render();
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import { useHistory } from "react-router-dom";
|
|||||||
import {
|
import {
|
||||||
FindMoviesQueryResult,
|
FindMoviesQueryResult,
|
||||||
SlimMovieDataFragment,
|
SlimMovieDataFragment,
|
||||||
|
MovieDataFragment,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
} from "src/hooks/ListHook";
|
} from "src/hooks/ListHook";
|
||||||
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
||||||
import { MovieCard } from "./MovieCard";
|
import { MovieCard } from "./MovieCard";
|
||||||
|
import { EditMoviesDialog } from "./EditMoviesDialog";
|
||||||
|
|
||||||
interface IMovieList {
|
interface IMovieList {
|
||||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||||
@@ -57,6 +59,17 @@ export const MovieList: React.FC<IMovieList> = ({ filterHook }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function renderEditDialog(
|
||||||
|
selectedMovies: MovieDataFragment[],
|
||||||
|
onClose: (applied: boolean) => void
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EditMoviesDialog selected={selectedMovies} onClose={onClose} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const renderDeleteDialog = (
|
const renderDeleteDialog = (
|
||||||
selectedMovies: SlimMovieDataFragment[],
|
selectedMovies: SlimMovieDataFragment[],
|
||||||
onClose: (confirmed: boolean) => void
|
onClose: (confirmed: boolean) => void
|
||||||
@@ -76,6 +89,7 @@ export const MovieList: React.FC<IMovieList> = ({ filterHook }) => {
|
|||||||
otherOperations,
|
otherOperations,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
persistState: PersistanceLevel.ALL,
|
persistState: PersistanceLevel.ALL,
|
||||||
|
renderEditDialog,
|
||||||
renderDeleteDialog,
|
renderDeleteDialog,
|
||||||
filterHook,
|
filterHook,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import { useToast } from "src/hooks";
|
|||||||
import { FormUtils } from "src/utils";
|
import { FormUtils } from "src/utils";
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import MultiSet from "../Shared/MultiSet";
|
||||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
||||||
|
import {
|
||||||
|
getAggregateInputIDs,
|
||||||
|
getAggregateInputValue,
|
||||||
|
getAggregateRating,
|
||||||
|
getAggregateTagIds,
|
||||||
|
} from "src/utils/bulkUpdate";
|
||||||
import { genderStrings, stringToGender } from "src/utils/gender";
|
import { genderStrings, stringToGender } from "src/utils/gender";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
@@ -46,20 +52,10 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
const checkboxRef = React.createRef<HTMLInputElement>();
|
const checkboxRef = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
function makeBulkUpdateIds(
|
|
||||||
ids: string[],
|
|
||||||
mode: GQL.BulkUpdateIdMode
|
|
||||||
): GQL.BulkUpdateIds {
|
|
||||||
return {
|
|
||||||
mode,
|
|
||||||
ids,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPerformerInput(): GQL.BulkPerformerUpdateInput {
|
function getPerformerInput(): GQL.BulkPerformerUpdateInput {
|
||||||
// need to determine what we are actually setting on each performer
|
// need to determine what we are actually setting on each performer
|
||||||
const aggregateTagIds = getTagIds(props.selected);
|
const aggregateTagIds = getAggregateTagIds(props.selected);
|
||||||
const aggregateRating = getRating(props.selected);
|
const aggregateRating = getAggregateRating(props.selected);
|
||||||
|
|
||||||
const performerInput: GQL.BulkPerformerUpdateInput = {
|
const performerInput: GQL.BulkPerformerUpdateInput = {
|
||||||
ids: props.selected.map((performer) => {
|
ids: props.selected.map((performer) => {
|
||||||
@@ -67,33 +63,13 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// if rating is undefined
|
performerInput.rating = getAggregateInputValue(rating, aggregateRating);
|
||||||
if (rating === undefined) {
|
|
||||||
// and all galleries have the same rating, then we are unsetting the rating.
|
|
||||||
if (aggregateRating) {
|
|
||||||
// null to unset rating
|
|
||||||
performerInput.rating = null;
|
|
||||||
}
|
|
||||||
// otherwise not setting the rating
|
|
||||||
} else {
|
|
||||||
// if rating is set, then we are setting the rating for all
|
|
||||||
performerInput.rating = rating;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if tagIds non-empty, then we are setting them
|
performerInput.tag_ids = getAggregateInputIDs(
|
||||||
if (
|
tagMode,
|
||||||
tagMode === GQL.BulkUpdateIdMode.Set &&
|
tagIds,
|
||||||
(!tagIds || tagIds.length === 0)
|
aggregateTagIds
|
||||||
) {
|
);
|
||||||
// and all performers have the same ids,
|
|
||||||
if (aggregateTagIds.length > 0) {
|
|
||||||
// then unset the tagIds, otherwise ignore
|
|
||||||
performerInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if tagIds non-empty, then we are setting them
|
|
||||||
performerInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
performerInput.favorite = favorite;
|
performerInput.favorite = favorite;
|
||||||
performerInput.ethnicity = ethnicity;
|
performerInput.ethnicity = ethnicity;
|
||||||
@@ -130,44 +106,6 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTagIds(state: GQL.SlimPerformerDataFragment[]) {
|
|
||||||
let ret: string[] = [];
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((performer: GQL.SlimPerformerDataFragment) => {
|
|
||||||
if (first) {
|
|
||||||
ret = performer.tags ? performer.tags.map((t) => t.id).sort() : [];
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
const tIds = performer.tags
|
|
||||||
? performer.tags.map((t) => t.id).sort()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!_.isEqual(ret, tIds)) {
|
|
||||||
ret = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRating(state: GQL.SlimPerformerDataFragment[]) {
|
|
||||||
let ret: number | undefined;
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((performer) => {
|
|
||||||
if (first) {
|
|
||||||
ret = performer.rating ?? undefined;
|
|
||||||
first = false;
|
|
||||||
} else if (ret !== performer.rating) {
|
|
||||||
ret = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const state = props.selected;
|
const state = props.selected;
|
||||||
let updateTagIds: string[] = [];
|
let updateTagIds: string[] = [];
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import { useToast } from "src/hooks";
|
|||||||
import { FormUtils } from "src/utils";
|
import { FormUtils } from "src/utils";
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import MultiSet from "../Shared/MultiSet";
|
||||||
import { RatingStars } from "./SceneDetails/RatingStars";
|
import { RatingStars } from "./SceneDetails/RatingStars";
|
||||||
|
import {
|
||||||
|
getAggregateInputIDs,
|
||||||
|
getAggregateInputValue,
|
||||||
|
getAggregateMovieIds,
|
||||||
|
getAggregatePerformerIds,
|
||||||
|
getAggregateRating,
|
||||||
|
getAggregateStudioId,
|
||||||
|
getAggregateTagIds,
|
||||||
|
} from "src/utils/bulkUpdate";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
selected: GQL.SlimSceneDataFragment[];
|
selected: GQL.SlimSceneDataFragment[];
|
||||||
@@ -47,23 +56,13 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
const checkboxRef = React.createRef<HTMLInputElement>();
|
const checkboxRef = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
function makeBulkUpdateIds(
|
|
||||||
ids: string[],
|
|
||||||
mode: GQL.BulkUpdateIdMode
|
|
||||||
): GQL.BulkUpdateIds {
|
|
||||||
return {
|
|
||||||
mode,
|
|
||||||
ids,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSceneInput(): GQL.BulkSceneUpdateInput {
|
function getSceneInput(): GQL.BulkSceneUpdateInput {
|
||||||
// need to determine what we are actually setting on each scene
|
// need to determine what we are actually setting on each scene
|
||||||
const aggregateRating = getRating(props.selected);
|
const aggregateRating = getAggregateRating(props.selected);
|
||||||
const aggregateStudioId = getStudioId(props.selected);
|
const aggregateStudioId = getAggregateStudioId(props.selected);
|
||||||
const aggregatePerformerIds = getPerformerIds(props.selected);
|
const aggregatePerformerIds = getAggregatePerformerIds(props.selected);
|
||||||
const aggregateTagIds = getTagIds(props.selected);
|
const aggregateTagIds = getAggregateTagIds(props.selected);
|
||||||
const aggregateMovieIds = getMovieIds(props.selected);
|
const aggregateMovieIds = getAggregateMovieIds(props.selected);
|
||||||
|
|
||||||
const sceneInput: GQL.BulkSceneUpdateInput = {
|
const sceneInput: GQL.BulkSceneUpdateInput = {
|
||||||
ids: props.selected.map((scene) => {
|
ids: props.selected.map((scene) => {
|
||||||
@@ -71,82 +70,20 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// if rating is undefined
|
sceneInput.rating = getAggregateInputValue(rating, aggregateRating);
|
||||||
if (rating === undefined) {
|
sceneInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||||
// and all scenes have the same rating, then we are unsetting the rating.
|
|
||||||
if (aggregateRating) {
|
|
||||||
// null rating unsets it
|
|
||||||
sceneInput.rating = null;
|
|
||||||
}
|
|
||||||
// otherwise not setting the rating
|
|
||||||
} else {
|
|
||||||
// if rating is set, then we are setting the rating for all
|
|
||||||
sceneInput.rating = rating;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if studioId is undefined
|
sceneInput.performer_ids = getAggregateInputIDs(
|
||||||
if (studioId === undefined) {
|
performerMode,
|
||||||
// and all scenes have the same studioId,
|
performerIds,
|
||||||
// then unset the studioId, otherwise ignoring studioId
|
aggregatePerformerIds
|
||||||
if (aggregateStudioId) {
|
|
||||||
// null studio_id unsets it
|
|
||||||
sceneInput.studio_id = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if studioId is set, then we are setting it
|
|
||||||
sceneInput.studio_id = studioId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if performerIds are empty
|
|
||||||
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 = makeBulkUpdateIds(
|
|
||||||
performerIds || [],
|
|
||||||
performerMode
|
|
||||||
);
|
);
|
||||||
}
|
sceneInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
|
||||||
} else {
|
sceneInput.movie_ids = getAggregateInputIDs(
|
||||||
// if performerIds non-empty, then we are setting them
|
movieMode,
|
||||||
sceneInput.performer_ids = makeBulkUpdateIds(
|
movieIds,
|
||||||
performerIds || [],
|
aggregateMovieIds
|
||||||
performerMode
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// if tagIds non-empty, then we are setting them
|
|
||||||
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 = makeBulkUpdateIds(tagIds || [], tagMode);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if tagIds non-empty, then we are setting them
|
|
||||||
sceneInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if movieIds non-empty, then we are setting them
|
|
||||||
if (
|
|
||||||
movieMode === GQL.BulkUpdateIdMode.Set &&
|
|
||||||
(!movieIds || movieIds.length === 0)
|
|
||||||
) {
|
|
||||||
// and all scenes have the same ids,
|
|
||||||
if (aggregateMovieIds.length > 0) {
|
|
||||||
// then unset the movieIds, otherwise ignore
|
|
||||||
sceneInput.movie_ids = makeBulkUpdateIds(movieIds || [], movieMode);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if movieIds non-empty, then we are setting them
|
|
||||||
sceneInput.movie_ids = makeBulkUpdateIds(movieIds || [], movieMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organized !== undefined) {
|
if (organized !== undefined) {
|
||||||
sceneInput.organized = organized;
|
sceneInput.organized = organized;
|
||||||
@@ -172,105 +109,6 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRating(state: GQL.SlimSceneDataFragment[]) {
|
|
||||||
let ret: number | undefined;
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
|
||||||
if (first) {
|
|
||||||
ret = scene.rating ?? undefined;
|
|
||||||
first = false;
|
|
||||||
} else if (ret !== scene.rating) {
|
|
||||||
ret = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStudioId(state: GQL.SlimSceneDataFragment[]) {
|
|
||||||
let ret: string | undefined;
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
|
||||||
if (first) {
|
|
||||||
ret = scene?.studio?.id;
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
const studio = scene?.studio?.id;
|
|
||||||
if (ret !== studio) {
|
|
||||||
ret = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPerformerIds(state: GQL.SlimSceneDataFragment[]) {
|
|
||||||
let ret: string[] = [];
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
|
||||||
if (first) {
|
|
||||||
ret = scene.performers ? scene.performers.map((p) => p.id).sort() : [];
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
const perfIds = scene.performers
|
|
||||||
? scene.performers.map((p) => p.id).sort()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!_.isEqual(ret, perfIds)) {
|
|
||||||
ret = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTagIds(state: GQL.SlimSceneDataFragment[]) {
|
|
||||||
let ret: string[] = [];
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
|
||||||
if (first) {
|
|
||||||
ret = scene.tags ? scene.tags.map((t) => t.id).sort() : [];
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
const tIds = scene.tags ? scene.tags.map((t) => t.id).sort() : [];
|
|
||||||
|
|
||||||
if (!_.isEqual(ret, tIds)) {
|
|
||||||
ret = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMovieIds(state: GQL.SlimSceneDataFragment[]) {
|
|
||||||
let ret: string[] = [];
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
|
||||||
if (first) {
|
|
||||||
ret = scene.movies ? scene.movies.map((m) => m.movie.id).sort() : [];
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
const mIds = scene.movies
|
|
||||||
? scene.movies.map((m) => m.movie.id).sort()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!_.isEqual(ret, mIds)) {
|
|
||||||
ret = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const state = props.selected;
|
const state = props.selected;
|
||||||
let updateRating: number | undefined;
|
let updateRating: number | undefined;
|
||||||
|
|||||||
@@ -659,6 +659,14 @@ export const useMovieUpdate = () =>
|
|||||||
update: deleteCache(movieMutationImpactedQueries),
|
update: deleteCache(movieMutationImpactedQueries),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useBulkMovieUpdate = (input: GQL.BulkMovieUpdateInput) =>
|
||||||
|
GQL.useBulkMovieUpdateMutation({
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
update: deleteCache(movieMutationImpactedQueries),
|
||||||
|
});
|
||||||
|
|
||||||
export const useMovieDestroy = (input: GQL.MovieDestroyInput) =>
|
export const useMovieDestroy = (input: GQL.MovieDestroyInput) =>
|
||||||
GQL.useMovieDestroyMutation({
|
GQL.useMovieDestroyMutation({
|
||||||
variables: input,
|
variables: input,
|
||||||
|
|||||||
175
ui/v2.5/src/utils/bulkUpdate.ts
Normal file
175
ui/v2.5/src/utils/bulkUpdate.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
interface IHasRating {
|
||||||
|
rating?: GQL.Maybe<number> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAggregateRating(state: IHasRating[]) {
|
||||||
|
let ret: number | undefined;
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
state.forEach((o) => {
|
||||||
|
if (first) {
|
||||||
|
ret = o.rating ?? undefined;
|
||||||
|
first = false;
|
||||||
|
} else if (ret !== o.rating) {
|
||||||
|
ret = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IHasID {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IHasStudio {
|
||||||
|
studio?: GQL.Maybe<IHasID> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAggregateStudioId(state: IHasStudio[]) {
|
||||||
|
let ret: string | undefined;
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
state.forEach((o) => {
|
||||||
|
if (first) {
|
||||||
|
ret = o?.studio?.id;
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
const studio = o?.studio?.id;
|
||||||
|
if (ret !== studio) {
|
||||||
|
ret = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IHasPerformers {
|
||||||
|
performers: IHasID[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAggregatePerformerIds(state: IHasPerformers[]) {
|
||||||
|
let ret: string[] = [];
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
state.forEach((o) => {
|
||||||
|
if (first) {
|
||||||
|
ret = o.performers ? o.performers.map((p) => p.id).sort() : [];
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
const perfIds = o.performers ? o.performers.map((p) => p.id).sort() : [];
|
||||||
|
|
||||||
|
if (!_.isEqual(ret, perfIds)) {
|
||||||
|
ret = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IHasTags {
|
||||||
|
tags: IHasID[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAggregateTagIds(state: IHasTags[]) {
|
||||||
|
let ret: string[] = [];
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
state.forEach((o) => {
|
||||||
|
if (first) {
|
||||||
|
ret = o.tags ? o.tags.map((t) => t.id).sort() : [];
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
const tIds = o.tags ? o.tags.map((t) => t.id).sort() : [];
|
||||||
|
|
||||||
|
if (!_.isEqual(ret, tIds)) {
|
||||||
|
ret = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMovie {
|
||||||
|
movie: IHasID;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IHasMovies {
|
||||||
|
movies: IMovie[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAggregateMovieIds(state: IHasMovies[]) {
|
||||||
|
let ret: string[] = [];
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
state.forEach((o) => {
|
||||||
|
if (first) {
|
||||||
|
ret = o.movies ? o.movies.map((m) => m.movie.id).sort() : [];
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
const mIds = o.movies ? o.movies.map((m) => m.movie.id).sort() : [];
|
||||||
|
|
||||||
|
if (!_.isEqual(ret, mIds)) {
|
||||||
|
ret = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeBulkUpdateIds(
|
||||||
|
ids: string[],
|
||||||
|
mode: GQL.BulkUpdateIdMode
|
||||||
|
): GQL.BulkUpdateIds {
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
ids,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAggregateInputValue<V>(
|
||||||
|
inputValue: V | null | undefined,
|
||||||
|
aggregateValue: V | null | undefined
|
||||||
|
) {
|
||||||
|
if (inputValue === undefined) {
|
||||||
|
// and all objects have the same value, then we are unsetting the value.
|
||||||
|
if (aggregateValue !== undefined) {
|
||||||
|
// null to unset rating
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// otherwise not setting the rating
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
// if value is set, then we are setting the value for all
|
||||||
|
return inputValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAggregateInputIDs(
|
||||||
|
mode: GQL.BulkUpdateIdMode,
|
||||||
|
inputIds: string[] | undefined,
|
||||||
|
aggregateIds: string[]
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
mode === GQL.BulkUpdateIdMode.Set &&
|
||||||
|
(!inputIds || inputIds.length === 0)
|
||||||
|
) {
|
||||||
|
// and all scenes have the same ids,
|
||||||
|
if (aggregateIds.length > 0) {
|
||||||
|
// then unset the performerIds, otherwise ignore
|
||||||
|
return makeBulkUpdateIds(inputIds || [], mode);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if performerIds non-empty, then we are setting them
|
||||||
|
return makeBulkUpdateIds(inputIds || [], mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user