mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore: code cleanup for new project form pt 2 (#7190)
This is the second part of the code cleanup job. It primarily consists of breaking apart large files and organizing the code better
This commit is contained in:
		
							parent
							
								
									2649c8e7cd
								
							
						
					
					
						commit
						06de5de85c
					
				@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { styled } from '@mui/material';
 | 
				
			||||||
 | 
					import { StyledDropdownSearch } from './shared.styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const TableSearchInput = styled(StyledDropdownSearch)({
 | 
				
			||||||
 | 
					    maxWidth: '30ch',
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ScrollContainer = styled('div')({
 | 
				
			||||||
 | 
					    width: '100%',
 | 
				
			||||||
 | 
					    overflow: 'auto',
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					import { type FC, useState, useMemo } from 'react';
 | 
				
			||||||
 | 
					import { ConfigButton, type ConfigButtonProps } from './ConfigButton';
 | 
				
			||||||
 | 
					import { InputAdornment } from '@mui/material';
 | 
				
			||||||
 | 
					import Search from '@mui/icons-material/Search';
 | 
				
			||||||
 | 
					import { ChangeRequestTable } from './ChangeRequestTable';
 | 
				
			||||||
 | 
					import { TableSearchInput } from './ChangeRequestTableConfigButton.styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChangeRequestTableConfigButtonProps = Pick<
 | 
				
			||||||
 | 
					    ConfigButtonProps,
 | 
				
			||||||
 | 
					    'button' | 'onOpen' | 'onClose' | 'description'
 | 
				
			||||||
 | 
					> & {
 | 
				
			||||||
 | 
					    search: {
 | 
				
			||||||
 | 
					        label: string;
 | 
				
			||||||
 | 
					        placeholder: string;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    updateProjectChangeRequestConfiguration: {
 | 
				
			||||||
 | 
					        disableChangeRequests: (env: string) => void;
 | 
				
			||||||
 | 
					        enableChangeRequests: (env: string, requiredApprovals: number) => void;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    activeEnvironments: {
 | 
				
			||||||
 | 
					        name: string;
 | 
				
			||||||
 | 
					        type: string;
 | 
				
			||||||
 | 
					    }[];
 | 
				
			||||||
 | 
					    projectChangeRequestConfiguration: Record<
 | 
				
			||||||
 | 
					        string,
 | 
				
			||||||
 | 
					        { requiredApprovals: number }
 | 
				
			||||||
 | 
					    >;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ChangeRequestTableConfigButton: FC<
 | 
				
			||||||
 | 
					    ChangeRequestTableConfigButtonProps
 | 
				
			||||||
 | 
					> = ({
 | 
				
			||||||
 | 
					    button,
 | 
				
			||||||
 | 
					    search,
 | 
				
			||||||
 | 
					    projectChangeRequestConfiguration,
 | 
				
			||||||
 | 
					    updateProjectChangeRequestConfiguration,
 | 
				
			||||||
 | 
					    activeEnvironments,
 | 
				
			||||||
 | 
					    onOpen = () => {},
 | 
				
			||||||
 | 
					    onClose = () => {},
 | 
				
			||||||
 | 
					    ...props
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    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 [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>();
 | 
				
			||||||
 | 
					    const [searchText, setSearchText] = useState('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 (
 | 
				
			||||||
 | 
					        <ConfigButton
 | 
				
			||||||
 | 
					            button={button}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					            anchorEl={anchorEl}
 | 
				
			||||||
 | 
					            setAnchorEl={setAnchorEl}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <TableSearchInput
 | 
				
			||||||
 | 
					                variant='outlined'
 | 
				
			||||||
 | 
					                size='small'
 | 
				
			||||||
 | 
					                value={searchText}
 | 
				
			||||||
 | 
					                onChange={(event) => setSearchText(event.target.value)}
 | 
				
			||||||
 | 
					                hideLabel
 | 
				
			||||||
 | 
					                label={search.label}
 | 
				
			||||||
 | 
					                placeholder={search.placeholder}
 | 
				
			||||||
 | 
					                autoFocus
 | 
				
			||||||
 | 
					                InputProps={{
 | 
				
			||||||
 | 
					                    startAdornment: (
 | 
				
			||||||
 | 
					                        <InputAdornment position='start'>
 | 
				
			||||||
 | 
					                            <Search fontSize='small' />
 | 
				
			||||||
 | 
					                        </InputAdornment>
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					                onKeyDown={toggleTopItem}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <ChangeRequestTable
 | 
				
			||||||
 | 
					                environments={filteredEnvs}
 | 
				
			||||||
 | 
					                enableEnvironment={onEnable}
 | 
				
			||||||
 | 
					                disableEnvironment={onDisable}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					        </ConfigButton>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import { Popover, styled } from '@mui/material';
 | 
				
			||||||
 | 
					import { visuallyHiddenStyles } from './shared.styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StyledDropdown = styled('div')(({ theme }) => ({
 | 
				
			||||||
 | 
					    padding: theme.spacing(2),
 | 
				
			||||||
 | 
					    display: 'flex',
 | 
				
			||||||
 | 
					    flexDirection: 'column',
 | 
				
			||||||
 | 
					    gap: theme.spacing(1),
 | 
				
			||||||
 | 
					    maxHeight: '70vh',
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StyledPopover = styled(Popover)(({ theme }) => ({
 | 
				
			||||||
 | 
					    '& .MuiPaper-root': {
 | 
				
			||||||
 | 
					        borderRadius: `${theme.shape.borderRadiusMedium}px`,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const HiddenDescription = styled('p')(() => ({
 | 
				
			||||||
 | 
					    ...visuallyHiddenStyles,
 | 
				
			||||||
 | 
					    position: 'absolute',
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ButtonLabel = styled('span', {
 | 
				
			||||||
 | 
					    shouldForwardProp: (prop) => prop !== 'labelWidth',
 | 
				
			||||||
 | 
					})<{ labelWidth?: string }>(({ labelWidth }) => ({
 | 
				
			||||||
 | 
					    width: labelWidth || 'unset',
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					import { v4 as uuidv4 } from 'uuid';
 | 
				
			||||||
 | 
					import { type FC, type ReactNode, useRef, type PropsWithChildren } from 'react';
 | 
				
			||||||
 | 
					import { Box, Button } from '@mui/material';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    StyledDropdown,
 | 
				
			||||||
 | 
					    StyledPopover,
 | 
				
			||||||
 | 
					    HiddenDescription,
 | 
				
			||||||
 | 
					    ButtonLabel,
 | 
				
			||||||
 | 
					} from './ConfigButton.styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ConfigButtonProps = {
 | 
				
			||||||
 | 
					    button: { label: string; icon: ReactNode; labelWidth?: string };
 | 
				
			||||||
 | 
					    onOpen?: () => void;
 | 
				
			||||||
 | 
					    onClose?: () => void;
 | 
				
			||||||
 | 
					    description: string;
 | 
				
			||||||
 | 
					    preventOpen?: boolean;
 | 
				
			||||||
 | 
					    anchorEl: HTMLDivElement | null | undefined;
 | 
				
			||||||
 | 
					    setAnchorEl: (el: HTMLDivElement | null | undefined) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ConfigButton: FC<PropsWithChildren<ConfigButtonProps>> = ({
 | 
				
			||||||
 | 
					    button,
 | 
				
			||||||
 | 
					    onOpen = () => {},
 | 
				
			||||||
 | 
					    onClose = () => {},
 | 
				
			||||||
 | 
					    description,
 | 
				
			||||||
 | 
					    children,
 | 
				
			||||||
 | 
					    preventOpen,
 | 
				
			||||||
 | 
					    anchorEl,
 | 
				
			||||||
 | 
					    setAnchorEl,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    const ref = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					    const descriptionId = uuidv4();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const open = () => {
 | 
				
			||||||
 | 
					        setAnchorEl(ref.current);
 | 
				
			||||||
 | 
					        onOpen();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleClose = () => {
 | 
				
			||||||
 | 
					        setAnchorEl(null);
 | 
				
			||||||
 | 
					        onClose();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					            <Box ref={ref}>
 | 
				
			||||||
 | 
					                <Button
 | 
				
			||||||
 | 
					                    variant='outlined'
 | 
				
			||||||
 | 
					                    color='primary'
 | 
				
			||||||
 | 
					                    startIcon={button.icon}
 | 
				
			||||||
 | 
					                    onClick={() => {
 | 
				
			||||||
 | 
					                        if (!preventOpen) {
 | 
				
			||||||
 | 
					                            open();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                    <ButtonLabel labelWidth={button.labelWidth}>
 | 
				
			||||||
 | 
					                        {button.label}
 | 
				
			||||||
 | 
					                    </ButtonLabel>
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					            <StyledPopover
 | 
				
			||||||
 | 
					                open={Boolean(anchorEl)}
 | 
				
			||||||
 | 
					                anchorEl={anchorEl}
 | 
				
			||||||
 | 
					                onClose={handleClose}
 | 
				
			||||||
 | 
					                anchorOrigin={{
 | 
				
			||||||
 | 
					                    vertical: 'bottom',
 | 
				
			||||||
 | 
					                    horizontal: 'left',
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					                transformOrigin={{
 | 
				
			||||||
 | 
					                    vertical: 'top',
 | 
				
			||||||
 | 
					                    horizontal: 'left',
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					                <HiddenDescription id={descriptionId}>
 | 
				
			||||||
 | 
					                    {description}
 | 
				
			||||||
 | 
					                </HiddenDescription>
 | 
				
			||||||
 | 
					                <StyledDropdown aria-describedby={descriptionId}>
 | 
				
			||||||
 | 
					                    {children}
 | 
				
			||||||
 | 
					                </StyledDropdown>
 | 
				
			||||||
 | 
					            </StyledPopover>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import { Checkbox, ListItem, styled } from '@mui/material';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StyledListItem = styled(ListItem)(({ theme }) => ({
 | 
				
			||||||
 | 
					    paddingLeft: theme.spacing(1),
 | 
				
			||||||
 | 
					    cursor: 'pointer',
 | 
				
			||||||
 | 
					    '&:hover, &:focus': {
 | 
				
			||||||
 | 
					        backgroundColor: theme.palette.action.hover,
 | 
				
			||||||
 | 
					        outline: 'none',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    minHeight: theme.spacing(4.5),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StyledCheckbox = styled(Checkbox)(({ theme }) => ({
 | 
				
			||||||
 | 
					    padding: theme.spacing(1, 1, 1, 1.5),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
@ -0,0 +1,151 @@
 | 
				
			|||||||
 | 
					import Search from '@mui/icons-material/Search';
 | 
				
			||||||
 | 
					import { type FC, useRef, useState } from 'react';
 | 
				
			||||||
 | 
					import { InputAdornment, List, ListItemText } from '@mui/material';
 | 
				
			||||||
 | 
					import { StyledDropdownSearch } from './shared.styles';
 | 
				
			||||||
 | 
					import { StyledCheckbox, StyledListItem } from './DropdownList.styles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const useSelectionManagement = (
 | 
				
			||||||
 | 
					    handleToggle: (value: string) => () => void,
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					    const listRefs = useRef<Array<HTMLInputElement | HTMLLIElement | null>>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type DropdownListProps = {
 | 
				
			||||||
 | 
					    options: Array<{ label: string; value: string }>;
 | 
				
			||||||
 | 
					    onChange: (value: string) => void;
 | 
				
			||||||
 | 
					    search: {
 | 
				
			||||||
 | 
					        label: string;
 | 
				
			||||||
 | 
					        placeholder: string;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    multiselect?: { selectedOptions: Set<string> };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DropdownList: FC<DropdownListProps> = ({
 | 
				
			||||||
 | 
					    options,
 | 
				
			||||||
 | 
					    onChange,
 | 
				
			||||||
 | 
					    search,
 | 
				
			||||||
 | 
					    multiselect,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    const [searchText, setSearchText] = useState('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onSelection = (selected: string) => {
 | 
				
			||||||
 | 
					        onChange(selected);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { listRefs, handleSelection } = useSelectionManagement(
 | 
				
			||||||
 | 
					        (selected: string) => () => onSelection(selected),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const filteredOptions = options?.filter((option) =>
 | 
				
			||||||
 | 
					        option.label.toLowerCase().includes(searchText.toLowerCase()),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					            <StyledDropdownSearch
 | 
				
			||||||
 | 
					                variant='outlined'
 | 
				
			||||||
 | 
					                size='small'
 | 
				
			||||||
 | 
					                value={searchText}
 | 
				
			||||||
 | 
					                onChange={(event) => setSearchText(event.target.value)}
 | 
				
			||||||
 | 
					                label={search.label}
 | 
				
			||||||
 | 
					                hideLabel
 | 
				
			||||||
 | 
					                placeholder={search.placeholder}
 | 
				
			||||||
 | 
					                autoFocus
 | 
				
			||||||
 | 
					                InputProps={{
 | 
				
			||||||
 | 
					                    startAdornment: (
 | 
				
			||||||
 | 
					                        <InputAdornment position='start'>
 | 
				
			||||||
 | 
					                            <Search fontSize='small' />
 | 
				
			||||||
 | 
					                        </InputAdornment>
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					                inputRef={(el) => {
 | 
				
			||||||
 | 
					                    listRefs.current[0] = el;
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					                onKeyDown={(event) =>
 | 
				
			||||||
 | 
					                    handleSelection(event, 0, filteredOptions)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <List sx={{ overflowY: 'auto' }} disablePadding>
 | 
				
			||||||
 | 
					                {filteredOptions.map((option, index) => {
 | 
				
			||||||
 | 
					                    const labelId = `checkbox-list-label-${option.value}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return (
 | 
				
			||||||
 | 
					                        <StyledListItem
 | 
				
			||||||
 | 
					                            aria-describedby={labelId}
 | 
				
			||||||
 | 
					                            key={option.value}
 | 
				
			||||||
 | 
					                            dense
 | 
				
			||||||
 | 
					                            disablePadding
 | 
				
			||||||
 | 
					                            tabIndex={0}
 | 
				
			||||||
 | 
					                            onClick={() => {
 | 
				
			||||||
 | 
					                                onSelection(option.value);
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                            ref={(el) => {
 | 
				
			||||||
 | 
					                                listRefs.current[index + 1] = el;
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                            onKeyDown={(event) =>
 | 
				
			||||||
 | 
					                                handleSelection(
 | 
				
			||||||
 | 
					                                    event,
 | 
				
			||||||
 | 
					                                    index + 1,
 | 
				
			||||||
 | 
					                                    filteredOptions,
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            {multiselect ? (
 | 
				
			||||||
 | 
					                                <StyledCheckbox
 | 
				
			||||||
 | 
					                                    edge='start'
 | 
				
			||||||
 | 
					                                    checked={multiselect.selectedOptions.has(
 | 
				
			||||||
 | 
					                                        option.value,
 | 
				
			||||||
 | 
					                                    )}
 | 
				
			||||||
 | 
					                                    tabIndex={-1}
 | 
				
			||||||
 | 
					                                    inputProps={{
 | 
				
			||||||
 | 
					                                        'aria-labelledby': labelId,
 | 
				
			||||||
 | 
					                                    }}
 | 
				
			||||||
 | 
					                                    size='small'
 | 
				
			||||||
 | 
					                                    disableRipple
 | 
				
			||||||
 | 
					                                />
 | 
				
			||||||
 | 
					                            ) : null}
 | 
				
			||||||
 | 
					                            <ListItemText id={labelId} primary={option.label} />
 | 
				
			||||||
 | 
					                        </StyledListItem>
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                })}
 | 
				
			||||||
 | 
					            </List>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					import { type FC, useState } from 'react';
 | 
				
			||||||
 | 
					import { ConfigButton, type ConfigButtonProps } from './ConfigButton';
 | 
				
			||||||
 | 
					import { DropdownList, type DropdownListProps } from './DropdownList';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MultiSelectConfigButtonProps = Pick<
 | 
				
			||||||
 | 
					    ConfigButtonProps,
 | 
				
			||||||
 | 
					    'button' | 'onOpen' | 'onClose' | 'description'
 | 
				
			||||||
 | 
					> &
 | 
				
			||||||
 | 
					    Pick<DropdownListProps, 'search' | 'options'> & {
 | 
				
			||||||
 | 
					        selectedOptions: Set<string>;
 | 
				
			||||||
 | 
					        onChange: (values: Set<string>) => void;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MultiSelectConfigButton: FC<MultiSelectConfigButtonProps> = ({
 | 
				
			||||||
 | 
					    selectedOptions,
 | 
				
			||||||
 | 
					    onChange,
 | 
				
			||||||
 | 
					    ...rest
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleToggle = (value: string) => {
 | 
				
			||||||
 | 
					        if (selectedOptions.has(value)) {
 | 
				
			||||||
 | 
					            selectedOptions.delete(value);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            selectedOptions.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        onChange(new Set(selectedOptions));
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <ConfigButton {...rest} anchorEl={anchorEl} setAnchorEl={setAnchorEl}>
 | 
				
			||||||
 | 
					            <DropdownList
 | 
				
			||||||
 | 
					                multiselect={{
 | 
				
			||||||
 | 
					                    selectedOptions,
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					                onChange={handleToggle}
 | 
				
			||||||
 | 
					                {...rest}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					        </ConfigButton>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					import { type FC, useState } from 'react';
 | 
				
			||||||
 | 
					import { ConfigButton, type ConfigButtonProps } from './ConfigButton';
 | 
				
			||||||
 | 
					import { DropdownList, type DropdownListProps } from './DropdownList';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SingleSelectConfigButtonProps = Pick<
 | 
				
			||||||
 | 
					    ConfigButtonProps,
 | 
				
			||||||
 | 
					    'button' | 'onOpen' | 'onClose' | 'description'
 | 
				
			||||||
 | 
					> &
 | 
				
			||||||
 | 
					    Pick<DropdownListProps, 'search' | 'onChange' | 'options'>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SingleSelectConfigButton: FC<SingleSelectConfigButtonProps> = ({
 | 
				
			||||||
 | 
					    onChange,
 | 
				
			||||||
 | 
					    ...props
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>();
 | 
				
			||||||
 | 
					    const [recentlyClosed, setRecentlyClosed] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleChange = (value: any) => {
 | 
				
			||||||
 | 
					        onChange(value);
 | 
				
			||||||
 | 
					        setAnchorEl(null);
 | 
				
			||||||
 | 
					        props.onClose && props.onClose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setRecentlyClosed(true);
 | 
				
			||||||
 | 
					        // this is a hack to prevent the button from being
 | 
				
			||||||
 | 
					        // auto-clicked after you select an item by pressing enter
 | 
				
			||||||
 | 
					        // in the search bar for single-select lists.
 | 
				
			||||||
 | 
					        setTimeout(() => setRecentlyClosed(false), 1);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <ConfigButton
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					            preventOpen={recentlyClosed}
 | 
				
			||||||
 | 
					            anchorEl={anchorEl}
 | 
				
			||||||
 | 
					            setAnchorEl={setAnchorEl}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <DropdownList {...props} onChange={handleChange} />
 | 
				
			||||||
 | 
					        </ConfigButton>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					import { TextField, styled } from '@mui/material';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const visuallyHiddenStyles = {
 | 
				
			||||||
 | 
					    border: 0,
 | 
				
			||||||
 | 
					    clip: 'rect(0 0 0 0)',
 | 
				
			||||||
 | 
					    height: 'auto',
 | 
				
			||||||
 | 
					    margin: 0,
 | 
				
			||||||
 | 
					    overflow: 'hidden',
 | 
				
			||||||
 | 
					    padding: 0,
 | 
				
			||||||
 | 
					    position: 'absolute',
 | 
				
			||||||
 | 
					    width: '1px',
 | 
				
			||||||
 | 
					    whiteSpace: 'nowrap',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StyledDropdownSearch = styled(TextField, {
 | 
				
			||||||
 | 
					    shouldForwardProp: (prop) => prop !== 'hideLabel',
 | 
				
			||||||
 | 
					})<{ hideLabel?: boolean }>(({ theme, hideLabel }) => ({
 | 
				
			||||||
 | 
					    '& .MuiInputBase-root': {
 | 
				
			||||||
 | 
					        padding: theme.spacing(0, 1.5),
 | 
				
			||||||
 | 
					        borderRadius: `${theme.shape.borderRadiusMedium}px`,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    '& .MuiInputBase-input': {
 | 
				
			||||||
 | 
					        padding: theme.spacing(0.75, 0),
 | 
				
			||||||
 | 
					        fontSize: theme.typography.body2.fontSize,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ...(hideLabel
 | 
				
			||||||
 | 
					        ? {
 | 
				
			||||||
 | 
					              label: visuallyHiddenStyles,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              'fieldset > legend > span': visuallyHiddenStyles,
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        : {}),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
@ -2,7 +2,7 @@ import { formatUnknownError } from 'utils/formatUnknownError';
 | 
				
			|||||||
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
 | 
					import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
 | 
				
			||||||
import useToast from 'hooks/useToast';
 | 
					import useToast from 'hooks/useToast';
 | 
				
			||||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
 | 
					import FormTemplate from 'component/common/FormTemplate/FormTemplate';
 | 
				
			||||||
import { NewProjectForm } from '../NewProjectForm';
 | 
					import { NewProjectForm } from './NewProjectForm';
 | 
				
			||||||
import { CreateButton } from 'component/common/CreateButton/CreateButton';
 | 
					import { CreateButton } from 'component/common/CreateButton/CreateButton';
 | 
				
			||||||
import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions';
 | 
					import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions';
 | 
				
			||||||
import useProjectForm, {
 | 
					import useProjectForm, {
 | 
				
			||||||
@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					import { Typography, styled } from '@mui/material';
 | 
				
			||||||
 | 
					import Input from 'component/common/Input/Input';
 | 
				
			||||||
 | 
					import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StyledForm = styled('form')(({ theme }) => ({
 | 
				
			||||||
 | 
					    background: theme.palette.background.default,
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StyledFormSection = styled('div')(({ theme }) => ({
 | 
				
			||||||
 | 
					    '& + *': {
 | 
				
			||||||
 | 
					        borderBlockStart: `1px solid ${theme.palette.divider}`,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    padding: theme.spacing(6),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const TopGrid = styled(StyledFormSection)(({ theme }) => ({
 | 
				
			||||||
 | 
					    display: 'grid',
 | 
				
			||||||
 | 
					    gridTemplateAreas:
 | 
				
			||||||
 | 
					        '"icon header" "icon project-name" "icon project-description"',
 | 
				
			||||||
 | 
					    gridTemplateColumns: 'auto 1fr',
 | 
				
			||||||
 | 
					    gap: theme.spacing(4),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StyledIcon = styled(ProjectIcon)(({ theme }) => ({
 | 
				
			||||||
 | 
					    fill: theme.palette.primary.main,
 | 
				
			||||||
 | 
					    stroke: theme.palette.primary.main,
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StyledHeader = styled(Typography)({
 | 
				
			||||||
 | 
					    gridArea: 'header',
 | 
				
			||||||
 | 
					    alignSelf: 'center',
 | 
				
			||||||
 | 
					    fontWeight: 'lighter',
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ProjectNameContainer = styled('div')({
 | 
				
			||||||
 | 
					    gridArea: 'project-name',
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ProjectDescriptionContainer = styled('div')({
 | 
				
			||||||
 | 
					    gridArea: 'project-description',
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StyledInput = styled(Input)({
 | 
				
			||||||
 | 
					    width: '100%',
 | 
				
			||||||
 | 
					    fieldset: { border: 'none' },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const OptionButtons = styled(StyledFormSection)(({ theme }) => ({
 | 
				
			||||||
 | 
					    display: 'flex',
 | 
				
			||||||
 | 
					    flexFlow: 'row wrap',
 | 
				
			||||||
 | 
					    gap: theme.spacing(2),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const FormActions = styled(StyledFormSection)(({ theme }) => ({
 | 
				
			||||||
 | 
					    display: 'flex',
 | 
				
			||||||
 | 
					    gap: theme.spacing(5),
 | 
				
			||||||
 | 
					    justifyContent: 'flex-end',
 | 
				
			||||||
 | 
					    flexFlow: 'row wrap',
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
@ -1,12 +1,4 @@
 | 
				
			|||||||
import { Typography, styled } from '@mui/material';
 | 
					import type { ProjectMode } from '../../hooks/useProjectEnterpriseSettingsForm';
 | 
				
			||||||
import Input from 'component/common/Input/Input';
 | 
					 | 
				
			||||||
import type { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm';
 | 
					 | 
				
			||||||
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
    MultiSelectList,
 | 
					 | 
				
			||||||
    SingleSelectList,
 | 
					 | 
				
			||||||
    TableSelect,
 | 
					 | 
				
			||||||
} from './SelectionButton';
 | 
					 | 
				
			||||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
 | 
					import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
 | 
				
			||||||
import StickinessIcon from '@mui/icons-material/FormatPaint';
 | 
					import StickinessIcon from '@mui/icons-material/FormatPaint';
 | 
				
			||||||
import ProjectModeIcon from '@mui/icons-material/Adjust';
 | 
					import ProjectModeIcon from '@mui/icons-material/Adjust';
 | 
				
			||||||
@ -17,63 +9,20 @@ import { useStickinessOptions } from 'hooks/useStickinessOptions';
 | 
				
			|||||||
import { ReactComponent as ChangeRequestIcon } from 'assets/icons/merge.svg';
 | 
					import { ReactComponent as ChangeRequestIcon } from 'assets/icons/merge.svg';
 | 
				
			||||||
import type { ReactNode } from 'react';
 | 
					import type { ReactNode } from 'react';
 | 
				
			||||||
import theme from 'themes/theme';
 | 
					import theme from 'themes/theme';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
const StyledForm = styled('form')(({ theme }) => ({
 | 
					    FormActions,
 | 
				
			||||||
    background: theme.palette.background.default,
 | 
					    OptionButtons,
 | 
				
			||||||
}));
 | 
					    ProjectDescriptionContainer,
 | 
				
			||||||
 | 
					    ProjectNameContainer,
 | 
				
			||||||
const StyledFormSection = styled('div')(({ theme }) => ({
 | 
					    StyledForm,
 | 
				
			||||||
    '& + *': {
 | 
					    StyledHeader,
 | 
				
			||||||
        borderBlockStart: `1px solid ${theme.palette.divider}`,
 | 
					    StyledIcon,
 | 
				
			||||||
    },
 | 
					    StyledInput,
 | 
				
			||||||
 | 
					    TopGrid,
 | 
				
			||||||
    padding: theme.spacing(6),
 | 
					} from './NewProjectForm.styles';
 | 
				
			||||||
}));
 | 
					import { MultiSelectConfigButton } from './ConfigButtons/MultiSelectConfigButton';
 | 
				
			||||||
 | 
					import { SingleSelectConfigButton } from './ConfigButtons/SingleSelectConfigButton';
 | 
				
			||||||
const TopGrid = styled(StyledFormSection)(({ theme }) => ({
 | 
					import { ChangeRequestTableConfigButton } from './ConfigButtons/ChangeRequestTableConfigButton';
 | 
				
			||||||
    display: 'grid',
 | 
					 | 
				
			||||||
    gridTemplateAreas:
 | 
					 | 
				
			||||||
        '"icon header" "icon project-name" "icon project-description"',
 | 
					 | 
				
			||||||
    gridTemplateColumns: 'auto 1fr',
 | 
					 | 
				
			||||||
    gap: theme.spacing(4),
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const StyledIcon = styled(ProjectIcon)(({ theme }) => ({
 | 
					 | 
				
			||||||
    fill: theme.palette.primary.main,
 | 
					 | 
				
			||||||
    stroke: theme.palette.primary.main,
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const StyledHeader = styled(Typography)(({ theme }) => ({
 | 
					 | 
				
			||||||
    gridArea: 'header',
 | 
					 | 
				
			||||||
    alignSelf: 'center',
 | 
					 | 
				
			||||||
    fontWeight: 'lighter',
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const ProjectNameContainer = styled('div')(({ theme }) => ({
 | 
					 | 
				
			||||||
    gridArea: 'project-name',
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const ProjectDescriptionContainer = styled('div')(({ theme }) => ({
 | 
					 | 
				
			||||||
    gridArea: 'project-description',
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const StyledInput = styled(Input)(({ theme }) => ({
 | 
					 | 
				
			||||||
    width: '100%',
 | 
					 | 
				
			||||||
    fieldset: { border: 'none' },
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const OptionButtons = styled(StyledFormSection)(({ theme }) => ({
 | 
					 | 
				
			||||||
    display: 'flex',
 | 
					 | 
				
			||||||
    flexFlow: 'row wrap',
 | 
					 | 
				
			||||||
    gap: theme.spacing(2),
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const FormActions = styled(StyledFormSection)(({ theme }) => ({
 | 
					 | 
				
			||||||
    display: 'flex',
 | 
					 | 
				
			||||||
    gap: theme.spacing(5),
 | 
					 | 
				
			||||||
    justifyContent: 'flex-end',
 | 
					 | 
				
			||||||
    flexFlow: 'row wrap',
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type FormProps = {
 | 
					type FormProps = {
 | 
				
			||||||
    projectId: string;
 | 
					    projectId: string;
 | 
				
			||||||
@ -104,6 +53,31 @@ type FormProps = {
 | 
				
			|||||||
const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT';
 | 
					const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT';
 | 
				
			||||||
const PROJECT_DESCRIPTION_INPUT = 'PROJECT_DESCRIPTION_INPUT';
 | 
					const PROJECT_DESCRIPTION_INPUT = 'PROJECT_DESCRIPTION_INPUT';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const projectModeOptions = [
 | 
				
			||||||
 | 
					    { value: 'open', label: 'open' },
 | 
				
			||||||
 | 
					    { value: 'protected', label: 'protected' },
 | 
				
			||||||
 | 
					    { value: 'private', label: 'private' },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const configButtonData = {
 | 
				
			||||||
 | 
					    environments: {
 | 
				
			||||||
 | 
					        icon: <EnvironmentsIcon />,
 | 
				
			||||||
 | 
					        text: `Each feature flag can have a separate configuration per environment. This setting configures which environments your project should start with.`,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    stickiness: {
 | 
				
			||||||
 | 
					        icon: <StickinessIcon />,
 | 
				
			||||||
 | 
					        text: 'Stickiness is used to guarantee that your users see the same result when using a gradual rollout. Default stickiness allows you to choose which field is used by default in this project.',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    mode: {
 | 
				
			||||||
 | 
					        icon: <ProjectModeIcon />,
 | 
				
			||||||
 | 
					        text: 'Mode defines who should be allowed to interact and see your project. Private mode hides the project from anyone except the project owner and members.',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    changeRequests: {
 | 
				
			||||||
 | 
					        icon: <ChangeRequestIcon />,
 | 
				
			||||||
 | 
					        text: 'Change requests can be configured per environment and require changes to go through an approval process before being applied.',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const NewProjectForm: React.FC<FormProps> = ({
 | 
					export const NewProjectForm: React.FC<FormProps> = ({
 | 
				
			||||||
    children,
 | 
					    children,
 | 
				
			||||||
    handleSubmit,
 | 
					    handleSubmit,
 | 
				
			||||||
@ -126,6 +100,7 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
				
			|||||||
    const { isEnterprise } = useUiConfig();
 | 
					    const { isEnterprise } = useUiConfig();
 | 
				
			||||||
    const { environments: allEnvironments } = useEnvironments();
 | 
					    const { environments: allEnvironments } = useEnvironments();
 | 
				
			||||||
    const activeEnvironments = allEnvironments.filter((env) => env.enabled);
 | 
					    const activeEnvironments = allEnvironments.filter((env) => env.enabled);
 | 
				
			||||||
 | 
					    const stickinessOptions = useStickinessOptions(projectStickiness);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleProjectNameUpdate = (
 | 
					    const handleProjectNameUpdate = (
 | 
				
			||||||
        e: React.ChangeEvent<HTMLInputElement>,
 | 
					        e: React.ChangeEvent<HTMLInputElement>,
 | 
				
			||||||
@ -134,33 +109,6 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
				
			|||||||
        setProjectName(input);
 | 
					        setProjectName(input);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const projectModeOptions = [
 | 
					 | 
				
			||||||
        { value: 'open', label: 'open' },
 | 
					 | 
				
			||||||
        { value: 'protected', label: 'protected' },
 | 
					 | 
				
			||||||
        { value: 'private', label: 'private' },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const stickinessOptions = useStickinessOptions(projectStickiness);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const selectionButtonData = {
 | 
					 | 
				
			||||||
        environments: {
 | 
					 | 
				
			||||||
            icon: <EnvironmentsIcon />,
 | 
					 | 
				
			||||||
            text: `Each feature flag can have a separate configuration per environment. This setting configures which environments your project should start with.`,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        stickiness: {
 | 
					 | 
				
			||||||
            icon: <StickinessIcon />,
 | 
					 | 
				
			||||||
            text: 'Stickiness is used to guarantee that your users see the same result when using a gradual rollout. Default stickiness allows you to choose which field is used by default in this project.',
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        mode: {
 | 
					 | 
				
			||||||
            icon: <ProjectModeIcon />,
 | 
					 | 
				
			||||||
            text: 'Mode defines who should be allowed to interact and see your project. Private mode hides the project from anyone except the project owner and members.',
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        changeRequests: {
 | 
					 | 
				
			||||||
            icon: <ChangeRequestIcon />,
 | 
					 | 
				
			||||||
            text: 'Change requests can be configured per environment and require changes to go through an approval process before being applied.',
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const numberOfConfiguredChangeRequestEnvironments = Object.keys(
 | 
					    const numberOfConfiguredChangeRequestEnvironments = Object.keys(
 | 
				
			||||||
        projectChangeRequestConfiguration,
 | 
					        projectChangeRequestConfiguration,
 | 
				
			||||||
    ).length;
 | 
					    ).length;
 | 
				
			||||||
@ -231,8 +179,8 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
				
			|||||||
            </TopGrid>
 | 
					            </TopGrid>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <OptionButtons>
 | 
					            <OptionButtons>
 | 
				
			||||||
                <MultiSelectList
 | 
					                <MultiSelectConfigButton
 | 
				
			||||||
                    description={selectionButtonData.environments.text}
 | 
					                    description={configButtonData.environments.text}
 | 
				
			||||||
                    selectedOptions={projectEnvironments}
 | 
					                    selectedOptions={projectEnvironments}
 | 
				
			||||||
                    options={activeEnvironments.map((env) => ({
 | 
					                    options={activeEnvironments.map((env) => ({
 | 
				
			||||||
                        label: env.name,
 | 
					                        label: env.name,
 | 
				
			||||||
@ -252,13 +200,13 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
				
			|||||||
                        placeholder: 'Select project environments',
 | 
					                        placeholder: 'Select project environments',
 | 
				
			||||||
                    }}
 | 
					                    }}
 | 
				
			||||||
                    onOpen={() =>
 | 
					                    onOpen={() =>
 | 
				
			||||||
                        overrideDocumentation(selectionButtonData.environments)
 | 
					                        overrideDocumentation(configButtonData.environments)
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    onClose={clearDocumentationOverride}
 | 
					                    onClose={clearDocumentationOverride}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <SingleSelectList
 | 
					                <SingleSelectConfigButton
 | 
				
			||||||
                    description={selectionButtonData.stickiness.text}
 | 
					                    description={configButtonData.stickiness.text}
 | 
				
			||||||
                    options={stickinessOptions.map(({ key, ...rest }) => ({
 | 
					                    options={stickinessOptions.map(({ key, ...rest }) => ({
 | 
				
			||||||
                        value: key,
 | 
					                        value: key,
 | 
				
			||||||
                        ...rest,
 | 
					                        ...rest,
 | 
				
			||||||
@ -275,7 +223,7 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
				
			|||||||
                        placeholder: 'Select default stickiness',
 | 
					                        placeholder: 'Select default stickiness',
 | 
				
			||||||
                    }}
 | 
					                    }}
 | 
				
			||||||
                    onOpen={() =>
 | 
					                    onOpen={() =>
 | 
				
			||||||
                        overrideDocumentation(selectionButtonData.stickiness)
 | 
					                        overrideDocumentation(configButtonData.stickiness)
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    onClose={clearDocumentationOverride}
 | 
					                    onClose={clearDocumentationOverride}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
@ -283,8 +231,8 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
				
			|||||||
                <ConditionallyRender
 | 
					                <ConditionallyRender
 | 
				
			||||||
                    condition={isEnterprise()}
 | 
					                    condition={isEnterprise()}
 | 
				
			||||||
                    show={
 | 
					                    show={
 | 
				
			||||||
                        <SingleSelectList
 | 
					                        <SingleSelectConfigButton
 | 
				
			||||||
                            description={selectionButtonData.mode.text}
 | 
					                            description={configButtonData.mode.text}
 | 
				
			||||||
                            options={projectModeOptions}
 | 
					                            options={projectModeOptions}
 | 
				
			||||||
                            onChange={(value: any) => {
 | 
					                            onChange={(value: any) => {
 | 
				
			||||||
                                setProjectMode(value);
 | 
					                                setProjectMode(value);
 | 
				
			||||||
@ -299,7 +247,7 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
				
			|||||||
                                placeholder: 'Select project mode',
 | 
					                                placeholder: 'Select project mode',
 | 
				
			||||||
                            }}
 | 
					                            }}
 | 
				
			||||||
                            onOpen={() =>
 | 
					                            onOpen={() =>
 | 
				
			||||||
                                overrideDocumentation(selectionButtonData.mode)
 | 
					                                overrideDocumentation(configButtonData.mode)
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            onClose={clearDocumentationOverride}
 | 
					                            onClose={clearDocumentationOverride}
 | 
				
			||||||
                        />
 | 
					                        />
 | 
				
			||||||
@ -308,10 +256,8 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
				
			|||||||
                <ConditionallyRender
 | 
					                <ConditionallyRender
 | 
				
			||||||
                    condition={isEnterprise()}
 | 
					                    condition={isEnterprise()}
 | 
				
			||||||
                    show={
 | 
					                    show={
 | 
				
			||||||
                        <TableSelect
 | 
					                        <ChangeRequestTableConfigButton
 | 
				
			||||||
                            description={
 | 
					                            description={configButtonData.changeRequests.text}
 | 
				
			||||||
                                selectionButtonData.changeRequests.text
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            activeEnvironments={
 | 
					                            activeEnvironments={
 | 
				
			||||||
                                availableChangeRequestEnvironments
 | 
					                                availableChangeRequestEnvironments
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
@ -334,7 +280,7 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
				
			|||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            onOpen={() =>
 | 
					                            onOpen={() =>
 | 
				
			||||||
                                overrideDocumentation(
 | 
					                                overrideDocumentation(
 | 
				
			||||||
                                    selectionButtonData.changeRequests,
 | 
					                                    configButtonData.changeRequests,
 | 
				
			||||||
                                )
 | 
					                                )
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            onClose={clearDocumentationOverride}
 | 
					                            onClose={clearDocumentationOverride}
 | 
				
			||||||
@ -1,82 +0,0 @@
 | 
				
			|||||||
import { Checkbox, ListItem, Popover, TextField, styled } from '@mui/material';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const StyledDropdown = styled('div')(({ theme }) => ({
 | 
					 | 
				
			||||||
    padding: theme.spacing(2),
 | 
					 | 
				
			||||||
    display: 'flex',
 | 
					 | 
				
			||||||
    flexDirection: 'column',
 | 
					 | 
				
			||||||
    gap: theme.spacing(1),
 | 
					 | 
				
			||||||
    maxHeight: '70vh',
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const StyledListItem = styled(ListItem)(({ theme }) => ({
 | 
					 | 
				
			||||||
    paddingLeft: theme.spacing(1),
 | 
					 | 
				
			||||||
    cursor: 'pointer',
 | 
					 | 
				
			||||||
    '&:hover, &:focus': {
 | 
					 | 
				
			||||||
        backgroundColor: theme.palette.action.hover,
 | 
					 | 
				
			||||||
        outline: 'none',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    minHeight: theme.spacing(4.5),
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const StyledCheckbox = styled(Checkbox)(({ theme }) => ({
 | 
					 | 
				
			||||||
    padding: theme.spacing(1, 1, 1, 1.5),
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const StyledPopover = styled(Popover)(({ theme }) => ({
 | 
					 | 
				
			||||||
    '& .MuiPaper-root': {
 | 
					 | 
				
			||||||
        borderRadius: `${theme.shape.borderRadiusMedium}px`,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const visuallyHiddenStyles = {
 | 
					 | 
				
			||||||
    border: 0,
 | 
					 | 
				
			||||||
    clip: 'rect(0 0 0 0)',
 | 
					 | 
				
			||||||
    height: 'auto',
 | 
					 | 
				
			||||||
    margin: 0,
 | 
					 | 
				
			||||||
    overflow: 'hidden',
 | 
					 | 
				
			||||||
    padding: 0,
 | 
					 | 
				
			||||||
    position: 'absolute',
 | 
					 | 
				
			||||||
    width: '1px',
 | 
					 | 
				
			||||||
    whiteSpace: 'nowrap',
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const HiddenDescription = styled('p')(() => ({
 | 
					 | 
				
			||||||
    ...visuallyHiddenStyles,
 | 
					 | 
				
			||||||
    position: 'absolute',
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const StyledDropdownSearch = styled(TextField, {
 | 
					 | 
				
			||||||
    shouldForwardProp: (prop) => prop !== 'hideLabel',
 | 
					 | 
				
			||||||
})<{ hideLabel?: boolean }>(({ theme, hideLabel }) => ({
 | 
					 | 
				
			||||||
    '& .MuiInputBase-root': {
 | 
					 | 
				
			||||||
        padding: theme.spacing(0, 1.5),
 | 
					 | 
				
			||||||
        borderRadius: `${theme.shape.borderRadiusMedium}px`,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    '& .MuiInputBase-input': {
 | 
					 | 
				
			||||||
        padding: theme.spacing(0.75, 0),
 | 
					 | 
				
			||||||
        fontSize: theme.typography.body2.fontSize,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ...(hideLabel
 | 
					 | 
				
			||||||
        ? {
 | 
					 | 
				
			||||||
              label: visuallyHiddenStyles,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              'fieldset > legend > span': visuallyHiddenStyles,
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        : {}),
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const TableSearchInput = styled(StyledDropdownSearch)(({ theme }) => ({
 | 
					 | 
				
			||||||
    maxWidth: '30ch',
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const ScrollContainer = styled('div')(({ theme }) => ({
 | 
					 | 
				
			||||||
    width: '100%',
 | 
					 | 
				
			||||||
    overflow: 'auto',
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const ButtonLabel = styled('span', {
 | 
					 | 
				
			||||||
    shouldForwardProp: (prop) => prop !== 'labelWidth',
 | 
					 | 
				
			||||||
})<{ labelWidth?: string }>(({ labelWidth }) => ({
 | 
					 | 
				
			||||||
    width: labelWidth || 'unset',
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
@ -1,459 +0,0 @@
 | 
				
			|||||||
import Search from '@mui/icons-material/Search';
 | 
					 | 
				
			||||||
import { v4 as uuidv4 } from 'uuid';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
    type FC,
 | 
					 | 
				
			||||||
    type ReactNode,
 | 
					 | 
				
			||||||
    useRef,
 | 
					 | 
				
			||||||
    useState,
 | 
					 | 
				
			||||||
    useMemo,
 | 
					 | 
				
			||||||
    type PropsWithChildren,
 | 
					 | 
				
			||||||
} from 'react';
 | 
					 | 
				
			||||||
import { Box, Button, InputAdornment, List, ListItemText } from '@mui/material';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
    StyledCheckbox,
 | 
					 | 
				
			||||||
    StyledDropdown,
 | 
					 | 
				
			||||||
    StyledListItem,
 | 
					 | 
				
			||||||
    StyledPopover,
 | 
					 | 
				
			||||||
    StyledDropdownSearch,
 | 
					 | 
				
			||||||
    TableSearchInput,
 | 
					 | 
				
			||||||
    HiddenDescription,
 | 
					 | 
				
			||||||
    ButtonLabel,
 | 
					 | 
				
			||||||
} from './SelectionButton.styles';
 | 
					 | 
				
			||||||
import { ChangeRequestTable } from './ChangeRequestTable';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface IFilterItemProps {
 | 
					 | 
				
			||||||
    label: ReactNode;
 | 
					 | 
				
			||||||
    options: Array<{ label: string; value: string }>;
 | 
					 | 
				
			||||||
    selectedOptions: Set<string>;
 | 
					 | 
				
			||||||
    onChange: (values: Set<string>) => void;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type FilterItemParams = {
 | 
					 | 
				
			||||||
    operator: string;
 | 
					 | 
				
			||||||
    values: string[];
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface UseSelectionManagementProps {
 | 
					 | 
				
			||||||
    handleToggle: (value: string) => () => void;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const useSelectionManagement = ({
 | 
					 | 
				
			||||||
    handleToggle,
 | 
					 | 
				
			||||||
}: UseSelectionManagementProps) => {
 | 
					 | 
				
			||||||
    const listRefs = useRef<Array<HTMLInputElement | HTMLLIElement | null>>([]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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; labelWidth?: string };
 | 
					 | 
				
			||||||
    search: {
 | 
					 | 
				
			||||||
        label: string;
 | 
					 | 
				
			||||||
        placeholder: string;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    multiselect?: { selectedOptions: Set<string> };
 | 
					 | 
				
			||||||
    onOpen?: () => void;
 | 
					 | 
				
			||||||
    onClose?: () => void;
 | 
					 | 
				
			||||||
    description: string; // visually hidden, for assistive tech
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const CombinedSelect: FC<
 | 
					 | 
				
			||||||
    PropsWithChildren<{
 | 
					 | 
				
			||||||
        button: { label: string; icon: ReactNode; labelWidth?: string };
 | 
					 | 
				
			||||||
        onOpen?: () => void;
 | 
					 | 
				
			||||||
        onClose?: () => void;
 | 
					 | 
				
			||||||
        description: string;
 | 
					 | 
				
			||||||
        preventOpen?: boolean;
 | 
					 | 
				
			||||||
        anchorEl: HTMLDivElement | null | undefined;
 | 
					 | 
				
			||||||
        setAnchorEl: (el: HTMLDivElement | null | undefined) => void;
 | 
					 | 
				
			||||||
    }>
 | 
					 | 
				
			||||||
> = ({
 | 
					 | 
				
			||||||
    button,
 | 
					 | 
				
			||||||
    onOpen = () => {},
 | 
					 | 
				
			||||||
    onClose = () => {},
 | 
					 | 
				
			||||||
    description,
 | 
					 | 
				
			||||||
    children,
 | 
					 | 
				
			||||||
    preventOpen,
 | 
					 | 
				
			||||||
    anchorEl,
 | 
					 | 
				
			||||||
    setAnchorEl,
 | 
					 | 
				
			||||||
}) => {
 | 
					 | 
				
			||||||
    const ref = useRef<HTMLDivElement>(null);
 | 
					 | 
				
			||||||
    const descriptionId = uuidv4();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const open = () => {
 | 
					 | 
				
			||||||
        setAnchorEl(ref.current);
 | 
					 | 
				
			||||||
        onOpen();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleClose = () => {
 | 
					 | 
				
			||||||
        setAnchorEl(null);
 | 
					 | 
				
			||||||
        onClose();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
        <>
 | 
					 | 
				
			||||||
            <Box ref={ref}>
 | 
					 | 
				
			||||||
                <Button
 | 
					 | 
				
			||||||
                    variant='outlined'
 | 
					 | 
				
			||||||
                    color='primary'
 | 
					 | 
				
			||||||
                    startIcon={button.icon}
 | 
					 | 
				
			||||||
                    onClick={() => {
 | 
					 | 
				
			||||||
                        if (!preventOpen) {
 | 
					 | 
				
			||||||
                            open();
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                    <ButtonLabel labelWidth={button.labelWidth}>
 | 
					 | 
				
			||||||
                        {button.label}
 | 
					 | 
				
			||||||
                    </ButtonLabel>
 | 
					 | 
				
			||||||
                </Button>
 | 
					 | 
				
			||||||
            </Box>
 | 
					 | 
				
			||||||
            <StyledPopover
 | 
					 | 
				
			||||||
                open={Boolean(anchorEl)}
 | 
					 | 
				
			||||||
                anchorEl={anchorEl}
 | 
					 | 
				
			||||||
                onClose={handleClose}
 | 
					 | 
				
			||||||
                anchorOrigin={{
 | 
					 | 
				
			||||||
                    vertical: 'bottom',
 | 
					 | 
				
			||||||
                    horizontal: 'left',
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                transformOrigin={{
 | 
					 | 
				
			||||||
                    vertical: 'top',
 | 
					 | 
				
			||||||
                    horizontal: 'left',
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
                <HiddenDescription id={descriptionId}>
 | 
					 | 
				
			||||||
                    {description}
 | 
					 | 
				
			||||||
                </HiddenDescription>
 | 
					 | 
				
			||||||
                <StyledDropdown aria-describedby={descriptionId}>
 | 
					 | 
				
			||||||
                    {children}
 | 
					 | 
				
			||||||
                </StyledDropdown>
 | 
					 | 
				
			||||||
            </StyledPopover>
 | 
					 | 
				
			||||||
        </>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const DropdownList: FC<CombinedSelectProps> = ({
 | 
					 | 
				
			||||||
    options,
 | 
					 | 
				
			||||||
    onChange,
 | 
					 | 
				
			||||||
    search,
 | 
					 | 
				
			||||||
    multiselect,
 | 
					 | 
				
			||||||
}) => {
 | 
					 | 
				
			||||||
    const [searchText, setSearchText] = useState('');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const onSelection = (selected: string) => {
 | 
					 | 
				
			||||||
        onChange(selected);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { listRefs, handleSelection } = useSelectionManagement({
 | 
					 | 
				
			||||||
        handleToggle: (selected: string) => () => onSelection(selected),
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const filteredOptions = options?.filter((option) =>
 | 
					 | 
				
			||||||
        option.label.toLowerCase().includes(searchText.toLowerCase()),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
        <>
 | 
					 | 
				
			||||||
            <StyledDropdownSearch
 | 
					 | 
				
			||||||
                variant='outlined'
 | 
					 | 
				
			||||||
                size='small'
 | 
					 | 
				
			||||||
                value={searchText}
 | 
					 | 
				
			||||||
                onChange={(event) => setSearchText(event.target.value)}
 | 
					 | 
				
			||||||
                label={search.label}
 | 
					 | 
				
			||||||
                hideLabel
 | 
					 | 
				
			||||||
                placeholder={search.placeholder}
 | 
					 | 
				
			||||||
                autoFocus
 | 
					 | 
				
			||||||
                InputProps={{
 | 
					 | 
				
			||||||
                    startAdornment: (
 | 
					 | 
				
			||||||
                        <InputAdornment position='start'>
 | 
					 | 
				
			||||||
                            <Search fontSize='small' />
 | 
					 | 
				
			||||||
                        </InputAdornment>
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                inputRef={(el) => {
 | 
					 | 
				
			||||||
                    listRefs.current[0] = el;
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                onKeyDown={(event) =>
 | 
					 | 
				
			||||||
                    handleSelection(event, 0, filteredOptions)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <List sx={{ overflowY: 'auto' }} disablePadding>
 | 
					 | 
				
			||||||
                {filteredOptions.map((option, index) => {
 | 
					 | 
				
			||||||
                    const labelId = `checkbox-list-label-${option.value}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    return (
 | 
					 | 
				
			||||||
                        <StyledListItem
 | 
					 | 
				
			||||||
                            aria-describedby={labelId}
 | 
					 | 
				
			||||||
                            key={option.value}
 | 
					 | 
				
			||||||
                            dense
 | 
					 | 
				
			||||||
                            disablePadding
 | 
					 | 
				
			||||||
                            tabIndex={0}
 | 
					 | 
				
			||||||
                            onClick={() => {
 | 
					 | 
				
			||||||
                                onSelection(option.value);
 | 
					 | 
				
			||||||
                            }}
 | 
					 | 
				
			||||||
                            ref={(el) => {
 | 
					 | 
				
			||||||
                                listRefs.current[index + 1] = el;
 | 
					 | 
				
			||||||
                            }}
 | 
					 | 
				
			||||||
                            onKeyDown={(event) =>
 | 
					 | 
				
			||||||
                                handleSelection(
 | 
					 | 
				
			||||||
                                    event,
 | 
					 | 
				
			||||||
                                    index + 1,
 | 
					 | 
				
			||||||
                                    filteredOptions,
 | 
					 | 
				
			||||||
                                )
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        >
 | 
					 | 
				
			||||||
                            {multiselect ? (
 | 
					 | 
				
			||||||
                                <StyledCheckbox
 | 
					 | 
				
			||||||
                                    edge='start'
 | 
					 | 
				
			||||||
                                    checked={multiselect.selectedOptions.has(
 | 
					 | 
				
			||||||
                                        option.value,
 | 
					 | 
				
			||||||
                                    )}
 | 
					 | 
				
			||||||
                                    tabIndex={-1}
 | 
					 | 
				
			||||||
                                    inputProps={{
 | 
					 | 
				
			||||||
                                        'aria-labelledby': labelId,
 | 
					 | 
				
			||||||
                                    }}
 | 
					 | 
				
			||||||
                                    size='small'
 | 
					 | 
				
			||||||
                                    disableRipple
 | 
					 | 
				
			||||||
                                />
 | 
					 | 
				
			||||||
                            ) : null}
 | 
					 | 
				
			||||||
                            <ListItemText id={labelId} primary={option.label} />
 | 
					 | 
				
			||||||
                        </StyledListItem>
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                })}
 | 
					 | 
				
			||||||
            </List>
 | 
					 | 
				
			||||||
        </>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type SingleSelectListProps = Pick<
 | 
					 | 
				
			||||||
    CombinedSelectProps,
 | 
					 | 
				
			||||||
    | 'options'
 | 
					 | 
				
			||||||
    | 'button'
 | 
					 | 
				
			||||||
    | 'search'
 | 
					 | 
				
			||||||
    | 'onChange'
 | 
					 | 
				
			||||||
    | 'onOpen'
 | 
					 | 
				
			||||||
    | 'onClose'
 | 
					 | 
				
			||||||
    | 'description'
 | 
					 | 
				
			||||||
>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const SingleSelectList: FC<SingleSelectListProps> = ({
 | 
					 | 
				
			||||||
    onChange,
 | 
					 | 
				
			||||||
    ...props
 | 
					 | 
				
			||||||
}) => {
 | 
					 | 
				
			||||||
    const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>();
 | 
					 | 
				
			||||||
    const [recentlyClosed, setRecentlyClosed] = useState(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleChange = (value: any) => {
 | 
					 | 
				
			||||||
        onChange(value);
 | 
					 | 
				
			||||||
        setAnchorEl(null);
 | 
					 | 
				
			||||||
        props.onClose && props.onClose();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        setRecentlyClosed(true);
 | 
					 | 
				
			||||||
        // this is a hack to prevent the button from being
 | 
					 | 
				
			||||||
        // auto-clicked after you select an item by pressing enter
 | 
					 | 
				
			||||||
        // in the search bar for single-select lists.
 | 
					 | 
				
			||||||
        setTimeout(() => setRecentlyClosed(false), 1);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
        <CombinedSelect
 | 
					 | 
				
			||||||
            {...props}
 | 
					 | 
				
			||||||
            preventOpen={recentlyClosed}
 | 
					 | 
				
			||||||
            anchorEl={anchorEl}
 | 
					 | 
				
			||||||
            setAnchorEl={setAnchorEl}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
            <DropdownList {...props} onChange={handleChange} />
 | 
					 | 
				
			||||||
        </CombinedSelect>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type MultiselectListProps = Pick<
 | 
					 | 
				
			||||||
    CombinedSelectProps,
 | 
					 | 
				
			||||||
    'options' | 'button' | 'search' | 'onOpen' | 'onClose' | 'description'
 | 
					 | 
				
			||||||
> & {
 | 
					 | 
				
			||||||
    selectedOptions: Set<string>;
 | 
					 | 
				
			||||||
    onChange: (values: Set<string>) => void;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const MultiSelectList: FC<MultiselectListProps> = ({
 | 
					 | 
				
			||||||
    selectedOptions,
 | 
					 | 
				
			||||||
    onChange,
 | 
					 | 
				
			||||||
    ...rest
 | 
					 | 
				
			||||||
}) => {
 | 
					 | 
				
			||||||
    const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleToggle = (value: string) => {
 | 
					 | 
				
			||||||
        if (selectedOptions.has(value)) {
 | 
					 | 
				
			||||||
            selectedOptions.delete(value);
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            selectedOptions.add(value);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        onChange(new Set(selectedOptions));
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
        <CombinedSelect {...rest} anchorEl={anchorEl} setAnchorEl={setAnchorEl}>
 | 
					 | 
				
			||||||
            <DropdownList
 | 
					 | 
				
			||||||
                multiselect={{
 | 
					 | 
				
			||||||
                    selectedOptions,
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                onChange={handleToggle}
 | 
					 | 
				
			||||||
                {...rest}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
        </CombinedSelect>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type TableSelectProps = Pick<
 | 
					 | 
				
			||||||
    CombinedSelectProps,
 | 
					 | 
				
			||||||
    'button' | 'search' | 'onOpen' | 'onClose' | 'description'
 | 
					 | 
				
			||||||
> & {
 | 
					 | 
				
			||||||
    updateProjectChangeRequestConfiguration: {
 | 
					 | 
				
			||||||
        disableChangeRequests: (env: string) => void;
 | 
					 | 
				
			||||||
        enableChangeRequests: (env: string, requiredApprovals: number) => void;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    activeEnvironments: {
 | 
					 | 
				
			||||||
        name: string;
 | 
					 | 
				
			||||||
        type: string;
 | 
					 | 
				
			||||||
    }[];
 | 
					 | 
				
			||||||
    projectChangeRequestConfiguration: Record<
 | 
					 | 
				
			||||||
        string,
 | 
					 | 
				
			||||||
        { requiredApprovals: number }
 | 
					 | 
				
			||||||
    >;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const TableSelect: FC<TableSelectProps> = ({
 | 
					 | 
				
			||||||
    button,
 | 
					 | 
				
			||||||
    search,
 | 
					 | 
				
			||||||
    projectChangeRequestConfiguration,
 | 
					 | 
				
			||||||
    updateProjectChangeRequestConfiguration,
 | 
					 | 
				
			||||||
    activeEnvironments,
 | 
					 | 
				
			||||||
    onOpen = () => {},
 | 
					 | 
				
			||||||
    onClose = () => {},
 | 
					 | 
				
			||||||
    ...props
 | 
					 | 
				
			||||||
}) => {
 | 
					 | 
				
			||||||
    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 [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>();
 | 
					 | 
				
			||||||
    const [searchText, setSearchText] = useState('');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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 (
 | 
					 | 
				
			||||||
        <CombinedSelect
 | 
					 | 
				
			||||||
            button={button}
 | 
					 | 
				
			||||||
            {...props}
 | 
					 | 
				
			||||||
            anchorEl={anchorEl}
 | 
					 | 
				
			||||||
            setAnchorEl={setAnchorEl}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
            <TableSearchInput
 | 
					 | 
				
			||||||
                variant='outlined'
 | 
					 | 
				
			||||||
                size='small'
 | 
					 | 
				
			||||||
                value={searchText}
 | 
					 | 
				
			||||||
                onChange={(event) => setSearchText(event.target.value)}
 | 
					 | 
				
			||||||
                hideLabel
 | 
					 | 
				
			||||||
                label={search.label}
 | 
					 | 
				
			||||||
                placeholder={search.placeholder}
 | 
					 | 
				
			||||||
                autoFocus
 | 
					 | 
				
			||||||
                InputProps={{
 | 
					 | 
				
			||||||
                    startAdornment: (
 | 
					 | 
				
			||||||
                        <InputAdornment position='start'>
 | 
					 | 
				
			||||||
                            <Search fontSize='small' />
 | 
					 | 
				
			||||||
                        </InputAdornment>
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                onKeyDown={toggleTopItem}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <ChangeRequestTable
 | 
					 | 
				
			||||||
                environments={filteredEnvs}
 | 
					 | 
				
			||||||
                enableEnvironment={onEnable}
 | 
					 | 
				
			||||||
                disableEnvironment={onDisable}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
        </CombinedSelect>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@ -24,7 +24,7 @@ import { useUiFlag } from 'hooks/useUiFlag';
 | 
				
			|||||||
import { useProfile } from 'hooks/api/getters/useProfile/useProfile';
 | 
					import { useProfile } from 'hooks/api/getters/useProfile/useProfile';
 | 
				
			||||||
import { groupProjects } from './group-projects';
 | 
					import { groupProjects } from './group-projects';
 | 
				
			||||||
import { ProjectGroup } from './ProjectGroup';
 | 
					import { ProjectGroup } from './ProjectGroup';
 | 
				
			||||||
import { CreateProjectDialog } from '../Project/CreateProject/CreateProjectDialog/CreateProjectDialog';
 | 
					import { CreateProjectDialog } from '../Project/CreateProject/NewCreateProjectForm/CreateProjectDialog';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledApiError = styled(ApiError)(({ theme }) => ({
 | 
					const StyledApiError = styled(ApiError)(({ theme }) => ({
 | 
				
			||||||
    maxWidth: '500px',
 | 
					    maxWidth: '500px',
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user