1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02: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:

![image](https://github.com/user-attachments/assets/cd49c283-6044-46d4-bcef-182cb6a1de4e)

Flag off:

![image](https://github.com/user-attachments/assets/7ef92b6d-31e5-4218-90b2-dedd5e6cc6de)

## 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:
Thomas Heartman 2025-02-26 16:24:50 +01:00 committed by GitHub
parent 192bd83fa6
commit e25fb9f7c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1009 additions and 105 deletions

View File

@ -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[];

View File

@ -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 {

View File

@ -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>
);
};

View File

@ -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 () => {

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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;

View File

@ -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;
};

View File

@ -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}

View File

@ -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>
);
};

View File

@ -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';

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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';

View File

@ -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';

View File

@ -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,

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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,

View File

@ -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';

View File

@ -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';

View File

@ -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,

View File

@ -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',

View File

@ -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 {