From cdf33988c5246a9f98aa2765c7ef63c19c560125 Mon Sep 17 00:00:00 2001 From: Amy Benson Date: Wed, 14 Aug 2024 13:15:40 +0100 Subject: [PATCH] EES-5419 import new data set version --- .../data/ReleaseApiDataSetDetailsPage.tsx | 71 ++++-- .../ReleaseApiDataSetDetailsPage.test.tsx | 210 +++++++++++++++++- .../components/ApiDataSetFinaliseBanner.tsx | 93 ++++++++ .../ApiDataSetFinaliseBanner.test.tsx | 78 +++++++ .../src/services/apiDataSetVersionService.ts | 23 +- .../components/NotificationBanner.module.scss | 10 + .../src/components/NotificationBanner.tsx | 25 ++- 7 files changed, 482 insertions(+), 28 deletions(-) create mode 100644 src/explore-education-statistics-admin/src/pages/release/data/components/ApiDataSetFinaliseBanner.tsx create mode 100644 src/explore-education-statistics-admin/src/pages/release/data/components/__tests__/ApiDataSetFinaliseBanner.test.tsx create mode 100644 src/explore-education-statistics-common/src/components/NotificationBanner.module.scss diff --git a/src/explore-education-statistics-admin/src/pages/release/data/ReleaseApiDataSetDetailsPage.tsx b/src/explore-education-statistics-admin/src/pages/release/data/ReleaseApiDataSetDetailsPage.tsx index feb9e34e737..14aa8ca2817 100644 --- a/src/explore-education-statistics-admin/src/pages/release/data/ReleaseApiDataSetDetailsPage.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/data/ReleaseApiDataSetDetailsPage.tsx @@ -3,6 +3,7 @@ import { useConfig } from '@admin/contexts/ConfigContext'; import ApiDataSetVersionSummaryList from '@admin/pages/release/data/components/ApiDataSetVersionSummaryList'; import DeleteDraftVersionButton from '@admin/pages/release/data/components/DeleteDraftVersionButton'; import ApiDataSetCreateModal from '@admin/pages/release/data/components/ApiDataSetCreateModal'; +import ApiDataSetFinaliseBanner from '@admin/pages/release/data/components/ApiDataSetFinaliseBanner'; import { useReleaseContext } from '@admin/pages/release/contexts/ReleaseContext'; import apiDataSetQueries from '@admin/queries/apiDataSetQueries'; import { @@ -25,9 +26,11 @@ import Tag, { TagProps } from '@common/components/Tag'; import TaskList from '@common/components/TaskList'; import TaskListItem from '@common/components/TaskListItem'; import { useQuery } from '@tanstack/react-query'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { generatePath, useHistory, useParams } from 'react-router-dom'; +export type DataSetFinalisingStatus = 'finalising' | 'finalised' | undefined; + // TODO: EES-4367 const showChangelog = false; // TODO: EES-4382 @@ -40,6 +43,9 @@ export default function ReleaseApiDataSetDetailsPage() { const { publicAppUrl } = useConfig(); const { release } = useReleaseContext(); + const [finalisingStatus, setFinalisingStatus] = + useState(undefined); + const { data: dataSet, isLoading, @@ -47,7 +53,10 @@ export default function ReleaseApiDataSetDetailsPage() { } = useQuery({ ...apiDataSetQueries.get(dataSetId), refetchInterval: data => { - return data?.draftVersion?.status === 'Processing' ? 3000 : false; + return data?.draftVersion?.status === 'Processing' || + finalisingStatus === 'finalising' + ? 3000 + : false; }, }); @@ -58,6 +67,24 @@ export default function ReleaseApiDataSetDetailsPage() { ? 'govuk-grid-column-one-half-from-desktop' : 'govuk-grid-column-two-thirds-from-desktop'; + const handleFinalise = async () => { + if (dataSet?.draftVersion) { + setFinalisingStatus('finalising'); + await apiDataSetVersionService.completeVersion({ + dataSetVersionId: dataSet?.draftVersion?.id, + }); + } + }; + + useEffect(() => { + if ( + finalisingStatus === 'finalising' && + dataSet?.draftVersion?.status !== 'Mapping' + ) { + setFinalisingStatus('finalised'); + } + }, [finalisingStatus, dataSet?.draftVersion?.status, setFinalisingStatus]); + const draftVersionSummary = dataSet?.draftVersion ? ( ) : null; - function getDataSetStatusColour(status: DataSetStatus): TagProps['colour'] { - switch (status) { - case 'Deprecated': - return 'purple'; - case 'Withdrawn': - return 'red'; - default: - return 'blue'; - } - } + const mappingComplete = + dataSet?.draftVersion?.mappingStatus && + dataSet.draftVersion.mappingStatus.filtersComplete && + dataSet.draftVersion.mappingStatus.locationsComplete; + + const showDraftVersionTasks = + finalisingStatus !== 'finalising' && + (dataSet?.draftVersion?.status === 'Draft' || + dataSet?.draftVersion?.status === 'Mapping'); return ( <> @@ -186,6 +212,14 @@ export default function ReleaseApiDataSetDetailsPage() { API data set details

{dataSet.title}

+ {mappingComplete && ( + + )} + - {dataSet.draftVersion?.status === 'Mapping' && ( + {showDraftVersionTasks && dataSet.draftVersion && (

Draft version tasks

@@ -343,3 +377,14 @@ export default function ReleaseApiDataSetDetailsPage() { ); } + +function getDataSetStatusColour(status: DataSetStatus): TagProps['colour'] { + switch (status) { + case 'Deprecated': + return 'purple'; + case 'Withdrawn': + return 'red'; + default: + return 'blue'; + } +} diff --git a/src/explore-education-statistics-admin/src/pages/release/data/__tests__/ReleaseApiDataSetDetailsPage.test.tsx b/src/explore-education-statistics-admin/src/pages/release/data/__tests__/ReleaseApiDataSetDetailsPage.test.tsx index 5a6f45e6ea4..15e43fd451b 100644 --- a/src/explore-education-statistics-admin/src/pages/release/data/__tests__/ReleaseApiDataSetDetailsPage.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/data/__tests__/ReleaseApiDataSetDetailsPage.test.tsx @@ -11,15 +11,18 @@ import _apiDataSetService, { ApiDataSetDraftVersion, ApiDataSetLiveVersion, } from '@admin/services/apiDataSetService'; +import _apiDataSetVersionService from '@admin/services/apiDataSetVersionService'; import { Release } from '@admin/services/releaseService'; import render from '@common-test/render'; -import { screen, within } from '@testing-library/react'; +import { screen, waitFor, within } from '@testing-library/react'; import React from 'react'; import { generatePath, MemoryRouter, Route } from 'react-router-dom'; jest.mock('@admin/services/apiDataSetService'); +jest.mock('@admin/services/apiDataSetVersionService'); const apiDataSetService = jest.mocked(_apiDataSetService); +const apiDataSetVersionService = jest.mocked(_apiDataSetVersionService); describe('ReleaseApiDataSetDetailsPage', () => { const testDataSet: ApiDataSet = { @@ -396,10 +399,213 @@ describe('ReleaseApiDataSetDetailsPage', () => { ).not.toBeInTheDocument(); }); + describe('finalising', () => { + beforeEach(() => { + jest.useFakeTimers({ advanceTimers: true }); + }); + afterEach(() => { + jest.useRealTimers(); + }); + + test('shows the finalise banner when mapping is complete', async () => { + apiDataSetService.getDataSet.mockResolvedValue({ + ...testDataSet, + draftVersion: { + ...testDraftVersion, + status: 'Mapping', + mappingStatus: { + filtersComplete: true, + locationsComplete: true, + }, + }, + latestLiveVersion: testLiveVersion, + }); + + renderPage(); + + expect( + await screen.findByText('Draft version details'), + ).toBeInTheDocument(); + + const banner = within(screen.getByTestId('notificationBanner')); + expect( + banner.getByRole('heading', { name: 'Action required' }), + ).toBeInTheDocument(); + expect( + banner.getByText('Draft API data set version is ready to be finalised'), + ).toBeInTheDocument(); + expect( + banner.getByRole('button', { name: 'Finalise this data set version' }), + ).toBeInTheDocument(); + }); + + test('successfully finalised', async () => { + apiDataSetService.getDataSet.mockResolvedValueOnce({ + ...testDataSet, + draftVersion: { + ...testDraftVersion, + status: 'Mapping', + mappingStatus: { + filtersComplete: true, + locationsComplete: true, + }, + }, + latestLiveVersion: testLiveVersion, + }); + apiDataSetService.getDataSet.mockResolvedValueOnce({ + ...testDataSet, + draftVersion: { + ...testDraftVersion, + status: 'Draft', + mappingStatus: { + filtersComplete: true, + locationsComplete: true, + }, + }, + latestLiveVersion: testLiveVersion, + }); + + const { user } = renderPage(); + + expect( + await screen.findByText('Draft version details'), + ).toBeInTheDocument(); + + expect(apiDataSetVersionService.completeVersion).not.toHaveBeenCalled(); + + const banner = within(screen.getByTestId('notificationBanner')); + expect( + banner.getByRole('heading', { name: 'Action required' }), + ).toBeInTheDocument(); + + await user.click( + banner.getByRole('button', { name: 'Finalise this data set version' }), + ); + + await waitFor(() => { + expect(apiDataSetVersionService.completeVersion).toHaveBeenCalledTimes( + 1, + ); + expect(apiDataSetVersionService.completeVersion).toHaveBeenCalledWith({ + dataSetVersionId: 'draft-version-id', + }); + }); + + expect( + banner.getByRole('heading', { name: 'Finalising' }), + ).toBeInTheDocument(); + expect( + banner.getByText('Finalising draft API data set version'), + ).toBeInTheDocument(); + expect( + banner.queryByRole('button', { + name: 'Finalise this data set version', + }), + ).not.toBeInTheDocument(); + + jest.runOnlyPendingTimers(); + + await waitFor(() => { + expect( + banner.getByText( + 'Draft API data set version is ready to be published', + ), + ).toBeInTheDocument(); + }); + + expect( + banner.getByRole('heading', { + name: 'Mappings finalised', + }), + ).toBeInTheDocument(); + + expect( + banner.queryByRole('button', { + name: 'Finalise this data set version', + }), + ).not.toBeInTheDocument(); + }); + + test('finalising failed', async () => { + apiDataSetService.getDataSet.mockResolvedValueOnce({ + ...testDataSet, + draftVersion: { + ...testDraftVersion, + status: 'Mapping', + mappingStatus: { + filtersComplete: true, + locationsComplete: true, + }, + }, + latestLiveVersion: testLiveVersion, + }); + apiDataSetService.getDataSet.mockResolvedValueOnce({ + ...testDataSet, + draftVersion: { + ...testDraftVersion, + status: 'Failed', + mappingStatus: { + filtersComplete: true, + locationsComplete: true, + }, + }, + latestLiveVersion: testLiveVersion, + }); + + const { user } = renderPage(); + + expect( + await screen.findByText('Draft version details'), + ).toBeInTheDocument(); + + expect(apiDataSetVersionService.completeVersion).not.toHaveBeenCalled(); + + const banner = within(screen.getByTestId('notificationBanner')); + expect( + banner.getByRole('heading', { name: 'Action required' }), + ).toBeInTheDocument(); + + await user.click( + banner.getByRole('button', { name: 'Finalise this data set version' }), + ); + + await waitFor(() => { + expect(apiDataSetVersionService.completeVersion).toHaveBeenCalledTimes( + 1, + ); + expect(apiDataSetVersionService.completeVersion).toHaveBeenCalledWith({ + dataSetVersionId: 'draft-version-id', + }); + }); + + expect( + banner.getByRole('heading', { name: 'Finalising' }), + ).toBeInTheDocument(); + expect( + banner.getByText('Finalising draft API data set version'), + ).toBeInTheDocument(); + expect( + banner.queryByRole('button', { + name: 'Finalise this data set version', + }), + ).not.toBeInTheDocument(); + + jest.runOnlyPendingTimers(); + + await waitFor(() => { + expect(banner.getByText('There is a problem')).toBeInTheDocument(); + }); + + expect( + banner.getByText('Data set version finalisation failed'), + ).toBeInTheDocument(); + }); + }); + function renderPage(options?: { release?: Release; dataSetId?: string }) { const { release = testRelease, dataSetId = 'data-set-id' } = options ?? {}; - render( + return render( void; +} + +export default function ApiDataSetFinaliseBanner({ + draftVersionStatus, + finalisingStatus, + onFinalise, +}: Props) { + if (finalisingStatus === 'finalising') { + return ( + +

This process can take a few minutes.

+
+ ); + } + + if (draftVersionStatus === 'Mapping') { + return ( + +

+ The mapping changes need to be finalised before the draft API data set + version can be published. After finalising, you will also be able to + view the changelog, add guidance notes and preview this API data set + version. +

+

+ You will not be able to make further changes after finalising. If you + need to make any changes to the mappings you will have to remove this + draft API data set version and create a new version. +

+ +
+ ); + } + + if (draftVersionStatus === 'Draft') { + return ( + +

+ If you need to make any changes to the mappings you will have to + remove this draft API data set version and create a new version. You + can remove this draft API data set version up until the release + publication. +

+

+ These changes will not be made in the public API until the next + release has been published. +

+ +

+ View changelog and public guidance notes +

+
+ ); + } + + if (finalisingStatus === 'finalised' && draftVersionStatus === 'Failed') { + return ( + + ); + } + return null; +} diff --git a/src/explore-education-statistics-admin/src/pages/release/data/components/__tests__/ApiDataSetFinaliseBanner.test.tsx b/src/explore-education-statistics-admin/src/pages/release/data/components/__tests__/ApiDataSetFinaliseBanner.test.tsx new file mode 100644 index 00000000000..247abfaba34 --- /dev/null +++ b/src/explore-education-statistics-admin/src/pages/release/data/components/__tests__/ApiDataSetFinaliseBanner.test.tsx @@ -0,0 +1,78 @@ +import ApiDataSetFinaliseBanner from '@admin/pages/release/data/components/ApiDataSetFinaliseBanner'; +import render from '@common-test/render'; +import { screen, within } from '@testing-library/react'; +import noop from 'lodash/noop'; +import React from 'react'; +import { MemoryRouter } from 'react-router'; + +describe('ApiDataSetFinaliseBanner', () => { + test('renders the finalising banner when `finalisingStatus` is `finalising', () => { + render( + , + ); + const banner = within(screen.getByTestId('notificationBanner')); + expect( + banner.getByRole('heading', { name: 'Finalising' }), + ).toBeInTheDocument(); + expect( + banner.getByText('Finalising draft API data set version'), + ).toBeInTheDocument(); + }); + + test('renders the action required banner when `draftVersionStatus` is `Mapping', () => { + render( + , + ); + const banner = within(screen.getByTestId('notificationBanner')); + expect( + banner.getByRole('heading', { name: 'Action required' }), + ).toBeInTheDocument(); + expect( + banner.getByText('Draft API data set version is ready to be finalised'), + ).toBeInTheDocument(); + expect( + banner.getByRole('button', { name: 'Finalise this data set version' }), + ).toBeInTheDocument(); + }); + + test('renders the success banner when `draftVersionStatus` is `Failed', () => { + render( + + + , + ); + const banner = within(screen.getByTestId('notificationBanner')); + expect( + banner.getByText('Draft API data set version is ready to be published'), + ).toBeInTheDocument(); + expect( + banner.getByRole('heading', { + name: 'Mappings finalised', + }), + ).toBeInTheDocument(); + }); + + test('renders the error banner when `finalisingStatus` is `finalised` and `draftVersionStatus` is `Draft', () => { + render( + , + ); + const banner = within(screen.getByTestId('notificationBanner')); + expect(banner.getByText('There is a problem')).toBeInTheDocument(); + expect( + banner.getByText('Data set version finalisation failed'), + ).toBeInTheDocument(); + }); +}); diff --git a/src/explore-education-statistics-admin/src/services/apiDataSetVersionService.ts b/src/explore-education-statistics-admin/src/services/apiDataSetVersionService.ts index 0761134e387..06a4d409d6d 100644 --- a/src/explore-education-statistics-admin/src/services/apiDataSetVersionService.ts +++ b/src/explore-education-statistics-admin/src/services/apiDataSetVersionService.ts @@ -103,34 +103,37 @@ const apiDataSetVersionService = { }): Promise { return client.post('/public-data/data-set-versions', data); }, - deleteVersion(versionId: string): Promise { - return client.delete(`/public-data/data-set-versions/${versionId}`); + completeVersion(data: { dataSetVersionId: string }): Promise { + return client.post('/public-data/data-set-versions/complete', data); }, - getFiltersMapping(versionId: string): Promise { + deleteVersion(dataSetVersionId: string): Promise { + return client.delete(`/public-data/data-set-versions/${dataSetVersionId}`); + }, + getFiltersMapping(dataSetVersionId: string): Promise { return client.get( - `/public-data/data-set-versions/${versionId}/mapping/filters`, + `/public-data/data-set-versions/${dataSetVersionId}/mapping/filters`, ); }, updateFilterOptionsMapping( - versionId: string, + dataSetVersionId: string, data: FilterOptionsMappingUpdateRequest, ): Promise { return client.patch( - `/public-data/data-set-versions/${versionId}/mapping/filters/options`, + `/public-data/data-set-versions/${dataSetVersionId}/mapping/filters/options`, data, ); }, - getLocationsMapping(versionId: string): Promise { + getLocationsMapping(dataSetVersionId: string): Promise { return client.get( - `/public-data/data-set-versions/${versionId}/mapping/locations`, + `/public-data/data-set-versions/${dataSetVersionId}/mapping/locations`, ); }, updateLocationsMapping( - versionId: string, + dataSetVersionId: string, data: LocationsMappingUpdateRequest, ): Promise { return client.patch( - `/public-data/data-set-versions/${versionId}/mapping/locations`, + `/public-data/data-set-versions/${dataSetVersionId}/mapping/locations`, data, ); }, diff --git a/src/explore-education-statistics-common/src/components/NotificationBanner.module.scss b/src/explore-education-statistics-common/src/components/NotificationBanner.module.scss new file mode 100644 index 00000000000..5df488dea10 --- /dev/null +++ b/src/explore-education-statistics-common/src/components/NotificationBanner.module.scss @@ -0,0 +1,10 @@ +@import '~govuk-frontend/dist/govuk/base'; + +.fullWidthContent * { + max-width: none; +} + +.error { + background-color: govuk-colour('red'); + border-color: govuk-colour('red'); +} diff --git a/src/explore-education-statistics-common/src/components/NotificationBanner.tsx b/src/explore-education-statistics-common/src/components/NotificationBanner.tsx index c0c030326dd..afc508d2074 100644 --- a/src/explore-education-statistics-common/src/components/NotificationBanner.tsx +++ b/src/explore-education-statistics-common/src/components/NotificationBanner.tsx @@ -1,18 +1,37 @@ +import styles from '@common/components/NotificationBanner.module.scss'; +import classNames from 'classnames'; import React, { ReactNode } from 'react'; interface Props { children?: ReactNode; + fullWidthContent?: boolean; heading?: string; + role?: 'region' | 'alert'; title: string; + variant?: 'error' | 'success'; } -const NotificationBanner = ({ children, heading, title }: Props) => { +const NotificationBanner = ({ + children, + fullWidthContent = false, + heading, + role = 'region', + title, + variant, +}: Props) => { return (