From 2129352311dc68409bd716cb1248ac9bda15f9df Mon Sep 17 00:00:00 2001 From: John Gedeon <940173+gidjin@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:35:40 -0800 Subject: [PATCH] feat: Support published status for landing pages and preview of draft pages (#1175) * reword banner to line up with status * add support for displaying draft landing to cms users * update types so tests continue to pass * show draft tag on landing page index * hide unpublished pages from non-cms users * support pages published in the future * add long titled sample to storybook * undoes hiding publish date and sets colors for different status tags * sets default tag color to mars in case other statuses are addes later * update tests for showing published tag * hide status tag for landing pages for portal user --------- Co-authored-by: Shauna Keating --- src/__fixtures__/data/landingPage.ts | 4 + src/__fixtures__/data/landingPages.ts | 13 ++ src/__tests__/pages/landing.test.tsx | 64 ++++++++-- .../LandingPageIndexTable.module.scss | 13 ++ .../LandingPageIndexTable.stories.tsx | 34 +++++- .../LandingPageIndexTable.test.tsx | 111 +++++++++++++++++- .../LandingPageIndexTable.tsx | 32 ++++- .../SingleArticle/SingleArticle.test.tsx | 4 +- .../SingleArticle/SingleArticle.tsx | 2 +- src/operations/cms/queries/getLandingPage.ts | 2 + src/operations/cms/queries/getLandingPages.ts | 2 + src/pages/landing/[landingPage]/index.tsx | 13 +- src/pages/landing/index.tsx | 51 ++++++-- src/styles/pages/landingPage.module.scss | 8 ++ 14 files changed, 320 insertions(+), 33 deletions(-) diff --git a/src/__fixtures__/data/landingPage.ts b/src/__fixtures__/data/landingPage.ts index 14963bc72..f65cc488f 100644 --- a/src/__fixtures__/data/landingPage.ts +++ b/src/__fixtures__/data/landingPage.ts @@ -1,9 +1,13 @@ +import { DateTime } from 'luxon' + export const mockLandingPage = { __typename: 'LandingPage', pageTitle: 'Test Landing Page', pageDescription: 'em ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque ac odio ultrices, varius diam at, iaculis sapien. Integer risus quam, congue quis nibh in, iaculis ultrices justo. Sed viverra, massa in finibus vehicula, odio dui fringilla tellus, nec consequat arcu nulla eu augue. Maecenas at ornare orci. Aenean mattis et sapien at vulputate. Sed vel arcu at lorem consequat pulvinar quis ac ante. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Praesent commodo eros eget gravida hendrerit. Suspendisse facilisis odio vel lacus mollis condimentum. Proin in lectus et erat congue luctus non et ligula. Aenean elementum, risus quis tristique cursus, metus leo ornare sem, ut convallis dui velit sit amet mauris.', slug: 'a-page', + publishedDate: DateTime.now().toISO(), + status: 'Published', documents: [ { __typename: 'DocumentSection', diff --git a/src/__fixtures__/data/landingPages.ts b/src/__fixtures__/data/landingPages.ts index 6eb399b0e..4fa50a403 100644 --- a/src/__fixtures__/data/landingPages.ts +++ b/src/__fixtures__/data/landingPages.ts @@ -1,12 +1,25 @@ +import { DateTime } from 'luxon' + export const mockLandingPages = [ { pageDescription: 'This is a test landing page', pageTitle: 'Test Landing Page', slug: 'test-landing-page', + publishedDate: DateTime.now().toISO()!, + status: 'Published' as 'Draft' | 'Published' | 'Archived', }, { pageDescription: 'Another landing page', pageTitle: 'Another Landing Page', slug: 'another-landing-page', + publishedDate: DateTime.now().toISO()!, + status: 'Published' as 'Draft' | 'Published' | 'Archived', + }, + { + pageDescription: 'Draft landing page', + pageTitle: 'Another Landing Page', + slug: 'draft-landing-page', + publishedDate: '', + status: 'Draft' as 'Draft' | 'Published' | 'Archived', }, ] diff --git a/src/__tests__/pages/landing.test.tsx b/src/__tests__/pages/landing.test.tsx index 964cf7016..0d043f186 100644 --- a/src/__tests__/pages/landing.test.tsx +++ b/src/__tests__/pages/landing.test.tsx @@ -2,32 +2,76 @@ * @jest-environment jsdom */ import { screen } from '@testing-library/react' +import { GetServerSidePropsContext } from 'next' import { renderWithAuth } from '../../testHelpers' +import { getSession } from 'lib/session' import Landing, { getServerSideProps } from 'pages/landing' import { mockLandingPages } from '__fixtures__/data/landingPages' -import { testUser1 } from '__fixtures__/authUsers' +import { cmsUser, testUser1 } from '__fixtures__/authUsers' +import { PublishableItemType } from 'types' +import { isPublished } from 'helpers/index' jest.mock('../../lib/keystoneClient', () => ({ client: { query: () => { return { - data: { - landingPages: mockLandingPages, - }, + data: { landingPages: mockLandingPages }, } }, }, })) +type LandingPage = { + pageTitle: string + slug: string +} & PublishableItemType + +const testContext = {} as unknown as GetServerSidePropsContext + +jest.mock('lib/session', () => ({ + getSession: jest.fn(), +})) + +const mockedGetSession = getSession as jest.Mock + describe('Landing page index', () => { describe('when logged in', () => { - test('returns correct props from getServerSideProps', async () => { - const response = await getServerSideProps() + describe('geServerSideProps', () => { + test('returns only published sorted landing pages for non-cms user', async () => { + mockedGetSession.mockImplementationOnce(() => + Promise.resolve({ passport: { user: testUser1 } }) + ) + const response = await getServerSideProps(testContext) + const expectedSortedPages = mockLandingPages + .filter(isPublished) + .sort((a: LandingPage, b: LandingPage) => + a.pageTitle.localeCompare(b.pageTitle) + ) + + expect(response).toEqual({ + props: { + landingPages: expectedSortedPages, + showStatus: false, + }, + }) + }) + + test('returns all sorted landing pages for cms user', async () => { + mockedGetSession.mockImplementationOnce(() => + Promise.resolve({ passport: { user: cmsUser } }) + ) + const response = await getServerSideProps(testContext) + const expectedSortedPages = mockLandingPages.sort( + (a: LandingPage, b: LandingPage) => + a.pageTitle.localeCompare(b.pageTitle) + ) - expect(response).toEqual({ - props: { - landingPages: [...mockLandingPages], - }, + expect(response).toEqual({ + props: { + landingPages: expectedSortedPages, + showStatus: true, + }, + }) }) }) diff --git a/src/components/LandingPageIndexTable/LandingPageIndexTable.module.scss b/src/components/LandingPageIndexTable/LandingPageIndexTable.module.scss index addb70720..f5fbd885b 100644 --- a/src/components/LandingPageIndexTable/LandingPageIndexTable.module.scss +++ b/src/components/LandingPageIndexTable/LandingPageIndexTable.module.scss @@ -19,4 +19,17 @@ > tbody > tr:nth-child(even) > td { background-color: var(--lp-even-row); } + + .status { + color: $theme-spacebase-dark; + background: $theme-mars-light; + position: relative; + float: right; + } + :global(.usa-tag.Archived) { + @include u-bg('blue-cool-20'); + } + :global(.usa-tag.Published) { + @include u-bg('green-cool-20'); + } } diff --git a/src/components/LandingPageIndexTable/LandingPageIndexTable.stories.tsx b/src/components/LandingPageIndexTable/LandingPageIndexTable.stories.tsx index 19e3d80df..97ac6ad02 100644 --- a/src/components/LandingPageIndexTable/LandingPageIndexTable.stories.tsx +++ b/src/components/LandingPageIndexTable/LandingPageIndexTable.stories.tsx @@ -1,5 +1,6 @@ import React from 'react' import { Meta } from '@storybook/react' +import { DateTime } from 'luxon' import LandingPageIndexTable from './LandingPageIndexTable' export default { @@ -11,17 +12,46 @@ const testLandingPages = [ { pageTitle: 'Test Landing Page 1', slug: 'test-landing-page-1', + publishedDate: DateTime.now().toISO()!, + status: 'Published' as 'Draft' | 'Published' | 'Archived', + }, + { + pageTitle: 'Test Landing Page 4', + slug: 'test-landing-page-4', + publishedDate: DateTime.now().toISO()!, + status: 'Archived' as 'Draft' | 'Published' | 'Archived', }, { pageTitle: 'Test Landing Page 2', slug: 'test-landing-page-2', + publishedDate: DateTime.now().toISO()!, + status: 'Published' as 'Draft' | 'Published' | 'Archived', }, { pageTitle: 'Test Landing Page 3', slug: 'test-landing-page-3', + publishedDate: DateTime.now().toISO()!, + status: 'Draft' as 'Draft' | 'Published' | 'Archived', + }, + { + pageTitle: 'Test Landing Page 5', + slug: 'test-landing-page-5', + publishedDate: DateTime.now().plus({ weeks: 2 }).toISO()!, + status: 'Published' as 'Draft' | 'Published' | 'Archived', + }, + { + pageTitle: + 'Test Landing Page 6 with a really long title to ensure that the table is responsive', + slug: 'test-landing-page-6', + publishedDate: DateTime.now().plus({ weeks: 3 }).toISO()!, + status: 'Published' as 'Draft' | 'Published' | 'Archived', }, ] -export const ExampleLandingPageIndexTable = () => ( - +export const ShowStatus = () => ( + +) + +export const HideStatus = () => ( + ) diff --git a/src/components/LandingPageIndexTable/LandingPageIndexTable.test.tsx b/src/components/LandingPageIndexTable/LandingPageIndexTable.test.tsx index 89724b523..770525bd0 100644 --- a/src/components/LandingPageIndexTable/LandingPageIndexTable.test.tsx +++ b/src/components/LandingPageIndexTable/LandingPageIndexTable.test.tsx @@ -3,26 +3,52 @@ */ import React from 'react' import { render } from '@testing-library/react' +import { DateTime } from 'luxon' import LandingPageIndexTable from './LandingPageIndexTable' +const expectedFutueDate = DateTime.now().plus({ weeks: 2 }) +const expectedPastDate = DateTime.now().minus({ weeks: 2 }) const testLandingPages = [ { pageTitle: 'Test Landing Page 1', slug: 'test-landing-page-1', + publishedDate: DateTime.now().toISO()!, + status: 'Published' as 'Draft' | 'Published' | 'Archived', }, { pageTitle: 'Test Landing Page 2', slug: 'test-landing-page-2', + publishedDate: expectedPastDate.toISO()!, + status: 'Published' as 'Draft' | 'Published' | 'Archived', }, { pageTitle: 'Test Landing Page 3', slug: 'test-landing-page-3', + publishedDate: DateTime.now().toISO()!, + status: 'Draft' as 'Draft' | 'Published' | 'Archived', + }, + { + pageTitle: 'Test Landing Page 4', + slug: 'test-landing-page-4', + publishedDate: DateTime.now().toISO()!, + status: 'Archived' as 'Draft' | 'Published' | 'Archived', + }, + { + pageTitle: 'Test Landing Page 5', + slug: 'test-landing-page-5', + publishedDate: expectedFutueDate.toISO()!, + status: 'Published' as 'Draft' | 'Published' | 'Archived', }, ] describe('LandingPageIndexTable', () => { test('renders without error', () => { - render() + render( + + ) const table = document.querySelector('table') expect(table).toBeInTheDocument() @@ -30,6 +56,87 @@ describe('LandingPageIndexTable', () => { expect(table).toHaveClass('usa-table--borderless') const rows = document.querySelectorAll('tr') - expect(rows.length).toEqual(3) + expect(rows.length).toEqual(5) + }) + + test('renders draft tag for draft page', () => { + render( + + ) + + const rows = document.querySelectorAll('tr') + + // expect draft page to have a tag + const draftTag = rows[2].querySelector('td')?.querySelector('span') + expect(draftTag).toHaveTextContent('Draft') + }) + + test('renders tag for page published in past', () => { + render( + + ) + + const rows = document.querySelectorAll('tr') + + // expect published page to have a tag + const published = rows[1].querySelector('td')?.querySelector('span') + expect(published).toHaveTextContent( + `Published on: ${expectedPastDate.toFormat('dd MMM yyyy HH:mm')}` + ) + }) + + test('renders published tag for page published in the future', () => { + render( + + ) + + const rows = document.querySelectorAll('tr') + + // expect published page to have a tag + const published = rows[4].querySelector('td')?.querySelector('span') + expect(published).toHaveTextContent( + `Publishing on: ${expectedFutueDate.toFormat('dd MMM yyyy HH:mm')}` + ) + }) + + test('renders archived tag for archived page', () => { + render( + + ) + + const rows = document.querySelectorAll('tr') + + // expect archived page to have a tag + const archivedTag = rows[3].querySelector('td')?.querySelector('span') + expect(archivedTag).toHaveTextContent('Archived') + }) + + test('renders no status tags if showStatus false', () => { + render( + + ) + + const rows = document.querySelectorAll('tr') + + // expect all pages to not have a tag + rows.forEach((row) => { + const noTag = row.querySelector('td')?.querySelector('span') + expect(noTag).toBeNull() + }) }) }) diff --git a/src/components/LandingPageIndexTable/LandingPageIndexTable.tsx b/src/components/LandingPageIndexTable/LandingPageIndexTable.tsx index e82231205..ae2dae174 100644 --- a/src/components/LandingPageIndexTable/LandingPageIndexTable.tsx +++ b/src/components/LandingPageIndexTable/LandingPageIndexTable.tsx @@ -1,30 +1,56 @@ import React from 'react' -import { Table } from '@trussworks/react-uswds' +import { Table, Tag } from '@trussworks/react-uswds' import Link from 'next/link' +import { DateTime } from 'luxon' import styles from './LandingPageIndexTable.module.scss' +import { PublishableItemType } from 'types' type LandingPage = { pageTitle: string slug: string -} +} & PublishableItemType type LandingPageIndexTableProps = { landingPages: LandingPage[] + showStatus: boolean } const LandingPageIndexTable = ({ landingPages, + showStatus, }: LandingPageIndexTableProps) => { return ( {landingPages.map((landingPage: LandingPage) => { + const rowId = `landing_page_${landingPage.slug}` + // if the date is in the past we want to show past tense text + const publishText = + landingPage.publishedDate && + DateTime.fromISO(landingPage.publishedDate) > DateTime.now() + ? 'Publishing' + : 'Published' + // if a future publish date is set format the date for display + // we don't need to check if the date is in the future as the + // isPublished helper function will return false if the date is in the future + const status = + landingPage.status === 'Published' + ? `${publishText} on: ${DateTime.fromISO( + landingPage.publishedDate + ).toFormat('dd MMM yyyy HH:mm')}` + : landingPage.status return ( - + ) diff --git a/src/components/SingleArticle/SingleArticle.test.tsx b/src/components/SingleArticle/SingleArticle.test.tsx index 98315a8fd..c2634403a 100644 --- a/src/components/SingleArticle/SingleArticle.test.tsx +++ b/src/components/SingleArticle/SingleArticle.test.tsx @@ -12,7 +12,7 @@ describe('SingleArticle component', () => { test('renders the article', () => { render() - const banner = screen.queryByText('Unpublished Article Preview') + const banner = screen.queryByText('Draft Article Preview') expect(banner).toBeNull() expect(screen.getByText('May 18, 2022')).toBeInTheDocument() expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent( @@ -46,7 +46,7 @@ describe('SingleArticle component', () => { const unpublished: ArticleRecord = { ...testArticle, status: 'Draft' } render() - const banner = screen.getByText('Unpublished Article Preview') + const banner = screen.getByText('Draft Article Preview') expect(banner).toBeVisible() expect(banner).toHaveClass('previewBanner') }) diff --git a/src/components/SingleArticle/SingleArticle.tsx b/src/components/SingleArticle/SingleArticle.tsx index a8f870c86..3f034f2b2 100644 --- a/src/components/SingleArticle/SingleArticle.tsx +++ b/src/components/SingleArticle/SingleArticle.tsx @@ -61,7 +61,7 @@ export const SingleArticle = ({ article }: { article: ArticleRecord }) => { return (
{!isPublished(article) && ( -

Unpublished Article Preview

+

Draft Article Preview

)}
{hero && ( diff --git a/src/operations/cms/queries/getLandingPage.ts b/src/operations/cms/queries/getLandingPage.ts index fb6f19c98..d436c2b91 100644 --- a/src/operations/cms/queries/getLandingPage.ts +++ b/src/operations/cms/queries/getLandingPage.ts @@ -6,6 +6,8 @@ export const GET_LANDING_PAGE = gql` pageTitle pageDescription slug + status + publishedDate documents { title document { diff --git a/src/operations/cms/queries/getLandingPages.ts b/src/operations/cms/queries/getLandingPages.ts index 17a5f1321..52f235091 100644 --- a/src/operations/cms/queries/getLandingPages.ts +++ b/src/operations/cms/queries/getLandingPages.ts @@ -6,6 +6,8 @@ export const GET_LANDING_PAGES = gql` pageTitle pageDescription slug + status + publishedDate } } ` diff --git a/src/pages/landing/[landingPage]/index.tsx b/src/pages/landing/[landingPage]/index.tsx index b6616ff78..b3e17181a 100644 --- a/src/pages/landing/[landingPage]/index.tsx +++ b/src/pages/landing/[landingPage]/index.tsx @@ -16,6 +16,8 @@ import { client } from 'lib/keystoneClient' import { GET_LANDING_PAGE } from 'operations/cms/queries/getLandingPage' import { CMSBookmark, CollectionRecord } from 'types' import { isPdf, handleOpenPdfLink } from 'helpers/openDocumentLink' +import { getSession } from 'lib/session' +import { isCmsUser, isPublished } from 'helpers/index' type DocumentsType = { title: string @@ -131,6 +133,9 @@ const LandingPage = ({ return ( + {!isPublished(landingPage) && ( +

Draft Landing Page Preview

+ )}
) @@ -144,6 +149,9 @@ LandingPage.getLayout = (page: JSX.Element) => export const getServerSideProps: GetServerSideProps = async (context) => { const { landingPage: slug } = context.query + const session = await getSession(context.req, context.res) + const user = session?.passport?.user + const { data: { landingPage }, } = await client.query({ @@ -151,7 +159,10 @@ export const getServerSideProps: GetServerSideProps = async (context) => { variables: { slug }, }) - if (!landingPage) { + // if landing page is not published or not found return 404 + // unless the current user is a CMS user or admin + // then allow them to see any article + if (!landingPage || (!isPublished(landingPage) && !isCmsUser(user))) { return { notFound: true, } diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx index d9efaa321..956defe71 100644 --- a/src/pages/landing/index.tsx +++ b/src/pages/landing/index.tsx @@ -1,25 +1,28 @@ -import { InferGetServerSidePropsType } from 'next' +import { GetServerSideProps, InferGetServerSidePropsType } from 'next' import LandingPageIndexTable from 'components/LandingPageIndexTable/LandingPageIndexTable' import { withDefaultLayout } from 'layout/DefaultLayout/DefaultLayout' import { GET_LANDING_PAGES } from 'operations/cms/queries/getLandingPages' import { client } from 'lib/keystoneClient' +import { PublishableItemType } from 'types' +import { isCmsUser, isPublished } from 'helpers/index' +import { getSession } from 'lib/session' type LandingPage = { pageTitle: string slug: string -} +} & PublishableItemType const Landing = ({ landingPages, + showStatus, }: InferGetServerSidePropsType) => { - const sortedLandingPages = landingPages.sort( - (a: LandingPage, b: LandingPage) => a.pageTitle.localeCompare(b.pageTitle) - ) - return (

Landing Pages

- +
) } @@ -28,16 +31,40 @@ export default Landing Landing.getLayout = (page: JSX.Element) => withDefaultLayout(page, false) -export async function getServerSideProps() { +export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const session = await getSession(req, res) + const user = session?.passport?.user + const { data: { landingPages }, } = await client.query({ query: GET_LANDING_PAGES, }) - return { - props: { - landingPages, - }, + // Sort landing pages + const sortableCopy = [...landingPages] + if (isCmsUser(user)) { + const sortedLandingPages = sortableCopy.sort( + (a: LandingPage, b: LandingPage) => a.pageTitle.localeCompare(b.pageTitle) + ) + return { + props: { + landingPages: sortedLandingPages, + showStatus: true, + }, + } + } else { + // non cms user filter out draft and archived pages + const sortedLandingPages = sortableCopy + .filter(isPublished) + .sort((a: LandingPage, b: LandingPage) => + a.pageTitle.localeCompare(b.pageTitle) + ) + return { + props: { + landingPages: sortedLandingPages, + showStatus: false, + }, + } } } diff --git a/src/styles/pages/landingPage.module.scss b/src/styles/pages/landingPage.module.scss index f06f7cfd6..f2a6fe929 100644 --- a/src/styles/pages/landingPage.module.scss +++ b/src/styles/pages/landingPage.module.scss @@ -64,3 +64,11 @@ } } } + +h2.previewBanner { + position: sticky; + top: 0; + text-align: center; + background: var(--btn-secondary-bg); + color: var(--btn-secondary-text); +}
{landingPage.pageTitle} + + {showStatus ? ( + + {status} + + ) : null}