mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-18 00:19:49 +01:00
feat: release plan template milestone UI listing strategies (#8933)
This commit is contained in:
parent
e8179d4dd2
commit
15950e4ea0
frontend/src
@ -1,12 +1,24 @@
|
|||||||
import Input from 'component/common/Input/Input';
|
import Input from 'component/common/Input/Input';
|
||||||
import { Box, Button, Card, Grid, Popover, styled } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
|
Popover,
|
||||||
|
styled,
|
||||||
|
Accordion,
|
||||||
|
AccordionSummary,
|
||||||
|
AccordionDetails,
|
||||||
|
} from '@mui/material';
|
||||||
import Edit from '@mui/icons-material/Edit';
|
import Edit from '@mui/icons-material/Edit';
|
||||||
import type {
|
import type {
|
||||||
IReleasePlanMilestonePayload,
|
IReleasePlanMilestonePayload,
|
||||||
IReleasePlanMilestoneStrategy,
|
IReleasePlanMilestoneStrategy,
|
||||||
} from 'interfaces/releasePlans';
|
} from 'interfaces/releasePlans';
|
||||||
import { useState } from 'react';
|
import { type DragEventHandler, type RefObject, useState } from 'react';
|
||||||
|
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||||
import { MilestoneStrategyMenuCards } from './MilestoneStrategyMenuCards';
|
import { MilestoneStrategyMenuCards } from './MilestoneStrategyMenuCards';
|
||||||
|
import { MilestoneStrategyDraggableItem } from './MilestoneStrategyDraggableItem';
|
||||||
|
|
||||||
const StyledEditIcon = styled(Edit)(({ theme }) => ({
|
const StyledEditIcon = styled(Edit)(({ theme }) => ({
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@ -24,6 +36,7 @@ const StyledMilestoneCard = styled(Card)(({ theme }) => ({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
@ -32,7 +45,6 @@ const StyledMilestoneCard = styled(Card)(({ theme }) => ({
|
|||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.neutral.light,
|
backgroundColor: theme.palette.neutral.light,
|
||||||
},
|
},
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledMilestoneCardBody = styled(Box)(({ theme }) => ({
|
const StyledMilestoneCardBody = styled(Box)(({ theme }) => ({
|
||||||
@ -53,9 +65,58 @@ const StyledMilestoneCardTitle = styled('span')(({ theme }) => ({
|
|||||||
fontSize: theme.fontSizes.bodySize,
|
fontSize: theme.fontSizes.bodySize,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledAddStrategyButton = styled(Button)(({ theme }) => ({}));
|
||||||
|
|
||||||
|
const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
boxShadow: 'none',
|
||||||
|
background: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
transition: 'background-color 0.2s ease-in-out',
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.neutral.light,
|
||||||
|
},
|
||||||
|
'&.Mui-expanded': {
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||||
|
boxShadow: 'none',
|
||||||
|
padding: theme.spacing(1.5, 2),
|
||||||
|
[theme.breakpoints.down(400)]: {
|
||||||
|
padding: theme.spacing(1, 2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||||
|
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
||||||
|
borderBottomRightRadius: theme.shape.borderRadiusMedium,
|
||||||
|
padding: theme.spacing(0),
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
padding: theme.spacing(2, 1),
|
||||||
|
},
|
||||||
|
backgroundColor: theme.palette.neutral.light,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledAccordionFooter = styled(Grid)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
paddingTop: 0,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
|
}));
|
||||||
|
|
||||||
interface IMilestoneCardProps {
|
interface IMilestoneCardProps {
|
||||||
milestone: IReleasePlanMilestonePayload;
|
milestone: IReleasePlanMilestonePayload;
|
||||||
milestoneNameChanged: (milestoneId: string, name: string) => void;
|
milestoneChanged: (milestone: IReleasePlanMilestonePayload) => void;
|
||||||
showAddStrategyDialog: (
|
showAddStrategyDialog: (
|
||||||
milestoneId: string,
|
milestoneId: string,
|
||||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
@ -66,13 +127,18 @@ interface IMilestoneCardProps {
|
|||||||
|
|
||||||
export const MilestoneCard = ({
|
export const MilestoneCard = ({
|
||||||
milestone,
|
milestone,
|
||||||
milestoneNameChanged,
|
milestoneChanged,
|
||||||
showAddStrategyDialog,
|
showAddStrategyDialog,
|
||||||
errors,
|
errors,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
}: IMilestoneCardProps) => {
|
}: IMilestoneCardProps) => {
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
const [anchor, setAnchor] = useState<Element>();
|
const [anchor, setAnchor] = useState<Element>();
|
||||||
|
const [dragItem, setDragItem] = useState<{
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
height: number;
|
||||||
|
} | null>(null);
|
||||||
const isPopoverOpen = Boolean(anchor);
|
const isPopoverOpen = Boolean(anchor);
|
||||||
const popoverId = isPopoverOpen
|
const popoverId = isPopoverOpen
|
||||||
? 'MilestoneStrategyMenuPopover'
|
? 'MilestoneStrategyMenuPopover'
|
||||||
@ -83,82 +149,250 @@ export const MilestoneCard = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSelectStrategy = (
|
const onSelectStrategy = (
|
||||||
milestoneId: string,
|
|
||||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => {
|
) => {
|
||||||
showAddStrategyDialog(milestone.id, strategy);
|
showAddStrategyDialog(milestone.id, strategy);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const onDragOver =
|
||||||
<StyledMilestoneCard>
|
(targetId: string) =>
|
||||||
<StyledMilestoneCardBody>
|
(
|
||||||
<Grid container>
|
ref: RefObject<HTMLDivElement>,
|
||||||
<StyledGridItem item xs={10} md={10}>
|
targetIndex: number,
|
||||||
{editMode && (
|
): DragEventHandler<HTMLDivElement> =>
|
||||||
<StyledInput
|
(event) => {
|
||||||
label=''
|
if (dragItem === null || ref.current === null) return;
|
||||||
value={milestone.name}
|
if (dragItem.index === targetIndex || targetId === dragItem.id)
|
||||||
onChange={(e) =>
|
return;
|
||||||
milestoneNameChanged(
|
|
||||||
milestone.id,
|
const { top, bottom } = ref.current.getBoundingClientRect();
|
||||||
e.target.value,
|
const overTargetTop = event.clientY - top < dragItem.height;
|
||||||
)
|
const overTargetBottom = bottom - event.clientY < dragItem.height;
|
||||||
}
|
const draggingUp = dragItem.index > targetIndex;
|
||||||
error={Boolean(errors?.name)}
|
|
||||||
errorText={errors?.name}
|
// prevent oscillating by only reordering if there is sufficient space
|
||||||
onFocus={() => clearErrors()}
|
if (
|
||||||
onBlur={() => setEditMode(false)}
|
(overTargetTop && draggingUp) ||
|
||||||
autoFocus
|
(overTargetBottom && !draggingUp)
|
||||||
onKeyDownCapture={(e) => {
|
) {
|
||||||
if (e.code === 'Enter') {
|
const oldStrategies = milestone.strategies || [];
|
||||||
e.preventDefault();
|
const newStrategies = [...oldStrategies];
|
||||||
e.stopPropagation();
|
const movedStrategy = newStrategies.splice(
|
||||||
setEditMode(false);
|
dragItem.index,
|
||||||
|
1,
|
||||||
|
)[0];
|
||||||
|
newStrategies.splice(targetIndex, 0, movedStrategy);
|
||||||
|
milestoneChanged({ ...milestone, strategies: newStrategies });
|
||||||
|
setDragItem({
|
||||||
|
...dragItem,
|
||||||
|
index: targetIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragStartRef =
|
||||||
|
(
|
||||||
|
ref: RefObject<HTMLDivElement>,
|
||||||
|
index: number,
|
||||||
|
): DragEventHandler<HTMLButtonElement> =>
|
||||||
|
(event) => {
|
||||||
|
if (!ref.current || !milestone.strategies) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragItem({
|
||||||
|
id: milestone.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 onDragEnd = () => {
|
||||||
|
setDragItem(null);
|
||||||
|
onReOrderStrategies();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onReOrderStrategies = () => {
|
||||||
|
if (!milestone.strategies) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newStrategies = [...milestone.strategies];
|
||||||
|
newStrategies.forEach((strategy, index) => {
|
||||||
|
strategy.sortOrder = index;
|
||||||
|
});
|
||||||
|
milestoneChanged({ ...milestone, strategies: newStrategies });
|
||||||
|
};
|
||||||
|
|
||||||
|
const milestoneStrategyDeleted = (strategyId: string) => {
|
||||||
|
const strategies = milestone.strategies || [];
|
||||||
|
milestoneChanged({
|
||||||
|
...milestone,
|
||||||
|
strategies: [
|
||||||
|
...strategies.filter((strat) => strat.id !== strategyId),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const milestoneNameChanged = (name: string) => {
|
||||||
|
milestoneChanged({ ...milestone, name });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!milestone.strategies || milestone.strategies.length === 0) {
|
||||||
|
return (
|
||||||
|
<StyledMilestoneCard>
|
||||||
|
<StyledMilestoneCardBody>
|
||||||
|
<Grid container>
|
||||||
|
<StyledGridItem item xs={10} md={10}>
|
||||||
|
{editMode && (
|
||||||
|
<StyledInput
|
||||||
|
label=''
|
||||||
|
value={milestone.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
milestoneNameChanged(e.target.value)
|
||||||
}
|
}
|
||||||
}}
|
error={Boolean(errors?.name)}
|
||||||
/>
|
errorText={errors?.name}
|
||||||
)}
|
onFocus={() => clearErrors()}
|
||||||
{!editMode && (
|
onBlur={() => setEditMode(false)}
|
||||||
<>
|
autoFocus
|
||||||
<StyledMilestoneCardTitle
|
onKeyDownCapture={(e) => {
|
||||||
onClick={() => setEditMode(true)}
|
if (e.code === 'Enter') {
|
||||||
>
|
e.preventDefault();
|
||||||
{milestone.name}
|
e.stopPropagation();
|
||||||
</StyledMilestoneCardTitle>
|
setEditMode(false);
|
||||||
<StyledEditIcon
|
}
|
||||||
onClick={() => setEditMode(true)}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
)}
|
||||||
)}
|
{!editMode && (
|
||||||
</StyledGridItem>
|
<>
|
||||||
<Grid item xs={2} md={2}>
|
<StyledMilestoneCardTitle
|
||||||
<Button
|
onClick={() => setEditMode(true)}
|
||||||
variant='outlined'
|
>
|
||||||
color='primary'
|
{milestone.name}
|
||||||
onClick={(ev) => setAnchor(ev.currentTarget)}
|
</StyledMilestoneCardTitle>
|
||||||
>
|
<StyledEditIcon
|
||||||
Add strategy
|
onClick={() => setEditMode(true)}
|
||||||
</Button>
|
/>
|
||||||
<Popover
|
</>
|
||||||
id={popoverId}
|
)}
|
||||||
open={isPopoverOpen}
|
</StyledGridItem>
|
||||||
anchorEl={anchor}
|
<Grid item xs={2} md={2}>
|
||||||
onClose={onClose}
|
<Button
|
||||||
onClick={onClose}
|
variant='outlined'
|
||||||
PaperProps={{
|
color='primary'
|
||||||
sx: (theme) => ({
|
onClick={(ev) => setAnchor(ev.currentTarget)}
|
||||||
paddingBottom: theme.spacing(1),
|
>
|
||||||
}),
|
Add strategy
|
||||||
}}
|
</Button>
|
||||||
>
|
<Popover
|
||||||
<MilestoneStrategyMenuCards
|
id={popoverId}
|
||||||
milestoneId={milestone.id}
|
open={isPopoverOpen}
|
||||||
openAddStrategy={onSelectStrategy}
|
anchorEl={anchor}
|
||||||
/>
|
onClose={onClose}
|
||||||
</Popover>
|
onClick={onClose}
|
||||||
|
PaperProps={{
|
||||||
|
sx: (theme) => ({
|
||||||
|
paddingBottom: theme.spacing(1),
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MilestoneStrategyMenuCards
|
||||||
|
openEditAddStrategy={onSelectStrategy}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</StyledMilestoneCardBody>
|
||||||
</StyledMilestoneCardBody>
|
</StyledMilestoneCard>
|
||||||
</StyledMilestoneCard>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledAccordion>
|
||||||
|
<StyledAccordionSummary
|
||||||
|
expandIcon={<ExpandMore titleAccess='Toggle' />}
|
||||||
|
>
|
||||||
|
{editMode && (
|
||||||
|
<StyledInput
|
||||||
|
label=''
|
||||||
|
value={milestone.name}
|
||||||
|
onChange={(e) => milestoneNameChanged(e.target.value)}
|
||||||
|
error={Boolean(errors?.name)}
|
||||||
|
errorText={errors?.name}
|
||||||
|
onFocus={() => clearErrors()}
|
||||||
|
onBlur={() => setEditMode(false)}
|
||||||
|
autoFocus
|
||||||
|
onKeyDownCapture={(e) => {
|
||||||
|
if (e.code === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setEditMode(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!editMode && (
|
||||||
|
<>
|
||||||
|
<StyledMilestoneCardTitle
|
||||||
|
onClick={() => setEditMode(true)}
|
||||||
|
>
|
||||||
|
{milestone.name}
|
||||||
|
</StyledMilestoneCardTitle>
|
||||||
|
<StyledEditIcon onClick={() => setEditMode(true)} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</StyledAccordionSummary>
|
||||||
|
<StyledAccordionDetails>
|
||||||
|
{milestone.strategies.map((strg, index) => (
|
||||||
|
<div key={strg.id}>
|
||||||
|
<MilestoneStrategyDraggableItem
|
||||||
|
index={index}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onDragStartRef={onDragStartRef}
|
||||||
|
onDragOver={onDragOver(strg.id)}
|
||||||
|
onDeleteClick={() =>
|
||||||
|
milestoneStrategyDeleted(strg.id)
|
||||||
|
}
|
||||||
|
onEditClick={() => {
|
||||||
|
onSelectStrategy(strg);
|
||||||
|
}}
|
||||||
|
isDragging={false}
|
||||||
|
strategy={strg}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<StyledAccordionFooter>
|
||||||
|
<StyledAddStrategyButton
|
||||||
|
variant='outlined'
|
||||||
|
color='primary'
|
||||||
|
onClick={(ev) => setAnchor(ev.currentTarget)}
|
||||||
|
>
|
||||||
|
Add strategy
|
||||||
|
</StyledAddStrategyButton>
|
||||||
|
<Popover
|
||||||
|
id={popoverId}
|
||||||
|
open={isPopoverOpen}
|
||||||
|
anchorEl={anchor}
|
||||||
|
onClose={onClose}
|
||||||
|
onClick={onClose}
|
||||||
|
PaperProps={{
|
||||||
|
sx: (theme) => ({
|
||||||
|
paddingBottom: theme.spacing(1),
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MilestoneStrategyMenuCards
|
||||||
|
openEditAddStrategy={onSelectStrategy}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
</StyledAccordionFooter>
|
||||||
|
</StyledAccordionDetails>
|
||||||
|
</StyledAccordion>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,8 +3,7 @@ import type {
|
|||||||
IReleasePlanMilestoneStrategy,
|
IReleasePlanMilestoneStrategy,
|
||||||
} from 'interfaces/releasePlans';
|
} from 'interfaces/releasePlans';
|
||||||
import { MilestoneCard } from './MilestoneCard';
|
import { MilestoneCard } from './MilestoneCard';
|
||||||
import { styled } from '@mui/material';
|
import { styled, Button } from '@mui/material';
|
||||||
import { Button } from '@mui/material';
|
|
||||||
import Add from '@mui/icons-material/Add';
|
import Add from '@mui/icons-material/Add';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
@ -19,6 +18,7 @@ interface IMilestoneListProps {
|
|||||||
) => void;
|
) => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
|
milestoneChanged: (milestone: IReleasePlanMilestonePayload) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledAddMilestoneButton = styled(Button)(({ theme }) => ({
|
const StyledAddMilestoneButton = styled(Button)(({ theme }) => ({
|
||||||
@ -32,24 +32,15 @@ export const MilestoneList = ({
|
|||||||
openAddStrategyForm,
|
openAddStrategyForm,
|
||||||
errors,
|
errors,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
|
milestoneChanged,
|
||||||
}: IMilestoneListProps) => {
|
}: IMilestoneListProps) => {
|
||||||
const milestoneNameChanged = (milestoneId: string, name: string) => {
|
|
||||||
setMilestones((prev) =>
|
|
||||||
prev.map((milestone) =>
|
|
||||||
milestone.id === milestoneId
|
|
||||||
? { ...milestone, name }
|
|
||||||
: milestone,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{milestones.map((milestone) => (
|
{milestones.map((milestone) => (
|
||||||
<MilestoneCard
|
<MilestoneCard
|
||||||
key={milestone.id}
|
key={milestone.id}
|
||||||
milestone={milestone}
|
milestone={milestone}
|
||||||
milestoneNameChanged={milestoneNameChanged}
|
milestoneChanged={milestoneChanged}
|
||||||
showAddStrategyDialog={openAddStrategyForm}
|
showAddStrategyDialog={openAddStrategyForm}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
clearErrors={clearErrors}
|
clearErrors={clearErrors}
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans';
|
||||||
|
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 { MilestoneStrategyItem } from './MilestoneStrategyItem';
|
||||||
|
|
||||||
|
interface IMilestoneStrategyDraggableItemProps {
|
||||||
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>;
|
||||||
|
index: number;
|
||||||
|
isDragging?: boolean;
|
||||||
|
onDragStartRef: (
|
||||||
|
ref: RefObject<HTMLDivElement>,
|
||||||
|
index: number,
|
||||||
|
) => DragEventHandler<HTMLButtonElement>;
|
||||||
|
onDragOver: (
|
||||||
|
ref: RefObject<HTMLDivElement>,
|
||||||
|
index: number,
|
||||||
|
) => DragEventHandler<HTMLDivElement>;
|
||||||
|
onDragEnd: () => void;
|
||||||
|
onDeleteClick: () => void;
|
||||||
|
onEditClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MilestoneStrategyDraggableItem = ({
|
||||||
|
strategy,
|
||||||
|
index,
|
||||||
|
isDragging,
|
||||||
|
onDragStartRef,
|
||||||
|
onDragOver,
|
||||||
|
onDragEnd,
|
||||||
|
onDeleteClick,
|
||||||
|
onEditClick,
|
||||||
|
}: IMilestoneStrategyDraggableItemProps) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={strategy.id}
|
||||||
|
ref={ref}
|
||||||
|
onDragOver={onDragOver(ref, index)}
|
||||||
|
sx={{ opacity: isDragging ? '0.5' : '1' }}
|
||||||
|
>
|
||||||
|
{index > 0 && <StrategySeparator text='OR' />}
|
||||||
|
<MilestoneStrategyItem
|
||||||
|
strategy={strategy}
|
||||||
|
onDragStart={onDragStartRef(ref, index)}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<IconButton title='Edit strategy' onClick={onEditClick}>
|
||||||
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
title='Remove release plan'
|
||||||
|
onClick={onDeleteClick}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,99 @@
|
|||||||
|
import { Box, IconButton, styled } from '@mui/material';
|
||||||
|
import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider';
|
||||||
|
import {
|
||||||
|
formatStrategyName,
|
||||||
|
getFeatureStrategyIcon,
|
||||||
|
} from 'utils/strategyNames';
|
||||||
|
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
|
import { StrategyExecution } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
|
||||||
|
import type { DragEventHandler, ReactNode } from 'react';
|
||||||
|
import DragIndicator from '@mui/icons-material/DragIndicator';
|
||||||
|
|
||||||
|
const StyledStrategy = styled('div')(({ theme }) => ({
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const DragIcon = styled(IconButton)({
|
||||||
|
padding: 0,
|
||||||
|
cursor: 'inherit',
|
||||||
|
transition: 'color 0.2s ease-in-out',
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledHeader = styled('div', {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'draggable',
|
||||||
|
})<{ draggable: boolean }>(({ theme, draggable }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
alignItems: 'center',
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'& > svg': {
|
||||||
|
fill: theme.palette.action.disabled,
|
||||||
|
},
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledStrategyExecution = styled('div')(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IReleasePlanMilestoneStrategyProps {
|
||||||
|
strategy: IFeatureStrategy;
|
||||||
|
onDragStart?: DragEventHandler<HTMLButtonElement>;
|
||||||
|
onDragEnd?: DragEventHandler<HTMLButtonElement>;
|
||||||
|
actions?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MilestoneStrategyItem = ({
|
||||||
|
strategy,
|
||||||
|
onDragStart,
|
||||||
|
onDragEnd,
|
||||||
|
actions,
|
||||||
|
}: IReleasePlanMilestoneStrategyProps) => {
|
||||||
|
const Icon = getFeatureStrategyIcon(strategy.strategyName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledStrategy>
|
||||||
|
<StyledHeader draggable={Boolean(onDragStart)}>
|
||||||
|
<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 />
|
||||||
|
{`${formatStrategyName(String(strategy.strategyName))}${strategy.title ? `: ${strategy.title}` : ''}`}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
minHeight: (theme) => theme.spacing(6),
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{actions}
|
||||||
|
</Box>
|
||||||
|
</StyledHeader>
|
||||||
|
<StyledStrategyExecution>
|
||||||
|
<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} />
|
||||||
|
))}
|
||||||
|
</StyledStrategyExecution>
|
||||||
|
</StyledStrategy>
|
||||||
|
);
|
||||||
|
};
|
@ -66,9 +66,16 @@ export const MilestoneStrategyMenuCard = ({
|
|||||||
<StyledCard
|
<StyledCard
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const strat = createFeatureStrategy('', strategy);
|
const strat = createFeatureStrategy('', strategy);
|
||||||
|
if (strat.name === 'flexibleRollout') {
|
||||||
|
strat.parameters = {
|
||||||
|
...strat.parameters,
|
||||||
|
groupId: '{{featureName}}',
|
||||||
|
};
|
||||||
|
}
|
||||||
onClick({
|
onClick({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
name: strat.name,
|
name: strat.name,
|
||||||
|
strategyName: strat.name,
|
||||||
title: '',
|
title: '',
|
||||||
constraints: strat.constraints,
|
constraints: strat.constraints,
|
||||||
parameters: strat.parameters,
|
parameters: strat.parameters,
|
||||||
|
@ -9,16 +9,13 @@ const StyledTypography = styled(Typography)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface IMilestoneStrategyMenuCardsProps {
|
interface IMilestoneStrategyMenuCardsProps {
|
||||||
milestoneId: string;
|
openEditAddStrategy: (
|
||||||
openAddStrategy: (
|
|
||||||
milestoneId: string,
|
|
||||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MilestoneStrategyMenuCards = ({
|
export const MilestoneStrategyMenuCards = ({
|
||||||
milestoneId,
|
openEditAddStrategy,
|
||||||
openAddStrategy,
|
|
||||||
}: IMilestoneStrategyMenuCardsProps) => {
|
}: IMilestoneStrategyMenuCardsProps) => {
|
||||||
const { strategies } = useStrategies();
|
const { strategies } = useStrategies();
|
||||||
|
|
||||||
@ -29,7 +26,7 @@ export const MilestoneStrategyMenuCards = ({
|
|||||||
const onClick = (
|
const onClick = (
|
||||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => {
|
) => {
|
||||||
openAddStrategy(milestoneId, strategy);
|
openEditAddStrategy(strategy);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -24,6 +24,7 @@ import { MilestoneStrategySegment } from './MilestoneStrategySegment';
|
|||||||
import { MilestoneStrategyConstraints } from './MilestoneStrategyConstraints';
|
import { MilestoneStrategyConstraints } from './MilestoneStrategyConstraints';
|
||||||
import { MilestoneStrategyVariants } from './MilestoneStrategyVariants';
|
import { MilestoneStrategyVariants } from './MilestoneStrategyVariants';
|
||||||
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
|
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
|
||||||
|
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
||||||
|
|
||||||
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||||
marginLeft: theme.spacing(3),
|
marginLeft: theme.spacing(3),
|
||||||
@ -121,7 +122,7 @@ interface IReleasePlanTemplateAddStrategyFormProps {
|
|||||||
milestoneId: string | undefined;
|
milestoneId: string | undefined;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>;
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>;
|
||||||
onAddStrategy: (
|
onAddUpdateStrategy: (
|
||||||
milestoneId: string,
|
milestoneId: string,
|
||||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => void;
|
) => void;
|
||||||
@ -131,27 +132,34 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
milestoneId,
|
milestoneId,
|
||||||
onCancel,
|
onCancel,
|
||||||
strategy,
|
strategy,
|
||||||
onAddStrategy,
|
onAddUpdateStrategy,
|
||||||
}: IReleasePlanTemplateAddStrategyFormProps) => {
|
}: IReleasePlanTemplateAddStrategyFormProps) => {
|
||||||
const [addStrategy, setAddStrategy] = useState(strategy);
|
const [currentStrategy, setCurrentStrategy] = useState(strategy);
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const { segments: allSegments, refetchSegments } = useSegments();
|
||||||
const [segments, setSegments] = useState<ISegment[]>([]);
|
const [segments, setSegments] = useState<ISegment[]>([]);
|
||||||
const { strategyDefinition } = useStrategy(strategy?.name);
|
const { strategyDefinition } = useStrategy(strategy?.strategyName);
|
||||||
const hasValidConstraints = useConstraintsValidation(strategy?.constraints);
|
const hasValidConstraints = useConstraintsValidation(strategy?.constraints);
|
||||||
const errors = useFormErrors();
|
const errors = useFormErrors();
|
||||||
const showVariants = Boolean(
|
const showVariants = Boolean(
|
||||||
addStrategy?.parameters && 'stickiness' in addStrategy?.parameters,
|
currentStrategy?.parameters &&
|
||||||
|
'stickiness' in currentStrategy?.parameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
const stickiness =
|
const stickiness =
|
||||||
addStrategy?.parameters && 'stickiness' in addStrategy?.parameters
|
currentStrategy?.parameters &&
|
||||||
? String(addStrategy.parameters.stickiness)
|
'stickiness' in currentStrategy?.parameters
|
||||||
|
? String(currentStrategy.parameters.stickiness)
|
||||||
: 'default';
|
: 'default';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAddStrategy((prev) => ({
|
setSegments([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentStrategy((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
variants: (addStrategy.variants || []).map((variant) => ({
|
variants: (currentStrategy.variants || []).map((variant) => ({
|
||||||
stickiness,
|
stickiness,
|
||||||
name: variant.name,
|
name: variant.name,
|
||||||
weight: variant.weight,
|
weight: variant.weight,
|
||||||
@ -159,9 +167,9 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
weightType: variant.weightType,
|
weightType: variant.weightType,
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
}, [stickiness, JSON.stringify(addStrategy.variants)]);
|
}, [stickiness, JSON.stringify(currentStrategy.variants)]);
|
||||||
|
|
||||||
if (!strategy || !addStrategy || !strategyDefinition) {
|
if (!strategy || !currentStrategy || !strategyDefinition) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +178,7 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getTargetingCount = () => {
|
const getTargetingCount = () => {
|
||||||
const constraintCount = addStrategy?.constraints?.length || 0;
|
const constraintCount = currentStrategy?.constraints?.length || 0;
|
||||||
const segmentCount = segments?.length || 0;
|
const segmentCount = segments?.length || 0;
|
||||||
|
|
||||||
return constraintCount + segmentCount;
|
return constraintCount + segmentCount;
|
||||||
@ -178,7 +186,7 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
|
|
||||||
const validateParameter = (key: string, value: string) => true;
|
const validateParameter = (key: string, value: string) => true;
|
||||||
const updateParameter = (name: string, value: string) => {
|
const updateParameter = (name: string, value: string) => {
|
||||||
setAddStrategy(
|
setCurrentStrategy(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
if (!draft) {
|
if (!draft) {
|
||||||
return;
|
return;
|
||||||
@ -194,11 +202,12 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addStrategyToMilestone = () => {
|
const AddUpdateMilestoneStrategy = () => {
|
||||||
if (!milestoneId) {
|
if (!milestoneId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onAddStrategy(milestoneId, addStrategy);
|
|
||||||
|
onAddUpdateStrategy(milestoneId, currentStrategy);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -208,15 +217,17 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
>
|
>
|
||||||
<StyledHeaderBox>
|
<StyledHeaderBox>
|
||||||
<StyledTitle>
|
<StyledTitle>
|
||||||
{formatStrategyName(addStrategy.name || '')}
|
{formatStrategyName(currentStrategy.strategyName || '')}
|
||||||
{addStrategy.name === 'flexibleRollout' && (
|
{currentStrategy.strategyName === 'flexibleRollout' && (
|
||||||
<Badge color='success' sx={{ marginLeft: '1rem' }}>
|
<Badge color='success' sx={{ marginLeft: '1rem' }}>
|
||||||
{addStrategy.parameters?.rollout}%
|
{currentStrategy.parameters?.rollout}%
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</StyledTitle>
|
</StyledTitle>
|
||||||
</StyledHeaderBox>
|
</StyledHeaderBox>
|
||||||
{!BuiltInStrategies.includes(strategy.name || 'default') && (
|
{!BuiltInStrategies.includes(
|
||||||
|
strategy.strategyName || 'default',
|
||||||
|
) && (
|
||||||
<StyledAlertBox>
|
<StyledAlertBox>
|
||||||
<Alert severity='warning'>
|
<Alert severity='warning'>
|
||||||
Custom strategies are deprecated. We recommend not
|
Custom strategies are deprecated. We recommend not
|
||||||
@ -251,7 +262,7 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
<Typography>
|
<Typography>
|
||||||
Variants
|
Variants
|
||||||
<StyledBadge>
|
<StyledBadge>
|
||||||
{addStrategy?.variants?.length || 0}
|
{currentStrategy?.variants?.length || 0}
|
||||||
</StyledBadge>
|
</StyledBadge>
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
@ -262,16 +273,16 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
{activeTab === 0 && (
|
{activeTab === 0 && (
|
||||||
<>
|
<>
|
||||||
<MilestoneStrategyTitle
|
<MilestoneStrategyTitle
|
||||||
title={addStrategy.title || ''}
|
title={currentStrategy.title || ''}
|
||||||
setTitle={(title) =>
|
setTitle={(title) =>
|
||||||
updateParameter('title', title)
|
updateParameter('title', title)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MilestoneStrategyType
|
<MilestoneStrategyType
|
||||||
strategy={addStrategy}
|
strategy={currentStrategy}
|
||||||
strategyDefinition={strategyDefinition}
|
strategyDefinition={strategyDefinition}
|
||||||
parameters={addStrategy.parameters}
|
parameters={currentStrategy.parameters}
|
||||||
updateParameter={updateParameter}
|
updateParameter={updateParameter}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
/>
|
/>
|
||||||
@ -291,8 +302,8 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
<StyledDividerContent>AND</StyledDividerContent>
|
<StyledDividerContent>AND</StyledDividerContent>
|
||||||
</StyledBox>
|
</StyledBox>
|
||||||
<MilestoneStrategyConstraints
|
<MilestoneStrategyConstraints
|
||||||
strategy={addStrategy}
|
strategy={currentStrategy}
|
||||||
setStrategy={setAddStrategy}
|
setStrategy={setCurrentStrategy}
|
||||||
/>
|
/>
|
||||||
be evaluated for users and applications that match
|
be evaluated for users and applications that match
|
||||||
the specified preconditions.
|
the specified preconditions.
|
||||||
@ -301,8 +312,8 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
)}
|
)}
|
||||||
{activeTab === 2 && showVariants && (
|
{activeTab === 2 && showVariants && (
|
||||||
<MilestoneStrategyVariants
|
<MilestoneStrategyVariants
|
||||||
strategy={addStrategy}
|
strategy={currentStrategy}
|
||||||
setStrategy={setAddStrategy}
|
setStrategy={setCurrentStrategy}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledContentDiv>
|
</StyledContentDiv>
|
||||||
@ -312,7 +323,7 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
color='primary'
|
color='primary'
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={!hasValidConstraints || errors.hasFormErrors()}
|
disabled={!hasValidConstraints || errors.hasFormErrors()}
|
||||||
onClick={addStrategyToMilestone}
|
onClick={AddUpdateMilestoneStrategy}
|
||||||
>
|
>
|
||||||
Save strategy
|
Save strategy
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -58,7 +58,7 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const [addStrategyOpen, setAddStrategyOpen] = useState(false);
|
const [addUpdateStrategyOpen, setAddUpdateStrategyOpen] = useState(false);
|
||||||
const [activeMilestoneId, setActiveMilestoneId] = useState<
|
const [activeMilestoneId, setActiveMilestoneId] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
@ -71,37 +71,49 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({
|
|||||||
title: '',
|
title: '',
|
||||||
id: 'temp',
|
id: 'temp',
|
||||||
});
|
});
|
||||||
const openAddStrategyForm = (
|
const openAddUpdateStrategyForm = (
|
||||||
milestoneId: string,
|
milestoneId: string,
|
||||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => {
|
) => {
|
||||||
setActiveMilestoneId(milestoneId);
|
setActiveMilestoneId(milestoneId);
|
||||||
setStrategy(strategy);
|
setStrategy(strategy);
|
||||||
setAddStrategyOpen(true);
|
setAddUpdateStrategyOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addStrategy = (
|
const addUpdateStrategy = (
|
||||||
milestoneId: string,
|
milestoneId: string,
|
||||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => {
|
) => {
|
||||||
setMilestones((prev) =>
|
const milestone = milestones.find((m) => m.id === milestoneId);
|
||||||
prev.map((milestone, i) =>
|
const existingStrategy = milestone?.strategies?.find(
|
||||||
milestone.id === milestoneId
|
(strat) => strat.id === strategy.id,
|
||||||
? {
|
|
||||||
...milestone,
|
|
||||||
strategies: [
|
|
||||||
...(milestone.strategies || []),
|
|
||||||
{
|
|
||||||
...strategy,
|
|
||||||
strategyName: strategy.name,
|
|
||||||
sortOrder: milestone.strategies?.length || 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: milestone,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
setAddStrategyOpen(false);
|
if (!milestone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (existingStrategy) {
|
||||||
|
milestoneStrategyChanged(milestone, strategy);
|
||||||
|
} else {
|
||||||
|
setMilestones((prev) =>
|
||||||
|
prev.map((milestone, i) =>
|
||||||
|
milestone.id === milestoneId
|
||||||
|
? {
|
||||||
|
...milestone,
|
||||||
|
strategies: [
|
||||||
|
...(milestone.strategies || []),
|
||||||
|
{
|
||||||
|
...strategy,
|
||||||
|
strategyName: strategy.strategyName,
|
||||||
|
sortOrder:
|
||||||
|
milestone.strategies?.length || 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: milestone,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setAddUpdateStrategyOpen(false);
|
||||||
setActiveMilestoneId(undefined);
|
setActiveMilestoneId(undefined);
|
||||||
setStrategy({
|
setStrategy({
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
@ -112,6 +124,29 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const milestoneChanged = (milestone: IReleasePlanMilestonePayload) => {
|
||||||
|
setMilestones((prev) =>
|
||||||
|
prev.map((mstone) =>
|
||||||
|
mstone.id === milestone.id ? { ...milestone } : mstone,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const milestoneStrategyChanged = (
|
||||||
|
milestone: IReleasePlanMilestonePayload,
|
||||||
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
|
) => {
|
||||||
|
const strategies = milestone.strategies || [];
|
||||||
|
milestoneChanged({
|
||||||
|
...milestone,
|
||||||
|
strategies: [
|
||||||
|
...strategies.map((strat) =>
|
||||||
|
strat.id === strategy.id ? strategy : strat,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormTemplate
|
<FormTemplate
|
||||||
title={formTitle}
|
title={formTitle}
|
||||||
@ -145,9 +180,10 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({
|
|||||||
<MilestoneList
|
<MilestoneList
|
||||||
milestones={milestones}
|
milestones={milestones}
|
||||||
setMilestones={setMilestones}
|
setMilestones={setMilestones}
|
||||||
openAddStrategyForm={openAddStrategyForm}
|
openAddStrategyForm={openAddUpdateStrategyForm}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
clearErrors={clearErrors}
|
clearErrors={clearErrors}
|
||||||
|
milestoneChanged={milestoneChanged}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
@ -155,14 +191,14 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({
|
|||||||
<SidebarModal
|
<SidebarModal
|
||||||
label='Add strategy to template milestone'
|
label='Add strategy to template milestone'
|
||||||
onClose={() => {}}
|
onClose={() => {}}
|
||||||
open={addStrategyOpen}
|
open={addUpdateStrategyOpen}
|
||||||
>
|
>
|
||||||
<ReleasePlanTemplateAddStrategyForm
|
<ReleasePlanTemplateAddStrategyForm
|
||||||
milestoneId={activeMilestoneId}
|
milestoneId={activeMilestoneId}
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
onAddStrategy={addStrategy}
|
onAddUpdateStrategy={addUpdateStrategy}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setAddStrategyOpen(false);
|
setAddUpdateStrategyOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SidebarModal>
|
</SidebarModal>
|
||||||
|
@ -50,10 +50,5 @@ export interface IReleasePlanMilestonePayload {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
strategies?: IReleasePlanStrategyPayload[];
|
strategies?: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>[];
|
||||||
}
|
|
||||||
|
|
||||||
export interface IReleasePlanStrategyPayload {
|
|
||||||
id?: string;
|
|
||||||
name: string;
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user