mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Tag aliases (#1412)
* Add Tag Update/UpdateFull * Tag alias implementation * Refactor tag page * Add aliases in UI * Include tag aliases in q filter * Include aliases in tag select * Add aliases to auto-tagger * Use aliases in scraper * Add tag aliases for filename parser
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import Select, { ValueType, Styles } from "react-select";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import Select, {
|
||||
ValueType,
|
||||
Styles,
|
||||
OptionProps,
|
||||
components as reactSelectComponents,
|
||||
GroupedOptionsType,
|
||||
OptionsType,
|
||||
} from "react-select";
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
import { debounce } from "lodash";
|
||||
|
||||
@@ -16,6 +23,7 @@ import {
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { SelectComponents } from "react-select/src/components";
|
||||
|
||||
export type ValidTypes =
|
||||
| GQL.SlimPerformerDataFragment
|
||||
@@ -59,6 +67,13 @@ interface ISelectProps<T extends boolean> {
|
||||
isMulti: T;
|
||||
isClearable?: boolean;
|
||||
onInputChange?: (input: string) => void;
|
||||
components?: Partial<SelectComponents<Option, T>>;
|
||||
filterOption?: (option: Option, rawInput: string) => boolean;
|
||||
isValidNewOption?: (
|
||||
inputValue: string,
|
||||
value: ValueType<Option, T>,
|
||||
options: OptionsType<Option> | GroupedOptionsType<Option>
|
||||
) => boolean;
|
||||
placeholder?: string;
|
||||
showDropdown?: boolean;
|
||||
groupHeader?: string;
|
||||
@@ -109,6 +124,9 @@ const SelectComponent = <T extends boolean>({
|
||||
creatable = false,
|
||||
isMulti,
|
||||
onInputChange,
|
||||
filterOption,
|
||||
isValidNewOption,
|
||||
components,
|
||||
placeholder,
|
||||
showDropdown = true,
|
||||
groupHeader,
|
||||
@@ -158,12 +176,15 @@ const SelectComponent = <T extends boolean>({
|
||||
noOptionsMessage: () => noOptionsMessage,
|
||||
placeholder: isDisabled ? "" : placeholder,
|
||||
onInputChange,
|
||||
filterOption,
|
||||
isValidNewOption,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
styles,
|
||||
closeMenuOnSelect,
|
||||
menuPortalTarget,
|
||||
components: {
|
||||
...components,
|
||||
IndicatorSeparator: () => null,
|
||||
...((!showDropdown || isDisabled) && { DropdownIndicator: () => null }),
|
||||
...(isDisabled && { MultiValueRemove: () => null }),
|
||||
@@ -454,22 +475,108 @@ export const MovieSelect: React.FC<IFilterProps> = (props) => {
|
||||
};
|
||||
|
||||
export const TagSelect: React.FC<IFilterProps> = (props) => {
|
||||
const [tagAliases, setTagAliases] = useState<Record<string, string[]>>({});
|
||||
const [allAliases, setAllAliases] = useState<string[]>([]);
|
||||
const { data, loading } = useAllTagsForFilter();
|
||||
const [createTag] = useTagCreate({ name: "" });
|
||||
const [createTag] = useTagCreate();
|
||||
const placeholder = props.noSelectionString ?? "Select tags...";
|
||||
|
||||
const tags = data?.allTags ?? [];
|
||||
const tags = useMemo(() => data?.allTags ?? [], [data?.allTags]);
|
||||
|
||||
useEffect(() => {
|
||||
// build the tag aliases map
|
||||
const newAliases: Record<string, string[]> = {};
|
||||
const newAll: string[] = [];
|
||||
tags.forEach((t) => {
|
||||
newAliases[t.id] = t.aliases;
|
||||
newAll.push(...t.aliases);
|
||||
});
|
||||
setTagAliases(newAliases);
|
||||
setAllAliases(newAll);
|
||||
}, [tags]);
|
||||
|
||||
const TagOption: 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 tag aliases
|
||||
const aliases = tagAliases[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 createTag({
|
||||
variables: { name },
|
||||
variables: {
|
||||
input: {
|
||||
name,
|
||||
},
|
||||
},
|
||||
});
|
||||
return { item: result.data!.tagCreate!, message: "Created tag" };
|
||||
};
|
||||
|
||||
const isValidNewOption = (
|
||||
inputValue: string,
|
||||
value: ValueType<Option, boolean>,
|
||||
options: OptionsType<Option> | GroupedOptionsType<Option>
|
||||
) => {
|
||||
if (!inputValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(options as OptionsType<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: TagOption }}
|
||||
isMulti={props.isMulti ?? false}
|
||||
items={tags}
|
||||
creatable={props.creatable ?? true}
|
||||
|
||||
Reference in New Issue
Block a user