mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: project UI rework, move edit and delete buttons deeper (#4195)
This commit is contained in:
		
							parent
							
								
									ec2978b133
								
							
						
					
					
						commit
						a2b06e4222
					
				| @ -1 +1 @@ | ||||
| export const formTemplateSidebarWidth = '27.5rem'; | ||||
| export const formTemplateSidebarWidth = '36%'; | ||||
|  | ||||
| @ -19,12 +19,13 @@ import { formTemplateSidebarWidth } from './FormTemplate.styles'; | ||||
| import { relative } from 'themes/themeStyles'; | ||||
| 
 | ||||
| interface ICreateProps { | ||||
|     title: string; | ||||
|     title?: string; | ||||
|     description: string; | ||||
|     documentationLink: string; | ||||
|     documentationLinkLabel: string; | ||||
|     loading?: boolean; | ||||
|     modal?: boolean; | ||||
|     disablePadding?: boolean; | ||||
|     formatApiCode: () => string; | ||||
| } | ||||
| 
 | ||||
| @ -45,20 +46,22 @@ const StyledContainer = styled('section', { | ||||
| 
 | ||||
| const StyledRelativeDiv = styled('div')(({ theme }) => relative); | ||||
| 
 | ||||
| const StyledFormContent = styled('div')(({ theme }) => ({ | ||||
| const StyledFormContent = styled('div', { | ||||
|     shouldForwardProp: prop => prop !== 'disablePadding', | ||||
| })<{ disablePadding?: boolean }>(({ theme, disablePadding }) => ({ | ||||
|     backgroundColor: theme.palette.background.paper, | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     padding: theme.spacing(6), | ||||
|     flexGrow: 1, | ||||
|     padding: disablePadding ? 0 : theme.spacing(6), | ||||
|     [theme.breakpoints.down('lg')]: { | ||||
|         padding: theme.spacing(4), | ||||
|         padding: disablePadding ? 0 : theme.spacing(4), | ||||
|     }, | ||||
|     [theme.breakpoints.down(1100)]: { | ||||
|         width: '100%', | ||||
|     }, | ||||
|     [theme.breakpoints.down(500)]: { | ||||
|         padding: theme.spacing(4, 2), | ||||
|         padding: disablePadding ? 0 : theme.spacing(4, 2), | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| @ -157,6 +160,7 @@ const FormTemplate: React.FC<ICreateProps> = ({ | ||||
|     loading, | ||||
|     modal, | ||||
|     formatApiCode, | ||||
|     disablePadding, | ||||
| }) => { | ||||
|     const { setToastData } = useToast(); | ||||
|     const smallScreen = useMediaQuery(`(max-width:${1099}px)`); | ||||
| @ -194,13 +198,16 @@ const FormTemplate: React.FC<ICreateProps> = ({ | ||||
|                     </StyledRelativeDiv> | ||||
|                 } | ||||
|             /> | ||||
|             <StyledFormContent> | ||||
|             <StyledFormContent disablePadding={disablePadding}> | ||||
|                 <ConditionallyRender | ||||
|                     condition={loading || false} | ||||
|                     show={<Loader />} | ||||
|                     elseShow={ | ||||
|                         <> | ||||
|                             <StyledTitle>{title}</StyledTitle> | ||||
|                             <ConditionallyRender | ||||
|                                 condition={title !== undefined} | ||||
|                                 show={<StyledTitle>{title}</StyledTitle>} | ||||
|                             /> | ||||
|                             {children} | ||||
|                         </> | ||||
|                     } | ||||
|  | ||||
| @ -13,9 +13,14 @@ import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; | ||||
| import { Button, styled } from '@mui/material'; | ||||
| 
 | ||||
| const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN'; | ||||
| 
 | ||||
| const StyledButton = styled(Button)(({ theme }) => ({ | ||||
|     marginLeft: theme.spacing(3), | ||||
| })); | ||||
| 
 | ||||
| const CreateProject = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const { refetchUser } = useAuthUser(); | ||||
| @ -95,7 +100,6 @@ const CreateProject = () => { | ||||
|             <ProjectForm | ||||
|                 errors={errors} | ||||
|                 handleSubmit={handleSubmit} | ||||
|                 handleCancel={handleCancel} | ||||
|                 projectId={projectId} | ||||
|                 setProjectId={setProjectId} | ||||
|                 projectName={projectName} | ||||
| @ -115,6 +119,7 @@ const CreateProject = () => { | ||||
|                     permission={CREATE_PROJECT} | ||||
|                     data-testid={CREATE_PROJECT_BTN} | ||||
|                 /> | ||||
|                 <StyledButton onClick={handleCancel}>Cancel</StyledButton> | ||||
|             </ProjectForm> | ||||
|         </FormTemplate> | ||||
|     ); | ||||
|  | ||||
| @ -14,13 +14,17 @@ import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { useContext } from 'react'; | ||||
| import AccessContext from 'contexts/AccessContext'; | ||||
| import { Alert } from '@mui/material'; | ||||
| import { Alert, Button, styled } from '@mui/material'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings'; | ||||
| import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; | ||||
| 
 | ||||
| const EDIT_PROJECT_BTN = 'EDIT_PROJECT_BTN'; | ||||
| 
 | ||||
| const StyledButton = styled(Button)(({ theme }) => ({ | ||||
|     marginLeft: theme.spacing(3), | ||||
| })); | ||||
| 
 | ||||
| const EditProject = () => { | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| @ -114,7 +118,6 @@ const EditProject = () => { | ||||
|             <ProjectForm | ||||
|                 errors={errors} | ||||
|                 handleSubmit={handleSubmit} | ||||
|                 handleCancel={handleCancel} | ||||
|                 projectId={projectId} | ||||
|                 setProjectId={setProjectId} | ||||
|                 projectName={projectName} | ||||
| @ -133,7 +136,8 @@ const EditProject = () => { | ||||
|                     permission={UPDATE_PROJECT} | ||||
|                     projectId={projectId} | ||||
|                     data-testid={EDIT_PROJECT_BTN} | ||||
|                 /> | ||||
|                 />{' '} | ||||
|                 <StyledButton onClick={handleCancel}>Cancel</StyledButton> | ||||
|             </ProjectForm> | ||||
|         </FormTemplate> | ||||
|     ); | ||||
|  | ||||
| @ -165,7 +165,10 @@ export const Project = () => { | ||||
|                                 } | ||||
|                             /> | ||||
|                             <ConditionallyRender | ||||
|                                 condition={!isOss()} | ||||
|                                 condition={ | ||||
|                                     !isOss() && | ||||
|                                     !Boolean(uiConfig.flags.newProjectLayout) | ||||
|                                 } | ||||
|                                 show={ | ||||
|                                     <PermissionIconButton | ||||
|                                         permission={UPDATE_PROJECT} | ||||
| @ -184,7 +187,10 @@ export const Project = () => { | ||||
|                                 } | ||||
|                             /> | ||||
|                             <ConditionallyRender | ||||
|                                 condition={!isOss()} | ||||
|                                 condition={ | ||||
|                                     !isOss() && | ||||
|                                     !Boolean(uiConfig.flags.newProjectLayout) | ||||
|                                 } | ||||
|                                 show={ | ||||
|                                     <PermissionIconButton | ||||
|                                         permission={DELETE_PROJECT} | ||||
|  | ||||
| @ -1,37 +0,0 @@ | ||||
| import Input from 'component/common/Input/Input'; | ||||
| import { TextField, Button, styled } from '@mui/material'; | ||||
| 
 | ||||
| export const StyledForm = styled('form')(() => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     height: '100%', | ||||
| })); | ||||
| 
 | ||||
| export const StyledContainer = styled('div')(() => ({ | ||||
|     maxWidth: '400px', | ||||
| })); | ||||
| 
 | ||||
| export const StyledDescription = styled('p')(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(1), | ||||
|     marginRight: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| export const StyledInput = styled(Input)(({ theme }) => ({ | ||||
|     width: '100%', | ||||
|     marginBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| export const StyledTextField = styled(TextField)(({ theme }) => ({ | ||||
|     width: '100%', | ||||
|     marginBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| export const StyledButtonContainer = styled('div')(() => ({ | ||||
|     marginTop: 'auto', | ||||
|     display: 'flex', | ||||
|     justifyContent: 'flex-end', | ||||
| })); | ||||
| 
 | ||||
| export const StyledButton = styled(Button)(({ theme }) => ({ | ||||
|     marginLeft: theme.spacing(3), | ||||
| })); | ||||
| @ -1,20 +1,12 @@ | ||||
| import React from 'react'; | ||||
| import { trim } from 'component/common/util'; | ||||
| import { | ||||
|     StyledButton, | ||||
|     StyledButtonContainer, | ||||
|     StyledContainer, | ||||
|     StyledDescription, | ||||
|     StyledForm, | ||||
|     StyledInput, | ||||
|     StyledTextField, | ||||
| } from './ProjectForm.styles'; | ||||
| import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import Select from 'component/common/select'; | ||||
| import { ProjectMode } from '../hooks/useProjectForm'; | ||||
| import { Box } from '@mui/material'; | ||||
| import { Box, styled, TextField } from '@mui/material'; | ||||
| import { CollaborationModeTooltip } from './CollaborationModeTooltip'; | ||||
| import Input from 'component/common/Input/Input'; | ||||
| 
 | ||||
| interface IProjectForm { | ||||
|     projectId: string; | ||||
| @ -28,7 +20,6 @@ interface IProjectForm { | ||||
|     setProjectName: React.Dispatch<React.SetStateAction<string>>; | ||||
|     setProjectDesc: React.Dispatch<React.SetStateAction<string>>; | ||||
|     handleSubmit: (e: any) => void; | ||||
|     handleCancel: () => void; | ||||
|     errors: { [key: string]: string }; | ||||
|     mode: 'Create' | 'Edit'; | ||||
|     clearErrors: () => void; | ||||
| @ -40,10 +31,41 @@ const PROJECT_ID_INPUT = 'PROJECT_ID_INPUT'; | ||||
| const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT'; | ||||
| const PROJECT_DESCRIPTION_INPUT = 'PROJECT_DESCRIPTION_INPUT'; | ||||
| 
 | ||||
| const StyledForm = styled('form')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     height: '100%', | ||||
|     paddingBottom: theme.spacing(4), | ||||
| })); | ||||
| 
 | ||||
| const StyledContainer = styled('div')(() => ({ | ||||
|     maxWidth: '400px', | ||||
| })); | ||||
| 
 | ||||
| const StyledDescription = styled('p')(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(1), | ||||
|     marginRight: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| const StyledInput = styled(Input)(({ theme }) => ({ | ||||
|     width: '100%', | ||||
|     marginBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const StyledTextField = styled(TextField)(({ theme }) => ({ | ||||
|     width: '100%', | ||||
|     marginBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const StyledButtonContainer = styled('div')(() => ({ | ||||
|     marginTop: 'auto', | ||||
|     display: 'flex', | ||||
|     justifyContent: 'flex-end', | ||||
| })); | ||||
| 
 | ||||
| const ProjectForm: React.FC<IProjectForm> = ({ | ||||
|     children, | ||||
|     handleSubmit, | ||||
|     handleCancel, | ||||
|     projectId, | ||||
|     projectName, | ||||
|     projectDesc, | ||||
| @ -153,10 +175,7 @@ const ProjectForm: React.FC<IProjectForm> = ({ | ||||
|                 </> | ||||
|             </StyledContainer> | ||||
| 
 | ||||
|             <StyledButtonContainer> | ||||
|                 {children} | ||||
|                 <StyledButton onClick={handleCancel}>Cancel</StyledButton> | ||||
|             </StyledButtonContainer> | ||||
|             <StyledButtonContainer>{children}</StyledButtonContainer> | ||||
|         </StyledForm> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -12,12 +12,23 @@ import { ChangeRequestConfiguration } from './ChangeRequestConfiguration/ChangeR | ||||
| import { ProjectApiAccess } from 'component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess'; | ||||
| import { ProjectSegments } from './ProjectSegments/ProjectSegments'; | ||||
| import { ProjectDefaultStrategySettings } from './ProjectDefaultStrategySettings/ProjectDefaultStrategySettings'; | ||||
| import { Settings } from './Settings/Settings'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| 
 | ||||
| export const ProjectSettings = () => { | ||||
|     const location = useLocation(); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const navigate = useNavigate(); | ||||
| 
 | ||||
|     const tabs: ITab[] = [ | ||||
|         ...(uiConfig.flags.newProjectLayout | ||||
|             ? [ | ||||
|                   { | ||||
|                       id: '', | ||||
|                       label: 'Settings', | ||||
|                   }, | ||||
|               ] | ||||
|             : []), | ||||
|         { | ||||
|             id: 'environments', | ||||
|             label: 'Environments', | ||||
| @ -59,6 +70,9 @@ export const ProjectSettings = () => { | ||||
|             onChange={onChange} | ||||
|         > | ||||
|             <Routes> | ||||
|                 {uiConfig.flags.newProjectLayout ? ( | ||||
|                     <Route path="/*" element={<Settings />} /> | ||||
|                 ) : null} | ||||
|                 <Route | ||||
|                     path="environments/*" | ||||
|                     element={<ProjectEnvironmentList />} | ||||
|  | ||||
| @ -0,0 +1,79 @@ | ||||
| import { styled } from '@mui/material'; | ||||
| import { DELETE_PROJECT } from 'component/providers/AccessProvider/permissions'; | ||||
| import PermissionButton from 'component/common/PermissionButton/PermissionButton'; | ||||
| import { DeleteProjectDialogue } from '../../DeleteProject/DeleteProjectDialogue'; | ||||
| import { useState } from 'react'; | ||||
| import { useNavigate } from 'react-router'; | ||||
| 
 | ||||
| const StyledContainer = styled('div')(({ theme }) => ({ | ||||
|     borderTop: `1px solid ${theme.palette.divider}`, | ||||
| })); | ||||
| 
 | ||||
| const StyledTitle = styled('div')(({ theme }) => ({ | ||||
|     paddingTop: theme.spacing(4), | ||||
|     lineHeight: 2, | ||||
| })); | ||||
| 
 | ||||
| const StyledCounter = styled('div')(({ theme }) => ({ | ||||
|     paddingTop: theme.spacing(3), | ||||
| })); | ||||
| 
 | ||||
| const StyledButtonContainer = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     justifyContent: 'flex-end', | ||||
|     paddingTop: theme.spacing(3), | ||||
| })); | ||||
| 
 | ||||
| interface IDeleteProjectProps { | ||||
|     projectId: string; | ||||
|     featureCount: number; | ||||
| } | ||||
| 
 | ||||
| export const DeleteProject = ({ | ||||
|     projectId, | ||||
|     featureCount, | ||||
| }: IDeleteProjectProps) => { | ||||
|     const [showDelDialog, setShowDelDialog] = useState(false); | ||||
|     const navigate = useNavigate(); | ||||
|     return ( | ||||
|         <StyledContainer> | ||||
|             <StyledTitle>Delete project</StyledTitle> | ||||
|             <div> | ||||
|                 Before you can delete a project, you must first archive all the | ||||
|                 feature toggles associated with it. Keep in mind that deleting a | ||||
|                 project will permanently remove all the archived feature | ||||
|                 toggles, and they cannot be recovered once deleted. | ||||
|             </div> | ||||
|             <StyledCounter> | ||||
|                 Currently there are{' '} | ||||
|                 <strong>{featureCount} feature toggles active</strong> | ||||
|             </StyledCounter> | ||||
|             <StyledButtonContainer> | ||||
|                 <PermissionButton | ||||
|                     permission={DELETE_PROJECT} | ||||
|                     disabled={featureCount > 0} | ||||
|                     projectId={projectId} | ||||
|                     onClick={() => { | ||||
|                         setShowDelDialog(true); | ||||
|                     }} | ||||
|                     tooltipProps={{ | ||||
|                         title: 'Delete project', | ||||
|                     }} | ||||
|                     data-loading | ||||
|                 > | ||||
|                     Delete project | ||||
|                 </PermissionButton> | ||||
|             </StyledButtonContainer> | ||||
|             <DeleteProjectDialogue | ||||
|                 project={projectId} | ||||
|                 open={showDelDialog} | ||||
|                 onClose={() => { | ||||
|                     setShowDelDialog(false); | ||||
|                 }} | ||||
|                 onSuccess={() => { | ||||
|                     navigate('/projects'); | ||||
|                 }} | ||||
|             /> | ||||
|         </StyledContainer> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,143 @@ | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import FormTemplate from 'component/common/FormTemplate/FormTemplate'; | ||||
| import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; | ||||
| import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; | ||||
| import useProject from 'hooks/api/getters/useProject/useProject'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { useContext } from 'react'; | ||||
| import AccessContext from 'contexts/AccessContext'; | ||||
| import { Alert } from '@mui/material'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings'; | ||||
| import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; | ||||
| import useProjectForm, { | ||||
|     DEFAULT_PROJECT_STICKINESS, | ||||
| } from '../../hooks/useProjectForm'; | ||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import { DeleteProject } from './DeleteProject'; | ||||
| import PermissionButton from 'component/common/PermissionButton/PermissionButton'; | ||||
| import ProjectForm from '../../ProjectForm/ProjectForm'; | ||||
| 
 | ||||
| const EditProject = () => { | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const id = useRequiredPathParam('projectId'); | ||||
|     const { project } = useProject(id); | ||||
|     const { defaultStickiness } = useDefaultProjectSettings(id); | ||||
|     const navigate = useNavigate(); | ||||
|     const { trackEvent } = usePlausibleTracker(); | ||||
| 
 | ||||
|     const { | ||||
|         projectId, | ||||
|         projectName, | ||||
|         projectDesc, | ||||
|         projectStickiness, | ||||
|         projectMode, | ||||
|         setProjectId, | ||||
|         setProjectName, | ||||
|         setProjectDesc, | ||||
|         setProjectStickiness, | ||||
|         setProjectMode, | ||||
|         getProjectPayload, | ||||
|         clearErrors, | ||||
|         validateProjectId, | ||||
|         validateName, | ||||
|         errors, | ||||
|     } = useProjectForm( | ||||
|         id, | ||||
|         project.name, | ||||
|         project.description, | ||||
|         defaultStickiness, | ||||
|         project.mode | ||||
|     ); | ||||
| 
 | ||||
|     const formatApiCode = () => { | ||||
|         return `curl --location --request PUT '${ | ||||
|             uiConfig.unleashUrl | ||||
|         }/api/admin/projects/${id}' \\ | ||||
| --header 'Authorization: INSERT_API_KEY' \\ | ||||
| --header 'Content-Type: application/json' \\ | ||||
| --data-raw '${JSON.stringify(getProjectPayload(), undefined, 2)}'`;
 | ||||
|     }; | ||||
| 
 | ||||
|     const { editProject, loading } = useProjectApi(); | ||||
| 
 | ||||
|     const handleSubmit = async (e: Event) => { | ||||
|         e.preventDefault(); | ||||
|         const payload = getProjectPayload(); | ||||
| 
 | ||||
|         const validName = validateName(); | ||||
| 
 | ||||
|         if (validName) { | ||||
|             try { | ||||
|                 await editProject(id, payload); | ||||
|                 setToastData({ | ||||
|                     title: 'Project information updated', | ||||
|                     type: 'success', | ||||
|                 }); | ||||
|                 if (projectStickiness !== DEFAULT_PROJECT_STICKINESS) { | ||||
|                     trackEvent('project_stickiness_set'); | ||||
|                 } | ||||
|             } catch (error: unknown) { | ||||
|                 setToastApiError(formatUnknownError(error)); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const accessDeniedAlert = !hasAccess(UPDATE_PROJECT, projectId) && ( | ||||
|         <Alert severity="error" sx={{ mb: 4 }}> | ||||
|             You do not have the required permissions to edit this project. | ||||
|         </Alert> | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <FormTemplate | ||||
|             loading={loading} | ||||
|             disablePadding={true} | ||||
|             description="Projects allows you to group feature toggles together in the management UI." | ||||
|             documentationLink="https://docs.getunleash.io/reference/projects" | ||||
|             documentationLinkLabel="Projects documentation" | ||||
|             formatApiCode={formatApiCode} | ||||
|         > | ||||
|             {accessDeniedAlert} | ||||
|             <PageContent header={<PageHeader title="Settings" />}> | ||||
|                 <ProjectForm | ||||
|                     errors={errors} | ||||
|                     handleSubmit={handleSubmit} | ||||
|                     projectId={projectId} | ||||
|                     setProjectId={setProjectId} | ||||
|                     projectName={projectName} | ||||
|                     projectMode={projectMode} | ||||
|                     setProjectName={setProjectName} | ||||
|                     projectStickiness={projectStickiness} | ||||
|                     setProjectStickiness={setProjectStickiness} | ||||
|                     setProjectMode={setProjectMode} | ||||
|                     projectDesc={projectDesc} | ||||
|                     mode="Edit" | ||||
|                     setProjectDesc={setProjectDesc} | ||||
|                     clearErrors={clearErrors} | ||||
|                     validateProjectId={validateProjectId} | ||||
|                 > | ||||
|                     <PermissionButton | ||||
|                         type="submit" | ||||
|                         permission={UPDATE_PROJECT} | ||||
|                         projectId={projectId} | ||||
|                     > | ||||
|                         Save changes | ||||
|                     </PermissionButton> | ||||
|                 </ProjectForm> | ||||
|                 <DeleteProject | ||||
|                     projectId={projectId} | ||||
|                     featureCount={project.features.length} | ||||
|                 /> | ||||
|             </PageContent> | ||||
|         </FormTemplate> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default EditProject; | ||||
| @ -0,0 +1,29 @@ | ||||
| import { useContext } from 'react'; | ||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { Alert } from '@mui/material'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import AccessContext from 'contexts/AccessContext'; | ||||
| import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; | ||||
| import EditProject from './EditProject'; | ||||
| 
 | ||||
| export const Settings = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const projectName = useProjectNameOrId(projectId); | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     usePageTitle(`Project configuration – ${projectName}`); | ||||
| 
 | ||||
|     if (!hasAccess(UPDATE_PROJECT, projectId)) { | ||||
|         return ( | ||||
|             <PageContent header={<PageHeader title="Access" />}> | ||||
|                 <Alert severity="error"> | ||||
|                     You need project owner permissions to access this section. | ||||
|                 </Alert> | ||||
|             </PageContent> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     return <EditProject />; | ||||
| }; | ||||
| @ -1,29 +0,0 @@ | ||||
| import { makeStyles } from 'tss-react/mui'; | ||||
| 
 | ||||
| export const useStyles = makeStyles()(theme => ({ | ||||
|     pageContent: { | ||||
|         minHeight: '200px', | ||||
|     }, | ||||
|     divider: { | ||||
|         height: '1px', | ||||
|         position: 'relative', | ||||
|         left: 0, | ||||
|         right: 0, | ||||
|         backgroundColor: theme.palette.divider, | ||||
|         margin: theme.spacing(4, -4, 3), | ||||
|     }, | ||||
|     inputLabel: { | ||||
|         backgroundColor: theme.palette.background.paper, | ||||
|     }, | ||||
|     roleName: { | ||||
|         fontWeight: 'bold', | ||||
|         padding: '5px 0px', | ||||
|     }, | ||||
|     menuItem: { | ||||
|         width: '340px', | ||||
|         whiteSpace: 'normal', | ||||
|     }, | ||||
|     projectRoleSelect: { | ||||
|         minWidth: '150px', | ||||
|     }, | ||||
| })); | ||||
| @ -49,7 +49,7 @@ export const ProjectCard = ({ | ||||
|     isFavorite = false, | ||||
| }: IProjectCardProps) => { | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const { isOss } = useUiConfig(); | ||||
|     const { isOss, uiConfig } = useUiConfig(); | ||||
|     const [anchorEl, setAnchorEl] = useState<Element | null>(null); | ||||
|     const [showDelDialog, setShowDelDialog] = useState(false); | ||||
|     const navigate = useNavigate(); | ||||
| @ -117,24 +117,34 @@ export const ProjectCard = ({ | ||||
|                     <MenuItem | ||||
|                         onClick={e => { | ||||
|                             e.preventDefault(); | ||||
|                             navigate(getProjectEditPath(id)); | ||||
|                             navigate( | ||||
|                                 getProjectEditPath( | ||||
|                                     id, | ||||
|                                     Boolean(uiConfig.flags.newProjectLayout) | ||||
|                                 ) | ||||
|                             ); | ||||
|                         }} | ||||
|                     > | ||||
|                         <StyledEditIcon /> | ||||
|                         Edit project | ||||
|                     </MenuItem> | ||||
|                     <MenuItem | ||||
|                         onClick={e => { | ||||
|                             e.preventDefault(); | ||||
|                             setShowDelDialog(true); | ||||
|                         }} | ||||
|                         disabled={!canDeleteProject} | ||||
|                     > | ||||
|                         <StyledDeleteIcon /> | ||||
|                         {id === DEFAULT_PROJECT_ID && !canDeleteProject | ||||
|                             ? "You can't delete the default project" | ||||
|                             : 'Delete project'} | ||||
|                     </MenuItem> | ||||
|                     <ConditionallyRender | ||||
|                         condition={!Boolean(uiConfig.flags.newProjectLayout)} | ||||
|                         show={ | ||||
|                             <MenuItem | ||||
|                                 onClick={e => { | ||||
|                                     e.preventDefault(); | ||||
|                                     setShowDelDialog(true); | ||||
|                                 }} | ||||
|                                 disabled={!canDeleteProject} | ||||
|                             > | ||||
|                                 <StyledDeleteIcon /> | ||||
|                                 {id === DEFAULT_PROJECT_ID && !canDeleteProject | ||||
|                                     ? "You can't delete the default project" | ||||
|                                     : 'Delete project'} | ||||
|                             </MenuItem> | ||||
|                         } | ||||
|                     /> | ||||
|                 </Menu> | ||||
|             </StyledDivHeader> | ||||
|             <div data-loading> | ||||
|  | ||||
| @ -53,6 +53,7 @@ export interface IFlags { | ||||
|     advancedPlayground?: boolean; | ||||
|     customRootRoles?: boolean; | ||||
|     strategySplittedButton?: boolean; | ||||
|     newProjectLayout?: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface IVersionInfo { | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| import useUiConfig from '../hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| 
 | ||||
| export const getTogglePath = (projectId: string, featureToggleName: string) => { | ||||
|     return `/projects/${projectId}/features/${featureToggleName}`; | ||||
| }; | ||||
| @ -23,6 +25,11 @@ export const getCreateTogglePath = ( | ||||
|     return path; | ||||
| }; | ||||
| 
 | ||||
| export const getProjectEditPath = (projectId: string) => { | ||||
|     return `/projects/${projectId}/edit`; | ||||
| export const getProjectEditPath = ( | ||||
|     projectId: string, | ||||
|     newProjectPath: boolean | ||||
| ) => { | ||||
|     return newProjectPath | ||||
|         ? `/projects/${projectId}/settings` | ||||
|         : `/projects/${projectId}/edit`; | ||||
| }; | ||||
|  | ||||
| @ -89,6 +89,7 @@ exports[`should create default config 1`] = ` | ||||
|         }, | ||||
|       }, | ||||
|       "migrationLock": false, | ||||
|       "newProjectLayout": false, | ||||
|       "personalAccessTokensKillSwitch": false, | ||||
|       "proPlanAutoCharge": false, | ||||
|       "responseTimeWithAppNameKillSwitch": false, | ||||
| @ -121,6 +122,7 @@ exports[`should create default config 1`] = ` | ||||
|         }, | ||||
|       }, | ||||
|       "migrationLock": false, | ||||
|       "newProjectLayout": false, | ||||
|       "personalAccessTokensKillSwitch": false, | ||||
|       "proPlanAutoCharge": false, | ||||
|       "responseTimeWithAppNameKillSwitch": false, | ||||
|  | ||||
| @ -23,7 +23,8 @@ export type IFlagKey = | ||||
|     | 'disableNotifications' | ||||
|     | 'advancedPlayground' | ||||
|     | 'customRootRoles' | ||||
|     | 'strategySplittedButton'; | ||||
|     | 'strategySplittedButton' | ||||
|     | 'newProjectLayout'; | ||||
| 
 | ||||
| export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; | ||||
| 
 | ||||
| @ -108,6 +109,10 @@ const flags: IFlags = { | ||||
|         process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES, | ||||
|         false, | ||||
|     ), | ||||
|     newProjectLayout: parseEnvVarBoolean( | ||||
|         process.env.UNLEASH_EXPERIMENTAL_NEW_PROJECT_LAYOUT, | ||||
|         false, | ||||
|     ), | ||||
| }; | ||||
| 
 | ||||
| export const defaultExperimentalOptions: IExperimentalOptions = { | ||||
|  | ||||
| @ -40,6 +40,7 @@ process.nextTick(async () => { | ||||
|                         segmentContextFieldUsage: true, | ||||
|                         advancedPlayground: true, | ||||
|                         strategySplittedButton: true, | ||||
|                         newProjectLayout: true, | ||||
|                     }, | ||||
|                 }, | ||||
|                 authentication: { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user