1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-19 01:17:18 +02:00

chore: new project dialog code cleanup 1 (#7113)

This PR implements some initial cleanup work for the new project
creation dialog.

The primary focus here is to remove unused props and to use the same
logic for the configuration buttons regardless of the content (mode,
stickiness, envs, change requests).
This commit is contained in:
Thomas Heartman 2024-05-28 14:01:59 +02:00 committed by GitHub
parent 029d43bbcc
commit ff377cd704
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 231 additions and 260 deletions

View File

@ -14,7 +14,6 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
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),

View File

@ -15,7 +15,6 @@ import { GO_BACK } from 'constants/navigate';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { Button, styled } from '@mui/material';
import { useUiFlag } from 'hooks/useUiFlag';
import { useState } from 'react';
const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN';
@ -35,14 +34,10 @@ const CreateProject = () => {
projectName,
projectDesc,
projectMode,
projectEnvironments,
projectChangeRequestConfiguration,
setProjectMode,
setProjectId,
setProjectName,
setProjectDesc,
setProjectEnvironments,
updateProjectChangeRequestConfig,
getCreateProjectPayload,
clearErrors,
validateProjectId,
@ -56,14 +51,6 @@ const CreateProject = () => {
return <Navigate to={`/projects?create=true`} replace />;
}
const generalDocumentation =
'Projects allows you to group feature flags together in the management UI.';
const [documentation, setDocumentation] = useState(generalDocumentation);
const clearDocumentationOverride = () =>
setDocumentation(generalDocumentation);
const { createProject, loading } = useProjectApi();
const handleSubmit = async (e: Event) => {

View File

@ -60,14 +60,12 @@ export const CreateProjectDialog = ({
projectEnvironments,
projectChangeRequestConfiguration,
setProjectMode,
setProjectId,
setProjectName,
setProjectDesc,
setProjectEnvironments,
updateProjectChangeRequestConfig,
getCreateProjectPayload,
clearErrors,
validateProjectId,
validateName,
setProjectStickiness,
projectStickiness,
@ -151,7 +149,6 @@ export const CreateProjectDialog = ({
projectId={projectId}
projectEnvironments={projectEnvironments}
setProjectEnvironments={setProjectEnvironments}
setProjectId={setProjectId}
projectName={projectName}
projectStickiness={projectStickiness}
projectChangeRequestConfiguration={
@ -166,9 +163,6 @@ export const CreateProjectDialog = ({
setProjectName={setProjectName}
projectDesc={projectDesc}
setProjectDesc={setProjectDesc}
mode='Create'
clearErrors={clearErrors}
validateProjectId={validateProjectId}
overrideDocumentation={setDocumentation}
clearDocumentationOverride={clearDocumentationOverride}
>

View File

@ -3,7 +3,7 @@ import Input from 'component/common/Input/Input';
import type { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
import {
MultiselectList,
MultiSelectList,
SingleSelectList,
TableSelect,
} from './SelectionButton';
@ -80,8 +80,6 @@ type FormProps = {
projectName: string;
projectDesc: string;
projectStickiness: string;
featureLimit?: string;
featureCount?: number;
projectMode: string;
projectEnvironments: Set<string>;
projectChangeRequestConfiguration: Record<
@ -90,10 +88,8 @@ type FormProps = {
>;
setProjectStickiness: React.Dispatch<React.SetStateAction<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;
@ -101,9 +97,6 @@ type FormProps = {
};
handleSubmit: (e: any) => void;
errors: { [key: string]: string };
mode: 'Create' | 'Edit';
clearErrors: () => void;
validateProjectId: () => void;
overrideDocumentation: (args: { text: string; icon: ReactNode }) => void;
clearDocumentationOverride: () => void;
};
@ -119,20 +112,14 @@ export const NewProjectForm: React.FC<FormProps> = ({
projectStickiness,
projectEnvironments,
projectChangeRequestConfiguration,
featureLimit,
featureCount,
projectMode,
setProjectMode,
setProjectEnvironments,
setProjectId,
setProjectName,
setProjectDesc,
setProjectStickiness,
updateProjectChangeRequestConfig,
setFeatureLimit,
errors,
mode,
clearErrors,
overrideDocumentation,
clearDocumentationOverride,
}) => {
@ -183,6 +170,15 @@ export const NewProjectForm: React.FC<FormProps> = ({
: numberOfConfiguredChangeRequestEnvironments === 1
? `1 environment configured`
: 'Configure change requests';
const availableChangeRequestEnvironments = (
projectEnvironments.size === 0
? activeEnvironments
: activeEnvironments.filter((env) =>
projectEnvironments.has(env.name),
)
).map(({ name, type }) => ({ name, type }));
return (
<StyledForm
onSubmit={(submitEvent) => {
@ -235,7 +231,7 @@ export const NewProjectForm: React.FC<FormProps> = ({
</TopGrid>
<OptionButtons>
<MultiselectList
<MultiSelectList
description={selectionButtonData.environments.text}
selectedOptions={projectEnvironments}
options={activeEnvironments.map((env) => ({
@ -316,15 +312,9 @@ export const NewProjectForm: React.FC<FormProps> = ({
description={
selectionButtonData.changeRequests.text
}
disabled={projectEnvironments.size === 0}
activeEnvironments={activeEnvironments
.filter((env) =>
projectEnvironments.has(env.name),
)
.map((env) => ({
name: env.name,
type: env.type,
}))}
activeEnvironments={
availableChangeRequestEnvironments
}
updateProjectChangeRequestConfiguration={
updateProjectChangeRequestConfig
}

View File

@ -74,3 +74,9 @@ 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',
}));

View File

@ -1,14 +1,14 @@
import Search from '@mui/icons-material/Search';
import { v4 as uuidv4 } from 'uuid';
import {
Box,
Button,
InputAdornment,
List,
ListItemText,
styled,
} from '@mui/material';
import { type FC, type ReactNode, useRef, useState, useMemo } from 'react';
type FC,
type ReactNode,
useRef,
useState,
useMemo,
type PropsWithChildren,
} from 'react';
import { Box, Button, InputAdornment, List, ListItemText } from '@mui/material';
import {
StyledCheckbox,
StyledDropdown,
@ -17,7 +17,7 @@ import {
StyledDropdownSearch,
TableSearchInput,
HiddenDescription,
ScrollContainer,
ButtonLabel,
} from './SelectionButton.styles';
import { ChangeRequestTable } from './ChangeRequestTable';
@ -94,24 +94,30 @@ type CombinedSelectProps = {
description: string; // visually hidden, for assistive tech
};
const CombinedSelect: FC<CombinedSelectProps> = ({
options,
onChange,
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,
search,
multiselect,
onOpen = () => {},
onClose = () => {},
description,
children,
preventOpen,
anchorEl,
setAnchorEl,
}) => {
const ref = useRef<HTMLDivElement>(null);
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>();
const [searchText, setSearchText] = useState('');
const descriptionId = uuidv4();
const [recentlyClosed, setRecentlyClosed] = useState(false);
const open = () => {
setSearchText('');
setAnchorEl(ref.current);
onOpen();
};
@ -121,30 +127,6 @@ const CombinedSelect: FC<CombinedSelectProps> = ({
onClose();
};
const onSelection = (selected: string) => {
onChange(selected);
if (!multiselect) {
handleClose();
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);
}
};
const { listRefs, handleSelection } = useSelectionManagement({
handleToggle: (selected: string) => () => onSelection(selected),
});
const filteredOptions = options?.filter((option) =>
option.label.toLowerCase().includes(searchText.toLowerCase()),
);
const ButtonLabel = styled('span')(() => ({
width: button.labelWidth || 'unset',
}));
return (
<>
<Box ref={ref}>
@ -153,12 +135,14 @@ const CombinedSelect: FC<CombinedSelectProps> = ({
color='primary'
startIcon={button.icon}
onClick={() => {
if (!recentlyClosed) {
if (!preventOpen) {
open();
}
}}
>
<ButtonLabel>{button.label}</ButtonLabel>
<ButtonLabel labelWidth={button.labelWidth}>
{button.label}
</ButtonLabel>
</Button>
</Box>
<StyledPopover
@ -178,115 +162,103 @@ const CombinedSelect: FC<CombinedSelectProps> = ({
{description}
</HiddenDescription>
<StyledDropdown aria-describedby={descriptionId}>
<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>
{children}
</StyledDropdown>
</StyledPopover>
</>
);
};
type MultiselectListProps = Pick<
CombinedSelectProps,
'options' | 'button' | 'search' | 'onOpen' | 'onClose' | 'description'
> & {
selectedOptions: Set<string>;
onChange: (values: Set<string>) => void;
};
export const MultiselectList: FC<MultiselectListProps> = ({
selectedOptions,
const DropdownList: FC<CombinedSelectProps> = ({
options,
onChange,
...rest
search,
multiselect,
}) => {
// todo: add "select all" and "deselect all"
const [searchText, setSearchText] = useState('');
const handleToggle = (value: string) => {
if (selectedOptions.has(value)) {
selectedOptions.delete(value);
} else {
selectedOptions.add(value);
}
onChange(new Set(selectedOptions));
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 (
<CombinedSelect
{...rest}
onChange={handleToggle}
multiselect={{
selectedOptions,
}}
/>
<>
<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>
</>
);
};
@ -301,8 +273,73 @@ type SingleSelectListProps = Pick<
| 'description'
>;
export const SingleSelectList: FC<SingleSelectListProps> = (props) => {
return <CombinedSelect {...props} />;
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<
@ -321,17 +358,17 @@ type TableSelectProps = Pick<
string,
{ requiredApprovals: number }
>;
disabled: boolean;
};
export const TableSelect: FC<TableSelectProps> = ({
button,
disabled,
search,
projectChangeRequestConfiguration,
updateProjectChangeRequestConfiguration,
activeEnvironments,
onOpen = () => {},
onClose = () => {},
...props
}) => {
const configured = useMemo(() => {
return Object.fromEntries(
@ -365,21 +402,9 @@ export const TableSelect: FC<TableSelectProps> = ({
updateProjectChangeRequestConfiguration.disableChangeRequests(name);
};
const ref = useRef<HTMLDivElement>(null);
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>();
const [searchText, setSearchText] = useState('');
const open = () => {
setSearchText('');
setAnchorEl(ref.current);
onOpen();
};
const handleClose = () => {
setAnchorEl(null);
onClose();
};
const filteredEnvs = tableEnvs.filter((env) =>
env.name.toLowerCase().includes(searchText.toLowerCase()),
);
@ -399,66 +424,36 @@ export const TableSelect: FC<TableSelectProps> = ({
}
};
const ButtonLabel = styled('span')(() => ({
width: button.labelWidth || 'unset',
}));
return (
<>
<Box ref={ref}>
<Button
variant='outlined'
color='primary'
startIcon={button.icon}
onClick={() => {
open();
}}
disabled={disabled}
>
<ButtonLabel>{button.label}</ButtonLabel>
</Button>
</Box>
<StyledPopover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
<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>
),
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<StyledDropdown>
<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}
/>
<ScrollContainer>
<ChangeRequestTable
environments={filteredEnvs}
enableEnvironment={onEnable}
disableEnvironment={onDisable}
/>
</ScrollContainer>
</StyledDropdown>
</StyledPopover>
</>
onKeyDown={toggleTopItem}
/>
<ChangeRequestTable
environments={filteredEnvs}
enableEnvironment={onEnable}
disableEnvironment={onDisable}
/>
</CombinedSelect>
);
};