mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore(1-3389): new env strategy containers (#9361)
Updates the strategy list based on the new designs and moves the current versions of the touched components into `Legacy...` files (the vast majority of changes are that and updating imports). The relevant changes to the components are listed in their original files. Flag on:  Flag off:  ## Next steps There's two items to review for improving these current comments (also noted inline): - Whether to aria-hide the "or" separator or not (I need to read up a bit and think whether it makes sense to show that or not) - Changing the list of strategies into an actual ordered list (`ol`). That'd reflect the semantics better. Next would be checking the other places we use strategy lists and then updating those too. In doing so, I might find that some things need to be updated, but I'll handle those when I get there. There's also handling release plans.
This commit is contained in:
		
							parent
							
								
									192bd83fa6
								
							
						
					
					
						commit
						e25fb9f7c0
					
				| @ -15,7 +15,7 @@ import { type IUseWeakMap, useWeakMap } from 'hooks/useWeakMap'; | ||||
| import { objectId } from 'utils/objectId'; | ||||
| import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| 
 | ||||
| export interface IConstraintAccordionListProps { | ||||
|     constraints: IConstraint[]; | ||||
|  | ||||
| @ -11,7 +11,7 @@ import { | ||||
|     createEmptyConstraint, | ||||
| } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { NewConstraintAccordion } from 'component/common/NewConstraintAccordion/NewConstraintAccordion'; | ||||
| 
 | ||||
| export interface IConstraintAccordionListProps { | ||||
|  | ||||
| @ -0,0 +1,205 @@ | ||||
| // deprecated; remove with the `flagOverviewRedesign` flag
 | ||||
| import type React from 'react'; | ||||
| import type { DragEventHandler, FC, ReactNode } from 'react'; | ||||
| import DragIndicator from '@mui/icons-material/DragIndicator'; | ||||
| import { Box, IconButton, styled } from '@mui/material'; | ||||
| import type { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| import { | ||||
|     formatStrategyName, | ||||
|     getFeatureStrategyIcon, | ||||
| } from 'utils/strategyNames'; | ||||
| import StringTruncator from 'component/common/StringTruncator/StringTruncator'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import type { PlaygroundStrategySchema } from 'openapi'; | ||||
| import { Badge } from '../Badge/Badge'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| 
 | ||||
| interface IStrategyItemContainerProps { | ||||
|     strategy: IFeatureStrategy | PlaygroundStrategySchema; | ||||
|     onDragStart?: DragEventHandler<HTMLButtonElement>; | ||||
|     onDragEnd?: DragEventHandler<HTMLButtonElement>; | ||||
|     actions?: ReactNode; | ||||
|     orderNumber?: number; | ||||
|     className?: string; | ||||
|     style?: React.CSSProperties; | ||||
|     description?: string; | ||||
|     children?: React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| const DragIcon = styled(IconButton)({ | ||||
|     padding: 0, | ||||
|     cursor: 'inherit', | ||||
|     transition: 'color 0.2s ease-in-out', | ||||
| }); | ||||
| 
 | ||||
| const StyledIndexLabel = styled('div')(({ theme }) => ({ | ||||
|     fontSize: theme.typography.fontSize, | ||||
|     color: theme.palette.text.secondary, | ||||
|     position: 'absolute', | ||||
|     display: 'none', | ||||
|     right: 'calc(100% + 6px)', | ||||
|     top: theme.spacing(2.5), | ||||
|     [theme.breakpoints.up('md')]: { | ||||
|         display: 'block', | ||||
|     }, | ||||
| })); | ||||
| const StyledDescription = styled('div')(({ theme }) => ({ | ||||
|     fontSize: theme.typography.fontSize, | ||||
|     fontWeight: 'normal', | ||||
|     color: theme.palette.text.secondary, | ||||
|     display: 'none', | ||||
|     top: theme.spacing(2.5), | ||||
|     [theme.breakpoints.up('md')]: { | ||||
|         display: 'block', | ||||
|     }, | ||||
| })); | ||||
| const StyledCustomTitle = styled('div')(({ theme }) => ({ | ||||
|     fontWeight: 'normal', | ||||
|     display: 'none', | ||||
|     [theme.breakpoints.up('md')]: { | ||||
|         display: 'block', | ||||
|     }, | ||||
| })); | ||||
| const StyledHeaderContainer = styled('div')({ | ||||
|     flexDirection: 'column', | ||||
|     justifyContent: 'center', | ||||
|     verticalAlign: 'middle', | ||||
| }); | ||||
| 
 | ||||
| const StyledContainer = styled(Box, { | ||||
|     shouldForwardProp: (prop) => prop !== 'disabled', | ||||
| })<{ disabled?: boolean }>(({ theme, disabled }) => ({ | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     border: `1px solid ${theme.palette.divider}`, | ||||
|     '& + &': { | ||||
|         marginTop: theme.spacing(2), | ||||
|     }, | ||||
|     background: disabled | ||||
|         ? theme.palette.envAccordion.disabled | ||||
|         : theme.palette.background.paper, | ||||
| })); | ||||
| 
 | ||||
| const StyledHeader = styled('div', { | ||||
|     shouldForwardProp: (prop) => prop !== 'draggable' && prop !== 'disabled', | ||||
| })<{ draggable: boolean; disabled: boolean }>( | ||||
|     ({ theme, draggable, disabled }) => ({ | ||||
|         padding: theme.spacing(0.5, 2), | ||||
|         display: 'flex', | ||||
|         gap: theme.spacing(1), | ||||
|         alignItems: 'center', | ||||
|         borderBottom: `1px solid ${theme.palette.divider}`, | ||||
|         fontWeight: theme.typography.fontWeightMedium, | ||||
|         paddingLeft: draggable ? theme.spacing(1) : theme.spacing(2), | ||||
|         color: disabled | ||||
|             ? theme.palette.text.secondary | ||||
|             : theme.palette.text.primary, | ||||
|     }), | ||||
| ); | ||||
| 
 | ||||
| export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({ | ||||
|     strategy, | ||||
|     onDragStart, | ||||
|     onDragEnd, | ||||
|     actions, | ||||
|     children, | ||||
|     orderNumber, | ||||
|     style = {}, | ||||
|     description, | ||||
| }) => { | ||||
|     const Icon = getFeatureStrategyIcon(strategy.name); | ||||
| 
 | ||||
|     const StrategyHeaderLink: React.FC<{ children?: React.ReactNode }> = | ||||
|         'links' in strategy | ||||
|             ? ({ children }) => <Link to={strategy.links.edit}>{children}</Link> | ||||
|             : ({ children }) => <> {children} </>; | ||||
| 
 | ||||
|     return ( | ||||
|         <Box sx={{ position: 'relative' }}> | ||||
|             <ConditionallyRender | ||||
|                 condition={orderNumber !== undefined} | ||||
|                 show={<StyledIndexLabel>{orderNumber}</StyledIndexLabel>} | ||||
|             /> | ||||
|             <StyledContainer | ||||
|                 disabled={strategy?.disabled || false} | ||||
|                 style={style} | ||||
|             > | ||||
|                 <StyledHeader | ||||
|                     draggable={Boolean(onDragStart)} | ||||
|                     disabled={Boolean(strategy?.disabled)} | ||||
|                 > | ||||
|                     <ConditionallyRender | ||||
|                         condition={Boolean(onDragStart)} | ||||
|                         show={() => ( | ||||
|                             <DragIcon | ||||
|                                 draggable | ||||
|                                 disableRipple | ||||
|                                 size='small' | ||||
|                                 onDragStart={onDragStart} | ||||
|                                 onDragEnd={onDragEnd} | ||||
|                                 sx={{ cursor: 'move' }} | ||||
|                             > | ||||
|                                 <DragIndicator | ||||
|                                     titleAccess='Drag to reorder' | ||||
|                                     cursor='grab' | ||||
|                                     sx={{ color: 'action.active' }} | ||||
|                                 /> | ||||
|                             </DragIcon> | ||||
|                         )} | ||||
|                     /> | ||||
|                     <Icon | ||||
|                         sx={{ | ||||
|                             fill: (theme) => theme.palette.action.disabled, | ||||
|                         }} | ||||
|                     /> | ||||
|                     <StyledHeaderContainer> | ||||
|                         <StrategyHeaderLink> | ||||
|                             <StringTruncator | ||||
|                                 maxWidth='400' | ||||
|                                 maxLength={15} | ||||
|                                 text={formatStrategyName(String(strategy.name))} | ||||
|                             /> | ||||
|                             <ConditionallyRender | ||||
|                                 condition={Boolean(strategy.title)} | ||||
|                                 show={ | ||||
|                                     <StyledCustomTitle> | ||||
|                                         {formatStrategyName( | ||||
|                                             String(strategy.title), | ||||
|                                         )} | ||||
|                                     </StyledCustomTitle> | ||||
|                                 } | ||||
|                             /> | ||||
|                         </StrategyHeaderLink> | ||||
|                         <ConditionallyRender | ||||
|                             condition={Boolean(description)} | ||||
|                             show={ | ||||
|                                 <StyledDescription> | ||||
|                                     {description} | ||||
|                                 </StyledDescription> | ||||
|                             } | ||||
|                         /> | ||||
|                     </StyledHeaderContainer> | ||||
| 
 | ||||
|                     <ConditionallyRender | ||||
|                         condition={Boolean(strategy?.disabled)} | ||||
|                         show={() => ( | ||||
|                             <> | ||||
|                                 <Badge color='disabled'>Disabled</Badge> | ||||
|                             </> | ||||
|                         )} | ||||
|                     /> | ||||
|                     <Box | ||||
|                         sx={{ | ||||
|                             marginLeft: 'auto', | ||||
|                             display: 'flex', | ||||
|                             minHeight: (theme) => theme.spacing(6), | ||||
|                             alignItems: 'center', | ||||
|                         }} | ||||
|                     > | ||||
|                         {actions} | ||||
|                     </Box> | ||||
|                 </StyledHeader> | ||||
|                 <Box sx={{ p: 2 }}>{children}</Box> | ||||
|             </StyledContainer> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -1,6 +1,6 @@ | ||||
| import { screen } from '@testing-library/react'; | ||||
| import { render } from 'utils/testRenderer'; | ||||
| import { StrategyItemContainer } from './StrategyItemContainer'; | ||||
| import { StrategyItemContainer } from './LegacyStrategyItemContainer'; | ||||
| import type { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| 
 | ||||
| test('should render strategy name, custom title and description', async () => { | ||||
|  | ||||
| @ -31,17 +31,6 @@ const DragIcon = styled(IconButton)({ | ||||
|     transition: 'color 0.2s ease-in-out', | ||||
| }); | ||||
| 
 | ||||
| const StyledIndexLabel = styled('div')(({ theme }) => ({ | ||||
|     fontSize: theme.typography.fontSize, | ||||
|     color: theme.palette.text.secondary, | ||||
|     position: 'absolute', | ||||
|     display: 'none', | ||||
|     right: 'calc(100% + 6px)', | ||||
|     top: theme.spacing(2.5), | ||||
|     [theme.breakpoints.up('md')]: { | ||||
|         display: 'block', | ||||
|     }, | ||||
| })); | ||||
| const StyledDescription = styled('div')(({ theme }) => ({ | ||||
|     fontSize: theme.typography.fontSize, | ||||
|     fontWeight: 'normal', | ||||
| @ -65,20 +54,13 @@ const StyledHeaderContainer = styled('div')({ | ||||
|     verticalAlign: 'middle', | ||||
| }); | ||||
| 
 | ||||
| const StyledContainer = styled(Box, { | ||||
| const NewStyledContainer = styled(Box, { | ||||
|     shouldForwardProp: (prop) => prop !== 'disabled', | ||||
| })<{ disabled?: boolean }>(({ theme, disabled }) => ({ | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     border: `1px solid ${theme.palette.divider}`, | ||||
|     '& + &': { | ||||
|         marginTop: theme.spacing(2), | ||||
|     }, | ||||
|     background: disabled | ||||
|         ? theme.palette.envAccordion.disabled | ||||
|         : theme.palette.background.paper, | ||||
| })); | ||||
| })({ | ||||
|     background: 'inherit', | ||||
| }); | ||||
| 
 | ||||
| const StyledHeader = styled('div', { | ||||
| const NewStyledHeader = styled('div', { | ||||
|     shouldForwardProp: (prop) => prop !== 'draggable' && prop !== 'disabled', | ||||
| })<{ draggable: boolean; disabled: boolean }>( | ||||
|     ({ theme, draggable, disabled }) => ({ | ||||
| @ -86,7 +68,6 @@ const StyledHeader = styled('div', { | ||||
|         display: 'flex', | ||||
|         gap: theme.spacing(1), | ||||
|         alignItems: 'center', | ||||
|         borderBottom: `1px solid ${theme.palette.divider}`, | ||||
|         fontWeight: theme.typography.fontWeightMedium, | ||||
|         paddingLeft: draggable ? theme.spacing(1) : theme.spacing(2), | ||||
|         color: disabled | ||||
| @ -101,7 +82,6 @@ export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({ | ||||
|     onDragEnd, | ||||
|     actions, | ||||
|     children, | ||||
|     orderNumber, | ||||
|     style = {}, | ||||
|     description, | ||||
| }) => { | ||||
| @ -114,15 +94,8 @@ export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({ | ||||
| 
 | ||||
|     return ( | ||||
|         <Box sx={{ position: 'relative' }}> | ||||
|             <ConditionallyRender | ||||
|                 condition={orderNumber !== undefined} | ||||
|                 show={<StyledIndexLabel>{orderNumber}</StyledIndexLabel>} | ||||
|             /> | ||||
|             <StyledContainer | ||||
|                 disabled={strategy?.disabled || false} | ||||
|                 style={style} | ||||
|             > | ||||
|                 <StyledHeader | ||||
|             <NewStyledContainer style={style}> | ||||
|                 <NewStyledHeader | ||||
|                     draggable={Boolean(onDragStart)} | ||||
|                     disabled={Boolean(strategy?.disabled)} | ||||
|                 > | ||||
| @ -196,9 +169,9 @@ export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({ | ||||
|                     > | ||||
|                         {actions} | ||||
|                     </Box> | ||||
|                 </StyledHeader> | ||||
|                 <Box sx={{ p: 2 }}>{children}</Box> | ||||
|             </StyledContainer> | ||||
|                 </NewStyledHeader> | ||||
|                 <Box sx={{ p: 2, pt: 0 }}>{children}</Box> | ||||
|             </NewStyledContainer> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -0,0 +1,52 @@ | ||||
| // deprecated; remove with the `flagOverviewRedesign` flag
 | ||||
| import { Box, styled, useTheme } from '@mui/material'; | ||||
| import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| interface IStrategySeparatorProps { | ||||
|     text: 'AND' | 'OR'; | ||||
| } | ||||
| 
 | ||||
| const StyledContent = styled('div')(({ theme }) => ({ | ||||
|     padding: theme.spacing(0.75, 1), | ||||
|     color: theme.palette.text.primary, | ||||
|     fontSize: theme.fontSizes.smallerBody, | ||||
|     backgroundColor: theme.palette.background.elevation2, | ||||
|     borderRadius: theme.shape.borderRadius, | ||||
|     position: 'absolute', | ||||
|     zIndex: theme.zIndex.fab, | ||||
|     top: '50%', | ||||
|     left: theme.spacing(2), | ||||
|     transform: 'translateY(-50%)', | ||||
|     lineHeight: 1, | ||||
| })); | ||||
| 
 | ||||
| const StyledCenteredContent = styled(StyledContent)(({ theme }) => ({ | ||||
|     top: '50%', | ||||
|     left: '50%', | ||||
|     transform: 'translate(-50%, -50%)', | ||||
|     backgroundColor: theme.palette.seen.primary, | ||||
|     borderRadius: theme.shape.borderRadiusLarge, | ||||
|     padding: theme.spacing(0.75, 1.5), | ||||
| })); | ||||
| 
 | ||||
| export const StrategySeparator = ({ text }: IStrategySeparatorProps) => { | ||||
|     const theme = useTheme(); | ||||
| 
 | ||||
|     return ( | ||||
|         <Box | ||||
|             sx={{ | ||||
|                 height: theme.spacing(text === 'AND' ? 1 : 1.5), | ||||
|                 position: 'relative', | ||||
|                 width: '100%', | ||||
|             }} | ||||
|         > | ||||
|             <ConditionallyRender | ||||
|                 condition={text === 'AND'} | ||||
|                 show={() => <StyledContent>{text}</StyledContent>} | ||||
|                 elseShow={() => ( | ||||
|                     <StyledCenteredContent>{text}</StyledCenteredContent> | ||||
|                 )} | ||||
|             /> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -1,51 +1,57 @@ | ||||
| import { Box, styled, useTheme } from '@mui/material'; | ||||
| import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| interface IStrategySeparatorProps { | ||||
|     text: 'AND' | 'OR'; | ||||
| } | ||||
| 
 | ||||
| const StyledContent = styled('div')(({ theme }) => ({ | ||||
| const StyledAnd = styled('div')(({ theme }) => ({ | ||||
|     padding: theme.spacing(0.75, 1), | ||||
|     color: theme.palette.text.primary, | ||||
|     fontSize: theme.fontSizes.smallerBody, | ||||
|     backgroundColor: theme.palette.background.elevation2, | ||||
|     borderRadius: theme.shape.borderRadius, | ||||
|     position: 'absolute', | ||||
|     zIndex: theme.zIndex.fab, | ||||
|     top: '50%', | ||||
|     left: theme.spacing(2), | ||||
|     transform: 'translateY(-50%)', | ||||
|     lineHeight: 1, | ||||
|     borderRadius: theme.shape.borderRadiusLarge, | ||||
| })); | ||||
| 
 | ||||
| const StyledCenteredContent = styled(StyledContent)(({ theme }) => ({ | ||||
| const StyledOr = styled(StyledAnd)(({ theme }) => ({ | ||||
|     fontWeight: 'bold', | ||||
|     backgroundColor: theme.palette.background.alternative, | ||||
|     color: theme.palette.primary.contrastText, | ||||
|     left: theme.spacing(4), | ||||
| })); | ||||
| 
 | ||||
| const StyledSeparator = styled('hr')(({ theme }) => ({ | ||||
|     border: 0, | ||||
|     borderTop: `1px solid ${theme.palette.divider}`, | ||||
|     margin: 0, | ||||
|     position: 'absolute', | ||||
|     top: '50%', | ||||
|     left: '50%', | ||||
|     transform: 'translate(-50%, -50%)', | ||||
|     backgroundColor: theme.palette.seen.primary, | ||||
|     borderRadius: theme.shape.borderRadiusLarge, | ||||
|     padding: theme.spacing(0.75, 1.5), | ||||
|     width: '100%', | ||||
| })); | ||||
| 
 | ||||
| export const StrategySeparator = ({ text }: IStrategySeparatorProps) => { | ||||
|     const theme = useTheme(); | ||||
| 
 | ||||
|     return ( | ||||
|         <Box | ||||
|             sx={{ | ||||
|                 height: theme.spacing(text === 'AND' ? 1 : 1.5), | ||||
|                 position: 'relative', | ||||
|                 width: '100%', | ||||
|             }} | ||||
|             aria-hidden={true} | ||||
|         > | ||||
|             <ConditionallyRender | ||||
|                 condition={text === 'AND'} | ||||
|                 show={() => <StyledContent>{text}</StyledContent>} | ||||
|                 elseShow={() => ( | ||||
|                     <StyledCenteredContent>{text}</StyledCenteredContent> | ||||
|                 )} | ||||
|             /> | ||||
|             {text === 'AND' ? ( | ||||
|                 <StyledAnd>{text}</StyledAnd> | ||||
|             ) : ( | ||||
|                 <> | ||||
|                     <StyledSeparator /> | ||||
|                     <StyledOr>{text}</StyledOr> | ||||
|                 </> | ||||
|             )} | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -9,7 +9,7 @@ import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFe | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategyDraggableItem } from './StrategyDraggableItem/StrategyDraggableItem'; | ||||
| import { StrategyDraggableItem } from './StrategyDraggableItem/LegacyStrategyDraggableItem'; | ||||
| import type { IFeatureEnvironment } from 'interfaces/featureToggle'; | ||||
| import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| @ -25,6 +25,7 @@ import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePla | ||||
| import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| import { SectionSeparator } from '../SectionSeparator/SectionSeparator'; | ||||
| import { StrategyDraggableItem as NewStrategyDraggableItem } from './StrategyDraggableItem/StrategyDraggableItem'; | ||||
| 
 | ||||
| interface IEnvironmentAccordionBodyProps { | ||||
|     isDisabled: boolean; | ||||
| @ -59,7 +60,7 @@ const AdditionalStrategiesDiv = styled('div')(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const EnvironmentAccordionBody = ({ | ||||
| export const EnvironmentAccordionBody = ({ | ||||
|     featureEnvironment, | ||||
|     isDisabled, | ||||
|     otherEnvironments, | ||||
| @ -223,18 +224,6 @@ const EnvironmentAccordionBody = ({ | ||||
|     return ( | ||||
|         <StyledAccordionBody> | ||||
|             <StyledAccordionBodyInnerContainer> | ||||
|                 <ConditionallyRender | ||||
|                     condition={ | ||||
|                         (releasePlans.length > 0 || strategies.length > 0) && | ||||
|                         isDisabled | ||||
|                     } | ||||
|                     show={() => ( | ||||
|                         <Alert severity='warning' sx={{ mb: 2 }}> | ||||
|                             This environment is disabled, which means that none | ||||
|                             of your strategies are executing. | ||||
|                         </Alert> | ||||
|                     )} | ||||
|                 /> | ||||
|                 <ConditionallyRender | ||||
|                     condition={releasePlans.length > 0 || strategies.length > 0} | ||||
|                     show={ | ||||
| @ -270,7 +259,7 @@ const EnvironmentAccordionBody = ({ | ||||
|                                 show={ | ||||
|                                     <> | ||||
|                                         {strategies.map((strategy, index) => ( | ||||
|                                             <StrategyDraggableItem | ||||
|                                             <NewStrategyDraggableItem | ||||
|                                                 key={strategy.id} | ||||
|                                                 strategy={strategy} | ||||
|                                                 index={index} | ||||
| @ -349,5 +338,3 @@ const EnvironmentAccordionBody = ({ | ||||
|         </StyledAccordionBody> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default EnvironmentAccordionBody; | ||||
|  | ||||
| @ -0,0 +1,354 @@ | ||||
| // deprecated; remove with the `flagOverviewRedesign` flag
 | ||||
| import { | ||||
|     type DragEventHandler, | ||||
|     type RefObject, | ||||
|     useEffect, | ||||
|     useState, | ||||
| } from 'react'; | ||||
| import { Alert, Pagination, styled } from '@mui/material'; | ||||
| import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategyDraggableItem } from './StrategyDraggableItem/LegacyStrategyDraggableItem'; | ||||
| import type { IFeatureEnvironment } from 'interfaces/featureToggle'; | ||||
| import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; | ||||
| import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi'; | ||||
| import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; | ||||
| import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; | ||||
| import usePagination from 'hooks/usePagination'; | ||||
| 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'; | ||||
| 
 | ||||
| interface IEnvironmentAccordionBodyProps { | ||||
|     isDisabled: boolean; | ||||
|     featureEnvironment?: IFeatureEnvironment; | ||||
|     otherEnvironments?: IFeatureEnvironment['name'][]; | ||||
| } | ||||
| 
 | ||||
| const StyledAccordionBody = styled('div')(({ theme }) => ({ | ||||
|     width: '100%', | ||||
|     position: 'relative', | ||||
|     paddingBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const StyledAccordionBodyInnerContainer = styled('div')(({ theme }) => ({ | ||||
|     [theme.breakpoints.down(400)]: { | ||||
|         padding: theme.spacing(1), | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledBadge = styled(Badge)(({ theme }) => ({ | ||||
|     backgroundColor: theme.palette.primary.light, | ||||
|     border: 'none', | ||||
|     padding: theme.spacing(0.75, 1.5), | ||||
|     borderRadius: theme.shape.borderRadiusLarge, | ||||
|     color: theme.palette.common.white, | ||||
| })); | ||||
| 
 | ||||
| const AdditionalStrategiesDiv = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     alignItems: 'center', | ||||
|     justifyContent: 'center', | ||||
|     marginBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const EnvironmentAccordionBody = ({ | ||||
|     featureEnvironment, | ||||
|     isDisabled, | ||||
|     otherEnvironments, | ||||
| }: IEnvironmentAccordionBodyProps) => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const featureId = useRequiredPathParam('featureId'); | ||||
|     const { setStrategiesSortOrder } = useFeatureStrategyApi(); | ||||
|     const { addChange } = useChangeRequestApi(); | ||||
|     const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); | ||||
|     const { refetch: refetchChangeRequests } = | ||||
|         usePendingChangeRequests(projectId); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const { refetchFeature } = useFeature(projectId, featureId); | ||||
|     const manyStrategiesPagination = useUiFlag('manyStrategiesPagination'); | ||||
|     const [strategies, setStrategies] = useState( | ||||
|         featureEnvironment?.strategies || [], | ||||
|     ); | ||||
|     const { releasePlans } = useReleasePlans( | ||||
|         projectId, | ||||
|         featureId, | ||||
|         featureEnvironment?.name, | ||||
|     ); | ||||
|     const { trackEvent } = usePlausibleTracker(); | ||||
| 
 | ||||
|     const [dragItem, setDragItem] = useState<{ | ||||
|         id: string; | ||||
|         index: number; | ||||
|         height: number; | ||||
|     } | null>(null); | ||||
|     useEffect(() => { | ||||
|         // Use state to enable drag and drop, but switch to API output when it arrives
 | ||||
|         setStrategies(featureEnvironment?.strategies || []); | ||||
|     }, [featureEnvironment?.strategies]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         if (strategies.length > 50) { | ||||
|             trackEvent('many-strategies'); | ||||
|         } | ||||
|     }, []); | ||||
| 
 | ||||
|     if (!featureEnvironment) { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     const pageSize = 20; | ||||
|     const { page, pages, setPageIndex, pageIndex } = | ||||
|         usePagination<IFeatureStrategy>(strategies, pageSize); | ||||
| 
 | ||||
|     const onReorder = async (payload: { id: string; sortOrder: number }[]) => { | ||||
|         try { | ||||
|             await setStrategiesSortOrder( | ||||
|                 projectId, | ||||
|                 featureId, | ||||
|                 featureEnvironment.name, | ||||
|                 payload, | ||||
|             ); | ||||
|             refetchFeature(); | ||||
|             setToastData({ | ||||
|                 text: 'Order of strategies updated', | ||||
|                 type: 'success', | ||||
|             }); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const onChangeRequestReorder = async ( | ||||
|         payload: { id: string; sortOrder: number }[], | ||||
|     ) => { | ||||
|         await addChange(projectId, featureEnvironment.name, { | ||||
|             action: 'reorderStrategy', | ||||
|             feature: featureId, | ||||
|             payload, | ||||
|         }); | ||||
| 
 | ||||
|         setToastData({ | ||||
|             text: 'Strategy execution order added to draft', | ||||
|             type: 'success', | ||||
|         }); | ||||
|         refetchChangeRequests(); | ||||
|     }; | ||||
| 
 | ||||
|     const onStrategyReorder = async ( | ||||
|         payload: { id: string; sortOrder: number }[], | ||||
|     ) => { | ||||
|         try { | ||||
|             if (isChangeRequestConfigured(featureEnvironment.name)) { | ||||
|                 await onChangeRequestReorder(payload); | ||||
|             } else { | ||||
|                 await onReorder(payload); | ||||
|             } | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const onDragStartRef = | ||||
|         ( | ||||
|             ref: RefObject<HTMLDivElement>, | ||||
|             index: number, | ||||
|         ): DragEventHandler<HTMLButtonElement> => | ||||
|         (event) => { | ||||
|             setDragItem({ | ||||
|                 id: strategies[index].id, | ||||
|                 index, | ||||
|                 height: ref.current?.offsetHeight || 0, | ||||
|             }); | ||||
| 
 | ||||
|             if (ref?.current) { | ||||
|                 event.dataTransfer.effectAllowed = 'move'; | ||||
|                 event.dataTransfer.setData('text/html', ref.current.outerHTML); | ||||
|                 event.dataTransfer.setDragImage(ref.current, 20, 20); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|     const onDragOver = | ||||
|         (targetId: string) => | ||||
|         ( | ||||
|             ref: RefObject<HTMLDivElement>, | ||||
|             targetIndex: number, | ||||
|         ): DragEventHandler<HTMLDivElement> => | ||||
|         (event) => { | ||||
|             if (dragItem === null || ref.current === null) return; | ||||
|             if (dragItem.index === targetIndex || targetId === dragItem.id) | ||||
|                 return; | ||||
| 
 | ||||
|             const { top, bottom } = ref.current.getBoundingClientRect(); | ||||
|             const overTargetTop = event.clientY - top < dragItem.height; | ||||
|             const overTargetBottom = bottom - event.clientY < dragItem.height; | ||||
|             const draggingUp = dragItem.index > targetIndex; | ||||
| 
 | ||||
|             // prevent oscillating by only reordering if there is sufficient space
 | ||||
|             if ( | ||||
|                 (overTargetTop && draggingUp) || | ||||
|                 (overTargetBottom && !draggingUp) | ||||
|             ) { | ||||
|                 const newStrategies = [...strategies]; | ||||
|                 const movedStrategy = newStrategies.splice( | ||||
|                     dragItem.index, | ||||
|                     1, | ||||
|                 )[0]; | ||||
|                 newStrategies.splice(targetIndex, 0, movedStrategy); | ||||
|                 setStrategies(newStrategies); | ||||
|                 setDragItem({ | ||||
|                     ...dragItem, | ||||
|                     index: targetIndex, | ||||
|                 }); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|     const onDragEnd = () => { | ||||
|         setDragItem(null); | ||||
|         onStrategyReorder( | ||||
|             strategies.map((strategy, sortOrder) => ({ | ||||
|                 id: strategy.id, | ||||
|                 sortOrder, | ||||
|             })), | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledAccordionBody> | ||||
|             <StyledAccordionBodyInnerContainer> | ||||
|                 <ConditionallyRender | ||||
|                     condition={ | ||||
|                         (releasePlans.length > 0 || strategies.length > 0) && | ||||
|                         isDisabled | ||||
|                     } | ||||
|                     show={() => ( | ||||
|                         <Alert severity='warning' sx={{ mb: 2 }}> | ||||
|                             This environment is disabled, which means that none | ||||
|                             of your strategies are executing. | ||||
|                         </Alert> | ||||
|                     )} | ||||
|                 /> | ||||
|                 <ConditionallyRender | ||||
|                     condition={releasePlans.length > 0 || strategies.length > 0} | ||||
|                     show={ | ||||
|                         <> | ||||
|                             {releasePlans.map((plan) => ( | ||||
|                                 <ReleasePlan | ||||
|                                     key={plan.id} | ||||
|                                     plan={plan} | ||||
|                                     environmentIsDisabled={isDisabled} | ||||
|                                 /> | ||||
|                             ))} | ||||
|                             <ConditionallyRender | ||||
|                                 condition={ | ||||
|                                     releasePlans.length > 0 && | ||||
|                                     strategies.length > 0 | ||||
|                                 } | ||||
|                                 show={ | ||||
|                                     <> | ||||
|                                         <SectionSeparator> | ||||
|                                             <StyledBadge>OR</StyledBadge> | ||||
|                                         </SectionSeparator> | ||||
|                                         <AdditionalStrategiesDiv> | ||||
|                                             Additional strategies | ||||
|                                         </AdditionalStrategiesDiv> | ||||
|                                     </> | ||||
|                                 } | ||||
|                             /> | ||||
|                             <ConditionallyRender | ||||
|                                 condition={ | ||||
|                                     strategies.length < 50 || | ||||
|                                     !manyStrategiesPagination | ||||
|                                 } | ||||
|                                 show={ | ||||
|                                     <> | ||||
|                                         {strategies.map((strategy, index) => ( | ||||
|                                             <StrategyDraggableItem | ||||
|                                                 key={strategy.id} | ||||
|                                                 strategy={strategy} | ||||
|                                                 index={index} | ||||
|                                                 environmentName={ | ||||
|                                                     featureEnvironment.name | ||||
|                                                 } | ||||
|                                                 otherEnvironments={ | ||||
|                                                     otherEnvironments | ||||
|                                                 } | ||||
|                                                 isDragging={ | ||||
|                                                     dragItem?.id === strategy.id | ||||
|                                                 } | ||||
|                                                 onDragStartRef={onDragStartRef} | ||||
|                                                 onDragOver={onDragOver( | ||||
|                                                     strategy.id, | ||||
|                                                 )} | ||||
|                                                 onDragEnd={onDragEnd} | ||||
|                                             /> | ||||
|                                         ))} | ||||
|                                     </> | ||||
|                                 } | ||||
|                                 elseShow={ | ||||
|                                     <> | ||||
|                                         <Alert severity='error'> | ||||
|                                             We noticed you're using a high | ||||
|                                             number of activation strategies. To | ||||
|                                             ensure a more targeted approach, | ||||
|                                             consider leveraging constraints or | ||||
|                                             segments. | ||||
|                                         </Alert> | ||||
|                                         <br /> | ||||
|                                         {page.map((strategy, index) => ( | ||||
|                                             <StrategyDraggableItem | ||||
|                                                 key={strategy.id} | ||||
|                                                 strategy={strategy} | ||||
|                                                 index={ | ||||
|                                                     index + pageIndex * pageSize | ||||
|                                                 } | ||||
|                                                 environmentName={ | ||||
|                                                     featureEnvironment.name | ||||
|                                                 } | ||||
|                                                 otherEnvironments={ | ||||
|                                                     otherEnvironments | ||||
|                                                 } | ||||
|                                                 isDragging={false} | ||||
|                                                 onDragStartRef={ | ||||
|                                                     (() => {}) as any | ||||
|                                                 } | ||||
|                                                 onDragOver={(() => {}) as any} | ||||
|                                                 onDragEnd={(() => {}) as any} | ||||
|                                             /> | ||||
|                                         ))} | ||||
|                                         <br /> | ||||
|                                         <Pagination | ||||
|                                             count={pages.length} | ||||
|                                             shape='rounded' | ||||
|                                             page={pageIndex + 1} | ||||
|                                             onChange={(_, page) => | ||||
|                                                 setPageIndex(page - 1) | ||||
|                                             } | ||||
|                                         /> | ||||
|                                     </> | ||||
|                                 } | ||||
|                             /> | ||||
|                         </> | ||||
|                     } | ||||
|                     elseShow={ | ||||
|                         <FeatureStrategyEmpty | ||||
|                             projectId={projectId} | ||||
|                             featureId={featureId} | ||||
|                             environmentId={featureEnvironment.name} | ||||
|                         /> | ||||
|                     } | ||||
|                 /> | ||||
|             </StyledAccordionBodyInnerContainer> | ||||
|         </StyledAccordionBody> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default EnvironmentAccordionBody; | ||||
| @ -0,0 +1,150 @@ | ||||
| // deprecated; remove with the `flagOverviewRedesign` flag
 | ||||
| import { type DragEventHandler, type RefObject, useRef } from 'react'; | ||||
| import { Box, useMediaQuery, useTheme } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import type { IFeatureEnvironment } from 'interfaces/featureToggle'; | ||||
| import type { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| import { StrategyItem } from './StrategyItem/LegacyStrategyItem'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { | ||||
|     useStrategyChangesFromRequest, | ||||
|     type UseStrategyChangeFromRequestResult, | ||||
| } from './StrategyItem/useStrategyChangesFromRequest'; | ||||
| import { ChangesScheduledBadge } from 'component/changeRequest/ModifiedInChangeRequestStatusBadge/ChangesScheduledBadge'; | ||||
| import type { IFeatureChange } from 'component/changeRequest/changeRequest.types'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| import { | ||||
|     type ScheduledChangeRequestViewModel, | ||||
|     useScheduledChangeRequestsWithStrategy, | ||||
| } from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy'; | ||||
| 
 | ||||
| interface IStrategyDraggableItemProps { | ||||
|     strategy: IFeatureStrategy; | ||||
|     environmentName: string; | ||||
|     index: number; | ||||
|     otherEnvironments?: IFeatureEnvironment['name'][]; | ||||
|     isDragging?: boolean; | ||||
|     onDragStartRef: ( | ||||
|         ref: RefObject<HTMLDivElement>, | ||||
|         index: number, | ||||
|     ) => DragEventHandler<HTMLButtonElement>; | ||||
|     onDragOver: ( | ||||
|         ref: RefObject<HTMLDivElement>, | ||||
|         index: number, | ||||
|     ) => DragEventHandler<HTMLDivElement>; | ||||
|     onDragEnd: () => void; | ||||
| } | ||||
| 
 | ||||
| export const StrategyDraggableItem = ({ | ||||
|     strategy, | ||||
|     index, | ||||
|     environmentName, | ||||
|     otherEnvironments, | ||||
|     isDragging, | ||||
|     onDragStartRef, | ||||
|     onDragOver, | ||||
|     onDragEnd, | ||||
| }: IStrategyDraggableItemProps) => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const featureId = useRequiredPathParam('featureId'); | ||||
|     const ref = useRef<HTMLDivElement>(null); | ||||
|     const strategyChangesFromRequest = useStrategyChangesFromRequest( | ||||
|         projectId, | ||||
|         featureId, | ||||
|         environmentName, | ||||
|         strategy.id, | ||||
|     ); | ||||
| 
 | ||||
|     const { changeRequests: scheduledChangesUsingStrategy } = | ||||
|         useScheduledChangeRequestsWithStrategy(projectId, strategy.id); | ||||
| 
 | ||||
|     return ( | ||||
|         <Box | ||||
|             key={strategy.id} | ||||
|             ref={ref} | ||||
|             onDragOver={onDragOver(ref, index)} | ||||
|             sx={{ opacity: isDragging ? '0.5' : '1' }} | ||||
|         > | ||||
|             <ConditionallyRender | ||||
|                 condition={index > 0} | ||||
|                 show={<StrategySeparator text='OR' />} | ||||
|             /> | ||||
| 
 | ||||
|             <StrategyItem | ||||
|                 strategy={strategy} | ||||
|                 environmentId={environmentName} | ||||
|                 otherEnvironments={otherEnvironments} | ||||
|                 onDragStart={onDragStartRef(ref, index)} | ||||
|                 onDragEnd={onDragEnd} | ||||
|                 orderNumber={index + 1} | ||||
|                 headerChildren={renderHeaderChildren( | ||||
|                     strategyChangesFromRequest, | ||||
|                     scheduledChangesUsingStrategy, | ||||
|                 )} | ||||
|             /> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const ChangeRequestStatusBadge = ({ | ||||
|     change, | ||||
| }: { | ||||
|     change: IFeatureChange | undefined; | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
|     const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); | ||||
| 
 | ||||
|     if (isSmallScreen) { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <Box sx={{ mr: 1.5 }}> | ||||
|             <ConditionallyRender | ||||
|                 condition={change?.action === 'updateStrategy'} | ||||
|                 show={<Badge color='warning'>Modified in draft</Badge>} | ||||
|             /> | ||||
|             <ConditionallyRender | ||||
|                 condition={change?.action === 'deleteStrategy'} | ||||
|                 show={<Badge color='error'>Deleted in draft</Badge>} | ||||
|             /> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const renderHeaderChildren = ( | ||||
|     changes?: UseStrategyChangeFromRequestResult, | ||||
|     scheduledChanges?: ScheduledChangeRequestViewModel[], | ||||
| ): JSX.Element[] => { | ||||
|     const badges: JSX.Element[] = []; | ||||
|     if (changes?.length === 0 && scheduledChanges?.length === 0) { | ||||
|         return []; | ||||
|     } | ||||
| 
 | ||||
|     const draftChange = changes?.find( | ||||
|         ({ isScheduledChange }) => !isScheduledChange, | ||||
|     ); | ||||
| 
 | ||||
|     if (draftChange) { | ||||
|         badges.push( | ||||
|             <ChangeRequestStatusBadge | ||||
|                 key={`draft-change#${draftChange.change.id}`} | ||||
|                 change={draftChange.change} | ||||
|             />, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     if (scheduledChanges && scheduledChanges.length > 0) { | ||||
|         badges.push( | ||||
|             <ChangesScheduledBadge | ||||
|                 key='scheduled-changes' | ||||
|                 scheduledChangeRequestIds={scheduledChanges.map( | ||||
|                     (scheduledChange) => scheduledChange.id, | ||||
|                 )} | ||||
|             />, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     return badges; | ||||
| }; | ||||
| @ -1,10 +1,8 @@ | ||||
| import { type DragEventHandler, type RefObject, useRef } from 'react'; | ||||
| import { Box, useMediaQuery, useTheme } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import type { IFeatureEnvironment } from 'interfaces/featureToggle'; | ||||
| import type { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| import { StrategyItem } from './StrategyItem/StrategyItem'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { | ||||
|     useStrategyChangesFromRequest, | ||||
| @ -17,6 +15,8 @@ import { | ||||
|     type ScheduledChangeRequestViewModel, | ||||
|     useScheduledChangeRequestsWithStrategy, | ||||
| } from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy'; | ||||
| import { StrategySeparator as NewStrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategyItem as NewStrategyItem } from './StrategyItem/StrategyItem'; | ||||
| 
 | ||||
| interface IStrategyDraggableItemProps { | ||||
|     strategy: IFeatureStrategy; | ||||
| @ -67,10 +67,10 @@ export const StrategyDraggableItem = ({ | ||||
|         > | ||||
|             <ConditionallyRender | ||||
|                 condition={index > 0} | ||||
|                 show={<StrategySeparator text='OR' />} | ||||
|                 show={<NewStrategySeparator text='OR' />} | ||||
|             /> | ||||
| 
 | ||||
|             <StrategyItem | ||||
|             <NewStrategyItem | ||||
|                 strategy={strategy} | ||||
|                 environmentId={environmentName} | ||||
|                 otherEnvironments={otherEnvironments} | ||||
|  | ||||
| @ -0,0 +1,181 @@ | ||||
| // deprecated; remove with the `flagOverviewRedesign` flag
 | ||||
| import type { DragEventHandler, FC } from 'react'; | ||||
| import Edit from '@mui/icons-material/Edit'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| import type { IFeatureEnvironment } from 'interfaces/featureToggle'; | ||||
| import type { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; | ||||
| import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions'; | ||||
| import { formatEditStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { StrategyExecution } from './StrategyExecution/StrategyExecution'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu'; | ||||
| import { StrategyItemContainer } from 'component/common/StrategyItemContainer/LegacyStrategyItemContainer'; | ||||
| import MenuStrategyRemove from './MenuStrategyRemove/MenuStrategyRemove'; | ||||
| import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider'; | ||||
| import { Box } from '@mui/material'; | ||||
| import { StrategyItemContainer as NewStrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer'; | ||||
| interface IStrategyItemProps { | ||||
|     environmentId: string; | ||||
|     strategy: IFeatureStrategy; | ||||
|     onDragStart?: DragEventHandler<HTMLButtonElement>; | ||||
|     onDragEnd?: DragEventHandler<HTMLButtonElement>; | ||||
|     otherEnvironments?: IFeatureEnvironment['name'][]; | ||||
|     orderNumber?: number; | ||||
|     headerChildren?: JSX.Element[] | JSX.Element; | ||||
| } | ||||
| 
 | ||||
| export const StrategyItem: FC<IStrategyItemProps> = ({ | ||||
|     environmentId, | ||||
|     strategy, | ||||
|     onDragStart, | ||||
|     onDragEnd, | ||||
|     otherEnvironments, | ||||
|     orderNumber, | ||||
|     headerChildren, | ||||
| }) => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const featureId = useRequiredPathParam('featureId'); | ||||
| 
 | ||||
|     const editStrategyPath = formatEditStrategyPath( | ||||
|         projectId, | ||||
|         featureId, | ||||
|         environmentId, | ||||
|         strategy.id, | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <StrategyItemContainer | ||||
|             strategy={strategy} | ||||
|             onDragStart={onDragStart} | ||||
|             onDragEnd={onDragEnd} | ||||
|             orderNumber={orderNumber} | ||||
|             actions={ | ||||
|                 <> | ||||
|                     {headerChildren} | ||||
|                     <ConditionallyRender | ||||
|                         condition={Boolean( | ||||
|                             otherEnvironments && otherEnvironments?.length > 0, | ||||
|                         )} | ||||
|                         show={() => ( | ||||
|                             <CopyStrategyIconMenu | ||||
|                                 environmentId={environmentId} | ||||
|                                 environments={otherEnvironments as string[]} | ||||
|                                 strategy={strategy} | ||||
|                             /> | ||||
|                         )} | ||||
|                     /> | ||||
|                     <PermissionIconButton | ||||
|                         permission={UPDATE_FEATURE_STRATEGY} | ||||
|                         environmentId={environmentId} | ||||
|                         projectId={projectId} | ||||
|                         component={Link} | ||||
|                         to={editStrategyPath} | ||||
|                         tooltipProps={{ | ||||
|                             title: 'Edit strategy', | ||||
|                         }} | ||||
|                         data-testid={`STRATEGY_EDIT-${strategy.name}`} | ||||
|                     > | ||||
|                         <Edit /> | ||||
|                     </PermissionIconButton> | ||||
|                     <MenuStrategyRemove | ||||
|                         projectId={projectId} | ||||
|                         featureId={featureId} | ||||
|                         environmentId={environmentId} | ||||
|                         strategy={strategy} | ||||
|                     /> | ||||
|                 </> | ||||
|             } | ||||
|         > | ||||
|             <StrategyExecution strategy={strategy} /> | ||||
| 
 | ||||
|             {strategy.variants && | ||||
|                 strategy.variants.length > 0 && | ||||
|                 (strategy.disabled ? ( | ||||
|                     <Box sx={{ opacity: '0.5' }}> | ||||
|                         <SplitPreviewSlider variants={strategy.variants} /> | ||||
|                     </Box> | ||||
|                 ) : ( | ||||
|                     <SplitPreviewSlider variants={strategy.variants} /> | ||||
|                 ))} | ||||
|         </StrategyItemContainer> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export const NewStrategyItem: FC<IStrategyItemProps> = ({ | ||||
|     environmentId, | ||||
|     strategy, | ||||
|     onDragStart, | ||||
|     onDragEnd, | ||||
|     otherEnvironments, | ||||
|     orderNumber, | ||||
|     headerChildren, | ||||
| }) => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const featureId = useRequiredPathParam('featureId'); | ||||
| 
 | ||||
|     const editStrategyPath = formatEditStrategyPath( | ||||
|         projectId, | ||||
|         featureId, | ||||
|         environmentId, | ||||
|         strategy.id, | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <NewStrategyItemContainer | ||||
|             strategy={strategy} | ||||
|             onDragStart={onDragStart} | ||||
|             onDragEnd={onDragEnd} | ||||
|             orderNumber={orderNumber} | ||||
|             actions={ | ||||
|                 <> | ||||
|                     {headerChildren} | ||||
|                     <ConditionallyRender | ||||
|                         condition={Boolean( | ||||
|                             otherEnvironments && otherEnvironments?.length > 0, | ||||
|                         )} | ||||
|                         show={() => ( | ||||
|                             <CopyStrategyIconMenu | ||||
|                                 environmentId={environmentId} | ||||
|                                 environments={otherEnvironments as string[]} | ||||
|                                 strategy={strategy} | ||||
|                             /> | ||||
|                         )} | ||||
|                     /> | ||||
|                     <PermissionIconButton | ||||
|                         permission={UPDATE_FEATURE_STRATEGY} | ||||
|                         environmentId={environmentId} | ||||
|                         projectId={projectId} | ||||
|                         component={Link} | ||||
|                         to={editStrategyPath} | ||||
|                         tooltipProps={{ | ||||
|                             title: 'Edit strategy', | ||||
|                         }} | ||||
|                         data-testid={`STRATEGY_EDIT-${strategy.name}`} | ||||
|                     > | ||||
|                         <Edit /> | ||||
|                     </PermissionIconButton> | ||||
|                     <MenuStrategyRemove | ||||
|                         projectId={projectId} | ||||
|                         featureId={featureId} | ||||
|                         environmentId={environmentId} | ||||
|                         strategy={strategy} | ||||
|                     /> | ||||
|                 </> | ||||
|             } | ||||
|         > | ||||
|             <StrategyExecution strategy={strategy} /> | ||||
| 
 | ||||
|             {strategy.variants && | ||||
|                 strategy.variants.length > 0 && | ||||
|                 (strategy.disabled ? ( | ||||
|                     <Box sx={{ opacity: '0.5' }}> | ||||
|                         <SplitPreviewSlider variants={strategy.variants} /> | ||||
|                     </Box> | ||||
|                 ) : ( | ||||
|                     <SplitPreviewSlider variants={strategy.variants} /> | ||||
|                 ))} | ||||
|         </NewStrategyItemContainer> | ||||
|     ); | ||||
| }; | ||||
| @ -2,7 +2,7 @@ import { type FC, Fragment, useMemo } from 'react'; | ||||
| import { Alert, Box, Chip, Link, styled } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { ConstraintItem } from './ConstraintItem/ConstraintItem'; | ||||
| import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; | ||||
| import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; | ||||
|  | ||||
| @ -10,10 +10,10 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { StrategyExecution } from './StrategyExecution/StrategyExecution'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu'; | ||||
| import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer'; | ||||
| import MenuStrategyRemove from './MenuStrategyRemove/MenuStrategyRemove'; | ||||
| import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider'; | ||||
| import { Box } from '@mui/material'; | ||||
| import { StrategyItemContainer as NewStrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer'; | ||||
| interface IStrategyItemProps { | ||||
|     environmentId: string; | ||||
|     strategy: IFeatureStrategy; | ||||
| @ -44,7 +44,7 @@ export const StrategyItem: FC<IStrategyItemProps> = ({ | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <StrategyItemContainer | ||||
|         <NewStrategyItemContainer | ||||
|             strategy={strategy} | ||||
|             onDragStart={onDragStart} | ||||
|             onDragEnd={onDragEnd} | ||||
| @ -97,6 +97,6 @@ export const StrategyItem: FC<IStrategyItemProps> = ({ | ||||
|                 ) : ( | ||||
|                     <SplitPreviewSlider variants={strategy.variants} /> | ||||
|                 ))} | ||||
|         </StrategyItemContainer> | ||||
|         </NewStrategyItemContainer> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -3,7 +3,6 @@ import type { | ||||
|     IFeatureEnvironment, | ||||
|     IFeatureEnvironmentMetrics, | ||||
| } from 'interfaces/featureToggle'; | ||||
| import EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAccordionBody'; | ||||
| import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu'; | ||||
| import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| @ -14,6 +13,7 @@ import FeatureOverviewEnvironmentMetrics from './EnvironmentHeader/FeatureOvervi | ||||
| import { FeatureOverviewEnvironmentToggle } from './EnvironmentHeader/FeatureOverviewEnvironmentToggle/FeatureOverviewEnvironmentToggle'; | ||||
| import { useState } from 'react'; | ||||
| import type { IReleasePlan } from 'interfaces/releasePlans'; | ||||
| import { EnvironmentAccordionBody as NewEnvironmentAccordionBody } from './EnvironmentAccordionBody/EnvironmentAccordionBody'; | ||||
| 
 | ||||
| const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({ | ||||
|     borderRadius: theme.shape.borderRadiusLarge, | ||||
| @ -32,15 +32,12 @@ const StyledAccordion = styled(Accordion)(({ theme }) => ({ | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({ | ||||
| const NewStyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({ | ||||
|     padding: 0, | ||||
|     background: theme.palette.envAccordion.expanded, | ||||
|     background: theme.palette.background.elevation1, | ||||
|     borderBottomLeftRadius: theme.shape.borderRadiusLarge, | ||||
|     borderBottomRightRadius: theme.shape.borderRadiusLarge, | ||||
|     boxShadow: theme.boxShadows.accordionFooter, | ||||
|     [theme.breakpoints.down('md')]: { | ||||
|         padding: theme.spacing(2, 1), | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledAccordionFooter = styled('footer')(({ theme }) => ({ | ||||
| @ -55,7 +52,6 @@ const StyledAccordionFooter = styled('footer')(({ theme }) => ({ | ||||
| const StyledEnvironmentAccordionContainer = styled('div')(({ theme }) => ({ | ||||
|     width: '100%', | ||||
|     position: 'relative', | ||||
|     padding: theme.spacing(3, 3, 1), | ||||
| })); | ||||
| 
 | ||||
| type FeatureOverviewEnvironmentProps = { | ||||
| @ -112,9 +108,9 @@ export const FeatureOverviewEnvironment = ({ | ||||
|                         collapsed={!hasActivations} | ||||
|                     /> | ||||
|                 </EnvironmentHeader> | ||||
|                 <StyledAccordionDetails> | ||||
|                 <NewStyledAccordionDetails> | ||||
|                     <StyledEnvironmentAccordionContainer> | ||||
|                         <EnvironmentAccordionBody | ||||
|                         <NewEnvironmentAccordionBody | ||||
|                             featureEnvironment={environment} | ||||
|                             isDisabled={!environment.enabled} | ||||
|                             otherEnvironments={otherEnvironments} | ||||
| @ -131,7 +127,7 @@ export const FeatureOverviewEnvironment = ({ | ||||
|                             <UpgradeChangeRequests /> | ||||
|                         ) : null} | ||||
|                     </StyledAccordionFooter> | ||||
|                 </StyledAccordionDetails> | ||||
|                 </NewStyledAccordionDetails> | ||||
|             </StyledAccordion> | ||||
|         </StyledFeatureOverviewEnvironment> | ||||
|     ); | ||||
|  | ||||
| @ -13,7 +13,7 @@ import { getFeatureMetrics } from 'utils/getFeatureMetrics'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon'; | ||||
| import StringTruncator from 'component/common/StringTruncator/StringTruncator'; | ||||
| import EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAccordionBody'; | ||||
| import EnvironmentAccordionBody from './EnvironmentAccordionBody/LegacyEnvironmentAccordionBody'; | ||||
| import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter'; | ||||
| import FeatureOverviewEnvironmentMetrics from './EnvironmentHeader/FeatureOverviewEnvironmentMetrics/LegacyFeatureOverviewEnvironmentMetrics'; | ||||
| import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu'; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { Fragment } from 'react'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { SegmentItem } from '../../../../common/SegmentItem/SegmentItem'; | ||||
| import type { ISegment } from 'interfaces/segment'; | ||||
| 
 | ||||
|  | ||||
| @ -8,7 +8,7 @@ import { | ||||
| import type { IReleasePlanMilestone } from 'interfaces/releasePlans'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { ReleasePlanMilestoneStrategy } from './ReleasePlanMilestoneStrategy'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { | ||||
|     ReleasePlanMilestoneStatus, | ||||
|     type MilestoneStatus, | ||||
|  | ||||
| @ -5,7 +5,7 @@ import type { | ||||
| } from 'openapi'; | ||||
| import { objectId } from 'utils/objectId'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { styled } from '@mui/material'; | ||||
| import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView'; | ||||
| import { ConstraintError } from './ConstraintError/ConstraintError'; | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { Fragment, type VFC } from 'react'; | ||||
| import type { PlaygroundConstraintSchema } from 'openapi'; | ||||
| import { objectId } from 'utils/objectId'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { styled } from '@mui/material'; | ||||
| import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView'; | ||||
| 
 | ||||
|  | ||||
| @ -5,7 +5,7 @@ import { | ||||
|     parseParameterStrings, | ||||
| } from 'utils/parseParameter'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; | ||||
| import { CustomParameterItem } from './CustomParameterItem/CustomParameterItem'; | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { Fragment, type VFC } from 'react'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { styled } from '@mui/material'; | ||||
| import type { | ||||
|     PlaygroundRequestSchema, | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { Fragment, type VFC } from 'react'; | ||||
| import type { PlaygroundSegmentSchema, PlaygroundRequestSchema } from 'openapi'; | ||||
| import { ConstraintExecution } from '../ConstraintExecution/ConstraintExecution'; | ||||
| import CancelOutlined from '@mui/icons-material/CancelOutlined'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { styled, Typography } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { SegmentItem } from 'component/common/SegmentItem/SegmentItem'; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { Fragment, type VFC } from 'react'; | ||||
| import type { PlaygroundSegmentSchema } from 'openapi'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { SegmentItem } from 'component/common/SegmentItem/SegmentItem'; | ||||
| import { ConstraintExecutionWithoutResults } from '../ConstraintExecution/ConstraintExecutionWithoutResults'; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { Fragment, type VFC } from 'react'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { styled } from '@mui/material'; | ||||
| import type { | ||||
|     PlaygroundRequestSchema, | ||||
|  | ||||
| @ -7,7 +7,7 @@ import type { | ||||
| } from 'openapi'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| 
 | ||||
| const StyledAlertWrapper = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|  | ||||
| @ -3,7 +3,7 @@ import { type DragEventHandler, type RefObject, useRef } from 'react'; | ||||
| import { Box, IconButton } from '@mui/material'; | ||||
| import Edit from '@mui/icons-material/Edit'; | ||||
| import Delete from '@mui/icons-material/DeleteOutlined'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; | ||||
| import { MilestoneStrategyItem } from './MilestoneStrategyItem'; | ||||
| 
 | ||||
| interface IMilestoneStrategyDraggableItemProps { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user