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

chore: create shared dialog form template (#7663)

This PR extracts the dialog form that we created for the new project
form into a shared component in the `common` folder.

Most of the code has been lifted and shifted, but there's been some
minor adjustments along the way. The main file is
`frontend/src/component/common/DialogFormTemplate/DialogFormTemplate.tsx`.
Everything else is just cleanup.
This commit is contained in:
Thomas Heartman 2024-07-25 13:41:09 +02:00 committed by GitHub
parent 1e3c690185
commit eb7208025f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 398 additions and 407 deletions

View File

@ -84,14 +84,14 @@ export const createProject_UI = (
cy.intercept('POST', `/api/admin/projects`).as('createProject');
cy.get("[data-testid='PROJECT_ID_INPUT']").type(projectName);
cy.get("[data-testid='PROJECT_NAME_INPUT']").type(projectName);
cy.get("[data-testid='FORM_NAME_INPUT']").type(projectName);
cy.get("[id='stickiness-select']")
.first()
.click()
.get(`[data-testid=SELECT_ITEM_ID-${defaultStickiness}`)
.first()
.click();
cy.get("[data-testid='CREATE_PROJECT_BTN']").click();
cy.get("[data-testid='FORM_CREATE_BTN']").click();
cy.wait('@createProject');
return cy.visit(`/projects/${projectName}`);
};

View File

@ -1,12 +1,11 @@
import { Typography, styled } from '@mui/material';
import { Box, Typography, styled } from '@mui/material';
import Input from 'component/common/Input/Input';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
export const StyledForm = styled('form')(({ theme }) => ({
background: theme.palette.background.default,
}));
export const StyledFormSection = styled('div')(({ theme }) => ({
const StyledFormSection = styled('div')(({ theme }) => ({
'& + *': {
borderBlockStart: `1px solid ${theme.palette.divider}`,
},
@ -16,16 +15,19 @@ export const StyledFormSection = styled('div')(({ theme }) => ({
export const TopGrid = styled(StyledFormSection)(({ theme }) => ({
display: 'grid',
gridTemplateAreas:
'"icon header" "icon project-name" "icon project-description"',
gridTemplateAreas: `
"icon header"
". project-name"
". project-description"`,
gridTemplateColumns: 'auto 1fr',
gap: theme.spacing(4),
}));
export const StyledIcon = styled(ProjectIcon)(({ theme }) => ({
export const styleIcon = (Icon: React.ComponentType) =>
styled(Icon)(({ theme }) => ({
fill: theme.palette.primary.main,
stroke: theme.palette.primary.main,
}));
}));
export const StyledHeader = styled(Typography)({
gridArea: 'header',
@ -46,7 +48,7 @@ export const StyledInput = styled(Input)({
fieldset: { border: 'none' },
});
export const OptionButtons = styled(StyledFormSection)(({ theme }) => ({
export const ConfigButtons = styled(StyledFormSection)(({ theme }) => ({
display: 'flex',
flexFlow: 'row wrap',
gap: theme.spacing(2),
@ -78,15 +80,8 @@ export const FormActions = styled(StyledFormSection)(({ theme }) => ({
},
}));
export const StyledDefinitionList = styled('dl')(({ theme }) => ({
dt: {
fontWeight: 'bold',
'&:after': {
content: '":"',
},
},
'dd + dt': {
marginBlockStart: theme.spacing(1),
export const LimitContainer = styled(Box)(({ theme }) => ({
'&:has(*)': {
padding: theme.spacing(4, 6, 0, 6),
},
}));

View File

@ -0,0 +1,113 @@
import type { FormEventHandler } from 'react';
import theme from 'themes/theme';
import {
ConfigButtons,
ProjectDescriptionContainer,
ProjectNameContainer,
StyledForm,
StyledHeader,
StyledInput,
TopGrid,
LimitContainer,
FormActions,
styleIcon,
} from './DialogFormTemplate.styles';
import { Button } from '@mui/material';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import type { IPermissionButtonProps } from 'component/common/PermissionButton/PermissionButton';
type FormProps = {
createButtonProps: IPermissionButtonProps;
description: string;
errors: { [key: string]: string };
handleSubmit: FormEventHandler<HTMLFormElement>;
icon: React.ComponentType;
Limit?: React.ReactNode;
name: string;
onClose: () => void;
configButtons: React.ReactNode;
resource: string;
setDescription: (newDescription: string) => void;
setName: (newName: string) => void;
validateName?: () => void;
};
export const DialogFormTemplate: React.FC<FormProps> = ({
Limit,
handleSubmit,
name,
setName,
description,
setDescription,
errors,
icon,
resource,
onClose,
configButtons,
createButtonProps,
validateName = () => {},
}) => {
const StyledIcon = styleIcon(icon);
return (
<StyledForm onSubmit={handleSubmit}>
<TopGrid>
<StyledIcon aria-hidden='true' />
<StyledHeader variant='h2'>Create {resource}</StyledHeader>
<ProjectNameContainer>
<StyledInput
label={`${resource} name`}
aria-required
value={name}
onChange={(e) => setName(e.target.value)}
error={Boolean(errors.name)}
errorText={errors.name}
onBlur={validateName}
onFocus={() => {
delete errors.name;
}}
autoFocus
InputProps={{
style: { fontSize: theme.typography.h1.fontSize },
}}
InputLabelProps={{
style: { fontSize: theme.typography.h1.fontSize },
}}
data-testid='FORM_NAME_INPUT'
size='medium'
/>
</ProjectNameContainer>
<ProjectDescriptionContainer>
<StyledInput
size='medium'
className='description'
label='Description (optional)'
multiline
maxRows={3}
value={description}
onChange={(e) => setDescription(e.target.value)}
InputProps={{
style: { fontSize: theme.typography.h2.fontSize },
}}
InputLabelProps={{
style: { fontSize: theme.typography.h2.fontSize },
}}
data-testid='FORM_DESCRIPTION_INPUT'
/>
</ProjectDescriptionContainer>
</TopGrid>
<ConfigButtons>{configButtons}</ConfigButtons>
<LimitContainer>{Limit}</LimitContainer>
<FormActions>
<Button onClick={onClose}>Cancel</Button>
<CreateButton
data-testid='FORM_CREATE_BUTTON'
name={resource}
{...createButtonProps}
/>
</FormActions>
</StyledForm>
);
};

View File

@ -1,5 +1,5 @@
import { styled } from '@mui/material';
import { StyledDropdownSearch } from './shared.styles';
import { StyledDropdownSearch } from 'component/common/DialogFormTemplate/ConfigButtons/shared.styles';
export const TableSearchInput = styled(StyledDropdownSearch)({
maxWidth: '30ch',

View File

@ -1,5 +1,4 @@
import { type FC, useState, useMemo } from 'react';
import { ConfigButton, type ConfigButtonProps } from './ConfigButton';
import { InputAdornment } from '@mui/material';
import Search from '@mui/icons-material/Search';
import { ChangeRequestTable } from './ChangeRequestTable';
@ -7,6 +6,10 @@ import {
ScrollContainer,
TableSearchInput,
} from './ChangeRequestTableConfigButton.styles';
import {
ConfigButton,
type ConfigButtonProps,
} from 'component/common/DialogFormTemplate/ConfigButtons/ConfigButton';
type ChangeRequestTableConfigButtonProps = Pick<
ConfigButtonProps,

View File

@ -0,0 +1,14 @@
import { styled } from '@mui/material';
export const StyledDefinitionList = styled('dl')(({ theme }) => ({
dt: {
fontWeight: 'bold',
'&:after': {
content: '":"',
},
},
'dd + dt': {
marginBlockStart: theme.spacing(1),
},
}));

View File

@ -32,11 +32,15 @@ test('Enabled new project button when limits, version and permission allow for i
permissions: [{ permission: CREATE_PROJECT }],
});
const button = await screen.findByText('Create project');
const button = await screen.findByRole('button', {
name: 'Create project',
});
expect(button).toBeDisabled();
await waitFor(async () => {
const button = await screen.findByText('Create project');
const button = await screen.findByRole('button', {
name: 'Create project',
});
expect(button).not.toBeDisabled();
});
});
@ -49,6 +53,8 @@ test('Project limit reached', async () => {
await screen.findByText('You have reached the limit for projects');
const button = await screen.findByText('Create project');
const button = await screen.findByRole('button', {
name: 'Create project',
});
expect(button).toBeDisabled();
});

View File

@ -1,24 +1,33 @@
import { formatUnknownError } from 'utils/formatUnknownError';
import { ReactComponent as ChangeRequestIcon } from 'assets/icons/merge.svg';
import EnvironmentsIcon from '@mui/icons-material/CloudCircle';
import StickinessIcon from '@mui/icons-material/FormatPaint';
import ProjectModeIcon from '@mui/icons-material/Adjust';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import useToast from 'hooks/useToast';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { NewProjectForm } from './NewProjectForm';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import useProjectForm, {
DEFAULT_PROJECT_STICKINESS,
} from '../../hooks/useProjectForm';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { type ReactNode, useState } from 'react';
import { type ReactNode, useState, type FormEvent } from 'react';
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useNavigate } from 'react-router-dom';
import { Button, Dialog, styled } from '@mui/material';
import { Dialog, styled } from '@mui/material';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
import { useUiFlag } from 'hooks/useUiFlag';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { Limit } from 'component/common/Limit/Limit';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { DialogFormTemplate } from 'component/common/DialogFormTemplate/DialogFormTemplate';
import { MultiSelectConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/MultiSelectConfigButton';
import { SingleSelectConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/SingleSelectConfigButton';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import { useStickinessOptions } from 'hooks/useStickinessOptions';
import { ChangeRequestTableConfigButton } from './ConfigButtons/ChangeRequestTableConfigButton';
import { StyledDefinitionList } from './CreateProjectDialog.styles';
interface ICreateProjectDialogProps {
open: boolean;
@ -38,13 +47,59 @@ const StyledDialog = styled(Dialog)(({ theme }) => ({
},
}));
const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN';
const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({
fill: theme.palette.common.white,
stroke: theme.palette.common.white,
}));
const projectModeOptions = [
{ value: 'open', label: 'open' },
{ value: 'protected', label: 'protected' },
{ value: 'private', label: 'private' },
];
const configButtonData = {
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: "A project's collaboration mode defines who should be allowed see your project and create change requests in it.",
additionalTooltipContent: (
<>
<p>The modes and their functions are:</p>
<StyledDefinitionList>
<dt>Open</dt>
<dd>
Anyone can see the project and anyone can create change
requests.
</dd>
<dt>Protected</dt>
<dd>
Anyone can see the project, but only admins and project
members can submit change requests.
</dd>
<dt>Private</dt>
<dd>
Hides the project from users with the "viewer" root role
who are not members of the project. Only project members
and admins can submit change requests.
</dd>
</StyledDefinitionList>
</>
),
},
changeRequests: {
icon: <ChangeRequestIcon />,
text: 'Change requests can be configured per environment and require changes to go through an approval process before being applied.',
},
};
const useProjectLimit = () => {
const resourceLimitsEnabled = useUiFlag('resourceLimits');
const { projects, loading: loadingProjects } = useProjects();
@ -122,7 +177,7 @@ export const CreateProjectDialog = ({
--data-raw '${JSON.stringify(projectPayload, undefined, 2)}'`;
};
const handleSubmit = async (e: Event) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
clearErrors();
const validName = validateName();
@ -159,6 +214,29 @@ export const CreateProjectDialog = ({
loading: loadingLimit,
} = useProjectLimit();
const { isEnterprise } = useUiConfig();
const { environments: allEnvironments } = useEnvironments();
const activeEnvironments = allEnvironments.filter((env) => env.enabled);
const stickinessOptions = useStickinessOptions(projectStickiness);
const numberOfConfiguredChangeRequestEnvironments = Object.keys(
projectChangeRequestConfiguration,
).length;
const changeRequestSelectorLabel =
numberOfConfiguredChangeRequestEnvironments > 1
? `${numberOfConfiguredChangeRequestEnvironments} environments configured`
: 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 (
<StyledDialog open={open} onClose={onClose}>
<FormTemplate
@ -171,28 +249,13 @@ export const CreateProjectDialog = ({
formatApiCode={formatApiCode}
useFixedSidebar
>
<NewProjectForm
errors={errors}
handleSubmit={handleSubmit}
projectId={projectId}
projectEnvironments={projectEnvironments}
setProjectEnvironments={setProjectEnvironments}
projectName={projectName}
projectStickiness={projectStickiness}
projectChangeRequestConfiguration={
projectChangeRequestConfiguration
}
updateProjectChangeRequestConfig={
updateProjectChangeRequestConfig
}
projectMode={projectMode}
setProjectMode={setProjectMode}
setProjectStickiness={setProjectStickiness}
setProjectName={setProjectName}
projectDesc={projectDesc}
setProjectDesc={setProjectDesc}
overrideDocumentation={setDocumentation}
clearDocumentationOverride={clearDocumentationOverride}
<DialogFormTemplate
resource='project'
createButtonProps={{
permission: CREATE_PROJECT,
disabled:
creatingProject || limitReached || loadingLimit,
}}
Limit={
<ConditionallyRender
condition={resourceLimitsEnabled}
@ -205,17 +268,154 @@ export const CreateProjectDialog = ({
}
/>
}
>
<Button onClick={onClose}>Cancel</Button>
<CreateButton
name='project'
permission={CREATE_PROJECT}
disabled={
creatingProject || limitReached || loadingLimit
handleSubmit={handleSubmit}
name={projectName}
setName={setProjectName}
description={projectDesc}
setDescription={setProjectDesc}
errors={errors}
icon={StyledProjectIcon}
onClose={onClose}
configButtons={
<>
<MultiSelectConfigButton
tooltip={{
header: 'Select project environments',
}}
description={configButtonData.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',
labelWidth: `${'all environments'.length}ch`,
icon: <EnvironmentsIcon />,
}}
search={{
label: 'Filter project environments',
placeholder: 'Select project environments',
}}
onOpen={() =>
setDocumentation(
configButtonData.environments,
)
}
onClose={clearDocumentationOverride}
/>
<SingleSelectConfigButton
tooltip={{
header: 'Set default project stickiness',
}}
description={configButtonData.stickiness.text}
options={stickinessOptions.map(
({ key, ...rest }) => ({
value: key,
...rest,
}),
)}
onChange={(value: any) => {
setProjectStickiness(value);
}}
button={{
label: projectStickiness,
icon: <StickinessIcon />,
labelWidth: '12ch',
}}
search={{
label: 'Filter stickiness options',
placeholder: 'Select default stickiness',
}}
onOpen={() =>
setDocumentation(
configButtonData.stickiness,
)
}
onClose={clearDocumentationOverride}
/>
<ConditionallyRender
condition={isEnterprise()}
show={
<SingleSelectConfigButton
tooltip={{
header: 'Set project collaboration mode',
additionalContent:
configButtonData.mode
.additionalTooltipContent,
}}
description={configButtonData.mode.text}
options={projectModeOptions}
onChange={(value: any) => {
setProjectMode(value);
}}
button={{
label: projectMode,
icon: <ProjectModeIcon />,
labelWidth: `${`protected`.length}ch`,
}}
search={{
label: 'Filter project mode options',
placeholder: 'Select project mode',
}}
onOpen={() =>
setDocumentation(
configButtonData.mode,
)
}
onClose={clearDocumentationOverride}
/>
}
/>
<ConditionallyRender
condition={isEnterprise()}
show={
<ChangeRequestTableConfigButton
tooltip={{
header: 'Configure change requests',
}}
description={
configButtonData.changeRequests.text
}
activeEnvironments={
availableChangeRequestEnvironments
}
updateProjectChangeRequestConfiguration={
updateProjectChangeRequestConfig
}
button={{
label: changeRequestSelectorLabel,
icon: <ChangeRequestIcon />,
labelWidth: `${
'nn environments configured'
.length
}ch`,
}}
search={{
label: 'Filter environments',
placeholder: 'Filter environments',
}}
projectChangeRequestConfiguration={
projectChangeRequestConfiguration
}
onOpen={() =>
setDocumentation(
configButtonData.changeRequests,
)
}
onClose={clearDocumentationOverride}
/>
}
/>
</>
}
data-testid={CREATE_PROJECT_BTN}
/>
</NewProjectForm>
</FormTemplate>
</StyledDialog>
);

View File

@ -1,340 +0,0 @@
import type { ProjectMode } from '../../hooks/useProjectEnterpriseSettingsForm';
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 React from 'react';
import type { ReactNode } from 'react';
import theme from 'themes/theme';
import {
FormActions,
OptionButtons,
ProjectDescriptionContainer,
ProjectNameContainer,
StyledDefinitionList,
StyledForm,
StyledHeader,
StyledIcon,
StyledInput,
TopGrid,
} from './NewProjectForm.styles';
import { MultiSelectConfigButton } from './ConfigButtons/MultiSelectConfigButton';
import { SingleSelectConfigButton } from './ConfigButtons/SingleSelectConfigButton';
import { ChangeRequestTableConfigButton } from './ConfigButtons/ChangeRequestTableConfigButton';
import { Box, styled } from '@mui/material';
type FormProps = {
projectId: string;
projectName: string;
projectDesc: string;
projectStickiness: string;
projectMode: string;
projectEnvironments: Set<string>;
projectChangeRequestConfiguration: Record<
string,
{ requiredApprovals: number }
>;
setProjectStickiness: React.Dispatch<React.SetStateAction<string>>;
setProjectEnvironments: (envs: Set<string>) => void;
setProjectName: React.Dispatch<React.SetStateAction<string>>;
setProjectDesc: 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 };
overrideDocumentation: (args: { text: string; icon: ReactNode }) => void;
clearDocumentationOverride: () => void;
children?: React.ReactNode;
Limit?: React.ReactNode;
};
const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT';
const PROJECT_DESCRIPTION_INPUT = 'PROJECT_DESCRIPTION_INPUT';
const projectModeOptions = [
{ value: 'open', label: 'open' },
{ value: 'protected', label: 'protected' },
{ value: 'private', label: 'private' },
];
const configButtonData = {
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: "A project's collaboration mode defines who should be allowed see your project and create change requests in it.",
additionalTooltipContent: (
<>
<p>The modes and their functions are:</p>
<StyledDefinitionList>
<dt>Open</dt>
<dd>
Anyone can see the project and anyone can create change
requests.
</dd>
<dt>Protected</dt>
<dd>
Anyone can see the project, but only admins and project
members can submit change requests.
</dd>
<dt>Private</dt>
<dd>
Hides the project from users with the "viewer" root role
who are not members of the project. Only project members
and admins can submit change requests.
</dd>
</StyledDefinitionList>
</>
),
},
changeRequests: {
icon: <ChangeRequestIcon />,
text: 'Change requests can be configured per environment and require changes to go through an approval process before being applied.',
},
};
const LimitContainer = styled(Box)(({ theme }) => ({
'&:has(*)': {
padding: theme.spacing(4, 6, 0, 6),
},
}));
export const NewProjectForm: React.FC<FormProps> = ({
children,
Limit,
handleSubmit,
projectName,
projectDesc,
projectStickiness,
projectEnvironments,
projectChangeRequestConfiguration,
projectMode,
setProjectMode,
setProjectEnvironments,
setProjectName,
setProjectDesc,
setProjectStickiness,
updateProjectChangeRequestConfig,
errors,
overrideDocumentation,
clearDocumentationOverride,
}) => {
const { isEnterprise } = useUiConfig();
const { environments: allEnvironments } = useEnvironments();
const activeEnvironments = allEnvironments.filter((env) => env.enabled);
const stickinessOptions = useStickinessOptions(projectStickiness);
const handleProjectNameUpdate = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
const input = e.target.value;
setProjectName(input);
};
const numberOfConfiguredChangeRequestEnvironments = Object.keys(
projectChangeRequestConfiguration,
).length;
const changeRequestSelectorLabel =
numberOfConfiguredChangeRequestEnvironments > 1
? `${numberOfConfiguredChangeRequestEnvironments} environments configured`
: 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) => {
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={3}
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>
<MultiSelectConfigButton
tooltip={{ header: 'Select project environments' }}
description={configButtonData.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',
labelWidth: `${'all environments'.length}ch`,
icon: <EnvironmentsIcon />,
}}
search={{
label: 'Filter project environments',
placeholder: 'Select project environments',
}}
onOpen={() =>
overrideDocumentation(configButtonData.environments)
}
onClose={clearDocumentationOverride}
/>
<SingleSelectConfigButton
tooltip={{ header: 'Set default project stickiness' }}
description={configButtonData.stickiness.text}
options={stickinessOptions.map(({ key, ...rest }) => ({
value: key,
...rest,
}))}
onChange={(value: any) => {
setProjectStickiness(value);
}}
button={{
label: projectStickiness,
icon: <StickinessIcon />,
labelWidth: '12ch',
}}
search={{
label: 'Filter stickiness options',
placeholder: 'Select default stickiness',
}}
onOpen={() =>
overrideDocumentation(configButtonData.stickiness)
}
onClose={clearDocumentationOverride}
/>
<ConditionallyRender
condition={isEnterprise()}
show={
<SingleSelectConfigButton
tooltip={{
header: 'Set project collaboration mode',
additionalContent:
configButtonData.mode
.additionalTooltipContent,
}}
description={configButtonData.mode.text}
options={projectModeOptions}
onChange={(value: any) => {
setProjectMode(value);
}}
button={{
label: projectMode,
icon: <ProjectModeIcon />,
labelWidth: `${`protected`.length}ch`,
}}
search={{
label: 'Filter project mode options',
placeholder: 'Select project mode',
}}
onOpen={() =>
overrideDocumentation(configButtonData.mode)
}
onClose={clearDocumentationOverride}
/>
}
/>
<ConditionallyRender
condition={isEnterprise()}
show={
<ChangeRequestTableConfigButton
tooltip={{ header: 'Configure change requests' }}
description={configButtonData.changeRequests.text}
activeEnvironments={
availableChangeRequestEnvironments
}
updateProjectChangeRequestConfiguration={
updateProjectChangeRequestConfig
}
button={{
label: changeRequestSelectorLabel,
icon: <ChangeRequestIcon />,
labelWidth: `${
'nn environments configured'.length
}ch`,
}}
search={{
label: 'Filter environments',
placeholder: 'Filter environments',
}}
projectChangeRequestConfiguration={
projectChangeRequestConfiguration
}
onOpen={() =>
overrideDocumentation(
configButtonData.changeRequests,
)
}
onClose={clearDocumentationOverride}
/>
}
/>
</OptionButtons>
<LimitContainer>{Limit}</LimitContainer>
<FormActions>{children}</FormActions>
</StyledForm>
);
};