mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-14 00:19:16 +01:00
feat: rework frontend validation for release plan templates (#9055)
This commit is contained in:
parent
3c16616c36
commit
7893d3fbd3
@ -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<Element>();
|
||||
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 (
|
||||
<StyledMilestoneCard>
|
||||
<StyledMilestoneCard
|
||||
hasError={
|
||||
Boolean(errors?.[milestone.id]) ||
|
||||
Boolean(errors?.[`${milestone.id}_name`])
|
||||
}
|
||||
>
|
||||
<StyledMilestoneCardBody>
|
||||
<Grid container>
|
||||
<StyledGridItem item xs={8} md={9}>
|
||||
{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)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<MilestoneCardName
|
||||
milestone={milestone}
|
||||
errors={errors}
|
||||
clearErrors={clearErrors}
|
||||
milestoneNameChanged={milestoneNameChanged}
|
||||
/>
|
||||
</StyledGridItem>
|
||||
<StyledMilestoneActionGrid item xs={4} md={3}>
|
||||
<Button
|
||||
@ -310,6 +270,7 @@ export const MilestoneCard = ({
|
||||
<StyledIconButton
|
||||
title='Remove milestone'
|
||||
onClick={onDeleteMilestone}
|
||||
disabled={!removable}
|
||||
>
|
||||
<Delete />
|
||||
</StyledIconButton>
|
||||
@ -342,35 +303,12 @@ export const MilestoneCard = ({
|
||||
<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)} />
|
||||
</>
|
||||
)}
|
||||
<MilestoneCardName
|
||||
milestone={milestone}
|
||||
errors={errors}
|
||||
clearErrors={clearErrors}
|
||||
milestoneNameChanged={milestoneNameChanged}
|
||||
/>
|
||||
</StyledAccordionSummary>
|
||||
<StyledAccordionDetails>
|
||||
{milestone.strategies.map((strg, index) => (
|
||||
@ -403,6 +341,7 @@ export const MilestoneCard = ({
|
||||
variant='text'
|
||||
color='primary'
|
||||
onClick={onDeleteMilestone}
|
||||
disabled={!removable}
|
||||
>
|
||||
<Delete /> Remove milestone
|
||||
</Button>
|
||||
|
@ -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 && (
|
||||
<StyledInput
|
||||
label=''
|
||||
value={milestone.name}
|
||||
onChange={(e) => 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 && (
|
||||
<>
|
||||
<StyledMilestoneCardTitle
|
||||
onClick={(ev) => {
|
||||
setEditMode(true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{milestone.name}
|
||||
</StyledMilestoneCardTitle>
|
||||
<StyledEditIcon
|
||||
hasError={Boolean(errors?.[`${milestone.id}_name`])}
|
||||
onClick={(ev) => {
|
||||
setEditMode(true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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) => (
|
||||
<MilestoneCard
|
||||
key={milestone.id}
|
||||
milestone={milestone}
|
||||
milestoneChanged={milestoneChanged}
|
||||
showAddStrategyDialog={openAddStrategyForm}
|
||||
errors={errors}
|
||||
clearErrors={clearErrors}
|
||||
onDeleteMilestone={onDeleteMilestone(milestone.id)}
|
||||
/>
|
||||
<>
|
||||
<MilestoneCard
|
||||
key={milestone.id}
|
||||
milestone={milestone}
|
||||
milestoneChanged={milestoneChanged}
|
||||
showAddStrategyDialog={openAddStrategyForm}
|
||||
errors={errors}
|
||||
clearErrors={clearErrors}
|
||||
removable={milestones.length > 1}
|
||||
onDeleteMilestone={onDeleteMilestone(milestone.id)}
|
||||
/>
|
||||
|
||||
<FormHelperText error={Boolean(errors?.[milestone.id])}>
|
||||
{errors?.[milestone.id]}
|
||||
</FormHelperText>
|
||||
</>
|
||||
))}
|
||||
<StyledAddMilestoneButton
|
||||
variant='text'
|
||||
|
@ -27,11 +27,62 @@ export const useTemplateForm = (
|
||||
}, [initialMilestones.length]);
|
||||
|
||||
const validate = () => {
|
||||
let valid = true;
|
||||
|
||||
if (name.length === 0) {
|
||||
setErrors((prev) => ({ ...prev, name: 'Name can not be empty.' }));
|
||||
return false;
|
||||
valid = false;
|
||||
}
|
||||
return true;
|
||||
|
||||
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 valid;
|
||||
};
|
||||
|
||||
const clearErrors = () => {
|
||||
|
Loading…
Reference in New Issue
Block a user