mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Movie select overhaul (#4563)
* Add ids to findMovies input * Use ids for other find interfaces * Update client side * Fix gallery select function * Replace movie select * Re-add creatable * Overhaul movie table * Remove and deprecated unused code
This commit is contained in:
@@ -12,7 +12,8 @@ type Query {
|
||||
"A function which queries Scene objects"
|
||||
findScenes(
|
||||
scene_filter: SceneFilterType
|
||||
scene_ids: [Int!]
|
||||
scene_ids: [Int!] @deprecated(reason: "use ids")
|
||||
ids: [ID!]
|
||||
filter: FindFilterType
|
||||
): FindScenesResultType!
|
||||
|
||||
@@ -50,7 +51,8 @@ type Query {
|
||||
"A function which queries Scene objects"
|
||||
findImages(
|
||||
image_filter: ImageFilterType
|
||||
image_ids: [Int!]
|
||||
image_ids: [Int!] @deprecated(reason: "use ids")
|
||||
ids: [ID!]
|
||||
filter: FindFilterType
|
||||
): FindImagesResultType!
|
||||
|
||||
@@ -60,7 +62,8 @@ type Query {
|
||||
findPerformers(
|
||||
performer_filter: PerformerFilterType
|
||||
filter: FindFilterType
|
||||
performer_ids: [Int!]
|
||||
performer_ids: [Int!] @deprecated(reason: "use ids")
|
||||
ids: [ID!]
|
||||
): FindPerformersResultType!
|
||||
|
||||
"Find a studio by ID"
|
||||
@@ -78,6 +81,7 @@ type Query {
|
||||
findMovies(
|
||||
movie_filter: MovieFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindMoviesResultType!
|
||||
|
||||
findGallery(id: ID!): Gallery
|
||||
@@ -91,7 +95,7 @@ type Query {
|
||||
findTags(
|
||||
tag_filter: TagFilterType
|
||||
filter: FindFilterType
|
||||
ids: [Int!]
|
||||
ids: [ID!]
|
||||
): FindTagsResultType!
|
||||
|
||||
"Retrieve random scene markers for the wall"
|
||||
@@ -200,15 +204,16 @@ type Query {
|
||||
|
||||
# Get everything
|
||||
|
||||
allScenes: [Scene!]!
|
||||
allScenes: [Scene!]! @deprecated(reason: "Use findScenes instead")
|
||||
allSceneMarkers: [SceneMarker!]!
|
||||
allImages: [Image!]!
|
||||
allGalleries: [Gallery!]!
|
||||
allMovies: [Movie!]!
|
||||
@deprecated(reason: "Use findSceneMarkers instead")
|
||||
allImages: [Image!]! @deprecated(reason: "Use findImages instead")
|
||||
allGalleries: [Gallery!]! @deprecated(reason: "Use findGalleries instead")
|
||||
|
||||
allPerformers: [Performer!]! @deprecated(reason: "Use findPerformers instead")
|
||||
allPerformers: [Performer!]!
|
||||
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
|
||||
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
|
||||
allMovies: [Movie!]! @deprecated(reason: "Use findMovies instead")
|
||||
|
||||
# Get everything with minimal metadata
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
|
||||
@@ -46,13 +47,52 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *FindImagesResultType, err error) {
|
||||
func (r *queryResolver) FindImages(
|
||||
ctx context.Context,
|
||||
imageFilter *models.ImageFilterType,
|
||||
imageIds []int,
|
||||
ids []string,
|
||||
filter *models.FindFilterType,
|
||||
) (ret *FindImagesResultType, err error) {
|
||||
if len(ids) > 0 {
|
||||
imageIds, err = stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Image
|
||||
|
||||
var images []*models.Image
|
||||
fields := graphql.CollectAllFields(ctx)
|
||||
result := &models.ImageQueryResult{}
|
||||
|
||||
result, err := qb.Query(ctx, models.ImageQueryOptions{
|
||||
if len(imageIds) > 0 {
|
||||
images, err = r.repository.Image.FindMany(ctx, imageIds)
|
||||
if err == nil {
|
||||
result.Count = len(images)
|
||||
for _, s := range images {
|
||||
if err = s.LoadPrimaryFile(ctx, r.repository.File); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
f := s.Files.Primary()
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
imageFile, ok := f.(*models.ImageFile)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
result.Megapixels += float64(imageFile.Width*imageFile.Height) / float64(1000000)
|
||||
result.TotalSize += float64(f.Base().Size)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result, err = qb.Query(ctx, models.ImageQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: sliceutil.Contains(fields, "count"),
|
||||
@@ -61,13 +101,9 @@ func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.Imag
|
||||
Megapixels: sliceutil.Contains(fields, "megapixels"),
|
||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
if err == nil {
|
||||
images, err = result.Resolve(ctx)
|
||||
}
|
||||
|
||||
images, err := result.Resolve(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &FindImagesResultType{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Movie, err error) {
|
||||
@@ -23,9 +24,24 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *FindMoviesResultType, err error) {
|
||||
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType, ids []string) (ret *FindMoviesResultType, err error) {
|
||||
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
movies, total, err := r.repository.Movie.Query(ctx, movieFilter, filter)
|
||||
var movies []*models.Movie
|
||||
var err error
|
||||
var total int
|
||||
|
||||
if len(idInts) > 0 {
|
||||
movies, err = r.repository.Movie.FindMany(ctx, idInts)
|
||||
total = len(movies)
|
||||
} else {
|
||||
movies, total, err = r.repository.Movie.Query(ctx, movieFilter, filter)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *models.Performer, err error) {
|
||||
@@ -23,7 +24,14 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType, performerIDs []int) (ret *FindPerformersResultType, err error) {
|
||||
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType, performerIDs []int, ids []string) (ret *FindPerformersResultType, err error) {
|
||||
if len(ids) > 0 {
|
||||
performerIDs, err = stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var performers []*models.Performer
|
||||
var err error
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
|
||||
@@ -74,7 +75,20 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInpu
|
||||
return scene, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
|
||||
func (r *queryResolver) FindScenes(
|
||||
ctx context.Context,
|
||||
sceneFilter *models.SceneFilterType,
|
||||
sceneIDs []int,
|
||||
ids []string,
|
||||
filter *models.FindFilterType,
|
||||
) (ret *FindScenesResultType, err error) {
|
||||
if len(ids) > 0 {
|
||||
sceneIDs, err = stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var scenes []*models.Scene
|
||||
var err error
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag, err error) {
|
||||
@@ -23,14 +24,19 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType, ids []int) (ret *FindTagsResultType, err error) {
|
||||
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType, ids []string) (ret *FindTagsResultType, err error) {
|
||||
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var tags []*models.Tag
|
||||
var err error
|
||||
var total int
|
||||
|
||||
if len(ids) > 0 {
|
||||
tags, err = r.repository.Tag.FindMany(ctx, ids)
|
||||
if len(idInts) > 0 {
|
||||
tags, err = r.repository.Tag.FindMany(ctx, idInts)
|
||||
total = len(tags)
|
||||
} else {
|
||||
tags, total, err = r.repository.Tag.Query(ctx, tagFilter, filter)
|
||||
|
||||
@@ -4,3 +4,9 @@ fragment SlimMovieData on Movie {
|
||||
front_image_path
|
||||
rating100
|
||||
}
|
||||
|
||||
fragment SelectMovieData on Movie {
|
||||
id
|
||||
name
|
||||
front_image_path
|
||||
}
|
||||
|
||||
@@ -6,13 +6,6 @@ query MarkerStrings($q: String, $sort: String) {
|
||||
}
|
||||
}
|
||||
|
||||
query AllMoviesForFilter {
|
||||
allMovies {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
query Stats {
|
||||
stats {
|
||||
scene_count
|
||||
|
||||
@@ -12,3 +12,16 @@ query FindMovie($id: ID!) {
|
||||
...MovieData
|
||||
}
|
||||
}
|
||||
|
||||
query FindMoviesForSelect(
|
||||
$filter: FindFilterType
|
||||
$movie_filter: MovieFilterType
|
||||
$ids: [ID!]
|
||||
) {
|
||||
findMovies(filter: $filter, movie_filter: $movie_filter, ids: $ids) {
|
||||
count
|
||||
movies {
|
||||
...SelectMovieData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ query FindPerformer($id: ID!) {
|
||||
query FindPerformersForSelect(
|
||||
$filter: FindFilterType
|
||||
$performer_filter: PerformerFilterType
|
||||
$performer_ids: [Int!]
|
||||
$ids: [ID!]
|
||||
) {
|
||||
findPerformers(
|
||||
filter: $filter
|
||||
performer_filter: $performer_filter
|
||||
performer_ids: $performer_ids
|
||||
ids: $ids
|
||||
) {
|
||||
count
|
||||
performers {
|
||||
|
||||
@@ -16,7 +16,7 @@ query FindTag($id: ID!) {
|
||||
query FindTagsForSelect(
|
||||
$filter: FindFilterType
|
||||
$tag_filter: TagFilterType
|
||||
$ids: [Int!]
|
||||
$ids: [ID!]
|
||||
) {
|
||||
findTags(filter: $filter, tag_filter: $tag_filter, ids: $ids) {
|
||||
count
|
||||
|
||||
@@ -9,7 +9,7 @@ import cx from "classnames";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
queryFindGalleries,
|
||||
queryFindGalleriesForSelect,
|
||||
queryFindGalleriesByIDForSelect,
|
||||
} from "src/core/StashService";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
@@ -56,7 +56,7 @@ const _GallerySelect: React.FC<
|
||||
filter.itemsPerPage = maxOptionsShown;
|
||||
filter.sortBy = "title";
|
||||
filter.sortDirection = GQL.SortDirectionEnum.Asc;
|
||||
const query = await queryFindGalleries(filter);
|
||||
const query = await queryFindGalleriesForSelect(filter);
|
||||
let ret = query.data.findGalleries.galleries.filter((gallery) => {
|
||||
// HACK - we should probably exclude these in the backend query, but
|
||||
// this will do in the short-term
|
||||
@@ -190,8 +190,7 @@ const _GalleryIDSelect: React.FC<IFilterProps & IFilterIDProps<Gallery>> = (
|
||||
}
|
||||
|
||||
async function loadObjectsByID(idsToLoad: string[]): Promise<Gallery[]> {
|
||||
const galleryIDs = idsToLoad.map((id) => parseInt(id));
|
||||
const query = await queryFindGalleriesByIDForSelect(galleryIDs);
|
||||
const query = await queryFindGalleriesByIDForSelect(idsToLoad);
|
||||
const { galleries: loadedGalleries } = query.data.findGalleries;
|
||||
|
||||
return loadedGalleries;
|
||||
|
||||
238
ui/v2.5/src/components/Movies/MovieSelect.tsx
Normal file
238
ui/v2.5/src/components/Movies/MovieSelect.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
OptionProps,
|
||||
components as reactSelectComponents,
|
||||
MultiValueGenericProps,
|
||||
SingleValueProps,
|
||||
} from "react-select";
|
||||
import cx from "classnames";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
queryFindMoviesForSelect,
|
||||
queryFindMoviesByIDForSelect,
|
||||
useMovieCreate,
|
||||
} from "src/core/StashService";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useIntl } from "react-intl";
|
||||
import { defaultMaxOptionsShown } from "src/core/config";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import {
|
||||
FilterSelectComponent,
|
||||
IFilterIDProps,
|
||||
IFilterProps,
|
||||
IFilterValueProps,
|
||||
Option as SelectOption,
|
||||
} from "../Shared/FilterSelect";
|
||||
import { useCompare } from "src/hooks/state";
|
||||
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||
import { sortByRelevance } from "src/utils/query";
|
||||
import { PatchComponent } from "src/pluginApi";
|
||||
|
||||
export type Movie = Pick<GQL.Movie, "id" | "name">;
|
||||
type Option = SelectOption<Movie>;
|
||||
|
||||
const _MovieSelect: React.FC<
|
||||
IFilterProps &
|
||||
IFilterValueProps<Movie> & {
|
||||
hoverPlacement?: Placement;
|
||||
excludeIds?: string[];
|
||||
}
|
||||
> = (props) => {
|
||||
const [createMovie] = useMovieCreate();
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const intl = useIntl();
|
||||
const maxOptionsShown =
|
||||
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;
|
||||
const defaultCreatable =
|
||||
!configuration?.interface.disableDropdownCreate.movie ?? true;
|
||||
|
||||
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
|
||||
|
||||
async function loadMovies(input: string): Promise<Option[]> {
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Movies);
|
||||
filter.searchTerm = input;
|
||||
filter.currentPage = 1;
|
||||
filter.itemsPerPage = maxOptionsShown;
|
||||
filter.sortBy = "name";
|
||||
filter.sortDirection = GQL.SortDirectionEnum.Asc;
|
||||
const query = await queryFindMoviesForSelect(filter);
|
||||
let ret = query.data.findMovies.movies.filter((movie) => {
|
||||
// HACK - we should probably exclude these in the backend query, but
|
||||
// this will do in the short-term
|
||||
return !exclude.includes(movie.id.toString());
|
||||
});
|
||||
|
||||
return sortByRelevance(input, ret, (m) => m.name).map((movie) => ({
|
||||
value: movie.id,
|
||||
object: movie,
|
||||
}));
|
||||
}
|
||||
|
||||
const MovieOption: React.FC<OptionProps<Option, boolean>> = (optionProps) => {
|
||||
let thisOptionProps = optionProps;
|
||||
|
||||
const { object } = optionProps.data;
|
||||
|
||||
const title = object.name;
|
||||
|
||||
thisOptionProps = {
|
||||
...optionProps,
|
||||
children: <span>{title}</span>,
|
||||
};
|
||||
|
||||
return <reactSelectComponents.Option {...thisOptionProps} />;
|
||||
};
|
||||
|
||||
const MovieMultiValueLabel: React.FC<
|
||||
MultiValueGenericProps<Option, boolean>
|
||||
> = (optionProps) => {
|
||||
let thisOptionProps = optionProps;
|
||||
|
||||
const { object } = optionProps.data;
|
||||
|
||||
thisOptionProps = {
|
||||
...optionProps,
|
||||
children: object.name,
|
||||
};
|
||||
|
||||
return <reactSelectComponents.MultiValueLabel {...thisOptionProps} />;
|
||||
};
|
||||
|
||||
const MovieValueLabel: React.FC<SingleValueProps<Option, boolean>> = (
|
||||
optionProps
|
||||
) => {
|
||||
let thisOptionProps = optionProps;
|
||||
|
||||
const { object } = optionProps.data;
|
||||
|
||||
thisOptionProps = {
|
||||
...optionProps,
|
||||
children: <>{object.name}</>,
|
||||
};
|
||||
|
||||
return <reactSelectComponents.SingleValue {...thisOptionProps} />;
|
||||
};
|
||||
|
||||
const onCreate = async (name: string) => {
|
||||
const result = await createMovie({
|
||||
variables: { input: { name } },
|
||||
});
|
||||
return {
|
||||
value: result.data!.movieCreate!.id,
|
||||
item: result.data!.movieCreate!,
|
||||
message: "Created movie",
|
||||
};
|
||||
};
|
||||
|
||||
const getNamedObject = (id: string, name: string) => {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
};
|
||||
};
|
||||
|
||||
const isValidNewOption = (inputValue: string, options: Movie[]) => {
|
||||
if (!inputValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
options.some((o) => {
|
||||
return o.name.toLowerCase() === inputValue.toLowerCase();
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<FilterSelectComponent<Movie, boolean>
|
||||
{...props}
|
||||
className={cx(
|
||||
"movie-select",
|
||||
{
|
||||
"movie-select-active": props.active,
|
||||
},
|
||||
props.className
|
||||
)}
|
||||
loadOptions={loadMovies}
|
||||
getNamedObject={getNamedObject}
|
||||
isValidNewOption={isValidNewOption}
|
||||
components={{
|
||||
Option: MovieOption,
|
||||
MultiValueLabel: MovieMultiValueLabel,
|
||||
SingleValue: MovieValueLabel,
|
||||
}}
|
||||
isMulti={props.isMulti ?? false}
|
||||
creatable={props.creatable ?? defaultCreatable}
|
||||
onCreate={onCreate}
|
||||
placeholder={
|
||||
props.noSelectionString ??
|
||||
intl.formatMessage(
|
||||
{ id: "actions.select_entity" },
|
||||
{
|
||||
entityType: intl.formatMessage({
|
||||
id: props.isMulti ? "movies" : "movie",
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
closeMenuOnSelect={!props.isMulti}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const MovieSelect = PatchComponent("MovieSelect", _MovieSelect);
|
||||
|
||||
const _MovieIDSelect: React.FC<IFilterProps & IFilterIDProps<Movie>> = (
|
||||
props
|
||||
) => {
|
||||
const { ids, onSelect: onSelectValues } = props;
|
||||
|
||||
const [values, setValues] = useState<Movie[]>([]);
|
||||
const idsChanged = useCompare(ids);
|
||||
|
||||
function onSelect(items: Movie[]) {
|
||||
setValues(items);
|
||||
onSelectValues?.(items);
|
||||
}
|
||||
|
||||
async function loadObjectsByID(idsToLoad: string[]): Promise<Movie[]> {
|
||||
const query = await queryFindMoviesByIDForSelect(idsToLoad);
|
||||
const { movies: loadedMovies } = query.data.findMovies;
|
||||
|
||||
return loadedMovies;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!idsChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ids || ids?.length === 0) {
|
||||
setValues([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// load the values if we have ids and they haven't been loaded yet
|
||||
const filteredValues = values.filter((v) => ids.includes(v.id.toString()));
|
||||
if (filteredValues.length === ids.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
const items = await loadObjectsByID(ids);
|
||||
setValues(items);
|
||||
};
|
||||
|
||||
load();
|
||||
}, [ids, idsChanged, values]);
|
||||
|
||||
return <MovieSelect {...props} values={values} onSelect={onSelect} />;
|
||||
};
|
||||
|
||||
export const MovieIDSelect = PatchComponent("MovieIDSelect", _MovieIDSelect);
|
||||
@@ -256,8 +256,7 @@ const _PerformerIDSelect: React.FC<IFilterProps & IFilterIDProps<Performer>> = (
|
||||
}
|
||||
|
||||
async function loadObjectsByID(idsToLoad: string[]): Promise<Performer[]> {
|
||||
const performerIDs = idsToLoad.map((id) => parseInt(id));
|
||||
const query = await queryFindPerformersByIDForSelect(performerIDs);
|
||||
const query = await queryFindPerformersByIDForSelect(idsToLoad);
|
||||
const { performers: loadedPerformers } = query.data.findPerformers;
|
||||
|
||||
return loadedPerformers;
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
mutateReloadScrapers,
|
||||
queryScrapeSceneQueryFragment,
|
||||
} from "src/core/StashService";
|
||||
import { MovieSelect } from "src/components/Shared/Select";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { ImageInput } from "src/components/Shared/ImageInput";
|
||||
@@ -30,7 +29,7 @@ import { useFormik } from "formik";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||
import { SceneMovieTable } from "./SceneMovieTable";
|
||||
import { IMovieEntry, SceneMovieTable } from "./SceneMovieTable";
|
||||
import { faSearch, faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { objectTitle } from "src/core/files";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
@@ -50,6 +49,7 @@ import { formikUtils } from "src/utils/form";
|
||||
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
||||
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
||||
import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect";
|
||||
import { Movie } from "src/components/Movies/MovieSelect";
|
||||
|
||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||
@@ -76,6 +76,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
|
||||
const [galleries, setGalleries] = useState<Gallery[]>([]);
|
||||
const [performers, setPerformers] = useState<Performer[]>([]);
|
||||
const [movies, setMovies] = useState<Movie[]>([]);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [studio, setStudio] = useState<Studio | null>(null);
|
||||
|
||||
@@ -104,6 +105,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
setPerformers(scene.performers ?? []);
|
||||
}, [scene.performers]);
|
||||
|
||||
useEffect(() => {
|
||||
setMovies(scene.movies?.map((m) => m.movie) ?? []);
|
||||
}, [scene.movies]);
|
||||
|
||||
useEffect(() => {
|
||||
setTags(scene.tags ?? []);
|
||||
}, [scene.tags]);
|
||||
@@ -185,6 +190,17 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
return sceneImage;
|
||||
}, [formik.values.cover_image, scene.paths?.screenshot]);
|
||||
|
||||
const movieEntries = useMemo(() => {
|
||||
return formik.values.movies
|
||||
.map((m) => {
|
||||
return {
|
||||
movie: movies.find((mm) => mm.id === m.movie_id),
|
||||
scene_index: m.scene_index,
|
||||
};
|
||||
})
|
||||
.filter((m) => m.movie !== undefined) as IMovieEntry[];
|
||||
}, [formik.values.movies, movies]);
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating100", v);
|
||||
}
|
||||
@@ -258,17 +274,19 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
setQueryableScrapers(newQueryableScrapers);
|
||||
}, [Scrapers, stashConfig]);
|
||||
|
||||
function setMovieIds(movieIds: string[]) {
|
||||
function onSetMovies(items: Movie[]) {
|
||||
setMovies(items);
|
||||
|
||||
const existingMovies = formik.values.movies;
|
||||
|
||||
const newMovies = movieIds.map((m) => {
|
||||
const existing = existingMovies.find((mm) => mm.movie_id === m);
|
||||
const newMovies = items.map((m) => {
|
||||
const existing = existingMovies.find((mm) => mm.movie_id === m.id);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return {
|
||||
movie_id: m,
|
||||
movie_id: m.id,
|
||||
scene_index: null,
|
||||
};
|
||||
});
|
||||
@@ -287,17 +305,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
function renderTableMovies() {
|
||||
return (
|
||||
<SceneMovieTable
|
||||
movieScenes={formik.values.movies}
|
||||
onUpdate={(items) => {
|
||||
formik.setFieldValue("movies", items);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
@@ -400,6 +407,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
sceneStudio={studio}
|
||||
sceneTags={tags}
|
||||
scenePerformers={performers}
|
||||
sceneMovies={movies}
|
||||
scraped={scrapedScene}
|
||||
endpoint={endpoint}
|
||||
onClose={(s) => onScrapeDialogClosed(s)}
|
||||
@@ -589,8 +597,14 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
});
|
||||
|
||||
if (idMovis.length > 0) {
|
||||
const newIds = idMovis.map((p) => p.stored_id);
|
||||
setMovieIds(newIds as string[]);
|
||||
onSetMovies(
|
||||
idMovis.map((p) => {
|
||||
return {
|
||||
id: p.stored_id!,
|
||||
name: p.name ?? "",
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,17 +765,21 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
return renderField("performer_ids", title, control, fullWidthProps);
|
||||
}
|
||||
|
||||
function onSetMovieEntries(input: IMovieEntry[]) {
|
||||
setMovies(input.map((m) => m.movie));
|
||||
|
||||
const newMovies = input.map((m) => ({
|
||||
movie_id: m.movie.id,
|
||||
scene_index: m.scene_index,
|
||||
}));
|
||||
|
||||
formik.setFieldValue("movies", newMovies);
|
||||
}
|
||||
|
||||
function renderMoviesField() {
|
||||
const title = intl.formatMessage({ id: "movies" });
|
||||
const control = (
|
||||
<>
|
||||
<MovieSelect
|
||||
isMulti
|
||||
onSelect={(items) => setMovieIds(items.map((item) => item.id))}
|
||||
ids={formik.values.movies.map((m) => m.movie_id)}
|
||||
/>
|
||||
{renderTableMovies()}
|
||||
</>
|
||||
<SceneMovieTable value={movieEntries} onUpdate={onSetMovieEntries} />
|
||||
);
|
||||
|
||||
return renderField("movies", title, control, fullWidthProps);
|
||||
|
||||
@@ -1,50 +1,96 @@
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useAllMoviesForFilter } from "src/core/StashService";
|
||||
import { Form, Row, Col } from "react-bootstrap";
|
||||
import { Movie, MovieSelect } from "src/components/Movies/MovieSelect";
|
||||
import cx from "classnames";
|
||||
|
||||
export type MovieSceneIndexMap = Map<string, number | undefined>;
|
||||
|
||||
export interface IMovieEntry {
|
||||
movie: Movie;
|
||||
scene_index?: GQL.InputMaybe<number> | undefined;
|
||||
}
|
||||
|
||||
export interface IProps {
|
||||
movieScenes: GQL.SceneMovieInput[];
|
||||
onUpdate: (value: GQL.SceneMovieInput[]) => void;
|
||||
value: IMovieEntry[];
|
||||
onUpdate: (input: IMovieEntry[]) => void;
|
||||
}
|
||||
|
||||
export const SceneMovieTable: React.FC<IProps> = (props) => {
|
||||
const { value, onUpdate } = props;
|
||||
|
||||
const intl = useIntl();
|
||||
const { data } = useAllMoviesForFilter();
|
||||
|
||||
const items = !!data && !!data.allMovies ? data.allMovies : [];
|
||||
const movieIDs = useMemo(() => value.map((m) => m.movie.id), [value]);
|
||||
|
||||
const movieEntries = props.movieScenes.map((m) => {
|
||||
const updateFieldChanged = (index: number, sceneIndex: number | null) => {
|
||||
const newValues = value.map((existing, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
movie: items.find((mm) => m.movie_id === mm.id),
|
||||
...m,
|
||||
};
|
||||
});
|
||||
|
||||
const updateFieldChanged = (movieId: string, value: number) => {
|
||||
const newValues = props.movieScenes.map((ms) => {
|
||||
if (ms.movie_id === movieId) {
|
||||
return {
|
||||
movie_id: movieId,
|
||||
scene_index: value,
|
||||
...existing,
|
||||
scene_index: sceneIndex,
|
||||
};
|
||||
}
|
||||
return ms;
|
||||
return existing;
|
||||
});
|
||||
props.onUpdate(newValues);
|
||||
|
||||
onUpdate(newValues);
|
||||
};
|
||||
|
||||
function onMovieSet(index: number, movies: Movie[]) {
|
||||
if (!movies.length) {
|
||||
// remove this entry
|
||||
const newValues = value.filter((_, i) => i !== index);
|
||||
onUpdate(newValues);
|
||||
return;
|
||||
}
|
||||
|
||||
const movie = movies[0];
|
||||
|
||||
const newValues = value.map((existing, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...existing,
|
||||
movie: movie,
|
||||
};
|
||||
}
|
||||
return existing;
|
||||
});
|
||||
|
||||
onUpdate(newValues);
|
||||
}
|
||||
|
||||
function onNewMovieSet(movies: Movie[]) {
|
||||
if (!movies.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const movie = movies[0];
|
||||
|
||||
const newValues = [
|
||||
...value,
|
||||
{
|
||||
movie: movie,
|
||||
scene_index: null,
|
||||
},
|
||||
];
|
||||
|
||||
onUpdate(newValues);
|
||||
}
|
||||
|
||||
function renderTableData() {
|
||||
return (
|
||||
<>
|
||||
{movieEntries.map((m) => (
|
||||
<Row key={m.movie_id}>
|
||||
<Form.Label column xs={9}>
|
||||
{m.movie?.name ?? ""}
|
||||
</Form.Label>
|
||||
{value.map((m, i) => (
|
||||
<Row key={m.movie.id} className="movie-row">
|
||||
<Col xs={9}>
|
||||
<MovieSelect
|
||||
onSelect={(items) => onMovieSet(i, items)}
|
||||
values={[m.movie!]}
|
||||
excludeIds={movieIDs}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
@@ -52,36 +98,38 @@ export const SceneMovieTable: React.FC<IProps> = (props) => {
|
||||
value={m.scene_index ?? ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateFieldChanged(
|
||||
m.movie_id,
|
||||
Number.parseInt(
|
||||
e.currentTarget.value ? e.currentTarget.value : "0",
|
||||
10
|
||||
)
|
||||
i,
|
||||
e.currentTarget.value === ""
|
||||
? null
|
||||
: Number.parseInt(e.currentTarget.value, 10)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Row className="movie-row">
|
||||
<Col xs={12}>
|
||||
<MovieSelect
|
||||
onSelect={(items) => onNewMovieSet(items)}
|
||||
values={[]}
|
||||
excludeIds={movieIDs}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.movieScenes.length > 0) {
|
||||
return (
|
||||
<div className="movie-table">
|
||||
<Row>
|
||||
<Form.Label column xs={9}>
|
||||
{intl.formatMessage({ id: "movie" })}
|
||||
</Form.Label>
|
||||
<Form.Label column xs={3}>
|
||||
<div className={cx("movie-table", { "no-movies": !value.length })}>
|
||||
<Row className="movie-table-header">
|
||||
<Col xs={9}></Col>
|
||||
<Form.Label column xs={3} className="movie-scene-number-header">
|
||||
{intl.formatMessage({ id: "movie_scene_number" })}
|
||||
</Form.Label>
|
||||
</Row>
|
||||
{renderTableData()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@@ -7,11 +7,10 @@ import {
|
||||
ScrapedImageRow,
|
||||
ScrapedStringListRow,
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import clone from "lodash-es/clone";
|
||||
import { useIntl } from "react-intl";
|
||||
import { uniq } from "lodash-es";
|
||||
import { Performer } from "src/components/Performers/PerformerSelect";
|
||||
import { IHasStoredID, sortStoredIdObjects } from "src/utils/data";
|
||||
import { sortStoredIdObjects } from "src/utils/data";
|
||||
import {
|
||||
ObjectListScrapeResult,
|
||||
ObjectScrapeResult,
|
||||
@@ -31,12 +30,14 @@ import {
|
||||
} from "src/components/Shared/ScrapeDialog/createObjects";
|
||||
import { Tag } from "src/components/Tags/TagSelect";
|
||||
import { Studio } from "src/components/Studios/StudioSelect";
|
||||
import { Movie } from "src/components/Movies/MovieSelect";
|
||||
|
||||
interface ISceneScrapeDialogProps {
|
||||
scene: Partial<GQL.SceneUpdateInput>;
|
||||
sceneStudio: Studio | null;
|
||||
scenePerformers: Performer[];
|
||||
sceneTags: Tag[];
|
||||
sceneMovies: Movie[];
|
||||
scraped: GQL.ScrapedScene;
|
||||
endpoint?: string;
|
||||
|
||||
@@ -48,6 +49,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
||||
sceneStudio,
|
||||
scenePerformers,
|
||||
sceneTags,
|
||||
sceneMovies,
|
||||
scraped,
|
||||
onClose,
|
||||
endpoint,
|
||||
@@ -96,44 +98,6 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
||||
)
|
||||
);
|
||||
|
||||
function mapStoredIdObjects(
|
||||
scrapedObjects?: IHasStoredID[]
|
||||
): string[] | undefined {
|
||||
if (!scrapedObjects) {
|
||||
return undefined;
|
||||
}
|
||||
const ret = scrapedObjects
|
||||
.map((p) => p.stored_id)
|
||||
.filter((p) => {
|
||||
return p !== undefined && p !== null;
|
||||
}) as string[];
|
||||
|
||||
if (ret.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// sort by id numerically
|
||||
ret.sort((a, b) => {
|
||||
return parseInt(a, 10) - parseInt(b, 10);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function sortIdList(idList?: string[] | null) {
|
||||
if (!idList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ret = clone(idList);
|
||||
// sort by id numerically
|
||||
ret.sort((a, b) => {
|
||||
return parseInt(a, 10) - parseInt(b, 10);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
const [performers, setPerformers] = useState<
|
||||
ObjectListScrapeResult<GQL.ScrapedPerformer>
|
||||
>(
|
||||
@@ -151,10 +115,17 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
||||
scraped.performers?.filter((t) => !t.stored_id) ?? []
|
||||
);
|
||||
|
||||
const [movies, setMovies] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(
|
||||
sortIdList(scene.movies?.map((p) => p.movie_id)),
|
||||
mapStoredIdObjects(scraped.movies ?? undefined)
|
||||
const [movies, setMovies] = useState<
|
||||
ObjectListScrapeResult<GQL.ScrapedMovie>
|
||||
>(
|
||||
new ObjectListScrapeResult<GQL.ScrapedMovie>(
|
||||
sortStoredIdObjects(
|
||||
sceneMovies.map((p) => ({
|
||||
stored_id: p.id,
|
||||
name: p.name,
|
||||
}))
|
||||
),
|
||||
sortStoredIdObjects(scraped.movies ?? undefined)
|
||||
)
|
||||
);
|
||||
const [newMovies, setNewMovies] = useState<GQL.ScrapedMovie[]>(
|
||||
@@ -249,12 +220,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
||||
director: director.getNewValue(),
|
||||
studio: newStudioValue,
|
||||
performers: performers.getNewValue(),
|
||||
movies: movies.getNewValue()?.map((m) => {
|
||||
return {
|
||||
stored_id: m,
|
||||
name: "",
|
||||
};
|
||||
}),
|
||||
movies: movies.getNewValue(),
|
||||
tags: tags.getNewValue(),
|
||||
details: details.getNewValue(),
|
||||
image: image.getNewValue(),
|
||||
|
||||
@@ -93,6 +93,13 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
};
|
||||
}
|
||||
|
||||
function movieToStoredID(o: { movie: { id: string; name: string } }) {
|
||||
return {
|
||||
stored_id: o.movie.id,
|
||||
name: o.movie.name,
|
||||
};
|
||||
}
|
||||
|
||||
const [studio, setStudio] = useState<ScrapeResult<GQL.ScrapedStudio>>(
|
||||
new ScrapeResult<GQL.ScrapedStudio>(
|
||||
dest.studio ? idToStoredID(dest.studio) : undefined
|
||||
@@ -127,8 +134,12 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
)
|
||||
);
|
||||
|
||||
const [movies, setMovies] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(sortIdList(dest.movies.map((p) => p.movie.id)))
|
||||
const [movies, setMovies] = useState<
|
||||
ObjectListScrapeResult<GQL.ScrapedMovie>
|
||||
>(
|
||||
new ObjectListScrapeResult<GQL.ScrapedMovie>(
|
||||
sortStoredIdObjects(dest.movies.map(movieToStoredID))
|
||||
)
|
||||
);
|
||||
|
||||
const [tags, setTags] = useState<ObjectListScrapeResult<GQL.ScrapedTag>>(
|
||||
@@ -235,9 +246,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
);
|
||||
|
||||
setMovies(
|
||||
new ScrapeResult(
|
||||
dest.movies.map((m) => m.movie.id),
|
||||
uniq(all.map((s) => s.movies.map((m) => m.movie.id)).flat())
|
||||
new ObjectListScrapeResult<GQL.ScrapedMovie>(
|
||||
sortStoredIdObjects(dest.movies.map(movieToStoredID)),
|
||||
uniqIDStoredIDs(all.map((s) => s.movies.map(movieToStoredID)).flat())
|
||||
)
|
||||
);
|
||||
|
||||
@@ -571,9 +582,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
const found = all
|
||||
.map((s) => s.movies)
|
||||
.flat()
|
||||
.find((mm) => mm.movie.id === m);
|
||||
.find((mm) => mm.movie.id === m.stored_id);
|
||||
return {
|
||||
movie_id: m,
|
||||
movie_id: m.stored_id!,
|
||||
scene_index: found!.scene_index,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -237,9 +237,21 @@ textarea.scene-description {
|
||||
.movie-table {
|
||||
width: 100%;
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
.movie-row {
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.movie-scene-number-header {
|
||||
color: $text-muted;
|
||||
font-size: 0.8em;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.movie-table.no-movies .movie-table-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scene-tabs {
|
||||
|
||||
@@ -25,16 +25,11 @@ import { StringListInput } from "../StringListInput";
|
||||
import { ImageSelector } from "../ImageSelector";
|
||||
import { ScrapeResult } from "./scrapeResult";
|
||||
|
||||
export interface IHasName {
|
||||
name: string | undefined;
|
||||
}
|
||||
|
||||
interface IScrapedFieldProps<T> {
|
||||
result: ScrapeResult<T>;
|
||||
}
|
||||
|
||||
interface IScrapedRowProps<T, V extends IHasName>
|
||||
extends IScrapedFieldProps<T> {
|
||||
interface IScrapedRowProps<T, V> extends IScrapedFieldProps<T> {
|
||||
className?: string;
|
||||
title: string;
|
||||
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||
@@ -42,6 +37,7 @@ interface IScrapedRowProps<T, V extends IHasName>
|
||||
onChange: (value: ScrapeResult<T>) => void;
|
||||
newValues?: V[];
|
||||
onCreateNew?: (index: number) => void;
|
||||
getName?: (value: V) => string;
|
||||
}
|
||||
|
||||
function renderButtonIcon(selected: boolean) {
|
||||
@@ -55,9 +51,9 @@ function renderButtonIcon(selected: boolean) {
|
||||
);
|
||||
}
|
||||
|
||||
export const ScrapeDialogRow = <T, V extends IHasName>(
|
||||
props: IScrapedRowProps<T, V>
|
||||
) => {
|
||||
export const ScrapeDialogRow = <T, V>(props: IScrapedRowProps<T, V>) => {
|
||||
const { getName = () => "" } = props;
|
||||
|
||||
function handleSelectClick(isNew: boolean) {
|
||||
const ret = clone(props.result);
|
||||
ret.useNewValue = isNew;
|
||||
@@ -83,10 +79,10 @@ export const ScrapeDialogRow = <T, V extends IHasName>(
|
||||
<Badge
|
||||
className="tag-item"
|
||||
variant="secondary"
|
||||
key={t.name}
|
||||
key={getName(t)}
|
||||
onClick={() => props.onCreateNew!(i)}
|
||||
>
|
||||
{t.name}
|
||||
{getName(t)}
|
||||
<Button className="minimal ml-2">
|
||||
<Icon className="fa-fw" icon={faPlus} />
|
||||
</Button>
|
||||
@@ -173,6 +169,10 @@ const ScrapedInputGroup: React.FC<IScrapedInputGroupProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
function getNameString(value: string) {
|
||||
return value;
|
||||
}
|
||||
|
||||
interface IScrapedInputGroupRowProps {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
@@ -206,6 +206,7 @@ export const ScrapedInputGroupRow: React.FC<IScrapedInputGroupRowProps> = (
|
||||
/>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -271,6 +272,7 @@ export const ScrapedStringListRow: React.FC<IScrapedStringListRowProps> = (
|
||||
/>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -316,6 +318,7 @@ export const ScrapedTextAreaRow: React.FC<IScrapedInputGroupRowProps> = (
|
||||
/>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -369,6 +372,7 @@ export const ScrapedImageRow: React.FC<IScrapedImageRowProps> = (props) => {
|
||||
/>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -412,6 +416,7 @@ export const ScrapedImagesRow: React.FC<IScrapedImagesRowProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -514,5 +519,6 @@ export const ScrapedCountryRow: React.FC<IScrapedCountryRowProps> = ({
|
||||
/>
|
||||
)}
|
||||
onChange={onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React, { useMemo } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { MovieSelect } from "src/components/Shared/Select";
|
||||
import {
|
||||
ScrapeDialogRow,
|
||||
IHasName,
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import { ScrapeDialogRow } from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import { PerformerSelect } from "src/components/Performers/PerformerSelect";
|
||||
import {
|
||||
ObjectScrapeResult,
|
||||
@@ -12,6 +8,7 @@ import {
|
||||
} from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||
import { TagSelect } from "src/components/Tags/TagSelect";
|
||||
import { StudioSelect } from "src/components/Studios/StudioSelect";
|
||||
import { MovieSelect } from "src/components/Movies/MovieSelect";
|
||||
|
||||
interface IScrapedStudioRow {
|
||||
title: string;
|
||||
@@ -21,6 +18,10 @@ interface IScrapedStudioRow {
|
||||
onCreateNew?: (value: GQL.ScrapedStudio) => void;
|
||||
}
|
||||
|
||||
function getObjectName<T extends { name: string }>(value: T) {
|
||||
return value.name;
|
||||
}
|
||||
|
||||
export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
||||
title,
|
||||
result,
|
||||
@@ -76,28 +77,35 @@ export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
||||
onCreateNew={() => {
|
||||
if (onCreateNew && newStudio) onCreateNew(newStudio);
|
||||
}}
|
||||
getName={getObjectName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapedObjectsRow<T, R> {
|
||||
interface IScrapedObjectsRow<T> {
|
||||
title: string;
|
||||
result: ScrapeResult<R[]>;
|
||||
onChange: (value: ScrapeResult<R[]>) => void;
|
||||
result: ScrapeResult<T[]>;
|
||||
onChange: (value: ScrapeResult<T[]>) => void;
|
||||
newObjects?: T[];
|
||||
onCreateNew?: (value: T) => void;
|
||||
renderObjects: (
|
||||
result: ScrapeResult<R[]>,
|
||||
result: ScrapeResult<T[]>,
|
||||
isNew?: boolean,
|
||||
onChange?: (value: R[]) => void
|
||||
onChange?: (value: T[]) => void
|
||||
) => JSX.Element;
|
||||
getName: (value: T) => string;
|
||||
}
|
||||
|
||||
export const ScrapedObjectsRow = <T extends IHasName, R>(
|
||||
props: IScrapedObjectsRow<T, R>
|
||||
) => {
|
||||
const { title, result, onChange, newObjects, onCreateNew, renderObjects } =
|
||||
props;
|
||||
export const ScrapedObjectsRow = <T,>(props: IScrapedObjectsRow<T>) => {
|
||||
const {
|
||||
title,
|
||||
result,
|
||||
onChange,
|
||||
newObjects,
|
||||
onCreateNew,
|
||||
renderObjects,
|
||||
getName,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
@@ -114,17 +122,18 @@ export const ScrapedObjectsRow = <T extends IHasName, R>(
|
||||
onCreateNew={(i) => {
|
||||
if (onCreateNew) onCreateNew(newObjects![i]);
|
||||
}}
|
||||
getName={getName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type IScrapedObjectRowImpl<T, R> = Omit<
|
||||
IScrapedObjectsRow<T, R>,
|
||||
"renderObjects"
|
||||
type IScrapedObjectRowImpl<T> = Omit<
|
||||
IScrapedObjectsRow<T>,
|
||||
"renderObjects" | "getName"
|
||||
>;
|
||||
|
||||
export const ScrapedPerformersRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedPerformer, GQL.ScrapedPerformer>
|
||||
IScrapedObjectRowImpl<GQL.ScrapedPerformer>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
const performersCopy = useMemo(() => {
|
||||
return (
|
||||
@@ -170,24 +179,21 @@ export const ScrapedPerformersRow: React.FC<
|
||||
);
|
||||
}
|
||||
|
||||
type PerformerType = GQL.ScrapedPerformer & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<PerformerType, GQL.ScrapedPerformer>
|
||||
<ScrapedObjectsRow<GQL.ScrapedPerformer>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedPerformers}
|
||||
onChange={onChange}
|
||||
newObjects={performersCopy}
|
||||
onCreateNew={onCreateNew}
|
||||
getName={(value) => value.name ?? ""}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrapedMoviesRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedMovie, string>
|
||||
IScrapedObjectRowImpl<GQL.ScrapedMovie>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
const moviesCopy = useMemo(() => {
|
||||
return (
|
||||
@@ -198,20 +204,25 @@ export const ScrapedMoviesRow: React.FC<
|
||||
);
|
||||
}, [newObjects]);
|
||||
|
||||
type MovieType = GQL.ScrapedMovie & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
function renderScrapedMovies(
|
||||
scrapeResult: ScrapeResult<string[]>,
|
||||
scrapeResult: ScrapeResult<GQL.ScrapedMovie[]>,
|
||||
isNew?: boolean,
|
||||
onChangeFn?: (value: string[]) => void
|
||||
onChangeFn?: (value: GQL.ScrapedMovie[]) => void
|
||||
) {
|
||||
const resultValue = isNew
|
||||
? scrapeResult.newValue
|
||||
: scrapeResult.originalValue;
|
||||
const value = resultValue ?? [];
|
||||
|
||||
const selectValue = value.map((p) => {
|
||||
const aliases: string[] = [];
|
||||
return {
|
||||
id: p.stored_id ?? "",
|
||||
name: p.name ?? "",
|
||||
aliases,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<MovieSelect
|
||||
isMulti
|
||||
@@ -219,28 +230,30 @@ export const ScrapedMoviesRow: React.FC<
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChangeFn) {
|
||||
onChangeFn(items.map((i) => i.id));
|
||||
// map the id back to stored_id
|
||||
onChangeFn(items.map((p) => ({ ...p, stored_id: p.id })));
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
values={selectValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<MovieType, string>
|
||||
<ScrapedObjectsRow<GQL.ScrapedMovie>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedMovies}
|
||||
onChange={onChange}
|
||||
newObjects={moviesCopy}
|
||||
onCreateNew={onCreateNew}
|
||||
getName={(value) => value.name ?? ""}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrapedTagsRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedTag, GQL.ScrapedTag>
|
||||
IScrapedObjectRowImpl<GQL.ScrapedTag>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
function renderScrapedTags(
|
||||
scrapeResult: ScrapeResult<GQL.ScrapedTag[]>,
|
||||
@@ -278,13 +291,14 @@ export const ScrapedTagsRow: React.FC<
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<GQL.ScrapedTag, GQL.ScrapedTag>
|
||||
<ScrapedObjectsRow<GQL.ScrapedTag>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedTags}
|
||||
onChange={onChange}
|
||||
newObjects={newObjects}
|
||||
onCreateNew={onCreateNew}
|
||||
getName={getObjectName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -123,62 +123,41 @@ export function useCreateScrapedPerformer(
|
||||
return useCreateObject("performer", createNewPerformer);
|
||||
}
|
||||
|
||||
interface IUseCreateNewObjectIDListProps<
|
||||
T extends { name?: string | undefined | null }
|
||||
> {
|
||||
scrapeResult: ScrapeResult<string[]>;
|
||||
setScrapeResult: (scrapeResult: ScrapeResult<string[]>) => void;
|
||||
newObjects: T[];
|
||||
setNewObjects: (newObject: T[]) => void;
|
||||
}
|
||||
|
||||
function useCreateNewObjectIDList<
|
||||
T extends { name?: string | undefined | null }
|
||||
>(
|
||||
entityTypeID: string,
|
||||
props: IUseCreateNewObjectIDListProps<T>,
|
||||
createObject: (toCreate: T) => Promise<string>
|
||||
export function useCreateScrapedMovie(
|
||||
props: IUseCreateNewObjectProps<GQL.ScrapedMovie>
|
||||
) {
|
||||
const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props;
|
||||
const [createMovie] = useMovieCreate();
|
||||
|
||||
async function createNewObject(toCreate: T) {
|
||||
const newID = await createObject(toCreate);
|
||||
async function createNewMovie(toCreate: GQL.ScrapedMovie) {
|
||||
const input = scrapedMovieToCreateInput(toCreate);
|
||||
|
||||
// add the new object to the new objects value
|
||||
const newResult = scrapeResult.cloneWithValue(scrapeResult.newValue);
|
||||
if (!newResult.newValue) {
|
||||
newResult.newValue = [];
|
||||
}
|
||||
newResult.newValue.push(newID);
|
||||
setScrapeResult(newResult);
|
||||
const result = await createMovie({
|
||||
variables: { input: input },
|
||||
});
|
||||
|
||||
const newValue = [...(scrapeResult.newValue ?? [])];
|
||||
if (result.data?.movieCreate)
|
||||
newValue.push({
|
||||
stored_id: result.data.movieCreate.id,
|
||||
name: result.data.movieCreate.name,
|
||||
});
|
||||
|
||||
// add the new object to the new object value
|
||||
const resultClone = scrapeResult.cloneWithValue(newValue);
|
||||
setScrapeResult(resultClone);
|
||||
|
||||
// remove the object from the list
|
||||
const newObjectsClone = newObjects.concat();
|
||||
const pIndex = newObjectsClone.findIndex((p) => p.name === toCreate.name);
|
||||
if (pIndex === -1) throw new Error("Could not find object to remove");
|
||||
if (pIndex === -1) throw new Error("Could not find movie to remove");
|
||||
|
||||
newObjectsClone.splice(pIndex, 1);
|
||||
|
||||
setNewObjects(newObjectsClone);
|
||||
}
|
||||
|
||||
return useCreateObject(entityTypeID, createNewObject);
|
||||
}
|
||||
|
||||
export function useCreateScrapedMovie(
|
||||
props: IUseCreateNewObjectIDListProps<GQL.ScrapedMovie>
|
||||
) {
|
||||
const [createMovie] = useMovieCreate();
|
||||
|
||||
async function createNewMovie(toCreate: GQL.ScrapedMovie) {
|
||||
const movieInput = scrapedMovieToCreateInput(toCreate);
|
||||
const result = await createMovie({
|
||||
variables: { input: movieInput },
|
||||
});
|
||||
|
||||
return result.data?.movieCreate?.id ?? "";
|
||||
}
|
||||
|
||||
return useCreateNewObjectIDList("movie", props, createNewMovie);
|
||||
return useCreateObject("movie", createNewMovie);
|
||||
}
|
||||
|
||||
export function useCreateScrapedTag(
|
||||
|
||||
@@ -13,15 +13,9 @@ import Select, {
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
useAllMoviesForFilter,
|
||||
useMarkerStrings,
|
||||
useMovieCreate,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { useMarkerStrings } from "src/core/StashService";
|
||||
import { SelectComponents } from "react-select/dist/declarations/src/components";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useIntl } from "react-intl";
|
||||
import { objectTitle } from "src/core/files";
|
||||
import { defaultMaxOptionsShown } from "src/core/config";
|
||||
import { useDebounce } from "src/hooks/debounce";
|
||||
@@ -32,6 +26,7 @@ import { faTableColumns } from "@fortawesome/free-solid-svg-icons";
|
||||
import { TagIDSelect } from "../Tags/TagSelect";
|
||||
import { StudioIDSelect } from "../Studios/StudioSelect";
|
||||
import { GalleryIDSelect } from "../Galleries/GallerySelect";
|
||||
import { MovieIDSelect } from "../Movies/MovieSelect";
|
||||
|
||||
export type SelectObject = {
|
||||
id: string;
|
||||
@@ -89,23 +84,6 @@ interface ISelectProps<T extends boolean> {
|
||||
closeMenuOnSelect?: boolean;
|
||||
noOptionsMessage?: string | null;
|
||||
}
|
||||
interface IFilterComponentProps extends IFilterProps {
|
||||
items: SelectObject[];
|
||||
toOption?: (item: SelectObject) => Option;
|
||||
onCreate?: (name: string) => Promise<{ item: SelectObject; message: string }>;
|
||||
}
|
||||
interface IFilterSelectProps<T extends boolean>
|
||||
extends Pick<
|
||||
ISelectProps<T>,
|
||||
| "isLoading"
|
||||
| "isMulti"
|
||||
| "components"
|
||||
| "filterOption"
|
||||
| "isValidNewOption"
|
||||
| "placeholder"
|
||||
| "closeMenuOnSelect"
|
||||
> {}
|
||||
|
||||
type TitledObject = { id: string; title: string };
|
||||
interface ITitledSelect {
|
||||
className?: string;
|
||||
@@ -125,9 +103,6 @@ const getSelectedItems = (selectedItems: OnChangeValue<Option, boolean>) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedValues = (selectedItems: OnChangeValue<Option, boolean>) =>
|
||||
getSelectedItems(selectedItems).map((item) => item.value);
|
||||
|
||||
const LimitedSelectMenu = <T extends boolean>(
|
||||
props: MenuListProps<Option, T, GroupBase<Option>>
|
||||
) => {
|
||||
@@ -273,67 +248,6 @@ const SelectComponent = <T extends boolean>({
|
||||
);
|
||||
};
|
||||
|
||||
const FilterSelectComponent = <T extends boolean>(
|
||||
props: IFilterComponentProps & ITypeProps & IFilterSelectProps<T>
|
||||
) => {
|
||||
const { items, ids, isMulti, onSelect } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const selectedIds = ids ?? [];
|
||||
const Toast = useToast();
|
||||
|
||||
const options = items.map((i) => {
|
||||
if (props.toOption) {
|
||||
return props.toOption(i);
|
||||
}
|
||||
return {
|
||||
value: i.id,
|
||||
label: i.name ?? i.title ?? "",
|
||||
};
|
||||
});
|
||||
|
||||
const selected = options.filter((option) =>
|
||||
selectedIds.includes(option.value)
|
||||
);
|
||||
const selectedOptions = (
|
||||
isMulti ? selected : selected[0] ?? null
|
||||
) as OnChangeValue<Option, T>;
|
||||
|
||||
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
|
||||
const selectedValues = getSelectedValues(selectedItems);
|
||||
onSelect?.(items.filter((item) => selectedValues.includes(item.id)));
|
||||
};
|
||||
|
||||
const onCreate = async (name: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { item: newItem, message } = await props.onCreate!(name);
|
||||
props.onSelect?.([
|
||||
...items.filter((item) => selectedIds.includes(item.id)),
|
||||
newItem,
|
||||
]);
|
||||
setLoading(false);
|
||||
Toast.success(
|
||||
<span>
|
||||
{message}: <b>{name}</b>
|
||||
</span>
|
||||
);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectComponent<T>
|
||||
{...props}
|
||||
isLoading={props.isLoading || loading}
|
||||
onChange={onChange}
|
||||
items={options}
|
||||
selectedOptions={selectedOptions}
|
||||
onCreateOption={props.creatable ? onCreate : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const GallerySelect: React.FC<
|
||||
IFilterProps & { excludeIds?: string[] }
|
||||
> = (props) => {
|
||||
@@ -493,50 +407,7 @@ export const StudioSelect: React.FC<
|
||||
};
|
||||
|
||||
export const MovieSelect: React.FC<IFilterProps> = (props) => {
|
||||
const { data, loading } = useAllMoviesForFilter();
|
||||
const [createMovie] = useMovieCreate();
|
||||
const items = data?.allMovies ?? [];
|
||||
const intl = useIntl();
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const defaultCreatable =
|
||||
!configuration?.interface.disableDropdownCreate.movie ?? true;
|
||||
|
||||
const onCreate = async (name: string) => {
|
||||
const result = await createMovie({
|
||||
variables: { input: { name } },
|
||||
});
|
||||
return {
|
||||
item: result.data!.movieCreate!,
|
||||
message: intl.formatMessage(
|
||||
{ id: "toast.created_entity" },
|
||||
{ entity: intl.formatMessage({ id: "movie" }).toLocaleLowerCase() }
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<FilterSelectComponent
|
||||
{...props}
|
||||
isMulti={props.isMulti ?? false}
|
||||
type="movies"
|
||||
isLoading={loading}
|
||||
items={items}
|
||||
placeholder={
|
||||
props.noSelectionString ??
|
||||
intl.formatMessage(
|
||||
{ id: "actions.select_entity" },
|
||||
{
|
||||
entityType: intl.formatMessage({
|
||||
id: props.isMulti ? "movies" : "movie",
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
creatable={props.creatable ?? defaultCreatable}
|
||||
onCreate={onCreate}
|
||||
/>
|
||||
);
|
||||
return <MovieIDSelect {...props} />;
|
||||
};
|
||||
|
||||
export const TagSelect: React.FC<
|
||||
|
||||
@@ -233,8 +233,7 @@ const _StudioIDSelect: React.FC<IFilterProps & IFilterIDProps<Studio>> = (
|
||||
}
|
||||
|
||||
async function loadObjectsByID(idsToLoad: string[]): Promise<Studio[]> {
|
||||
const studioIDs = idsToLoad.map((id) => parseInt(id));
|
||||
const query = await queryFindStudiosByIDForSelect(studioIDs);
|
||||
const query = await queryFindStudiosByIDForSelect(idsToLoad);
|
||||
const { studios: loadedStudios } = query.data.findStudios;
|
||||
|
||||
return loadedStudios;
|
||||
|
||||
@@ -251,8 +251,7 @@ const _TagIDSelect: React.FC<IFilterProps & IFilterIDProps<Tag>> = (props) => {
|
||||
}
|
||||
|
||||
async function loadObjectsByID(idsToLoad: string[]): Promise<Tag[]> {
|
||||
const tagIDs = idsToLoad.map((id) => parseInt(id));
|
||||
const query = await queryFindTagsByIDForSelect(tagIDs);
|
||||
const query = await queryFindTagsByIDForSelect(idsToLoad);
|
||||
const { tags: loadedTags } = query.data.findTags;
|
||||
|
||||
return loadedTags;
|
||||
|
||||
@@ -73,26 +73,6 @@ function evictTypeFields(
|
||||
}
|
||||
}
|
||||
|
||||
// Appends obj to the cached result of the given query.
|
||||
// Use to append objects to "All*" queries in "Create" mutations.
|
||||
function appendObject(
|
||||
cache: ApolloCache<unknown>,
|
||||
obj: StoreObject,
|
||||
query: DocumentNode
|
||||
) {
|
||||
const field = getQueryDefinition(query).selectionSet.selections[0];
|
||||
if (!isField(field)) return;
|
||||
const keyName = field.name.value;
|
||||
|
||||
cache.modify({
|
||||
fields: {
|
||||
[keyName]: (value, { toReference }) => {
|
||||
return [...(value as unknown[]), toReference(obj)];
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Deletes obj from the cache, and sets the
|
||||
// cached result of the given query to null.
|
||||
// Use with "Destroy" mutations.
|
||||
@@ -199,7 +179,22 @@ export const queryFindMovies = (filter: ListFilterModel) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const useAllMoviesForFilter = () => GQL.useAllMoviesForFilterQuery();
|
||||
export const queryFindMoviesByIDForSelect = (movieIDs: string[]) =>
|
||||
client.query<GQL.FindMoviesForSelectQuery>({
|
||||
query: GQL.FindMoviesForSelectDocument,
|
||||
variables: {
|
||||
ids: movieIDs,
|
||||
},
|
||||
});
|
||||
|
||||
export const queryFindMoviesForSelect = (filter: ListFilterModel) =>
|
||||
client.query<GQL.FindMoviesForSelectQuery>({
|
||||
query: GQL.FindMoviesForSelectDocument,
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
movie_filter: filter.makeFilter(),
|
||||
},
|
||||
});
|
||||
|
||||
export const useFindSceneMarkers = (filter?: ListFilterModel) =>
|
||||
GQL.useFindSceneMarkersQuery({
|
||||
@@ -244,7 +239,16 @@ export const queryFindGalleries = (filter: ListFilterModel) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const queryFindGalleriesByIDForSelect = (galleryIDs: number[]) =>
|
||||
export const queryFindGalleriesForSelect = (filter: ListFilterModel) =>
|
||||
client.query<GQL.FindGalleriesForSelectQuery>({
|
||||
query: GQL.FindGalleriesForSelectDocument,
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
gallery_filter: filter.makeFilter(),
|
||||
},
|
||||
});
|
||||
|
||||
export const queryFindGalleriesByIDForSelect = (galleryIDs: string[]) =>
|
||||
client.query<GQL.FindGalleriesForSelectQuery>({
|
||||
query: GQL.FindGalleriesForSelectDocument,
|
||||
variables: {
|
||||
@@ -281,11 +285,11 @@ export const queryFindPerformers = (filter: ListFilterModel) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const queryFindPerformersByIDForSelect = (performerIDs: number[]) =>
|
||||
export const queryFindPerformersByIDForSelect = (performerIDs: string[]) =>
|
||||
client.query<GQL.FindPerformersForSelectQuery>({
|
||||
query: GQL.FindPerformersForSelectDocument,
|
||||
variables: {
|
||||
performer_ids: performerIDs,
|
||||
ids: performerIDs,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -327,7 +331,7 @@ export const queryFindStudios = (filter: ListFilterModel) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const queryFindStudiosByIDForSelect = (studioIDs: number[]) =>
|
||||
export const queryFindStudiosByIDForSelect = (studioIDs: string[]) =>
|
||||
client.query<GQL.FindStudiosForSelectQuery>({
|
||||
query: GQL.FindStudiosForSelectDocument,
|
||||
variables: {
|
||||
@@ -367,7 +371,7 @@ export const queryFindTags = (filter: ListFilterModel) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const queryFindTagsByIDForSelect = (tagIDs: number[]) =>
|
||||
export const queryFindTagsByIDForSelect = (tagIDs: string[]) =>
|
||||
client.query<GQL.FindTagsForSelectQuery>({
|
||||
query: GQL.FindTagsForSelectDocument,
|
||||
variables: {
|
||||
@@ -1072,6 +1076,7 @@ export const mutateImageSetPrimaryFile = (id: string, fileID: string) =>
|
||||
});
|
||||
|
||||
const movieMutationImpactedTypeFields = {
|
||||
Performer: ["movie_count"],
|
||||
Studio: ["movie_count"],
|
||||
};
|
||||
|
||||
@@ -1085,10 +1090,8 @@ export const useMovieCreate = () =>
|
||||
const movie = result.data?.movieCreate;
|
||||
if (!movie) return;
|
||||
|
||||
appendObject(cache, movie, GQL.AllMoviesForFilterDocument);
|
||||
|
||||
// update stats
|
||||
updateStats(cache, "studio_count", 1);
|
||||
updateStats(cache, "movie_count", 1);
|
||||
|
||||
evictTypeFields(cache, movieMutationImpactedTypeFields);
|
||||
evictQueries(cache, movieMutationImpactedQueries);
|
||||
|
||||
@@ -1073,7 +1073,7 @@
|
||||
"megabits_per_second": "{value} mbps",
|
||||
"metadata": "Metadata",
|
||||
"movie": "Movie",
|
||||
"movie_scene_number": "Movie Scene Number",
|
||||
"movie_scene_number": "Scene Number",
|
||||
"movies": "Movies",
|
||||
"name": "Name",
|
||||
"new": "New",
|
||||
|
||||
Reference in New Issue
Block a user