azure-devops-ui
Version:
React components for building web UI in Azure DevOps
248 lines (247 loc) • 15.1 kB
JavaScript
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";