mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
feat: create and edit release plan template milestones (#8768)
This commit is contained in:
parent
82e752be45
commit
8935a01d90
@ -1,10 +1,9 @@
|
|||||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
import { Button, styled } from '@mui/material';
|
import { Button, styled } from '@mui/material';
|
||||||
import { TemplateForm } from './TemplateForm';
|
import { TemplateForm } from './TemplateForm';
|
||||||
import { useTemplateForm } from '../hooks/useTemplateForm';
|
import { useTemplateForm } from '../hooks/useTemplateForm';
|
||||||
import { CreateButton } from 'component/common/CreateButton/CreateButton';
|
import { CreateButton } from 'component/common/CreateButton/CreateButton';
|
||||||
import { ADMIN } from '@server/types/permissions';
|
import { RELEASE_PLAN_TEMPLATE_CREATE } from '@server/types/permissions';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { GO_BACK } from 'constants/navigate';
|
import { GO_BACK } from 'constants/navigate';
|
||||||
import useReleasePlanTemplatesApi from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi';
|
import useReleasePlanTemplatesApi from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi';
|
||||||
@ -13,12 +12,6 @@ import useToast from 'hooks/useToast';
|
|||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
const StyledForm = styled('form')(() => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
height: '100%',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledButtonContainer = styled('div')(() => ({
|
const StyledButtonContainer = styled('div')(() => ({
|
||||||
marginTop: 'auto',
|
marginTop: 'auto',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -31,15 +24,17 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
|
|||||||
|
|
||||||
export const CreateReleasePlanTemplate = () => {
|
export const CreateReleasePlanTemplate = () => {
|
||||||
const releasePlansEnabled = useUiFlag('releasePlans');
|
const releasePlansEnabled = useUiFlag('releasePlans');
|
||||||
usePageTitle('Create release plan template');
|
|
||||||
const { setToastApiError, setToastData } = useToast();
|
const { setToastApiError, setToastData } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { createReleasePlanTemplate } = useReleasePlanTemplatesApi();
|
const { createReleasePlanTemplate } = useReleasePlanTemplatesApi();
|
||||||
|
usePageTitle('Create release plan template');
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
setName,
|
setName,
|
||||||
description,
|
description,
|
||||||
setDescription,
|
setDescription,
|
||||||
|
milestones,
|
||||||
|
setMilestones,
|
||||||
errors,
|
errors,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
validate,
|
validate,
|
||||||
@ -57,7 +52,10 @@ export const CreateReleasePlanTemplate = () => {
|
|||||||
if (isValid) {
|
if (isValid) {
|
||||||
const payload = getTemplatePayload();
|
const payload = getTemplatePayload();
|
||||||
try {
|
try {
|
||||||
const template = await createReleasePlanTemplate(payload);
|
const template = await createReleasePlanTemplate({
|
||||||
|
...payload,
|
||||||
|
milestones,
|
||||||
|
});
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -75,28 +73,28 @@ export const CreateReleasePlanTemplate = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<TemplateForm
|
||||||
<FormTemplate
|
name={name}
|
||||||
title='Create release plan template'
|
setName={setName}
|
||||||
description='Create a release plan template to make it easier for you and your team to release features.'
|
description={description}
|
||||||
>
|
setDescription={setDescription}
|
||||||
<StyledForm onSubmit={handleSubmit}>
|
milestones={milestones}
|
||||||
<TemplateForm
|
setMilestones={setMilestones}
|
||||||
name={name}
|
errors={errors}
|
||||||
setName={setName}
|
clearErrors={clearErrors}
|
||||||
description={description}
|
formTitle='Create release plan template'
|
||||||
setDescription={setDescription}
|
formDescription='Create a release plan template to make it easier for you and your team to release features.'
|
||||||
errors={errors}
|
handleSubmit={handleSubmit}
|
||||||
clearErrors={clearErrors}
|
>
|
||||||
/>
|
<StyledButtonContainer>
|
||||||
<StyledButtonContainer>
|
<CreateButton
|
||||||
<CreateButton name='template' permission={ADMIN} />
|
name='template'
|
||||||
<StyledCancelButton onClick={handleCancel}>
|
permission={RELEASE_PLAN_TEMPLATE_CREATE}
|
||||||
Cancel
|
/>
|
||||||
</StyledCancelButton>
|
<StyledCancelButton onClick={handleCancel}>
|
||||||
</StyledButtonContainer>
|
Cancel
|
||||||
</StyledForm>
|
</StyledCancelButton>
|
||||||
</FormTemplate>
|
</StyledButtonContainer>
|
||||||
</>
|
</TemplateForm>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,50 +2,16 @@ import { useUiFlag } from 'hooks/useUiFlag';
|
|||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useReleasePlanTemplate } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplate';
|
import { useReleasePlanTemplate } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplate';
|
||||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
|
||||||
import { useTemplateForm } from '../hooks/useTemplateForm';
|
import { useTemplateForm } from '../hooks/useTemplateForm';
|
||||||
import { TemplateForm } from './TemplateForm';
|
import { TemplateForm } from './TemplateForm';
|
||||||
import { Box, Button, Card, styled } from '@mui/material';
|
import { Button, styled } from '@mui/material';
|
||||||
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
|
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
|
||||||
import { ADMIN } from '@server/types/permissions';
|
import { RELEASE_PLAN_TEMPLATE_UPDATE } from '@server/types/permissions';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import useReleasePlanTemplatesApi from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi';
|
import useReleasePlanTemplatesApi from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi';
|
||||||
|
|
||||||
const StyledForm = styled('form')(() => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
height: '100%',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledMilestoneCard = styled(Card)(({ theme }) => ({
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
boxShadow: 'none',
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
[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,
|
|
||||||
},
|
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledMilestoneCardBody = styled(Box)(({ theme }) => ({
|
|
||||||
padding: theme.spacing(3, 2),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledMilestoneCardTitle = styled('span')(({ theme }) => ({
|
|
||||||
fontWeight: theme.fontWeight.bold,
|
|
||||||
fontSize: theme.fontSizes.bodySize,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledButtonContainer = styled('div')(() => ({
|
const StyledButtonContainer = styled('div')(() => ({
|
||||||
marginTop: 'auto',
|
marginTop: 'auto',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -63,7 +29,7 @@ export const EditReleasePlanTemplate = () => {
|
|||||||
useReleasePlanTemplate(templateId);
|
useReleasePlanTemplate(templateId);
|
||||||
usePageTitle(`Edit template: ${template.name}`);
|
usePageTitle(`Edit template: ${template.name}`);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setToastApiError } = useToast();
|
const { setToastApiError, setToastData } = useToast();
|
||||||
const { updateReleasePlanTemplate } = useReleasePlanTemplatesApi();
|
const { updateReleasePlanTemplate } = useReleasePlanTemplatesApi();
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
@ -72,9 +38,15 @@ export const EditReleasePlanTemplate = () => {
|
|||||||
setDescription,
|
setDescription,
|
||||||
errors,
|
errors,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
|
milestones,
|
||||||
|
setMilestones,
|
||||||
validate,
|
validate,
|
||||||
getTemplatePayload,
|
getTemplatePayload,
|
||||||
} = useTemplateForm(template.name, template.description);
|
} = useTemplateForm(
|
||||||
|
template.name,
|
||||||
|
template.description,
|
||||||
|
template.milestones,
|
||||||
|
);
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
navigate('/release-management');
|
navigate('/release-management');
|
||||||
@ -89,9 +61,13 @@ export const EditReleasePlanTemplate = () => {
|
|||||||
await updateReleasePlanTemplate({
|
await updateReleasePlanTemplate({
|
||||||
...payload,
|
...payload,
|
||||||
id: templateId,
|
id: templateId,
|
||||||
milestones: template.milestones,
|
milestones,
|
||||||
|
});
|
||||||
|
await refetch();
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Release plan template updated',
|
||||||
});
|
});
|
||||||
navigate('/release-management');
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setToastApiError(formatUnknownError(error));
|
setToastApiError(formatUnknownError(error));
|
||||||
}
|
}
|
||||||
@ -103,38 +79,29 @@ export const EditReleasePlanTemplate = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<TemplateForm
|
||||||
<FormTemplate
|
name={name}
|
||||||
title={`Edit template ${template.name}`}
|
setName={setName}
|
||||||
description='Edit a release plan template that makes it easier for you and your team to release features.'
|
description={description}
|
||||||
>
|
setDescription={setDescription}
|
||||||
<StyledForm onSubmit={handleSubmit}>
|
milestones={milestones}
|
||||||
<TemplateForm
|
setMilestones={setMilestones}
|
||||||
name={name}
|
errors={errors}
|
||||||
setName={setName}
|
clearErrors={clearErrors}
|
||||||
description={description}
|
formTitle={`Edit template ${template.name}`}
|
||||||
setDescription={setDescription}
|
formDescription='Edit a release plan template that makes it easier for you and your team to release features.'
|
||||||
errors={errors}
|
handleSubmit={handleSubmit}
|
||||||
clearErrors={clearErrors}
|
loading={loading}
|
||||||
/>
|
>
|
||||||
|
<StyledButtonContainer>
|
||||||
{template.milestones.map((milestone) => (
|
<UpdateButton
|
||||||
<StyledMilestoneCard key={milestone.id}>
|
name='template'
|
||||||
<StyledMilestoneCardBody>
|
permission={RELEASE_PLAN_TEMPLATE_UPDATE}
|
||||||
<StyledMilestoneCardTitle>
|
/>
|
||||||
{milestone.name}
|
<StyledCancelButton onClick={handleCancel}>
|
||||||
</StyledMilestoneCardTitle>
|
Cancel
|
||||||
</StyledMilestoneCardBody>
|
</StyledCancelButton>
|
||||||
</StyledMilestoneCard>
|
</StyledButtonContainer>
|
||||||
))}
|
</TemplateForm>
|
||||||
<StyledButtonContainer>
|
|
||||||
<UpdateButton name='template' permission={ADMIN} />
|
|
||||||
<StyledCancelButton onClick={handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</StyledCancelButton>
|
|
||||||
</StyledButtonContainer>
|
|
||||||
</StyledForm>
|
|
||||||
</FormTemplate>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
import Input from 'component/common/Input/Input';
|
||||||
|
import { Box, Button, Card, Grid, styled } from '@mui/material';
|
||||||
|
import Edit from '@mui/icons-material/Edit';
|
||||||
|
import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
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 }) => ({
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
boxShadow: 'none',
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
[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,
|
||||||
|
},
|
||||||
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledMilestoneCardBody = styled(Box)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2, 2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledGridItem = styled(Grid)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledInput = styled(Input)(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledMilestoneCardTitle = styled('span')(({ theme }) => ({
|
||||||
|
fontWeight: theme.fontWeight.bold,
|
||||||
|
fontSize: theme.fontSizes.bodySize,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IMilestoneCardProps {
|
||||||
|
index: number;
|
||||||
|
milestone: IReleasePlanMilestonePayload;
|
||||||
|
milestoneNameChanged: (index: number, name: string) => void;
|
||||||
|
showAddStrategyDialog: (index: number) => void;
|
||||||
|
errors: { [key: string]: string };
|
||||||
|
clearErrors: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MilestoneCard = ({
|
||||||
|
index,
|
||||||
|
milestone,
|
||||||
|
milestoneNameChanged,
|
||||||
|
showAddStrategyDialog,
|
||||||
|
errors,
|
||||||
|
clearErrors,
|
||||||
|
}: IMilestoneCardProps) => {
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledMilestoneCard>
|
||||||
|
<StyledMilestoneCardBody>
|
||||||
|
<Grid container>
|
||||||
|
<StyledGridItem item xs={10} md={10}>
|
||||||
|
{editMode && (
|
||||||
|
<StyledInput
|
||||||
|
label=''
|
||||||
|
value={milestone.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
milestoneNameChanged(index, 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)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</StyledGridItem>
|
||||||
|
<Grid item xs={2} md={2}>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
color='primary'
|
||||||
|
onClick={() => showAddStrategyDialog(index)}
|
||||||
|
>
|
||||||
|
Add strategy
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</StyledMilestoneCardBody>
|
||||||
|
</StyledMilestoneCard>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,72 @@
|
|||||||
|
import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans';
|
||||||
|
import { MilestoneCard } from './MilestoneCard';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import Add from '@mui/icons-material/Add';
|
||||||
|
|
||||||
|
interface IMilestoneListProps {
|
||||||
|
milestones: IReleasePlanMilestonePayload[];
|
||||||
|
setMilestones: React.Dispatch<
|
||||||
|
React.SetStateAction<IReleasePlanMilestonePayload[]>
|
||||||
|
>;
|
||||||
|
setAddStrategyOpen: (open: boolean) => void;
|
||||||
|
errors: { [key: string]: string };
|
||||||
|
clearErrors: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledAddMilestoneButton = styled(Button)(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
maxWidth: theme.spacing(20),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const MilestoneList = ({
|
||||||
|
milestones,
|
||||||
|
setMilestones,
|
||||||
|
setAddStrategyOpen,
|
||||||
|
errors,
|
||||||
|
clearErrors,
|
||||||
|
}: IMilestoneListProps) => {
|
||||||
|
const showAddStrategyDialog = (index: number) => {
|
||||||
|
setAddStrategyOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const milestoneNameChanged = (index: number, name: string) => {
|
||||||
|
setMilestones((prev) =>
|
||||||
|
prev.map((milestone, i) =>
|
||||||
|
i === index ? { ...milestone, name } : milestone,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{milestones.map((milestone, index) => (
|
||||||
|
<MilestoneCard
|
||||||
|
key={`milestone_${index.toString()}`}
|
||||||
|
index={index}
|
||||||
|
milestone={milestone}
|
||||||
|
milestoneNameChanged={milestoneNameChanged}
|
||||||
|
showAddStrategyDialog={showAddStrategyDialog}
|
||||||
|
errors={errors}
|
||||||
|
clearErrors={clearErrors}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<StyledAddMilestoneButton
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
startIcon={<Add />}
|
||||||
|
onClick={() =>
|
||||||
|
setMilestones((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
name: `Milestone ${prev.length + 1}`,
|
||||||
|
sortOrder: prev.length,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add milestone
|
||||||
|
</StyledAddMilestoneButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,33 @@
|
|||||||
|
import { Button, styled } from '@mui/material';
|
||||||
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
|
|
||||||
|
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||||
|
marginLeft: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledButtonContainer = styled('div')(() => ({
|
||||||
|
marginTop: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IReleasePlanTemplateAddStrategyFormProps {
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReleasePlanTemplateAddStrategyForm = ({
|
||||||
|
onCancel,
|
||||||
|
}: IReleasePlanTemplateAddStrategyFormProps) => {
|
||||||
|
return (
|
||||||
|
<FormTemplate
|
||||||
|
modal
|
||||||
|
description='Add a strategy to your release plan template.'
|
||||||
|
>
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<StyledCancelButton onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</StyledCancelButton>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
</FormTemplate>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,12 @@
|
|||||||
import Input from 'component/common/Input/Input';
|
import Input from 'component/common/Input/Input';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
|
import { MilestoneList } from './MilestoneList';
|
||||||
|
import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans';
|
||||||
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
|
import ReleaseTemplateIcon from '@mui/icons-material/DashboardOutlined';
|
||||||
|
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ReleasePlanTemplateAddStrategyForm } from './ReleasePlanTemplateAddStrategyForm';
|
||||||
|
|
||||||
const StyledInputDescription = styled('p')(({ theme }) => ({
|
const StyledInputDescription = styled('p')(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(1),
|
marginBottom: theme.spacing(1),
|
||||||
@ -10,48 +17,98 @@ const StyledInput = styled(Input)(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface ITemplateForm {
|
const StyledForm = styled('form')(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface ITemplateFormProps {
|
||||||
name: string;
|
name: string;
|
||||||
setName: React.Dispatch<React.SetStateAction<string>>;
|
setName: React.Dispatch<React.SetStateAction<string>>;
|
||||||
description: string;
|
description: string;
|
||||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
milestones: IReleasePlanMilestonePayload[];
|
||||||
|
setMilestones: React.Dispatch<
|
||||||
|
React.SetStateAction<IReleasePlanMilestonePayload[]>
|
||||||
|
>;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
|
formTitle: string;
|
||||||
|
formDescription: string;
|
||||||
|
handleSubmit: (e: React.FormEvent) => void;
|
||||||
|
loading?: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateForm: React.FC<ITemplateForm> = ({
|
export const TemplateForm: React.FC<ITemplateFormProps> = ({
|
||||||
name,
|
name,
|
||||||
setName,
|
setName,
|
||||||
description,
|
description,
|
||||||
setDescription,
|
setDescription,
|
||||||
|
milestones,
|
||||||
|
setMilestones,
|
||||||
errors,
|
errors,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
|
formTitle,
|
||||||
|
formDescription,
|
||||||
|
handleSubmit,
|
||||||
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [addStrategyOpen, setAddStrategyOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<FormTemplate
|
||||||
<StyledInputDescription>
|
title={formTitle}
|
||||||
What would you like to call your template?
|
description={formDescription}
|
||||||
</StyledInputDescription>
|
documentationIcon={<ReleaseTemplateIcon />}
|
||||||
<StyledInput
|
>
|
||||||
label='Template name'
|
<StyledForm onSubmit={handleSubmit}>
|
||||||
value={name}
|
<StyledInputDescription>
|
||||||
onChange={(e) => setName(e.target.value)}
|
What would you like to call your template?
|
||||||
error={Boolean(errors.name)}
|
</StyledInputDescription>
|
||||||
errorText={errors.name}
|
<StyledInput
|
||||||
onFocus={() => clearErrors()}
|
label='Template name'
|
||||||
autoFocus
|
value={name}
|
||||||
/>
|
onChange={(e) => setName(e.target.value)}
|
||||||
<StyledInputDescription>
|
error={Boolean(errors.name)}
|
||||||
What's the purpose of this template?
|
errorText={errors.name}
|
||||||
</StyledInputDescription>
|
onFocus={() => clearErrors()}
|
||||||
<StyledInput
|
autoFocus
|
||||||
label='Template description (optional)'
|
/>
|
||||||
value={description}
|
<StyledInputDescription>
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
What's the purpose of this template?
|
||||||
error={Boolean(errors.description)}
|
</StyledInputDescription>
|
||||||
errorText={errors.description}
|
<StyledInput
|
||||||
onFocus={() => clearErrors()}
|
label='Template description (optional)'
|
||||||
/>
|
value={description}
|
||||||
</>
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
error={Boolean(errors.description)}
|
||||||
|
errorText={errors.description}
|
||||||
|
onFocus={() => clearErrors()}
|
||||||
|
/>
|
||||||
|
<MilestoneList
|
||||||
|
milestones={milestones}
|
||||||
|
setMilestones={setMilestones}
|
||||||
|
setAddStrategyOpen={setAddStrategyOpen}
|
||||||
|
errors={errors}
|
||||||
|
clearErrors={clearErrors}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<SidebarModal
|
||||||
|
label='Add strategy to template milestone'
|
||||||
|
onClose={() => {}}
|
||||||
|
open={addStrategyOpen}
|
||||||
|
>
|
||||||
|
<ReleasePlanTemplateAddStrategyForm
|
||||||
|
onCancel={() => {
|
||||||
|
setAddStrategyOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SidebarModal>
|
||||||
|
</StyledForm>
|
||||||
|
</FormTemplate>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
|
import type { IReleasePlanMilestonePayload } from 'interfaces/releasePlans';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const useTemplateForm = (initialName = '', initialDescription = '') => {
|
export const useTemplateForm = (
|
||||||
|
initialName = '',
|
||||||
|
initialDescription = '',
|
||||||
|
initialMilestones: IReleasePlanMilestonePayload[] = [
|
||||||
|
{ name: 'Milestone 1', sortOrder: 0 },
|
||||||
|
],
|
||||||
|
) => {
|
||||||
const [name, setName] = useState(initialName);
|
const [name, setName] = useState(initialName);
|
||||||
const [description, setDescription] = useState(initialDescription);
|
const [description, setDescription] = useState(initialDescription);
|
||||||
|
const [milestones, setMilestones] = useState(initialMilestones);
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -13,6 +21,10 @@ export const useTemplateForm = (initialName = '', initialDescription = '') => {
|
|||||||
setDescription(initialDescription);
|
setDescription(initialDescription);
|
||||||
}, [initialDescription]);
|
}, [initialDescription]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMilestones(initialMilestones);
|
||||||
|
}, [initialMilestones.length]);
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
if (name.length === 0) {
|
if (name.length === 0) {
|
||||||
setErrors((prev) => ({ ...prev, name: 'Name can not be empty.' }));
|
setErrors((prev) => ({ ...prev, name: 'Name can not be empty.' }));
|
||||||
@ -37,6 +49,8 @@ export const useTemplateForm = (initialName = '', initialDescription = '') => {
|
|||||||
setName,
|
setName,
|
||||||
description,
|
description,
|
||||||
setDescription,
|
setDescription,
|
||||||
|
milestones,
|
||||||
|
setMilestones,
|
||||||
errors,
|
errors,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
validate,
|
validate,
|
||||||
|
@ -12,7 +12,7 @@ export interface IReleasePlanTemplate {
|
|||||||
description: string;
|
description: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
createdByUserId: number;
|
createdByUserId: number;
|
||||||
milestones: IReleasePlanMilestone[];
|
milestones: IReleasePlanMilestonePayload[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleasePlanMilestone {
|
export interface IReleasePlanMilestone {
|
||||||
@ -24,5 +24,17 @@ export interface IReleasePlanTemplatePayload {
|
|||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
milestones?: IReleasePlanMilestone[];
|
milestones?: IReleasePlanMilestonePayload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReleasePlanMilestonePayload {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
sortOrder: number;
|
||||||
|
strategies?: IReleasePlanStrategyPayload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReleasePlanStrategyPayload {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user