github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/components/dropdown/dropdown.tsx (about)

     1  /*This file is part of kuberpult.
     2  
     3  Kuberpult is free software: you can redistribute it and/or modify
     4  it under the terms of the Expat(MIT) License as published by
     5  the Free Software Foundation.
     6  
     7  Kuberpult is distributed in the hope that it will be useful,
     8  but WITHOUT ANY WARRANTY; without even the implied warranty of
     9  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    10  MIT License for more details.
    11  
    12  You should have received a copy of the MIT License
    13  along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>.
    14  
    15  Copyright 2023 freiheit.com*/
    16  import * as React from 'react';
    17  import classNames from 'classnames';
    18  import { useCallback, useRef } from 'react';
    19  import { useTeamNames } from '../../utils/store';
    20  import { useSearchParams } from 'react-router-dom';
    21  import { Checkbox } from './checkbox';
    22  import { PlainDialog } from '../dialog/ConfirmationDialog';
    23  import { Button } from '../button';
    24  
    25  export type DropdownProps = {
    26      className?: string;
    27      placeholder?: string;
    28      leadingIcon?: string;
    29  };
    30  
    31  export type DropdownSelectProps = {
    32      handleChange: (id: string | undefined) => void;
    33      isEmpty: (arr: string[] | undefined) => boolean;
    34      allTeams: string[];
    35      selectedTeams: string[];
    36  };
    37  
    38  const allTeamsId = 'all-teams';
    39  
    40  // A dropdown allowing multiple selections
    41  export const DropdownSelect: React.FC<DropdownSelectProps> = (props) => {
    42      const { handleChange, allTeams, selectedTeams } = props;
    43  
    44      const [open, setOpen] = React.useState(false);
    45      const openClose = React.useCallback(() => {
    46          setOpen(!open);
    47      }, [open, setOpen]);
    48      const onCancel = React.useCallback(() => {
    49          setOpen(false);
    50      }, []);
    51  
    52      const onChange = React.useCallback(
    53          (id: string) => {
    54              handleChange(id);
    55          },
    56          [handleChange]
    57      );
    58      const onClear = React.useCallback(() => {
    59          handleChange(undefined);
    60      }, [handleChange]);
    61      const onSelectAll = React.useCallback(() => {
    62          handleChange(allTeamsId);
    63      }, [handleChange]);
    64  
    65      const allTeamsLabel = 'Clear';
    66      return (
    67          <div className={'dropdown-container'}>
    68              <div className={'dropdown-arrow-container'}>
    69                  <div className={'dropdown-arrow'}>⌄</div>
    70                  <input
    71                      type="text"
    72                      className="dropdown-input"
    73                      value={selectedTeams.length === 0 ? 'Filter Teams' : '' + selectedTeams.join(', ')}
    74                      aria-label={'Teams'}
    75                      disabled={open}
    76                      onChange={openClose}
    77                      onSelect={openClose}
    78                      data-testid="teams-dropdown-input"
    79                  />
    80              </div>
    81              <PlainDialog open={open} onClose={onCancel} classNames={'dropdown'} disableBackground={true} center={false}>
    82                  <div>
    83                      {allTeams.map((team: string) => (
    84                          <div key={team}>
    85                              <Checkbox
    86                                  id={team}
    87                                  enabled={selectedTeams?.includes(team)}
    88                                  label={team}
    89                                  onClick={onChange}
    90                              />
    91                          </div>
    92                      ))}
    93                      <div className={'confirmation-dialog-footer'}>
    94                          <div className={'item'} key={'button-menu-clear'} title={'ESC also closes the dialog'}>
    95                              <Button
    96                                  className="mdc-button--unelevated button-confirm"
    97                                  label={'Select All'}
    98                                  onClick={onSelectAll}
    99                              />
   100                          </div>
   101                          <div className={'item'} key={'button-menu-all'} title={'ESC also closes the dialog'}>
   102                              <Button
   103                                  className="mdc-button--unelevated button-confirm"
   104                                  label={allTeamsLabel}
   105                                  onClick={onClear}
   106                              />
   107                          </div>
   108                      </div>
   109                  </div>
   110              </PlainDialog>
   111          </div>
   112      );
   113  };
   114  
   115  export const Dropdown = (props: DropdownProps): JSX.Element => {
   116      const { className, placeholder, leadingIcon } = props;
   117      const control = useRef<HTMLDivElement>(null);
   118      const teams = useTeamNames();
   119      const [searchParams, setSearchParams] = useSearchParams();
   120  
   121      const allClassName = classNames(
   122          'mdc-select',
   123          'mdc-select--outlined',
   124          {
   125              'mdc-select--no-label': !placeholder,
   126              'mdc-select--with-leading-icon': leadingIcon,
   127          },
   128          className
   129      );
   130      const separator = ',';
   131      const selectedTeams = (searchParams.get('teams') || '')
   132          .split(separator)
   133          .filter((t: string) => t !== null && t !== '');
   134  
   135      const isEmpty = useCallback(
   136          (arr: string[] | undefined) => (arr ? arr.filter((val) => val !== '').length === 0 : true),
   137          []
   138      );
   139  
   140      const handleChange = useCallback(
   141          (team: string | undefined) => {
   142              if (team === undefined) {
   143                  searchParams.delete('teams');
   144                  setSearchParams(searchParams);
   145                  return;
   146              }
   147              if (team === allTeamsId) {
   148                  searchParams.set('teams', teams.join(separator));
   149                  setSearchParams(searchParams);
   150                  return;
   151              }
   152  
   153              const index = selectedTeams.indexOf(team);
   154              let newTeams = selectedTeams;
   155              if (index >= 0) {
   156                  newTeams.splice(index, 1);
   157              } else {
   158                  newTeams = selectedTeams.concat([team]);
   159              }
   160              if (newTeams.length === 0) {
   161                  searchParams.delete('teams');
   162              } else {
   163                  searchParams.set('teams', newTeams.join(separator));
   164              }
   165              setSearchParams(searchParams);
   166          },
   167          [teams, searchParams, setSearchParams, selectedTeams]
   168      );
   169  
   170      return (
   171          <div className={allClassName} ref={control}>
   172              <DropdownSelect
   173                  handleChange={handleChange}
   174                  isEmpty={isEmpty}
   175                  allTeams={teams}
   176                  selectedTeams={selectedTeams}
   177              />
   178          </div>
   179      );
   180  };