Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create and edit release plan template milestones #8768

Merged
merged 9 commits into from
Nov 19, 2024
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { usePageTitle } from 'hooks/usePageTitle';
import { Button, styled } from '@mui/material';
import { TemplateForm } from './TemplateForm';
import { useTemplateForm } from '../hooks/useTemplateForm';
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 { GO_BACK } from 'constants/navigate';
import useReleasePlanTemplatesApi from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi';
Expand All @@ -13,12 +12,6 @@ import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useUiFlag } from 'hooks/useUiFlag';

const StyledForm = styled('form')(() => ({
display: 'flex',
flexDirection: 'column',
height: '100%',
}));

const StyledButtonContainer = styled('div')(() => ({
marginTop: 'auto',
display: 'flex',
Expand All @@ -31,15 +24,17 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({

export const CreateReleasePlanTemplate = () => {
const releasePlansEnabled = useUiFlag('releasePlans');
usePageTitle('Create release plan template');
const { setToastApiError, setToastData } = useToast();
const navigate = useNavigate();
const { createReleasePlanTemplate } = useReleasePlanTemplatesApi();
usePageTitle('Create release plan template');
const {
name,
setName,
description,
setDescription,
milestones,
setMilestones,
errors,
clearErrors,
validate,
Expand All @@ -57,7 +52,10 @@ export const CreateReleasePlanTemplate = () => {
if (isValid) {
const payload = getTemplatePayload();
try {
const template = await createReleasePlanTemplate(payload);
const template = await createReleasePlanTemplate({
...payload,
milestones,
});
scrollToTop();
setToastData({
type: 'success',
Expand All @@ -75,28 +73,29 @@ export const CreateReleasePlanTemplate = () => {
}

return (
<>
<FormTemplate
title='Create release plan template'
description='Create a release plan template to make it easier for you and your team to release features.'
>
<StyledForm onSubmit={handleSubmit}>
<TemplateForm
name={name}
setName={setName}
description={description}
setDescription={setDescription}
errors={errors}
clearErrors={clearErrors}
/>
<StyledButtonContainer>
<CreateButton name='template' permission={ADMIN} />
<StyledCancelButton onClick={handleCancel}>
Cancel
</StyledCancelButton>
</StyledButtonContainer>
</StyledForm>
</FormTemplate>
</>
<TemplateForm
mode='create'
name={name}
setName={setName}
description={description}
setDescription={setDescription}
milestones={milestones}
setMilestones={setMilestones}
errors={errors}
clearErrors={clearErrors}
formTitle='Create release plan template'
formDescription='Create a release plan template to make it easier for you and your team to release features.'
handleSubmit={handleSubmit}
>
<StyledButtonContainer>
<CreateButton
name='template'
permission={RELEASE_PLAN_TEMPLATE_CREATE}
/>
<StyledCancelButton onClick={handleCancel}>
Cancel
</StyledCancelButton>
</StyledButtonContainer>
</TemplateForm>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,16 @@ import { useUiFlag } from 'hooks/useUiFlag';
import { usePageTitle } from 'hooks/usePageTitle';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useReleasePlanTemplate } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplate';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { useTemplateForm } from '../hooks/useTemplateForm';
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 { ADMIN } from '@server/types/permissions';
import { RELEASE_PLAN_TEMPLATE_UPDATE } from '@server/types/permissions';
import { useNavigate } from 'react-router-dom';
import { formatUnknownError } from 'utils/formatUnknownError';
import useToast from 'hooks/useToast';
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')(() => ({
marginTop: 'auto',
display: 'flex',
Expand All @@ -63,7 +29,7 @@ export const EditReleasePlanTemplate = () => {
useReleasePlanTemplate(templateId);
usePageTitle(`Edit template: ${template.name}`);
const navigate = useNavigate();
const { setToastApiError } = useToast();
const { setToastApiError, setToastData } = useToast();
const { updateReleasePlanTemplate } = useReleasePlanTemplatesApi();
const {
name,
Expand All @@ -72,9 +38,15 @@ export const EditReleasePlanTemplate = () => {
setDescription,
errors,
clearErrors,
milestones,
setMilestones,
validate,
getTemplatePayload,
} = useTemplateForm(template.name, template.description);
} = useTemplateForm(
template.name,
template.description,
template.milestones,
);

const handleCancel = () => {
navigate('/release-management');
Expand All @@ -89,9 +61,13 @@ export const EditReleasePlanTemplate = () => {
await updateReleasePlanTemplate({
...payload,
id: templateId,
milestones: template.milestones,
milestones,
});
await refetch();
setToastData({
type: 'success',
title: 'Release plan template updated',
});
navigate('/release-management');
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
Expand All @@ -103,38 +79,30 @@ export const EditReleasePlanTemplate = () => {
}

return (
<>
<FormTemplate
title={`Edit template ${template.name}`}
description='Edit a release plan template that makes it easier for you and your team to release features.'
>
<StyledForm onSubmit={handleSubmit}>
<TemplateForm
name={name}
setName={setName}
description={description}
setDescription={setDescription}
errors={errors}
clearErrors={clearErrors}
/>

{template.milestones.map((milestone) => (
<StyledMilestoneCard key={milestone.id}>
<StyledMilestoneCardBody>
<StyledMilestoneCardTitle>
{milestone.name}
</StyledMilestoneCardTitle>
</StyledMilestoneCardBody>
</StyledMilestoneCard>
))}
<StyledButtonContainer>
<UpdateButton name='template' permission={ADMIN} />
<StyledCancelButton onClick={handleCancel}>
Cancel
</StyledCancelButton>
</StyledButtonContainer>
</StyledForm>
</FormTemplate>
</>
<TemplateForm
mode='edit'
name={name}
setName={setName}
description={description}
setDescription={setDescription}
milestones={milestones}
setMilestones={setMilestones}
errors={errors}
clearErrors={clearErrors}
formTitle={`Edit template ${template.name}`}
formDescription='Edit a release plan template that makes it easier for you and your team to release features.'
handleSubmit={handleSubmit}
loading={loading}
>
<StyledButtonContainer>
<UpdateButton
name='template'
permission={RELEASE_PLAN_TEMPLATE_UPDATE}
/>
<StyledCancelButton onClick={handleCancel}>
Cancel
</StyledCancelButton>
</StyledButtonContainer>
</TemplateForm>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think you could have potentially merged the edit and create components into a single one, with a simple const isEdit = Boolean(templateId), but this is probably fine for now, and has much less duplicated code than before, so good job 👍

);
};
123 changes: 123 additions & 0 deletions frontend/src/component/releases/ReleasePlanTemplate/MilestoneCard.tsx
Original file line number Diff line number Diff line change
@@ -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 IMilestoneCard {
daveleek marked this conversation as resolved.
Show resolved Hide resolved
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,
}: IMilestoneCard) => {
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)}
/>
</>
)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this a part of the above block, since the condition is the same?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Argh, I moved things around and forgot to remove the duplicate condition 🙄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could have added the onClick handler on the parent component, catching any click in the children, instead of duplicating it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parent is the same for this and the other section which is only shown during edit mode, and would have a weird effect if you click inside. I'll have a think if I can introduce a parent just for these two while doing add strategies

</StyledGridItem>
<Grid item xs={2} md={2}>
<Button
variant='outlined'
color='primary'
onClick={() => showAddStrategyDialog(index)}
>
Add strategy
</Button>
</Grid>
</Grid>
</StyledMilestoneCardBody>
</StyledMilestoneCard>
);
};
Loading
Loading