Skip to content

Commit d6ea258

Browse files
Implement delete template
1 parent cb74bd5 commit d6ea258

File tree

22 files changed

+461
-7
lines changed

22 files changed

+461
-7
lines changed

frontend/amplify/backend.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,9 @@ const attachPolicy = new PolicyStatement({
2020
});
2121

2222
sendEmailLambda.addToRolePolicy(attachPolicy);
23+
24+
backend.data.resources.cfnResources.amplifyDynamoDbTables.TemplateStorage.timeToLiveAttribute =
25+
{
26+
attributeName: 'ttl',
27+
enabled: true,
28+
};

frontend/amplify/data/resource.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ enum TemplateType {
1010
enum TemplateStatus {
1111
NOT_YET_SUBMITTED = 'NOT_YET_SUBMITTED',
1212
SUBMITTED = 'SUBMITTED',
13+
DELETED = 'DELETED',
1314
}
1415

1516
const templateTypes = [
@@ -21,6 +22,7 @@ const templateTypes = [
2122
const templateStatuses = [
2223
TemplateStatus.NOT_YET_SUBMITTED,
2324
TemplateStatus.SUBMITTED,
25+
TemplateStatus.DELETED,
2426
] as const;
2527

2628
const TemplateStorageModel = {
@@ -31,6 +33,7 @@ const TemplateStorageModel = {
3133
name: a.string().required(),
3234
subject: a.string(),
3335
message: a.string().required(),
36+
ttl: a.integer(),
3437
};
3538

3639
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
import DeleteTemplatePage from '@app/delete-template/[templateId]/page';
5+
import { DeleteTemplate } from '@forms/DeleteTemplate/DeleteTemplate';
6+
import {
7+
EmailTemplate,
8+
TemplateType,
9+
TemplateStatus,
10+
} from 'nhs-notify-web-template-management-utils';
11+
import { redirect } from 'next/navigation';
12+
import { getTemplate } from '@utils/form-actions';
13+
14+
jest.mock('@utils/form-actions');
15+
jest.mock('next/navigation');
16+
jest.mock('@forms/DeleteTemplate/DeleteTemplate');
17+
18+
const redirectMock = jest.mocked(redirect);
19+
const getTemplateMock = jest.mocked(getTemplate);
20+
21+
describe('PreviewEmailTemplatePage', () => {
22+
beforeEach(jest.resetAllMocks);
23+
24+
it('should load page', async () => {
25+
const state: EmailTemplate = {
26+
id: 'template-id',
27+
version: 1,
28+
templateType: TemplateType.EMAIL,
29+
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
30+
name: 'template-name',
31+
subject: 'template-subject-line',
32+
message: 'template-message',
33+
};
34+
35+
getTemplateMock.mockResolvedValueOnce(state);
36+
37+
const page = await DeleteTemplatePage({
38+
params: {
39+
templateId: 'template-id',
40+
},
41+
});
42+
43+
expect(page).toEqual(<DeleteTemplate template={state} />);
44+
});
45+
46+
it('should redirect to invalid-template when no templateId is found', async () => {
47+
await DeleteTemplatePage({
48+
params: {
49+
templateId: 'template-id',
50+
},
51+
});
52+
53+
expect(redirectMock).toHaveBeenCalledWith('/invalid-template', 'replace');
54+
});
55+
56+
test('should redirect to invalid-template when template is already submitted', async () => {
57+
getTemplateMock.mockResolvedValueOnce({
58+
id: 'template-id',
59+
templateStatus: TemplateStatus.SUBMITTED,
60+
templateType: TemplateType.NHS_APP,
61+
name: 'template-name',
62+
message: 'template-message',
63+
version: 1,
64+
});
65+
66+
await DeleteTemplatePage({
67+
params: {
68+
templateId: 'template-id',
69+
},
70+
});
71+
72+
expect(redirectMock).toHaveBeenCalledWith('/invalid-template', 'replace');
73+
});
74+
75+
test('should redirect to manage-templates when template is already deleted', async () => {
76+
getTemplateMock.mockResolvedValueOnce({
77+
id: 'template-id',
78+
templateStatus: TemplateStatus.DELETED,
79+
templateType: TemplateType.NHS_APP,
80+
name: 'template-name',
81+
message: 'template-message',
82+
version: 1,
83+
});
84+
85+
await DeleteTemplatePage({
86+
params: {
87+
templateId: 'template-id',
88+
},
89+
});
90+
91+
expect(redirectMock).toHaveBeenCalledWith('/manage-templates', 'push');
92+
});
93+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { render } from '@testing-library/react';
2+
import { DeleteTemplate } from '@forms/DeleteTemplate/DeleteTemplate';
3+
import { mockDeep } from 'jest-mock-extended';
4+
import {
5+
EmailTemplate,
6+
TemplateFormState,
7+
} from 'nhs-notify-web-template-management-utils';
8+
9+
jest.mock('react-dom', () => {
10+
const originalModule = jest.requireActual('react-dom');
11+
12+
return {
13+
...originalModule,
14+
useFormState: (
15+
_: (
16+
formState: TemplateFormState,
17+
formData: FormData
18+
) => Promise<TemplateFormState>,
19+
initialState: TemplateFormState
20+
) => [initialState, '/action'],
21+
};
22+
});
23+
24+
test('renders component correctly', () => {
25+
const container = render(
26+
<DeleteTemplate
27+
template={mockDeep<EmailTemplate>({
28+
id: 'template-id',
29+
name: 'template-name',
30+
})}
31+
/>
32+
);
33+
34+
expect(container.asFragment()).toMatchSnapshot();
35+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`renders component correctly 1`] = `
4+
<DocumentFragment>
5+
<div
6+
class="nhsuk-grid-row"
7+
>
8+
<div
9+
class="nhsuk-grid-column-full"
10+
>
11+
<h1>
12+
Are you sure you want to delete the template 'template-name'?
13+
</h1>
14+
<p>
15+
The template will be removed and you won't be able to recover it
16+
</p>
17+
<form
18+
action="/templates/manage-templates"
19+
class="nhsuk-u-margin-right-3"
20+
style="display: inline;"
21+
>
22+
<button
23+
aria-disabled="false"
24+
class="nhsuk-button nhsuk-button--secondary"
25+
type="submit"
26+
>
27+
No, go back
28+
</button>
29+
</form>
30+
<form
31+
action="/action"
32+
style="display: inline;"
33+
>
34+
<button
35+
aria-disabled="false"
36+
class="nhsuk-button nhsuk-button--warning"
37+
type="submit"
38+
>
39+
Yes, delete template
40+
</button>
41+
</form>
42+
</div>
43+
</div>
44+
</DocumentFragment>
45+
`;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { redirect, RedirectType } from 'next/navigation';
2+
import { deleteTemplateAction } from '@forms/DeleteTemplate/server-action';
3+
import {
4+
NHSAppTemplate,
5+
TemplateStatus,
6+
TemplateType,
7+
} from 'nhs-notify-web-template-management-utils';
8+
import { saveTemplate } from '@utils/form-actions';
9+
10+
jest.mock('next/navigation');
11+
jest.mock('@utils/form-actions');
12+
13+
beforeAll(() => {
14+
jest.useFakeTimers();
15+
jest.setSystemTime(new Date('2022-01-01 09:00'));
16+
});
17+
18+
test('calls form action and redirects', async () => {
19+
const mockRedirect = jest.mocked(redirect);
20+
const mockSaveTemplate = jest.mocked(saveTemplate);
21+
22+
const mockTemplate: NHSAppTemplate = {
23+
id: 'template-id',
24+
name: 'template-name',
25+
message: 'template-message',
26+
version: 1,
27+
templateType: TemplateType.NHS_APP,
28+
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
29+
};
30+
31+
await deleteTemplateAction(mockTemplate);
32+
33+
expect(mockSaveTemplate).toHaveBeenCalledWith(
34+
{
35+
...mockTemplate,
36+
templateStatus: TemplateStatus.DELETED,
37+
},
38+
1_643_619_600
39+
);
40+
41+
expect(mockRedirect).toHaveBeenCalledWith(
42+
'/manage-templates',
43+
RedirectType.push
44+
);
45+
});

frontend/src/__tests__/components/molecules/__snapshots__/ManageTemplates.test.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ exports[`ManageTemplates component matches snapshot with not submitted status 1`
144144
class="nhsuk-u-margin-bottom-2"
145145
>
146146
<a
147-
href="#"
147+
href="/delete-template/1"
148148
>
149149
Delete
150150
</a>

frontend/src/__tests__/utils/form-actions.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,30 @@ test('saveTemplate', async () => {
133133
expect(response).toEqual(mockResponseData);
134134
});
135135

136+
test('saveTemplate - includes TTL', async () => {
137+
setup({
138+
models: {
139+
TemplateStorage: {
140+
update: jest.fn().mockReturnValue({ data: mockResponseData }),
141+
},
142+
},
143+
});
144+
145+
const response = await saveTemplate(
146+
{
147+
id: '0c1d3422-a2f6-44ef-969d-d513c7c9d212',
148+
version: 1,
149+
templateType: TemplateType.NHS_APP,
150+
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
151+
name: 'template-name',
152+
message: 'template-message',
153+
},
154+
10
155+
);
156+
157+
expect(response).toEqual(mockResponseData);
158+
});
159+
136160
test('saveTemplate - error handling', async () => {
137161
setup({
138162
models: {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use server';
2+
3+
import {
4+
PageProps,
5+
TemplateStatus,
6+
} from 'nhs-notify-web-template-management-utils';
7+
import { getTemplate } from '@utils/form-actions';
8+
import { redirect, RedirectType } from 'next/navigation';
9+
import { DeleteTemplate } from '@forms/DeleteTemplate/DeleteTemplate';
10+
import { validateChannelTemplate } from '@utils/validate-template';
11+
12+
const DeleteTemplatePage = async ({ params: { templateId } }: PageProps) => {
13+
const template = await getTemplate(templateId);
14+
15+
if (template?.templateStatus === TemplateStatus.DELETED) {
16+
return redirect('/manage-templates', RedirectType.push);
17+
}
18+
19+
const validatedTemplate = validateChannelTemplate(template);
20+
21+
if (!validatedTemplate) {
22+
return redirect('/invalid-template', RedirectType.replace);
23+
}
24+
25+
return <DeleteTemplate template={validatedTemplate} />;
26+
};
27+
28+
export default DeleteTemplatePage;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use client';
2+
3+
import { FC } from 'react';
4+
import { useFormState } from 'react-dom';
5+
import { ChannelTemplate } from 'nhs-notify-web-template-management-utils';
6+
import { deleteTemplatePageContent } from '@content/content';
7+
import { Button } from 'nhsuk-react-components';
8+
import { getBasePath } from '@utils/get-base-path';
9+
import { deleteTemplateAction } from './server-action';
10+
11+
type DeleteTemplateProps = {
12+
template: ChannelTemplate;
13+
};
14+
15+
export const DeleteTemplate: FC<DeleteTemplateProps> = ({ template }) => {
16+
const { pageHeading, hintText, noButtonText, yesButtonText } =
17+
deleteTemplatePageContent;
18+
19+
const [state, action] = useFormState(deleteTemplateAction, template);
20+
21+
const fullPageHeading = `${pageHeading} '${state.name}'?`;
22+
return (
23+
<div className='nhsuk-grid-row'>
24+
<div className='nhsuk-grid-column-full'>
25+
<h1>{fullPageHeading}</h1>
26+
<p>{hintText}</p>
27+
<form
28+
action={`${getBasePath()}/manage-templates`}
29+
className='nhsuk-u-margin-right-3'
30+
style={{ display: 'inline' }}
31+
>
32+
<Button secondary>{noButtonText}</Button>
33+
</form>
34+
<form action={action} style={{ display: 'inline' }}>
35+
<Button className='nhsuk-button--warning'>{yesButtonText}</Button>
36+
</form>
37+
</div>
38+
</div>
39+
);
40+
};

0 commit comments

Comments
 (0)