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 useToast from 'hooks/useToast';
 | 
			
		||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
 | 
			
		||||
import { NewProjectForm } from '../NewProjectForm';
 | 
			
		||||
import { NewProjectForm } from './NewProjectForm';
 | 
			
		||||
import { CreateButton } from 'component/common/CreateButton/CreateButton';
 | 
			
		||||
import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
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 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 type { ProjectMode } from '../../hooks/useProjectEnterpriseSettingsForm';
 | 
			
		||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
 | 
			
		||||
import StickinessIcon from '@mui/icons-material/FormatPaint';
 | 
			
		||||
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 type { ReactNode } from 'react';
 | 
			
		||||
import theme from 'themes/theme';
 | 
			
		||||
 | 
			
		||||
const StyledForm = styled('form')(({ theme }) => ({
 | 
			
		||||
    background: theme.palette.background.default,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledFormSection = styled('div')(({ theme }) => ({
 | 
			
		||||
    '& + *': {
 | 
			
		||||
        borderBlockStart: `1px solid ${theme.palette.divider}`,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    padding: theme.spacing(6),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const TopGrid = styled(StyledFormSection)(({ theme }) => ({
 | 
			
		||||
    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',
 | 
			
		||||
}));
 | 
			
		||||
import {
 | 
			
		||||
    FormActions,
 | 
			
		||||
    OptionButtons,
 | 
			
		||||
    ProjectDescriptionContainer,
 | 
			
		||||
    ProjectNameContainer,
 | 
			
		||||
    StyledForm,
 | 
			
		||||
    StyledHeader,
 | 
			
		||||
    StyledIcon,
 | 
			
		||||
    StyledInput,
 | 
			
		||||
    TopGrid,
 | 
			
		||||
} from './NewProjectForm.styles';
 | 
			
		||||
import { MultiSelectConfigButton } from './ConfigButtons/MultiSelectConfigButton';
 | 
			
		||||
import { SingleSelectConfigButton } from './ConfigButtons/SingleSelectConfigButton';
 | 
			
		||||
import { ChangeRequestTableConfigButton } from './ConfigButtons/ChangeRequestTableConfigButton';
 | 
			
		||||
 | 
			
		||||
type FormProps = {
 | 
			
		||||
    projectId: string;
 | 
			
		||||
@ -104,6 +53,31 @@ type FormProps = {
 | 
			
		||||
const PROJECT_NAME_INPUT = 'PROJECT_NAME_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> = ({
 | 
			
		||||
    children,
 | 
			
		||||
    handleSubmit,
 | 
			
		||||
@ -126,6 +100,7 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
    const { isEnterprise } = useUiConfig();
 | 
			
		||||
    const { environments: allEnvironments } = useEnvironments();
 | 
			
		||||
    const activeEnvironments = allEnvironments.filter((env) => env.enabled);
 | 
			
		||||
    const stickinessOptions = useStickinessOptions(projectStickiness);
 | 
			
		||||
 | 
			
		||||
    const handleProjectNameUpdate = (
 | 
			
		||||
        e: React.ChangeEvent<HTMLInputElement>,
 | 
			
		||||
@ -134,33 +109,6 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
        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(
 | 
			
		||||
        projectChangeRequestConfiguration,
 | 
			
		||||
    ).length;
 | 
			
		||||
@ -231,8 +179,8 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
            </TopGrid>
 | 
			
		||||
 | 
			
		||||
            <OptionButtons>
 | 
			
		||||
                <MultiSelectList
 | 
			
		||||
                    description={selectionButtonData.environments.text}
 | 
			
		||||
                <MultiSelectConfigButton
 | 
			
		||||
                    description={configButtonData.environments.text}
 | 
			
		||||
                    selectedOptions={projectEnvironments}
 | 
			
		||||
                    options={activeEnvironments.map((env) => ({
 | 
			
		||||
                        label: env.name,
 | 
			
		||||
@ -252,13 +200,13 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
                        placeholder: 'Select project environments',
 | 
			
		||||
                    }}
 | 
			
		||||
                    onOpen={() =>
 | 
			
		||||
                        overrideDocumentation(selectionButtonData.environments)
 | 
			
		||||
                        overrideDocumentation(configButtonData.environments)
 | 
			
		||||
                    }
 | 
			
		||||
                    onClose={clearDocumentationOverride}
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <SingleSelectList
 | 
			
		||||
                    description={selectionButtonData.stickiness.text}
 | 
			
		||||
                <SingleSelectConfigButton
 | 
			
		||||
                    description={configButtonData.stickiness.text}
 | 
			
		||||
                    options={stickinessOptions.map(({ key, ...rest }) => ({
 | 
			
		||||
                        value: key,
 | 
			
		||||
                        ...rest,
 | 
			
		||||
@ -275,7 +223,7 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
                        placeholder: 'Select default stickiness',
 | 
			
		||||
                    }}
 | 
			
		||||
                    onOpen={() =>
 | 
			
		||||
                        overrideDocumentation(selectionButtonData.stickiness)
 | 
			
		||||
                        overrideDocumentation(configButtonData.stickiness)
 | 
			
		||||
                    }
 | 
			
		||||
                    onClose={clearDocumentationOverride}
 | 
			
		||||
                />
 | 
			
		||||
@ -283,8 +231,8 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={isEnterprise()}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <SingleSelectList
 | 
			
		||||
                            description={selectionButtonData.mode.text}
 | 
			
		||||
                        <SingleSelectConfigButton
 | 
			
		||||
                            description={configButtonData.mode.text}
 | 
			
		||||
                            options={projectModeOptions}
 | 
			
		||||
                            onChange={(value: any) => {
 | 
			
		||||
                                setProjectMode(value);
 | 
			
		||||
@ -299,7 +247,7 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
                                placeholder: 'Select project mode',
 | 
			
		||||
                            }}
 | 
			
		||||
                            onOpen={() =>
 | 
			
		||||
                                overrideDocumentation(selectionButtonData.mode)
 | 
			
		||||
                                overrideDocumentation(configButtonData.mode)
 | 
			
		||||
                            }
 | 
			
		||||
                            onClose={clearDocumentationOverride}
 | 
			
		||||
                        />
 | 
			
		||||
@ -308,10 +256,8 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={isEnterprise()}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <TableSelect
 | 
			
		||||
                            description={
 | 
			
		||||
                                selectionButtonData.changeRequests.text
 | 
			
		||||
                            }
 | 
			
		||||
                        <ChangeRequestTableConfigButton
 | 
			
		||||
                            description={configButtonData.changeRequests.text}
 | 
			
		||||
                            activeEnvironments={
 | 
			
		||||
                                availableChangeRequestEnvironments
 | 
			
		||||
                            }
 | 
			
		||||
@ -334,7 +280,7 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
                            }
 | 
			
		||||
                            onOpen={() =>
 | 
			
		||||
                                overrideDocumentation(
 | 
			
		||||
                                    selectionButtonData.changeRequests,
 | 
			
		||||
                                    configButtonData.changeRequests,
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                            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 { groupProjects } from './group-projects';
 | 
			
		||||
import { ProjectGroup } from './ProjectGroup';
 | 
			
		||||
import { CreateProjectDialog } from '../Project/CreateProject/CreateProjectDialog/CreateProjectDialog';
 | 
			
		||||
import { CreateProjectDialog } from '../Project/CreateProject/NewCreateProjectForm/CreateProjectDialog';
 | 
			
		||||
 | 
			
		||||
const StyledApiError = styled(ApiError)(({ theme }) => ({
 | 
			
		||||
    maxWidth: '500px',
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user