mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Studio select refactor (#4493)
* Add id interface to findStudios * Replace existing selects * Remove unused code * Fix scrape/merge select * Make clearable
This commit is contained in:
@@ -33,3 +33,16 @@ fragment StudioData on Studio {
|
|||||||
rating100
|
rating100
|
||||||
aliases
|
aliases
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragment SelectStudioData on Studio {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
aliases
|
||||||
|
details
|
||||||
|
image_path
|
||||||
|
|
||||||
|
parent_studio {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,14 +6,6 @@ query MarkerStrings($q: String, $sort: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query AllStudiosForFilter {
|
|
||||||
allStudios {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
aliases
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query AllMoviesForFilter {
|
query AllMoviesForFilter {
|
||||||
allMovies {
|
allMovies {
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -12,3 +12,16 @@ query FindStudio($id: ID!) {
|
|||||||
...StudioData
|
...StudioData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query FindStudiosForSelect(
|
||||||
|
$filter: FindFilterType
|
||||||
|
$studio_filter: StudioFilterType
|
||||||
|
$ids: [ID!]
|
||||||
|
) {
|
||||||
|
findStudios(filter: $filter, studio_filter: $studio_filter, ids: $ids) {
|
||||||
|
count
|
||||||
|
studios {
|
||||||
|
...SelectStudioData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ type Query {
|
|||||||
findStudios(
|
findStudios(
|
||||||
studio_filter: StudioFilterType
|
studio_filter: StudioFilterType
|
||||||
filter: FindFilterType
|
filter: FindFilterType
|
||||||
|
ids: [ID!]
|
||||||
): FindStudiosResultType!
|
): FindStudiosResultType!
|
||||||
|
|
||||||
"Find a movie by ID"
|
"Find a movie by ID"
|
||||||
@@ -202,11 +203,11 @@ type Query {
|
|||||||
allSceneMarkers: [SceneMarker!]!
|
allSceneMarkers: [SceneMarker!]!
|
||||||
allImages: [Image!]!
|
allImages: [Image!]!
|
||||||
allGalleries: [Gallery!]!
|
allGalleries: [Gallery!]!
|
||||||
allStudios: [Studio!]!
|
|
||||||
allMovies: [Movie!]!
|
allMovies: [Movie!]!
|
||||||
|
|
||||||
allPerformers: [Performer!]! @deprecated(reason: "Use findPerformers instead")
|
allPerformers: [Performer!]! @deprecated(reason: "Use findPerformers instead")
|
||||||
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
|
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
|
||||||
|
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
|
||||||
|
|
||||||
# Get everything with minimal metadata
|
# Get everything with minimal metadata
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.Studio, err error) {
|
func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.Studio, err error) {
|
||||||
@@ -24,9 +25,23 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (ret *FindStudiosResultType, err error) {
|
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType, ids []string) (ret *FindStudiosResultType, err error) {
|
||||||
|
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
studios, total, err := r.repository.Studio.Query(ctx, studioFilter, filter)
|
var studios []*models.Studio
|
||||||
|
var err error
|
||||||
|
var total int
|
||||||
|
|
||||||
|
if len(idInts) > 0 {
|
||||||
|
studios, err = r.repository.Studio.FindMany(ctx, idInts)
|
||||||
|
total = len(studios)
|
||||||
|
} else {
|
||||||
|
studios, total, err = r.repository.Studio.Query(ctx, studioFilter, filter)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
useListGalleryScrapers,
|
useListGalleryScrapers,
|
||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { SceneSelect, StudioSelect } from "src/components/Shared/Select";
|
import { SceneSelect } from "src/components/Shared/Select";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
} from "src/utils/yup";
|
} from "src/utils/yup";
|
||||||
import { formikUtils } from "src/utils/form";
|
import { formikUtils } from "src/utils/form";
|
||||||
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
||||||
|
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
@@ -66,6 +67,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
const [performers, setPerformers] = useState<Performer[]>([]);
|
const [performers, setPerformers] = useState<Performer[]>([]);
|
||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
|
const [studio, setStudio] = useState<Studio | null>(null);
|
||||||
|
|
||||||
const isNew = gallery.id === undefined;
|
const isNew = gallery.id === undefined;
|
||||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||||
@@ -152,6 +154,11 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSetStudio(item: Studio | null) {
|
||||||
|
setStudio(item);
|
||||||
|
formik.setFieldValue("studio_id", item ? item.id : null);
|
||||||
|
}
|
||||||
|
|
||||||
useRatingKeybinds(
|
useRatingKeybinds(
|
||||||
isVisible,
|
isVisible,
|
||||||
stashConfig?.ui?.ratingSystemOptions?.type,
|
stashConfig?.ui?.ratingSystemOptions?.type,
|
||||||
@@ -166,6 +173,10 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
setTags(gallery.tags ?? []);
|
setTags(gallery.tags ?? []);
|
||||||
}, [gallery.tags]);
|
}, [gallery.tags]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStudio(gallery.studio ?? null);
|
||||||
|
}, [gallery.studio]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
Mousetrap.bind("s s", () => {
|
Mousetrap.bind("s s", () => {
|
||||||
@@ -252,6 +263,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
return (
|
return (
|
||||||
<GalleryScrapeDialog
|
<GalleryScrapeDialog
|
||||||
gallery={currentGallery}
|
gallery={currentGallery}
|
||||||
|
galleryStudio={studio}
|
||||||
galleryTags={tags}
|
galleryTags={tags}
|
||||||
galleryPerformers={performers}
|
galleryPerformers={performers}
|
||||||
scraped={scrapedGallery}
|
scraped={scrapedGallery}
|
||||||
@@ -324,7 +336,11 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (galleryData.studio?.stored_id) {
|
if (galleryData.studio?.stored_id) {
|
||||||
formik.setFieldValue("studio_id", galleryData.studio.stored_id);
|
onSetStudio({
|
||||||
|
id: galleryData.studio.stored_id,
|
||||||
|
name: galleryData.studio.name ?? "",
|
||||||
|
aliases: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (galleryData.performers?.length) {
|
if (galleryData.performers?.length) {
|
||||||
@@ -429,13 +445,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
const title = intl.formatMessage({ id: "studio" });
|
const title = intl.formatMessage({ id: "studio" });
|
||||||
const control = (
|
const control = (
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
onSelect={(items) =>
|
onSelect={(items) => onSetStudio(items.length > 0 ? items[0] : null)}
|
||||||
formik.setFieldValue(
|
values={studio ? [studio] : []}
|
||||||
"studio_id",
|
|
||||||
items.length > 0 ? items[0]?.id : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ import {
|
|||||||
} from "src/components/Shared/ScrapeDialog/createObjects";
|
} from "src/components/Shared/ScrapeDialog/createObjects";
|
||||||
import { uniq } from "lodash-es";
|
import { uniq } from "lodash-es";
|
||||||
import { Tag } from "src/components/Tags/TagSelect";
|
import { Tag } from "src/components/Tags/TagSelect";
|
||||||
|
import { Studio } from "src/components/Studios/StudioSelect";
|
||||||
|
|
||||||
interface IGalleryScrapeDialogProps {
|
interface IGalleryScrapeDialogProps {
|
||||||
gallery: Partial<GQL.GalleryUpdateInput>;
|
gallery: Partial<GQL.GalleryUpdateInput>;
|
||||||
|
galleryStudio: Studio | null;
|
||||||
galleryTags: Tag[];
|
galleryTags: Tag[];
|
||||||
galleryPerformers: Performer[];
|
galleryPerformers: Performer[];
|
||||||
scraped: GQL.ScrapedGallery;
|
scraped: GQL.ScrapedGallery;
|
||||||
@@ -37,6 +39,7 @@ interface IGalleryScrapeDialogProps {
|
|||||||
|
|
||||||
export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
|
export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
|
||||||
gallery,
|
gallery,
|
||||||
|
galleryStudio,
|
||||||
galleryTags,
|
galleryTags,
|
||||||
galleryPerformers,
|
galleryPerformers,
|
||||||
scraped,
|
scraped,
|
||||||
@@ -63,8 +66,16 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
|
|||||||
const [photographer, setPhotographer] = useState<ScrapeResult<string>>(
|
const [photographer, setPhotographer] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(gallery.photographer, scraped.photographer)
|
new ScrapeResult<string>(gallery.photographer, scraped.photographer)
|
||||||
);
|
);
|
||||||
const [studio, setStudio] = useState<ScrapeResult<string>>(
|
const [studio, setStudio] = useState<ScrapeResult<GQL.ScrapedStudio>>(
|
||||||
new ScrapeResult<string>(gallery.studio_id, scraped.studio?.stored_id)
|
new ScrapeResult<GQL.ScrapedStudio>(
|
||||||
|
galleryStudio
|
||||||
|
? {
|
||||||
|
stored_id: galleryStudio.id,
|
||||||
|
name: galleryStudio.name,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
scraped.studio
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const [newStudio, setNewStudio] = useState<GQL.ScrapedStudio | undefined>(
|
const [newStudio, setNewStudio] = useState<GQL.ScrapedStudio | undefined>(
|
||||||
scraped.studio && !scraped.studio.stored_id ? scraped.studio : undefined
|
scraped.studio && !scraped.studio.stored_id ? scraped.studio : undefined
|
||||||
@@ -156,12 +167,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
|
|||||||
urls: urls.getNewValue(),
|
urls: urls.getNewValue(),
|
||||||
date: date.getNewValue(),
|
date: date.getNewValue(),
|
||||||
photographer: photographer.getNewValue(),
|
photographer: photographer.getNewValue(),
|
||||||
studio: newStudioValue
|
studio: newStudioValue,
|
||||||
? {
|
|
||||||
stored_id: newStudioValue,
|
|
||||||
name: "",
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
performers: performers.getNewValue(),
|
performers: performers.getNewValue(),
|
||||||
tags: tags.getNewValue(),
|
tags: tags.getNewValue(),
|
||||||
details: details.getNewValue(),
|
details: details.getNewValue(),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import { StudioSelect } from "src/components/Shared/Select";
|
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
@@ -23,6 +22,7 @@ import {
|
|||||||
} from "src/components/Performers/PerformerSelect";
|
} from "src/components/Performers/PerformerSelect";
|
||||||
import { formikUtils } from "src/utils/form";
|
import { formikUtils } from "src/utils/form";
|
||||||
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
||||||
|
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
image: GQL.ImageDataFragment;
|
image: GQL.ImageDataFragment;
|
||||||
@@ -47,6 +47,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
const [performers, setPerformers] = useState<Performer[]>([]);
|
const [performers, setPerformers] = useState<Performer[]>([]);
|
||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
|
const [studio, setStudio] = useState<Studio | null>(null);
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: yup.string().ensure(),
|
title: yup.string().ensure(),
|
||||||
@@ -103,6 +104,11 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSetStudio(item: Studio | null) {
|
||||||
|
setStudio(item);
|
||||||
|
formik.setFieldValue("studio_id", item ? item.id : null);
|
||||||
|
}
|
||||||
|
|
||||||
useRatingKeybinds(
|
useRatingKeybinds(
|
||||||
true,
|
true,
|
||||||
configuration?.ui?.ratingSystemOptions?.type,
|
configuration?.ui?.ratingSystemOptions?.type,
|
||||||
@@ -117,6 +123,10 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
setTags(image.tags ?? []);
|
setTags(image.tags ?? []);
|
||||||
}, [image.tags]);
|
}, [image.tags]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStudio(image.studio ?? null);
|
||||||
|
}, [image.studio]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
Mousetrap.bind("s s", () => {
|
Mousetrap.bind("s s", () => {
|
||||||
@@ -183,13 +193,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
const title = intl.formatMessage({ id: "studio" });
|
const title = intl.formatMessage({ id: "studio" });
|
||||||
const control = (
|
const control = (
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
onSelect={(items) =>
|
onSelect={(items) => onSetStudio(items.length > 0 ? items[0] : null)}
|
||||||
formik.setFieldValue(
|
values={studio ? [studio] : []}
|
||||||
"studio_id",
|
|
||||||
items.length > 0 ? items[0]?.id : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
useListMovieScrapers,
|
useListMovieScrapers,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { StudioSelect } from "src/components/Shared/Select";
|
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { URLField } from "src/components/Shared/URLField";
|
import { URLField } from "src/components/Shared/URLField";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
@@ -22,6 +21,7 @@ import isEqual from "lodash-es/isEqual";
|
|||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
import { formikUtils } from "src/utils/form";
|
import { formikUtils } from "src/utils/form";
|
||||||
import { yupDateString, yupFormikValidate } from "src/utils/yup";
|
import { yupDateString, yupFormikValidate } from "src/utils/yup";
|
||||||
|
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
||||||
|
|
||||||
interface IMovieEditPanel {
|
interface IMovieEditPanel {
|
||||||
movie: Partial<GQL.MovieDataFragment>;
|
movie: Partial<GQL.MovieDataFragment>;
|
||||||
@@ -55,6 +55,8 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
const Scrapers = useListMovieScrapers();
|
const Scrapers = useListMovieScrapers();
|
||||||
const [scrapedMovie, setScrapedMovie] = useState<GQL.ScrapedMovie>();
|
const [scrapedMovie, setScrapedMovie] = useState<GQL.ScrapedMovie>();
|
||||||
|
|
||||||
|
const [studio, setStudio] = useState<Studio | null>(null);
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
aliases: yup.string().ensure(),
|
aliases: yup.string().ensure(),
|
||||||
@@ -88,6 +90,15 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
onSubmit: (values) => onSave(schema.cast(values)),
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onSetStudio(item: Studio | null) {
|
||||||
|
setStudio(item);
|
||||||
|
formik.setFieldValue("studio_id", item ? item.id : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStudio(movie.studio ?? null);
|
||||||
|
}, [movie.studio]);
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Mousetrap.bind("u", (e) => {
|
// Mousetrap.bind("u", (e) => {
|
||||||
@@ -129,7 +140,11 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.studio && state.studio.stored_id) {
|
if (state.studio && state.studio.stored_id) {
|
||||||
formik.setFieldValue("studio_id", state.studio.stored_id);
|
onSetStudio({
|
||||||
|
id: state.studio.stored_id,
|
||||||
|
name: state.studio.name ?? "",
|
||||||
|
aliases: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.director) {
|
if (state.director) {
|
||||||
@@ -324,13 +339,8 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
const title = intl.formatMessage({ id: "studio" });
|
const title = intl.formatMessage({ id: "studio" });
|
||||||
const control = (
|
const control = (
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
onSelect={(items) =>
|
onSelect={(items) => onSetStudio(items.length > 0 ? items[0] : null)}
|
||||||
formik.setFieldValue(
|
values={studio ? [studio] : []}
|
||||||
"studio_id",
|
|
||||||
items.length > 0 ? items[0]?.id : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,7 @@ import {
|
|||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
queryScrapeSceneQueryFragment,
|
queryScrapeSceneQueryFragment,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import {
|
import { GallerySelect, MovieSelect } from "src/components/Shared/Select";
|
||||||
StudioSelect,
|
|
||||||
GallerySelect,
|
|
||||||
MovieSelect,
|
|
||||||
} from "src/components/Shared/Select";
|
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { ImageInput } from "src/components/Shared/ImageInput";
|
import { ImageInput } from "src/components/Shared/ImageInput";
|
||||||
@@ -52,6 +48,7 @@ import {
|
|||||||
} from "src/components/Performers/PerformerSelect";
|
} from "src/components/Performers/PerformerSelect";
|
||||||
import { formikUtils } from "src/utils/form";
|
import { formikUtils } from "src/utils/form";
|
||||||
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
||||||
|
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
||||||
|
|
||||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||||
@@ -81,6 +78,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
const [performers, setPerformers] = useState<Performer[]>([]);
|
const [performers, setPerformers] = useState<Performer[]>([]);
|
||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
|
const [studio, setStudio] = useState<Studio | null>(null);
|
||||||
|
|
||||||
const Scrapers = useListSceneScrapers();
|
const Scrapers = useListSceneScrapers();
|
||||||
const [fragmentScrapers, setFragmentScrapers] = useState<GQL.Scraper[]>([]);
|
const [fragmentScrapers, setFragmentScrapers] = useState<GQL.Scraper[]>([]);
|
||||||
@@ -109,6 +107,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
setTags(scene.tags ?? []);
|
setTags(scene.tags ?? []);
|
||||||
}, [scene.tags]);
|
}, [scene.tags]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStudio(scene.studio ?? null);
|
||||||
|
}, [scene.studio]);
|
||||||
|
|
||||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
@@ -215,6 +217,11 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSetStudio(item: Studio | null) {
|
||||||
|
setStudio(item);
|
||||||
|
formik.setFieldValue("studio_id", item ? item.id : null);
|
||||||
|
}
|
||||||
|
|
||||||
useRatingKeybinds(
|
useRatingKeybinds(
|
||||||
isVisible,
|
isVisible,
|
||||||
stashConfig?.ui?.ratingSystemOptions?.type,
|
stashConfig?.ui?.ratingSystemOptions?.type,
|
||||||
@@ -394,6 +401,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
return (
|
return (
|
||||||
<SceneScrapeDialog
|
<SceneScrapeDialog
|
||||||
scene={currentScene}
|
scene={currentScene}
|
||||||
|
sceneStudio={studio}
|
||||||
sceneTags={tags}
|
sceneTags={tags}
|
||||||
scenePerformers={performers}
|
scenePerformers={performers}
|
||||||
scraped={scrapedScene}
|
scraped={scrapedScene}
|
||||||
@@ -554,7 +562,11 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updatedScene.studio && updatedScene.studio.stored_id) {
|
if (updatedScene.studio && updatedScene.studio.stored_id) {
|
||||||
formik.setFieldValue("studio_id", updatedScene.studio.stored_id);
|
onSetStudio({
|
||||||
|
id: updatedScene.studio.stored_id,
|
||||||
|
name: updatedScene.studio.name ?? "",
|
||||||
|
aliases: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedScene.performers && updatedScene.performers.length > 0) {
|
if (updatedScene.performers && updatedScene.performers.length > 0) {
|
||||||
@@ -726,13 +738,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
const title = intl.formatMessage({ id: "studio" });
|
const title = intl.formatMessage({ id: "studio" });
|
||||||
const control = (
|
const control = (
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
onSelect={(items) =>
|
onSelect={(items) => onSetStudio(items.length > 0 ? items[0] : null)}
|
||||||
formik.setFieldValue(
|
values={studio ? [studio] : []}
|
||||||
"studio_id",
|
|
||||||
items.length > 0 ? items[0]?.id : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,11 @@ import {
|
|||||||
useCreateScrapedTag,
|
useCreateScrapedTag,
|
||||||
} from "src/components/Shared/ScrapeDialog/createObjects";
|
} from "src/components/Shared/ScrapeDialog/createObjects";
|
||||||
import { Tag } from "src/components/Tags/TagSelect";
|
import { Tag } from "src/components/Tags/TagSelect";
|
||||||
|
import { Studio } from "src/components/Studios/StudioSelect";
|
||||||
|
|
||||||
interface ISceneScrapeDialogProps {
|
interface ISceneScrapeDialogProps {
|
||||||
scene: Partial<GQL.SceneUpdateInput>;
|
scene: Partial<GQL.SceneUpdateInput>;
|
||||||
|
sceneStudio: Studio | null;
|
||||||
scenePerformers: Performer[];
|
scenePerformers: Performer[];
|
||||||
sceneTags: Tag[];
|
sceneTags: Tag[];
|
||||||
scraped: GQL.ScrapedScene;
|
scraped: GQL.ScrapedScene;
|
||||||
@@ -42,6 +44,7 @@ interface ISceneScrapeDialogProps {
|
|||||||
|
|
||||||
export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
||||||
scene,
|
scene,
|
||||||
|
sceneStudio,
|
||||||
scenePerformers,
|
scenePerformers,
|
||||||
sceneTags,
|
sceneTags,
|
||||||
scraped,
|
scraped,
|
||||||
@@ -70,8 +73,16 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
const [director, setDirector] = useState<ScrapeResult<string>>(
|
const [director, setDirector] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(scene.director, scraped.director)
|
new ScrapeResult<string>(scene.director, scraped.director)
|
||||||
);
|
);
|
||||||
const [studio, setStudio] = useState<ScrapeResult<string>>(
|
const [studio, setStudio] = useState<ScrapeResult<GQL.ScrapedStudio>>(
|
||||||
new ScrapeResult<string>(scene.studio_id, scraped.studio?.stored_id)
|
new ScrapeResult<GQL.ScrapedStudio>(
|
||||||
|
sceneStudio
|
||||||
|
? {
|
||||||
|
stored_id: sceneStudio.id,
|
||||||
|
name: sceneStudio.name,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
scraped.studio?.stored_id ? scraped.studio : undefined
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const [newStudio, setNewStudio] = useState<GQL.ScrapedStudio | undefined>(
|
const [newStudio, setNewStudio] = useState<GQL.ScrapedStudio | undefined>(
|
||||||
scraped.studio && !scraped.studio.stored_id ? scraped.studio : undefined
|
scraped.studio && !scraped.studio.stored_id ? scraped.studio : undefined
|
||||||
@@ -235,12 +246,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
urls: urls.getNewValue(),
|
urls: urls.getNewValue(),
|
||||||
date: date.getNewValue(),
|
date: date.getNewValue(),
|
||||||
director: director.getNewValue(),
|
director: director.getNewValue(),
|
||||||
studio: newStudioValue
|
studio: newStudioValue,
|
||||||
? {
|
|
||||||
stored_id: newStudioValue,
|
|
||||||
name: "",
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
performers: performers.getNewValue(),
|
performers: performers.getNewValue(),
|
||||||
movies: movies.getNewValue()?.map((m) => {
|
movies: movies.getNewValue()?.map((m) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -87,8 +87,17 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
new ScrapeResult<number>(dest.play_duration)
|
new ScrapeResult<number>(dest.play_duration)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [studio, setStudio] = useState<ScrapeResult<string>>(
|
function idToStoredID(o: { id: string; name: string }) {
|
||||||
new ScrapeResult<string>(dest.studio?.id)
|
return {
|
||||||
|
stored_id: o.id,
|
||||||
|
name: o.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [studio, setStudio] = useState<ScrapeResult<GQL.ScrapedStudio>>(
|
||||||
|
new ScrapeResult<GQL.ScrapedStudio>(
|
||||||
|
dest.studio ? idToStoredID(dest.studio) : undefined
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
function sortIdList(idList?: string[] | null) {
|
function sortIdList(idList?: string[] | null) {
|
||||||
@@ -105,13 +114,6 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
function idToStoredID(o: { id: string; name: string }) {
|
|
||||||
return {
|
|
||||||
stored_id: o.id,
|
|
||||||
name: o.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniqIDStoredIDs<T extends IHasStoredID>(objs: T[]) {
|
function uniqIDStoredIDs<T extends IHasStoredID>(objs: T[]) {
|
||||||
return objs.filter((o, i) => {
|
return objs.filter((o, i) => {
|
||||||
return objs.findIndex((oo) => oo.stored_id === o.stored_id) === i;
|
return objs.findIndex((oo) => oo.stored_id === o.stored_id) === i;
|
||||||
@@ -197,10 +199,18 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
setDate(
|
setDate(
|
||||||
new ScrapeResult(dest.date, sources.find((s) => s.date)?.date, !dest.date)
|
new ScrapeResult(dest.date, sources.find((s) => s.date)?.date, !dest.date)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const foundStudio = sources.find((s) => s.studio)?.studio;
|
||||||
|
|
||||||
setStudio(
|
setStudio(
|
||||||
new ScrapeResult(
|
new ScrapeResult<GQL.ScrapedStudio>(
|
||||||
dest.studio?.id,
|
dest.studio ? idToStoredID(dest.studio) : undefined,
|
||||||
sources.find((s) => s.studio)?.studio?.id,
|
foundStudio
|
||||||
|
? {
|
||||||
|
stored_id: foundStudio.id,
|
||||||
|
name: foundStudio.name,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
!dest.studio
|
!dest.studio
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -581,7 +591,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
play_count: playCount.getNewValue(),
|
play_count: playCount.getNewValue(),
|
||||||
play_duration: playDuration.getNewValue(),
|
play_duration: playDuration.getNewValue(),
|
||||||
gallery_ids: galleries.getNewValue(),
|
gallery_ids: galleries.getNewValue(),
|
||||||
studio_id: studio.getNewValue(),
|
studio_id: studio.getNewValue()?.stored_id,
|
||||||
performer_ids: performers.getNewValue()?.map((p) => p.stored_id!),
|
performer_ids: performers.getNewValue()?.map((p) => p.stored_id!),
|
||||||
movies: movies.getNewValue()?.map((m) => {
|
movies: movies.getNewValue()?.map((m) => {
|
||||||
// find the equivalent movie in the original scenes
|
// find the equivalent movie in the original scenes
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ const SelectComponent = <T, IsMulti extends boolean>(
|
|||||||
...props,
|
...props,
|
||||||
styles,
|
styles,
|
||||||
defaultOptions: true,
|
defaultOptions: true,
|
||||||
|
isClearable: true,
|
||||||
value: selectedOptions ?? null,
|
value: selectedOptions ?? null,
|
||||||
className: cx("react-select", props.className),
|
className: cx("react-select", props.className),
|
||||||
classNamePrefix: "react-select",
|
classNamePrefix: "react-select",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { MovieSelect, StudioSelect } from "src/components/Shared/Select";
|
import { MovieSelect } from "src/components/Shared/Select";
|
||||||
import {
|
import {
|
||||||
ScrapeDialogRow,
|
ScrapeDialogRow,
|
||||||
IHasName,
|
IHasName,
|
||||||
@@ -8,11 +8,12 @@ import {
|
|||||||
import { PerformerSelect } from "src/components/Performers/PerformerSelect";
|
import { PerformerSelect } from "src/components/Performers/PerformerSelect";
|
||||||
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||||
import { TagSelect } from "src/components/Tags/TagSelect";
|
import { TagSelect } from "src/components/Tags/TagSelect";
|
||||||
|
import { StudioSelect } from "src/components/Studios/StudioSelect";
|
||||||
|
|
||||||
interface IScrapedStudioRow {
|
interface IScrapedStudioRow {
|
||||||
title: string;
|
title: string;
|
||||||
result: ScrapeResult<string>;
|
result: ScrapeResult<GQL.ScrapedStudio>;
|
||||||
onChange: (value: ScrapeResult<string>) => void;
|
onChange: (value: ScrapeResult<GQL.ScrapedStudio>) => void;
|
||||||
newStudio?: GQL.ScrapedStudio;
|
newStudio?: GQL.ScrapedStudio;
|
||||||
onCreateNew?: (value: GQL.ScrapedStudio) => void;
|
onCreateNew?: (value: GQL.ScrapedStudio) => void;
|
||||||
}
|
}
|
||||||
@@ -25,25 +26,34 @@ export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
|||||||
onCreateNew,
|
onCreateNew,
|
||||||
}) => {
|
}) => {
|
||||||
function renderScrapedStudio(
|
function renderScrapedStudio(
|
||||||
scrapeResult: ScrapeResult<string>,
|
scrapeResult: ScrapeResult<GQL.ScrapedStudio>,
|
||||||
isNew?: boolean,
|
isNew?: boolean,
|
||||||
onChangeFn?: (value: string) => void
|
onChangeFn?: (value: GQL.ScrapedStudio) => void
|
||||||
) {
|
) {
|
||||||
const resultValue = isNew
|
const resultValue = isNew
|
||||||
? scrapeResult.newValue
|
? scrapeResult.newValue
|
||||||
: scrapeResult.originalValue;
|
: scrapeResult.originalValue;
|
||||||
const value = resultValue ? [resultValue] : [];
|
const value = resultValue ? [resultValue] : [];
|
||||||
|
|
||||||
|
const selectValue = value.map((p) => {
|
||||||
|
const aliases: string[] = [];
|
||||||
|
return {
|
||||||
|
id: p.stored_id ?? "",
|
||||||
|
name: p.name ?? "",
|
||||||
|
aliases,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
className="form-control react-select"
|
className="form-control react-select"
|
||||||
isDisabled={!isNew}
|
isDisabled={!isNew}
|
||||||
onSelect={(items) => {
|
onSelect={(items) => {
|
||||||
if (onChangeFn) {
|
if (onChangeFn) {
|
||||||
onChangeFn(items[0]?.id);
|
onChangeFn(items[0]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
ids={value}
|
values={selectValue}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ function useCreateObject<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IUseCreateNewStudioProps {
|
interface IUseCreateNewStudioProps {
|
||||||
scrapeResult: ScrapeResult<string>;
|
scrapeResult: ScrapeResult<GQL.ScrapedStudio>;
|
||||||
setScrapeResult: (scrapeResult: ScrapeResult<string>) => void;
|
setScrapeResult: (scrapeResult: ScrapeResult<GQL.ScrapedStudio>) => void;
|
||||||
setNewObject: (newObject: GQL.ScrapedStudio | undefined) => void;
|
setNewObject: (newObject: GQL.ScrapedStudio | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,12 @@ export function useCreateScrapedStudio(props: IUseCreateNewStudioProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// set the new studio as the value
|
// set the new studio as the value
|
||||||
setScrapeResult(scrapeResult.cloneWithValue(result.data!.studioCreate!.id));
|
setScrapeResult(
|
||||||
|
scrapeResult.cloneWithValue({
|
||||||
|
stored_id: result.data!.studioCreate!.id,
|
||||||
|
name: toCreate.name,
|
||||||
|
})
|
||||||
|
);
|
||||||
setNewObject(undefined);
|
setNewObject(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import Select, {
|
import Select, {
|
||||||
OnChangeValue,
|
OnChangeValue,
|
||||||
StylesConfig,
|
StylesConfig,
|
||||||
@@ -15,9 +15,7 @@ import CreatableSelect from "react-select/creatable";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
useAllMoviesForFilter,
|
useAllMoviesForFilter,
|
||||||
useAllStudiosForFilter,
|
|
||||||
useMarkerStrings,
|
useMarkerStrings,
|
||||||
useStudioCreate,
|
|
||||||
useMovieCreate,
|
useMovieCreate,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
@@ -33,6 +31,7 @@ import { PerformerIDSelect } from "../Performers/PerformerSelect";
|
|||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
import { faTableColumns } from "@fortawesome/free-solid-svg-icons";
|
import { faTableColumns } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { TagIDSelect } from "../Tags/TagSelect";
|
import { TagIDSelect } from "../Tags/TagSelect";
|
||||||
|
import { StudioIDSelect } from "../Studios/StudioSelect";
|
||||||
|
|
||||||
export type SelectObject = {
|
export type SelectObject = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -534,144 +533,7 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
|||||||
export const StudioSelect: React.FC<
|
export const StudioSelect: React.FC<
|
||||||
IFilterProps & { excludeIds?: string[] }
|
IFilterProps & { excludeIds?: string[] }
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const [studioAliases, setStudioAliases] = useState<Record<string, string[]>>(
|
return <StudioIDSelect {...props} />;
|
||||||
{}
|
|
||||||
);
|
|
||||||
const [allAliases, setAllAliases] = useState<string[]>([]);
|
|
||||||
const { data, loading } = useAllStudiosForFilter();
|
|
||||||
const [createStudio] = useStudioCreate();
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
|
||||||
const defaultCreatable =
|
|
||||||
!configuration?.interface.disableDropdownCreate.studio ?? true;
|
|
||||||
|
|
||||||
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
|
|
||||||
const studios = useMemo(
|
|
||||||
() =>
|
|
||||||
(data?.allStudios ?? []).filter((studio) => !exclude.includes(studio.id)),
|
|
||||||
[data?.allStudios, exclude]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// build the studio aliases map
|
|
||||||
const newAliases: Record<string, string[]> = {};
|
|
||||||
const newAll: string[] = [];
|
|
||||||
studios.forEach((s) => {
|
|
||||||
newAliases[s.id] = s.aliases;
|
|
||||||
newAll.push(...s.aliases);
|
|
||||||
});
|
|
||||||
setStudioAliases(newAliases);
|
|
||||||
setAllAliases(newAll);
|
|
||||||
}, [studios]);
|
|
||||||
|
|
||||||
const StudioOption: React.FC<OptionProps<Option, boolean>> = (
|
|
||||||
optionProps
|
|
||||||
) => {
|
|
||||||
const { inputValue } = optionProps.selectProps;
|
|
||||||
|
|
||||||
let thisOptionProps = optionProps;
|
|
||||||
if (
|
|
||||||
inputValue &&
|
|
||||||
!optionProps.label.toLowerCase().includes(inputValue.toLowerCase())
|
|
||||||
) {
|
|
||||||
// must be alias
|
|
||||||
const newLabel = `${optionProps.data.label} (alias)`;
|
|
||||||
thisOptionProps = {
|
|
||||||
...optionProps,
|
|
||||||
children: newLabel,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return <reactSelectComponents.Option {...thisOptionProps} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterOption = (option: Option, rawInput: string): boolean => {
|
|
||||||
if (!rawInput) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = rawInput.toLowerCase();
|
|
||||||
const optionVal = option.label.toLowerCase();
|
|
||||||
|
|
||||||
if (optionVal.includes(input)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// search for studio aliases
|
|
||||||
const aliases = studioAliases[option.value];
|
|
||||||
// only match on alias if exact
|
|
||||||
if (aliases && aliases.some((a) => a.toLowerCase() === input)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCreate = async (name: string) => {
|
|
||||||
const result = await createStudio({
|
|
||||||
variables: {
|
|
||||||
input: { name },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
item: result.data!.studioCreate!,
|
|
||||||
message: intl.formatMessage(
|
|
||||||
{ id: "toast.created_entity" },
|
|
||||||
{ entity: intl.formatMessage({ id: "studio" }).toLocaleLowerCase() }
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const isValidNewOption = (
|
|
||||||
inputValue: string,
|
|
||||||
value: OnChangeValue<Option, boolean>,
|
|
||||||
options: OptionsOrGroups<Option, GroupBase<Option>>
|
|
||||||
) => {
|
|
||||||
if (!inputValue) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(options as Options<Option>).some((o: Option) => {
|
|
||||||
return o.label.toLowerCase() === inputValue.toLowerCase();
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allAliases.some((a) => a.toLowerCase() === inputValue.toLowerCase())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FilterSelectComponent
|
|
||||||
{...props}
|
|
||||||
filterOption={filterOption}
|
|
||||||
isValidNewOption={isValidNewOption}
|
|
||||||
components={{ Option: StudioOption }}
|
|
||||||
isMulti={props.isMulti ?? false}
|
|
||||||
type="studios"
|
|
||||||
isLoading={loading}
|
|
||||||
items={studios}
|
|
||||||
placeholder={
|
|
||||||
props.noSelectionString ??
|
|
||||||
intl.formatMessage(
|
|
||||||
{ id: "actions.select_entity" },
|
|
||||||
{
|
|
||||||
entityType: intl.formatMessage({
|
|
||||||
id: props.isMulti ? "studios" : "studio",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
creatable={props.creatable ?? defaultCreatable}
|
|
||||||
onCreate={onCreate}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MovieSelect: React.FC<IFilterProps> = (props) => {
|
export const MovieSelect: React.FC<IFilterProps> = (props) => {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { StudioSelect } from "src/components/Shared/Select";
|
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import ImageUtils from "src/utils/image";
|
import ImageUtils from "src/utils/image";
|
||||||
@@ -16,6 +15,7 @@ import { useToast } from "src/hooks/Toast";
|
|||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
import { formikUtils } from "src/utils/form";
|
import { formikUtils } from "src/utils/form";
|
||||||
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
|
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
|
||||||
|
import { Studio, StudioSelect } from "../StudioSelect";
|
||||||
|
|
||||||
interface IStudioEditPanel {
|
interface IStudioEditPanel {
|
||||||
studio: Partial<GQL.StudioDataFragment>;
|
studio: Partial<GQL.StudioDataFragment>;
|
||||||
@@ -42,6 +42,8 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [parentStudio, setParentStudio] = useState<Studio | null>(null);
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
url: yup.string().ensure(),
|
url: yup.string().ensure(),
|
||||||
@@ -73,10 +75,27 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
onSubmit: (values) => onSave(schema.cast(values)),
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onSetParentStudio(item: Studio | null) {
|
||||||
|
setParentStudio(item);
|
||||||
|
formik.setFieldValue("parent_id", item ? item.id : null);
|
||||||
|
}
|
||||||
|
|
||||||
const encodingImage = ImageUtils.usePasteImage((imageData) =>
|
const encodingImage = ImageUtils.usePasteImage((imageData) =>
|
||||||
formik.setFieldValue("image", imageData)
|
formik.setFieldValue("image", imageData)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setParentStudio(
|
||||||
|
studio.parent_studio
|
||||||
|
? {
|
||||||
|
id: studio.parent_studio.id,
|
||||||
|
name: studio.parent_studio.name,
|
||||||
|
aliases: [],
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}, [studio.parent_studio]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setImage(formik.values.image);
|
setImage(formik.values.image);
|
||||||
}, [formik.values.image, setImage]);
|
}, [formik.values.image, setImage]);
|
||||||
@@ -129,12 +148,9 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
const control = (
|
const control = (
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
onSelect={(items) =>
|
onSelect={(items) =>
|
||||||
formik.setFieldValue(
|
onSetParentStudio(items.length > 0 ? items[0] : null)
|
||||||
"parent_id",
|
|
||||||
items.length > 0 ? items[0]?.id : null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
ids={formik.values.parent_id ? [formik.values.parent_id] : []}
|
values={parentStudio ? [parentStudio] : []}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
259
ui/v2.5/src/components/Studios/StudioSelect.tsx
Normal file
259
ui/v2.5/src/components/Studios/StudioSelect.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
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 {
|
||||||
|
useStudioCreate,
|
||||||
|
queryFindStudiosByIDForSelect,
|
||||||
|
queryFindStudiosForSelect,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { defaultMaxOptionsShown, IUIConfig } 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";
|
||||||
|
|
||||||
|
export type SelectObject = {
|
||||||
|
id: string;
|
||||||
|
name?: string | null;
|
||||||
|
title?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Studio = Pick<GQL.Studio, "id" | "name" | "aliases" | "image_path">;
|
||||||
|
type Option = SelectOption<Studio>;
|
||||||
|
|
||||||
|
export const StudioSelect: React.FC<
|
||||||
|
IFilterProps &
|
||||||
|
IFilterValueProps<Studio> & {
|
||||||
|
hoverPlacement?: Placement;
|
||||||
|
excludeIds?: string[];
|
||||||
|
}
|
||||||
|
> = (props) => {
|
||||||
|
const [createStudio] = useStudioCreate();
|
||||||
|
|
||||||
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
const intl = useIntl();
|
||||||
|
const maxOptionsShown =
|
||||||
|
(configuration?.ui as IUIConfig).maxOptionsShown ?? defaultMaxOptionsShown;
|
||||||
|
const defaultCreatable =
|
||||||
|
!configuration?.interface.disableDropdownCreate.studio ?? true;
|
||||||
|
|
||||||
|
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
|
||||||
|
|
||||||
|
async function loadStudios(input: string): Promise<Option[]> {
|
||||||
|
const filter = new ListFilterModel(GQL.FilterMode.Studios);
|
||||||
|
filter.searchTerm = input;
|
||||||
|
filter.currentPage = 1;
|
||||||
|
filter.itemsPerPage = maxOptionsShown;
|
||||||
|
filter.sortBy = "name";
|
||||||
|
filter.sortDirection = GQL.SortDirectionEnum.Asc;
|
||||||
|
const query = await queryFindStudiosForSelect(filter);
|
||||||
|
return query.data.findStudios.studios
|
||||||
|
.filter((studio) => {
|
||||||
|
// HACK - we should probably exclude these in the backend query, but
|
||||||
|
// this will do in the short-term
|
||||||
|
return !exclude.includes(studio.id.toString());
|
||||||
|
})
|
||||||
|
.map((studio) => ({
|
||||||
|
value: studio.id,
|
||||||
|
object: studio,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const StudioOption: React.FC<OptionProps<Option, boolean>> = (
|
||||||
|
optionProps
|
||||||
|
) => {
|
||||||
|
let thisOptionProps = optionProps;
|
||||||
|
|
||||||
|
const { object } = optionProps.data;
|
||||||
|
|
||||||
|
let { name } = object;
|
||||||
|
|
||||||
|
// if name does not match the input value but an alias does, show the alias
|
||||||
|
const { inputValue } = optionProps.selectProps;
|
||||||
|
let alias: string | undefined = "";
|
||||||
|
if (!name.toLowerCase().includes(inputValue.toLowerCase())) {
|
||||||
|
alias = object.aliases?.find((a) =>
|
||||||
|
a.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
thisOptionProps = {
|
||||||
|
...optionProps,
|
||||||
|
children: (
|
||||||
|
<span className="react-select-image-option">
|
||||||
|
<span>{name}</span>
|
||||||
|
{alias && <span className="alias">{` (${alias})`}</span>}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return <reactSelectComponents.Option {...thisOptionProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StudioMultiValueLabel: React.FC<
|
||||||
|
MultiValueGenericProps<Option, boolean>
|
||||||
|
> = (optionProps) => {
|
||||||
|
let thisOptionProps = optionProps;
|
||||||
|
|
||||||
|
const { object } = optionProps.data;
|
||||||
|
|
||||||
|
thisOptionProps = {
|
||||||
|
...optionProps,
|
||||||
|
children: object.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <reactSelectComponents.MultiValueLabel {...thisOptionProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StudioValueLabel: 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 createStudio({
|
||||||
|
variables: { input: { name } },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
value: result.data!.studioCreate!.id,
|
||||||
|
item: result.data!.studioCreate!,
|
||||||
|
message: "Created studio",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNamedObject = (id: string, name: string) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
aliases: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidNewOption = (inputValue: string, options: Studio[]) => {
|
||||||
|
if (!inputValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.some((o) => {
|
||||||
|
return (
|
||||||
|
o.name.toLowerCase() === inputValue.toLowerCase() ||
|
||||||
|
o.aliases?.some((a) => a.toLowerCase() === inputValue.toLowerCase())
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterSelectComponent<Studio, boolean>
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"studio-select",
|
||||||
|
{
|
||||||
|
"studio-select-active": props.active,
|
||||||
|
},
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
loadOptions={loadStudios}
|
||||||
|
getNamedObject={getNamedObject}
|
||||||
|
isValidNewOption={isValidNewOption}
|
||||||
|
components={{
|
||||||
|
Option: StudioOption,
|
||||||
|
MultiValueLabel: StudioMultiValueLabel,
|
||||||
|
SingleValue: StudioValueLabel,
|
||||||
|
}}
|
||||||
|
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 ? "studios" : "studio",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
closeMenuOnSelect={!props.isMulti}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StudioIDSelect: React.FC<IFilterProps & IFilterIDProps<Studio>> = (
|
||||||
|
props
|
||||||
|
) => {
|
||||||
|
const { ids, onSelect: onSelectValues } = props;
|
||||||
|
|
||||||
|
const [values, setValues] = useState<Studio[]>([]);
|
||||||
|
const idsChanged = useCompare(ids);
|
||||||
|
|
||||||
|
function onSelect(items: Studio[]) {
|
||||||
|
setValues(items);
|
||||||
|
onSelectValues?.(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadObjectsByID(idsToLoad: string[]): Promise<Studio[]> {
|
||||||
|
const studioIDs = idsToLoad.map((id) => parseInt(id));
|
||||||
|
const query = await queryFindStudiosByIDForSelect(studioIDs);
|
||||||
|
const { studios: loadedStudios } = query.data.findStudios;
|
||||||
|
|
||||||
|
return loadedStudios;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <StudioSelect {...props} values={values} onSelect={onSelect} />;
|
||||||
|
};
|
||||||
@@ -319,7 +319,22 @@ export const queryFindStudios = (filter: ListFilterModel) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAllStudiosForFilter = () => GQL.useAllStudiosForFilterQuery();
|
export const queryFindStudiosByIDForSelect = (studioIDs: number[]) =>
|
||||||
|
client.query<GQL.FindStudiosForSelectQuery>({
|
||||||
|
query: GQL.FindStudiosForSelectDocument,
|
||||||
|
variables: {
|
||||||
|
ids: studioIDs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const queryFindStudiosForSelect = (filter: ListFilterModel) =>
|
||||||
|
client.query<GQL.FindStudiosForSelectQuery>({
|
||||||
|
query: GQL.FindStudiosForSelectDocument,
|
||||||
|
variables: {
|
||||||
|
filter: filter.makeFindFilter(),
|
||||||
|
studio_filter: filter.makeFilter(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const useFindTag = (id: string) => {
|
export const useFindTag = (id: string) => {
|
||||||
const skip = id === "new" || id === "";
|
const skip = id === "new" || id === "";
|
||||||
@@ -1519,8 +1534,6 @@ export const useStudioCreate = () =>
|
|||||||
const studio = result.data?.studioCreate;
|
const studio = result.data?.studioCreate;
|
||||||
if (!studio || !variables) return;
|
if (!studio || !variables) return;
|
||||||
|
|
||||||
appendObject(cache, studio, GQL.AllStudiosForFilterDocument);
|
|
||||||
|
|
||||||
// update stats
|
// update stats
|
||||||
updateStats(cache, "studio_count", 1);
|
updateStats(cache, "studio_count", 1);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user