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 };