diff --git a/public/uploads/Guardian Ideal - FINAL - 1600 17Sept21.pdf b/public/uploads/Guardian Ideal - FINAL - 1600 17Sept21.pdf deleted file mode 100644 index 66bf31775..000000000 Binary files a/public/uploads/Guardian Ideal - FINAL - 1600 17Sept21.pdf and /dev/null differ diff --git a/public/uploads/US Space Force Enlisted Rank Insig Info Sheet (1).pdf b/public/uploads/US Space Force Enlisted Rank Insig Info Sheet (1).pdf deleted file mode 100644 index 3eab9c73d..000000000 Binary files a/public/uploads/US Space Force Enlisted Rank Insig Info Sheet (1).pdf and /dev/null differ diff --git a/public/uploads/USSF Guardian Commitment Poster.pdf b/public/uploads/USSF Guardian Commitment Poster.pdf deleted file mode 100644 index 8c9516363..000000000 Binary files a/public/uploads/USSF Guardian Commitment Poster.pdf and /dev/null differ diff --git a/src/__fixtures__/data/cmsAnnouncments.ts b/src/__fixtures__/data/cmsAnnouncments.ts index 49adc4e44..a48865d51 100644 --- a/src/__fixtures__/data/cmsAnnouncments.ts +++ b/src/__fixtures__/data/cmsAnnouncments.ts @@ -239,3 +239,111 @@ export const testAnnouncementWithDeletedArticle = { status: 'Published', publishedDate: '2022-05-17T13:44:39.796Z', } + +export const testAnnouncementWithPdfDocument = { + id: 'testAnnouncementDocument', + title: 'Test Announcement Document', + body: { + document: [ + { + type: 'paragraph', + children: [ + { + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + ], + }, + { + type: 'component-block', + props: { + link: { + value: { + data: { + file: { + url: 'https://test.com/file.pdf', + }, + }, + }, + discriminant: 'document', + }, + ctaText: 'Read more', + }, + children: [ + { + type: 'component-inline-prop', + children: [ + { + text: '', + }, + ], + }, + ], + component: 'callToAction', + }, + { + type: 'paragraph', + children: [ + { + text: '', + }, + ], + }, + ], + }, + status: 'Published', + publishedDate: '2022-05-17T13:44:39.796Z', +} + +export const testAnnouncementWithJpgDocument = { + id: 'testAnnouncementJpgDocument', + title: 'Test Announcement Jpg Document', + body: { + document: [ + { + type: 'paragraph', + children: [ + { + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + ], + }, + { + type: 'component-block', + props: { + link: { + value: { + data: { + file: { + url: 'https://test.com/file.jpg', + }, + }, + }, + discriminant: 'document', + }, + ctaText: 'Read more', + }, + children: [ + { + type: 'component-inline-prop', + children: [ + { + text: '', + }, + ], + }, + ], + component: 'callToAction', + }, + { + type: 'paragraph', + children: [ + { + text: '', + }, + ], + }, + ], + }, + status: 'Published', + publishedDate: '2022-05-17T13:44:39.796Z', +} diff --git a/src/components/AnnouncementInfo/AnnouncementInfo.test.tsx b/src/components/AnnouncementInfo/AnnouncementInfo.test.tsx index 9fe63efef..deff60cd6 100644 --- a/src/components/AnnouncementInfo/AnnouncementInfo.test.tsx +++ b/src/components/AnnouncementInfo/AnnouncementInfo.test.tsx @@ -2,17 +2,25 @@ * @jest-environment jsdom */ -import { render, screen } from '@testing-library/react' -import React from 'react' +import { render, screen, fireEvent } from '@testing-library/react' +import React from 'react' import AnnouncementInfo from './AnnouncementInfo' +import * as openDocumentLink from 'helpers/openDocumentLink' import { testAnnouncementWithArticle, testAnnouncementWithArticleNoSlug, testAnnouncementWithDeletedArticle, testAnnouncementWithUrl, + testAnnouncementWithPdfDocument, + testAnnouncementWithJpgDocument, } from '__fixtures__/data/cmsAnnouncments' +// Spy on the functions that test files and opens PDFs in the browser +// We are not mocking because we want to test isPdf +jest.spyOn(openDocumentLink, 'handleOpenPdfLink').mockImplementation() +jest.spyOn(openDocumentLink, 'isPdf') + describe('AnnouncementInfo component', () => { test('renders an announcement with a url CTA', () => { render() @@ -50,4 +58,48 @@ describe('AnnouncementInfo component', () => { const link = screen.getByRole('link', { name: 'View deleted article' }) expect(link).toHaveAttribute('href', '/404') }) + + test('renders an announcement with a pdf document CTA', async () => { + const pdfString = + testAnnouncementWithPdfDocument.body.document[1].props?.link?.value?.data + ?.file?.url + + render() + + expect(screen.getAllByText('Test Announcement Document')).toHaveLength(1) + expect(screen.getAllByText('Read more')).toHaveLength(1) + + const link = screen.getByRole('link', { name: 'Read more' }) + expect(link).toHaveAttribute('href', pdfString) + + fireEvent.click(link) + expect(openDocumentLink.isPdf).toHaveBeenCalledTimes(1) + expect(openDocumentLink.isPdf).toHaveBeenCalledWith(pdfString) + expect(openDocumentLink.handleOpenPdfLink).toHaveBeenCalledTimes(1) + expect(openDocumentLink.handleOpenPdfLink).toHaveBeenCalledWith(pdfString) + }) + + test('renders an announcement with a non-pdf document CTA', async () => { + jest.resetAllMocks() + + const jpgString = + testAnnouncementWithJpgDocument.body.document[1].props?.link?.value?.data + ?.file?.url + + render() + + expect(screen.getAllByText('Test Announcement Jpg Document')).toHaveLength( + 1 + ) + expect(screen.getAllByText('Read more')).toHaveLength(1) + + const link = screen.getByRole('link', { name: 'Read more' }) + expect(link).toHaveAttribute('href', jpgString) + + fireEvent.click(link) + expect(openDocumentLink.isPdf).toHaveBeenCalledTimes(1) + expect(openDocumentLink.isPdf).toHaveBeenCalledWith(jpgString) + + expect(openDocumentLink.handleOpenPdfLink).toHaveBeenCalledTimes(0) + }) }) diff --git a/src/components/AnnouncementInfo/AnnouncementInfo.tsx b/src/components/AnnouncementInfo/AnnouncementInfo.tsx index 909ca2624..c12974849 100644 --- a/src/components/AnnouncementInfo/AnnouncementInfo.tsx +++ b/src/components/AnnouncementInfo/AnnouncementInfo.tsx @@ -1,11 +1,13 @@ import React from 'react' import { DocumentRenderer } from '@keystone-6/document-renderer' import { InferRenderersForComponentBlocks } from '@keystone-6/fields-document/component-blocks' +import Link from 'next/link' import styles from './AnnouncementInfo.module.scss' -import { componentBlocks } from 'components/ComponentBlocks/callToAction' +import type { componentBlocks } from 'components/ComponentBlocks/callToAction' import { AnnouncementRecord } from 'types' import LinkTo from 'components/util/LinkTo/LinkTo' import AnnouncementDate from 'components/AnnouncementDate/AnnouncementDate' +import { isPdf, handleOpenPdfLink } from 'helpers/openDocumentLink' const AnnouncementInfo = ({ announcement, @@ -22,6 +24,8 @@ const AnnouncementInfo = ({ typeof componentBlocks > = { callToAction: (props: any) => { + const fileUrl = props.link.value.data?.file?.url || '' + return ( <> {props.link.discriminant === 'article' && ( @@ -47,6 +51,26 @@ const AnnouncementInfo = ({ {props.ctaText} )} + + {props.link.discriminant === 'document' && ( + // We need to use the default Next Link component + // with legacyBehavior=false, so we can pass in an onClick + // that will open PDFs in the browser + + { + if (isPdf(fileUrl)) { + e.preventDefault() + handleOpenPdfLink(fileUrl) + } else return + }} + href={fileUrl} + rel="noreferrer" + className="usa-button"> + {props.ctaText} + + )} ) }, diff --git a/src/components/ComponentBlocks/callToAction.tsx b/src/components/ComponentBlocks/callToAction.tsx index d5fb1e146..0dd16a51d 100644 --- a/src/components/ComponentBlocks/callToAction.tsx +++ b/src/components/ComponentBlocks/callToAction.tsx @@ -43,6 +43,11 @@ export const componentBlocks = { listKey: 'Article', selection: 'id title slug', }), + document: fields.relationship({ + label: 'Document', + listKey: 'Document', + selection: 'id title file { url }', + }), } ), }, diff --git a/src/helpers/openDocumentLink.test.ts b/src/helpers/openDocumentLink.test.ts new file mode 100644 index 000000000..62dfa2019 --- /dev/null +++ b/src/helpers/openDocumentLink.test.ts @@ -0,0 +1,40 @@ +/** + * @jest-environment jsdom + */ +import axios from 'axios' +import { isPdf, handleOpenPdfLink } from './openDocumentLink' + +jest.mock('axios', () => ({ + get: jest.fn(() => Promise.resolve({ data: { blob: () => 'test' } })), +})) + +// Mock URL.createObjectURL +const mockCreateObjectURL = jest.fn() +const mockRevokeObjectURL = jest.fn() +URL.createObjectURL = mockCreateObjectURL +URL.revokeObjectURL = mockRevokeObjectURL + +// Mock window.open +const mockWindowOpen = jest.fn() +window.open = mockWindowOpen + +describe('isPdf', () => { + test('returns true if url ends with .pdf', () => { + expect(isPdf('https://www.google.com/test.pdf')).toBe(true) + }) + + test('returns false if url does not end with .pdf', () => { + expect(isPdf('https://www.google.com/test')).toBe(false) + }) +}) + +describe('handleOpenPdfLink', () => { + test('opens a new window if the url is a pdf', async () => { + const pdfString = 'https://www.google.com/test.pdf' + await handleOpenPdfLink(pdfString) + expect(axios.get).toHaveBeenCalled() + expect(mockCreateObjectURL).toHaveBeenCalled() + expect(mockWindowOpen).toHaveBeenCalled() + expect(mockRevokeObjectURL).toHaveBeenCalled() + }) +}) diff --git a/src/helpers/openDocumentLink.ts b/src/helpers/openDocumentLink.ts new file mode 100644 index 000000000..82c29a302 --- /dev/null +++ b/src/helpers/openDocumentLink.ts @@ -0,0 +1,19 @@ +import axios from 'axios' + +export const isPdf = (url: string) => { + return url.toString().endsWith('.pdf') +} + +export const handleOpenPdfLink = async (pdfString: string) => { + // Fetch the file from Keystone / S3 + const res = await axios.get(pdfString, { responseType: 'blob' }) + + // Create a blob from the file and open it in a new tab + const blobData = await res.data + const file = new Blob([blobData], { type: 'application/pdf' }) + const fileUrl = URL.createObjectURL(file) + + window.open(fileUrl) + // Let the browser know not to keep the reference to the file any longer. + URL.revokeObjectURL(fileUrl) +} diff --git a/src/pages/ussf-documentation.tsx b/src/pages/ussf-documentation.tsx index 26392d272..81df13c23 100644 --- a/src/pages/ussf-documentation.tsx +++ b/src/pages/ussf-documentation.tsx @@ -1,8 +1,8 @@ import React from 'react' import { Accordion, Grid } from '@trussworks/react-uswds' import { InferGetServerSidePropsType } from 'next' +import Link from 'next/link' import { client } from '../lib/keystoneClient' -import LinkTo from 'components/util/LinkTo/LinkTo' import EPubsCard from 'components/EPubsCard/EPubsCard' import { withDefaultLayout } from 'layout/DefaultLayout/DefaultLayout' import styles from 'styles/pages/ussf-documentation.module.scss' @@ -10,73 +10,13 @@ import { GET_DOCUMENTS_PAGE } from 'operations/cms/queries/getDocumentsPage' import { DocumentType, DocumentPageType, DocumentSectionType } from 'types' import { useUser } from 'hooks/useUser' import Loader from 'components/Loader/Loader' +import { isPdf, handleOpenPdfLink } from 'helpers/openDocumentLink' -// Export for easier unit testing -// We're leaving these hardcoded docs as a backup until -// the switch to CMS-only content is complete -// #TODO: Remove these links and the documents stored in the repo -export const staticPage: DocumentPageType = { - id: '', - pageTitle: 'USSF Documentation', - sections: [ - { - id: '17ced46f-9523-4b18-b778-c9e6f9dced3e', - title: 'Essential Reading', - document: [ - { - // Generating random ids so iterator in component is happy - // These are not stored anywhere and mean nothing. - id: '16ced46f-9523-4b18-b778-c9e6f9dced3e', - title: - 'Space Capstone Publication: Spacepower. Doctrine for Space Forces', - file: { - url: 'https://www.spaceforce.mil/Portals/1/Space%20Capstone%20Publication_10%20Aug%202020.pdf', - }, - }, - { - id: '8d619762-6ecd-47df-92a2-c172d95c488c', - title: 'CSO’s Planning Guidance', - file: { - url: 'https://media.defense.gov/2020/Nov/09/2002531998/-1/-1/0/CSO%20PLANNING%20GUIDANCE.PDF', - }, - }, - { - id: '39a31099-e760-4bcb-9726-781ce2a7ac4b', - title: ' Guardian Ideal', - file: { - url: '/uploads/Guardian Ideal - FINAL - 1600 17Sept21.pdf', - }, - }, - { - id: '983504a5-e3d1-43aa-af88-28ba0a7345ba', - title: 'USSF Enlisted Rank and Insignia', - file: { - url: '/uploads/US Space Force Enlisted Rank Insig Info Sheet (1).pdf', - }, - }, - { - id: '272a5b6b-bc30-4775-887e-eeece78b214d', - title: ' USSF/S1 Health, Wellness and Fitness Memo (16 MAR 2022)', - file: { - url: '/uploads/USSF Health Wellness and Fitness Memo dated 16Mar22.pdf', - }, - }, - { - id: '20accc42-3e4a-4d21-9fd8-3fcc2ed4531b', - title: 'USSF Guardian Commitment Poster', - file: { - url: '/uploads/USSF Guardian Commitment Poster.pdf', - }, - }, - ], - }, - ], -} const USSFDocumentation = ({ documentsPage, }: InferGetServerSidePropsType) => { const { user } = useUser() - // LaunchDarkly toggle for cms vs static data + return !user ? ( ) : ( @@ -100,13 +40,22 @@ const USSFDocumentation = ({ content: (
{s.document.map((d: DocumentType) => ( - { + if (isPdf(d.file.url)) { + e.preventDefault() + handleOpenPdfLink(d.file.url) + } else return + }} key={d.id} - target="_blank" rel="noreferrer noopener" href={`${d.file.url}`}> {d.title} - + ))}
),