Files
stash/ui/v2.5/src/components/Shared/Select.tsx
InfiniteTF a0306bfbd2 Remove hotkeys and fix tag selection (#505)
* Remove broken scene player hotkeys
* Disable closing tag select menu after a select
2020-04-29 09:55:34 +10:00

445 lines
12 KiB
TypeScript

import React, { useState, useCallback, CSSProperties } from "react";
import Select, { ValueType } from "react-select";
import CreatableSelect from "react-select/creatable";
import { debounce } from "lodash";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { useToast } from "src/hooks";
type ValidTypes =
| GQL.SlimPerformerDataFragment
| GQL.Tag
| GQL.SlimStudioDataFragment;
type Option = { value: string; label: string };
interface ITypeProps {
type?: "performers" | "studios" | "tags" | "movies";
}
interface IFilterProps {
ids?: string[];
initialIds?: string[];
onSelect?: (item: ValidTypes[]) => void;
noSelectionString?: string;
className?: string;
isMulti?: boolean;
isClearable?: boolean;
isDisabled?: boolean;
}
interface ISelectProps {
className?: string;
items: Option[];
selectedOptions?: Option[];
creatable?: boolean;
onCreateOption?: (value: string) => void;
isLoading: boolean;
isDisabled?: boolean;
onChange: (item: ValueType<Option>) => void;
initialIds?: string[];
isMulti?: boolean;
isClearable?: boolean;
onInputChange?: (input: string) => void;
placeholder?: string;
showDropdown?: boolean;
groupHeader?: string;
closeMenuOnSelect?: boolean;
}
interface ISceneGallerySelect {
initialId?: string;
sceneId: string;
onSelect: (
item:
| GQL.ValidGalleriesForSceneQuery["validGalleriesForScene"][0]
| undefined
) => void;
}
const getSelectedValues = (selectedItems: ValueType<Option>) =>
selectedItems
? (Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map(
(item) => item.value
)
: [];
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => {
const { data, loading } = StashService.useValidGalleriesForScene(
props.sceneId
);
const galleries = data?.validGalleriesForScene ?? [];
const items = (galleries.length > 0
? [{ path: "None", id: "0" }, ...galleries]
: []
).map((g) => ({ label: g.path, value: g.id }));
const onChange = (selectedItems: ValueType<Option>) => {
const selectedItem = getSelectedValues(selectedItems)[0];
props.onSelect(
selectedItem ? galleries.find((g) => g.id === selectedItem) : undefined
);
};
const selectedOptions: Option[] = props.initialId
? items.filter((item) => props.initialId?.indexOf(item.value) !== -1)
: [];
return (
<SelectComponent
className="input-control"
onChange={onChange}
isLoading={loading}
items={items}
selectedOptions={selectedOptions}
/>
);
};
interface IScrapePerformerSuggestProps {
scraperId: string;
onSelectPerformer: (performer: GQL.ScrapedPerformerDataFragment) => void;
placeholder?: string;
}
export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = (
props
) => {
const [query, setQuery] = React.useState<string>("");
const { data, loading } = StashService.useScrapePerformerList(
props.scraperId,
query
);
const performers = data?.scrapePerformerList ?? [];
const items = performers.map((item) => ({
label: item.name ?? "",
value: item.name ?? "",
}));
const onInputChange = useCallback(
debounce((input: string) => {
setQuery(input);
}, 500),
[]
);
const onChange = (selectedItems: ValueType<Option>) => {
const name = getSelectedValues(selectedItems)[0];
const performer = performers.find((p) => p.name === name);
if (performer) props.onSelectPerformer(performer);
};
return (
<SelectComponent
onChange={onChange}
onInputChange={onInputChange}
isLoading={loading}
items={items}
initialIds={[]}
placeholder={props.placeholder}
className="select-suggest"
showDropdown={false}
/>
);
};
interface IMarkerSuggestProps {
initialMarkerTitle?: string;
onChange: (title: string) => void;
}
export const MarkerTitleSuggest: React.FC<IMarkerSuggestProps> = (props) => {
const { data, loading } = StashService.useMarkerStrings();
const suggestions = data?.markerStrings ?? [];
const onChange = (selectedItems: ValueType<Option>) =>
props.onChange(getSelectedValues(selectedItems)[0]);
const items = suggestions.map((item) => ({
label: item?.title ?? "",
value: item?.title ?? "",
}));
const initialIds = props.initialMarkerTitle ? [props.initialMarkerTitle] : [];
return (
<SelectComponent
creatable
onChange={onChange}
isLoading={loading}
items={items}
initialIds={initialIds}
placeholder="Marker title..."
className="select-suggest"
showDropdown={false}
groupHeader="Previously used titles..."
/>
);
};
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) =>
props.type === "performers" ? (
<PerformerSelect {...(props as IFilterProps)} />
) : props.type === "studios" ? (
<StudioSelect {...(props as IFilterProps)} />
) : props.type === "movies" ? (
<MovieSelect {...(props as IFilterProps)} />
) : (
<TagSelect {...(props as IFilterProps)} />
);
export const PerformerSelect: React.FC<IFilterProps> = (props) => {
const { data, loading } = StashService.useAllPerformersForFilter();
const normalizedData = data?.allPerformersSlim ?? [];
const items: Option[] = normalizedData.map((item) => ({
value: item.id,
label: item.name ?? "",
}));
const placeholder = props.noSelectionString ?? "Select performer...";
const selectedOptions: Option[] = props.ids
? items.filter((item) => props.ids?.indexOf(item.value) !== -1)
: [];
const onChange = (selectedItems: ValueType<Option>) => {
const selectedIds = getSelectedValues(selectedItems);
props.onSelect?.(
normalizedData.filter((item) => selectedIds.indexOf(item.id) !== -1)
);
};
return (
<SelectComponent
{...props}
selectedOptions={selectedOptions}
onChange={onChange}
type="performers"
isLoading={loading}
items={items}
placeholder={placeholder}
/>
);
};
export const StudioSelect: React.FC<IFilterProps> = (props) => {
const { data, loading } = StashService.useAllStudiosForFilter();
const normalizedData = data?.allStudiosSlim ?? [];
const items = (normalizedData.length > 0
? [{ name: "None", id: "0" }, ...normalizedData]
: []
).map((item) => ({
value: item.id,
label: item.name,
}));
const placeholder = props.noSelectionString ?? "Select studio...";
const selectedOptions: Option[] = props.ids
? items.filter((item) => props.ids?.indexOf(item.value) !== -1)
: [];
const onChange = (selectedItems: ValueType<Option>) => {
const selectedIds = getSelectedValues(selectedItems);
props.onSelect?.(
normalizedData.filter((item) => selectedIds.indexOf(item.id) !== -1)
);
};
return (
<SelectComponent
{...props}
onChange={onChange}
type="studios"
isLoading={loading}
items={items}
placeholder={placeholder}
selectedOptions={selectedOptions}
/>
);
};
export const MovieSelect: React.FC<IFilterProps> = (props) => {
const { data, loading } = StashService.useAllMoviesForFilter();
const normalizedData = data?.allMoviesSlim ?? [];
const items = (normalizedData.length > 0
? [{ name: "None", id: "0" }, ...normalizedData]
: []
).map((item) => ({
value: item.id,
label: item.name,
}));
const placeholder = props.noSelectionString ?? "Select movie...";
const selectedOptions: Option[] = props.ids
? items.filter((item) => props.ids?.indexOf(item.value) !== -1)
: [];
const onChange = (selectedItems: ValueType<Option>) => {
const selectedIds = getSelectedValues(selectedItems);
props.onSelect?.(
normalizedData.filter((item) => selectedIds.indexOf(item.id) !== -1)
);
};
return (
<SelectComponent
{...props}
onChange={onChange}
type="studios"
isLoading={loading}
items={items}
placeholder={placeholder}
selectedOptions={selectedOptions}
/>
);
};
export const TagSelect: React.FC<IFilterProps> = (props) => {
const [loading, setLoading] = useState(false);
const [selectedIds, setSelectedIds] = useState<string[]>(props.ids ?? []);
const { data, loading: dataLoading } = StashService.useAllTagsForFilter();
const [createTag] = StashService.useTagCreate({ name: "" });
const Toast = useToast();
const placeholder = props.noSelectionString ?? "Select tags...";
const selectedTags = props.ids ?? selectedIds;
const tags = data?.allTagsSlim ?? [];
const selected = tags
.filter((tag) => selectedTags.indexOf(tag.id) !== -1)
.map((tag) => ({ value: tag.id, label: tag.name }));
const items: Option[] = tags.map((item) => ({
value: item.id,
label: item.name,
}));
const onCreate = async (tagName: string) => {
try {
setLoading(true);
const result = await createTag({
variables: { name: tagName },
});
if (result?.data?.tagCreate) {
setSelectedIds([...selectedIds, result.data.tagCreate.id]);
props.onSelect?.([
...tags.filter((item) => selectedIds.indexOf(item.id) !== -1),
result.data.tagCreate,
]);
setLoading(false);
Toast.success({
content: (
<span>
Created tag: <b>{tagName}</b>
</span>
),
});
}
} catch (e) {
Toast.error(e);
}
};
const onChange = (selectedItems: ValueType<Option>) => {
const selectedValues = getSelectedValues(selectedItems);
setSelectedIds(selectedValues);
props.onSelect?.(
tags.filter((item) => selectedValues.indexOf(item.id) !== -1)
);
};
return (
<SelectComponent
{...props}
onChange={onChange}
creatable
type="tags"
placeholder={placeholder}
isLoading={loading || dataLoading}
items={items}
onCreateOption={onCreate}
selectedOptions={selected}
closeMenuOnSelect={false}
/>
);
};
const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
type,
initialIds,
onChange,
className,
items,
selectedOptions,
isLoading,
isDisabled = false,
onCreateOption,
isClearable = true,
creatable = false,
isMulti = false,
onInputChange,
placeholder,
showDropdown = true,
groupHeader,
closeMenuOnSelect = true,
}) => {
const defaultValue =
items.filter((item) => initialIds?.indexOf(item.value) !== -1) ?? null;
const options = groupHeader
? [
{
label: groupHeader,
options: items,
},
]
: items;
const styles = {
option: (base: CSSProperties) => ({
...base,
color: "#000",
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
container: (base: CSSProperties, state: any) => ({
...base,
zIndex: state.isFocused ? 10 : base.zIndex,
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
multiValueRemove: (base: CSSProperties, state: any) => ({
...base,
color: state.isFocused ? base.color : "#333333",
}),
};
const props = {
options,
value: selectedOptions,
className,
classNamePrefix: "react-select",
onChange,
isMulti,
isClearable,
defaultValue,
noOptionsMessage: () => (type !== "tags" ? "None" : null),
placeholder: isDisabled ? "" : placeholder,
onInputChange,
isDisabled,
isLoading,
styles,
closeMenuOnSelect,
components: {
IndicatorSeparator: () => null,
...((!showDropdown || isDisabled) && { DropdownIndicator: () => null }),
...(isDisabled && { MultiValueRemove: () => null }),
},
};
return creatable ? (
<CreatableSelect
{...props}
isDisabled={isLoading || isDisabled}
onCreateOption={onCreateOption}
/>
) : (
<Select {...props} />
);
};