1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-06 00:07:44 +01:00
unleash.unleash/frontend/src/component/project/Project/CreateProject/NewProjectForm.tsx
Thomas Heartman be4bb86b92
fix: add accessible descriptions to the dropdowns (#7112)
This PR adds accessible descriptions to the dropdown widgets in the new
project creation form. The description is the same as we show in the
background
2024-05-22 14:02:05 +02:00

353 lines
14 KiB
TypeScript

import { Typography, styled } from '@mui/material';
import Input from 'component/common/Input/Input';
import type { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
import {
MultiselectList,
SingleSelectList,
TableSelect,
} from './SelectionButton';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import StickinessIcon from '@mui/icons-material/FormatPaint';
import ProjectModeIcon from '@mui/icons-material/Adjust';
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';
import type { ReactNode } from 'react';
import theme from 'themes/theme';
const StyledForm = styled('form')(({ theme }) => ({
background: theme.palette.background.default,
}));
const StyledFormSection = styled('div')(({ theme }) => ({
'& + *': {
borderBlockStart: `1px solid ${theme.palette.divider}`,
},
padding: theme.spacing(6),
}));
const TopGrid = styled(StyledFormSection)(({ theme }) => ({
display: 'grid',
gridTemplateAreas:
'"icon header" "icon project-name" "icon project-description"',
gridTemplateColumns: 'auto 1fr',
gap: theme.spacing(4),
}));
const StyledIcon = styled(ProjectIcon)(({ theme }) => ({
fill: theme.palette.primary.main,
stroke: theme.palette.primary.main,
}));
const StyledHeader = styled(Typography)(({ theme }) => ({
gridArea: 'header',
alignSelf: 'center',
fontWeight: 'lighter',
}));
const ProjectNameContainer = styled('div')(({ theme }) => ({
gridArea: 'project-name',
}));
const ProjectDescriptionContainer = styled('div')(({ theme }) => ({
gridArea: 'project-description',
}));
const StyledInput = styled(Input)(({ theme }) => ({
width: '100%',
fieldset: { border: 'none' },
}));
const OptionButtons = styled(StyledFormSection)(({ theme }) => ({
display: 'flex',
flexFlow: 'row wrap',
gap: theme.spacing(2),
}));
const FormActions = styled(StyledFormSection)(({ theme }) => ({
display: 'flex',
gap: theme.spacing(5),
justifyContent: 'flex-end',
}));
type FormProps = {
projectId: string;
projectName: string;
projectDesc: string;
projectStickiness: string;
featureLimit?: string;
featureCount?: number;
projectMode: string;
projectEnvironments: Set<string>;
projectChangeRequestConfiguration: Record<
string,
{ requiredApprovals: number }
>;
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;
enableChangeRequests: (env: string, requiredApprovals: number) => void;
};
handleSubmit: (e: any) => void;
errors: { [key: string]: string };
mode: 'Create' | 'Edit';
clearErrors: () => void;
validateProjectId: () => void;
overrideDocumentation: (args: { text: string; icon: ReactNode }) => void;
clearDocumentationOverride: () => void;
};
const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT';
const PROJECT_DESCRIPTION_INPUT = 'PROJECT_DESCRIPTION_INPUT';
export const NewProjectForm: React.FC<FormProps> = ({
children,
handleSubmit,
projectName,
projectDesc,
projectStickiness,
projectEnvironments,
projectChangeRequestConfiguration,
featureLimit,
featureCount,
projectMode,
setProjectMode,
setProjectEnvironments,
setProjectId,
setProjectName,
setProjectDesc,
setProjectStickiness,
updateProjectChangeRequestConfig,
setFeatureLimit,
errors,
mode,
clearErrors,
overrideDocumentation,
clearDocumentationOverride,
}) => {
const { isEnterprise } = useUiConfig();
const { environments: allEnvironments } = useEnvironments();
const activeEnvironments = allEnvironments.filter((env) => env.enabled);
const handleProjectNameUpdate = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
const input = e.target.value;
setProjectName(input);
};
const projectModeOptions = [
{ value: 'open', label: 'open' },
{ value: 'protected', label: 'protected' },
{ value: 'private', label: 'private' },
];
const stickinessOptions = useStickinessOptions(projectStickiness);
const selectionButtonData = {
environments: {
icon: <EnvironmentsIcon />,
text: `Each feature flag can have a separate configuration per environment. This setting configures which environments your project should start with.`,
},
stickiness: {
icon: <StickinessIcon />,
text: 'Stickiness is used to guarantee that your users see the same result when using a gradual rollout. Default stickiness allows you to choose which field is used by default in this project.',
},
mode: {
icon: <ProjectModeIcon />,
text: 'Mode defines who should be allowed to interact and see your project. Private mode hides the project from anyone except the project owner and members.',
},
changeRequests: {
icon: <ChangeRequestIcon />,
text: 'Change requests can be configured per environment and require changes to go through an approval process before being applied.',
},
};
return (
<StyledForm
onSubmit={(submitEvent) => {
handleSubmit(submitEvent);
}}
>
<TopGrid>
<StyledIcon aria-hidden='true' />
<StyledHeader variant='h2'>New project</StyledHeader>
<ProjectNameContainer>
<StyledInput
label='Project name'
aria-required
value={projectName}
onChange={handleProjectNameUpdate}
error={Boolean(errors.name)}
errorText={errors.name}
onFocus={() => {
delete errors.name;
}}
data-testid={PROJECT_NAME_INPUT}
autoFocus
InputProps={{
style: { fontSize: theme.typography.h1.fontSize },
}}
InputLabelProps={{
style: { fontSize: theme.typography.h1.fontSize },
}}
size='medium'
/>
</ProjectNameContainer>
<ProjectDescriptionContainer>
<StyledInput
size='medium'
className='description'
label='Description (optional)'
multiline
maxRows={20}
value={projectDesc}
onChange={(e) => setProjectDesc(e.target.value)}
data-testid={PROJECT_DESCRIPTION_INPUT}
InputProps={{
style: { fontSize: theme.typography.h2.fontSize },
}}
InputLabelProps={{
style: { fontSize: theme.typography.h2.fontSize },
}}
/>
</ProjectDescriptionContainer>
</TopGrid>
<OptionButtons>
<MultiselectList
description={selectionButtonData.environments.text}
selectedOptions={projectEnvironments}
options={activeEnvironments.map((env) => ({
label: env.name,
value: env.name,
}))}
onChange={setProjectEnvironments}
button={{
label:
projectEnvironments.size > 0
? `${projectEnvironments.size} selected`
: 'All environments',
icon: <EnvironmentsIcon />,
}}
search={{
label: 'Filter project environments',
placeholder: 'Select project environments',
}}
onOpen={() =>
overrideDocumentation(selectionButtonData.environments)
}
onClose={clearDocumentationOverride}
/>
<SingleSelectList
description={selectionButtonData.stickiness.text}
options={stickinessOptions.map(({ key, ...rest }) => ({
value: key,
...rest,
}))}
onChange={(value: any) => {
setProjectStickiness(value);
}}
button={{
label: projectStickiness,
icon: <StickinessIcon />,
}}
search={{
label: 'Filter stickiness options',
placeholder: 'Select default stickiness',
}}
onOpen={() =>
overrideDocumentation(selectionButtonData.stickiness)
}
onClose={clearDocumentationOverride}
/>
<ConditionallyRender
condition={isEnterprise()}
show={
<SingleSelectList
description={selectionButtonData.mode.text}
options={projectModeOptions}
onChange={(value: any) => {
setProjectMode(value);
}}
button={{
label: projectMode,
icon: <ProjectModeIcon />,
}}
search={{
label: 'Filter project mode options',
placeholder: 'Select project mode',
}}
onOpen={() =>
overrideDocumentation(selectionButtonData.mode)
}
onClose={clearDocumentationOverride}
/>
}
/>
<ConditionallyRender
condition={isEnterprise()}
show={
<TableSelect
description={
selectionButtonData.changeRequests.text
}
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
}
onOpen={() =>
overrideDocumentation(
selectionButtonData.changeRequests,
)
}
onClose={clearDocumentationOverride}
/>
}
/>
</OptionButtons>
<FormActions>{children}</FormActions>
</StyledForm>
);
};