mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: create and edit release plan template milestones (#8768)
This commit is contained in:
		
							parent
							
								
									82e752be45
								
							
						
					
					
						commit
						8935a01d90
					
				| @ -1,10 +1,9 @@ | ||||
| import FormTemplate from 'component/common/FormTemplate/FormTemplate'; | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { Button, styled } from '@mui/material'; | ||||
| import { TemplateForm } from './TemplateForm'; | ||||
| import { useTemplateForm } from '../hooks/useTemplateForm'; | ||||
| import { CreateButton } from 'component/common/CreateButton/CreateButton'; | ||||
| import { ADMIN } from '@server/types/permissions'; | ||||
| import { RELEASE_PLAN_TEMPLATE_CREATE } from '@server/types/permissions'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| import useReleasePlanTemplatesApi from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi'; | ||||
| @ -13,12 +12,6 @@ import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| 
 | ||||
| const StyledForm = styled('form')(() => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     height: '100%', | ||||
| })); | ||||
| 
 | ||||
| const StyledButtonContainer = styled('div')(() => ({ | ||||
|     marginTop: 'auto', | ||||
|     display: 'flex', | ||||
| @ -31,15 +24,17 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({ | ||||
| 
 | ||||
| export const CreateReleasePlanTemplate = () => { | ||||
|     const releasePlansEnabled = useUiFlag('releasePlans'); | ||||
|     usePageTitle('Create release plan template'); | ||||
|     const { setToastApiError, setToastData } = useToast(); | ||||
|     const navigate = useNavigate(); | ||||
|     const { createReleasePlanTemplate } = useReleasePlanTemplatesApi(); | ||||
|     usePageTitle('Create release plan template'); | ||||
|     const { | ||||
|         name, | ||||
|         setName, | ||||
|         description, | ||||
|         setDescription, | ||||
|         milestones, | ||||
|         setMilestones, | ||||
|         errors, | ||||
|         clearErrors, | ||||
|         validate, | ||||
| @ -57,7 +52,10 @@ export const CreateReleasePlanTemplate = () => { | ||||
|         if (isValid) { | ||||
|             const payload = getTemplatePayload(); | ||||
|             try { | ||||
|                 const template = await createReleasePlanTemplate(payload); | ||||
|                 const template = await createReleasePlanTemplate({ | ||||
|                     ...payload, | ||||
|                     milestones, | ||||
|                 }); | ||||
|                 scrollToTop(); | ||||
|                 setToastData({ | ||||
|                     type: 'success', | ||||
| @ -75,28 +73,28 @@ export const CreateReleasePlanTemplate = () => { | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <FormTemplate | ||||
|                 title='Create release plan template' | ||||
|                 description='Create a release plan template to make it easier for you and your team to release features.' | ||||
|             > | ||||
|                 <StyledForm onSubmit={handleSubmit}> | ||||
|                     <TemplateForm | ||||
|                         name={name} | ||||
|                         setName={setName} | ||||
|                         description={description} | ||||
|                         setDescription={setDescription} | ||||
|                         errors={errors} | ||||
|                         clearErrors={clearErrors} | ||||
|                     /> | ||||
|                     <StyledButtonContainer> | ||||
|                         <CreateButton name='template' permission={ADMIN} /> | ||||
|                         <StyledCancelButton onClick={handleCancel}> | ||||
|                             Cancel | ||||
|                         </StyledCancelButton> | ||||
|                     </StyledButtonContainer> | ||||
|                 </StyledForm> | ||||
|             </FormTemplate> | ||||
|         </> | ||||
|         <TemplateForm | ||||
|             name={name} | ||||
|             setName={setName} | ||||
|             description={description} | ||||
|             setDescription={setDescription} | ||||
|             milestones={milestones} | ||||
|             setMilestones={setMilestones} | ||||
|             errors={errors} | ||||
|             clearErrors={clearErrors} | ||||
|             formTitle='Create release plan template' | ||||
|             formDescription='Create a release plan template to make it easier for you and your team to release features.' | ||||
|             handleSubmit={handleSubmit} | ||||
|         > | ||||
|             <StyledButtonContainer> | ||||
|                 <CreateButton | ||||
|                     name='template' | ||||
|                     permission={RELEASE_PLAN_TEMPLATE_CREATE} | ||||
|                 /> | ||||
|                 <StyledCancelButton onClick={handleCancel}> | ||||
|                     Cancel | ||||
|                 </StyledCancelButton> | ||||
|             </StyledButtonContainer> | ||||
|         </TemplateForm> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -2,50 +2,16 @@ import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { useReleasePlanTemplate } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplate'; | ||||
| import FormTemplate from 'component/common/FormTemplate/FormTemplate'; | ||||
| import { useTemplateForm } from '../hooks/useTemplateForm'; | ||||
| import { TemplateForm } from './TemplateForm'; | ||||
| import { Box, Button, Card, styled } from '@mui/material'; | ||||
| import { Button, styled } from '@mui/material'; | ||||
| import { UpdateButton } from 'component/common/UpdateButton/UpdateButton'; | ||||
| import { ADMIN } from '@server/types/permissions'; | ||||
| import { RELEASE_PLAN_TEMPLATE_UPDATE } from '@server/types/permissions'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import useReleasePlanTemplatesApi from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi'; | ||||
| 
 | ||||
| const StyledForm = styled('form')(() => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     height: '100%', | ||||
| })); | ||||
| 
 | ||||
| const StyledMilestoneCard = styled(Card)(({ theme }) => ({ | ||||
|     marginTop: theme.spacing(2), | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     justifyContent: 'space-between', | ||||
|     boxShadow: 'none', | ||||
|     border: `1px solid ${theme.palette.divider}`, | ||||
|     [theme.breakpoints.down('sm')]: { | ||||
|         justifyContent: 'center', | ||||
|     }, | ||||
|     transition: 'background-color 0.2s ease-in-out', | ||||
|     backgroundColor: theme.palette.background.default, | ||||
|     '&:hover': { | ||||
|         backgroundColor: theme.palette.neutral.light, | ||||
|     }, | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
| })); | ||||
| 
 | ||||
| const StyledMilestoneCardBody = styled(Box)(({ theme }) => ({ | ||||
|     padding: theme.spacing(3, 2), | ||||
| })); | ||||
| 
 | ||||
| const StyledMilestoneCardTitle = styled('span')(({ theme }) => ({ | ||||
|     fontWeight: theme.fontWeight.bold, | ||||
|     fontSize: theme.fontSizes.bodySize, | ||||
| })); | ||||
| 
 | ||||
| const StyledButtonContainer = styled('div')(() => ({ | ||||
|     marginTop: 'auto', | ||||
|     display: 'flex', | ||||
| @ -63,7 +29,7 @@ export const EditReleasePlanTemplate = () => { | ||||
|         useReleasePlanTemplate(templateId); | ||||
|     usePageTitle(`Edit template: ${template.name}`); | ||||
|     const navigate = useNavigate(); | ||||
|     const { setToastApiError } = useToast(); | ||||
|     const { setToastApiError, setToastData } = useToast(); | ||||
|     const { updateReleasePlanTemplate } = useReleasePlanTemplatesApi(); | ||||
|     const { | ||||
|         name, | ||||
| @ -72,9 +38,15 @@ export const EditReleasePlanTemplate = () => { | ||||
|         setDescription, | ||||
|         errors, | ||||
|         clearErrors, | ||||
|         milestones, | ||||
|         setMilestones, | ||||
|         validate, | ||||
|         getTemplatePayload, | ||||
|     } = useTemplateForm(template.name, template.description); | ||||
|     } = useTemplateForm( | ||||
|         template.name, | ||||
|         template.description, | ||||
|         template.milestones, | ||||
|     ); | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate('/release-management'); | ||||
| @ -89,9 +61,13 @@ export const EditReleasePlanTemplate = () => { | ||||
|                 await updateReleasePlanTemplate({ | ||||
|                     ...payload, | ||||
|                     id: templateId, | ||||
|                     milestones: template.milestones, | ||||
|                     milestones, | ||||
|                 }); | ||||
|                 await refetch(); | ||||
|                 setToastData({ | ||||
|                     type: 'success', | ||||
|                     title: 'Release plan template updated', | ||||
|                 }); | ||||
|                 navigate('/release-management'); | ||||
|             } catch (error: unknown) { | ||||
|                 setToastApiError(formatUnknownError(error)); | ||||
|             } | ||||
| @ -103,38 +79,29 @@ export const EditReleasePlanTemplate = () => { | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <FormTemplate | ||||
|                 title={`Edit template ${template.name}`} | ||||
|                 description='Edit a release plan template that makes it easier for you and your team to release features.' | ||||
|             > | ||||
|                 <StyledForm onSubmit={handleSubmit}> | ||||
|                     <TemplateForm | ||||
|                         name={name} | ||||
|                         setName={setName} | ||||
|                         description={description} | ||||
|                         setDescription={setDescription} | ||||
|                         errors={errors} | ||||
|                         clearErrors={clearErrors} | ||||
|                     /> | ||||
| 
 | ||||
|                     {template.milestones.map((milestone) => ( | ||||
|                         <StyledMilestoneCard key={milestone.id}> | ||||
|                             <StyledMilestoneCardBody> | ||||
|                                 <StyledMilestoneCardTitle> | ||||
|                                     {milestone.name} | ||||
|                                 </StyledMilestoneCardTitle> | ||||
|                             </StyledMilestoneCardBody> | ||||
|                         </StyledMilestoneCard> | ||||
|                     ))} | ||||
|                     <StyledButtonContainer> | ||||
|                         <UpdateButton name='template' permission={ADMIN} /> | ||||
|                         <StyledCancelButton onClick={handleCancel}> | ||||
|                             Cancel | ||||
|                         </StyledCancelButton> | ||||
|                     </StyledButtonContainer> | ||||
|                 </StyledForm> | ||||
|             </FormTemplate> | ||||
|         </> | ||||
|         <TemplateForm | ||||
|             name={name} | ||||
|             setName={setName} | ||||
|             description={description} | ||||
|             setDescription={setDescription} | ||||
|             milestones={milestones} | ||||
|             setMilestones={setMilestones} | ||||
|             errors={errors} | ||||
|             clearErrors={clearErrors} | ||||
|             formTitle={`Edit template ${template.name}`} | ||||
|             formDescription='Edit a release plan template that makes it easier for you and your team to release features.' | ||||
|             handleSubmit={handleSubmit} | ||||
|             loading={loading} | ||||
|         > | ||||
|             <StyledButtonContainer> | ||||
|                 <UpdateButton | ||||
|                     name='template' | ||||
|                     permission={RELEASE_PLAN_TEMPLATE_UPDATE} | ||||
|                 /> | ||||
|                 <StyledCancelButton onClick={handleCancel}> | ||||
|                     Cancel | ||||
|                 </StyledCancelButton> | ||||
|             </StyledButtonContainer> | ||||
|         </TemplateForm> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -0,0 +1,123 @@ | ||||
| import Input from 'component/common/Input/Input'; | ||||
| import { Box, Button, Card, Grid, styled } from '@mui/material'; | ||||
| import Edit from '@mui/icons-material/Edit'; | ||||
| import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans'; | ||||
| import { useState } from 'react'; | ||||
| 
 | ||||
| const StyledEditIcon = styled(Edit)(({ theme }) => ({ | ||||
|     cursor: 'pointer', | ||||
|     marginTop: theme.spacing(-0.25), | ||||
|     marginLeft: theme.spacing(0.5), | ||||
|     height: theme.spacing(2.5), | ||||
|     width: theme.spacing(2.5), | ||||
|     color: theme.palette.text.secondary, | ||||
| })); | ||||
| 
 | ||||
| const StyledMilestoneCard = styled(Card)(({ theme }) => ({ | ||||
|     marginTop: theme.spacing(2), | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     justifyContent: 'space-between', | ||||
|     boxShadow: 'none', | ||||
|     border: `1px solid ${theme.palette.divider}`, | ||||
|     [theme.breakpoints.down('sm')]: { | ||||
|         justifyContent: 'center', | ||||
|     }, | ||||
|     transition: 'background-color 0.2s ease-in-out', | ||||
|     backgroundColor: theme.palette.background.default, | ||||
|     '&:hover': { | ||||
|         backgroundColor: theme.palette.neutral.light, | ||||
|     }, | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
| })); | ||||
| 
 | ||||
| const StyledMilestoneCardBody = styled(Box)(({ theme }) => ({ | ||||
|     padding: theme.spacing(2, 2), | ||||
| })); | ||||
| 
 | ||||
| const StyledGridItem = styled(Grid)(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     alignItems: 'center', | ||||
| })); | ||||
| 
 | ||||
| const StyledInput = styled(Input)(({ theme }) => ({ | ||||
|     width: '100%', | ||||
| })); | ||||
| 
 | ||||
| const StyledMilestoneCardTitle = styled('span')(({ theme }) => ({ | ||||
|     fontWeight: theme.fontWeight.bold, | ||||
|     fontSize: theme.fontSizes.bodySize, | ||||
| })); | ||||
| 
 | ||||
| interface IMilestoneCardProps { | ||||
|     index: number; | ||||
|     milestone: IReleasePlanMilestonePayload; | ||||
|     milestoneNameChanged: (index: number, name: string) => void; | ||||
|     showAddStrategyDialog: (index: number) => void; | ||||
|     errors: { [key: string]: string }; | ||||
|     clearErrors: () => void; | ||||
| } | ||||
| 
 | ||||
| export const MilestoneCard = ({ | ||||
|     index, | ||||
|     milestone, | ||||
|     milestoneNameChanged, | ||||
|     showAddStrategyDialog, | ||||
|     errors, | ||||
|     clearErrors, | ||||
| }: IMilestoneCardProps) => { | ||||
|     const [editMode, setEditMode] = useState(false); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledMilestoneCard> | ||||
|             <StyledMilestoneCardBody> | ||||
|                 <Grid container> | ||||
|                     <StyledGridItem item xs={10} md={10}> | ||||
|                         {editMode && ( | ||||
|                             <StyledInput | ||||
|                                 label='' | ||||
|                                 value={milestone.name} | ||||
|                                 onChange={(e) => | ||||
|                                     milestoneNameChanged(index, e.target.value) | ||||
|                                 } | ||||
|                                 error={Boolean(errors?.name)} | ||||
|                                 errorText={errors?.name} | ||||
|                                 onFocus={() => clearErrors()} | ||||
|                                 onBlur={() => setEditMode(false)} | ||||
|                                 autoFocus | ||||
|                                 onKeyDownCapture={(e) => { | ||||
|                                     if (e.code === 'Enter') { | ||||
|                                         e.preventDefault(); | ||||
|                                         e.stopPropagation(); | ||||
|                                         setEditMode(false); | ||||
|                                     } | ||||
|                                 }} | ||||
|                             /> | ||||
|                         )} | ||||
|                         {!editMode && ( | ||||
|                             <> | ||||
|                                 <StyledMilestoneCardTitle | ||||
|                                     onClick={() => setEditMode(true)} | ||||
|                                 > | ||||
|                                     {milestone.name} | ||||
|                                 </StyledMilestoneCardTitle> | ||||
|                                 <StyledEditIcon | ||||
|                                     onClick={() => setEditMode(true)} | ||||
|                                 /> | ||||
|                             </> | ||||
|                         )} | ||||
|                     </StyledGridItem> | ||||
|                     <Grid item xs={2} md={2}> | ||||
|                         <Button | ||||
|                             variant='outlined' | ||||
|                             color='primary' | ||||
|                             onClick={() => showAddStrategyDialog(index)} | ||||
|                         > | ||||
|                             Add strategy | ||||
|                         </Button> | ||||
|                     </Grid> | ||||
|                 </Grid> | ||||
|             </StyledMilestoneCardBody> | ||||
|         </StyledMilestoneCard> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,72 @@ | ||||
| import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans'; | ||||
| import { MilestoneCard } from './MilestoneCard'; | ||||
| import { styled } from '@mui/material'; | ||||
| import { Button } from '@mui/material'; | ||||
| import Add from '@mui/icons-material/Add'; | ||||
| 
 | ||||
| interface IMilestoneListProps { | ||||
|     milestones: IReleasePlanMilestonePayload[]; | ||||
|     setMilestones: React.Dispatch< | ||||
|         React.SetStateAction<IReleasePlanMilestonePayload[]> | ||||
|     >; | ||||
|     setAddStrategyOpen: (open: boolean) => void; | ||||
|     errors: { [key: string]: string }; | ||||
|     clearErrors: () => void; | ||||
| } | ||||
| 
 | ||||
| const StyledAddMilestoneButton = styled(Button)(({ theme }) => ({ | ||||
|     marginTop: theme.spacing(1), | ||||
|     maxWidth: theme.spacing(20), | ||||
| })); | ||||
| 
 | ||||
| export const MilestoneList = ({ | ||||
|     milestones, | ||||
|     setMilestones, | ||||
|     setAddStrategyOpen, | ||||
|     errors, | ||||
|     clearErrors, | ||||
| }: IMilestoneListProps) => { | ||||
|     const showAddStrategyDialog = (index: number) => { | ||||
|         setAddStrategyOpen(true); | ||||
|     }; | ||||
| 
 | ||||
|     const milestoneNameChanged = (index: number, name: string) => { | ||||
|         setMilestones((prev) => | ||||
|             prev.map((milestone, i) => | ||||
|                 i === index ? { ...milestone, name } : milestone, | ||||
|             ), | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             {milestones.map((milestone, index) => ( | ||||
|                 <MilestoneCard | ||||
|                     key={`milestone_${index.toString()}`} | ||||
|                     index={index} | ||||
|                     milestone={milestone} | ||||
|                     milestoneNameChanged={milestoneNameChanged} | ||||
|                     showAddStrategyDialog={showAddStrategyDialog} | ||||
|                     errors={errors} | ||||
|                     clearErrors={clearErrors} | ||||
|                 /> | ||||
|             ))} | ||||
|             <StyledAddMilestoneButton | ||||
|                 variant='text' | ||||
|                 color='primary' | ||||
|                 startIcon={<Add />} | ||||
|                 onClick={() => | ||||
|                     setMilestones((prev) => [ | ||||
|                         ...prev, | ||||
|                         { | ||||
|                             name: `Milestone ${prev.length + 1}`, | ||||
|                             sortOrder: prev.length, | ||||
|                         }, | ||||
|                     ]) | ||||
|                 } | ||||
|             > | ||||
|                 Add milestone | ||||
|             </StyledAddMilestoneButton> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,33 @@ | ||||
| import { Button, styled } from '@mui/material'; | ||||
| import FormTemplate from 'component/common/FormTemplate/FormTemplate'; | ||||
| 
 | ||||
| const StyledCancelButton = styled(Button)(({ theme }) => ({ | ||||
|     marginLeft: theme.spacing(3), | ||||
| })); | ||||
| 
 | ||||
| const StyledButtonContainer = styled('div')(() => ({ | ||||
|     marginTop: 'auto', | ||||
|     display: 'flex', | ||||
|     justifyContent: 'flex-end', | ||||
| })); | ||||
| 
 | ||||
| interface IReleasePlanTemplateAddStrategyFormProps { | ||||
|     onCancel: () => void; | ||||
| } | ||||
| 
 | ||||
| export const ReleasePlanTemplateAddStrategyForm = ({ | ||||
|     onCancel, | ||||
| }: IReleasePlanTemplateAddStrategyFormProps) => { | ||||
|     return ( | ||||
|         <FormTemplate | ||||
|             modal | ||||
|             description='Add a strategy to your release plan template.' | ||||
|         > | ||||
|             <StyledButtonContainer> | ||||
|                 <StyledCancelButton onClick={onCancel}> | ||||
|                     Cancel | ||||
|                 </StyledCancelButton> | ||||
|             </StyledButtonContainer> | ||||
|         </FormTemplate> | ||||
|     ); | ||||
| }; | ||||
| @ -1,5 +1,12 @@ | ||||
| import Input from 'component/common/Input/Input'; | ||||
| import { styled } from '@mui/material'; | ||||
| import { MilestoneList } from './MilestoneList'; | ||||
| import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans'; | ||||
| import FormTemplate from 'component/common/FormTemplate/FormTemplate'; | ||||
| import ReleaseTemplateIcon from '@mui/icons-material/DashboardOutlined'; | ||||
| import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; | ||||
| import { useState } from 'react'; | ||||
| import { ReleasePlanTemplateAddStrategyForm } from './ReleasePlanTemplateAddStrategyForm'; | ||||
| 
 | ||||
| const StyledInputDescription = styled('p')(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(1), | ||||
| @ -10,48 +17,98 @@ const StyledInput = styled(Input)(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| interface ITemplateForm { | ||||
| const StyledForm = styled('form')(() => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     height: '100%', | ||||
| })); | ||||
| 
 | ||||
| interface ITemplateFormProps { | ||||
|     name: string; | ||||
|     setName: React.Dispatch<React.SetStateAction<string>>; | ||||
|     description: string; | ||||
|     setDescription: React.Dispatch<React.SetStateAction<string>>; | ||||
|     milestones: IReleasePlanMilestonePayload[]; | ||||
|     setMilestones: React.Dispatch< | ||||
|         React.SetStateAction<IReleasePlanMilestonePayload[]> | ||||
|     >; | ||||
|     errors: { [key: string]: string }; | ||||
|     clearErrors: () => void; | ||||
|     formTitle: string; | ||||
|     formDescription: string; | ||||
|     handleSubmit: (e: React.FormEvent) => void; | ||||
|     loading?: boolean; | ||||
|     children?: React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| export const TemplateForm: React.FC<ITemplateForm> = ({ | ||||
| export const TemplateForm: React.FC<ITemplateFormProps> = ({ | ||||
|     name, | ||||
|     setName, | ||||
|     description, | ||||
|     setDescription, | ||||
|     milestones, | ||||
|     setMilestones, | ||||
|     errors, | ||||
|     clearErrors, | ||||
|     formTitle, | ||||
|     formDescription, | ||||
|     handleSubmit, | ||||
|     children, | ||||
| }) => { | ||||
|     const [addStrategyOpen, setAddStrategyOpen] = useState(false); | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <StyledInputDescription> | ||||
|                 What would you like to call your template? | ||||
|             </StyledInputDescription> | ||||
|             <StyledInput | ||||
|                 label='Template name' | ||||
|                 value={name} | ||||
|                 onChange={(e) => setName(e.target.value)} | ||||
|                 error={Boolean(errors.name)} | ||||
|                 errorText={errors.name} | ||||
|                 onFocus={() => clearErrors()} | ||||
|                 autoFocus | ||||
|             /> | ||||
|             <StyledInputDescription> | ||||
|                 What's the purpose of this template? | ||||
|             </StyledInputDescription> | ||||
|             <StyledInput | ||||
|                 label='Template description (optional)' | ||||
|                 value={description} | ||||
|                 onChange={(e) => setDescription(e.target.value)} | ||||
|                 error={Boolean(errors.description)} | ||||
|                 errorText={errors.description} | ||||
|                 onFocus={() => clearErrors()} | ||||
|             /> | ||||
|         </> | ||||
|         <FormTemplate | ||||
|             title={formTitle} | ||||
|             description={formDescription} | ||||
|             documentationIcon={<ReleaseTemplateIcon />} | ||||
|         > | ||||
|             <StyledForm onSubmit={handleSubmit}> | ||||
|                 <StyledInputDescription> | ||||
|                     What would you like to call your template? | ||||
|                 </StyledInputDescription> | ||||
|                 <StyledInput | ||||
|                     label='Template name' | ||||
|                     value={name} | ||||
|                     onChange={(e) => setName(e.target.value)} | ||||
|                     error={Boolean(errors.name)} | ||||
|                     errorText={errors.name} | ||||
|                     onFocus={() => clearErrors()} | ||||
|                     autoFocus | ||||
|                 /> | ||||
|                 <StyledInputDescription> | ||||
|                     What's the purpose of this template? | ||||
|                 </StyledInputDescription> | ||||
|                 <StyledInput | ||||
|                     label='Template description (optional)' | ||||
|                     value={description} | ||||
|                     onChange={(e) => setDescription(e.target.value)} | ||||
|                     error={Boolean(errors.description)} | ||||
|                     errorText={errors.description} | ||||
|                     onFocus={() => clearErrors()} | ||||
|                 /> | ||||
|                 <MilestoneList | ||||
|                     milestones={milestones} | ||||
|                     setMilestones={setMilestones} | ||||
|                     setAddStrategyOpen={setAddStrategyOpen} | ||||
|                     errors={errors} | ||||
|                     clearErrors={clearErrors} | ||||
|                 /> | ||||
| 
 | ||||
|                 {children} | ||||
| 
 | ||||
|                 <SidebarModal | ||||
|                     label='Add strategy to template milestone' | ||||
|                     onClose={() => {}} | ||||
|                     open={addStrategyOpen} | ||||
|                 > | ||||
|                     <ReleasePlanTemplateAddStrategyForm | ||||
|                         onCancel={() => { | ||||
|                             setAddStrategyOpen(false); | ||||
|                         }} | ||||
|                     /> | ||||
|                 </SidebarModal> | ||||
|             </StyledForm> | ||||
|         </FormTemplate> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -1,8 +1,16 @@ | ||||
| import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans'; | ||||
| import { useEffect, useState } from 'react'; | ||||
| 
 | ||||
| export const useTemplateForm = (initialName = '', initialDescription = '') => { | ||||
| export const useTemplateForm = ( | ||||
|     initialName = '', | ||||
|     initialDescription = '', | ||||
|     initialMilestones: IReleasePlanMilestonePayload[] = [ | ||||
|         { name: 'Milestone 1', sortOrder: 0 }, | ||||
|     ], | ||||
| ) => { | ||||
|     const [name, setName] = useState(initialName); | ||||
|     const [description, setDescription] = useState(initialDescription); | ||||
|     const [milestones, setMilestones] = useState(initialMilestones); | ||||
|     const [errors, setErrors] = useState({}); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
| @ -13,6 +21,10 @@ export const useTemplateForm = (initialName = '', initialDescription = '') => { | ||||
|         setDescription(initialDescription); | ||||
|     }, [initialDescription]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setMilestones(initialMilestones); | ||||
|     }, [initialMilestones.length]); | ||||
| 
 | ||||
|     const validate = () => { | ||||
|         if (name.length === 0) { | ||||
|             setErrors((prev) => ({ ...prev, name: 'Name can not be empty.' })); | ||||
| @ -37,6 +49,8 @@ export const useTemplateForm = (initialName = '', initialDescription = '') => { | ||||
|         setName, | ||||
|         description, | ||||
|         setDescription, | ||||
|         milestones, | ||||
|         setMilestones, | ||||
|         errors, | ||||
|         clearErrors, | ||||
|         validate, | ||||
|  | ||||
| @ -12,7 +12,7 @@ export interface IReleasePlanTemplate { | ||||
|     description: string; | ||||
|     createdAt: string; | ||||
|     createdByUserId: number; | ||||
|     milestones: IReleasePlanMilestone[]; | ||||
|     milestones: IReleasePlanMilestonePayload[]; | ||||
| } | ||||
| 
 | ||||
| export interface IReleasePlanMilestone { | ||||
| @ -24,5 +24,17 @@ export interface IReleasePlanTemplatePayload { | ||||
|     id?: string; | ||||
|     name: string; | ||||
|     description: string; | ||||
|     milestones?: IReleasePlanMilestone[]; | ||||
|     milestones?: IReleasePlanMilestonePayload[]; | ||||
| } | ||||
| 
 | ||||
| export interface IReleasePlanMilestonePayload { | ||||
|     id?: string; | ||||
|     name: string; | ||||
|     sortOrder: number; | ||||
|     strategies?: IReleasePlanStrategyPayload[]; | ||||
| } | ||||
| 
 | ||||
| export interface IReleasePlanStrategyPayload { | ||||
|     id?: string; | ||||
|     name: string; | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user