Browse Source

ref(ui): Use TeamBadge in member details + project teams (#27050)

Evan Purkhiser 3 years ago
parent
commit
acac0beae9

+ 51 - 37
static/app/views/settings/components/teamSelect.tsx

@@ -8,6 +8,7 @@ import Confirm from 'app/components/confirm';
 import DropdownAutoComplete from 'app/components/dropdownAutoComplete';
 import {Item} from 'app/components/dropdownAutoComplete/types';
 import DropdownButton from 'app/components/dropdownButton';
+import TeamBadge from 'app/components/idBadge/teamBadge';
 import Link from 'app/components/links/link';
 import {Panel, PanelBody, PanelHeader, PanelItem} from 'app/components/panels';
 import {DEFAULT_DEBOUNCE_DURATION, TEAMS_PER_PAGE} from 'app/constants';
@@ -28,7 +29,7 @@ type Props = {
   /**
    * Teams that are already selected.
    */
-  selectedTeams: string[];
+  selectedTeams: Team[];
   /**
    * callback when teams are added
    */
@@ -52,13 +53,13 @@ type Props = {
 
 type State = {
   loading: boolean;
-  teams: null | Team[];
+  teamsSearch: null | Team[];
 };
 
 class TeamSelect extends React.Component<Props, State> {
   state: State = {
     loading: true,
-    teams: null,
+    teamsSearch: null,
   };
 
   componentDidMount() {
@@ -67,10 +68,13 @@ class TeamSelect extends React.Component<Props, State> {
 
   fetchTeams = debounce(async (query?: string) => {
     const {api, organization} = this.props;
-    const teams = await api.requestPromise(`/organizations/${organization.slug}/teams/`, {
-      query: {query, per_page: TEAMS_PER_PAGE},
-    });
-    this.setState({teams, loading: false});
+    const teamsSearch = await api.requestPromise(
+      `/organizations/${organization.slug}/teams/`,
+      {
+        query: {query, per_page: TEAMS_PER_PAGE},
+      }
+    );
+    this.setState({teamsSearch, loading: false});
   }, DEFAULT_DEBOUNCE_DURATION);
 
   handleQueryUpdate = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -79,7 +83,7 @@ class TeamSelect extends React.Component<Props, State> {
   };
 
   handleAddTeam = (option: Item) => {
-    const team = this.state.teams?.find(tm => tm.slug === option.value);
+    const team = this.state.teamsSearch?.find(tm => tm.slug === option.value);
     if (team) {
       this.props.onAddTeam(team);
     }
@@ -91,20 +95,22 @@ class TeamSelect extends React.Component<Props, State> {
 
   renderTeamAddDropDown() {
     const {disabled, selectedTeams, menuHeader} = this.props;
-    const {teams} = this.state;
+    const {teamsSearch} = this.state;
     const isDisabled = disabled;
 
     let options: Item[] = [];
-    if (teams === null || teams.length === 0) {
+    if (teamsSearch === null || teamsSearch.length === 0) {
       options = [];
     } else {
-      options = teams
-        .filter(team => !selectedTeams.includes(team.slug))
+      options = teamsSearch
+        .filter(
+          team => !selectedTeams.some(selectedTeam => selectedTeam.slug === team.slug)
+        )
         .map((team, index) => ({
           index,
           value: team.slug,
           searchKey: team.slug,
-          label: <TeamDropdownElement>#{team.slug}</TeamDropdownElement>,
+          label: <DropdownTeamBadge avatarSize={18} team={team} />,
         }));
     }
 
@@ -147,7 +153,7 @@ class TeamSelect extends React.Component<Props, State> {
 
     return selectedTeams.map(team => (
       <TeamRow
-        key={team}
+        key={team.slug}
         orgId={organization.slug}
         team={team}
         onRemove={this.handleRemove}
@@ -171,31 +177,39 @@ class TeamSelect extends React.Component<Props, State> {
   }
 }
 
-const TeamRow = props => {
-  const {orgId, team, onRemove, disabled, confirmMessage} = props;
-  return (
-    <TeamPanelItem>
-      <StyledLink to={`/settings/${orgId}/teams/${team}/`}>{`#${team}`}</StyledLink>
-      <Confirm
-        message={confirmMessage}
-        bypass={!confirmMessage}
-        onConfirm={() => onRemove(team)}
-        disabled={disabled}
-      >
-        <Button
-          size="xsmall"
-          icon={<IconSubtract isCircled size="xs" />}
-          disabled={disabled}
-        >
-          {t('Remove')}
-        </Button>
-      </Confirm>
-    </TeamPanelItem>
-  );
+type TeamRowProps = {
+  orgId: string;
+  team: Team;
+  onRemove: Props['onRemoveTeam'];
+  disabled: boolean;
+  confirmMessage: string | null;
 };
 
-const TeamDropdownElement = styled('div')`
-  padding: ${space(0.5)} 0px;
+const TeamRow = ({orgId, team, onRemove, disabled, confirmMessage}: TeamRowProps) => (
+  <TeamPanelItem>
+    <StyledLink to={`/settings/${orgId}/teams/${team.slug}/`}>
+      <TeamBadge team={team} />
+    </StyledLink>
+    <Confirm
+      message={confirmMessage}
+      bypass={!confirmMessage}
+      onConfirm={() => onRemove(team.slug)}
+      disabled={disabled}
+    >
+      <Button
+        size="xsmall"
+        icon={<IconSubtract isCircled size="xs" />}
+        disabled={disabled}
+      >
+        {t('Remove')}
+      </Button>
+    </Confirm>
+  </TeamPanelItem>
+);
+
+const DropdownTeamBadge = styled(TeamBadge)`
+  font-weight: normal;
+  font-size: ${p => p.theme.fontSizeMedium};
   text-transform: none;
 `;
 

+ 7 - 3
static/app/views/settings/organizationMembers/organizationMemberDetail.tsx

@@ -26,6 +26,7 @@ import {Member, Organization, Team} from 'app/types';
 import isMemberDisabledFromLimit from 'app/utils/isMemberDisabledFromLimit';
 import recreateRoute from 'app/utils/recreateRoute';
 import withOrganization from 'app/utils/withOrganization';
+import withTeams from 'app/utils/withTeams';
 import AsyncView from 'app/views/asyncView';
 import Field from 'app/views/settings/components/forms/field';
 import SettingsPageHeader from 'app/views/settings/components/settingsPageHeader';
@@ -47,6 +48,7 @@ type RouteParams = {
 
 type Props = {
   organization: Organization;
+  teams: Team[];
 } & RouteComponentProps<RouteParams, {}>;
 
 type State = {
@@ -232,7 +234,7 @@ class OrganizationMemberDetail extends AsyncView<Props, State> {
   }
 
   renderBody() {
-    const {organization} = this.props;
+    const {organization, teams} = this.props;
     const {member} = this.state;
 
     if (!member) {
@@ -362,7 +364,9 @@ class OrganizationMemberDetail extends AsyncView<Props, State> {
 
         <TeamSelect
           organization={organization}
-          selectedTeams={member.teams}
+          selectedTeams={member.teams
+            .map(teamSlug => teams.find(team => team.slug === teamSlug)!)
+            .filter(team => team !== undefined)}
           disabled={!canEdit}
           onAddTeam={this.handleAddTeam}
           onRemoveTeam={this.handleRemoveTeam}
@@ -383,7 +387,7 @@ class OrganizationMemberDetail extends AsyncView<Props, State> {
   }
 }
 
-export default withOrganization(OrganizationMemberDetail);
+export default withTeams(withOrganization(OrganizationMemberDetail));
 
 const ExtraHeaderText = styled('div')`
   color: ${p => p.theme.gray300};

+ 1 - 2
static/app/views/settings/project/projectTeams.tsx

@@ -126,7 +126,6 @@ class ProjectTeams extends AsyncView<Props, State> {
       params.projectId
     );
     const {projectTeams} = this.state;
-    const selectedTeams = projectTeams?.map(({slug}) => slug) ?? [];
 
     const menuHeader = (
       <StyledTeamsLabel>
@@ -152,7 +151,7 @@ class ProjectTeams extends AsyncView<Props, State> {
         <SettingsPageHeader title={t('%s Teams', params.projectId)} />
         <TeamSelect
           organization={organization}
-          selectedTeams={selectedTeams}
+          selectedTeams={projectTeams ?? []}
           onAddTeam={this.handleAdd}
           onRemoveTeam={this.handleRemove}
           menuHeader={menuHeader}

+ 4 - 1
tests/js/spec/views/settings/organizationMembers/organizationMemberDetail.spec.jsx

@@ -2,6 +2,7 @@ import {mountWithTheme} from 'sentry-test/enzyme';
 import {mountGlobalModal} from 'sentry-test/modal';
 
 import {updateMember} from 'app/actionCreators/members';
+import TeamStore from 'app/stores/teamStore';
 import OrganizationMemberDetail from 'app/views/settings/organizationMembers/organizationMemberDetail';
 
 jest.mock('app/actionCreators/members', () => ({
@@ -51,6 +52,8 @@ describe('OrganizationMemberDetail', function () {
       routerContext = TestStubs.routerContext([{organization}]);
     });
 
+    TeamStore.loadInitialData(teams);
+
     beforeEach(function () {
       MockApiClient.clearMockResponses();
       MockApiClient.addMockResponse({
@@ -139,7 +142,7 @@ describe('OrganizationMemberDetail', function () {
       wrapper.find('TeamSelect DropdownButton').simulate('click');
 
       // Click the first item
-      wrapper.find('TeamSelect TeamDropdownElement').first().simulate('click');
+      wrapper.find('TeamSelect DropdownTeamBadge').first().simulate('click');
 
       // Save Member
       wrapper.find('Button[priority="primary"]').simulate('click');