Skip to content

Commit

Permalink
feat: edit release plan template (#8723)
Browse files Browse the repository at this point in the history
  • Loading branch information
daveleek authored Nov 13, 2024
1 parent 584be70 commit 7feba0c
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -230,20 +230,6 @@ exports[`returns all baseRoutes 1`] = `
"title": "Strategy types",
"type": "protected",
},
{
"component": [Function],
"enterprise": true,
"flag": "releasePlans",
"menu": {
"advanced": true,
"mode": [
"enterprise",
],
},
"path": "/release-management",
"title": "Release management",
"type": "protected",
},
{
"component": [Function],
"menu": {},
Expand All @@ -270,6 +256,34 @@ exports[`returns all baseRoutes 1`] = `
"title": "Environments",
"type": "protected",
},
{
"component": [Function],
"enterprise": true,
"flag": "releasePlans",
"menu": {
"advanced": true,
"mode": [
"enterprise",
],
},
"path": "/release-management",
"title": "Release management",
"type": "protected",
},
{
"component": [Function],
"enterprise": true,
"flag": "releasePlans",
"menu": {
"mode": [
"enterprise",
],
},
"parent": "/release-management",
"path": "/release-management/edit/:templateId",
"title": "Edit release plan template",
"type": "protected",
},
{
"component": [Function],
"menu": {},
Expand Down
31 changes: 22 additions & 9 deletions frontend/src/component/menu/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { Signals } from 'component/signals/Signals';
import { LazyCreateProject } from '../project/Project/CreateProject/LazyCreateProject';
import { PersonalDashboard } from '../personalDashboard/PersonalDashboard';
import { ReleaseManagement } from 'component/releases/ReleaseManagement/ReleaseManagement';
import { EditReleasePlanTemplate } from 'component/releases/ReleasePlanTemplate/EditReleasePlanTemplate';

export const routes: IRoute[] = [
// Splash
Expand Down Expand Up @@ -246,15 +247,6 @@ export const routes: IRoute[] = [
type: 'protected',
menu: { mobile: true, advanced: true },
},
{
path: '/release-management',
title: 'Release management',
component: ReleaseManagement,
type: 'protected',
menu: { advanced: true, mode: ['enterprise'] },
flag: 'releasePlans',
enterprise: true,
},
{
path: '/environments/create',
title: 'Environments',
Expand All @@ -279,6 +271,27 @@ export const routes: IRoute[] = [
enterprise: true,
},

// Release management/plans
{
path: '/release-management',
title: 'Release management',
component: ReleaseManagement,
type: 'protected',
menu: { advanced: true, mode: ['enterprise'] },
flag: 'releasePlans',
enterprise: true,
},
{
path: '/release-management/edit/:templateId',
title: 'Edit release plan template',
parent: '/release-management',
component: EditReleasePlanTemplate,
type: 'protected',
menu: { mode: ['enterprise'] },
flag: 'releasePlans',
enterprise: true,
},

// Tags
{
path: '/tag-types/create',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
import { ReactComponent as ReleaseTemplateIcon } from 'assets/img/releaseTemplates.svg';
import { styled, Typography } from '@mui/material';
import { ReleasePlanTemplateCardMenu } from './ReleasePlanTemplateCardMenu';
import { useNavigate } from 'react-router-dom';

const StyledTemplateCard = styled('aside')(({ theme }) => ({
height: '100%',
Expand Down Expand Up @@ -58,8 +59,13 @@ const StyledMenu = styled('div')(({ theme }) => ({
export const ReleasePlanTemplateCard = ({
template,
}: { template: IReleasePlanTemplate }) => {
const navigate = useNavigate();
const onClick = () => {
navigate(`/release-management/edit/${template.id}`);
};

return (
<StyledTemplateCard>
<StyledTemplateCard onClick={onClick}>
<TemplateCardHeader>
<StyledCenter>
<ReleaseTemplateIcon />
Expand All @@ -71,8 +77,16 @@ export const ReleasePlanTemplateCard = ({
<StyledCreatedBy>
Created by {template.createdByUserId}
</StyledCreatedBy>
<StyledMenu onClick={(e) => e.preventDefault()}>
<ReleasePlanTemplateCardMenu template={template} />
<StyledMenu
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<ReleasePlanTemplateCardMenu
template={template}
onClick={onClick}
/>
</StyledMenu>
</StyledDiv>
</TemplateCardBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { TemplateDeleteDialog } from './TemplateDeleteDialog';

export const ReleasePlanTemplateCardMenu = ({
template,
}: { template: IReleasePlanTemplate }) => {
onClick,
}: { template: IReleasePlanTemplate; onClick: () => void }) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const { deleteReleasePlanTemplate } = useReleasePlanTemplatesApi();
Expand All @@ -43,6 +44,7 @@ export const ReleasePlanTemplateCardMenu = ({
};

const handleMenuClick = (event: React.SyntheticEvent) => {
event.stopPropagation();
if (isMenuOpen) {
closeMenu();
} else {
Expand Down Expand Up @@ -81,7 +83,7 @@ export const ReleasePlanTemplateCardMenu = ({
>
<MenuItem
onClick={() => {
closeMenu();
onClick();
}}
>
<ListItemText>Edit template</ListItemText>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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 { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
import { ADMIN } 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',
justifyContent: 'flex-end',
}));

const StyledCancelButton = styled(Button)(({ theme }) => ({
marginLeft: theme.spacing(3),
}));

export const EditReleasePlanTemplate = () => {
const releasePlansEnabled = useUiFlag('releasePlans');
const templateId = useRequiredPathParam('templateId');
const { template, loading, error, refetch } =
useReleasePlanTemplate(templateId);
usePageTitle(`Edit template: ${template.name}`);
const navigate = useNavigate();
const { setToastApiError } = useToast();
const { updateReleasePlanTemplate } = useReleasePlanTemplatesApi();
const {
name,
setName,
description,
setDescription,
errors,
clearErrors,
validate,
getTemplatePayload,
} = useTemplateForm(template.name, template.description);

const handleCancel = () => {
navigate('/release-management');
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
clearErrors();
const isValid = validate();
if (isValid) {
const payload = getTemplatePayload();
try {
await updateReleasePlanTemplate({
...payload,
id: templateId,
milestones: template.milestones,
});
navigate('/release-management');
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
}
};

if (!releasePlansEnabled) {
return null;
}

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>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Input from 'component/common/Input/Input';
import { styled } from '@mui/material';

const StyledInputDescription = styled('p')(({ theme }) => ({
marginBottom: theme.spacing(1),
}));

const StyledInput = styled(Input)(({ theme }) => ({
width: '100%',
marginBottom: theme.spacing(2),
}));

interface ITemplateForm {
name: string;
setName: React.Dispatch<React.SetStateAction<string>>;
description: string;
setDescription: React.Dispatch<React.SetStateAction<string>>;
errors: { [key: string]: string };
clearErrors: () => void;
}

export const TemplateForm: React.FC<ITemplateForm> = ({
name,
setName,
description,
setDescription,
errors,
clearErrors,
}) => {
return (
<>
<StyledInputDescription>
What would you like to call your template?
</StyledInputDescription>
<StyledInput
label='Template name'
value={name}
onChange={(e) => setName(e.target.value)}
error={Boolean(errors.name)}
errorText={errors.name}
onFocus={() => clearErrors()}
autoFocus
/>
<StyledInputDescription>
What's the purpose of this template?
</StyledInputDescription>
<StyledInput
label='Template description (optional)'
value={description}
onChange={(e) => setDescription(e.target.value)}
error={Boolean(errors.description)}
errorText={errors.description}
onFocus={() => clearErrors()}
/>
</>
);
};
Loading

0 comments on commit 7feba0c

Please sign in to comment.