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 GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import KeyboardArrowDownOutlined from '@mui/icons-material/KeyboardArrowDownOutlined'; import KeyboardArrowDownOutlined from '@mui/icons-material/KeyboardArrowDownOutlined';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
// import { PROJECT_CHANGE_REQUEST_WRITE } from '../../../../providers/AccessProvider/permissions';
const StyledBox = styled(Box)(({ theme }) => ({ const StyledBox = styled(Box)(({ theme }) => ({
padding: theme.spacing(1), padding: theme.spacing(1),

View File

@ -15,7 +15,6 @@ import { GO_BACK } from 'constants/navigate';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { Button, styled } from '@mui/material'; import { Button, styled } from '@mui/material';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { useState } from 'react';
const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN'; const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN';
@ -35,14 +34,10 @@ const CreateProject = () => {
projectName, projectName,
projectDesc, projectDesc,
projectMode, projectMode,
projectEnvironments,
projectChangeRequestConfiguration,
setProjectMode, setProjectMode,
setProjectId, setProjectId,
setProjectName, setProjectName,
setProjectDesc, setProjectDesc,
setProjectEnvironments,
updateProjectChangeRequestConfig,
getCreateProjectPayload, getCreateProjectPayload,
clearErrors, clearErrors,
validateProjectId, validateProjectId,
@ -56,14 +51,6 @@ const CreateProject = () => {
return <Navigate to={`/projects?create=true`} replace />; 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 { createProject, loading } = useProjectApi();
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: Event) => {

View File

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

View File

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

View File

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