server/frontend/src/Selector.tsx (78 lines of code) (raw):

import { React } from '@jetbrains/teamcity-api'; import { Size } from "@jetbrains/ring-ui/components/input/input"; import TagsInput, { TagsInputRequestParams, ToggleTagParams } from "@jetbrains/ring-ui/components/tags-input/tags-input"; import { useRef, useState } from "react"; import { TagType } from '@jetbrains/ring-ui/components/tags-list/tags-list'; import { getTagsAutocompletion, isParameterSearch } from "./autocompletion"; type TagSelectorConfig = { readonly values: TagType[], readonly selectedValues: TagType[], readonly name: string, readonly separator: string, readonly allowAddNewTags: Boolean, readonly buildTypeId: string, } const TagSelector = (config: TagSelectorConfig) => { const [tags, setTags] = useState(config.selectedValues); const [selectorDisabled, setSelectorDisabled] = useState(false); const hiddenInput = useRef<HTMLInputElement>(null); const getValues = (selectedValues: TagType[]) => selectedValues.map(t => t.key).join(config.separator); const handleTagAdd = (params: ToggleTagParams) => { const tagAsParameterReference = () => { const referenceMarker = "%"; return { key: referenceMarker + params.tag.key + referenceMarker, label: referenceMarker + params.tag.label + referenceMarker, }; } const isPlainKnownValue = config.values.map((value) => value.key).includes(params.tag.key); const newTag = isPlainKnownValue ? params.tag : tagAsParameterReference(); const updatedTags = [...tags, newTag]; hiddenInput.current!.value = getValues(updatedTags); setTags(updatedTags); } const handleTagRemove = (params: ToggleTagParams) => { const updatedTags = tags.filter((t) => t !== params.tag); hiddenInput.current!.value = getValues(updatedTags); setTags(updatedTags); } // Since we're not controlling the runner's form, we need to somehow disable the selector on form submission. // TC automatically sets the "disabled" attribute for all inputs within the form. // Here, we just need to keep track of our hidden input and act accordingly. React.useEffect(() => { if (hiddenInput.current) { const observer = new MutationObserver((records) => { for (const mutation of records) { if (mutation.attributeName === "disabled") { setSelectorDisabled(hiddenInput.current?.disabled ?? false); } } }); observer.observe(hiddenInput.current, { attributes: true }); return () => observer.disconnect(); } }) const dataSource = async (userInput: TagsInputRequestParams): Promise<readonly TagType[]> => { if (!isParameterSearch(userInput.query)) { return config.values; } return await getTagsAutocompletion(config.buildTypeId, userInput.query); }; return ( <> <TagsInput onAddTag={handleTagAdd} onRemoveTag={handleTagRemove} tags={tags} maxPopupHeight={300} dataSource={dataSource} allowAddNewTags={config.allowAddNewTags} placeholder={''} size={Size.L} disabled={selectorDisabled} /> <input ref={hiddenInput} // "prop:" prefix is required so that the value is presented after form submission name={`prop:${config.name}`} type="hidden" value={getValues(tags)} /> </> ); } export { TagSelector, TagSelectorConfig, }