Browse Source

ref(teamFilter): Use CompactSelect (#34933)

* ref(teamFilter): Use CompactSelect

* ref(teamFilter): Remove filter.tsx file

* ref(teamFilter): Remove comment
Vu Luong 2 years ago
parent
commit
1cde816b95

+ 2 - 2
static/app/views/alerts/filterBar.tsx

@@ -13,7 +13,7 @@ import {getQueryStatus, getTeamParams} from './utils';
 
 interface Props {
   location: Location<any>;
-  onChangeFilter: (activeFilters: Set<string>) => void;
+  onChangeFilter: (activeFilters: string[]) => void;
   onChangeSearch: (query: string) => void;
   hasStatusFilters?: boolean;
   onChangeStatus?: (status: string) => void;
@@ -26,7 +26,7 @@ function FilterBar({
   onChangeStatus,
   hasStatusFilters,
 }: Props) {
-  const selectedTeams = new Set(getTeamParams(location.query.team));
+  const selectedTeams = getTeamParams(location.query.team);
   const selectedStatus = getQueryStatus(location.query.status);
 
   return (

+ 2 - 4
static/app/views/alerts/list/incidents/index.tsx

@@ -144,18 +144,16 @@ class IncidentsList extends AsyncComponent<Props, State & AsyncComponent['state'
     });
   };
 
-  handleChangeFilter = (activeFilters: Set<string>) => {
+  handleChangeFilter = (activeFilters: string[]) => {
     const {router, location} = this.props;
     const {cursor: _cursor, page: _page, ...currentQuery} = location.query;
 
-    const team = activeFilters.size ? [...activeFilters] : '';
-
     router.push({
       pathname: location.pathname,
       query: {
         ...currentQuery,
         // Preserve empty team query parameter
-        team: team.length === 0 ? '' : team,
+        team: activeFilters.length > 0 ? activeFilters : '',
       },
     });
   };

+ 0 - 181
static/app/views/alerts/list/rules/filter.tsx

@@ -1,181 +0,0 @@
-import {Fragment} from 'react';
-import styled from '@emotion/styled';
-
-import Badge from 'sentry/components/badge';
-import CheckboxFancy from 'sentry/components/checkboxFancy/checkboxFancy';
-import DropdownButton from 'sentry/components/dropdownButton';
-import DropdownControl, {Content} from 'sentry/components/dropdownControl';
-import {IconUser} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import overflowEllipsis from 'sentry/styles/overflowEllipsis';
-import space from 'sentry/styles/space';
-
-type Props = {
-  header: React.ReactElement;
-  items: Array<{checked: boolean; filtered: boolean; label: string; value: string}>;
-  onFilterChange: (filterSelection: Set<string>) => void;
-  fullWidth?: boolean;
-  showMyTeamsDescription?: boolean;
-};
-
-function Filter({
-  onFilterChange,
-  header,
-  items,
-  showMyTeamsDescription,
-  fullWidth = false,
-}: Props) {
-  function toggleFilter(value: string) {
-    const newSelection = new Set(
-      items.filter(item => item.checked).map(item => item.value)
-    );
-    if (newSelection.has(value)) {
-      newSelection.delete(value);
-    } else {
-      newSelection.add(value);
-    }
-    onFilterChange(newSelection);
-  }
-
-  function getActiveFilters() {
-    return items.filter(item => item.checked);
-  }
-
-  const activeFilters = getActiveFilters();
-
-  let filterDescription = showMyTeamsDescription ? t('My Teams') : t('All Teams');
-  if (activeFilters.length > 0) {
-    filterDescription = activeFilters[0].label;
-  }
-
-  return (
-    <DropdownControl
-      menuWidth="240px"
-      fullWidth={fullWidth}
-      alwaysRenderMenu={false}
-      button={({isOpen, getActorProps}) => (
-        <StyledDropdownButton
-          {...getActorProps()}
-          isOpen={isOpen}
-          icon={<IconUser />}
-          priority="default"
-          data-test-id="filter-button"
-          fullWidth={fullWidth}
-          rightAlignChevron={fullWidth}
-          detached
-        >
-          <DropdownButtonText fullWidth={fullWidth}>
-            {filterDescription}
-          </DropdownButtonText>
-          {activeFilters.length > 1 && (
-            <StyledBadge text={`+${activeFilters.length - 1}`} />
-          )}
-        </StyledDropdownButton>
-      )}
-    >
-      {({isOpen, getMenuProps}) => (
-        <MenuContent
-          {...getMenuProps()}
-          isOpen={isOpen}
-          blendCorner
-          alignMenu="left"
-          width="240px"
-          detached
-        >
-          <List>
-            {header}
-            <Fragment>
-              {items
-                .filter(item => !item.filtered)
-                .map(item => (
-                  <ListItem
-                    key={item.value}
-                    isChecked={item.checked}
-                    onClick={event => {
-                      event.stopPropagation();
-                      toggleFilter(item.value);
-                    }}
-                  >
-                    <TeamName>{item.label}</TeamName>
-                    <CheckboxFancy isChecked={item.checked} />
-                  </ListItem>
-                ))}
-            </Fragment>
-          </List>
-        </MenuContent>
-      )}
-    </DropdownControl>
-  );
-}
-
-const MenuContent = styled(Content)`
-  max-height: 290px;
-  overflow-y: auto;
-`;
-
-const StyledDropdownButton = styled(DropdownButton)<{fullWidth: boolean}>`
-  white-space: nowrap;
-  display: flex;
-  align-items: center;
-
-  z-index: ${p => p.theme.zIndex.dropdown};
-
-  ${p =>
-    p.fullWidth
-      ? `
-      width: 100%
-  `
-      : `max-width: 200px`}
-`;
-
-const DropdownButtonText = styled('span')<{fullWidth: boolean}>`
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  overflow: hidden;
-  flex: 1;
-
-  @media (max-width: ${p => p.theme.breakpoints[0]}) {
-    text-align: ${p => p.fullWidth && 'start'};
-  }
-`;
-
-const StyledBadge = styled(Badge)`
-  flex-shrink: 0;
-`;
-
-const List = styled('ul')`
-  list-style: none;
-  margin: 0;
-  padding: 0;
-`;
-
-const ListItem = styled('li')<{isChecked?: boolean}>`
-  display: grid;
-  grid-template-columns: 1fr max-content;
-  grid-column-gap: ${space(1)};
-  align-items: center;
-  padding: ${space(1)} ${space(2)};
-  border-bottom: 1px solid ${p => p.theme.border};
-  :hover {
-    background-color: ${p => p.theme.backgroundSecondary};
-  }
-  ${CheckboxFancy} {
-    opacity: ${p => (p.isChecked ? 1 : 0.3)};
-  }
-
-  &:hover ${CheckboxFancy} {
-    opacity: 1;
-  }
-
-  &:hover span {
-    color: ${p => p.theme.blue300};
-    text-decoration: underline;
-  }
-`;
-
-const TeamName = styled('div')`
-  font-size: ${p => p.theme.fontSizeMedium};
-  ${overflowEllipsis};
-`;
-
-export default Filter;

+ 2 - 3
static/app/views/alerts/list/rules/index.tsx

@@ -64,15 +64,14 @@ class AlertRulesList extends AsyncComponent<Props, State & AsyncComponent['state
     return [...new Set(ruleList?.map(({projects}) => projects).flat())];
   }
 
-  handleChangeFilter = (activeFilters: Set<string>) => {
+  handleChangeFilter = (activeFilters: string[]) => {
     const {router, location} = this.props;
     const {cursor: _cursor, page: _page, ...currentQuery} = location.query;
-    const teams = [...activeFilters];
     router.push({
       pathname: location.pathname,
       query: {
         ...currentQuery,
-        team: teams.length ? teams : '',
+        team: activeFilters.length > 0 ? activeFilters : '',
       },
     });
   };

+ 82 - 82
static/app/views/alerts/list/rules/teamFilter.tsx

@@ -1,119 +1,119 @@
-import {useState} from 'react';
+import {Fragment, useMemo} from 'react';
 import styled from '@emotion/styled';
 import debounce from 'lodash/debounce';
 
-import Input from 'sentry/components/forms/controls/input';
-import LoadingIndicator from 'sentry/components/loadingIndicator';
+import TeamAvatar from 'sentry/components/avatar/teamAvatar';
+import Badge from 'sentry/components/badge';
+import CompactSelect from 'sentry/components/forms/compactSelect';
 import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
+import {IconUser} from 'sentry/icons';
 import {t} from 'sentry/locale';
-import space from 'sentry/styles/space';
-import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
 import useTeams from 'sentry/utils/useTeams';
 
-import Filter from './filter';
-
 interface Props {
-  handleChangeFilter: (activeFilters: Set<string>) => void;
-  selectedTeams: Set<string>;
-  fullWidth?: boolean;
+  handleChangeFilter: (activeFilters: string[]) => void;
+  selectedTeams: string[];
   /**
    * only show teams user is a member of
    */
   showIsMemberTeams?: boolean;
-  /**
-   * show My Teams and Unassigned options
-   */
-  showMyTeamsAndUnassigned?: boolean;
   /**
    * show My Teams as the default dropdown description
    */
   showMyTeamsDescription?: boolean;
+  /**
+   * show suggested options (My Teams and Unassigned)
+   */
+  showSuggestedOptions?: boolean;
 }
 
+const suggestedOptions = [
+  {
+    label: t('My Teams'),
+    value: 'myteams',
+  },
+  {
+    label: t('Unassigned'),
+    value: 'unassigned',
+  },
+];
+
 function TeamFilter({
   selectedTeams,
   handleChangeFilter,
-  fullWidth = false,
   showIsMemberTeams = false,
-  showMyTeamsAndUnassigned = true,
+  showSuggestedOptions = true,
   showMyTeamsDescription = false,
 }: Props) {
-  const {teams, onSearch, fetching} = useTeams();
-  const debouncedSearch = debounce(onSearch, DEFAULT_DEBOUNCE_DURATION);
-  const [teamFilterSearch, setTeamFilterSearch] = useState<string | undefined>();
-  const isSuperuser = isActiveSuperuser();
+  const {teams, onSearch, fetching} = useTeams({provideUserTeams: showIsMemberTeams});
 
-  const additionalOptions = [
-    {
-      label: t('My Teams'),
-      value: 'myteams',
-      checked: selectedTeams.has('myteams'),
-      filtered: false,
-    },
-    {
-      label: t('Unassigned'),
-      value: 'unassigned',
-      checked: selectedTeams.has('unassigned'),
-      filtered: false,
-    },
-  ];
-  const isMemberTeams = teams.filter(team => team.isMember);
-  const teamItems = (isSuperuser ? teams : showIsMemberTeams ? isMemberTeams : teams).map(
-    ({id, slug}) => ({
-      label: slug,
-      value: id,
-      filtered: teamFilterSearch
-        ? !slug.toLowerCase().includes(teamFilterSearch.toLowerCase())
-        : false,
-      checked: selectedTeams.has(id),
-    })
+  const teamOptions = useMemo(
+    () =>
+      teams.map(team => ({
+        value: team.id,
+        label: `#${team.slug}`,
+        leadingItems: <TeamAvatar team={team} size={18} />,
+      })),
+    [teams]
   );
 
+  const [triggerIcon, triggerLabel] = useMemo(() => {
+    const firstSelectedSuggestion =
+      selectedTeams[0] && suggestedOptions.find(opt => opt.value === selectedTeams[0]);
+
+    const firstSelectedTeam =
+      selectedTeams[0] && teams.find(team => team.id === selectedTeams[0]);
+
+    if (firstSelectedSuggestion) {
+      return [<IconUser key={0} />, firstSelectedSuggestion.label];
+    }
+
+    if (firstSelectedTeam) {
+      return [
+        <TeamAvatar team={firstSelectedTeam} size={16} key={0} />,
+        `#${firstSelectedTeam.slug}`,
+      ];
+    }
+
+    return [
+      <IconUser key={0} />,
+      showMyTeamsDescription ? t('My Teams') : t('All Teams'),
+    ];
+  }, [selectedTeams, teams, showMyTeamsDescription]);
+
   return (
-    <Filter
-      fullWidth={fullWidth}
-      showMyTeamsDescription={showMyTeamsDescription}
-      header={
-        <InputWrapper>
-          <StyledInput
-            autoFocus
-            placeholder={t('Filter teams')}
-            onClick={event => {
-              event.stopPropagation();
-            }}
-            onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
-              const search = event.target.value;
-              setTeamFilterSearch(search);
-              debouncedSearch(search);
-            }}
-            value={teamFilterSearch || ''}
-          />
-          {fetching && <StyledLoadingIndicator size={16} mini />}
-        </InputWrapper>
+    <CompactSelect
+      multiple
+      isClearable
+      isSearchable
+      isLoading={fetching}
+      menuTitle={t('Filter teams')}
+      options={
+        showSuggestedOptions
+          ? [
+              {value: '_suggested', label: t('Suggested'), options: suggestedOptions},
+              {value: '_teams', label: t('Teams'), options: teamOptions},
+            ]
+          : teamOptions
       }
-      onFilterChange={handleChangeFilter}
-      items={
-        showMyTeamsAndUnassigned ? [...additionalOptions, ...teamItems] : [...teamItems]
+      value={selectedTeams}
+      onInputChange={debounce(val => void onSearch(val), DEFAULT_DEBOUNCE_DURATION)}
+      onChange={opts => handleChangeFilter(opts.map(opt => opt.value))}
+      triggerLabel={
+        <Fragment>
+          {triggerLabel}
+          {selectedTeams.length > 1 && (
+            <StyledBadge text={`+${selectedTeams.length - 1}`} />
+          )}
+        </Fragment>
       }
+      triggerProps={{icon: triggerIcon}}
     />
   );
 }
 
 export default TeamFilter;
 
-const InputWrapper = styled('div')`
-  position: relative;
-`;
-
-const StyledInput = styled(Input)`
-  border: none;
-  border-radius: 0;
-  border-bottom: solid 1px ${p => p.theme.border};
-  font-size: ${p => p.theme.fontSizeMedium};
-`;
-
-const StyledLoadingIndicator = styled(LoadingIndicator)`
-  position: absolute;
-  right: 0;
-  top: ${space(0.75)};
+const StyledBadge = styled(Badge)`
+  flex-shrink: 0;
 `;

+ 6 - 8
static/app/views/projectsDashboard/index.tsx

@@ -64,15 +64,15 @@ function Dashboard({teams, organization, loadingTeams, error, router, location}:
   const canJoinTeam = organization.access.includes('team:read');
   const hasProjectAccess = organization.access.includes('project:read');
 
-  const selectedTeams = new Set(getTeamParams(location ? location.query.team : ''));
-  const filteredTeams = teams.filter(team => selectedTeams.has(team.id));
+  const selectedTeams = getTeamParams(location ? location.query.team : '');
+  const filteredTeams = teams.filter(team => selectedTeams.includes(team.id));
 
   const filteredTeamProjects = uniqBy(
     flatten((filteredTeams ?? teams).map(team => team.projects)),
     'id'
   );
   const projects = uniqBy(flatten(teams.map(teamObj => teamObj.projects)), 'id');
-  const currentProjects = !selectedTeams.size ? projects : filteredTeamProjects;
+  const currentProjects = selectedTeams.length === 0 ? projects : filteredTeamProjects;
   const filteredProjects = (currentProjects ?? projects).filter(project =>
     project.slug.includes(projectQuery)
   );
@@ -85,15 +85,14 @@ function Dashboard({teams, organization, loadingTeams, error, router, location}:
     setProjectQuery(searchQuery);
   }
 
-  function handleChangeFilter(activeFilters: Set<string>) {
+  function handleChangeFilter(activeFilters: string[]) {
     const {...currentQuery} = location.query;
-    const team = activeFilters.size ? [...activeFilters] : '';
 
     router.push({
       pathname: location.pathname,
       query: {
         ...currentQuery,
-        team: team.length === 0 ? '' : team,
+        team: activeFilters.length > 0 ? activeFilters : '',
       },
     });
   }
@@ -151,9 +150,8 @@ function Dashboard({teams, organization, loadingTeams, error, router, location}:
                 <TeamFilter
                   selectedTeams={selectedTeams}
                   handleChangeFilter={handleChangeFilter}
-                  fullWidth
                   showIsMemberTeams
-                  showMyTeamsAndUnassigned={false}
+                  showSuggestedOptions={false}
                   showMyTeamsDescription
                 />
                 <StyledSearchBar

+ 1 - 1
tests/js/spec/views/alerts/rules/index.spec.jsx

@@ -299,7 +299,7 @@ describe('AlertRulesList', () => {
       getComponent({location: {query: {team: 'myteams'}, search: '?team=myteams`'}})
     );
 
-    userEvent.click(await screen.findByTestId('filter-button'));
+    userEvent.click(await screen.findByRole('button', {name: 'My Teams'}));
 
     // Uncheck myteams
     const myTeams = await screen.findAllByText('My Teams');