123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- import {Component} from 'react';
- import styled from '@emotion/styled';
- import debounce from 'lodash/debounce';
- import {Client} from 'sentry/api';
- import SelectControl from 'sentry/components/forms/controls/selectControl';
- import IdBadge from 'sentry/components/idBadge';
- import Tooltip from 'sentry/components/tooltip';
- import {t} from 'sentry/locale';
- import MemberListStore from 'sentry/stores/memberListStore';
- import {Member, Organization, Project, User} from 'sentry/types';
- import {callIfFunction} from 'sentry/utils/callIfFunction';
- import withApi from 'sentry/utils/withApi';
- const getSearchKeyForUser = (user: User) =>
- `${user.email && user.email.toLowerCase()} ${user.name && user.name.toLowerCase()}`;
- type MentionableUser = {
- actor: {
- id: string;
- name: string;
- type: 'user';
- };
- label: React.ReactElement;
- searchKey: string;
- value: string;
- disabled?: boolean;
- };
- type Props = {
- api: Client;
- onChange: (value: any) => any;
- organization: Organization;
- value: any;
- disabled?: boolean;
- onInputChange?: (value: any) => any;
- placeholder?: string;
- project?: Project;
- styles?: {control?: (provided: any) => any};
- };
- type State = {
- inputValue: string;
- loading: boolean;
- memberListLoading: boolean;
- options: MentionableUser[] | null;
- };
- type FilterOption<T> = {
- data: T;
- label: React.ReactNode;
- value: string;
- };
- /**
- * A component that allows you to select either members and/or teams
- */
- class SelectMembers extends Component<Props, State> {
- state: State = {
- loading: false,
- inputValue: '',
- options: null,
- memberListLoading: !MemberListStore.isLoaded(),
- };
- componentWillUnmount() {
- this.unlisteners.forEach(callIfFunction);
- }
- unlisteners = [
- MemberListStore.listen(() => {
- this.setState({
- memberListLoading: !MemberListStore.isLoaded(),
- });
- }, undefined),
- ];
- renderUserBadge = (user: User) => (
- <IdBadge avatarSize={24} user={user} hideEmail useLink={false} />
- );
- createMentionableUser = (user: User): MentionableUser => ({
- value: user.id,
- label: this.renderUserBadge(user),
- searchKey: getSearchKeyForUser(user),
- actor: {
- type: 'user',
- id: user.id,
- name: user.name,
- },
- });
- createUnmentionableUser = ({user}) => ({
- ...this.createMentionableUser(user),
- disabled: true,
- label: (
- <DisabledLabel>
- <Tooltip
- position="left"
- title={t('%s is not a member of project', user.name || user.email)}
- >
- {this.renderUserBadge(user)}
- </Tooltip>
- </DisabledLabel>
- ),
- });
- getMentionableUsers() {
- return MemberListStore.getAll().map(this.createMentionableUser);
- }
- handleChange = newValue => {
- this.props.onChange(newValue);
- };
- handleInputChange = inputValue => {
- this.setState({inputValue});
- if (this.props.onInputChange) {
- this.props.onInputChange(inputValue);
- }
- };
- queryMembers = debounce((query, cb) => {
- const {api, organization} = this.props;
- // Because this function is debounced, the component can potentially be
- // unmounted before this fires, in which case, `api` is null
- if (!api) {
- return null;
- }
- return api
- .requestPromise(`/organizations/${organization.slug}/members/`, {
- query: {query},
- })
- .then(
- (data: Member[]) => cb(null, data),
- err => cb(err)
- );
- }, 250);
- handleLoadOptions = (): Promise<MentionableUser[]> => {
- const usersInProject = this.getMentionableUsers();
- const usersInProjectById = usersInProject.map(({actor}) => actor.id);
- // Return a promise for `react-select`
- return new Promise((resolve, reject) => {
- this.queryMembers(this.state.inputValue, (err, result) => {
- if (err) {
- reject(err);
- } else {
- resolve(result);
- }
- });
- })
- .then(
- members =>
- // Be careful here as we actually want the `users` object, otherwise it means user
- // has not registered for sentry yet, but has been invited
- (members
- ? (members as Member[])
- .filter(({user}) => user && usersInProjectById.indexOf(user.id) === -1)
- .map(this.createUnmentionableUser)
- : []) as MentionableUser[]
- )
- .then((members: MentionableUser[]) => {
- const options = [...usersInProject, ...members];
- this.setState({options});
- return options;
- });
- };
- render() {
- const {placeholder, styles} = this.props;
- // If memberList is still loading we need to disable a placeholder Select,
- // otherwise `react-select` will call `loadOptions` and prematurely load
- // options
- if (this.state.memberListLoading) {
- return <StyledSelectControl isDisabled placeholder={t('Loading')} />;
- }
- return (
- <StyledSelectControl
- filterOption={(option: FilterOption<MentionableUser>, filterText: string) =>
- option?.data?.searchKey?.indexOf(filterText) > -1
- }
- loadOptions={this.handleLoadOptions}
- defaultOptions
- async
- isDisabled={this.props.disabled}
- cacheOptions={false}
- placeholder={placeholder}
- onInputChange={this.handleInputChange}
- onChange={this.handleChange}
- value={this.state.options?.find(({value}) => value === this.props.value)}
- styles={{
- ...(styles ?? {}),
- option: (provided, state: any) => ({
- ...provided,
- svg: {
- color: state.isSelected && state.theme.white,
- },
- }),
- }}
- />
- );
- }
- }
- const DisabledLabel = styled('div')`
- display: flex;
- opacity: 0.5;
- overflow: hidden; /* Needed so that "Add to team" button can fit */
- `;
- const StyledSelectControl = styled(SelectControl)`
- .Select-value {
- display: flex;
- align-items: center;
- }
- .Select-input {
- margin-left: 32px;
- }
- `;
- export default withApi(SelectMembers);
|