mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: configure CRs when creating projects (#6979)
This PR adds the ability to configure CRs when creating a project. The design is unfinished, and the code certainly needs cleanup, but I'd like to get this into sandbox so we can look at it. Things that still need to be done: 1. What do we do about this button when the user has no environments selected? As a rough draft, I've disabled it. However, we should make it possible to navigate to and give you an explanation why it was disabled, e.g. "You have no project environments selected. Please select at least one project environment.". 2. The form design is not done: the width should be constant and not jumpy the way it is now. Also, the search field is too wide. 3. I've made the desicion that if you deselect a project env, we also remove that env from your CR config it it's in there. 4. Potential improvement: if you enable and then disable CRs for an env, we *could* probably store the data in between, so that if you set required approvers 5 and then disabled it, it'd still be 5 when you re-enabled it. That sounds like a good user experience. We should also be able to extend that to adding/removing environments from project envs.
This commit is contained in:
		
							parent
							
								
									2d9fb90443
								
							
						
					
					
						commit
						07871e73e5
					
				@ -0,0 +1,207 @@
 | 
			
		||||
import { useMemo } from 'react';
 | 
			
		||||
import { type HeaderGroup, useGlobalFilter, useTable } from 'react-table';
 | 
			
		||||
import { Box, Switch, styled } from '@mui/material';
 | 
			
		||||
import {
 | 
			
		||||
    SortableTableHeader,
 | 
			
		||||
    Table,
 | 
			
		||||
    TableBody,
 | 
			
		||||
    TableCell,
 | 
			
		||||
    TableRow,
 | 
			
		||||
} from 'component/common/Table';
 | 
			
		||||
import { sortTypes } from 'utils/sortTypes';
 | 
			
		||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
 | 
			
		||||
import KeyboardArrowDownOutlined from '@mui/icons-material/KeyboardArrowDownOutlined';
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
// import { PROJECT_CHANGE_REQUEST_WRITE } from '../../../../providers/AccessProvider/permissions';
 | 
			
		||||
 | 
			
		||||
const StyledBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    padding: theme.spacing(1),
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    justifyContent: 'center',
 | 
			
		||||
    '& .MuiInputBase-input': {
 | 
			
		||||
        fontSize: theme.fontSizes.smallBody,
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTable = styled(Table)(({ theme }) => ({
 | 
			
		||||
    th: { whiteSpace: 'nowrap' },
 | 
			
		||||
    width: '50rem',
 | 
			
		||||
    maxWidth: '90vw',
 | 
			
		||||
    'tr:last-of-type > td': {
 | 
			
		||||
        borderBottom: 'none',
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
type TableProps = {
 | 
			
		||||
    environments: {
 | 
			
		||||
        name: string;
 | 
			
		||||
        type: string;
 | 
			
		||||
        requiredApprovals: number;
 | 
			
		||||
        changeRequestEnabled: boolean;
 | 
			
		||||
    }[];
 | 
			
		||||
    enableEnvironment: (name: string, requiredApprovals: number) => void;
 | 
			
		||||
    disableEnvironment: (name: string) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ChangeRequestTable = (props: TableProps) => {
 | 
			
		||||
    const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
    const onToggleEnvironment =
 | 
			
		||||
        (
 | 
			
		||||
            environmentName: string,
 | 
			
		||||
            previousState: boolean,
 | 
			
		||||
            requiredApprovals: number,
 | 
			
		||||
        ) =>
 | 
			
		||||
        () => {
 | 
			
		||||
            const newState = !previousState;
 | 
			
		||||
            if (newState) {
 | 
			
		||||
                props.enableEnvironment(environmentName, requiredApprovals);
 | 
			
		||||
            } else {
 | 
			
		||||
                props.disableEnvironment(environmentName);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    const approvalOptions = Array.from(Array(10).keys())
 | 
			
		||||
        .map((key) => String(key + 1))
 | 
			
		||||
        .map((key) => {
 | 
			
		||||
            const labelText = key === '1' ? 'approval' : 'approvals';
 | 
			
		||||
            return {
 | 
			
		||||
                key,
 | 
			
		||||
                label: `${key} ${labelText}`,
 | 
			
		||||
                sx: { fontSize: theme.fontSizes.smallBody },
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    function onRequiredApprovalsChange(original: any, approvals: string) {
 | 
			
		||||
        props.enableEnvironment(original.environment, Number(approvals));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const columns = useMemo(
 | 
			
		||||
        () => [
 | 
			
		||||
            {
 | 
			
		||||
                Header: 'Environment',
 | 
			
		||||
                accessor: 'environment',
 | 
			
		||||
                disableSortBy: true,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                Header: 'Type',
 | 
			
		||||
                accessor: 'type',
 | 
			
		||||
                disableGlobalFilter: true,
 | 
			
		||||
                disableSortBy: true,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                Header: 'Required approvals',
 | 
			
		||||
                Cell: ({ row: { original } }: any) => {
 | 
			
		||||
                    return (
 | 
			
		||||
                        <ConditionallyRender
 | 
			
		||||
                            condition={original.changeRequestEnabled}
 | 
			
		||||
                            show={
 | 
			
		||||
                                <StyledBox data-loading>
 | 
			
		||||
                                    <GeneralSelect
 | 
			
		||||
                                        label={`Set required approvals for ${original.environment}`}
 | 
			
		||||
                                        id={`cr-approvals-${original.environment}`}
 | 
			
		||||
                                        sx={{ width: '140px' }}
 | 
			
		||||
                                        options={approvalOptions}
 | 
			
		||||
                                        value={original.requiredApprovals || 1}
 | 
			
		||||
                                        onChange={(approvals) => {
 | 
			
		||||
                                            onRequiredApprovalsChange(
 | 
			
		||||
                                                original,
 | 
			
		||||
                                                approvals,
 | 
			
		||||
                                            );
 | 
			
		||||
                                        }}
 | 
			
		||||
                                        IconComponent={
 | 
			
		||||
                                            KeyboardArrowDownOutlined
 | 
			
		||||
                                        }
 | 
			
		||||
                                        fullWidth
 | 
			
		||||
                                    />
 | 
			
		||||
                                </StyledBox>
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                    );
 | 
			
		||||
                },
 | 
			
		||||
                width: 100,
 | 
			
		||||
                disableGlobalFilter: true,
 | 
			
		||||
                disableSortBy: true,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                Header: 'Status',
 | 
			
		||||
                accessor: 'changeRequestEnabled',
 | 
			
		||||
                id: 'changeRequestEnabled',
 | 
			
		||||
                align: 'center',
 | 
			
		||||
 | 
			
		||||
                Cell: ({ value, row: { original } }: any) => {
 | 
			
		||||
                    return (
 | 
			
		||||
                        <StyledBox data-loading>
 | 
			
		||||
                            <Switch
 | 
			
		||||
                                checked={value}
 | 
			
		||||
                                inputProps={{
 | 
			
		||||
                                    'aria-label': `${
 | 
			
		||||
                                        value ? 'Disable' : 'Enable'
 | 
			
		||||
                                    } change requests for ${
 | 
			
		||||
                                        original.environment
 | 
			
		||||
                                    }`,
 | 
			
		||||
                                }}
 | 
			
		||||
                                onClick={onToggleEnvironment(
 | 
			
		||||
                                    original.environment,
 | 
			
		||||
                                    original.changeRequestEnabled,
 | 
			
		||||
                                    original.requiredApprovals,
 | 
			
		||||
                                )}
 | 
			
		||||
                            />
 | 
			
		||||
                        </StyledBox>
 | 
			
		||||
                    );
 | 
			
		||||
                },
 | 
			
		||||
                width: 100,
 | 
			
		||||
                disableGlobalFilter: true,
 | 
			
		||||
                disableSortBy: true,
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
        [],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
 | 
			
		||||
        useTable(
 | 
			
		||||
            {
 | 
			
		||||
                // @ts-ignore
 | 
			
		||||
                columns,
 | 
			
		||||
                data: props.environments.map((env) => {
 | 
			
		||||
                    return {
 | 
			
		||||
                        environment: env.name,
 | 
			
		||||
                        type: env.type,
 | 
			
		||||
                        changeRequestEnabled: env.changeRequestEnabled,
 | 
			
		||||
                        requiredApprovals: env.requiredApprovals ?? 1,
 | 
			
		||||
                    };
 | 
			
		||||
                }),
 | 
			
		||||
 | 
			
		||||
                sortTypes,
 | 
			
		||||
                autoResetGlobalFilter: false,
 | 
			
		||||
                disableSortRemove: true,
 | 
			
		||||
                defaultColumn: {
 | 
			
		||||
                    Cell: TextCell,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            useGlobalFilter,
 | 
			
		||||
        );
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledTable {...getTableProps()}>
 | 
			
		||||
            <SortableTableHeader
 | 
			
		||||
                headerGroups={headerGroups as HeaderGroup<object>[]}
 | 
			
		||||
            />
 | 
			
		||||
            <TableBody {...getTableBodyProps()}>
 | 
			
		||||
                {rows.map((row) => {
 | 
			
		||||
                    prepareRow(row);
 | 
			
		||||
                    return (
 | 
			
		||||
                        <TableRow hover {...row.getRowProps()}>
 | 
			
		||||
                            {row.cells.map((cell) => (
 | 
			
		||||
                                <TableCell {...cell.getCellProps()}>
 | 
			
		||||
                                    {cell.render('Cell')}
 | 
			
		||||
                                </TableCell>
 | 
			
		||||
                            ))}
 | 
			
		||||
                        </TableRow>
 | 
			
		||||
                    );
 | 
			
		||||
                })}
 | 
			
		||||
            </TableBody>
 | 
			
		||||
        </StyledTable>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -35,11 +35,13 @@ const CreateProject = () => {
 | 
			
		||||
        projectDesc,
 | 
			
		||||
        projectMode,
 | 
			
		||||
        projectEnvironments,
 | 
			
		||||
        projectChangeRequestConfiguration,
 | 
			
		||||
        setProjectMode,
 | 
			
		||||
        setProjectId,
 | 
			
		||||
        setProjectName,
 | 
			
		||||
        setProjectDesc,
 | 
			
		||||
        setProjectEnvironments,
 | 
			
		||||
        updateProjectChangeRequestConfig,
 | 
			
		||||
        getCreateProjectPayload,
 | 
			
		||||
        clearErrors,
 | 
			
		||||
        validateProjectId,
 | 
			
		||||
@ -114,6 +116,12 @@ const CreateProject = () => {
 | 
			
		||||
                    setProjectId={setProjectId}
 | 
			
		||||
                    projectName={projectName}
 | 
			
		||||
                    projectStickiness={projectStickiness}
 | 
			
		||||
                    projectChangeRequestConfiguration={
 | 
			
		||||
                        projectChangeRequestConfiguration
 | 
			
		||||
                    }
 | 
			
		||||
                    updateProjectChangeRequestConfig={
 | 
			
		||||
                        updateProjectChangeRequestConfig
 | 
			
		||||
                    }
 | 
			
		||||
                    projectMode={projectMode}
 | 
			
		||||
                    setProjectMode={setProjectMode}
 | 
			
		||||
                    setProjectStickiness={setProjectStickiness}
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,13 @@
 | 
			
		||||
import { Button, Typography, styled } from '@mui/material';
 | 
			
		||||
import { Typography, styled } from '@mui/material';
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid';
 | 
			
		||||
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 } from './SelectionButton';
 | 
			
		||||
import {
 | 
			
		||||
    MultiselectList,
 | 
			
		||||
    SingleSelectList,
 | 
			
		||||
    TableSelect,
 | 
			
		||||
} from './SelectionButton';
 | 
			
		||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
 | 
			
		||||
import StickinessIcon from '@mui/icons-material/FormatPaint';
 | 
			
		||||
import ProjectModeIcon from '@mui/icons-material/Adjust';
 | 
			
		||||
@ -11,6 +15,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import EnvironmentsIcon from '@mui/icons-material/CloudCircle';
 | 
			
		||||
import { useStickinessOptions } from 'hooks/useStickinessOptions';
 | 
			
		||||
import { ReactComponent as ChangeRequestIcon } from 'assets/icons/merge.svg';
 | 
			
		||||
 | 
			
		||||
const StyledForm = styled('form')(({ theme }) => ({
 | 
			
		||||
    background: theme.palette.background.default,
 | 
			
		||||
@ -65,6 +70,7 @@ const StyledProjectDescription = styled(StyledInput)(({ theme }) => ({
 | 
			
		||||
 | 
			
		||||
const OptionButtons = styled(StyledFormSection)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexFlow: 'row wrap',
 | 
			
		||||
    gap: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
@ -83,13 +89,21 @@ type FormProps = {
 | 
			
		||||
    featureCount?: number;
 | 
			
		||||
    projectMode: string;
 | 
			
		||||
    projectEnvironments: Set<string>;
 | 
			
		||||
    projectChangeRequestConfiguration: Record<
 | 
			
		||||
        string,
 | 
			
		||||
        { requiredApprovals: number }
 | 
			
		||||
    >;
 | 
			
		||||
    setProjectStickiness: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
    setProjectEnvironments: React.Dispatch<React.SetStateAction<Set<string>>>;
 | 
			
		||||
    setProjectEnvironments: (envs: Set<string>) => void;
 | 
			
		||||
    setProjectId: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
    setProjectName: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
    setProjectDesc: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
    setFeatureLimit?: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
    setProjectMode: React.Dispatch<React.SetStateAction<ProjectMode>>;
 | 
			
		||||
    updateProjectChangeRequestConfig: {
 | 
			
		||||
        disableChangeRequests: (env: string) => void;
 | 
			
		||||
        enableChangeRequests: (env: string, requiredApprovals: number) => void;
 | 
			
		||||
    };
 | 
			
		||||
    handleSubmit: (e: any) => void;
 | 
			
		||||
    errors: { [key: string]: string };
 | 
			
		||||
    mode: 'Create' | 'Edit';
 | 
			
		||||
@ -107,6 +121,7 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
    projectDesc,
 | 
			
		||||
    projectStickiness,
 | 
			
		||||
    projectEnvironments,
 | 
			
		||||
    projectChangeRequestConfiguration,
 | 
			
		||||
    featureLimit,
 | 
			
		||||
    featureCount,
 | 
			
		||||
    projectMode,
 | 
			
		||||
@ -116,6 +131,8 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
    setProjectName,
 | 
			
		||||
    setProjectDesc,
 | 
			
		||||
    setProjectStickiness,
 | 
			
		||||
    // setProjectChangeRequestConfiguration,
 | 
			
		||||
    updateProjectChangeRequestConfig,
 | 
			
		||||
    setFeatureLimit,
 | 
			
		||||
    errors,
 | 
			
		||||
    mode,
 | 
			
		||||
@ -138,10 +155,6 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
        setProjectId(maybeProjectId);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleFilterChange = (envs: Set<string>) => {
 | 
			
		||||
        setProjectEnvironments(envs);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const projectModeOptions = [
 | 
			
		||||
        { value: 'open', label: 'open' },
 | 
			
		||||
        { value: 'protected', label: 'protected' },
 | 
			
		||||
@ -194,7 +207,7 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
                        label: env.name,
 | 
			
		||||
                        value: env.name,
 | 
			
		||||
                    }))}
 | 
			
		||||
                    onChange={handleFilterChange}
 | 
			
		||||
                    onChange={setProjectEnvironments}
 | 
			
		||||
                    button={{
 | 
			
		||||
                        label:
 | 
			
		||||
                            projectEnvironments.size > 0
 | 
			
		||||
@ -245,7 +258,45 @@ export const NewProjectForm: React.FC<FormProps> = ({
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                <Button variant='outlined'>1 environment configured</Button>
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={isEnterprise()}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <TableSelect
 | 
			
		||||
                            disabled={projectEnvironments.size === 0}
 | 
			
		||||
                            activeEnvironments={activeEnvironments
 | 
			
		||||
                                .filter((env) =>
 | 
			
		||||
                                    projectEnvironments.has(env.name),
 | 
			
		||||
                                )
 | 
			
		||||
                                .map((env) => ({
 | 
			
		||||
                                    name: env.name,
 | 
			
		||||
                                    type: env.type,
 | 
			
		||||
                                }))}
 | 
			
		||||
                            updateProjectChangeRequestConfiguration={
 | 
			
		||||
                                updateProjectChangeRequestConfig
 | 
			
		||||
                            }
 | 
			
		||||
                            button={{
 | 
			
		||||
                                label:
 | 
			
		||||
                                    Object.keys(
 | 
			
		||||
                                        projectChangeRequestConfiguration,
 | 
			
		||||
                                    ).length > 0
 | 
			
		||||
                                        ? `${
 | 
			
		||||
                                              Object.keys(
 | 
			
		||||
                                                  projectChangeRequestConfiguration,
 | 
			
		||||
                                              ).length
 | 
			
		||||
                                          } selected`
 | 
			
		||||
                                        : 'Configure change requests',
 | 
			
		||||
                                icon: <ChangeRequestIcon />,
 | 
			
		||||
                            }}
 | 
			
		||||
                            search={{
 | 
			
		||||
                                label: 'Filter environments',
 | 
			
		||||
                                placeholder: 'Filter environments',
 | 
			
		||||
                            }}
 | 
			
		||||
                            projectChangeRequestConfiguration={
 | 
			
		||||
                                projectChangeRequestConfiguration
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
            </OptionButtons>
 | 
			
		||||
            <FormActions>{children}</FormActions>
 | 
			
		||||
        </StyledForm>
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Checkbox, ListItem, Popover, TextField, styled } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
export const StyledDropdown = styled('div')(({ theme }) => ({
 | 
			
		||||
    padding: theme.spacing(1.5),
 | 
			
		||||
    padding: theme.spacing(2),
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    gap: theme.spacing(1),
 | 
			
		||||
@ -36,16 +36,8 @@ export const StyledTextField = styled(TextField)(({ theme }) => ({
 | 
			
		||||
        padding: theme.spacing(0.75, 0),
 | 
			
		||||
        fontSize: theme.typography.body2.fontSize,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    '& label': {
 | 
			
		||||
        border: 0,
 | 
			
		||||
        clip: 'rect(0 0 0 0)',
 | 
			
		||||
        height: 'auto',
 | 
			
		||||
        margin: 0,
 | 
			
		||||
        overflow: 'hidden',
 | 
			
		||||
        padding: 0,
 | 
			
		||||
        position: 'absolute',
 | 
			
		||||
        width: '1px',
 | 
			
		||||
        whiteSpace: 'nowrap',
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const TableSearchInput = styled(StyledTextField)(({ theme }) => ({
 | 
			
		||||
    maxWidth: '30ch',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,15 @@
 | 
			
		||||
import Search from '@mui/icons-material/Search';
 | 
			
		||||
import { Box, Button, InputAdornment, List, ListItemText } from '@mui/material';
 | 
			
		||||
import { type FC, type ReactNode, useRef, useState } from 'react';
 | 
			
		||||
import { type FC, type ReactNode, useRef, useState, useMemo } from 'react';
 | 
			
		||||
import {
 | 
			
		||||
    StyledCheckbox,
 | 
			
		||||
    StyledDropdown,
 | 
			
		||||
    StyledListItem,
 | 
			
		||||
    StyledPopover,
 | 
			
		||||
    StyledTextField,
 | 
			
		||||
    TableSearchInput,
 | 
			
		||||
} from './SelectionButton.styles';
 | 
			
		||||
import { ChangeRequestTable } from './ChangeRequestTable';
 | 
			
		||||
 | 
			
		||||
export interface IFilterItemProps {
 | 
			
		||||
    label: ReactNode;
 | 
			
		||||
@ -262,3 +264,147 @@ type SingleSelectListProps = Pick<
 | 
			
		||||
export const SingleSelectList: FC<SingleSelectListProps> = (props) => {
 | 
			
		||||
    return <CombinedSelect {...props} />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type TableSelectProps = Pick<CombinedSelectProps, 'button' | 'search'> & {
 | 
			
		||||
    updateProjectChangeRequestConfiguration: {
 | 
			
		||||
        disableChangeRequests: (env: string) => void;
 | 
			
		||||
        enableChangeRequests: (env: string, requiredApprovals: number) => void;
 | 
			
		||||
    };
 | 
			
		||||
    activeEnvironments: {
 | 
			
		||||
        name: string;
 | 
			
		||||
        type: string;
 | 
			
		||||
    }[];
 | 
			
		||||
    projectChangeRequestConfiguration: Record<
 | 
			
		||||
        string,
 | 
			
		||||
        { requiredApprovals: number }
 | 
			
		||||
    >;
 | 
			
		||||
    disabled: boolean;
 | 
			
		||||
};
 | 
			
		||||
export const TableSelect: FC<TableSelectProps> = ({
 | 
			
		||||
    button,
 | 
			
		||||
    disabled,
 | 
			
		||||
    search,
 | 
			
		||||
    projectChangeRequestConfiguration,
 | 
			
		||||
    updateProjectChangeRequestConfiguration,
 | 
			
		||||
    activeEnvironments,
 | 
			
		||||
}) => {
 | 
			
		||||
    const configured = useMemo(() => {
 | 
			
		||||
        return Object.fromEntries(
 | 
			
		||||
            Object.entries(projectChangeRequestConfiguration).map(
 | 
			
		||||
                ([name, config]) => [
 | 
			
		||||
                    name,
 | 
			
		||||
                    { ...config, changeRequestEnabled: true },
 | 
			
		||||
                ],
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
    }, [projectChangeRequestConfiguration]);
 | 
			
		||||
 | 
			
		||||
    const tableEnvs = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            activeEnvironments.map(({ name, type }) => ({
 | 
			
		||||
                name,
 | 
			
		||||
                type,
 | 
			
		||||
                ...(configured[name] ?? { changeRequestEnabled: false }),
 | 
			
		||||
            })),
 | 
			
		||||
        [configured, activeEnvironments],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const onEnable = (name: string, requiredApprovals: number) => {
 | 
			
		||||
        updateProjectChangeRequestConfiguration.enableChangeRequests(
 | 
			
		||||
            name,
 | 
			
		||||
            requiredApprovals,
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onDisable = (name: string) => {
 | 
			
		||||
        updateProjectChangeRequestConfiguration.disableChangeRequests(name);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
    const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>();
 | 
			
		||||
    const [searchText, setSearchText] = useState('');
 | 
			
		||||
 | 
			
		||||
    const open = () => {
 | 
			
		||||
        setSearchText('');
 | 
			
		||||
        setAnchorEl(ref.current);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onClose = () => {
 | 
			
		||||
        setAnchorEl(null);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    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 (
 | 
			
		||||
        <>
 | 
			
		||||
            <Box ref={ref}>
 | 
			
		||||
                <Button
 | 
			
		||||
                    variant='outlined'
 | 
			
		||||
                    color='primary'
 | 
			
		||||
                    startIcon={button.icon}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                        open();
 | 
			
		||||
                    }}
 | 
			
		||||
                    disabled={disabled}
 | 
			
		||||
                >
 | 
			
		||||
                    {button.label}
 | 
			
		||||
                </Button>
 | 
			
		||||
            </Box>
 | 
			
		||||
            <StyledPopover
 | 
			
		||||
                open={Boolean(anchorEl)}
 | 
			
		||||
                anchorEl={anchorEl}
 | 
			
		||||
                onClose={onClose}
 | 
			
		||||
                anchorOrigin={{
 | 
			
		||||
                    vertical: 'bottom',
 | 
			
		||||
                    horizontal: 'left',
 | 
			
		||||
                }}
 | 
			
		||||
                transformOrigin={{
 | 
			
		||||
                    vertical: 'top',
 | 
			
		||||
                    horizontal: 'left',
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
                <StyledDropdown>
 | 
			
		||||
                    <TableSearchInput
 | 
			
		||||
                        variant='outlined'
 | 
			
		||||
                        size='small'
 | 
			
		||||
                        value={searchText}
 | 
			
		||||
                        onChange={(event) => setSearchText(event.target.value)}
 | 
			
		||||
                        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}
 | 
			
		||||
                    />
 | 
			
		||||
                </StyledDropdown>
 | 
			
		||||
            </StyledPopover>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,10 @@ const useProjectForm = (
 | 
			
		||||
    initialFeatureLimit = '',
 | 
			
		||||
    initialProjectMode: ProjectMode = 'open',
 | 
			
		||||
    initialProjectEnvironments: Set<string> = new Set(),
 | 
			
		||||
    initialProjectChangeRequestConfiguration: Record<
 | 
			
		||||
        string,
 | 
			
		||||
        { requiredApprovals: number }
 | 
			
		||||
    > = {},
 | 
			
		||||
) => {
 | 
			
		||||
    const { isEnterprise } = useUiConfig();
 | 
			
		||||
    const [projectId, setProjectId] = useState(initialProjectId);
 | 
			
		||||
@ -28,6 +32,39 @@ const useProjectForm = (
 | 
			
		||||
    const [projectEnvironments, setProjectEnvironments] = useState<Set<string>>(
 | 
			
		||||
        initialProjectEnvironments,
 | 
			
		||||
    );
 | 
			
		||||
    const [
 | 
			
		||||
        projectChangeRequestConfiguration,
 | 
			
		||||
        setProjectChangeRequestConfiguration,
 | 
			
		||||
    ] = useState(initialProjectChangeRequestConfiguration);
 | 
			
		||||
 | 
			
		||||
    // todo: write tests for this
 | 
			
		||||
    // also: disallow adding a project to cr config that isn't in envs
 | 
			
		||||
    const updateProjectEnvironments = (newState: Set<string>) => {
 | 
			
		||||
        const filteredChangeRequestEnvs = Object.fromEntries(
 | 
			
		||||
            Object.entries(projectChangeRequestConfiguration).filter(([env]) =>
 | 
			
		||||
                newState.has(env),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        setProjectChangeRequestConfiguration(filteredChangeRequestEnvs);
 | 
			
		||||
        setProjectEnvironments(newState);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const crConfig = {
 | 
			
		||||
        disableChangeRequests: (env: string) => {
 | 
			
		||||
            setProjectChangeRequestConfiguration((previousState) => {
 | 
			
		||||
                const { [env]: _, ...rest } = previousState;
 | 
			
		||||
                return rest;
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        enableChangeRequests: (env: string, approvals: number) => {
 | 
			
		||||
            setProjectChangeRequestConfiguration((previousState) => ({
 | 
			
		||||
                ...previousState,
 | 
			
		||||
                [env]: { requiredApprovals: approvals },
 | 
			
		||||
            }));
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const [errors, setErrors] = useState({});
 | 
			
		||||
 | 
			
		||||
@ -63,6 +100,13 @@ const useProjectForm = (
 | 
			
		||||
                ? { environments: [...projectEnvironments] }
 | 
			
		||||
                : {};
 | 
			
		||||
 | 
			
		||||
        const changeRequestEnvironments = Object.entries(
 | 
			
		||||
            projectChangeRequestConfiguration,
 | 
			
		||||
        ).map(([env, { requiredApprovals }]) => ({
 | 
			
		||||
            name: env,
 | 
			
		||||
            requiredApprovals,
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        return isEnterprise()
 | 
			
		||||
            ? {
 | 
			
		||||
                  id: projectId,
 | 
			
		||||
@ -71,6 +115,7 @@ const useProjectForm = (
 | 
			
		||||
                  defaultStickiness: projectStickiness,
 | 
			
		||||
                  mode: projectMode,
 | 
			
		||||
                  ...environmentsPayload,
 | 
			
		||||
                  changeRequestEnvironments,
 | 
			
		||||
              }
 | 
			
		||||
            : {
 | 
			
		||||
                  id: projectId,
 | 
			
		||||
@ -133,13 +178,15 @@ const useProjectForm = (
 | 
			
		||||
        projectStickiness,
 | 
			
		||||
        featureLimit,
 | 
			
		||||
        projectEnvironments,
 | 
			
		||||
        projectChangeRequestConfiguration,
 | 
			
		||||
        setProjectId,
 | 
			
		||||
        setProjectName,
 | 
			
		||||
        setProjectDesc,
 | 
			
		||||
        setProjectStickiness,
 | 
			
		||||
        setFeatureLimit,
 | 
			
		||||
        setProjectMode,
 | 
			
		||||
        setProjectEnvironments,
 | 
			
		||||
        setProjectEnvironments: updateProjectEnvironments,
 | 
			
		||||
        updateProjectChangeRequestConfig: crConfig,
 | 
			
		||||
        getCreateProjectPayload,
 | 
			
		||||
        getEditProjectPayload,
 | 
			
		||||
        validateName,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user