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:
parent
029d43bbcc
commit
ff377cd704
@ -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),
|
||||||
|
@ -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) => {
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
|
}));
|
||||||
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user