UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

248 lines (247 loc) 15.1 kB
import "../../CommonImports"; import "../../Core/core.css"; import "./IdentityPicker.css"; import * as React from "react"; import * as Resources from '../../Resources.IdentityPicker'; import { ObservableArray, ObservableLike, ObservableValue } from '../../Core/Observable'; import { makeCancelable } from '../../Core/Util/Promise'; import { format, localeIgnoreCaseComparer, startsWith } from '../../Core/Util/String'; import { IconSize } from '../../Icon'; import { IdentityCard } from '../../IdentityCard'; import { Observer } from '../../Observer'; import { Persona, PersonaSize } from '../../Persona'; import { TagPicker } from '../../TagPicker'; import { Tooltip } from '../../TooltipEx'; import { KeyCode } from '../../Util'; import { shouldShowIdentityCard } from "../IdentityPickerDropdown/IdentityPickerUtils"; import { IdentityType } from "../IdentityPickerDropdown/SharedIdentityPicker.Props"; import { IdentityPickerSuggestionItem } from "../IdentityPickerSuggestionsList/IdentityPickerSuggestionItem"; export class IdentityPicker extends React.Component { constructor(props) { super(props); this.resolveEmailPromises = []; this.itemRefs = {}; this.tagPickerRef = React.createRef(); this.openedIdentityCard = new ObservableValue(undefined); this.outerElement = React.createRef(); this.lastSearchVal = ""; this.suggestions = new ObservableArray([]); this.suggestionsLoading = new ObservableValue(false); this._isMounted = false; this.nonIdentitySuggestions = []; this.areTagsEqual = (first, second) => { return first.entityId === second.entityId; }; this.renderSuggestionItem = (suggestion) => { return (React.createElement("div", { className: "flex-row flex-grow full-width", onKeyDown: this.onKeyDownSuggestionItem }, React.createElement(IdentityPickerSuggestionItem, Object.assign({}, suggestion, { onOpenPersonaCard: this.onOpenPersonaCard, ref: itemRef => (this.itemRefs[suggestion.item.entityId] = itemRef), renderSuggestion: this.props.renderCustomIdentitySuggestion && this.renderCustomSuggestionItem })))); }; this.onKeyDownSuggestionItem = (event) => { if (!event.defaultPrevented) { if (event.which === KeyCode.leftArrow || event.which === KeyCode.escape || event.which === KeyCode.tab) { if (this.tagPickerRef.current) { this.tagPickerRef.current.focus(); event.preventDefault(); } } } }; this.createDefaultItem = (email) => { return { displayName: email, originDirectory: "email", originId: email, entityId: email, entityType: IdentityType.Custom }; }; this.onOpenPersonaCard = (identity) => { if (shouldShowIdentityCard(identity)) { this.openedIdentityCard.value = identity; this.outerElement.current && this.outerElement.current.dispatchEvent(new CustomEvent('vss-telemetry-proxy', { detail: { area: IdentityPicker.area, component: "IdentityPicker", feature: IdentityPicker.feature, level: 3, method: "openPersonaCard", message: '', properties: {} }, bubbles: true })); } }; this.onClosePersonaCard = () => { this.openedIdentityCard.value = undefined; this.tagPickerRef.current.focus(); }; this.shouldBlurClear = () => { return this.openedIdentityCard.value === undefined; }; this.onEmptyInputFocus = () => { this.updateSuggestionsList(this.props.pickerProvider.onEmptyInputFocus()); }; this.focusContactCardButton = (tag) => { if (this.itemRefs[tag.entityId]) { this.itemRefs[tag.entityId].focus(); } }; this.onAddIdentity = async (identity) => { // Reset the state back to valid as new input should be validated. this.setStateSafe({ error: "" }); if (this.props.pickerProvider.addIdentitiesToMRU) { try { await this.props.pickerProvider.addIdentitiesToMRU([identity]); this.props.onIdentityAdded(identity); } catch (error) { this.setStateSafe({ error: error.message }); // Remove selected identity from the picker and show the error message // as the identity selected from MRU was removed or marked as inactive. this.props.onIdentityRemoved(identity); } } else { this.props.onIdentityAdded(identity); } }; this.onDelimitedSearch = (emailList) => { const emails = emailList.map(email => email.trim()); emails.forEach(email => { this.updateResolvedEmail(this.props.pickerProvider.onFilterIdentities(email), email); this.props.onIdentityAdded(this.createDefaultItem(email)); }); this.tagPickerRef.current && this.tagPickerRef.current.clearTagPicker(); }; this.renderCustomSuggestionItem = (suggestion) => { return this.props.renderCustomIdentitySuggestion && this.props.renderCustomIdentitySuggestion(suggestion.item); }; this.convertItemToPill = (person, index) => { const isUnresolvedEmail = person.originDirectory === "email"; return !!this.props.convertItemToPill && person.entityType === IdentityType.Custom ? this.props.convertItemToPill(person, index) : { className: "bolt-identity-picker-pill flex-row", content: isUnresolvedEmail ? (React.createElement(Tooltip, { text: format(Resources.UnknownUserOrGroup, person.displayName) }, React.createElement("div", { className: "bolt-identity-picker-unresolved-email" }, person.displayName))) : (person.displayName || person.mailNickname), role: "presentation", onRenderFilledVisual: isUnresolvedEmail ? undefined : () => { return (React.createElement(Persona, { ariaLabel: "", className: "flex-row flex-center", identity: person, size: PersonaSize.size20, role: "presentation" })); } }; }; this.onResolveSuggestions = (updatedValue) => { // Reset the state back to valid as user started new input. this.setStateSafe({ error: "" }); this.lastSearchVal = updatedValue; const suggestions = this.props.pickerProvider.onFilterIdentities(updatedValue, ObservableLike.getValue(this.props.selectedIdentities)); if (suggestions !== null) { if (this.cachedResults[updatedValue]) { this.suggestions.value = this.cachedResults[updatedValue].filter(identity => !ObservableLike.getValue(this.props.selectedIdentities).some(selectedIdentity => selectedIdentity.entityId === identity.entityId)); } else { this.updateSuggestionsList(suggestions, updatedValue); } } }; this.setStateSafe = (state) => { if (!this._isMounted) { return; } this.setState(state); }; this.cachedResults = {}; this.state = { error: "" }; } render() { return (React.createElement("div", { className: this.props.className, ref: this.outerElement }, React.createElement(Observer, { openedIdentityCard: this.openedIdentityCard }, (props) => { return (React.createElement(React.Fragment, null, React.createElement(TagPicker, { ariaLabel: this.props.ariaLabel, ariaLabelledBy: this.props.ariaLabelledBy, suggestionsLoading: this.suggestionsLoading, areTagsEqual: this.areTagsEqual, convertItemToPill: this.convertItemToPill, deliminator: this.props.onResolveEntity && ";", noResultsFoundText: Resources.IdentityPickerNoResultsText, onBlur: this.props.onBlur, onDelimitedSearch: this.props.onResolveEntity && this.onDelimitedSearch, onEmptyInputFocus: this.onEmptyInputFocus, onSearchChanged: this.onResolveSuggestions, onSuggestionExpanded: this.focusContactCardButton, onTagAdded: this.onAddIdentity, onTagRemoved: this.props.onIdentityRemoved, onTagsRemoved: this.props.onIdentitiesRemoved, placeholderText: this.props.placeholderText || Resources.MultiIdentityPickerPlaceholderText, prefixIconProps: { className: "bolt-identity-picker-contact-icon secondary-text justify-center flex-center", iconName: "Contact", size: IconSize.medium }, ref: this.tagPickerRef, renderSuggestionItem: this.renderSuggestionItem, shouldBlurClear: this.shouldBlurClear, selectedTags: this.props.selectedIdentities, suggestions: this.suggestions, suggestionsLoadingText: Resources.Loading, suggestionsContainerAriaLabel: this.props.suggestionsContainerAriaLabel }), props.openedIdentityCard && (React.createElement(IdentityCard, { getEntityFromUniqueAttribute: this.props.pickerProvider.getEntityFromUniqueAttribute, key: props.openedIdentityCard.entityId, identity: props.openedIdentityCard, displayName: props.openedIdentityCard.displayName, target: this.outerElement.current, onDismissCallback: this.onClosePersonaCard, onRequestConnectionInformation: this.props.pickerProvider.onRequestConnectionInformation })), React.createElement("div", { className: "bolt-identity-picker-error" }, this.state.error))); }))); } componentDidMount() { this._isMounted = true; } componentWillUnmount() { this._isMounted = false; this.currentPromise && this.currentPromise.cancel(); for (const promise of this.resolveEmailPromises) { promise.cancel(); } } updateResolvedEmail(suggestions, email) { const suggestionsArray = suggestions; const suggestionsPromiseLike = suggestions; if (Array.isArray(suggestionsArray)) { this.props.onResolveEntity && suggestionsArray.length === 1 && this.props.onResolveEntity(email, !ObservableLike.getValue(this.props.selectedIdentities).some(identity => identity.entityId === suggestionsArray[0].entityId) ? suggestionsArray[0] : null); } else if (suggestionsPromiseLike && suggestionsPromiseLike.then) { const promise = (this.currentPromise = makeCancelable(suggestionsPromiseLike)); promise.promise.then((newSuggestions) => { this.props.onResolveEntity && newSuggestions.length === 1 && this.props.onResolveEntity(email, !ObservableLike.getValue(this.props.selectedIdentities).some(identity => identity.entityId === newSuggestions[0].entityId) ? newSuggestions[0] : null); }); } } updateSuggestionsList(suggestions, initialSearchValue) { const suggestionsArray = suggestions; const suggestionsPromiseLike = suggestions; // Check to see if the returned value is an array, if it is then just set the suggestions value. // If the returned value is not an array then check to see if it's a promise or PromiseLike. If it is then resolve it asynchronously. if (Array.isArray(suggestionsArray)) { this.suggestions.value = suggestionsArray.filter(identity => !ObservableLike.getValue(this.props.selectedIdentities).some(selectedIdentity => selectedIdentity.entityId === identity.entityId)); } else if (suggestionsPromiseLike && suggestionsPromiseLike.then) { this.suggestionsLoading.value = true; let filteredNonIdentitySuggestions = []; if (this.props.pickerProvider.getAdditionalEntries) { this.nonIdentitySuggestions = this.props.pickerProvider.getAdditionalEntries().map(value => { return { displayName: value, entityType: IdentityType.Custom, entityId: value, originDirectory: "", originId: "" }; }); filteredNonIdentitySuggestions = this.nonIdentitySuggestions && initialSearchValue ? this.nonIdentitySuggestions.filter(s => startsWith(s.displayName, initialSearchValue)) : this.nonIdentitySuggestions; } // Ensure that the promise will only use the callback if it was the most recent one. const promise = (this.currentPromise = makeCancelable(suggestionsPromiseLike)); this.resolveEmailPromises.push(promise); promise.promise.then((newSuggestions) => { this.resolveEmailPromises = this.resolveEmailPromises.filter(p => p !== promise); if (promise === this.currentPromise) { const resultSuggestions = filteredNonIdentitySuggestions.length ? [...newSuggestions, ...filteredNonIdentitySuggestions,].sort((a, b) => localeIgnoreCaseComparer(a.displayName, b.displayName)) : newSuggestions; // Only update the suggestion list if the search value hasn't changed if (!initialSearchValue || this.lastSearchVal === initialSearchValue) { this.suggestions.value = resultSuggestions.filter(identity => !ObservableLike.getValue(this.props.selectedIdentities).some(selectedIdentity => selectedIdentity.entityId === identity.entityId)); } if (!!initialSearchValue && initialSearchValue !== "" && this.suggestions.value && this.suggestions.value.length > 0) { this.cachedResults[initialSearchValue] = resultSuggestions; } this.suggestionsLoading.value = false; } }, () => { this.resolveEmailPromises = this.resolveEmailPromises.filter(p => p !== promise); }); } } } IdentityPicker.area = "IdentityPicker"; IdentityPicker.feature = "MRU";