From 01c53ac80f91ee0455eeb03e67eb406cfb70e682 Mon Sep 17 00:00:00 2001 From: bhansell1 Date: Wed, 21 Jan 2026 09:29:52 +0000 Subject: [PATCH 01/10] CCM-12890: add copy to clipboard button --- .../MessagePlansList.test.tsx.snap | 8 + .../MessagePlansList/MessagePlansList.tsx | 35 +++- frontend/src/content/content.ts | 2 + .../use-copy-table-to-clipboard.hook.test.tsx | 151 ++++++++++++++++++ .../hooks/use-copy-table-to-clipboard.hook.ts | 123 ++++++++++++++ 5 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 frontend/src/hooks/__tests__/use-copy-table-to-clipboard.hook.test.tsx create mode 100644 frontend/src/hooks/use-copy-table-to-clipboard.hook.ts diff --git a/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlansList.test.tsx.snap b/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlansList.test.tsx.snap index 41d128f97..156b78004 100644 --- a/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlansList.test.tsx.snap +++ b/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlansList.test.tsx.snap @@ -101,6 +101,14 @@ exports[`MessagePlansList matches snapshot when data is available 1`] = ` + diff --git a/frontend/src/components/molecules/MessagePlansList/MessagePlansList.tsx b/frontend/src/components/molecules/MessagePlansList/MessagePlansList.tsx index 104746c09..23c67decd 100644 --- a/frontend/src/components/molecules/MessagePlansList/MessagePlansList.tsx +++ b/frontend/src/components/molecules/MessagePlansList/MessagePlansList.tsx @@ -2,13 +2,14 @@ import classNames from 'classnames'; import content from '@content/content'; -import { Details, Table } from 'nhsuk-react-components'; +import { Button, Details, Table } from 'nhsuk-react-components'; import { format } from 'date-fns/format'; import Link from 'next/link'; import { MarkdownContent } from '@molecules/MarkdownContent/MarkdownContent'; import type { RoutingConfigStatusActive } from 'nhs-notify-backend-client'; import { messagePlanStatusToDisplayText } from 'nhs-notify-web-template-management-utils'; import { interpolate } from '@utils/interpolate'; +import { useCopyTableToClipboard } from '@hooks/use-copy-table-to-clipboard.hook'; export type MessagePlanListItem = { name: string; @@ -30,6 +31,18 @@ export const MessagePlansList = (props: MessagePlansListProps) => { const { status, count } = props; const statusDisplayMapping = messagePlanStatusToDisplayText(status); const statusDisplayLower = statusDisplayMapping.toLowerCase(); + const { copyToClipboard, copied } = + useCopyTableToClipboard(); + + const handleCopyToClipboard = async () => { + await copyToClipboard({ + data: props.plans, + columns: [ + { key: 'name', header: 'routing_plan_name' }, + { key: 'id', header: 'routing_plan_id' }, + ], + }); + }; const header = ( @@ -68,10 +81,22 @@ export const MessagePlansList = (props: MessagePlansListProps) => { {rows.length > 0 ? ( - - {header} - {rows} -
+ <> + + {header} + {rows} +
+ + ) : ( & { + name: string; + id: string; + value: number; +}; + +describe('useCopyTableToClipboard', () => { + let mockClipboardWrite: jest.Mock; + + beforeEach(() => { + mockClipboardWrite = jest.fn().mockResolvedValue(undefined); + + Object.defineProperty(navigator, 'clipboard', { + value: { write: mockClipboardWrite }, + writable: true, + configurable: true, + }); + + global.ClipboardItem = jest.fn( + (data) => data + ) as unknown as typeof ClipboardItem; + + jest.useFakeTimers(); + }); + + afterEach(() => { + act(() => { + jest.runOnlyPendingTimers(); + }); + jest.useRealTimers(); + jest.clearAllMocks(); + }); + + it('should copy data in both CSV and HTML formats to clipboard', async () => { + const { result } = renderHook(() => useCopyTableToClipboard()); + + const testData: TestData[] = [ + { name: 'Test "quoted" value', id: 'id-1', value: 100 }, + { name: '', id: 'id & value', value: 200 }, + ]; + + await act(async () => { + await result.current.copyToClipboard({ + data: testData, + columns: [ + { key: 'name', header: 'Name' }, + { key: 'id', header: 'ID' }, + ], + }); + }); + + expect(mockClipboardWrite).toHaveBeenCalledTimes(1); + + const callArgs = mockClipboardWrite.mock.calls[0][0]; + const clipboardItem = callArgs[0]; + const csv = clipboardItem['text/plain']; + const html = clipboardItem['text/html']; + + expect(csv).toContain('"Name","ID"'); + expect(csv).toContain('"Test ""quoted"" value","id-1"'); + expect(csv).toContain('"id & value"'); + + expect(html).toContain(''); + expect(html).toContain(''); + expect(html).toContain('<script>'); + expect(html).toContain('id & value'); + expect(html).not.toContain('', id: 'id & value', value: 200 }, + { name: 'Test "quoted" value', id: 'id-1', value: '100' }, + { name: '
Name