Tag select refactor (#4478)

* 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
This commit is contained in:
WithoutPants
2024-01-24 20:24:13 +11:00
committed by GitHub
parent dd8da7f339
commit 723211a620
21 changed files with 543 additions and 249 deletions

View File

@@ -14,11 +14,9 @@ import CreatableSelect from "react-select/creatable";
import * as GQL from "src/core/generated-graphql";
import {
useAllTagsForFilter,
useAllMoviesForFilter,
useAllStudiosForFilter,
useMarkerStrings,
useTagCreate,
useStudioCreate,
useMovieCreate,
} from "src/core/StashService";
@@ -28,13 +26,13 @@ import { ConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl";
import { objectTitle } from "src/core/files";
import { galleryTitle } from "src/core/galleries";
import { TagPopover } from "../Tags/TagPopover";
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;
@@ -726,146 +724,7 @@ export const MovieSelect: React.FC<IFilterProps> = (props) => {
export const TagSelect: React.FC<
IFilterProps & { excludeIds?: string[]; hoverPlacement?: Placement }
> = (props) => {
const [tagAliases, setTagAliases] = useState<Record<string, string[]>>({});
const [allAliases, setAllAliases] = useState<string[]>([]);
const { data, loading } = useAllTagsForFilter();
const [createTag] = useTagCreate();
const intl = useIntl();
const placeholder =
props.noSelectionString ??
intl.formatMessage(
{ id: "actions.select_entity" },
{ entityType: intl.formatMessage({ id: props.isMulti ? "tags" : "tag" }) }
);
const { configuration } = React.useContext(ConfigurationContext);
const defaultCreatable =
!configuration?.interface.disableDropdownCreate.tag ?? true;
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
const tags = useMemo(
() => (data?.allTags ?? []).filter((tag) => !exclude.includes(tag.id)),
[data?.allTags, exclude]
);
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,
};
}
const id = optionProps.data.value;
const hide = (optionProps.data as Option & { __isNew__: boolean })
.__isNew__;
return (
<TagPopover id={id} hide={hide} placement={props.hoverPlacement}>
<reactSelectComponents.Option {...thisOptionProps} />
</TagPopover>
);
};
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: {
input: {
name,
},
},
});
return {
item: result.data!.tagCreate!,
message: intl.formatMessage(
{ id: "toast.created_entity" },
{ entity: intl.formatMessage({ id: "tag" }).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: TagOption }}
isMulti={props.isMulti ?? false}
items={tags}
creatable={props.creatable ?? defaultCreatable}
type="tags"
placeholder={placeholder}
isLoading={loading}
onCreate={onCreate}
closeMenuOnSelect={!props.isMulti}
/>
);
return <TagIDSelect {...props} />;
};
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => {