import Search from '@mui/icons-material/Search'; import { Box, Button, InputAdornment, List, ListItemText } from '@mui/material'; import { type FC, type ReactNode, useRef, useState, useMemo } from 'react'; import { StyledCheckbox, StyledDropdown, StyledListItem, StyledPopover, StyledTextField, TableSearchInput, } from './SelectionButton.styles'; import { ChangeRequestTable } from './ChangeRequestTable'; export interface IFilterItemProps { label: ReactNode; options: Array<{ label: string; value: string }>; selectedOptions: Set; onChange: (values: Set) => void; } export type FilterItemParams = { operator: string; values: string[]; }; interface UseSelectionManagementProps { handleToggle: (value: string) => () => void; } const useSelectionManagement = ({ handleToggle, }: UseSelectionManagementProps) => { const listRefs = useRef>([]); const handleSelection = ( event: React.KeyboardEvent, index: number, filteredOptions: { label: string; value: string }[], ) => { // we have to be careful not to prevent other keys e.g tab if (event.key === 'ArrowDown' && index < listRefs.current.length - 1) { event.preventDefault(); listRefs.current[index + 1]?.focus(); } else if (event.key === 'ArrowUp' && index > 0) { event.preventDefault(); listRefs.current[index - 1]?.focus(); } else if ( event.key === 'Enter' && index === 0 && listRefs.current[0]?.value && filteredOptions.length > 0 ) { // if the search field is not empty and the user presses // enter from the search field, toggle the topmost item in // the filtered list event.preventDefault(); handleToggle(filteredOptions[0].value)(); } else if ( event.key === 'Enter' || // allow selection with space when not in the search field (index !== 0 && event.key === ' ') ) { event.preventDefault(); if (index > 0) { const listItemIndex = index - 1; handleToggle(filteredOptions[listItemIndex].value)(); } } }; return { listRefs, handleSelection }; }; type CombinedSelectProps = { options: Array<{ label: string; value: string }>; onChange: (value: string) => void; button: { label: string; icon: ReactNode }; search: { label: string; placeholder: string; }; multiselect?: { selectedOptions: Set }; onOpen?: () => void; onClose?: () => void; }; const CombinedSelect: FC = ({ options, onChange, button, search, multiselect, onOpen = () => {}, onClose = () => {}, }) => { const ref = useRef(null); const [anchorEl, setAnchorEl] = useState(); const [searchText, setSearchText] = useState(''); const open = () => { setSearchText(''); setAnchorEl(ref.current); onOpen(); }; const handleClose = () => { setAnchorEl(null); onClose(); }; const onSelection = (selected: string) => { onChange(selected); if (!multiselect) { handleClose(); } }; const { listRefs, handleSelection } = useSelectionManagement({ handleToggle: (selected: string) => () => onSelection(selected), }); const filteredOptions = options?.filter((option) => option.label.toLowerCase().includes(searchText.toLowerCase()), ); return ( <> setSearchText(event.target.value)} label={search.label} placeholder={search.placeholder} autoFocus InputProps={{ startAdornment: ( ), }} inputRef={(el) => { listRefs.current[0] = el; }} onKeyDown={(event) => handleSelection(event, 0, filteredOptions) } /> {filteredOptions.map((option, index) => { const labelId = `checkbox-list-label-${option.value}`; return ( { onSelection(option.value); }} ref={(el) => { listRefs.current[index + 1] = el; }} onKeyDown={(event) => handleSelection( event, index + 1, filteredOptions, ) } > {multiselect ? ( ) : null} ); })} ); }; type MultiselectListProps = Pick< CombinedSelectProps, 'options' | 'button' | 'search' | 'onOpen' | 'onClose' > & { selectedOptions: Set; onChange: (values: Set) => void; }; export const MultiselectList: FC = ({ selectedOptions, onChange, ...rest }) => { // todo: add "select all" and "deselect all" const handleToggle = (value: string) => { if (selectedOptions.has(value)) { selectedOptions.delete(value); } else { selectedOptions.add(value); } onChange(new Set(selectedOptions)); }; return ( ); }; type SingleSelectListProps = Pick< CombinedSelectProps, 'options' | 'button' | 'search' | 'onChange' | 'onOpen' | 'onClose' >; export const SingleSelectList: FC = (props) => { return ; }; type TableSelectProps = Pick< CombinedSelectProps, 'button' | 'search' | 'onOpen' | 'onClose' > & { updateProjectChangeRequestConfiguration: { disableChangeRequests: (env: string) => void; enableChangeRequests: (env: string, requiredApprovals: number) => void; }; activeEnvironments: { name: string; type: string; }[]; projectChangeRequestConfiguration: Record< string, { requiredApprovals: number } >; disabled: boolean; }; export const TableSelect: FC = ({ button, disabled, search, projectChangeRequestConfiguration, updateProjectChangeRequestConfiguration, activeEnvironments, onOpen = () => {}, onClose = () => {}, }) => { const configured = useMemo(() => { return Object.fromEntries( Object.entries(projectChangeRequestConfiguration).map( ([name, config]) => [ name, { ...config, changeRequestEnabled: true }, ], ), ); }, [projectChangeRequestConfiguration]); const tableEnvs = useMemo( () => activeEnvironments.map(({ name, type }) => ({ name, type, ...(configured[name] ?? { changeRequestEnabled: false }), })), [configured, activeEnvironments], ); const onEnable = (name: string, requiredApprovals: number) => { updateProjectChangeRequestConfiguration.enableChangeRequests( name, requiredApprovals, ); }; const onDisable = (name: string) => { updateProjectChangeRequestConfiguration.disableChangeRequests(name); }; const ref = useRef(null); const [anchorEl, setAnchorEl] = useState(); const [searchText, setSearchText] = useState(''); const open = () => { setSearchText(''); setAnchorEl(ref.current); onOpen(); }; const handleClose = () => { setAnchorEl(null); onClose(); }; const filteredEnvs = tableEnvs.filter((env) => env.name.toLowerCase().includes(searchText.toLowerCase()), ); const toggleTopItem = (event: React.KeyboardEvent) => { if ( event.key === 'Enter' && searchText.trim().length > 0 && filteredEnvs.length > 0 ) { const firstEnv = filteredEnvs[0]; if (firstEnv.name in configured) { onDisable(firstEnv.name); } else { onEnable(firstEnv.name, 1); } } }; return ( <> setSearchText(event.target.value)} label={search.label} placeholder={search.placeholder} autoFocus InputProps={{ startAdornment: ( ), }} onKeyDown={toggleTopItem} /> ); };