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:
WithoutPants
2021-05-26 14:36:05 +10:00
committed by GitHub
parent 9b57fbbf50
commit c70faa2a53
48 changed files with 1303 additions and 315 deletions

View File

@@ -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}