mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
* Add interface to load tags by id * Use minimal data for tag select queries * Center image/text in select list * Overhaul tag select * Support excludeIds. Comment out image in dropdown * Replace existing selects * Remove unused code * Fix styling of aliases
931 lines
24 KiB
TypeScript
931 lines
24 KiB
TypeScript
import React, { useEffect, useMemo, useState } from "react";
|
|
import Select, {
|
|
OnChangeValue,
|
|
StylesConfig,
|
|
OptionProps,
|
|
components as reactSelectComponents,
|
|
Options,
|
|
MenuListProps,
|
|
GroupBase,
|
|
OptionsOrGroups,
|
|
DropdownIndicatorProps,
|
|
} from "react-select";
|
|
import CreatableSelect from "react-select/creatable";
|
|
|
|
import * as GQL from "src/core/generated-graphql";
|
|
import {
|
|
useAllMoviesForFilter,
|
|
useAllStudiosForFilter,
|
|
useMarkerStrings,
|
|
useStudioCreate,
|
|
useMovieCreate,
|
|
} from "src/core/StashService";
|
|
import { useToast } from "src/hooks/Toast";
|
|
import { SelectComponents } from "react-select/dist/declarations/src/components";
|
|
import { ConfigurationContext } from "src/hooks/Config";
|
|
import { useIntl } from "react-intl";
|
|
import { objectTitle } from "src/core/files";
|
|
import { galleryTitle } from "src/core/galleries";
|
|
import { defaultMaxOptionsShown, IUIConfig } from "src/core/config";
|
|
import { useDebounce } from "src/hooks/debounce";
|
|
import { Placement } from "react-bootstrap/esm/Overlay";
|
|
import { PerformerIDSelect } from "../Performers/PerformerSelect";
|
|
import { Icon } from "./Icon";
|
|
import { faTableColumns } from "@fortawesome/free-solid-svg-icons";
|
|
import { TagIDSelect } from "../Tags/TagSelect";
|
|
|
|
export type SelectObject = {
|
|
id: string;
|
|
name?: string | null;
|
|
title?: string | null;
|
|
};
|
|
type Option = { value: string; label: string };
|
|
|
|
interface ITypeProps {
|
|
type?:
|
|
| "performers"
|
|
| "studios"
|
|
| "tags"
|
|
| "scene_tags"
|
|
| "performer_tags"
|
|
| "movies";
|
|
}
|
|
interface IFilterProps {
|
|
ids?: string[];
|
|
initialIds?: string[];
|
|
onSelect?: (item: SelectObject[]) => void;
|
|
noSelectionString?: string;
|
|
className?: string;
|
|
isMulti?: boolean;
|
|
isClearable?: boolean;
|
|
isDisabled?: boolean;
|
|
creatable?: boolean;
|
|
menuPortalTarget?: HTMLElement | null;
|
|
}
|
|
interface ISelectProps<T extends boolean> {
|
|
className?: string;
|
|
items: Option[];
|
|
selectedOptions?: OnChangeValue<Option, T>;
|
|
creatable?: boolean;
|
|
onCreateOption?: (value: string) => void;
|
|
isLoading: boolean;
|
|
isDisabled?: boolean;
|
|
onChange: (item: OnChangeValue<Option, T>) => void;
|
|
initialIds?: string[];
|
|
isMulti: T;
|
|
isClearable?: boolean;
|
|
onInputChange?: (input: string) => void;
|
|
components?: Partial<SelectComponents<Option, T, GroupBase<Option>>>;
|
|
filterOption?: (option: Option, rawInput: string) => boolean;
|
|
isValidNewOption?: (
|
|
inputValue: string,
|
|
value: Options<Option>,
|
|
options: OptionsOrGroups<Option, GroupBase<Option>>
|
|
) => boolean;
|
|
placeholder?: string;
|
|
showDropdown?: boolean;
|
|
groupHeader?: string;
|
|
menuPortalTarget?: HTMLElement | null;
|
|
closeMenuOnSelect?: boolean;
|
|
noOptionsMessage?: string | null;
|
|
}
|
|
interface IFilterComponentProps extends IFilterProps {
|
|
items: SelectObject[];
|
|
toOption?: (item: SelectObject) => Option;
|
|
onCreate?: (name: string) => Promise<{ item: SelectObject; message: string }>;
|
|
}
|
|
interface IFilterSelectProps<T extends boolean>
|
|
extends Pick<
|
|
ISelectProps<T>,
|
|
| "isLoading"
|
|
| "isMulti"
|
|
| "components"
|
|
| "filterOption"
|
|
| "isValidNewOption"
|
|
| "placeholder"
|
|
| "closeMenuOnSelect"
|
|
> {}
|
|
|
|
type TitledObject = { id: string; title: string };
|
|
interface ITitledSelect {
|
|
className?: string;
|
|
selected: TitledObject[];
|
|
onSelect: (items: TitledObject[]) => void;
|
|
isMulti?: boolean;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
const getSelectedItems = (selectedItems: OnChangeValue<Option, boolean>) => {
|
|
if (Array.isArray(selectedItems)) {
|
|
return selectedItems;
|
|
} else if (selectedItems) {
|
|
return [selectedItems];
|
|
} else {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const getSelectedValues = (selectedItems: OnChangeValue<Option, boolean>) =>
|
|
getSelectedItems(selectedItems).map((item) => item.value);
|
|
|
|
const LimitedSelectMenu = <T extends boolean>(
|
|
props: MenuListProps<Option, T, GroupBase<Option>>
|
|
) => {
|
|
const { configuration } = React.useContext(ConfigurationContext);
|
|
const maxOptionsShown =
|
|
(configuration?.ui as IUIConfig).maxOptionsShown ?? defaultMaxOptionsShown;
|
|
|
|
const [hiddenCount, setHiddenCount] = useState<number>(0);
|
|
const hiddenCountStyle = {
|
|
padding: "8px 12px",
|
|
opacity: "50%",
|
|
};
|
|
const menuChildren = useMemo(() => {
|
|
if (Array.isArray(props.children)) {
|
|
// limit the number of select options showing in the select dropdowns
|
|
// always showing the 'Create "..."' option when it exists
|
|
let creationOptionIndex = (props.children as React.ReactNode[]).findIndex(
|
|
(child: React.ReactNode) => {
|
|
let maybeCreatableOption = child as React.ReactElement<
|
|
OptionProps<
|
|
Option & { __isNew__: boolean },
|
|
T,
|
|
GroupBase<Option & { __isNew__: boolean }>
|
|
>,
|
|
""
|
|
>;
|
|
return maybeCreatableOption?.props?.data?.__isNew__;
|
|
}
|
|
);
|
|
if (creationOptionIndex >= maxOptionsShown) {
|
|
setHiddenCount(props.children.length - maxOptionsShown - 1);
|
|
return props.children
|
|
.slice(0, maxOptionsShown - 1)
|
|
.concat([props.children[creationOptionIndex]]);
|
|
} else {
|
|
setHiddenCount(Math.max(props.children.length - maxOptionsShown, 0));
|
|
return props.children.slice(0, maxOptionsShown);
|
|
}
|
|
}
|
|
setHiddenCount(0);
|
|
return props.children;
|
|
}, [props.children, maxOptionsShown]);
|
|
return (
|
|
<reactSelectComponents.MenuList {...props}>
|
|
{menuChildren}
|
|
{hiddenCount > 0 && (
|
|
<div style={hiddenCountStyle}>{hiddenCount} Options Hidden</div>
|
|
)}
|
|
</reactSelectComponents.MenuList>
|
|
);
|
|
};
|
|
|
|
const SelectComponent = <T extends boolean>({
|
|
type,
|
|
initialIds,
|
|
onChange,
|
|
className,
|
|
items,
|
|
selectedOptions,
|
|
isLoading,
|
|
isDisabled = false,
|
|
onCreateOption,
|
|
isClearable = true,
|
|
creatable = false,
|
|
isMulti,
|
|
onInputChange,
|
|
filterOption,
|
|
isValidNewOption,
|
|
components,
|
|
placeholder,
|
|
showDropdown = true,
|
|
groupHeader,
|
|
menuPortalTarget,
|
|
closeMenuOnSelect = true,
|
|
noOptionsMessage = type !== "tags" ? "None" : null,
|
|
}: ISelectProps<T> & ITypeProps) => {
|
|
const values = items.filter((item) => initialIds?.indexOf(item.value) !== -1);
|
|
const defaultValue = (isMulti ? values : values[0] ?? null) as OnChangeValue<
|
|
Option,
|
|
T
|
|
>;
|
|
|
|
const options = groupHeader
|
|
? [
|
|
{
|
|
label: groupHeader,
|
|
options: items,
|
|
},
|
|
]
|
|
: items;
|
|
|
|
const styles: StylesConfig<Option, T> = {
|
|
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 props = {
|
|
options,
|
|
value: selectedOptions,
|
|
className,
|
|
classNamePrefix: "react-select",
|
|
onChange,
|
|
isMulti,
|
|
isClearable,
|
|
defaultValue: defaultValue ?? undefined,
|
|
noOptionsMessage: () => noOptionsMessage,
|
|
placeholder: isDisabled ? "" : placeholder,
|
|
onInputChange,
|
|
filterOption,
|
|
isValidNewOption,
|
|
isDisabled,
|
|
isLoading,
|
|
styles,
|
|
closeMenuOnSelect,
|
|
menuPortalTarget,
|
|
components: {
|
|
...components,
|
|
MenuList: LimitedSelectMenu,
|
|
IndicatorSeparator: () => null,
|
|
...((!showDropdown || isDisabled) && { DropdownIndicator: () => null }),
|
|
...(isDisabled && { MultiValueRemove: () => null }),
|
|
},
|
|
};
|
|
|
|
return creatable ? (
|
|
<CreatableSelect
|
|
{...props}
|
|
isDisabled={isLoading || isDisabled}
|
|
onCreateOption={onCreateOption}
|
|
/>
|
|
) : (
|
|
<Select {...props} />
|
|
);
|
|
};
|
|
|
|
const FilterSelectComponent = <T extends boolean>(
|
|
props: IFilterComponentProps & ITypeProps & IFilterSelectProps<T>
|
|
) => {
|
|
const { items, ids, isMulti, onSelect } = props;
|
|
const [loading, setLoading] = useState(false);
|
|
const selectedIds = ids ?? [];
|
|
const Toast = useToast();
|
|
|
|
const options = items.map((i) => {
|
|
if (props.toOption) {
|
|
return props.toOption(i);
|
|
}
|
|
return {
|
|
value: i.id,
|
|
label: i.name ?? i.title ?? "",
|
|
};
|
|
});
|
|
|
|
const selected = options.filter((option) =>
|
|
selectedIds.includes(option.value)
|
|
);
|
|
const selectedOptions = (
|
|
isMulti ? selected : selected[0] ?? null
|
|
) as OnChangeValue<Option, T>;
|
|
|
|
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
|
|
const selectedValues = getSelectedValues(selectedItems);
|
|
onSelect?.(items.filter((item) => selectedValues.includes(item.id)));
|
|
};
|
|
|
|
const onCreate = async (name: string) => {
|
|
try {
|
|
setLoading(true);
|
|
const { item: newItem, message } = await props.onCreate!(name);
|
|
props.onSelect?.([
|
|
...items.filter((item) => selectedIds.includes(item.id)),
|
|
newItem,
|
|
]);
|
|
setLoading(false);
|
|
Toast.success(
|
|
<span>
|
|
{message}: <b>{name}</b>
|
|
</span>
|
|
);
|
|
} catch (e) {
|
|
Toast.error(e);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<SelectComponent<T>
|
|
{...props}
|
|
isLoading={props.isLoading || loading}
|
|
onChange={onChange}
|
|
items={options}
|
|
selectedOptions={selectedOptions}
|
|
onCreateOption={props.creatable ? onCreate : undefined}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const GallerySelect: React.FC<ITitledSelect> = (props) => {
|
|
const [query, setQuery] = useState<string>("");
|
|
const { data, loading } = GQL.useFindGalleriesQuery({
|
|
skip: query === "",
|
|
variables: {
|
|
filter: {
|
|
q: query,
|
|
},
|
|
},
|
|
});
|
|
|
|
const galleries = data?.findGalleries.galleries ?? [];
|
|
const items = galleries.map((g) => ({
|
|
label: galleryTitle(g),
|
|
value: g.id,
|
|
}));
|
|
|
|
const onInputChange = useDebounce(setQuery, 500);
|
|
|
|
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
|
|
const selected = getSelectedItems(selectedItems);
|
|
props.onSelect(
|
|
selected.map((s) => ({
|
|
id: s.value,
|
|
title: s.label,
|
|
}))
|
|
);
|
|
};
|
|
|
|
const options = props.selected.map((g) => ({
|
|
value: g.id,
|
|
label: g.title ?? "Unknown",
|
|
}));
|
|
|
|
return (
|
|
<SelectComponent
|
|
className={props.className}
|
|
onChange={onChange}
|
|
onInputChange={onInputChange}
|
|
isLoading={loading}
|
|
items={items}
|
|
selectedOptions={options}
|
|
isMulti
|
|
placeholder="Search for gallery..."
|
|
noOptionsMessage={query === "" ? null : "No galleries found."}
|
|
showDropdown={false}
|
|
isDisabled={props.disabled}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const SceneSelect: React.FC<ITitledSelect> = (props) => {
|
|
const [query, setQuery] = useState<string>("");
|
|
const { data, loading } = GQL.useFindScenesQuery({
|
|
skip: query === "",
|
|
variables: {
|
|
filter: {
|
|
q: query,
|
|
},
|
|
},
|
|
});
|
|
|
|
const scenes = data?.findScenes.scenes ?? [];
|
|
const items = scenes.map((s) => ({
|
|
label: objectTitle(s),
|
|
value: s.id,
|
|
}));
|
|
|
|
const onInputChange = useDebounce(setQuery, 500);
|
|
|
|
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
|
|
const selected = getSelectedItems(selectedItems);
|
|
props.onSelect(
|
|
(selected ?? []).map((s) => ({
|
|
id: s.value,
|
|
title: s.label,
|
|
}))
|
|
);
|
|
};
|
|
|
|
const options = props.selected.map((s) => ({
|
|
value: s.id,
|
|
label: s.title,
|
|
}));
|
|
|
|
return (
|
|
<SelectComponent
|
|
onChange={onChange}
|
|
onInputChange={onInputChange}
|
|
isLoading={loading}
|
|
items={items}
|
|
selectedOptions={options}
|
|
isMulti={props.isMulti ?? false}
|
|
placeholder="Search for scene..."
|
|
noOptionsMessage={query === "" ? null : "No scenes found."}
|
|
showDropdown={false}
|
|
isDisabled={props.disabled}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const ImageSelect: React.FC<ITitledSelect> = (props) => {
|
|
const [query, setQuery] = useState<string>("");
|
|
const { data, loading } = GQL.useFindImagesQuery({
|
|
skip: query === "",
|
|
variables: {
|
|
filter: {
|
|
q: query,
|
|
},
|
|
},
|
|
});
|
|
|
|
const images = data?.findImages.images ?? [];
|
|
const items = images.map((s) => ({
|
|
label: objectTitle(s),
|
|
value: s.id,
|
|
}));
|
|
|
|
const onInputChange = useDebounce(setQuery, 500);
|
|
|
|
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
|
|
const selected = getSelectedItems(selectedItems);
|
|
props.onSelect(
|
|
(selected ?? []).map((s) => ({
|
|
id: s.value,
|
|
title: s.label,
|
|
}))
|
|
);
|
|
};
|
|
|
|
const options = props.selected.map((s) => ({
|
|
value: s.id,
|
|
label: s.title,
|
|
}));
|
|
|
|
return (
|
|
<SelectComponent
|
|
onChange={onChange}
|
|
onInputChange={onInputChange}
|
|
isLoading={loading}
|
|
items={items}
|
|
selectedOptions={options}
|
|
isMulti={props.isMulti ?? false}
|
|
placeholder="Search for image..."
|
|
noOptionsMessage={query === "" ? null : "No images found."}
|
|
showDropdown={false}
|
|
isDisabled={props.disabled}
|
|
/>
|
|
);
|
|
};
|
|
|
|
interface IMarkerSuggestProps {
|
|
initialMarkerTitle?: string;
|
|
onChange: (title: string) => void;
|
|
}
|
|
export const MarkerTitleSuggest: React.FC<IMarkerSuggestProps> = (props) => {
|
|
const { data, loading } = useMarkerStrings();
|
|
const suggestions = data?.markerStrings ?? [];
|
|
|
|
const onChange = (selectedItem: OnChangeValue<Option, false>) =>
|
|
props.onChange(selectedItem?.value ?? "");
|
|
|
|
const items = suggestions.map((item) => ({
|
|
label: item?.title ?? "",
|
|
value: item?.title ?? "",
|
|
}));
|
|
const initialIds = props.initialMarkerTitle ? [props.initialMarkerTitle] : [];
|
|
|
|
// add initial value to items if still loading, to ensure existing value
|
|
// is populated
|
|
if (loading && initialIds.length > 0) {
|
|
items.push({
|
|
label: initialIds[0],
|
|
value: initialIds[0],
|
|
});
|
|
}
|
|
|
|
return (
|
|
<SelectComponent
|
|
isMulti={false}
|
|
creatable
|
|
onChange={onChange}
|
|
isLoading={loading}
|
|
items={items}
|
|
initialIds={initialIds}
|
|
placeholder="Marker title..."
|
|
className="select-suggest"
|
|
showDropdown={false}
|
|
groupHeader="Previously used titles..."
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
|
return <PerformerIDSelect {...props} />;
|
|
};
|
|
|
|
export const StudioSelect: React.FC<
|
|
IFilterProps & { excludeIds?: string[] }
|
|
> = (props) => {
|
|
const [studioAliases, setStudioAliases] = useState<Record<string, string[]>>(
|
|
{}
|
|
);
|
|
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) => {
|
|
const { data, loading } = useAllMoviesForFilter();
|
|
const [createMovie] = useMovieCreate();
|
|
const items = data?.allMovies ?? [];
|
|
const intl = useIntl();
|
|
|
|
const { configuration } = React.useContext(ConfigurationContext);
|
|
const defaultCreatable =
|
|
!configuration?.interface.disableDropdownCreate.movie ?? true;
|
|
|
|
const onCreate = async (name: string) => {
|
|
const result = await createMovie({
|
|
variables: { input: { name } },
|
|
});
|
|
return {
|
|
item: result.data!.movieCreate!,
|
|
message: intl.formatMessage(
|
|
{ id: "toast.created_entity" },
|
|
{ entity: intl.formatMessage({ id: "movie" }).toLocaleLowerCase() }
|
|
),
|
|
};
|
|
};
|
|
|
|
return (
|
|
<FilterSelectComponent
|
|
{...props}
|
|
isMulti={props.isMulti ?? false}
|
|
type="movies"
|
|
isLoading={loading}
|
|
items={items}
|
|
placeholder={
|
|
props.noSelectionString ??
|
|
intl.formatMessage(
|
|
{ id: "actions.select_entity" },
|
|
{
|
|
entityType: intl.formatMessage({
|
|
id: props.isMulti ? "movies" : "movie",
|
|
}),
|
|
}
|
|
)
|
|
}
|
|
creatable={props.creatable ?? defaultCreatable}
|
|
onCreate={onCreate}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const TagSelect: React.FC<
|
|
IFilterProps & { excludeIds?: string[]; hoverPlacement?: Placement }
|
|
> = (props) => {
|
|
return <TagIDSelect {...props} />;
|
|
};
|
|
|
|
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => {
|
|
if (props.type === "performers") {
|
|
return <PerformerSelect {...props} creatable={false} />;
|
|
} else if (props.type === "studios") {
|
|
return <StudioSelect {...props} creatable={false} />;
|
|
} else if (props.type === "movies") {
|
|
return <MovieSelect {...props} creatable={false} />;
|
|
} else {
|
|
return <TagSelect {...props} creatable={false} />;
|
|
}
|
|
};
|
|
|
|
interface IStringListSelect {
|
|
options?: string[];
|
|
value: string[];
|
|
}
|
|
|
|
export const StringListSelect: React.FC<IStringListSelect> = ({
|
|
options = [],
|
|
value,
|
|
}) => {
|
|
const translatedOptions = useMemo(() => {
|
|
return options.map((o) => {
|
|
return { label: o, value: o };
|
|
});
|
|
}, [options]);
|
|
const translatedValue = useMemo(() => {
|
|
return value.map((o) => {
|
|
return { label: o, value: o };
|
|
});
|
|
}, [value]);
|
|
|
|
const styles: StylesConfig<Option, true> = {
|
|
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",
|
|
}),
|
|
};
|
|
|
|
return (
|
|
<Select
|
|
classNamePrefix="react-select"
|
|
className="form-control react-select"
|
|
options={translatedOptions}
|
|
value={translatedValue}
|
|
isMulti
|
|
isDisabled
|
|
styles={styles}
|
|
components={{
|
|
IndicatorSeparator: () => null,
|
|
...{ DropdownIndicator: () => null },
|
|
...{ MultiValueRemove: () => null },
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
interface IListSelect<T> {
|
|
options?: T[];
|
|
value: T[];
|
|
toOptionType: (v: T) => { label: string; value: string };
|
|
fromOptionType?: (o: { label: string; value: string }) => T;
|
|
}
|
|
|
|
export const ListSelect = <T extends {}>(props: IListSelect<T>) => {
|
|
const { options = [], value, toOptionType } = props;
|
|
|
|
const translatedOptions = useMemo(() => {
|
|
return options.map(toOptionType);
|
|
}, [options, toOptionType]);
|
|
const translatedValue = useMemo(() => {
|
|
return value.map(toOptionType);
|
|
}, [value, toOptionType]);
|
|
|
|
const styles: StylesConfig<Option, true> = {
|
|
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",
|
|
}),
|
|
};
|
|
|
|
return (
|
|
<Select
|
|
classNamePrefix="react-select"
|
|
className="form-control react-select"
|
|
options={translatedOptions}
|
|
value={translatedValue}
|
|
isMulti
|
|
isDisabled
|
|
styles={styles}
|
|
components={{
|
|
IndicatorSeparator: () => null,
|
|
...{ DropdownIndicator: () => null },
|
|
...{ MultiValueRemove: () => null },
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
type DisableOption = Option & {
|
|
disabled?: boolean;
|
|
};
|
|
|
|
interface ICheckBoxSelectProps {
|
|
options: DisableOption[];
|
|
selectedOptions?: DisableOption[];
|
|
onChange: (item: OnChangeValue<DisableOption, true>) => void;
|
|
}
|
|
|
|
export const CheckBoxSelect: React.FC<ICheckBoxSelectProps> = ({
|
|
options,
|
|
selectedOptions,
|
|
onChange,
|
|
}) => {
|
|
const Option = (props: OptionProps<DisableOption, true>) => (
|
|
<reactSelectComponents.Option {...props}>
|
|
<input
|
|
type="checkbox"
|
|
disabled={props.data.disabled}
|
|
checked={props.isSelected}
|
|
onChange={() => null}
|
|
className="mr-1"
|
|
/>
|
|
<label>{props.label}</label>
|
|
</reactSelectComponents.Option>
|
|
);
|
|
|
|
const DropdownIndicator = (
|
|
props: DropdownIndicatorProps<DisableOption, true>
|
|
) => (
|
|
<reactSelectComponents.DropdownIndicator {...props}>
|
|
<Icon icon={faTableColumns} className="column-select" />
|
|
</reactSelectComponents.DropdownIndicator>
|
|
);
|
|
|
|
return (
|
|
<Select
|
|
className="CheckBoxSelect"
|
|
options={options}
|
|
value={selectedOptions}
|
|
isMulti
|
|
closeMenuOnSelect={false}
|
|
hideSelectedOptions={false}
|
|
isSearchable={false}
|
|
isClearable={false}
|
|
components={{
|
|
DropdownIndicator,
|
|
Option,
|
|
ValueContainer: () => null,
|
|
IndicatorSeparator: () => null,
|
|
}}
|
|
onChange={onChange}
|
|
styles={{
|
|
control: (base) => ({
|
|
...base,
|
|
height: "25px",
|
|
width: "25px",
|
|
backgroundColor: "none",
|
|
border: "none",
|
|
transition: "none",
|
|
cursor: "pointer",
|
|
}),
|
|
dropdownIndicator: (base) => ({
|
|
...base,
|
|
color: "rgb(255, 255, 255)",
|
|
padding: "0",
|
|
}),
|
|
menu: (base) => ({
|
|
...base,
|
|
backgroundColor: "rgb(57, 75, 89)",
|
|
}),
|
|
option: (base, fprops) => ({
|
|
...base,
|
|
backgroundColor: fprops.isFocused
|
|
? "rgb(37, 49, 58)"
|
|
: "rgb(57, 75, 89)",
|
|
padding: "0px 12px",
|
|
}),
|
|
menuList: (base) => ({
|
|
...base,
|
|
position: "fixed",
|
|
}),
|
|
}}
|
|
/>
|
|
);
|
|
};
|