mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Performer select refactor (#4013)
* Overhaul performer select * Add interface to load performers by id * Add Performer ID select and replace existing
This commit is contained in:
parent
3dc01a9362
commit
e40b3d78b2
15 changed files with 667 additions and 226 deletions
|
|
@ -34,3 +34,10 @@ fragment SlimPerformerData on Performer {
|
||||||
death_date
|
death_date
|
||||||
weight
|
weight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragment SelectPerformerData on Performer {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
disambiguation
|
||||||
|
alias_list
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,6 @@ query MarkerStrings($q: String, $sort: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query AllPerformersForFilter {
|
|
||||||
allPerformers {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
disambiguation
|
|
||||||
alias_list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query AllStudiosForFilter {
|
query AllStudiosForFilter {
|
||||||
allStudios {
|
allStudios {
|
||||||
id
|
id
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
query FindPerformers(
|
query FindPerformers(
|
||||||
$filter: FindFilterType
|
$filter: FindFilterType
|
||||||
$performer_filter: PerformerFilterType
|
$performer_filter: PerformerFilterType
|
||||||
|
$performer_ids: [Int!]
|
||||||
) {
|
) {
|
||||||
findPerformers(filter: $filter, performer_filter: $performer_filter) {
|
findPerformers(
|
||||||
|
filter: $filter
|
||||||
|
performer_filter: $performer_filter
|
||||||
|
performer_ids: $performer_ids
|
||||||
|
) {
|
||||||
count
|
count
|
||||||
performers {
|
performers {
|
||||||
...PerformerData
|
...PerformerData
|
||||||
|
|
@ -15,3 +20,20 @@ query FindPerformer($id: ID!) {
|
||||||
...PerformerData
|
...PerformerData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query FindPerformersForSelect(
|
||||||
|
$filter: FindFilterType
|
||||||
|
$performer_filter: PerformerFilterType
|
||||||
|
$performer_ids: [Int!]
|
||||||
|
) {
|
||||||
|
findPerformers(
|
||||||
|
filter: $filter
|
||||||
|
performer_filter: $performer_filter
|
||||||
|
performer_ids: $performer_ids
|
||||||
|
) {
|
||||||
|
count
|
||||||
|
performers {
|
||||||
|
...SelectPerformerData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ type Query {
|
||||||
findPerformers(
|
findPerformers(
|
||||||
performer_filter: PerformerFilterType
|
performer_filter: PerformerFilterType
|
||||||
filter: FindFilterType
|
filter: FindFilterType
|
||||||
|
performer_ids: [Int!]
|
||||||
): FindPerformersResultType!
|
): FindPerformersResultType!
|
||||||
|
|
||||||
"Find a studio by ID"
|
"Find a studio by ID"
|
||||||
|
|
@ -223,11 +224,13 @@ type Query {
|
||||||
allSceneMarkers: [SceneMarker!]!
|
allSceneMarkers: [SceneMarker!]!
|
||||||
allImages: [Image!]!
|
allImages: [Image!]!
|
||||||
allGalleries: [Gallery!]!
|
allGalleries: [Gallery!]!
|
||||||
allPerformers: [Performer!]!
|
|
||||||
allStudios: [Studio!]!
|
allStudios: [Studio!]!
|
||||||
allMovies: [Movie!]!
|
allMovies: [Movie!]!
|
||||||
allTags: [Tag!]!
|
allTags: [Tag!]!
|
||||||
|
|
||||||
|
# @deprecated
|
||||||
|
allPerformers: [Performer!]!
|
||||||
|
|
||||||
# Get everything with minimal metadata
|
# Get everything with minimal metadata
|
||||||
|
|
||||||
# Version
|
# Version
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,19 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (ret *FindPerformersResultType, err error) {
|
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType, performerIDs []int) (ret *FindPerformersResultType, err error) {
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
performers, total, err := r.repository.Performer.Query(ctx, performerFilter, filter)
|
var performers []*models.Performer
|
||||||
|
var err error
|
||||||
|
var total int
|
||||||
|
|
||||||
|
if len(performerIDs) > 0 {
|
||||||
|
performers, err = r.repository.Performer.FindMany(ctx, performerIDs)
|
||||||
|
total = len(performers)
|
||||||
|
} else {
|
||||||
|
performers, total, err = r.repository.Performer.Query(ctx, performerFilter, filter)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +44,7 @@ func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *mod
|
||||||
Count: total,
|
Count: total,
|
||||||
Performers: performers,
|
Performers: performers,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import {
|
||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import {
|
import {
|
||||||
PerformerSelect,
|
|
||||||
TagSelect,
|
TagSelect,
|
||||||
SceneSelect,
|
SceneSelect,
|
||||||
StudioSelect,
|
StudioSelect,
|
||||||
|
|
@ -39,6 +38,10 @@ import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { DateInput } from "src/components/Shared/DateInput";
|
import { DateInput } from "src/components/Shared/DateInput";
|
||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
|
import {
|
||||||
|
Performer,
|
||||||
|
PerformerSelect,
|
||||||
|
} from "src/components/Performers/PerformerSelect";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
|
|
@ -62,6 +65,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [performers, setPerformers] = useState<Performer[]>([]);
|
||||||
|
|
||||||
const isNew = gallery.id === undefined;
|
const isNew = gallery.id === undefined;
|
||||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
|
|
@ -139,12 +144,24 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSetPerformers(items: Performer[]) {
|
||||||
|
setPerformers(items);
|
||||||
|
formik.setFieldValue(
|
||||||
|
"performer_ids",
|
||||||
|
items.map((item) => item.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
useRatingKeybinds(
|
useRatingKeybinds(
|
||||||
isVisible,
|
isVisible,
|
||||||
stashConfig?.ui?.ratingSystemOptions?.type,
|
stashConfig?.ui?.ratingSystemOptions?.type,
|
||||||
setRating
|
setRating
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPerformers(gallery.performers ?? []);
|
||||||
|
}, [gallery.performers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
Mousetrap.bind("s s", () => {
|
Mousetrap.bind("s s", () => {
|
||||||
|
|
@ -309,8 +326,15 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (idPerfs.length > 0) {
|
if (idPerfs.length > 0) {
|
||||||
const newIds = idPerfs.map((p) => p.stored_id);
|
onSetPerformers(
|
||||||
formik.setFieldValue("performer_ids", newIds as string[]);
|
idPerfs.map((p) => {
|
||||||
|
return {
|
||||||
|
id: p.stored_id!,
|
||||||
|
name: p.name ?? "",
|
||||||
|
alias_list: [],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,13 +496,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
<Col sm={9} xl={12}>
|
<Col sm={9} xl={12}>
|
||||||
<PerformerSelect
|
<PerformerSelect
|
||||||
isMulti
|
isMulti
|
||||||
onSelect={(items) =>
|
onSelect={onSetPerformers}
|
||||||
formik.setFieldValue(
|
values={performers}
|
||||||
"performer_ids",
|
|
||||||
items.map((item) => item.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.performer_ids}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@ 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 {
|
import { TagSelect, StudioSelect } from "src/components/Shared/Select";
|
||||||
PerformerSelect,
|
|
||||||
TagSelect,
|
|
||||||
StudioSelect,
|
|
||||||
} from "src/components/Shared/Select";
|
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
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";
|
||||||
|
|
@ -20,6 +16,10 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { DateInput } from "src/components/Shared/DateInput";
|
import { DateInput } from "src/components/Shared/DateInput";
|
||||||
|
import {
|
||||||
|
Performer,
|
||||||
|
PerformerSelect,
|
||||||
|
} from "src/components/Performers/PerformerSelect";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
image: GQL.ImageDataFragment;
|
image: GQL.ImageDataFragment;
|
||||||
|
|
@ -42,6 +42,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||||
|
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
|
const [performers, setPerformers] = useState<Performer[]>([]);
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: yup.string().ensure(),
|
title: yup.string().ensure(),
|
||||||
url: yup.string().ensure(),
|
url: yup.string().ensure(),
|
||||||
|
|
@ -87,12 +89,24 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||||
formik.setFieldValue("rating100", v);
|
formik.setFieldValue("rating100", v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSetPerformers(items: Performer[]) {
|
||||||
|
setPerformers(items);
|
||||||
|
formik.setFieldValue(
|
||||||
|
"performer_ids",
|
||||||
|
items.map((item) => item.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
useRatingKeybinds(
|
useRatingKeybinds(
|
||||||
true,
|
true,
|
||||||
configuration?.ui?.ratingSystemOptions?.type,
|
configuration?.ui?.ratingSystemOptions?.type,
|
||||||
setRating
|
setRating
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPerformers(image.performers ?? []);
|
||||||
|
}, [image.performers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
Mousetrap.bind("s s", () => {
|
Mousetrap.bind("s s", () => {
|
||||||
|
|
@ -249,13 +263,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||||
<Col sm={9} xl={12}>
|
<Col sm={9} xl={12}>
|
||||||
<PerformerSelect
|
<PerformerSelect
|
||||||
isMulti
|
isMulti
|
||||||
onSelect={(items) =>
|
onSelect={onSetPerformers}
|
||||||
formik.setFieldValue(
|
values={performers}
|
||||||
"performer_ids",
|
|
||||||
items.map((item) => item.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.performer_ids}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
|
||||||
241
ui/v2.5/src/components/Performers/PerformerSelect.tsx
Normal file
241
ui/v2.5/src/components/Performers/PerformerSelect.tsx
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
OptionProps,
|
||||||
|
components as reactSelectComponents,
|
||||||
|
MultiValueGenericProps,
|
||||||
|
SingleValueProps,
|
||||||
|
} from "react-select";
|
||||||
|
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import {
|
||||||
|
usePerformerCreate,
|
||||||
|
queryFindPerformersByIDForSelect,
|
||||||
|
queryFindPerformersForSelect,
|
||||||
|
} 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";
|
||||||
|
|
||||||
|
export type SelectObject = {
|
||||||
|
id: string;
|
||||||
|
name?: string | null;
|
||||||
|
title?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Performer = Pick<
|
||||||
|
GQL.Performer,
|
||||||
|
"id" | "name" | "alias_list" | "disambiguation"
|
||||||
|
>;
|
||||||
|
type Option = SelectOption<Performer>;
|
||||||
|
|
||||||
|
export const PerformerSelect: React.FC<
|
||||||
|
IFilterProps & IFilterValueProps<Performer>
|
||||||
|
> = (props) => {
|
||||||
|
const [createPerformer] = usePerformerCreate();
|
||||||
|
|
||||||
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
const intl = useIntl();
|
||||||
|
const maxOptionsShown =
|
||||||
|
(configuration?.ui as IUIConfig).maxOptionsShown ?? defaultMaxOptionsShown;
|
||||||
|
const defaultCreatable =
|
||||||
|
!configuration?.interface.disableDropdownCreate.performer ?? true;
|
||||||
|
|
||||||
|
async function loadPerformers(input: string): Promise<Option[]> {
|
||||||
|
const filter = new ListFilterModel(GQL.FilterMode.Performers);
|
||||||
|
filter.searchTerm = input;
|
||||||
|
filter.currentPage = 1;
|
||||||
|
filter.itemsPerPage = maxOptionsShown;
|
||||||
|
filter.sortBy = "name";
|
||||||
|
filter.sortDirection = GQL.SortDirectionEnum.Asc;
|
||||||
|
const query = await queryFindPerformersForSelect(filter);
|
||||||
|
return query.data.findPerformers.performers.map((performer) => ({
|
||||||
|
value: performer.id,
|
||||||
|
object: performer,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const PerformerOption: 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.alias_list?.find((a) =>
|
||||||
|
a.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
thisOptionProps = {
|
||||||
|
...optionProps,
|
||||||
|
children: (
|
||||||
|
<span>
|
||||||
|
<span>{name}</span>
|
||||||
|
{object.disambiguation && (
|
||||||
|
<span className="performer-disambiguation">{` (${object.disambiguation})`}</span>
|
||||||
|
)}
|
||||||
|
{alias && <span className="alias">{` (${alias})`}</span>}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return <reactSelectComponents.Option {...thisOptionProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PerformerMultiValueLabel: React.FC<
|
||||||
|
MultiValueGenericProps<Option, boolean>
|
||||||
|
> = (optionProps) => {
|
||||||
|
let thisOptionProps = optionProps;
|
||||||
|
|
||||||
|
const { object } = optionProps.data;
|
||||||
|
|
||||||
|
thisOptionProps = {
|
||||||
|
...optionProps,
|
||||||
|
children: object.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <reactSelectComponents.MultiValueLabel {...thisOptionProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PerformerValueLabel: 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 createPerformer({
|
||||||
|
variables: { input: { name } },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
value: result.data!.performerCreate!.id,
|
||||||
|
item: result.data!.performerCreate!,
|
||||||
|
message: "Created performer",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNamedObject = (id: string, name: string) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
alias_list: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidNewOption = (inputValue: string, options: Performer[]) => {
|
||||||
|
if (!inputValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.some((o) => {
|
||||||
|
return (
|
||||||
|
o.name.toLowerCase() === inputValue.toLowerCase() ||
|
||||||
|
o.alias_list?.some(
|
||||||
|
(a) => a.toLowerCase() === inputValue.toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterSelectComponent<Performer, boolean>
|
||||||
|
{...props}
|
||||||
|
loadOptions={loadPerformers}
|
||||||
|
getNamedObject={getNamedObject}
|
||||||
|
isValidNewOption={isValidNewOption}
|
||||||
|
components={{
|
||||||
|
Option: PerformerOption,
|
||||||
|
MultiValueLabel: PerformerMultiValueLabel,
|
||||||
|
SingleValue: PerformerValueLabel,
|
||||||
|
}}
|
||||||
|
isMulti={props.isMulti ?? false}
|
||||||
|
creatable={props.creatable ?? defaultCreatable}
|
||||||
|
onCreate={onCreate}
|
||||||
|
placeholder={
|
||||||
|
props.noSelectionString ??
|
||||||
|
intl.formatMessage(
|
||||||
|
{ id: "actions.select_entity" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "performer" }) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PerformerIDSelect: React.FC<
|
||||||
|
IFilterProps & IFilterIDProps<Performer>
|
||||||
|
> = (props) => {
|
||||||
|
const { ids, onSelect: onSelectValues } = props;
|
||||||
|
|
||||||
|
const [values, setValues] = useState<Performer[]>([]);
|
||||||
|
const idsChanged = useCompare(ids);
|
||||||
|
|
||||||
|
function onSelect(items: Performer[]) {
|
||||||
|
setValues(items);
|
||||||
|
onSelectValues?.(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadObjectsByID(idsToLoad: string[]): Promise<Performer[]> {
|
||||||
|
const performerIDs = idsToLoad.map((id) => parseInt(id));
|
||||||
|
const query = await queryFindPerformersByIDForSelect(performerIDs);
|
||||||
|
const { performers: loadedPerformers } = query.data.findPerformers;
|
||||||
|
|
||||||
|
return loadedPerformers;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <PerformerSelect {...props} values={values} onSelect={onSelect} />;
|
||||||
|
};
|
||||||
|
|
@ -223,3 +223,7 @@
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-select .alias {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import {
|
||||||
queryScrapeSceneQueryFragment,
|
queryScrapeSceneQueryFragment,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import {
|
import {
|
||||||
PerformerSelect,
|
|
||||||
TagSelect,
|
TagSelect,
|
||||||
StudioSelect,
|
StudioSelect,
|
||||||
GallerySelect,
|
GallerySelect,
|
||||||
|
|
@ -51,6 +50,10 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
import { lazyComponent } from "src/utils/lazyComponent";
|
import { lazyComponent } from "src/utils/lazyComponent";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { DateInput } from "src/components/Shared/DateInput";
|
import { DateInput } from "src/components/Shared/DateInput";
|
||||||
|
import {
|
||||||
|
Performer,
|
||||||
|
PerformerSelect,
|
||||||
|
} from "src/components/Performers/PerformerSelect";
|
||||||
|
|
||||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||||
|
|
@ -78,6 +81,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
|
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
const [performers, setPerformers] = useState<Performer[]>([]);
|
||||||
|
|
||||||
const Scrapers = useListSceneScrapers();
|
const Scrapers = useListSceneScrapers();
|
||||||
const [fragmentScrapers, setFragmentScrapers] = useState<GQL.Scraper[]>([]);
|
const [fragmentScrapers, setFragmentScrapers] = useState<GQL.Scraper[]>([]);
|
||||||
|
|
@ -98,6 +102,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
);
|
);
|
||||||
}, [scene.galleries]);
|
}, [scene.galleries]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPerformers(scene.performers ?? []);
|
||||||
|
}, [scene.performers]);
|
||||||
|
|
||||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
|
|
@ -218,6 +226,14 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSetPerformers(items: Performer[]) {
|
||||||
|
setPerformers(items);
|
||||||
|
formik.setFieldValue(
|
||||||
|
"performer_ids",
|
||||||
|
items.map((item) => item.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
useRatingKeybinds(
|
useRatingKeybinds(
|
||||||
isVisible,
|
isVisible,
|
||||||
stashConfig?.ui?.ratingSystemOptions?.type,
|
stashConfig?.ui?.ratingSystemOptions?.type,
|
||||||
|
|
@ -581,8 +597,15 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (idPerfs.length > 0) {
|
if (idPerfs.length > 0) {
|
||||||
const newIds = idPerfs.map((p) => p.stored_id);
|
onSetPerformers(
|
||||||
formik.setFieldValue("performer_ids", newIds as string[]);
|
idPerfs.map((p) => {
|
||||||
|
return {
|
||||||
|
id: p.stored_id!,
|
||||||
|
name: p.name ?? "",
|
||||||
|
alias_list: [],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -852,13 +875,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
<Col sm={9} xl={12}>
|
<Col sm={9} xl={12}>
|
||||||
<PerformerSelect
|
<PerformerSelect
|
||||||
isMulti
|
isMulti
|
||||||
onSelect={(items) =>
|
onSelect={onSetPerformers}
|
||||||
formik.setFieldValue(
|
values={performers}
|
||||||
"performer_ids",
|
|
||||||
items.map((item) => item.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.performer_ids}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
|
||||||
257
ui/v2.5/src/components/Shared/FilterSelect.tsx
Normal file
257
ui/v2.5/src/components/Shared/FilterSelect.tsx
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
OnChangeValue,
|
||||||
|
StylesConfig,
|
||||||
|
GroupBase,
|
||||||
|
OptionsOrGroups,
|
||||||
|
Options,
|
||||||
|
} from "react-select";
|
||||||
|
import AsyncSelect from "react-select/async";
|
||||||
|
import AsyncCreatableSelect, {
|
||||||
|
AsyncCreatableProps,
|
||||||
|
} from "react-select/async-creatable";
|
||||||
|
|
||||||
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
import { useDebounce } from "src/hooks/debounce";
|
||||||
|
|
||||||
|
interface IHasID {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Option<T> = { value: string; object: T };
|
||||||
|
|
||||||
|
interface ISelectProps<T, IsMulti extends boolean>
|
||||||
|
extends AsyncCreatableProps<Option<T>, IsMulti, GroupBase<Option<T>>> {
|
||||||
|
selectedOptions?: OnChangeValue<Option<T>, IsMulti>;
|
||||||
|
creatable?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
showDropdown?: boolean;
|
||||||
|
groupHeader?: string;
|
||||||
|
noOptionsMessageText?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFilterSelectProps<T, IsMulti extends boolean>
|
||||||
|
extends Pick<
|
||||||
|
ISelectProps<T, IsMulti>,
|
||||||
|
| "selectedOptions"
|
||||||
|
| "isLoading"
|
||||||
|
| "isMulti"
|
||||||
|
| "components"
|
||||||
|
| "placeholder"
|
||||||
|
| "closeMenuOnSelect"
|
||||||
|
> {}
|
||||||
|
|
||||||
|
const getSelectedItems = <T,>(
|
||||||
|
selectedItems: OnChangeValue<Option<T>, boolean>
|
||||||
|
) => {
|
||||||
|
if (Array.isArray(selectedItems)) {
|
||||||
|
return selectedItems;
|
||||||
|
} else if (selectedItems) {
|
||||||
|
return [selectedItems];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectComponent = <T, IsMulti extends boolean>(
|
||||||
|
props: ISelectProps<T, IsMulti>
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
selectedOptions,
|
||||||
|
isLoading,
|
||||||
|
isDisabled = false,
|
||||||
|
creatable = false,
|
||||||
|
components,
|
||||||
|
placeholder,
|
||||||
|
showDropdown = true,
|
||||||
|
noOptionsMessageText: noOptionsMessage = "None",
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const styles: StylesConfig<Option<T>, IsMulti> = {
|
||||||
|
option: (base) => ({
|
||||||
|
...base,
|
||||||
|
color: "#000",
|
||||||
|
}),
|
||||||
|
container: (base, state) => ({
|
||||||
|
...base,
|
||||||
|
zIndex: state.isFocused ? 10 : base.zIndex,
|
||||||
|
}),
|
||||||
|
multiValueRemove: (base, state) => ({
|
||||||
|
...base,
|
||||||
|
color: state.isFocused ? base.color : "#333333",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentProps = {
|
||||||
|
...props,
|
||||||
|
styles,
|
||||||
|
defaultOptions: true,
|
||||||
|
value: selectedOptions,
|
||||||
|
className: "react-select",
|
||||||
|
classNamePrefix: "react-select",
|
||||||
|
noOptionsMessage: () => noOptionsMessage,
|
||||||
|
placeholder: isDisabled ? "" : placeholder,
|
||||||
|
components: {
|
||||||
|
...components,
|
||||||
|
IndicatorSeparator: () => null,
|
||||||
|
...((!showDropdown || isDisabled) && { DropdownIndicator: () => null }),
|
||||||
|
...(isDisabled && { MultiValueRemove: () => null }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return creatable ? (
|
||||||
|
<AsyncCreatableSelect
|
||||||
|
{...componentProps}
|
||||||
|
isDisabled={isLoading || isDisabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AsyncSelect {...componentProps} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IFilterValueProps<T> {
|
||||||
|
values?: T[];
|
||||||
|
onSelect?: (item: T[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilterProps {
|
||||||
|
noSelectionString?: string;
|
||||||
|
className?: string;
|
||||||
|
isMulti?: boolean;
|
||||||
|
isClearable?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
creatable?: boolean;
|
||||||
|
menuPortalTarget?: HTMLElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilterComponentProps<T> extends IFilterProps {
|
||||||
|
loadOptions: (inputValue: string) => Promise<Option<T>[]>;
|
||||||
|
onCreate?: (
|
||||||
|
name: string
|
||||||
|
) => Promise<{ value: string; item: T; message: string }>;
|
||||||
|
getNamedObject: (id: string, name: string) => T;
|
||||||
|
isValidNewOption: (inputValue: string, options: T[]) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilterSelectComponent = <
|
||||||
|
T extends IHasID,
|
||||||
|
IsMulti extends boolean
|
||||||
|
>(
|
||||||
|
props: IFilterValueProps<T> &
|
||||||
|
IFilterComponentProps<T> &
|
||||||
|
IFilterSelectProps<T, IsMulti>
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
values,
|
||||||
|
isMulti,
|
||||||
|
onSelect,
|
||||||
|
isValidNewOption,
|
||||||
|
getNamedObject,
|
||||||
|
loadOptions,
|
||||||
|
} = props;
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const selectedOptions = useMemo(() => {
|
||||||
|
if (isMulti && values) {
|
||||||
|
return values.map(
|
||||||
|
(value) =>
|
||||||
|
({
|
||||||
|
object: value,
|
||||||
|
value: value.id,
|
||||||
|
} as Option<T>)
|
||||||
|
) as unknown as OnChangeValue<Option<T>, IsMulti>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values?.length) {
|
||||||
|
return {
|
||||||
|
object: values[0],
|
||||||
|
value: values[0].id,
|
||||||
|
} as OnChangeValue<Option<T>, IsMulti>;
|
||||||
|
}
|
||||||
|
}, [values, isMulti]);
|
||||||
|
|
||||||
|
const onChange = (selectedItems: OnChangeValue<Option<T>, boolean>) => {
|
||||||
|
const selected = getSelectedItems(selectedItems);
|
||||||
|
|
||||||
|
onSelect?.(selected.map((item) => item.object));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreate = async (name: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { value, item: newItem, message } = await props.onCreate!(name);
|
||||||
|
const newItemOption = {
|
||||||
|
object: newItem,
|
||||||
|
value,
|
||||||
|
} as Option<T>;
|
||||||
|
if (!isMulti) {
|
||||||
|
onChange(newItemOption);
|
||||||
|
} else {
|
||||||
|
const o = (selectedOptions ?? []) as Option<T>[];
|
||||||
|
onChange([...o, newItemOption]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
Toast.success({
|
||||||
|
content: (
|
||||||
|
<span>
|
||||||
|
{message}: <b>{name}</b>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNewOptionData = (
|
||||||
|
inputValue: string,
|
||||||
|
optionLabel: React.ReactNode
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
value: "",
|
||||||
|
object: getNamedObject("", optionLabel as string),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const validNewOption = (
|
||||||
|
inputValue: string,
|
||||||
|
value: Options<Option<T>>,
|
||||||
|
options: OptionsOrGroups<Option<T>, GroupBase<Option<T>>>
|
||||||
|
) => {
|
||||||
|
return isValidNewOption(
|
||||||
|
inputValue,
|
||||||
|
(options as Options<Option<T>>).map((o) => o.object)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounceDelay = 100;
|
||||||
|
const debounceLoadOptions = useDebounce(
|
||||||
|
(inputValue, callback) => {
|
||||||
|
loadOptions(inputValue).then(callback);
|
||||||
|
},
|
||||||
|
[loadOptions],
|
||||||
|
debounceDelay
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectComponent<T, IsMulti>
|
||||||
|
{...props}
|
||||||
|
loadOptions={debounceLoadOptions}
|
||||||
|
isLoading={props.isLoading || loading}
|
||||||
|
onChange={onChange}
|
||||||
|
selectedOptions={selectedOptions}
|
||||||
|
onCreateOption={props.creatable ? onCreate : undefined}
|
||||||
|
getNewOptionData={getNewOptionData}
|
||||||
|
isValidNewOption={validNewOption}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IFilterIDProps<T> {
|
||||||
|
ids?: string[];
|
||||||
|
onSelect?: (item: T[]) => void;
|
||||||
|
}
|
||||||
|
|
@ -16,11 +16,9 @@ import {
|
||||||
useAllTagsForFilter,
|
useAllTagsForFilter,
|
||||||
useAllMoviesForFilter,
|
useAllMoviesForFilter,
|
||||||
useAllStudiosForFilter,
|
useAllStudiosForFilter,
|
||||||
useAllPerformersForFilter,
|
|
||||||
useMarkerStrings,
|
useMarkerStrings,
|
||||||
useTagCreate,
|
useTagCreate,
|
||||||
useStudioCreate,
|
useStudioCreate,
|
||||||
usePerformerCreate,
|
|
||||||
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 { TagPopover } from "../Tags/TagPopover";
|
||||||
import { defaultMaxOptionsShown, IUIConfig } from "src/core/config";
|
import { defaultMaxOptionsShown, IUIConfig } from "src/core/config";
|
||||||
import { useDebouncedSetState } from "src/hooks/debounce";
|
import { useDebouncedSetState } from "src/hooks/debounce";
|
||||||
import { Placement } from "react-bootstrap/esm/Overlay";
|
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||||
|
import { PerformerIDSelect } from "../Performers/PerformerSelect";
|
||||||
|
|
||||||
export type SelectObject = {
|
export type SelectObject = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -533,152 +532,7 @@ export const MarkerTitleSuggest: React.FC<IMarkerSuggestProps> = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
||||||
const [performerAliases, setPerformerAliases] = useState<
|
return <PerformerIDSelect {...props} />;
|
||||||
Record<string, string[]>
|
|
||||||
>({});
|
|
||||||
const [performerDisambiguations, setPerformerDisambiguations] = useState<
|
|
||||||
Record<string, string>
|
|
||||||
>({});
|
|
||||||
const [allAliases, setAllAliases] = useState<string[]>([]);
|
|
||||||
const { data, loading } = useAllPerformersForFilter();
|
|
||||||
const [createPerformer] = usePerformerCreate();
|
|
||||||
|
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
|
||||||
const intl = useIntl();
|
|
||||||
const defaultCreatable =
|
|
||||||
!configuration?.interface.disableDropdownCreate.performer ?? true;
|
|
||||||
|
|
||||||
const performers = useMemo(
|
|
||||||
() => data?.allPerformers ?? [],
|
|
||||||
[data?.allPerformers]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// build the tag aliases map
|
|
||||||
const newAliases: Record<string, string[]> = {};
|
|
||||||
const newDisambiguations: Record<string, string> = {};
|
|
||||||
const newAll: string[] = [];
|
|
||||||
performers.forEach((t) => {
|
|
||||||
if (t.alias_list.length) {
|
|
||||||
newAliases[t.id] = t.alias_list;
|
|
||||||
}
|
|
||||||
newAll.push(...t.alias_list);
|
|
||||||
if (t.disambiguation) {
|
|
||||||
newDisambiguations[t.id] = t.disambiguation;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setPerformerAliases(newAliases);
|
|
||||||
setAllAliases(newAll);
|
|
||||||
setPerformerDisambiguations(newDisambiguations);
|
|
||||||
}, [performers]);
|
|
||||||
|
|
||||||
const PerformerOption: React.FC<OptionProps<Option, boolean>> = (
|
|
||||||
optionProps
|
|
||||||
) => {
|
|
||||||
const { inputValue } = optionProps.selectProps;
|
|
||||||
|
|
||||||
let thisOptionProps = optionProps;
|
|
||||||
|
|
||||||
let { label } = optionProps.data;
|
|
||||||
const id = Number(optionProps.data.value);
|
|
||||||
|
|
||||||
if (id && performerDisambiguations[id]) {
|
|
||||||
label += ` (${performerDisambiguations[id]})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
inputValue &&
|
|
||||||
!optionProps.label.toLowerCase().includes(inputValue.toLowerCase())
|
|
||||||
) {
|
|
||||||
// must be alias
|
|
||||||
label += " (alias)";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (label != optionProps.data.label) {
|
|
||||||
thisOptionProps = {
|
|
||||||
...optionProps,
|
|
||||||
children: label,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 performer aliases
|
|
||||||
const aliases = performerAliases[option.value];
|
|
||||||
return aliases && aliases.some((a) => a.toLowerCase().includes(input));
|
|
||||||
};
|
|
||||||
|
|
||||||
const isValidNewOption = (
|
|
||||||
inputValue: string,
|
|
||||||
value: Options<Option>,
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCreate = async (name: string) => {
|
|
||||||
const result = await createPerformer({
|
|
||||||
variables: { input: { name } },
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
item: result.data!.performerCreate!,
|
|
||||||
message: intl.formatMessage(
|
|
||||||
{ id: "toast.created_entity" },
|
|
||||||
{ entity: intl.formatMessage({ id: "performer" }).toLocaleLowerCase() }
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FilterSelectComponent
|
|
||||||
{...props}
|
|
||||||
filterOption={filterOption}
|
|
||||||
isValidNewOption={isValidNewOption}
|
|
||||||
components={{ Option: PerformerOption }}
|
|
||||||
isMulti={props.isMulti ?? false}
|
|
||||||
creatable={props.creatable ?? defaultCreatable}
|
|
||||||
onCreate={onCreate}
|
|
||||||
type="performers"
|
|
||||||
isLoading={loading}
|
|
||||||
items={performers}
|
|
||||||
placeholder={
|
|
||||||
props.noSelectionString ??
|
|
||||||
intl.formatMessage(
|
|
||||||
{ id: "actions.select_entity" },
|
|
||||||
{ entityType: intl.formatMessage({ id: "performer" }) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StudioSelect: React.FC<
|
export const StudioSelect: React.FC<
|
||||||
|
|
|
||||||
|
|
@ -110,31 +110,6 @@ export const useCreatePerformer = () => {
|
||||||
update: (store, newPerformer) => {
|
update: (store, newPerformer) => {
|
||||||
if (!newPerformer?.data?.performerCreate) return;
|
if (!newPerformer?.data?.performerCreate) return;
|
||||||
|
|
||||||
const currentQuery = store.readQuery<
|
|
||||||
GQL.AllPerformersForFilterQuery,
|
|
||||||
GQL.AllPerformersForFilterQueryVariables
|
|
||||||
>({
|
|
||||||
query: GQL.AllPerformersForFilterDocument,
|
|
||||||
});
|
|
||||||
const allPerformers = sortBy(
|
|
||||||
[
|
|
||||||
...(currentQuery?.allPerformers ?? []),
|
|
||||||
newPerformer.data.performerCreate,
|
|
||||||
],
|
|
||||||
["name"]
|
|
||||||
);
|
|
||||||
if (allPerformers.length > 1) {
|
|
||||||
store.writeQuery<
|
|
||||||
GQL.AllPerformersForFilterQuery,
|
|
||||||
GQL.AllPerformersForFilterQueryVariables
|
|
||||||
>({
|
|
||||||
query: GQL.AllPerformersForFilterDocument,
|
|
||||||
data: {
|
|
||||||
allPerformers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
store.writeQuery<
|
store.writeQuery<
|
||||||
GQL.FindPerformersQuery,
|
GQL.FindPerformersQuery,
|
||||||
GQL.FindPerformersQueryVariables
|
GQL.FindPerformersQueryVariables
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, ButtonGroup } from "react-bootstrap";
|
import { Button, ButtonGroup } from "react-bootstrap";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
|
@ -6,9 +6,12 @@ import cx from "classnames";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { OperationButton } from "src/components/Shared/OperationButton";
|
import { OperationButton } from "src/components/Shared/OperationButton";
|
||||||
import { PerformerSelect, SelectObject } from "src/components/Shared/Select";
|
|
||||||
import { OptionalField } from "../IncludeButton";
|
import { OptionalField } from "../IncludeButton";
|
||||||
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import {
|
||||||
|
Performer,
|
||||||
|
PerformerSelect,
|
||||||
|
} from "src/components/Performers/PerformerSelect";
|
||||||
|
|
||||||
interface IPerformerResultProps {
|
interface IPerformerResultProps {
|
||||||
performer: GQL.ScrapedPerformer;
|
performer: GQL.ScrapedPerformer;
|
||||||
|
|
@ -40,10 +43,25 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
||||||
stashID.stash_id === performer.remote_site_id
|
stashID.stash_id === performer.remote_site_id
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePerformerSelect = (performers: SelectObject[]) => {
|
const [selectedPerformer, setSelectedPerformer] = useState<
|
||||||
|
Performer | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
performerData?.findPerformer &&
|
||||||
|
selectedID === performerData?.findPerformer?.id
|
||||||
|
) {
|
||||||
|
setSelectedPerformer(performerData.findPerformer);
|
||||||
|
}
|
||||||
|
}, [performerData?.findPerformer, selectedID]);
|
||||||
|
|
||||||
|
const handlePerformerSelect = (performers: Performer[]) => {
|
||||||
if (performers.length) {
|
if (performers.length) {
|
||||||
|
setSelectedPerformer(performers[0]);
|
||||||
setSelectedID(performers[0].id);
|
setSelectedID(performers[0].id);
|
||||||
} else {
|
} else {
|
||||||
|
setSelectedPerformer(undefined);
|
||||||
setSelectedID(undefined);
|
setSelectedID(undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -114,7 +132,7 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
||||||
<FormattedMessage id="actions.skip" />
|
<FormattedMessage id="actions.skip" />
|
||||||
</Button>
|
</Button>
|
||||||
<PerformerSelect
|
<PerformerSelect
|
||||||
ids={selectedID ? [selectedID] : []}
|
values={selectedPerformer ? [selectedPerformer] : []}
|
||||||
onSelect={handlePerformerSelect}
|
onSelect={handlePerformerSelect}
|
||||||
className={cx("performer-select", {
|
className={cx("performer-select", {
|
||||||
"performer-select-active": selectedSource === "existing",
|
"performer-select-active": selectedSource === "existing",
|
||||||
|
|
|
||||||
|
|
@ -268,8 +268,22 @@ export const queryFindPerformers = (filter: ListFilterModel) =>
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAllPerformersForFilter = () =>
|
export const queryFindPerformersByIDForSelect = (performerIDs: number[]) =>
|
||||||
GQL.useAllPerformersForFilterQuery();
|
client.query<GQL.FindPerformersForSelectQuery>({
|
||||||
|
query: GQL.FindPerformersForSelectDocument,
|
||||||
|
variables: {
|
||||||
|
performer_ids: performerIDs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const queryFindPerformersForSelect = (filter: ListFilterModel) =>
|
||||||
|
client.query<GQL.FindPerformersForSelectQuery>({
|
||||||
|
query: GQL.FindPerformersForSelectDocument,
|
||||||
|
variables: {
|
||||||
|
filter: filter.makeFindFilter(),
|
||||||
|
performer_filter: filter.makeFilter(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const useFindStudio = (id: string) => {
|
export const useFindStudio = (id: string) => {
|
||||||
const skip = id === "new" || id === "";
|
const skip = id === "new" || id === "";
|
||||||
|
|
@ -1372,8 +1386,6 @@ export const usePerformerCreate = () =>
|
||||||
const performer = result.data?.performerCreate;
|
const performer = result.data?.performerCreate;
|
||||||
if (!performer) return;
|
if (!performer) return;
|
||||||
|
|
||||||
appendObject(cache, performer, GQL.AllPerformersForFilterDocument);
|
|
||||||
|
|
||||||
// update stats
|
// update stats
|
||||||
updateStats(cache, "performer_count", 1);
|
updateStats(cache, "performer_count", 1);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue