diff --git a/frontend/src/component/releases/ReleasePlanTemplate/MilestoneCard.tsx b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneCard.tsx index 97d17267ce14..661fce23f2a7 100644 --- a/frontend/src/component/releases/ReleasePlanTemplate/MilestoneCard.tsx +++ b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneCard.tsx @@ -1,4 +1,3 @@ -import Input from 'component/common/Input/Input'; import { Box, Button, @@ -10,9 +9,7 @@ import { AccordionSummary, AccordionDetails, IconButton, - useTheme, } from '@mui/material'; -import Edit from '@mui/icons-material/Edit'; import Delete from '@mui/icons-material/DeleteOutlined'; import type { IReleasePlanMilestonePayload, @@ -22,23 +19,17 @@ import { type DragEventHandler, type RefObject, useState } from 'react'; import ExpandMore from '@mui/icons-material/ExpandMore'; import { MilestoneStrategyMenuCards } from './MilestoneStrategyMenuCards'; import { MilestoneStrategyDraggableItem } from './MilestoneStrategyDraggableItem'; +import { MilestoneCardName } from './MilestoneCardName'; -const StyledEditIcon = styled(Edit)(({ theme }) => ({ - cursor: 'pointer', - marginTop: theme.spacing(-0.25), - marginLeft: theme.spacing(0.5), - height: theme.spacing(2.5), - width: theme.spacing(2.5), - color: theme.palette.text.secondary, -})); - -const StyledMilestoneCard = styled(Card)(({ theme }) => ({ +const StyledMilestoneCard = styled(Card, { + shouldForwardProp: (prop) => prop !== 'hasError', +})<{ hasError: boolean }>(({ theme, hasError }) => ({ marginTop: theme.spacing(2), display: 'flex', flexDirection: 'column', justifyContent: 'space-between', boxShadow: 'none', - border: `1px solid ${theme.palette.divider}`, + border: `1px solid ${hasError ? theme.palette.error.border : theme.palette.divider}`, borderRadius: theme.shape.borderRadiusMedium, [theme.breakpoints.down('sm')]: { justifyContent: 'center', @@ -56,15 +47,6 @@ const StyledGridItem = styled(Grid)(({ theme }) => ({ alignItems: 'center', })); -const StyledInput = styled(Input)(({ theme }) => ({ - width: '100%', -})); - -const StyledMilestoneCardTitle = styled('span')(({ theme }) => ({ - fontWeight: theme.fontWeight.bold, - fontSize: theme.fontSizes.bodySize, -})); - const StyledAddStrategyButton = styled(Button)(({ theme }) => ({})); const StyledAccordion = styled(Accordion)(({ theme }) => ({ @@ -131,6 +113,7 @@ interface IMilestoneCardProps { ) => void; errors: { [key: string]: string }; clearErrors: () => void; + removable: boolean; onDeleteMilestone: () => void; } @@ -140,16 +123,15 @@ export const MilestoneCard = ({ showAddStrategyDialog, errors, clearErrors, + removable, onDeleteMilestone, }: IMilestoneCardProps) => { - const [editMode, setEditMode] = useState(false); const [anchor, setAnchor] = useState(); const [dragItem, setDragItem] = useState<{ id: string; index: number; height: number; } | null>(null); - const theme = useTheme(); const isPopoverOpen = Boolean(anchor); const popoverId = isPopoverOpen ? 'MilestoneStrategyMenuPopover' @@ -261,43 +243,21 @@ export const MilestoneCard = ({ if (!milestone.strategies || milestone.strategies.length === 0) { return ( - + - {editMode && ( - - 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 && ( - <> - setEditMode(true)} - > - {milestone.name} - - setEditMode(true)} - /> - - )} + diff --git a/frontend/src/component/releases/ReleasePlanTemplate/MilestoneCardName.tsx b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneCardName.tsx new file mode 100644 index 000000000000..602bd8969383 --- /dev/null +++ b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneCardName.tsx @@ -0,0 +1,85 @@ +import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans'; +import Edit from '@mui/icons-material/Edit'; +import { styled } from '@mui/material'; +import { useState } from 'react'; +import Input from 'component/common/Input/Input'; + +const StyledInput = styled(Input)(({ theme }) => ({ + width: '100%', +})); + +const StyledMilestoneCardTitle = styled('span')(({ theme }) => ({ + fontWeight: theme.fontWeight.bold, + fontSize: theme.fontSizes.bodySize, +})); + +const StyledEditIcon = styled(Edit, { + shouldForwardProp: (prop) => prop !== 'hasError', +})<{ hasError: boolean }>(({ theme, hasError = false }) => ({ + cursor: 'pointer', + marginTop: theme.spacing(-0.25), + marginLeft: theme.spacing(0.5), + height: theme.spacing(2.5), + width: theme.spacing(2.5), + color: hasError ? theme.palette.error.main : theme.palette.text.secondary, +})); + +interface IMilestoneCardNameProps { + milestone: IReleasePlanMilestonePayload; + errors: { [key: string]: string }; + clearErrors: () => void; + milestoneNameChanged: (name: string) => void; +} + +export const MilestoneCardName = ({ + milestone, + errors, + clearErrors, + milestoneNameChanged, +}: IMilestoneCardNameProps) => { + const [editMode, setEditMode] = useState(false); + return ( + <> + {editMode && ( + milestoneNameChanged(e.target.value)} + error={Boolean(errors?.[`${milestone.id}_name`])} + errorText={errors?.[`${milestone.id}_name`]} + onFocus={() => clearErrors()} + onBlur={() => setEditMode(false)} + autoFocus + onKeyDownCapture={(e) => { + if (e.code === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + setEditMode(false); + } + }} + /> + )} + {!editMode && ( + <> + { + setEditMode(true); + ev.preventDefault(); + ev.stopPropagation(); + }} + > + {milestone.name} + + { + setEditMode(true); + ev.preventDefault(); + ev.stopPropagation(); + }} + /> + + )} + + ); +}; diff --git a/frontend/src/component/releases/ReleasePlanTemplate/MilestoneList.tsx b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneList.tsx index fa46fd3cee98..9b481a8478e8 100644 --- a/frontend/src/component/releases/ReleasePlanTemplate/MilestoneList.tsx +++ b/frontend/src/component/releases/ReleasePlanTemplate/MilestoneList.tsx @@ -3,7 +3,7 @@ import type { IReleasePlanMilestoneStrategy, } from 'interfaces/releasePlans'; import { MilestoneCard } from './MilestoneCard'; -import { styled, Button } from '@mui/material'; +import { styled, Button, FormHelperText } from '@mui/material'; import Add from '@mui/icons-material/Add'; import { v4 as uuidv4 } from 'uuid'; @@ -46,15 +46,22 @@ export const MilestoneList = ({ return ( <> {milestones.map((milestone) => ( - + <> + 1} + onDeleteMilestone={onDeleteMilestone(milestone.id)} + /> + + + {errors?.[milestone.id]} + + ))} { + let valid = true; + if (name.length === 0) { setErrors((prev) => ({ ...prev, name: 'Name can not be empty.' })); - return false; + valid = false; + } + + if (milestones.length === 0) { + setErrors((prev) => ({ + ...prev, + milestones: 'At least one milestone is required.', + })); + valid = false; + } + + const milestoneNames = milestones.filter( + (m) => !m.name || m.name.length === 0, + ); + if (milestoneNames && milestoneNames.length > 0) { + setErrors((prev) => ({ + ...prev, + ...Object.assign( + {}, + ...milestoneNames.map((mst) => ({ + [mst.id]: 'Milestone must have a valid name.', + })), + ), + ...Object.assign( + {}, + ...milestoneNames.map((mst) => ({ + [`${mst.id}_name`]: 'Milestone must have a valid name.', + })), + ), + })); + valid = false; + } + + const emptyMilestones = milestones.filter( + (m) => !m.strategies || m.strategies.length === 0, + ); + if (emptyMilestones && emptyMilestones.length > 0) { + setErrors((prev) => ({ + ...prev, + milestones: + 'All milestones must have at least one strategy each.', + ...Object.assign( + {}, + ...emptyMilestones.map((mst) => ({ + [mst.id]: 'Milestone must have at least one strategy.', + })), + ), + })); + valid = false; } - return true; + + return valid; }; const clearErrors = () => {