diff --git a/static.config.json b/static.config.json index 37b7d07fc..bd1bffd1c 100644 --- a/static.config.json +++ b/static.config.json @@ -7,8 +7,8 @@ }, "ummVersions": { "ummC": "1.17.3", - "ummS": "1.4", - "ummT": "1.1", + "ummS": "1.5.2", + "ummT": "1.2.0", "ummV": "1.9.0" } } diff --git a/static/src/js/App.jsx b/static/src/js/App.jsx index bfea67e30..d954195dd 100644 --- a/static/src/js/App.jsx +++ b/static/src/js/App.jsx @@ -16,6 +16,7 @@ import DraftsPage from './pages/DraftsPage/DraftsPage' import Notifications from './components/Notifications/Notifications' import Providers from './providers/Providers/Providers' import Page from './components/Page/Page' +import PublishPreview from './components/PublishPreview/PublishPreview' import REDIRECTS from './constants/redirectsMap/redirectsMap' @@ -101,6 +102,7 @@ const App = () => { } /> Not Found :(} /> } /> + } /> diff --git a/static/src/js/components/CustomModal/CustomModal.jsx b/static/src/js/components/CustomModal/CustomModal.jsx new file mode 100644 index 000000000..8acd09018 --- /dev/null +++ b/static/src/js/components/CustomModal/CustomModal.jsx @@ -0,0 +1,72 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import Button from 'react-bootstrap/Button' +import Modal from 'react-bootstrap/Modal' + +/** + * @typedef {Object} CustomModalProps + * @property {Boolean} show Should the modal be open. + * @property {String} message A message to describe the modal + * @property {Object} actions A list of actions for the modal + */ + +/** + * Renders a DeleteModal component + * + * @component + * @example Render a Modal + * return ( + * func + * ]} + * /> + * ) + */ +const CustomModal = ({ + openModal, + message, + actions + +}) => ( + + {message} + + + { + actions.map((item) => { + const { label, variant, onClick } = item + + return ( + + ) + }) + } + + +) + +CustomModal.propTypes = { + actions: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.string.isRequired, + variant: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired + })).isRequired, + openModal: PropTypes.bool.isRequired, + message: PropTypes.string.isRequired +} + +export default CustomModal diff --git a/static/src/js/components/CustomModal/__tests__/CustomModal.test.js b/static/src/js/components/CustomModal/__tests__/CustomModal.test.js new file mode 100644 index 000000000..7687a33e0 --- /dev/null +++ b/static/src/js/components/CustomModal/__tests__/CustomModal.test.js @@ -0,0 +1,46 @@ +import { render, screen } from '@testing-library/react' +import { userEvent } from '@testing-library/user-event' +import React from 'react' +import CustomModal from '../CustomModal' + +const setup = () => { + const onClick = jest.fn() + const props = { + openModal: true, + message: 'Mock message', + actions: + [ + { + label: 'Yes', + variant: 'primary', + onClick + } + ] + } + + render() + + return { + props + } +} + +describe('CustomModal', () => { + test('render a Modal', () => { + setup() + + expect(screen.getByText('Mock message')).toBeInTheDocument() + }) + + describe('when selecting `Yes`', () => { + test('calls onClick', async () => { + const { props } = setup() + + const button = screen.getByText('Yes') + await userEvent.click(button) + + const onClickProp = props.actions.at(0).onClick + expect(onClickProp).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/static/src/js/components/DeleteDraftModal/DeleteDraftModal.jsx b/static/src/js/components/DeleteDraftModal/DeleteDraftModal.jsx deleted file mode 100644 index 5d0b91e99..000000000 --- a/static/src/js/components/DeleteDraftModal/DeleteDraftModal.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import Button from 'react-bootstrap/Button' -import Modal from 'react-bootstrap/Modal' - -/** - * @typedef {Object} DeleteDraftModalProps - * @property {Boolean} show Should the modal be open. - * @property {Function} closeModal A function to close the modal. - * @property {Function} onDelete A callback function triggered when the user selects `Yes`. - */ - -/** - * Renders a DeleteDraftModal component - * - * @component - * @example Render a DeleteDraftModal - * return ( - * - * ) - */ -const DeleteDraftModal = ({ - closeModal, - onDelete, - show -}) => ( - - Are you sure you want to delete this draft? - - - - - - - -) - -DeleteDraftModal.propTypes = { - closeModal: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, - show: PropTypes.bool.isRequired -} - -export default DeleteDraftModal diff --git a/static/src/js/components/DeleteDraftModal/__tests__/DeleteDraftModal.test.js b/static/src/js/components/DeleteDraftModal/__tests__/DeleteDraftModal.test.js deleted file mode 100644 index 62d150ce2..000000000 --- a/static/src/js/components/DeleteDraftModal/__tests__/DeleteDraftModal.test.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' - -import DeleteDraftModal from '../DeleteDraftModal' - -const setup = () => { - const props = { - closeModal: jest.fn(), - onDelete: jest.fn(), - show: true - } - - render() - - return { - props - } -} - -describe('DeleteDraftModal', () => { - test('renders a modal', () => { - setup() - - expect(screen.getByText('Are you sure you want to delete this draft?')).toBeInTheDocument() - }) - - describe('when selecting `No`', () => { - test('calls closeModal', async () => { - const { props } = setup() - - const button = screen.getByText('No') - await userEvent.click(button) - - expect(props.closeModal).toHaveBeenCalledTimes(1) - }) - }) - - describe('when selecting `Yes`', () => { - test('calls onDelete', async () => { - const { props } = setup() - - const button = screen.getByText('Yes') - await userEvent.click(button) - - expect(props.onDelete).toHaveBeenCalledTimes(1) - }) - }) -}) diff --git a/static/src/js/components/DraftPreview/DraftPreview.jsx b/static/src/js/components/DraftPreview/DraftPreview.jsx index e932b6efe..9ba9b5440 100644 --- a/static/src/js/components/DraftPreview/DraftPreview.jsx +++ b/static/src/js/components/DraftPreview/DraftPreview.jsx @@ -7,15 +7,9 @@ import { useNavigate, useParams } from 'react-router' import { useLazyQuery, useMutation } from '@apollo/client' import validator from '@rjsf/validator-ajv8' import { startCase } from 'lodash-es' -import { - CollectionPreview, - ServicePreview, - ToolPreview, - VariablePreview -} from '@edsc/metadata-preview' + import pluralize from 'pluralize' -import DeleteDraftModal from '../DeleteDraftModal/DeleteDraftModal' import ErrorBanner from '../ErrorBanner/ErrorBanner' import LoadingBanner from '../LoadingBanner/LoadingBanner' import Page from '../Page/Page' @@ -35,6 +29,9 @@ import parseError from '../../utils/parseError' import { DELETE_DRAFT } from '../../operations/mutations/deleteDraft' import conceptTypeDraftQueries from '../../constants/conceptTypeDraftQueries' +import usePublishMutation from '../../hooks/usePublishMutation' +import MetadataPreview from '../MetadataPreview/MetadataPreview' +import CustomModal from '../CustomModal/CustomModal' /** * Renders a DraftPreview component @@ -66,6 +63,7 @@ const DraftPreview = () => { const [retries, setRetries] = useState(0) const [deleteDraftMutation] = useMutation(DELETE_DRAFT) + const publishMutation = usePublishMutation() const [getDraft] = useLazyQuery(conceptTypeDraftQueries[derivedConceptType], { variables: { @@ -167,6 +165,12 @@ const DraftPreview = () => { ummMetadata } = draft || {} + // Handle the user selecting publish draft + const handlePublish = () => { + // Calls publish mutation hook + publishMutation(derivedConceptType, nativeId) + } + // Handle the user selecting delete from the delete draft modal const handleDelete = () => { deleteDraftMutation({ @@ -215,51 +219,6 @@ const DraftPreview = () => { // Pull the formSections out of the formConfigurations const formSections = formConfigurations[derivedConceptType] - // Determine which MetadataPreview component to show - const metadataPreviewComponent = () => { - if (derivedConceptType === 'Collection') { - return ( - - ) - } - - if (derivedConceptType === 'Service') { - return ( - - ) - } - - if (derivedConceptType === 'Tool') { - return ( - - ) - } - - if (derivedConceptType === 'Variable') { - return ( - - ) - } - - return null - } - // Accessible event props for the delete link const accessibleEventProps = useAccessibleEvent(() => { setShowDeleteModal(true) @@ -289,8 +248,7 @@ const DraftPreview = () => { className="eui-btn--blue display-modal" onClick={ () => { - // TODO MMT-3411 - console.log('Publish draft') + handlePublish() } } > @@ -310,14 +268,26 @@ const DraftPreview = () => { {startCase(conceptType)} - setShowDeleteModal(false)} - onDelete={handleDelete} + { setShowDeleteModal(false) } + }, + { + label: 'Yes', + variant: 'primary', + onClick: handleDelete + } + ] + } /> - @@ -334,7 +304,11 @@ const DraftPreview = () => { - {metadataPreviewComponent()} + diff --git a/static/src/js/components/DraftPreview/__tests__/DraftPreview.test.js b/static/src/js/components/DraftPreview/__tests__/DraftPreview.test.js index 6a732a8fc..3d0483d7a 100644 --- a/static/src/js/components/DraftPreview/__tests__/DraftPreview.test.js +++ b/static/src/js/components/DraftPreview/__tests__/DraftPreview.test.js @@ -11,7 +11,6 @@ import { Route, Routes } from 'react-router-dom' -import { ToolPreview } from '@edsc/metadata-preview' import * as router from 'react-router' import ummTSchema from '../../../schemas/umm/ummTSchema' @@ -27,8 +26,8 @@ import DraftPreview from '../DraftPreview' import ErrorBanner from '../../ErrorBanner/ErrorBanner' import PreviewProgress from '../../PreviewProgress/PreviewProgress' import Providers from '../../../providers/Providers/Providers' +import { PUBLISH_DRAFT } from '../../../operations/mutations/publishDraft' -jest.mock('@edsc/metadata-preview') jest.mock('../../ErrorBanner/ErrorBanner') jest.mock('../../PreviewProgress/PreviewProgress') jest.mock('../../../utils/errorLogger') @@ -144,23 +143,6 @@ const setup = ({ } describe('DraftPreview', () => { - describe('when the concept type is tool', () => { - test('renders a ToolPreview component', async () => { - setup({}) - - await waitForResponse() - - expect(ToolPreview).toHaveBeenCalledTimes(1) - expect(ToolPreview).toHaveBeenCalledWith({ - conceptId: 'TD1000000-MMT', - conceptType: 'tool-draft', - conceptUrlTemplate: '/{conceptType}/{conceptId}', - isPlugin: true, - tool: mockDraft.previewMetadata - }, {}) - }) - }) - test('renders the breadcrumbs', async () => { setup({}) @@ -572,5 +554,70 @@ describe('DraftPreview', () => { expect(navigateSpy).toHaveBeenCalledTimes(0) }) }) + + describe('when clicking on Publish Draft button with no errors', () => { + test('calls the publish mutation and navigates to the conceptId/revisionId page', async () => { + const navigateSpy = jest.fn() + jest.spyOn(router, 'useNavigate').mockImplementation(() => navigateSpy) + + const { user } = setup({ + additionalMocks: [{ + request: { + query: PUBLISH_DRAFT, + variables: { + draftConceptId: 'TD1000000-MMT', + nativeId: 'MMT_2331e312-cbbc-4e56-9d6f-fe217464be2c', + ummVersion: '1.2.0' + } + }, + result: { + data: { + publishDraft: { + conceptId: 'T1000000-MMT', + revisionId: '2' + } + } + } + }] + }) + + await waitForResponse() + + const button = screen.getByRole('button', { name: 'Publish Tool Draft' }) + await user.click(button) + await waitForResponse() + expect(navigateSpy).toHaveBeenCalledTimes(1) + expect(navigateSpy).toHaveBeenCalledWith('/tools/T1000000-MMT/2') + }) + }) + + describe('when clicking on Publish Draft button with errors', () => { + test('calls the publish mutation and returns errors', async () => { + const navigateSpy = jest.fn() + jest.spyOn(router, 'useNavigate').mockImplementation(() => navigateSpy) + + const { user } = setup({ + additionalMocks: [{ + request: { + query: PUBLISH_DRAFT, + variables: { + draftConceptId: 'TD1000000-MMT', + nativeId: 'MMT_2331e312-cbbc-4e56-9d6f-fe217464be2c', + ummVersion: '1.2.0' + } + }, + error: new Error('#: required key [Name] not found,#: required key [LongName] not found') + }] + }) + + await waitForResponse() + + const button = screen.getByRole('button', { name: 'Publish Tool Draft' }) + await user.click(button) + await waitForResponse() + expect(errorLogger).toHaveBeenCalledTimes(1) + expect(errorLogger).toHaveBeenCalledWith('#: required key [Name] not found,#: required key [LongName] not found', 'PublishMutation: publishMutation') + }) + }) }) }) diff --git a/static/src/js/components/MetadataForm/MetadataForm.jsx b/static/src/js/components/MetadataForm/MetadataForm.jsx index 26b0c145d..dd889ca56 100644 --- a/static/src/js/components/MetadataForm/MetadataForm.jsx +++ b/static/src/js/components/MetadataForm/MetadataForm.jsx @@ -51,6 +51,7 @@ import getUiSchema from '../../utils/getUiSchema' import './MetadataForm.scss' import removeEmpty from '../../utils/removeEmpty' +import usePublishMutation from '../../hooks/usePublishMutation' const MetadataForm = () => { const { @@ -79,6 +80,8 @@ const MetadataForm = () => { derivedConceptType = urlValueTypeToConceptTypeMap[draftType] } + const publishMutation = usePublishMutation() + useEffect(() => { if (conceptId === 'new') { setDraft({}) @@ -247,8 +250,8 @@ const MetadataForm = () => { } if (type === saveTypes.saveAndPublish) { - // Save and then publish - // TODO MMT-3411 + // Calls publish mutation + publishMutation(derivedConceptType, nativeId) } }, onError: (ingestError) => { diff --git a/static/src/js/components/MetadataForm/__tests__/MetadataForm.test.js b/static/src/js/components/MetadataForm/__tests__/MetadataForm.test.js index eaa77b1cc..b655e4d54 100644 --- a/static/src/js/components/MetadataForm/__tests__/MetadataForm.test.js +++ b/static/src/js/components/MetadataForm/__tests__/MetadataForm.test.js @@ -45,6 +45,7 @@ import toolsConfiguration from '../../../schemas/uiForms/toolsConfiguration' import { INGEST_DRAFT } from '../../../operations/mutations/ingestDraft' import OneOfField from '../../OneOfField/OneOfField' +import { PUBLISH_DRAFT } from '../../../operations/mutations/publishDraft' jest.mock('@rjsf/core', () => jest.fn(({ onChange, @@ -594,6 +595,76 @@ describe('MetadataForm', () => { }) }) + describe('when the saveType is save and preview', () => { + test('it should save the draft and call publish', async () => { + const navigateSpy = jest.fn() + jest.spyOn(router, 'useNavigate').mockImplementation(() => navigateSpy) + + const { user } = setup({ + additionalMocks: [{ + request: { + query: INGEST_DRAFT, + variables: { + conceptType: 'Tool', + metadata: { + LongName: 'Long Name', + MetadataSpecification: { + URL: 'https://cdn.earthdata.nasa.gov/umm/tool/v1.1', + Name: 'UMM-T', + Version: '1.1' + } + }, + nativeId: 'MMT_2331e312-cbbc-4e56-9d6f-fe217464be2c', + providerId: 'MMT_2', + ummVersion: '1.0.0' + } + }, + result: { + data: { + ingestDraft: { + conceptId: 'TD1000000-MMT', + revisionId: '1' + } + } + } + }, + { + request: { + query: PUBLISH_DRAFT, + variables: { + draftConceptId: 'TD1000000-MMT', + nativeId: 'MMT_2331e312-cbbc-4e56-9d6f-fe217464be2c', + ummVersion: '1.2.0' + } + }, + result: { + data: { + publishDraft: { + conceptId: 'TL1000000-MMT', + revisionId: '1' + } + } + } + } + ] + }) + + await waitForResponse() + + const dropdown = screen.getByRole('button', { name: 'Save Options' }) + + await user.click(dropdown) + + const button = screen.getByRole('button', { name: 'Save & Publish' }) + await user.click(button) + + await waitForResponse() + + expect(navigateSpy).toHaveBeenCalledTimes(1) + expect(navigateSpy).toHaveBeenCalledWith('/tools/TL1000000-MMT/1') + }) + }) + describe('when the ingest mutation returns an error', () => { test('calls errorLogger', async () => { const navigateSpy = jest.fn() diff --git a/static/src/js/components/MetadataPreview/MetadataPreview.jsx b/static/src/js/components/MetadataPreview/MetadataPreview.jsx new file mode 100644 index 000000000..c38b14f81 --- /dev/null +++ b/static/src/js/components/MetadataPreview/MetadataPreview.jsx @@ -0,0 +1,81 @@ +import React from 'react' +import { + CollectionPreview, + ServicePreview, + ToolPreview, + VariablePreview +} from '@edsc/metadata-preview' +import PropTypes from 'prop-types' +import toLowerKebabCase from '../../utils/toLowerKebabCase' + +/** + * MetadataPreview + * @typedef {Object} MetadataPreview + * @property {Object} previewMetadata An object with the metadata + * @property {string} conceptId A conceptId of the record + * @property {string} conceptType A conceptType of the record + */ +/** + * Renders a Metadata based on a given conceptType + * + * @component + * @example Render a MetadataPreview + * return ( + * + * ) + */ +const MetadataPreview = ({ + previewMetadata, + conceptId, + conceptType +}) => { + if (conceptType === 'Collection') { + return ( + + ) + } + + if (conceptType === 'Service') { + return ( + + ) + } + + if (conceptType === 'Tool') { + return ( + + ) + } + + if (conceptType === 'Variable') { + return ( + + ) + } + + return null +} + +MetadataPreview.propTypes = { + previewMetadata: PropTypes.shape({}).isRequired, + conceptId: PropTypes.string.isRequired, + conceptType: PropTypes.string.isRequired +} + +export default MetadataPreview diff --git a/static/src/js/components/MetadataPreview/__tests__/MetadataPreivew.test.js b/static/src/js/components/MetadataPreview/__tests__/MetadataPreivew.test.js new file mode 100644 index 000000000..617d6d3b0 --- /dev/null +++ b/static/src/js/components/MetadataPreview/__tests__/MetadataPreivew.test.js @@ -0,0 +1,146 @@ +import { + CollectionPreview, + ServicePreview, + ToolPreview, + VariablePreview +} from '@edsc/metadata-preview' +import { render } from '@testing-library/react' +import { userEvent } from '@testing-library/user-event' +import React from 'react' +import MetadataPreview from '../MetadataPreview' + +jest.mock('@edsc/metadata-preview') + +const setup = (overrideProps = {}) => { + const props = { + previewMetadata: {}, + conceptId: '', + conceptType: '', + ...overrideProps + } + render( + + ) + + return { + props, + user: userEvent.setup() + } +} + +describe('MetadataPreview', () => { + describe('when the conceptType is Tool draft', () => { + test(' renders a ToolPreview component', async () => { + setup({ + previewMetadata: { + name: 'mock tool test' + }, + conceptId: 'TD000000-MMT', + conceptType: 'Tool' + }) + + await waitForResponse() + expect(ToolPreview).toHaveBeenCalledTimes(1) + expect(ToolPreview).toHaveBeenCalledWith({ + conceptId: 'TD000000-MMT', + conceptType: 'tool', + conceptUrlTemplate: '/{conceptType}/{conceptId}', + isPlugin: true, + tool: { + name: 'mock tool test' + } + }, {}) + }) + }) + + describe('when the conceptType is Service draft', () => { + test('renders a Service Preview component', async () => { + setup({ + previewMetadata: { + name: 'mock service test' + }, + conceptId: 'SD000000-MMT', + conceptType: 'Service' + }) + + await waitForResponse() + expect(ServicePreview).toHaveBeenCalledTimes(1) + expect(ServicePreview).toHaveBeenCalledWith({ + conceptId: 'SD000000-MMT', + conceptType: 'service', + conceptUrlTemplate: '/{conceptType}/{conceptId}', + isPlugin: true, + service: { + name: 'mock service test' + } + }, {}) + }) + }) + + describe('when the conceptType is Variable draft', () => { + test('renders a Variable Preview component', async () => { + setup({ + previewMetadata: { + name: 'mock variable test' + }, + conceptId: 'VD000000-MMT', + conceptType: 'Variable' + }) + + await waitForResponse() + expect(VariablePreview).toHaveBeenCalledTimes(1) + expect(VariablePreview).toHaveBeenCalledWith({ + conceptId: 'VD000000-MMT', + conceptType: 'variable', + conceptUrlTemplate: '/{conceptType}/{conceptId}', + isPlugin: true, + variable: { + name: 'mock variable test' + } + }, {}) + }) + }) + + describe('when the conceptType is Collection draft', () => { + test('renders a Collection Preview component', async () => { + setup({ + previewMetadata: { + name: 'mock collection test' + }, + conceptId: 'CD000000-MMT', + conceptType: 'Collection' + }) + + await waitForResponse() + expect(CollectionPreview).toHaveBeenCalledTimes(1) + expect(CollectionPreview).toHaveBeenCalledWith({ + cmrHost: 'https://cmr.earthdata.nasa.gov', + conceptId: 'CD000000-MMT', + conceptType: 'collection', + conceptUrlTemplate: '/{conceptType}/{conceptId}', + isPlugin: true, + collection: { + name: 'mock collection test' + }, + token: null + }, {}) + }) + }) + + describe('when the conceptType is not valid', () => { + test('renders a Collection Preview component', async () => { + setup({ + previewMetadata: { + name: 'mock collection test' + }, + conceptId: 'CD000000-MMT', + conceptType: 'bad concept type' + }) + + expect(CollectionPreview).not.toHaveBeenCalledTimes(1) + expect(ServicePreview).not.toHaveBeenCalledTimes(1) + expect(VariablePreview).not.toHaveBeenCalledTimes(1) + expect(ToolPreview).not.toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/static/src/js/components/Page/__tests__/Page.test.js b/static/src/js/components/Page/__tests__/Page.test.js index 36b6ffae5..aae006197 100644 --- a/static/src/js/components/Page/__tests__/Page.test.js +++ b/static/src/js/components/Page/__tests__/Page.test.js @@ -36,12 +36,12 @@ describe('Page component', () => { { to: '/manage/services', title: 'Manage Services', - version: 'v1.4' + version: 'v1.5.2' }, { to: '/manage/tools', title: 'Manage Tools', - version: 'v1.1' + version: 'v1.2.0' }, { to: '/manage/cmr', diff --git a/static/src/js/components/PublishPreview/PublishPreview.jsx b/static/src/js/components/PublishPreview/PublishPreview.jsx new file mode 100644 index 000000000..37b9e68d8 --- /dev/null +++ b/static/src/js/components/PublishPreview/PublishPreview.jsx @@ -0,0 +1,211 @@ +import { useLazyQuery, useMutation } from '@apollo/client' +import pluralize from 'pluralize' +import React, { useState, useEffect } from 'react' +import { + Button, + Col, + Row +} from 'react-bootstrap' +import { useNavigate, useParams } from 'react-router' +import conceptTypeQueries from '../../constants/conceptTypeQueries' +import deleteMutationTypes from '../../constants/deleteMutationTypes' +import useNotificationsContext from '../../hooks/useNotificationsContext' +import errorLogger from '../../utils/errorLogger' +import getConceptTypeByConceptId from '../../utils/getConceptTypeByConcept' +import parseError from '../../utils/parseError' +import toLowerKebabCase from '../../utils/toLowerKebabCase' +import CustomModal from '../CustomModal/CustomModal' +import ErrorBanner from '../ErrorBanner/ErrorBanner' +import LoadingBanner from '../LoadingBanner/LoadingBanner' +import MetadataPreview from '../MetadataPreview/MetadataPreview' +import Page from '../Page/Page' + +/** + * Renders a PublishPreview component + * + * @component + * @example Render a PublishPreview + * return ( + * + * ) + */ +const PublishPreview = () => { + const { + conceptId, + revisionId + } = useParams() + + const navigate = useNavigate() + + const [previewMetadata, setPreviewMetadata] = useState() + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [error, setError] = useState() + const [retries, setRetries] = useState(0) + const [loading, setLoading] = useState(true) + const [nativeId, setNativeId] = useState() + const [providerId, setProviderId] = useState() + + const { addNotification } = useNotificationsContext() + + const derivedConceptType = getConceptTypeByConceptId(conceptId) + + const [deleteMutation] = useMutation(deleteMutationTypes[derivedConceptType]) + + // Calls CMR-Graphql to get the record + const [getMetadata] = useLazyQuery(conceptTypeQueries[derivedConceptType], { + variables: { + params: { + conceptId + } + }, + onCompleted: (getData) => { + const fetchedMetadata = getData[toLowerKebabCase(derivedConceptType)] + const { + revisionId: savedRevisionId, + nativeId: savedNativeId, + providerId: savedProviderId + } = fetchedMetadata || {} + + if (!fetchedMetadata || (savedRevisionId && revisionId !== savedRevisionId)) { + // If fetchedMetadata or the correct revision id does't exist in CMR, then call getMetadata again. + setRetries(retries + 1) + setPreviewMetadata() + } else { + // The correct version of the metadata has been fetched + setPreviewMetadata(fetchedMetadata) + setNativeId(savedNativeId) + setProviderId(savedProviderId) + setLoading(false) + } + }, + onError: (getDraftError) => { + setError(getDraftError) + setLoading(false) + // Send the error to the errorLogger + errorLogger(getDraftError, 'PublishPreview getPublish Query') + } + }) + + // Calls getMetadata and checks if the revision id matches the revision saved. + useEffect(() => { + if (!previewMetadata && retries < 10) { + setLoading(true) + getMetadata() + } + + if (retries >= 10) { + setLoading(false) + errorLogger('Max retries allowed', 'Publish Preview: getMetadata Query') + setError('Published record could not be loaded.') + } + }, [previewMetadata, retries]) + + // Handles the user selecting delete from the delete model + const handleDelete = () => { + deleteMutation({ + variables: { + nativeId, + providerId + }, + onCompleted: () => { + // Add a success notification + addNotification({ + message: `${conceptId} deleted successfully`, + variant: 'success' + }) + + // Hide the modal + setShowDeleteModal(false) + + // Navigate to the manage page + navigate(`/manage/${pluralize(derivedConceptType).toLowerCase()}`) + }, + onError: (deleteError) => { + // Add an error notification + addNotification({ + message: `Error deleting ${conceptId}`, + variant: 'danger' + }) + + // Send the error to the errorLogger + errorLogger(deleteError, 'PublishPreview: deleteMutation') + + // Hide the modal + setShowDeleteModal(false) + } + }) + } + + if (error) { + const message = parseError(error) + + return ( + + + + ) + } + + if (loading) { + return ( + + + + ) + } + + return ( + + + + + + { setShowDeleteModal(false) } + }, + { + label: 'Yes', + variant: 'primary', + onClick: handleDelete + } + ] + } + /> + + + + + + + + + + ) +} + +export default PublishPreview diff --git a/static/src/js/components/PublishPreview/__tests__/PublishPreview.test.js b/static/src/js/components/PublishPreview/__tests__/PublishPreview.test.js new file mode 100644 index 000000000..9bca15e40 --- /dev/null +++ b/static/src/js/components/PublishPreview/__tests__/PublishPreview.test.js @@ -0,0 +1,371 @@ +import { MockedProvider } from '@apollo/client/testing' +import { render, screen } from '@testing-library/react' +import { userEvent } from '@testing-library/user-event' +import React from 'react' +import { + MemoryRouter, + Route, + Routes +} from 'react-router' +import * as router from 'react-router' +import conceptTypeQueries from '../../../constants/conceptTypeQueries' +import Providers from '../../../providers/Providers/Providers' +import MetadataPreview from '../../MetadataPreview/MetadataPreview' +import PublishPreview from '../PublishPreview' +import errorLogger from '../../../utils/errorLogger' +import ErrorBanner from '../../ErrorBanner/ErrorBanner' +import { GET_TOOL } from '../../../operations/queries/getTool' +import { DELETE_TOOL } from '../../../operations/mutations/deleteTool' + +jest.mock('../../MetadataPreview/MetadataPreview') +jest.mock('../../ErrorBanner/ErrorBanner') +jest.mock('../../../utils/errorLogger') + +const mockedUsedNavigate = jest.fn() + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockedUsedNavigate +})) + +const mock = { + accessConstraints: null, + ancillaryKeywords: null, + associationDetails: null, + conceptId: 'TL1200000180-MMT_2', + contactGroups: null, + contactPersons: null, + description: 'asfd', + doi: null, + nativeId: 'MMT_PUBLISH_8b8a1965-67a5-415c-ae4c-8ecbafd84131', + lastUpdatedDate: null, + longName: 'test', + metadataSpecification: { + url: 'https://cdn.earthdata.nasa.gov/umm/tool/v1.2.0', + name: 'UMM-T', + version: '1.2.0' + }, + name: 'Testing tools for publiush asdfasfd', + organizations: [ + { + roles: [ + 'SERVICE PROVIDER' + ], + shortName: 'ESA/ED', + longName: 'Educational Office, Ecological Society of America', + urlValue: 'http://www.esa.org/education/' + } + ], + providerId: 'MMT_2', + potentialAction: null, + quality: null, + revisionId: '1', + revisionDate: '2024-01-19T20:00:56.974Z', + relatedUrls: null, + searchAction: null, + supportedBrowsers: null, + supportedInputFormats: null, + supportedOperatingSystems: null, + supportedOutputFormats: null, + supportedSoftwareLanguages: null, + toolKeywords: [ + { + toolCategory: 'EARTH SCIENCE SERVICES', + toolTopic: 'DATA ANALYSIS AND VISUALIZATION', + toolTerm: 'CALIBRATION/VALIDATION' + } + ], + type: 'Web User Interface', + url: { + urlContentType: 'DistributionURL', + type: 'DOWNLOAD SOFTWARE', + subtype: 'MOBILE APP', + urlValue: 'adfs' + }, + useConstraints: null, + version: '1', + versionDescription: null, + __typename: 'Tool' + +} + +const setup = ({ + additionalMocks = [], + overrideMocks = false +}) => { + const mocks = [{ + request: { + query: conceptTypeQueries.Tool, + variables: { + params: { + conceptId: 'T1000000-MMT' + } + } + }, + result: { + data: { + tool: mock + } + } + }, ...additionalMocks] + + render( + + + + + + } + /> + + + + + + ) + + return { + user: userEvent.setup() + } +} + +describe('PublishPreview', () => { + describe('when the publish page is called', () => { + test('renders a Publish Preview with a Published Tool Preview', async () => { + setup({}) + await waitForResponse() + + expect(MetadataPreview).toHaveBeenCalledTimes(1) + expect(MetadataPreview).toHaveBeenCalledWith({ + conceptId: 'T1000000-MMT', + conceptType: 'Tool', + previewMetadata: { + __typename: 'Tool', + accessConstraints: null, + ancillaryKeywords: null, + associationDetails: null, + conceptId: 'TL1200000180-MMT_2', + contactGroups: null, + contactPersons: null, + description: 'asfd', + doi: null, + lastUpdatedDate: null, + longName: 'test', + metadataSpecification: { + name: 'UMM-T', + url: 'https://cdn.earthdata.nasa.gov/umm/tool/v1.2.0', + version: '1.2.0' + }, + name: 'Testing tools for publiush asdfasfd', + nativeId: 'MMT_PUBLISH_8b8a1965-67a5-415c-ae4c-8ecbafd84131', + organizations: [ + { + longName: 'Educational Office, Ecological Society of America', + roles: [ + 'SERVICE PROVIDER' + ], + shortName: 'ESA/ED', + urlValue: 'http://www.esa.org/education/' + } + ], + potentialAction: null, + providerId: 'MMT_2', + quality: null, + relatedUrls: null, + revisionDate: '2024-01-19T20:00:56.974Z', + revisionId: '1', + searchAction: null, + supportedBrowsers: null, + supportedInputFormats: null, + supportedOperatingSystems: null, + supportedOutputFormats: null, + supportedSoftwareLanguages: null, + toolKeywords: [ + { + toolCategory: 'EARTH SCIENCE SERVICES', + toolTerm: 'CALIBRATION/VALIDATION', + toolTopic: 'DATA ANALYSIS AND VISUALIZATION' + } + ], + type: 'Web User Interface', + url: { + subtype: 'MOBILE APP', + type: 'DOWNLOAD SOFTWARE', + urlContentType: 'DistributionURL', + urlValue: 'adfs' + }, + useConstraints: null, + version: '1', + versionDescription: null + } + }, {}) + }) + }) + + describe('when the request results in an error', () => { + test('call errorLogger and renders an ErrorBanner', async () => { + setup({ + overrideMocks: [ + { + request: { + query: GET_TOOL, + variables: { + params: { + conceptId: 'T1000000-MMT' + } + } + }, + error: new Error('An error occurred') + } + ] + }) + + await waitForResponse() + + expect(errorLogger).toHaveBeenCalledTimes(1) + expect(errorLogger).toHaveBeenCalledWith(new Error('An error occurred'), 'PublishPreview getPublish Query') + + expect(ErrorBanner).toHaveBeenCalledTimes(1) + }) + }) + + describe('when the record could not be returned after 10 try', () => { + test('calls errorLogger and renders an ErrorBanner', async () => { + setup({ + overrideMocks: [ + { + request: { + query: conceptTypeQueries.Tool, + variables: { + params: { + conceptId: 'T1000000-MMT' + } + } + }, + result: { + data: { + tool: null + } + } + } + ] + }) + + await waitForResponse() + expect(errorLogger).toHaveBeenCalledTimes(1) + expect(errorLogger).toHaveBeenCalledWith('Max retries allowed', 'Publish Preview: getMetadata Query') + }) + }) + + describe('when clicking the delete button', () => { + test('show the DeleteModal', async () => { + const { user } = setup({}) + + await waitForResponse() + + const button = screen.getByRole('button', { name: 'Delete Tool Record' }) + await user.click(button) + + expect(screen.getByText('Are you sure you want to delete this record?')).toBeInTheDocument() + }) + }) + + describe('when clicking the Yes button in the modal', () => { + test('calls the deletePublish mutation and navigates to the /tools page', async () => { + const navigateSpy = jest.fn() + jest.spyOn(router, 'useNavigate').mockImplementation(() => navigateSpy) + + const { user } = setup({ + additionalMocks: [{ + request: { + query: DELETE_TOOL, + variables: { + nativeId: 'MMT_PUBLISH_8b8a1965-67a5-415c-ae4c-8ecbafd84131', + providerId: 'MMT_2' + } + }, + result: { + data: { + deleteTool: { + conceptId: 'T1000000-MMT', + revisionId: '1' + } + } + } + }] + }) + + await waitForResponse() + + const button = screen.getByRole('button', { name: 'Delete Tool Record' }) + await user.click(button) + + const yesButton = screen.getByRole('button', { name: 'Yes' }) + await user.click(yesButton) + + await waitForResponse() + + expect(navigateSpy).toHaveBeenCalledTimes(1) + expect(navigateSpy).toHaveBeenCalledWith('/manage/tools') + }) + }) + + describe('when clicking the Yes button in the modal results in an error', () => { + test('calls addNotification and errorLogger', async () => { + const navigateSpy = jest.fn() + jest.spyOn(router, 'useNavigate').mockImplementation(() => navigateSpy) + + const { user } = setup({ + additionalMocks: [{ + request: { + query: DELETE_TOOL, + variables: { + nativeId: 'MMT_PUBLISH_8b8a1965-67a5-415c-ae4c-8ecbafd84131', + providerId: 'MMT_2' + } + }, + error: new Error('An error occurred') + }] + }) + + await waitForResponse() + + const button = screen.getByRole('button', { name: 'Delete Tool Record' }) + await user.click(button) + + const yesButton = screen.getByRole('button', { name: 'Yes' }) + await user.click(yesButton) + + await waitForResponse() + await waitForResponse() + + expect(errorLogger).toHaveBeenCalledTimes(1) + expect(errorLogger).toHaveBeenCalledWith(new Error('An error occurred'), 'PublishPreview: deleteMutation') + }) + }) + + describe('when clicking the No Button in the modal', () => { + test('calls the the deleteModal and clicks no', async () => { + const navigateSpy = jest.fn() + jest.spyOn(router, 'useNavigate').mockImplementation(() => navigateSpy) + const { user } = setup({}) + + await waitForResponse() + + const button = screen.getByRole('button', { name: 'Delete Tool Record' }) + + await user.click(button) + + const noButton = screen.getByRole('button', { name: 'No' }) + await user.click(noButton) + + expect(navigateSpy).toHaveBeenCalledTimes(0) + }) + }) +}) diff --git a/static/src/js/constants/conceptIdTypes.js b/static/src/js/constants/conceptIdTypes.js new file mode 100644 index 000000000..2ab9fc224 --- /dev/null +++ b/static/src/js/constants/conceptIdTypes.js @@ -0,0 +1,11 @@ +/** + * Mapping of concept id prefixes to concept types + */ +const conceptIdTypes = { + C: 'Collection', + S: 'Service', + T: 'Tool', + V: 'Variable' +} + +export default conceptIdTypes diff --git a/static/src/js/constants/conceptTypeQueries.js b/static/src/js/constants/conceptTypeQueries.js new file mode 100644 index 000000000..ee56c3a1e --- /dev/null +++ b/static/src/js/constants/conceptTypeQueries.js @@ -0,0 +1,13 @@ +import { GET_COLLECTION } from '../operations/queries/getCollection' +import { GET_SERVICE } from '../operations/queries/getService' +import { GET_TOOL } from '../operations/queries/getTool' +import { GET_VARIABLE } from '../operations/queries/getVariable' + +const conceptTypeQueries = { + Collection: GET_COLLECTION, + Service: GET_SERVICE, + Tool: GET_TOOL, + Variable: GET_VARIABLE +} + +export default conceptTypeQueries diff --git a/static/src/js/constants/deleteMutationTypes.js b/static/src/js/constants/deleteMutationTypes.js new file mode 100644 index 000000000..b145174d9 --- /dev/null +++ b/static/src/js/constants/deleteMutationTypes.js @@ -0,0 +1,13 @@ +import { DELETE_COLLECTION } from '../operations/mutations/deleteCollection' +import { DELETE_SERVICE } from '../operations/mutations/deleteService' +import { DELETE_TOOL } from '../operations/mutations/deleteTool' +import { DELETE_VARIABLE } from '../operations/mutations/deleteVariable' + +const deleteMutationTypes = { + Collection: DELETE_COLLECTION, + Service: DELETE_SERVICE, + Tool: DELETE_TOOL, + Variable: DELETE_VARIABLE +} + +export default deleteMutationTypes diff --git a/static/src/js/hooks/usePublishMutation.js b/static/src/js/hooks/usePublishMutation.js new file mode 100644 index 000000000..2251540c0 --- /dev/null +++ b/static/src/js/hooks/usePublishMutation.js @@ -0,0 +1,54 @@ +import { useMutation } from '@apollo/client' +import pluralize from 'pluralize' +import { useNavigate, useParams } from 'react-router' +import { PUBLISH_DRAFT } from '../operations/mutations/publishDraft' +import errorLogger from '../utils/errorLogger' +import getUmmVersion from '../utils/getUmmVersion' +import useNotificationsContext from './useNotificationsContext' + +const usePublishMutation = () => { + const [publishDraftMutation] = useMutation(PUBLISH_DRAFT) + const { conceptId } = useParams() + + const navigate = useNavigate() + const { addNotification } = useNotificationsContext() + + const publishMutation = (conceptType, nativeId) => { + publishDraftMutation({ + variables: { + draftConceptId: conceptId, + nativeId, + ummVersion: getUmmVersion(conceptType) + }, + onCompleted: (getPublishedData) => { + const { publishDraft } = getPublishedData + const { conceptId: publishConceptId, revisionId } = publishDraft + + // Add a success notification + addNotification({ + message: `${publishConceptId} Published`, + variant: 'success' + }) + + navigate(`/${pluralize(conceptType).toLowerCase()}/${publishConceptId}/${revisionId}`) + }, + onError: (getPublishError) => { + const { message } = getPublishError + const parseErr = message.split(',') + // TODO: Trevor said when he has time he will look into how to display this. This is just temporary + parseErr.map((err) => ( + addNotification({ + message: err, + variant: 'danger' + }) + )) + + errorLogger(message, 'PublishMutation: publishMutation') + } + }) + } + + return publishMutation +} + +export default usePublishMutation diff --git a/static/src/js/operations/mutations/deleteCollection.js b/static/src/js/operations/mutations/deleteCollection.js new file mode 100644 index 000000000..9b101f44f --- /dev/null +++ b/static/src/js/operations/mutations/deleteCollection.js @@ -0,0 +1,22 @@ +import { gql } from '@apollo/client' + +export const DELETE_COLLECTION = gql` + mutation DeleteCollection ( + $providerId: String! + $nativeId: String! + ) { + deleteCollection ( + providerId: $providerId + nativeId: $nativeId + ) { + conceptId + revisionId + } + } +` + +// Example Variables: +// { +// "conceptId": "VC1200000096-MMT_2",", +// "providerId": "MMT_2" +// } diff --git a/static/src/js/operations/mutations/deleteService.js b/static/src/js/operations/mutations/deleteService.js new file mode 100644 index 000000000..d3b4d758c --- /dev/null +++ b/static/src/js/operations/mutations/deleteService.js @@ -0,0 +1,22 @@ +import { gql } from '@apollo/client' + +export const DELETE_SERVICE = gql` + mutation DeleteService ( + $providerId: String! + $nativeId: String! + ) { + deleteService ( + providerId: $providerId + nativeId: $nativeId + ) { + conceptId + revisionId + } + } +` + +// Example Variables: +// { +// "conceptId": "S1200000096-MMT_2",", +// "providerId": "MMT_2" +// } diff --git a/static/src/js/operations/mutations/deleteTool.js b/static/src/js/operations/mutations/deleteTool.js new file mode 100644 index 000000000..ead43fb08 --- /dev/null +++ b/static/src/js/operations/mutations/deleteTool.js @@ -0,0 +1,22 @@ +import { gql } from '@apollo/client' + +export const DELETE_TOOL = gql` + mutation DeleteTool ( + $providerId: String! + $nativeId: String! + ) { + deleteTool ( + providerId: $providerId + nativeId: $nativeId + ) { + conceptId + revisionId + } + } +` + +// Example Variables: +// { +// "nativeId": "T1200000096-MMT_2",", +// "providerId": "MMT_2" +// } diff --git a/static/src/js/operations/mutations/deleteVariable.js b/static/src/js/operations/mutations/deleteVariable.js new file mode 100644 index 000000000..b7b6878e0 --- /dev/null +++ b/static/src/js/operations/mutations/deleteVariable.js @@ -0,0 +1,22 @@ +import { gql } from '@apollo/client' + +export const DELETE_VARIABLE = gql` + mutation DeleteVariable ( + $providerId: String! + $nativeId: String! + ) { + deleteVariable ( + providerId: $providerId + nativeId: $nativeId + ) { + conceptId + revisionId + } + } +` + +// Example Variables: +// { +// "conceptId": "V1200000096-MMT_2",", +// "providerId": "MMT_2" +// } diff --git a/static/src/js/operations/mutations/publishDraft.js b/static/src/js/operations/mutations/publishDraft.js new file mode 100644 index 000000000..f8d27872c --- /dev/null +++ b/static/src/js/operations/mutations/publishDraft.js @@ -0,0 +1,24 @@ +import { gql } from '@apollo/client' + +export const PUBLISH_DRAFT = gql` + mutation PublishDraft ( + $draftConceptId: String! + $nativeId: String! + $ummVersion: String! + ) { + publishDraft ( + draftConceptId: $draftConceptId + nativeId: $nativeId + ummVersion: $ummVersion + ) { + conceptId + revisionId + } + } +` +// Example Variables: +// { +// "draftConceptId": "TD1200000-MMT_2", +// "nativeId": "publish native id", +// "ummVersion": "1.0.0" +// } diff --git a/static/src/js/operations/queries/getService.js b/static/src/js/operations/queries/getService.js new file mode 100644 index 000000000..e9d471fe8 --- /dev/null +++ b/static/src/js/operations/queries/getService.js @@ -0,0 +1,44 @@ +import { gql } from '@apollo/client' + +export const GET_SERVICE = gql` + query Service($params: ServiceInput) { + service(params: $params) { + accessConstraints + ancillaryKeywords + associationDetails + conceptId + contactGroups + contactPersons + description + lastUpdatedDate + longName + maxItemsPerOrder + name + nativeId + operationMetadata + providerId + revisionDate + revisionId + relatedUrls + serviceKeywords + serviceOptions + serviceOrganizations + supportedInputProjections + supportedOutputProjections + supportedReformattings + serviceQuality + type + url + useConstraints + version + versionDescription + } + } +` + +// Example Variables: +// { +// "params": { +// "conceptId": "S1200000096-MMT_2", +// } +// } diff --git a/static/src/js/operations/queries/getTool.js b/static/src/js/operations/queries/getTool.js new file mode 100644 index 000000000..0371cf4db --- /dev/null +++ b/static/src/js/operations/queries/getTool.js @@ -0,0 +1,47 @@ +import { gql } from '@apollo/client' + +export const GET_TOOL = gql` + query Tool($params: ToolInput){ + tool(params: $params) { + accessConstraints + ancillaryKeywords + associationDetails + conceptId + contactGroups + contactPersons + description + doi + nativeId + lastUpdatedDate + longName + metadataSpecification + name + organizations + providerId + potentialAction + quality + revisionId + revisionDate + relatedUrls + searchAction + supportedBrowsers + supportedInputFormats + supportedOperatingSystems + supportedOutputFormats + supportedSoftwareLanguages + toolKeywords + type + url + useConstraints + version + versionDescription + } +} +` + +// Example Variables: +// { +// "params": { +// "conceptId": "T1200000096-MMT_2", +// } +// } diff --git a/static/src/js/operations/queries/getVariable.js b/static/src/js/operations/queries/getVariable.js new file mode 100644 index 000000000..b9ff983fb --- /dev/null +++ b/static/src/js/operations/queries/getVariable.js @@ -0,0 +1,42 @@ +import { gql } from '@apollo/client' + +export const GET_VARIABLE = gql` + query Variable($params: VariableInput) { + variable(params: $params) { + additionalIdentifiers + associationDetails + conceptId + dataType + definition + dimensions + fillValues + indexRanges + instanceInformation + longName + measurementIdentifiers + name + nativeId + offset + providerId + relatedUrls + revisionDate + revisionId + samplingIdentifiers + scale + scienceKeywords + sets + standardName + units + validRanges + variableSubType + variableType + } + } +` + +// Example Variables: +// { +// "params": { +// "conceptId": "V1200000096-MMT_2", +// } +// } diff --git a/static/src/js/utils/__tests__/getConceptTypeByConcept.test.js b/static/src/js/utils/__tests__/getConceptTypeByConcept.test.js new file mode 100644 index 000000000..d1e9afd7e --- /dev/null +++ b/static/src/js/utils/__tests__/getConceptTypeByConcept.test.js @@ -0,0 +1,35 @@ +import getConceptTypeByConceptId from '../getConceptTypeByConcept' + +describe('getConceptTypeByConcept', () => { + describe('when concept id starts with T', () => { + test('returns tool type', () => { + expect(getConceptTypeByConceptId('T12345')).toEqual('Tool') + }) + }) + + describe('when concept id starts with V', () => { + test('returns tool type', () => { + expect(getConceptTypeByConceptId('V12345')).toEqual('Variable') + }) + }) + + describe('when concept id starts with C', () => { + test('returns tool type', () => { + expect(getConceptTypeByConceptId('C12345')).toEqual('Collection') + }) + }) + + describe('when concept id starts with S', () => { + test('returns tool type', () => { + expect(getConceptTypeByConceptId('S12345')).toEqual('Service') + }) + }) + + describe('when concept id does not start with known constants', () => { + test('returns tool type', () => { + expect(getConceptTypeByConceptId('xx12345')).toBe(undefined) + expect(getConceptTypeByConceptId('x')).toBe(undefined) + expect(getConceptTypeByConceptId(null)).toBe(undefined) + }) + }) +}) diff --git a/static/src/js/utils/__tests__/getConfig.test.js b/static/src/js/utils/__tests__/getConfig.test.js index a10eb1a07..c56b441ab 100644 --- a/static/src/js/utils/__tests__/getConfig.test.js +++ b/static/src/js/utils/__tests__/getConfig.test.js @@ -20,8 +20,8 @@ describe('getConfig', () => { test('returns a valid json object for ummVersionConfig', () => { const expectedUmmVersionConfig = { ummC: '1.17.3', - ummS: '1.4', - ummT: '1.1', + ummS: '1.5.2', + ummT: '1.2.0', ummV: '1.9.0' } diff --git a/static/src/js/utils/__tests__/getUmmVersion.test.js b/static/src/js/utils/__tests__/getUmmVersion.test.js new file mode 100644 index 000000000..1af7c1f90 --- /dev/null +++ b/static/src/js/utils/__tests__/getUmmVersion.test.js @@ -0,0 +1,35 @@ +import { getUmmVersionsConfig } from '../getConfig' +import getUmmVersion from '../getUmmVersion' + +const ummVersion = getUmmVersionsConfig() +describe('getUmmVersion', () => { + describe('when the concept type is Tool', () => { + test('returns correct UMM-T version', () => { + expect(getUmmVersion('Tool')).toEqual(ummVersion.ummT) + }) + }) + + describe('when the concept type is Service', () => { + test('returns correct UMM-S version', () => { + expect(getUmmVersion('Service')).toEqual(ummVersion.ummS) + }) + }) + + describe('when the concept type is Variable', () => { + test('returns correct UMM-V version', () => { + expect(getUmmVersion('Variable')).toEqual(ummVersion.ummV) + }) + }) + + describe('when the concept type is Collection', () => { + test('returns correct UMM-C version', () => { + expect(getUmmVersion('Collection')).toEqual(ummVersion.ummC) + }) + }) + + describe('when the concept type is Collection', () => { + test('returns correct UMM-C version', () => { + expect(getUmmVersion('bad-name')).toEqual(null) + }) + }) +}) diff --git a/static/src/js/utils/getConceptTypeByConcept.js b/static/src/js/utils/getConceptTypeByConcept.js new file mode 100644 index 000000000..bcdc3ee58 --- /dev/null +++ b/static/src/js/utils/getConceptTypeByConcept.js @@ -0,0 +1,15 @@ +import conceptIdTypes from '../constants/conceptIdTypes' + +/** + * Find the concept type based on the provided conceptId + * @param {String} conceptId concept ID to determine the concept type + */ +const getConceptTypeByConceptId = (conceptId) => { + if (!conceptId) return undefined + + const prefix = conceptId.substring(0, 1) + + return conceptIdTypes[prefix] +} + +export default getConceptTypeByConceptId diff --git a/static/src/js/utils/getUmmVersion.js b/static/src/js/utils/getUmmVersion.js new file mode 100644 index 000000000..ef4f25e8b --- /dev/null +++ b/static/src/js/utils/getUmmVersion.js @@ -0,0 +1,24 @@ +import { getUmmVersionsConfig } from './getConfig' + +/** + * Find the umm version based on the provided concept type + * @param {String} conceptId concept type to determine the concept type + */ +const getUmmVersion = (conceptType) => { + const ummVersion = getUmmVersionsConfig() + + switch (conceptType) { + case 'Collection': + return ummVersion.ummC + case 'Service': + return ummVersion.ummS + case 'Tool': + return ummVersion.ummT + case 'Variable': + return ummVersion.ummV + default: + return null + } +} + +export default getUmmVersion