mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore: handle release plans in new strategy list (#9380)
Splits the release plan component into a Legacy component and a new one with the initial changes for the new strategy list view. Here's what it looks like:  Notice that the background color stops a little early (before the OR token). I'll handle that in a follow-up because the changes also impact how the rest of the env accordion body is rendered.
This commit is contained in:
		
							parent
							
								
									e29eb51f3c
								
							
						
					
					
						commit
						359b7cc4c0
					
				| @ -22,10 +22,10 @@ import type { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; | ||||
| import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| import { SectionSeparator } from '../SectionSeparator/SectionSeparator'; | ||||
| import { StrategyDraggableItem as NewStrategyDraggableItem } from './StrategyDraggableItem/StrategyDraggableItem'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan'; | ||||
| 
 | ||||
| interface IEnvironmentAccordionBodyProps { | ||||
|     isDisabled: boolean; | ||||
| @ -66,6 +66,10 @@ const StyledStrategyList = styled('ol')({ | ||||
|     margin: 0, | ||||
| }); | ||||
| 
 | ||||
| const StyledReleasePlanList = styled(StyledStrategyList)(({ theme }) => ({ | ||||
|     background: theme.palette.background.elevation2, | ||||
| })); | ||||
| 
 | ||||
| export const EnvironmentAccordionBody = ({ | ||||
|     featureEnvironment, | ||||
|     isDisabled, | ||||
| @ -234,29 +238,20 @@ export const EnvironmentAccordionBody = ({ | ||||
|                     condition={releasePlans.length > 0 || strategies.length > 0} | ||||
|                     show={ | ||||
|                         <> | ||||
|                             <StyledReleasePlanList> | ||||
|                                 {releasePlans.map((plan) => ( | ||||
|                                     <li key={plan.id}> | ||||
|                                         <ReleasePlan | ||||
|                                     key={plan.id} | ||||
|                                             plan={plan} | ||||
|                                             environmentIsDisabled={isDisabled} | ||||
|                                         /> | ||||
|                                     </li> | ||||
|                                 ))} | ||||
|                             <ConditionallyRender | ||||
|                                 condition={ | ||||
|                                     releasePlans.length > 0 && | ||||
|                                     strategies.length > 0 | ||||
|                                 } | ||||
|                                 show={ | ||||
|                                     <> | ||||
|                                         <SectionSeparator> | ||||
|                                             <StyledBadge>OR</StyledBadge> | ||||
|                                         </SectionSeparator> | ||||
|                                         <AdditionalStrategiesDiv> | ||||
|                                             Additional strategies | ||||
|                                         </AdditionalStrategiesDiv> | ||||
|                                     </> | ||||
|                                 } | ||||
|                             /> | ||||
|                             </StyledReleasePlanList> | ||||
|                             {releasePlans.length > 0 && | ||||
|                             strategies.length > 0 ? ( | ||||
|                                 <StrategySeparator text='OR' /> | ||||
|                             ) : null} | ||||
|                             <ConditionallyRender | ||||
|                                 condition={ | ||||
|                                     strategies.length < 50 || | ||||
|  | ||||
| @ -23,7 +23,7 @@ import type { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; | ||||
| import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan'; | ||||
| import { ReleasePlan } from '../../../ReleasePlan/LegacyReleasePlan'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| import { SectionSeparator } from '../SectionSeparator/SectionSeparator'; | ||||
| 
 | ||||
|  | ||||
| @ -0,0 +1,324 @@ | ||||
| import Delete from '@mui/icons-material/Delete'; | ||||
| import { styled } from '@mui/material'; | ||||
| import { DELETE_FEATURE_STRATEGY } from '@server/types/permissions'; | ||||
| import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; | ||||
| import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi'; | ||||
| import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import type { | ||||
|     IReleasePlan, | ||||
|     IReleasePlanMilestone, | ||||
| } from 'interfaces/releasePlans'; | ||||
| import { useState } from 'react'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog'; | ||||
| import { ReleasePlanMilestone } from './ReleasePlanMilestone/ReleasePlanMilestone'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi'; | ||||
| import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; | ||||
| import { RemoveReleasePlanChangeRequestDialog } from './ChangeRequest/RemoveReleasePlanChangeRequestDialog'; | ||||
| import { StartMilestoneChangeRequestDialog } from './ChangeRequest/StartMilestoneChangeRequestDialog'; | ||||
| import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; | ||||
| import { Truncator } from 'component/common/Truncator/Truncator'; | ||||
| 
 | ||||
| const StyledContainer = styled('div', { | ||||
|     shouldForwardProp: (prop) => prop !== 'readonly', | ||||
| })<{ readonly?: boolean }>(({ theme, readonly }) => ({ | ||||
|     padding: theme.spacing(2), | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     '& + &': { | ||||
|         marginTop: theme.spacing(2), | ||||
|     }, | ||||
|     background: readonly | ||||
|         ? theme.palette.background.elevation1 | ||||
|         : theme.palette.background.paper, | ||||
| })); | ||||
| 
 | ||||
| const StyledHeader = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     color: theme.palette.text.primary, | ||||
| })); | ||||
| 
 | ||||
| const StyledHeaderTitleContainer = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     justifyContent: 'center', | ||||
|     gap: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| const StyledHeaderTitleLabel = styled('span')(({ theme }) => ({ | ||||
|     fontSize: theme.fontSizes.smallerBody, | ||||
|     lineHeight: 0.5, | ||||
|     color: theme.palette.text.secondary, | ||||
|     marginBottom: theme.spacing(0.5), | ||||
| })); | ||||
| 
 | ||||
| const StyledHeaderDescription = styled('span')(({ theme }) => ({ | ||||
|     fontSize: theme.fontSizes.smallBody, | ||||
|     color: theme.palette.text.secondary, | ||||
| })); | ||||
| 
 | ||||
| const StyledBody = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     marginTop: theme.spacing(3), | ||||
| })); | ||||
| 
 | ||||
| const StyledConnection = styled('div')(({ theme }) => ({ | ||||
|     width: 4, | ||||
|     height: theme.spacing(2), | ||||
|     backgroundColor: theme.palette.divider, | ||||
|     marginLeft: theme.spacing(3.25), | ||||
| })); | ||||
| 
 | ||||
| interface IReleasePlanProps { | ||||
|     plan: IReleasePlan; | ||||
|     environmentIsDisabled?: boolean; | ||||
|     readonly?: boolean; | ||||
| } | ||||
| 
 | ||||
| export const ReleasePlan = ({ | ||||
|     plan, | ||||
|     environmentIsDisabled, | ||||
|     readonly, | ||||
| }: IReleasePlanProps) => { | ||||
|     const { | ||||
|         id, | ||||
|         name, | ||||
|         description, | ||||
|         activeMilestoneId, | ||||
|         featureName, | ||||
|         environment, | ||||
|         milestones, | ||||
|     } = plan; | ||||
| 
 | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const { refetch } = useReleasePlans(projectId, featureName, environment); | ||||
|     const { removeReleasePlanFromFeature, startReleasePlanMilestone } = | ||||
|         useReleasePlansApi(); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const { trackEvent } = usePlausibleTracker(); | ||||
| 
 | ||||
|     const [removeOpen, setRemoveOpen] = useState(false); | ||||
|     const [changeRequestDialogRemoveOpen, setChangeRequestDialogRemoveOpen] = | ||||
|         useState(false); | ||||
|     const [ | ||||
|         changeRequestDialogStartMilestoneOpen, | ||||
|         setChangeRequestDialogStartMilestoneOpen, | ||||
|     ] = useState(false); | ||||
|     const [ | ||||
|         milestoneForChangeRequestDialog, | ||||
|         setMilestoneForChangeRequestDialog, | ||||
|     ] = useState<IReleasePlanMilestone>(); | ||||
|     const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); | ||||
|     const { addChange } = useChangeRequestApi(); | ||||
|     const { refetch: refetchChangeRequests } = | ||||
|         usePendingChangeRequests(projectId); | ||||
| 
 | ||||
|     const releasePlanChangeRequestsEnabled = useUiFlag( | ||||
|         'releasePlanChangeRequests', | ||||
|     ); | ||||
| 
 | ||||
|     const onAddRemovePlanChangesConfirm = async () => { | ||||
|         await addChange(projectId, environment, { | ||||
|             feature: featureName, | ||||
|             action: 'deleteReleasePlan', | ||||
|             payload: { | ||||
|                 planId: plan.id, | ||||
|             }, | ||||
|         }); | ||||
| 
 | ||||
|         await refetchChangeRequests(); | ||||
| 
 | ||||
|         setToastData({ | ||||
|             type: 'success', | ||||
|             text: 'Added to draft', | ||||
|         }); | ||||
| 
 | ||||
|         setChangeRequestDialogRemoveOpen(false); | ||||
|     }; | ||||
| 
 | ||||
|     const onAddStartMilestoneChangesConfirm = async () => { | ||||
|         await addChange(projectId, environment, { | ||||
|             feature: featureName, | ||||
|             action: 'startMilestone', | ||||
|             payload: { | ||||
|                 planId: plan.id, | ||||
|                 milestoneId: milestoneForChangeRequestDialog?.id, | ||||
|             }, | ||||
|         }); | ||||
| 
 | ||||
|         await refetchChangeRequests(); | ||||
| 
 | ||||
|         setToastData({ | ||||
|             type: 'success', | ||||
|             text: 'Added to draft', | ||||
|         }); | ||||
| 
 | ||||
|         setChangeRequestDialogStartMilestoneOpen(false); | ||||
|     }; | ||||
| 
 | ||||
|     const confirmRemoveReleasePlan = () => { | ||||
|         if ( | ||||
|             releasePlanChangeRequestsEnabled && | ||||
|             isChangeRequestConfigured(environment) | ||||
|         ) { | ||||
|             setChangeRequestDialogRemoveOpen(true); | ||||
|         } else { | ||||
|             setRemoveOpen(true); | ||||
|         } | ||||
| 
 | ||||
|         trackEvent('release-management', { | ||||
|             props: { | ||||
|                 eventType: 'remove-plan', | ||||
|                 plan: name, | ||||
|             }, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     const onRemoveConfirm = async () => { | ||||
|         try { | ||||
|             await removeReleasePlanFromFeature( | ||||
|                 projectId, | ||||
|                 featureName, | ||||
|                 environment, | ||||
|                 id, | ||||
|             ); | ||||
|             setToastData({ | ||||
|                 text: `Release plan "${name}" has been removed from ${featureName} in ${environment}`, | ||||
|                 type: 'success', | ||||
|             }); | ||||
| 
 | ||||
|             refetch(); | ||||
|             setRemoveOpen(false); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const onStartMilestone = async (milestone: IReleasePlanMilestone) => { | ||||
|         if ( | ||||
|             releasePlanChangeRequestsEnabled && | ||||
|             isChangeRequestConfigured(environment) | ||||
|         ) { | ||||
|             setMilestoneForChangeRequestDialog(milestone); | ||||
|             setChangeRequestDialogStartMilestoneOpen(true); | ||||
|         } else { | ||||
|             try { | ||||
|                 await startReleasePlanMilestone( | ||||
|                     projectId, | ||||
|                     featureName, | ||||
|                     environment, | ||||
|                     id, | ||||
|                     milestone.id, | ||||
|                 ); | ||||
|                 setToastData({ | ||||
|                     text: `Milestone "${milestone.name}" has started`, | ||||
|                     type: 'success', | ||||
|                 }); | ||||
|                 refetch(); | ||||
|             } catch (error: unknown) { | ||||
|                 setToastApiError(formatUnknownError(error)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         trackEvent('release-management', { | ||||
|             props: { | ||||
|                 eventType: 'start-milestone', | ||||
|                 plan: name, | ||||
|                 milestone: milestone.name, | ||||
|             }, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     const activeIndex = milestones.findIndex( | ||||
|         (milestone) => milestone.id === activeMilestoneId, | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledContainer readonly={readonly}> | ||||
|             <StyledHeader> | ||||
|                 <StyledHeaderTitleContainer> | ||||
|                     <StyledHeaderTitleLabel> | ||||
|                         Release plan | ||||
|                     </StyledHeaderTitleLabel> | ||||
|                     <span>{name}</span> | ||||
|                     <StyledHeaderDescription> | ||||
|                         <Truncator lines={2} title={description}> | ||||
|                             {description} | ||||
|                         </Truncator> | ||||
|                     </StyledHeaderDescription> | ||||
|                 </StyledHeaderTitleContainer> | ||||
|                 {!readonly && ( | ||||
|                     <PermissionIconButton | ||||
|                         onClick={confirmRemoveReleasePlan} | ||||
|                         permission={DELETE_FEATURE_STRATEGY} | ||||
|                         environmentId={environment} | ||||
|                         projectId={projectId} | ||||
|                         tooltipProps={{ | ||||
|                             title: 'Remove release plan', | ||||
|                         }} | ||||
|                     > | ||||
|                         <Delete /> | ||||
|                     </PermissionIconButton> | ||||
|                 )} | ||||
|             </StyledHeader> | ||||
|             <StyledBody> | ||||
|                 {milestones.map((milestone, index) => ( | ||||
|                     <div key={milestone.id}> | ||||
|                         <ReleasePlanMilestone | ||||
|                             readonly={readonly} | ||||
|                             milestone={milestone} | ||||
|                             status={ | ||||
|                                 milestone.id === activeMilestoneId | ||||
|                                     ? environmentIsDisabled | ||||
|                                         ? 'paused' | ||||
|                                         : 'active' | ||||
|                                     : index < activeIndex | ||||
|                                       ? 'completed' | ||||
|                                       : 'not-started' | ||||
|                             } | ||||
|                             onStartMilestone={onStartMilestone} | ||||
|                         /> | ||||
|                         <ConditionallyRender | ||||
|                             condition={index < milestones.length - 1} | ||||
|                             show={<StyledConnection />} | ||||
|                         /> | ||||
|                     </div> | ||||
|                 ))} | ||||
|             </StyledBody> | ||||
|             <ReleasePlanRemoveDialog | ||||
|                 plan={plan} | ||||
|                 open={removeOpen} | ||||
|                 setOpen={setRemoveOpen} | ||||
|                 onConfirm={onRemoveConfirm} | ||||
|                 environmentActive={!environmentIsDisabled} | ||||
|             /> | ||||
|             <RemoveReleasePlanChangeRequestDialog | ||||
|                 environmentId={environment} | ||||
|                 featureId={featureName} | ||||
|                 isOpen={changeRequestDialogRemoveOpen} | ||||
|                 onConfirm={onAddRemovePlanChangesConfirm} | ||||
|                 onClosing={() => setChangeRequestDialogRemoveOpen(false)} | ||||
|                 releasePlan={plan} | ||||
|                 environmentActive={!environmentIsDisabled} | ||||
|             /> | ||||
|             <StartMilestoneChangeRequestDialog | ||||
|                 environmentId={environment} | ||||
|                 featureId={featureName} | ||||
|                 isOpen={changeRequestDialogStartMilestoneOpen} | ||||
|                 onConfirm={onAddStartMilestoneChangesConfirm} | ||||
|                 onClosing={() => { | ||||
|                     setMilestoneForChangeRequestDialog(undefined); | ||||
|                     setChangeRequestDialogStartMilestoneOpen(false); | ||||
|                 }} | ||||
|                 releasePlan={plan} | ||||
|                 milestone={milestoneForChangeRequestDialog} | ||||
|             /> | ||||
|         </StyledContainer> | ||||
|     ); | ||||
| }; | ||||
| @ -28,13 +28,10 @@ const StyledContainer = styled('div', { | ||||
|     shouldForwardProp: (prop) => prop !== 'readonly', | ||||
| })<{ readonly?: boolean }>(({ theme, readonly }) => ({ | ||||
|     padding: theme.spacing(2), | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     '& + &': { | ||||
|         marginTop: theme.spacing(2), | ||||
|     }, | ||||
|     background: readonly | ||||
|         ? theme.palette.background.elevation1 | ||||
|         : theme.palette.background.paper, | ||||
|     background: 'inherit', | ||||
| })); | ||||
| 
 | ||||
| const StyledHeader = styled('div')(({ theme }) => ({ | ||||
| @ -43,22 +40,28 @@ const StyledHeader = styled('div')(({ theme }) => ({ | ||||
|     color: theme.palette.text.primary, | ||||
| })); | ||||
| 
 | ||||
| const StyledHeaderTitleContainer = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     justifyContent: 'center', | ||||
|     gap: theme.spacing(1), | ||||
| const StyledHeaderGroup = styled('hgroup')(({ theme }) => ({ | ||||
|     paddingTop: theme.spacing(1.5), | ||||
| })); | ||||
| 
 | ||||
| const StyledHeaderTitleLabel = styled('span')(({ theme }) => ({ | ||||
|     fontSize: theme.fontSizes.smallerBody, | ||||
| const StyledHeaderTitleLabel = styled('p')(({ theme }) => ({ | ||||
|     fontWeight: 'bold', | ||||
|     fontSize: theme.typography.body1.fontSize, | ||||
|     lineHeight: 0.5, | ||||
|     color: theme.palette.text.secondary, | ||||
|     marginBottom: theme.spacing(0.5), | ||||
|     display: 'inline', | ||||
| })); | ||||
| 
 | ||||
| const StyledHeaderDescription = styled('span')(({ theme }) => ({ | ||||
|     fontSize: theme.fontSizes.smallBody, | ||||
| const StyledHeaderTitle = styled('h3')(({ theme }) => ({ | ||||
|     display: 'inline', | ||||
|     margin: 0, | ||||
|     fontWeight: 'normal', | ||||
|     fontSize: theme.typography.body1.fontSize, | ||||
| })); | ||||
| 
 | ||||
| const StyledHeaderDescription = styled('p')(({ theme }) => ({ | ||||
|     marginTop: theme.spacing(1), | ||||
|     fontSize: theme.typography.body2.fontSize, | ||||
|     color: theme.palette.text.secondary, | ||||
| })); | ||||
| 
 | ||||
| @ -242,17 +245,17 @@ export const ReleasePlan = ({ | ||||
|     return ( | ||||
|         <StyledContainer readonly={readonly}> | ||||
|             <StyledHeader> | ||||
|                 <StyledHeaderTitleContainer> | ||||
|                 <StyledHeaderGroup> | ||||
|                     <StyledHeaderTitleLabel> | ||||
|                         Release plan | ||||
|                         Release plan:{' '} | ||||
|                     </StyledHeaderTitleLabel> | ||||
|                     <span>{name}</span> | ||||
|                     <StyledHeaderTitle>{name}</StyledHeaderTitle> | ||||
|                     <StyledHeaderDescription> | ||||
|                         <Truncator lines={2} title={description}> | ||||
|                             {description} | ||||
|                         </Truncator> | ||||
|                     </StyledHeaderDescription> | ||||
|                 </StyledHeaderTitleContainer> | ||||
|                 </StyledHeaderGroup> | ||||
|                 {!readonly && ( | ||||
|                     <PermissionIconButton | ||||
|                         onClick={confirmRemoveReleasePlan} | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user