diff --git a/static/src/js/components/CollectionAssociationForm/CollectionAssociationForm.jsx b/static/src/js/components/CollectionAssociationForm/CollectionAssociationForm.jsx
index 516a541a0..2cbafbba0 100644
--- a/static/src/js/components/CollectionAssociationForm/CollectionAssociationForm.jsx
+++ b/static/src/js/components/CollectionAssociationForm/CollectionAssociationForm.jsx
@@ -1,12 +1,10 @@
import Form from '@rjsf/core'
import pluralize from 'pluralize'
-
import React, {
useCallback,
useEffect,
useState
} from 'react'
-import PropTypes from 'prop-types'
import validator from '@rjsf/validator-ajv8'
import camelcaseKeys from 'camelcase-keys'
import {
@@ -25,10 +23,10 @@ import moment from 'moment'
import Alert from 'react-bootstrap/Alert'
import Col from 'react-bootstrap/Col'
-import Placeholder from 'react-bootstrap/Placeholder'
import Row from 'react-bootstrap/Row'
import Button from '@/js/components/Button/Button'
+import ControlledPaginatedContent from '@/js/components/ControlledPaginatedContent/ControlledPaginatedContent'
import CustomDateTimeWidget from '@/js/components/CustomDateTimeWidget/CustomDateTimeWidget'
import CustomFieldTemplate from '@/js/components/CustomFieldTemplate/CustomFieldTemplate'
import CustomSelectWidget from '@/js/components/CustomSelectWidget/CustomSelectWidget'
@@ -37,9 +35,7 @@ import CustomTitleField from '@/js/components/CustomTitleField/CustomTitleField'
import EllipsisLink from '@/js/components/EllipsisLink/EllipsisLink'
import EllipsisText from '@/js/components/EllipsisText/EllipsisText'
import GridLayout from '@/js/components/GridLayout/GridLayout'
-import LoadingBanner from '@/js/components/LoadingBanner/LoadingBanner'
import OneOfField from '@/js/components/OneOfField/OneOfField'
-import Pagination from '@/js/components/Pagination/Pagination'
import Table from '@/js/components/Table/Table'
import collectionAssociation from '@/js/schemas/collectionAssociation'
@@ -66,37 +62,33 @@ import conceptTypeQueries from '@/js/constants/conceptTypeQueries'
*
* )
*/
-const CollectionAssociationForm = ({ metadata }) => {
+const CollectionAssociationForm = () => {
const { conceptId } = useParams()
const navigate = useNavigate()
const { addNotification } = useNotificationsContext()
+ const derivedConceptType = getConceptTypeByConceptId(conceptId)
+
+ // Form state variables
const [searchFormData, setSearchFormData] = useState({})
+ const [isAssociatingCollections, setIsAssociatingCollections] = useState(false)
const [focusField, setFocusField] = useState(null)
- const [searchParams, setSearchParams] = useSearchParams()
- const [collectionLoading, setCollectionLoading] = useState()
- const [showSelectCollection, setShowSelectCollection] = useState(false)
- const [collectionSearchResult, setCollectionSearchResult] = useState({})
- const [collectionConceptIds, setCollectionConceptIds] = useState([])
- const [loading, setLoading] = useState(true)
- const [fetchedDraft, setFetchedDraft] = useState()
- useEffect(() => {
- setFetchedDraft(metadata)
- setLoading(false)
- setCollectionLoading(false)
- }, [metadata])
-
- const derivedConceptType = getConceptTypeByConceptId(conceptId)
+ // Get Collection variables
+ const [searchParams, setSearchParams] = useSearchParams()
+ const [collectionSearchResult, setCollectionSearchResult] = useState(null)
+ const sortKeyParam = searchParams.get('sortKey')
+ // Pagination variables
+ const [activePage, setActivePage] = useState(1)
const limit = 20
- const activePage = parseInt(searchParams.get('page'), 10) || 1
- const sortKeyParam = searchParams.get('sortKey')
- const page = searchParams.get('page')
const offset = (activePage - 1) * limit
+ // Checkbox variables
+ const [collectionConceptIds, setCollectionConceptIds] = useState([])
+
const fields = {
OneOfField,
TitleField: CustomTitleField,
@@ -128,8 +120,9 @@ const CollectionAssociationForm = ({ metadata }) => {
params: {
limit: 2000
}
- }
- }, { skip: derivedConceptType !== conceptIdTypes.O })
+ },
+ skip: derivedConceptType !== conceptIdTypes.O
+ })
if (derivedConceptType === conceptIdTypes.O) {
const { required } = schema
@@ -157,23 +150,38 @@ const CollectionAssociationForm = ({ metadata }) => {
// Validate ummMetadata
const { errors: validationErrors } = validator.validateFormData(searchFormData, schema)
- // Query to retrieve collections
- const [getCollections] = useLazyQuery(GET_COLLECTIONS, {
- onCompleted: (getCollectionsData) => {
- setCollectionSearchResult(getCollectionsData.collections)
- setCollectionLoading(false)
+ // Query to retrieve collections after 'Search for Collection' button click
+ const [
+ getCollections,
+ {
+ loading: collectionLoading,
+ data: collectionData
+ }] = useLazyQuery(GET_COLLECTIONS, {
+ onCompleted: () => {
+ setCollectionSearchResult(collectionData.collections)
},
onError: () => {
- setCollectionLoading(false)
errorLogger('Unable to get Collections', 'Collection Association: getCollections Query')
setCollectionSearchResult(null)
}
})
- const collectionSearch = () => {
- setCollectionLoading(true)
- setShowSelectCollection(true)
+ // Query to retrieve concept after 'Search for Collection' button click
+ const [
+ getConcept,
+ {
+ loading: conceptLoading,
+ data: conceptData
+ }
+ ] = useLazyQuery(conceptTypeQueries[derivedConceptType], {
+ fetchPolicy: 'network-only',
+ onError: () => {
+ errorLogger('Unable to get previous record', 'Collection Association: getConcept Query')
+ }
+ })
+
+ const collectionSearch = () => {
const searchField = searchParams.get('searchField')
const searchFieldValue = searchParams.get('searchFieldValue')
const provider = searchParams.get('provider')
@@ -193,6 +201,16 @@ const CollectionAssociationForm = ({ metadata }) => {
})
}
+ const setPage = (nextPage) => {
+ setActivePage(nextPage)
+ }
+
+ useEffect(() => {
+ if (collectionSearchResult) {
+ collectionSearch()
+ }
+ }, [activePage])
+
const handleCollectionSearch = () => {
const formattedFormData = camelcaseKeys(searchFormData, { deep: true })
const { searchField } = formattedFormData
@@ -215,17 +233,17 @@ const CollectionAssociationForm = ({ metadata }) => {
return Object.fromEntries(currentParams)
})
+ getConcept({
+ variables: {
+ params: {
+ conceptId
+ }
+ }
+ })
+
collectionSearch()
}
- // Calls handleCollectionSearch get the next set of collections for pagination
- useEffect(() => {
- if (page) {
- collectionSearch()
- }
- }, [page])
-
- // Handles checkbox selection. If selected, then adds the value to collectionConceptIds state variable
const handleCheckbox = (event) => {
const { target } = event
@@ -242,6 +260,7 @@ const CollectionAssociationForm = ({ metadata }) => {
// Handles selected collection association button by calling CREATE_ASSOCIATION mutation
const handleAssociateSelectedCollection = () => {
+ setIsAssociatingCollections(true)
let variables = {
conceptId,
associatedConceptIds: collectionConceptIds
@@ -264,7 +283,7 @@ const CollectionAssociationForm = ({ metadata }) => {
createAssociationMutation({
variables,
onCompleted: () => {
- setLoading(true)
+ setIsAssociatingCollections(false)
if (derivedConceptType === conceptIdTypes.O) {
navigate(`/order-options/${conceptId}`)
} else {
@@ -277,7 +296,7 @@ const CollectionAssociationForm = ({ metadata }) => {
})
},
onError: () => {
- setLoading(false)
+ setIsAssociatingCollections(false)
errorLogger('Unable to create association', 'Collection Association Form: createAssociationForm')
addNotification({
message: 'Error updating association',
@@ -310,7 +329,8 @@ const CollectionAssociationForm = ({ metadata }) => {
let checked = null
const { conceptId: collectionConceptId } = rowData
- const { collections = {} } = fetchedDraft
+ const fetchedConceptData = conceptData[camelCase(derivedConceptType)]
+ const { collections = {} } = fetchedConceptData
const { items } = collections
// Checks if collection is already associated to the record.
@@ -339,30 +359,22 @@ const CollectionAssociationForm = ({ metadata }) => {
)
})
- useEffect(() => {
- if (sortKeyParam) {
- collectionSearch()
- }
- }, [sortKeyParam])
-
const sortFn = useCallback((key, order) => {
let nextSortKey
- searchParams.set('sortKey', nextSortKey)
-
setSearchParams((currentParams) => {
if (order === 'ascending') nextSortKey = `-${key}`
if (order === 'descending') nextSortKey = key
- // Reset the page parameter
- currentParams.delete('page')
-
// Set the sort key
currentParams.set('sortKey', nextSortKey)
return Object.fromEntries(currentParams)
})
- }, [])
+
+ setActivePage(1) // Reset to first page when sorting
+ collectionSearch() // Trigger a new search
+ }, [setSearchParams, collectionSearch])
const collectionColumns = [
{
@@ -389,7 +401,7 @@ const CollectionAssociationForm = ({ metadata }) => {
className: 'col-auto',
dataAccessorFn: buildEllipsisTextCell,
align: 'center',
- sortFn
+ sortFn: (_, order) => sortFn('provider', order)
},
{
dataKey: 'version',
@@ -400,32 +412,36 @@ const CollectionAssociationForm = ({ metadata }) => {
]
- const setPage = (nextPage) => {
- setSearchParams((currentParams) => {
- currentParams.set('page', nextPage)
-
- return Object.fromEntries(currentParams)
- })
- }
-
- if (loading) {
- return (
-
- )
- }
-
const { items = [], count } = collectionSearchResult || {}
- const totalPages = Math.ceil(count / limit)
-
- const currentPageIndex = Math.floor(offset / limit)
- const firstResultIndex = currentPageIndex * limit
- const isLastPage = totalPages === activePage
- const lastResultIndex = firstResultIndex + (isLastPage ? count % limit : limit)
+ const renderTableContent = () => {
+ if (collectionSearchResult) {
+ return (
+
+
+ {' '}
+
+ Disabled rows in the results below represent collections that are
+ already associated with this record.
+
+
`column_${dataKey}_${conceptIdCell}`}
+ generateRowKey={({ conceptId: conceptIdRow }) => `row_${conceptIdRow}`}
+ limit={20}
+ noDataMessage="No Collections Found."
+ sortKey={sortKeyParam}
+ />
+
+ )
+ }
- const paginationMessage = count > 0
- ? `Showing Collections ${totalPages > 1 ? `${firstResultIndex + 1}-${lastResultIndex} of` : ''} ${count}`
- : 'No matching Collections found'
+ return null
+ }
return (
<>
@@ -446,13 +462,10 @@ const CollectionAssociationForm = ({ metadata }) => {
}
}
>
-
-
+
0}
+ disabled={validationErrors.length > 0 || collectionLoading || conceptLoading}
onClick={handleCollectionSearch}
variant="primary"
>
@@ -460,87 +473,75 @@ const CollectionAssociationForm = ({ metadata }) => {
-
- {
- showSelectCollection
- && (
- <>
-
-
- {
- !count && collectionLoading && (
-
- )
- }
- {
- (!!count || (!collectionLoading && !count)) && (
- {paginationMessage}
- )
- }
-
- {
- totalPages > 1 && (
-
-
-
- )
- }
-
- {
- (!collectionLoading && items.length > 0) && (
-
- {' '}
-
- Disabled rows in the results below represent collections that are
- already associated with this record.
-
+
+
+
+ {
+ ({
+ totalPages,
+ pagination,
+ firstResultPosition,
+ lastResultPosition
+ }) => {
+ const paginationMessage = `Showing ${totalPages > 1 ? `${firstResultPosition}-${lastResultPosition} of` : ''} ${count} ${pluralize('Collection', count)}`
+
+ return (
+ <>
+
+
+ {
+ (!!count) && (
+ {paginationMessage}
+ )
+ }
+
+
+ {
+ totalPages > 1 && (
+
+ {pagination}
+
+ )
+ }
+
+ {renderTableContent()}
+ {
+ totalPages > 1 && (
+
+
+
+ {pagination}
+
+
+
+ )
+ }
+ >
)
}
- `column_${dataKey}_${conceptIdCell}`}
- generateRowKey={({ conceptId: conceptIdRow }) => `row_${conceptIdRow}`}
- noDataMessage="No Collections Found."
- limit={limit}
- offset={offset}
- sortKey={sortKeyParam}
- />
- 0}
- onClick={handleAssociateSelectedCollection}
- variant="primary"
- >
- Associate Selected Collections
-
- >
- )
- }
-
+ }
+
+
+
+ {
+ collectionSearchResult ? (
+ 0 || isAssociatingCollections}
+ onClick={handleAssociateSelectedCollection}
+ variant="primary"
+ >
+ Associate Selected Collections
+
+ ) : null
+ }
>
)
}
-CollectionAssociationForm.defaultProps = {
- metadata: {}
-}
-
-CollectionAssociationForm.propTypes = {
- metadata: PropTypes.shape({})
-}
-
export default CollectionAssociationForm
diff --git a/static/src/js/components/CollectionAssociationForm/__tests__/CollectionAssociationForm.test.jsx b/static/src/js/components/CollectionAssociationForm/__tests__/CollectionAssociationForm.test.jsx
index fc3f78739..88cb8bcfe 100644
--- a/static/src/js/components/CollectionAssociationForm/__tests__/CollectionAssociationForm.test.jsx
+++ b/static/src/js/components/CollectionAssociationForm/__tests__/CollectionAssociationForm.test.jsx
@@ -1,53 +1,65 @@
+import React, { Suspense } from 'react'
import {
render,
screen,
+ waitFor,
within
} from '@testing-library/react'
-import React from 'react'
+
import {
MemoryRouter,
- Route,
- Routes
-} from 'react-router'
+ Routes,
+ Route
+} from 'react-router-dom'
import * as router from 'react-router'
import { MockedProvider } from '@apollo/client/testing'
import userEvent from '@testing-library/user-event'
import moment from 'moment'
+import { GET_COLLECTIONS } from '@/js/operations/queries/getCollections'
+
+import { DATE_FORMAT } from '@/js/constants/dateFormat'
+
import AppContext from '@/js/context/AppContext'
import NotificationsContext from '@/js/context/NotificationsContext'
-import { GET_COLLECTIONS } from '@/js/operations/queries/getCollections'
-
import errorLogger from '@/js/utils/errorLogger'
-import { DATE_FORMAT } from '@/js/constants/dateFormat'
+import ErrorBoundary from '@/js/components/ErrorBoundary/ErrorBoundary'
import CollectionAssociationForm from '../CollectionAssociationForm'
import {
- CollectionAssociationRequest,
- CollectionResultsWithPages,
createAssociationErrorRequest,
createAssociationRequest,
createAssociationWithServiceRequest,
- mockTool,
- mockVariable,
- mockToolWithAssociation,
- mockOrderOption,
- CollectionSortRequest,
- GetServicesRequest
+ getCollectionsMock,
+ getCollectionsMockPage1,
+ getCollectionsMockPage2,
+ getCollectionSortRequestByProvider,
+ getCollectionSortRequestByShortName,
+ getServices,
+ getToolMock,
+ getToolMockWithError,
+ mockNoCollections,
+ mockOrderOption
} from './__mocks__/CollectionAssociationResults'
-vi.mock('@/js/components/ErrorBanner/ErrorBanner')
-vi.mock('@/js/utils/errorLogger')
+vi.mock('../../../utils/errorLogger')
const setup = ({
additionalMocks = [],
overrideInitialEntries,
overridePath,
- overrideMock
+ overrideMocks
}) => {
+ const mocks = [
+ getToolMock,
+ getCollectionsMock,
+ getToolMock,
+ ...additionalMocks
+ ]
+
const notificationContext = {
addNotification: vi.fn()
}
@@ -63,20 +75,33 @@ const setup = ({
}
>
-
-
+
+
}
+ element={
+ (
+
+
+
+
+
+ )
+ }
/>
}
+ element={
+ (
+
+
+
+
+
+ )
+ }
/>
-
@@ -89,13 +114,10 @@ const setup = ({
}
}
-describe('CollectionAssociationForm', () => {
- describe('when searching for collection', () => {
- test('renders a list of collection', async () => {
- const { user } = setup({
- additionalMocks: [CollectionAssociationRequest],
- overrideMock: mockToolWithAssociation
- })
+describe('CollectionAssociationForm component', () => {
+ describe('when the component mounts', () => {
+ test('it should render the search form', async () => {
+ const { user } = setup({})
const searchField = await screen.findByText('Select Search Field')
@@ -107,37 +129,44 @@ describe('CollectionAssociationForm', () => {
const field = screen.getByRole('textbox')
await user.type(field, '*')
- const searchForCollections = screen.getByText('Search for Collection')
- await user.click(searchForCollections)
+ const searchButton = await screen.findByText('Search for Collection')
+ expect(searchButton).toBeEnabled()
+ await user.click(searchButton)
- const firstCheckbox = screen.getAllByRole('checkbox')[0]
- const secondCheckbox = screen.getAllByRole('checkbox')[1]
+ await waitFor(() => {
+ expect(screen.getByText('Collection Associations Entry Title 1')).toBeInTheDocument()
+ })
- await user.click(secondCheckbox)
- await user.click(firstCheckbox)
- await user.click(secondCheckbox)
+ const checkboxes = screen.getAllByRole('checkbox')
+ await user.click(checkboxes[1])
+ await user.click(checkboxes[0])
+ await user.click(checkboxes[1])
expect(errorLogger).toHaveBeenCalledTimes(0)
})
test('when there is an error to get the list of collections', async () => {
const { user } = setup({
- additionalMocks: [{
- request: {
- query: GET_COLLECTIONS,
- variables: {
- params: {
- limit: 20,
- offset: 0,
- provider: null,
- sortKey: null,
- options: { entryTitle: { pattern: true } },
- entryTitle: '*'
+ overrideMocks: [
+ getToolMock,
+ {
+ request: {
+ query: GET_COLLECTIONS,
+ variables: {
+ params: {
+ limit: 20,
+ offset: 0,
+ provider: null,
+ sortKey: null,
+ options: { entryTitle: { pattern: true } },
+ entryTitle: '*'
+ }
}
- }
+ },
+ error: new Error('An error occurred')
},
- error: new Error('An error occurred')
- }]
+ getToolMock
+ ]
})
const searchField = await screen.findByText('Select Search Field')
@@ -156,92 +185,143 @@ describe('CollectionAssociationForm', () => {
expect(errorLogger).toHaveBeenCalledWith('Unable to get Collections', 'Collection Association: getCollections Query')
})
- describe('when searching for temporal extent', () => {
- test('show fill out temporal extent form', async () => {
- const { user } = setup({
- additionalMocks: [
- {
- request: {
- query: GET_COLLECTIONS,
- variables: {
- params: {
- limit: 20,
- offset: 0,
- provider: null,
- sortKey: null,
- temporal: '1978-01-01T00:00:00.000,1978-01-01T00:00:00.000'
- }
+ test('when there is an error to get the concept', async () => {
+ const { user } = setup({
+ overrideMocks: [getToolMockWithError]
+ })
+
+ const searchField = await screen.findByText('Select Search Field')
+ await user.click(searchField)
+
+ const selectField = screen.getByText('Entry Title')
+ await user.click(selectField)
+
+ const field = screen.getByRole('textbox')
+ await user.type(field, '*')
+
+ const searchForCollections = screen.getByText('Search for Collection')
+ await user.click(searchForCollections)
+
+ expect(errorLogger).toHaveBeenCalledTimes(2)
+ expect(errorLogger).toHaveBeenCalledWith('Unable to get Collections', 'Collection Association: getCollections Query')
+ })
+
+ test('when there are no collections found', async () => {
+ const { user } = setup({
+ overrideMocks: [
+ getToolMock,
+ mockNoCollections,
+ getToolMock
+ ]
+ })
+
+ const searchField = await screen.findByText('Select Search Field')
+ await user.click(searchField)
+
+ const selectField = screen.getByText('Entry Title')
+ await user.click(selectField)
+
+ const field = screen.getByRole('textbox')
+ await user.type(field, '*')
+
+ const searchForCollections = screen.getByText('Search for Collection')
+ await user.click(searchForCollections)
+
+ await screen.findByText('No Collections Found.')
+ })
+ })
+
+ describe('when searching for temporal extent', () => {
+ test('show fill out temporal extent form', async () => {
+ const { user } = setup({
+ overrideMocks: [
+ getToolMock,
+ {
+ request: {
+ query: GET_COLLECTIONS,
+ variables: {
+ params: {
+ limit: 20,
+ offset: 0,
+ provider: null,
+ sortKey: null,
+ temporal: '1978-01-01T00:00:00.000,1978-01-01T00:00:00.000'
}
- },
- result: {
- data: {
- collections: {
- items: [
- {
- conceptId: 'C12000001123-MMT_2',
- provider: 'MMT_2',
- version: '1',
- revisionId: 1,
- tags: 1,
- granules: null,
- entryTitle: 'Collection Association Entry Title 1',
- shortName: 'Collection Associations Short Name 1',
- title: 'Collection Associations Title 1',
- revisionDate: null,
- tagDefinitions: {
- items: [{
- conceptId: 'C100000',
- description: 'Mock tag description',
- originatorId: 'test.user',
- revisionId: '1',
- tagKey: 'Mock tag key'
- }]
- },
- __typename: 'Collection'
- }
- ],
- count: 25,
- __typename: 'CollectionList'
- }
+ }
+ },
+ result: {
+ data: {
+ collections: {
+ items: [
+ {
+ conceptId: 'C12000001123-MMT_2',
+ provider: 'MMT_2',
+ version: '1',
+ revisionId: 1,
+ tags: 1,
+ granules: null,
+ entryTitle: 'Collection Association Entry Title 1',
+ shortName: 'Collection Associations Short Name 1',
+ title: 'Collection Associations Title 1',
+ revisionDate: null,
+ tagDefinitions: {
+ items: [{
+ conceptId: 'C100000',
+ description: 'Mock tag description',
+ originatorId: 'test.user',
+ revisionId: '1',
+ tagKey: 'Mock tag key'
+ }]
+ },
+ __typename: 'Collection'
+ }
+ ],
+ count: 25,
+ __typename: 'CollectionList'
}
}
}
- ]
- })
+ },
+ getToolMock
+ ]
+ })
- const searchField = await screen.findByText('Select Search Field')
- await user.click(searchField)
+ const searchField = await screen.findByText('Select Search Field')
+ await user.click(searchField)
- const selectField = screen.getByText('Temporal Extent')
- await user.click(selectField)
+ const selectField = screen.getByText('Temporal Extent')
+ await user.click(selectField)
- const startField = screen.getByText('Range Start')
- await user.type(startField, moment.utc('1978-01-01T00:00:00Z').format(DATE_FORMAT))
+ const startField = screen.getByText('Range Start')
+ await user.type(startField, moment.utc('1978-01-01T00:00:00Z').format(DATE_FORMAT))
- const endField = screen.getByText('Range End')
- await user.type(endField, moment.utc('1978-01-01T00:00:00Z').format(DATE_FORMAT))
+ const endField = screen.getByText('Range End')
+ await user.type(endField, moment.utc('1978-01-01T00:00:00Z').format(DATE_FORMAT))
- const searchForCollections = screen.getByText('Search for Collection')
- await user.click(searchForCollections)
+ const searchForCollections = screen.getByText('Search for Collection')
+ await user.click(searchForCollections)
- const table = await screen.findByRole('table')
+ const table = await screen.findByRole('table')
- const tableRows = within(table).getAllByRole('row')
+ const tableRows = within(table).getAllByRole('row')
- expect(tableRows.length).toEqual(2)
+ expect(tableRows.length).toEqual(2)
- const row1 = tableRows[1]
- const row1Cells = within(row1).queryAllByRole('cell')
+ const row1 = tableRows[1]
+ const row1Cells = within(row1).queryAllByRole('cell')
- expect(row1Cells[1].textContent).toBe('Collection Association Entry Title 1')
- })
+ expect(row1Cells[1].textContent).toBe('Collection Association Entry Title 1')
})
})
describe('when paging through the table', () => {
test('navigate to the next page', async () => {
const { user } = setup({
- additionalMocks: [CollectionAssociationRequest, CollectionResultsWithPages]
+ overrideMocks: [
+ getToolMock,
+ getCollectionsMockPage1,
+ getToolMock,
+ getCollectionsMockPage2]
})
const searchField = await screen.findByText('Select Search Field')
@@ -256,10 +336,15 @@ describe('CollectionAssociationForm', () => {
const searchForCollections = screen.getByText('Search for Collection')
await user.click(searchForCollections)
- const paginationButton = screen.getByRole('button', { name: 'Goto Page 3' })
- await user.click(paginationButton)
+ // Wait for the initial results to load
+ await screen.findByText('Showing 1-20 of 50 Collections')
- expect(await screen.findByText('Collection Associations Short Name 1')).toBeInTheDocument()
+ // Find the "Next" button in the new pagination structure
+ const nextPageButtons = screen.getAllByRole('button', { name: 'Goto Next Page' })
+ await user.click(nextPageButtons[0])
+
+ // Check if the page has changed by looking for a specific element on the new page
+ expect(await screen.findByText('Showing 21-40 of 50 Collections')).toBeInTheDocument()
})
})
@@ -269,7 +354,12 @@ describe('CollectionAssociationForm', () => {
vi.spyOn(router, 'useNavigate').mockImplementation(() => navigateSpy)
const { user } = setup({
- additionalMocks: [CollectionAssociationRequest, createAssociationRequest]
+ overrideMocks: [
+ getToolMock,
+ getCollectionsMock,
+ getToolMock,
+ createAssociationRequest
+ ]
})
const searchField = await screen.findByText('Select Search Field')
@@ -285,6 +375,8 @@ describe('CollectionAssociationForm', () => {
const searchForCollections = screen.getByText('Search for Collection')
await user.click(searchForCollections)
+ await screen.findByText('Unassociated Collection Entry Title 1')
+
const firstCheckbox = screen.getAllByRole('checkbox')[1]
await user.click(firstCheckbox)
@@ -294,7 +386,8 @@ describe('CollectionAssociationForm', () => {
await user.click(createSelectedAssociationButton)
expect(navigateSpy).toHaveBeenCalledTimes(2)
- expect(navigateSpy).toHaveBeenCalledWith('/tools/T12000000-MMT_2/collection-association')
+ expect(navigateSpy).toHaveBeenNthCalledWith(1, '?searchField=entryTitle&searchFieldValue=*', undefined)
+ expect(navigateSpy).toHaveBeenNthCalledWith(2, '/tools/T1200000098-MMT_2/collection-association')
})
})
@@ -305,7 +398,11 @@ describe('CollectionAssociationForm', () => {
const { user } = setup({
overrideInitialEntries: ['/order-options/OO1257381321-EDF_OPS/collection-association-search'],
- additionalMocks: [CollectionAssociationRequest, createAssociationWithServiceRequest]
+ overrideMocks: [
+ getServices,
+ mockOrderOption,
+ getCollectionsMockPage1,
+ createAssociationWithServiceRequest]
})
const serviceField = await screen.findByText('Select Service')
@@ -341,7 +438,12 @@ describe('CollectionAssociationForm', () => {
describe('when the create association request fails', () => {
test('should call errorLogger and display error message', async () => {
const { user } = setup({
- additionalMocks: [CollectionAssociationRequest, createAssociationErrorRequest]
+ overrideMocks: [
+ getToolMock,
+ getCollectionsMock,
+ getToolMock,
+ createAssociationErrorRequest
+ ]
})
const searchField = await screen.findByText('Select Search Field')
@@ -357,7 +459,9 @@ describe('CollectionAssociationForm', () => {
const searchForCollections = screen.getByText('Search for Collection')
await user.click(searchForCollections)
- const firstCheckbox = screen.getAllByRole('checkbox')[0]
+ await screen.findByText('Unassociated Collection Entry Title 1')
+
+ const firstCheckbox = screen.getAllByRole('checkbox')[1]
await user.click(firstCheckbox)
await user.click(firstCheckbox)
@@ -375,7 +479,7 @@ describe('CollectionAssociationForm', () => {
describe('when clicking an ascending sort button', () => {
test('should sorts and shows the button as active', async () => {
const { user } = setup({
- additionalMocks: [CollectionAssociationRequest]
+ additionalMocks: [getCollectionSortRequestByShortName, getCollectionSortRequestByProvider]
})
const searchField = await screen.findByText('Select Search Field')
@@ -393,23 +497,10 @@ describe('CollectionAssociationForm', () => {
const sortShortName = screen.getByRole('button', { name: /Sort Short Name in ascending order/ })
await user.click(sortShortName)
expect(screen.queryByRole('button', { name: /Sort Short Name in ascending order/ })).toHaveClass('d-flex align-items-center text-nowrap button--naked table__sort-button text-secondary d-flex justify-content-center btn')
- })
- })
-
- describe('when the URL searchParam', () => {
- test('should search for collection', async () => {
- const navigateSpy = vi.fn()
- vi.spyOn(router, 'useNavigate').mockImplementation(() => navigateSpy)
-
- setup({
- additionalMocks: [CollectionSortRequest],
- overrideMock: { mockVariable },
- overridePath: 'variables/:conceptId/collection-association-search',
- overrideInitialEntries: ['/variables/V12000000-MMT_2/collection-association-search?searchField=entryTitle&searchFieldValue=*&sortKey=-shortName']
- })
- const sortButton = await screen.findByRole('button', { name: /Sort Short Name in ascending order/ })
- expect(sortButton).toHaveClass('d-flex align-items-center text-nowrap button--naked table__sort-button text-secondary d-flex justify-content-center btn')
+ const sortProvider = screen.getByRole('button', { name: /Sort Provider in descending order/ })
+ await user.click(sortProvider)
+ expect(screen.queryByRole('button', { name: /Sort Provider in descending order/ })).toHaveClass('d-flex align-items-center text-nowrap button--naked table__sort-button text-secondary d-flex justify-content-center btn')
})
})
})
diff --git a/static/src/js/components/CollectionAssociationForm/__tests__/__mocks__/CollectionAssociationResults.js b/static/src/js/components/CollectionAssociationForm/__tests__/__mocks__/CollectionAssociationResults.js
index aaf11c650..f516037f4 100644
--- a/static/src/js/components/CollectionAssociationForm/__tests__/__mocks__/CollectionAssociationResults.js
+++ b/static/src/js/components/CollectionAssociationForm/__tests__/__mocks__/CollectionAssociationResults.js
@@ -1,141 +1,31 @@
+import { CREATE_ASSOCIATION } from '@/js/operations/mutations/createAssociation'
+import { GET_COLLECTIONS } from '@/js/operations/queries/getCollections'
+import { GET_ORDER_OPTION } from '@/js/operations/queries/getOrderOption'
import { GET_SERVICES } from '@/js/operations/queries/getServices'
-import { CREATE_ASSOCIATION } from '../../../../operations/mutations/createAssociation'
-import { GET_COLLECTIONS } from '../../../../operations/queries/getCollections'
+import { GET_TOOL } from '@/js/operations/queries/getTool'
-export const mockToolWithAssociation = {
- accessConstraints: null,
- ancillaryKeywords: null,
- associationDetails: {
- collections: [
- {
- conceptId: 'C12000001123-MMT_2'
- },
- {
- conceptId: 'C1200000034-SEDAC'
+export const mockOrderOption = {
+ request: {
+ query: GET_ORDER_OPTION,
+ variables: {
+ params: {
+ conceptId: 'OO1257381321-EDF_OPS'
}
- ]
- },
- conceptId: 'T1200000098-MMT_2',
- contactGroups: null,
- contactPersons: null,
- description: 'mock description',
- doi: null,
- nativeId: 'MMT_e090f57a-d611-48eb-a5d2-c6a94073f3f9',
- lastUpdatedDate: null,
- longName: 'mock long name',
- metadataSpecification: {
- url: 'https://cdn.earthdata.nasa.gov/umm/tool/v1.2.0',
- name: 'UMM-T',
- version: '1.2.0'
- },
- name: 'Collection Association Mock Test',
- organizations: [
- {
- roles: [
- 'PUBLISHER'
- ],
- shortName: 'UCAR/NCAR/EOL/CEOPDM',
- longName: 'CEOP Data Management, Earth Observing Laboratory, National Center for Atmospheric Research, University Corporation for Atmospheric Research',
- urlValue: 'http://www.eol.ucar.edu/projects/ceop/dm/'
- }
- ],
- providerId: 'MMT_2',
- potentialAction: null,
- quality: null,
- revisionId: '1',
- revisionDate: '2024-03-21T15:01:58.533Z',
- 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: 'Downloadable Tool',
- ummMetadata: {
- URL: {
- URLContentType: 'DistributionURL',
- Type: 'GOTO WEB TOOL',
- URLValue: 'mock url'
- },
- Type: 'Downloadable Tool',
- Description: 'mock description',
- Version: '1',
- ToolKeywords: [
- {
- ToolCategory: 'EARTH SCIENCE SERVICES',
- ToolTopic: 'DATA ANALYSIS AND VISUALIZATION',
- ToolTerm: 'CALIBRATION/VALIDATION'
- }
- ],
- Name: 'Collection Association Mock Test',
- Organizations: [
- {
- Roles: [
- 'PUBLISHER'
- ],
- ShortName: 'UCAR/NCAR/EOL/CEOPDM',
- LongName: 'CEOP Data Management, Earth Observing Laboratory, National Center for Atmospheric Research, University Corporation for Atmospheric Research',
- URLValue: 'http://www.eol.ucar.edu/projects/ceop/dm/'
- }
- ],
- MetadataSpecification: {
- URL: 'https://cdn.earthdata.nasa.gov/umm/tool/v1.2.0',
- Name: 'UMM-T',
- Version: '1.2.0'
- },
- LongName: 'mock long name'
- },
- url: {
- urlContentType: 'DistributionURL',
- type: 'GOTO WEB TOOL',
- urlValue: 'mock url'
},
- useConstraints: null,
- version: '1',
- versionDescription: null,
- collections: {
- count: 2,
- items: [
- {
- title: '2000 Pilot Environmental Sustainability Index (ESI)',
- conceptId: 'C12000001123-MMT_2',
- entryTitle: '2000 Pilot Environmental Sustainability Index (ESI)',
- shortName: 'CIESIN_SEDAC_ESI_2000',
- version: '2000.00',
- provider: 'SEDAC',
- __typename: 'Collection'
- },
- {
- title: '2001 Environmental Sustainability Index (ESI)',
- conceptId: 'C1200000035-SEDAC',
- entryTitle: '2001 Environmental Sustainability Index (ESI)',
- shortName: 'CIESIN_SEDAC_ESI_2001',
- version: '2001.00',
- provider: 'SEDAC',
- __typename: 'Collection'
+ result: {
+ data: {
+ orderOption: {
+ deprecated: false,
+ name: 'Test Name',
+ description: 'Test Description',
+ form: 'Test Form',
+ scope: 'PROVIDER',
+ providerId: 'MMT_2',
+ __typename: 'OrderOption'
}
- ],
- __typename: 'CollectionList'
- },
- __typename: 'Tool'
-}
-
-export const mockOrderOption = {
- deprecated: false,
- name: 'Test Name',
- description: 'Test Description',
- form: 'Test Form',
- scope: 'PROVIDER',
- providerId: 'MMT_2',
- __typename: 'OrderOption'
+ }
+ }
}
export const mockTool = {
@@ -274,88 +164,83 @@ export const mockTool = {
__typename: 'Tool'
}
-export const mockVariable = {
- additionalIdentifiers: [
- {
- identifier: '123'
- }
- ],
- associationDetails: {
- collections: [
- {
- conceptId: 'C1200000034-SEDAC'
- },
- {
- conceptId: 'C1200000035-SEDAC'
+export const mockNoCollections = {
+ request: {
+ query: GET_COLLECTIONS,
+ variables: {
+ params: {
+ limit: 20,
+ offset: 0,
+ provider: null,
+ sortKey: null,
+ options: { entryTitle: { pattern: true } },
+ entryTitle: '*'
}
- ]
+ }
},
- conceptId: 'V1200000107-SEDAC',
- dataType: null,
- definition: '213',
- dimensions: null,
- fillValues: null,
- indexRanges: null,
- instanceInformation: null,
- longName: '123',
- measurementIdentifiers: null,
- name: 'Testing association',
- nativeId: 'MMT_fe95add0-9ae8-471e-938b-1e8110ec8207.',
- offset: null,
- providerId: 'SEDAC',
- relatedUrls: null,
- revisionDate: '2024-03-18T14:40:14.945Z',
- revisionId: '24',
- samplingIdentifiers: null,
- scale: null,
- scienceKeywords: null,
- sets: null,
- standardName: null,
- ummMetadata: {
- MetadataSpecification: {
- URL: 'https://cdn.earthdata.nasa.gov/umm/variable/v1.9.0',
- Name: 'UMM-Var',
- Version: '1.9.0'
- },
- AdditionalIdentifiers: [
- {
- Identifier: '123'
+ result: {
+ data: {
+ collections: {
+ items: [],
+ count: 0,
+ __typename: 'CollectionList'
}
- ],
- Name: 'Testing association',
- LongName: '123',
- Definition: '213'
- },
- units: null,
- validRanges: null,
- variableSubType: null,
- variableType: null,
- collections: {
- count: 1,
- items: [
- {
- conceptId: 'C1200000034-SEDAC',
- entryTitle: '2000 Pilot Environmental Sustainability Index (ESI)',
- shortName: 'CIESIN_SEDAC_ESI_2000',
- version: '2000.00',
- provider: 'SEDAC',
- __typename: 'Collection'
+ }
+ }
+}
+
+export const mockCollections = {
+ items: [
+ {
+ conceptId: 'C12000001123-MMT_2',
+ provider: 'MMT_2',
+ version: '1',
+ revisionId: 1,
+ shortName: 'Collection Associations Short Name 1',
+ entryTitle: 'Collection Associations Entry Title 1',
+ tags: null,
+ granules: null,
+ revisionDate: null,
+ title: 'Collection Associations Title 1',
+ tagDefinitions: {
+ items: [{
+ conceptId: 'C100000',
+ description: 'Mock tag description',
+ originatorId: 'test.user',
+ revisionId: '1',
+ tagKey: 'Mock tag key'
+ }]
},
- {
- conceptId: 'C1200000035-SEDAC',
- entryTitle: '2001 Pilot Environmental Sustainability Index (ESI)',
- shortName: 'CIESIN_SEDAC_ESI_2001',
- version: '2000.00',
- provider: 'SEDAC',
- __typename: 'Collection'
- }
- ],
- __typename: 'CollectionList'
- },
- __typename: 'Variable'
+ __typename: 'Collection'
+ },
+ {
+ conceptId: 'C12000001124-MMT_2',
+ provider: 'MMT_2',
+ version: '1',
+ revisionId: 1,
+ shortName: 'Unassociated Collection Short Name 1',
+ entryTitle: 'Unassociated Collection Entry Title 1',
+ tags: null,
+ granules: null,
+ revisionDate: null,
+ title: 'Unassociated Title 1',
+ tagDefinitions: {
+ items: [{
+ conceptId: 'C100000',
+ description: 'Mock tag description',
+ originatorId: 'test.user',
+ revisionId: '1',
+ tagKey: 'Mock tag key'
+ }]
+ },
+ __typename: 'Collection'
+ }
+ ],
+ count: 50,
+ __typename: 'CollectionList'
}
-export const CollectionAssociationRequest = {
+export const getCollectionsMockPage1 = {
request: {
query: GET_COLLECTIONS,
variables: {
@@ -425,13 +310,13 @@ export const CollectionAssociationRequest = {
}
}
-export const CollectionResultsWithPages = {
+export const getCollectionsMockPage2 = {
request: {
query: GET_COLLECTIONS,
variables: {
params: {
limit: 20,
- offset: 40,
+ offset: 20,
provider: null,
sortKey: null,
options: { entryTitle: { pattern: true } },
@@ -473,7 +358,7 @@ export const CollectionResultsWithPages = {
}
}
-export const GetServicesRequest = {
+export const getServices = {
request: {
query: GET_SERVICES,
variables: {
@@ -506,7 +391,7 @@ export const createAssociationRequest = {
request: {
query: CREATE_ASSOCIATION,
variables: {
- conceptId: 'T12000000-MMT_2',
+ conceptId: 'T1200000098-MMT_2',
associatedConceptIds: ['C12000001124-MMT_2']
}
},
@@ -547,44 +432,84 @@ export const createAssociationErrorRequest = {
request: {
query: CREATE_ASSOCIATION,
variables: {
- conceptId: 'T12000000-MMT_2',
- associatedConceptIds: ['C12000001123-MMT_2']
+ conceptId: 'T1200000098-MMT_2',
+ associatedConceptIds: ['C12000001124-MMT_2']
}
},
error: new Error('An error occurred')
}
-export const ingestVariableRequest = {
+export const getCollectionSortRequestByShortName = {
request: {
- query: CREATE_ASSOCIATION,
+ query: GET_COLLECTIONS,
variables: {
- conceptId: 'V12000000-MMT_2',
- associatedConceptIds: ['C12000001123-MMT_2']
+ params: {
+ limit: 20,
+ offset: 0,
+ provider: null,
+ sortKey: '-shortName',
+ options: { entryTitle: { pattern: true } },
+ entryTitle: '*'
+ }
}
},
result: {
data: {
- createAssociation: {
- associatedConceptId: 'C1200000035-SEDAC',
- conceptId: 'VA1200000140-CMR',
- revisionId: 2
+ collections: {
+ items: [
+ {
+ conceptId: 'C12000001123-MMT_2',
+ provider: 'MMT_2',
+ version: '1',
+ revisionId: 1,
+ tags: 1,
+ granules: null,
+ shortName: 'Collection Associations Short Name 1',
+ entryTitle: 'Collection Associations Entry Title 1 ',
+ title: 'Collection Associations Title 1',
+ revisionDate: null,
+ tagDefinitions: {
+ items: [{
+ conceptId: 'C100000',
+ description: 'Mock tag description',
+ originatorId: 'test.user',
+ revisionId: '1',
+ tagKey: 'Mock tag key'
+ }]
+ },
+ __typename: 'Collection'
+ },
+ {
+ conceptId: 'C12000001124-MMT_2',
+ provider: 'MMT_2',
+ version: '1',
+ revisionId: 1,
+ tags: 1,
+ granules: null,
+ shortName: 'Collection Associations Short Name 2',
+ entryTitle: 'Collection Associations Entry Title 2',
+ title: 'Collection Associations Title 2',
+ revisionDate: null,
+ tagDefinitions: {
+ items: [{
+ conceptId: 'C100000',
+ description: 'Mock tag description',
+ originatorId: 'test.user',
+ revisionId: '1',
+ tagKey: 'Mock tag key'
+ }]
+ },
+ __typename: 'Collection'
+ }
+ ],
+ count: 50,
+ __typename: 'CollectionList'
}
}
}
}
-export const ingestVariableErrorRequest = {
- request: {
- query: CREATE_ASSOCIATION,
- variables: {
- conceptId: 'V12000000-MMT_2',
- associatedConceptIds: ['C12000001123-MMT_2']
- }
- },
- error: new Error('An error occurred')
-}
-
-export const CollectionSortRequest = {
+export const getCollectionSortRequestByProvider = {
request: {
query: GET_COLLECTIONS,
variables: {
@@ -592,7 +517,7 @@ export const CollectionSortRequest = {
limit: 20,
offset: 0,
provider: null,
- sortKey: '-shortName',
+ sortKey: 'provider',
options: { entryTitle: { pattern: true } },
entryTitle: '*'
}
@@ -653,3 +578,48 @@ export const CollectionSortRequest = {
}
}
}
+
+export const getToolMock = {
+ request: {
+ query: GET_TOOL,
+ variables: {
+ params: {
+ conceptId: 'T1200000098-MMT_2'
+ }
+ }
+ },
+ result: {
+ data: { tool: mockTool }
+ }
+}
+
+export const getToolMockWithError = {
+ request: {
+ query: GET_TOOL,
+ variables: {
+ params: {
+ conceptId: 'T1200000098-MMT_2'
+ }
+ }
+ },
+ error: new Error('An error occurred')
+}
+
+export const getCollectionsMock = {
+ request: {
+ query: GET_COLLECTIONS,
+ variables: {
+ params: {
+ limit: 20,
+ offset: 0,
+ provider: null,
+ sortKey: null,
+ options: { entryTitle: { pattern: true } },
+ entryTitle: '*'
+ }
+ }
+ },
+ result: {
+ data: { collections: mockCollections }
+ }
+}
diff --git a/static/src/js/components/ManageCollectionAssociation/ManageCollectionAssociation.jsx b/static/src/js/components/ManageCollectionAssociation/ManageCollectionAssociation.jsx
index dcab4dfdf..3e8844284 100644
--- a/static/src/js/components/ManageCollectionAssociation/ManageCollectionAssociation.jsx
+++ b/static/src/js/components/ManageCollectionAssociation/ManageCollectionAssociation.jsx
@@ -12,9 +12,9 @@ import React, { useCallback, useState } from 'react'
import pluralize from 'pluralize'
import Button from '@/js/components/Button/Button'
+import ControlledPaginatedContent from '@/js/components/ControlledPaginatedContent/ControlledPaginatedContent'
import CustomModal from '@/js/components/CustomModal/CustomModal'
import EllipsisText from '@/js/components/EllipsisText/EllipsisText'
-import Pagination from '@/js/components/Pagination/Pagination'
import Table from '@/js/components/Table/Table'
import conceptTypeQueries from '@/js/constants/conceptTypeQueries'
@@ -41,14 +41,17 @@ const ManageCollectionAssociation = () => {
const { addNotification } = useNotificationsContext()
- const [searchParams, setSearchParams] = useSearchParams()
+ const derivedConceptType = getConceptTypeByConceptId(conceptId)
+
+ // Variables for deletion mutation
const [collectionConceptIds, setCollectionConceptIds] = useState([])
const [showDeleteModal, setShowDeleteModal] = useState(false)
+ const [isDeleting, setIsDeleting] = useState(false)
- const derivedConceptType = getConceptTypeByConceptId(conceptId)
-
+ // Variables for pagination
+ const [searchParams, setSearchParams] = useSearchParams()
+ const [activePage, setActivePage] = useState(1)
const limit = 20
- const activePage = parseInt(searchParams.get('page'), 10) || 1
const offset = (activePage - 1) * limit
let params = {
@@ -79,10 +82,7 @@ const ManageCollectionAssociation = () => {
})
const [deleteAssociationMutation] = useMutation(DELETE_ASSOCIATION, {
- refetchQueries: [{
- query: conceptTypeQueries[derivedConceptType],
- variables: params
- }],
+
onCompleted: () => {
setShowDeleteModal(false)
@@ -93,8 +93,14 @@ const ManageCollectionAssociation = () => {
})
setCollectionConceptIds([])
+
+ // Gives time for CMR to update data
+ setTimeout(() => {
+ refetch()
+ }, 250)
},
onError: () => {
+ setShowDeleteModal(false)
addNotification({
message: 'Error disassociating collection',
variant: 'danger'
@@ -105,13 +111,15 @@ const ManageCollectionAssociation = () => {
})
// Handles deleting selected collection
- // if no collections selected, returns an error notification
const handleDeleteAssociation = () => {
+ setIsDeleting(true)
deleteAssociationMutation({
variables: {
conceptId,
associatedConceptIds: collectionConceptIds
}
+ }).finally(() => {
+ setIsDeleting(false)
})
}
@@ -124,14 +132,13 @@ const ManageCollectionAssociation = () => {
if (order === 'ascending') nextSortKey = `-${key}`
if (order === 'descending') nextSortKey = key
- // Reset the page parameter
- currentParams.delete('page')
-
// Set the sort key
currentParams.set('sortKey', nextSortKey)
return Object.fromEntries(currentParams)
})
+
+ setActivePage(1) // Reset to first page when sorting
}, [])
const buildEllipsisTextCell = useCallback((cellData) => (
@@ -140,8 +147,8 @@ const ManageCollectionAssociation = () => {
), [])
- // Handles checkbox selections, if checked add the conceptId to the state variable
- // and pops the added conceptId from the array.
+ // Adds or removes checked collections from collectionConceptIds array
+ // which is provided to the deleteMutation
const handleCheckbox = (event) => {
const { target } = event
const { value } = target
@@ -200,11 +207,7 @@ const ManageCollectionAssociation = () => {
]
const setPage = (nextPage) => {
- setSearchParams((currentParams) => {
- currentParams.set('page', nextPage)
-
- return Object.fromEntries(currentParams)
- })
+ setActivePage(nextPage)
}
const toggleShowDeleteModal = (nextState) => {
@@ -216,91 +219,101 @@ const ManageCollectionAssociation = () => {
toggleShowDeleteModal(true)
})
- // Handle refresh, calls getMetadata to get the list of association
- // TODO: MMT-4089 See if we can get rid of this refresh button.
- const handleRefreshPage = () => {
- refetch()
- }
-
- const refreshAccessibleEventProps = useAccessibleEvent(() => {
- handleRefreshPage()
- })
-
const { [camelCase(derivedConceptType)]: concept } = data
const { collections: associatedCollections } = concept
- const { items = [], count } = associatedCollections
-
- const totalPages = Math.ceil(count / limit)
-
- const currentPageIndex = Math.floor(offset / limit)
- const firstResultIndex = currentPageIndex * limit
- const isLastPage = totalPages === activePage
- const lastResultIndex = firstResultIndex + (isLastPage ? count % limit : limit)
-
- const paginationMessage = count > 0
- ? `Showing ${totalPages > 1 ? `Collection Associations ${firstResultIndex + 1}-${lastResultIndex} of ${count}` : `${count} ${pluralize('Collection Association', count)}`}`
- : 'No Collection Associations found'
+ const { items, count } = associatedCollections
return (
-
+
{' '}
Association operations may take some time. If you are not seeing what you expect below,
please
{' '}
- refetch()}
+ aria-label="Refresh the page"
+ type="button"
>
- refresh the page
-
+ refresh the page
+
-
-
- {
- (!!count) && (
- {paginationMessage}
+
+ {
+ ({
+ totalPages,
+ pagination,
+ firstResultPosition,
+ lastResultPosition
+ }) => {
+ const paginationMessage = count > 0
+ ? `Showing ${totalPages > 1 ? `${firstResultPosition}-${lastResultPosition} of` : ''} ${count} Collection ${pluralize('Association', count)}`
+ : 'No collection associations found'
+
+ return (
+ <>
+
+
+ {
+ (!!count) && (
+ {paginationMessage}
+ )
+ }
+
+
+ {
+ totalPages > 1 && (
+
+ {pagination}
+
+ )
+ }
+
+ `column_${dataKey}_${conceptIdCell}`}
+ generateRowKey={({ conceptId: conceptIdRow }) => `row_${conceptIdRow}`}
+ id="associated-collections"
+ limit={count}
+ noDataMessage="No collection associations found."
+ />
+ {
+ totalPages > 1 && (
+
+
+
+ {pagination}
+
+
+
+ )
+ }
+ >
)
}
-
- {
- totalPages > 1 && (
-
-
-
- )
}
-
- `column_${dataKey}_${conceptIdCell}`}
- generateRowKey={({ conceptId: conceptIdRow }) => `row_${conceptIdRow}`}
- id="associated-collections"
- limit={count}
- noDataMessage="No collection associations found."
- offset={offset}
- />
+
Delete Selected Associations
@@ -319,7 +332,8 @@ const ManageCollectionAssociation = () => {
{
label: 'Yes',
variant: 'primary',
- onClick: handleDeleteAssociation
+ onClick: handleDeleteAssociation,
+ disabled: isDeleting
}
]
}
diff --git a/static/src/js/components/ManageCollectionAssociation/__tests__/ManageCollectionAssociation.test.jsx b/static/src/js/components/ManageCollectionAssociation/__tests__/ManageCollectionAssociation.test.jsx
index 679d91ff7..b3ecc8c23 100644
--- a/static/src/js/components/ManageCollectionAssociation/__tests__/ManageCollectionAssociation.test.jsx
+++ b/static/src/js/components/ManageCollectionAssociation/__tests__/ManageCollectionAssociation.test.jsx
@@ -11,12 +11,10 @@ import {
} from 'react-router'
import { MockedProvider } from '@apollo/client/testing'
import userEvent from '@testing-library/user-event'
-import * as router from 'react-router'
import { InMemoryCache, defaultDataIdFromObject } from '@apollo/client'
import NotificationsContext from '@/js/context/NotificationsContext'
import errorLogger from '@/js/utils/errorLogger'
-import ErrorBanner from '@/js/components/ErrorBanner/ErrorBanner'
import { DELETE_ASSOCIATION } from '@/js/operations/mutations/deleteAssociation'
import ErrorBoundary from '@/js/components/ErrorBoundary/ErrorBoundary'
@@ -26,7 +24,7 @@ import {
deleteAssociationResponse,
deletedAssociationResponse,
toolRecordSearch,
- toolRecordSearchError,
+ toolRecordSearchNoAssociatedCollections,
toolRecordSearchTestPage,
toolRecordSearchwithPages,
toolRecordSortSearch
@@ -108,7 +106,9 @@ const setup = ({
describe('ManageCollectionAssociation', () => {
describe('when the collection association page is requested', () => {
test('renders the collection association page with the associated collections', async () => {
- setup({})
+ setup({
+ overrideMocks: [toolRecordSearch]
+ })
expect(await screen.findByText('Showing 2 Collection Associations')).toBeInTheDocument()
expect(screen.getByText('CIESIN_SEDAC_ESI_2000')).toBeInTheDocument()
@@ -116,33 +116,30 @@ describe('ManageCollectionAssociation', () => {
})
})
- describe('when paging through the table', () => {
- test('navigate to the next page', async () => {
- const { user } = setup({
- overrideMocks: [toolRecordSearchTestPage, toolRecordSearchwithPages]
+ describe('when the collection association page is requested but the record has no associations', () => {
+ test('renders the collection association page with message ', async () => {
+ setup({
+ overrideMocks: [toolRecordSearchNoAssociatedCollections]
})
- expect(await screen.findByText('Showing Collection Associations 1-20 of 50'))
- const paginationButton = screen.getByRole('button', { name: 'Goto Page 3' })
- await user.click(paginationButton)
-
- expect(await screen.findByText('Showing Collection Associations 41-50 of 50'))
+ await screen.findByText('No collection associations found.')
+ expect(screen.getByText('No collection associations found.')).toBeInTheDocument()
})
})
- describe.skip('when the request results in an error', () => {
- test('should call errorLogger and renders an ErrorBanner', async () => {
- setup({
- overrideMocks: [toolRecordSearchError]
+ describe('when paging through the table', () => {
+ test('navigate to the next page', async () => {
+ const { user } = setup({
+ overrideMocks: [toolRecordSearchTestPage, toolRecordSearchwithPages]
})
- await waitFor(() => {
- expect(errorLogger).toHaveBeenCalledWith('Unable to get draft', 'Manage Collection Association: getMetadata Query')
- })
+ expect(await screen.findByText('Showing 1-20 of 50 Collection Associations'))
- expect(errorLogger).toHaveBeenCalledTimes(1)
+ // Find the "Next" button in the new pagination structure
+ const nextPageButtons = screen.getAllByRole('button', { name: 'Goto Next Page' })
+ await user.click(nextPageButtons[0])
- expect(ErrorBanner).toHaveBeenCalledTimes(1)
+ expect(await screen.findByText('Showing 21-40 of 50 Collection Associations'))
})
})
@@ -228,14 +225,15 @@ describe('ManageCollectionAssociation', () => {
describe('when selecting Yes in the modal and results in a success ', () => {
test('should remove the deleted collection', async () => {
const { user } = setup({
- overrideMocks: [
- deletedAssociationResponse,
+ additionalMocks: [
deleteAssociationResponse,
+ deletedAssociationResponse,
deletedAssociationResponse
]
})
+ await screen.findByText('Showing 2 Collection Associations')
- const checkboxes = await screen.findAllByRole('checkbox')
+ const checkboxes = screen.queryAllByRole('checkbox')
const firstCheckbox = checkboxes[0]
await user.click(firstCheckbox)
@@ -249,38 +247,29 @@ describe('ManageCollectionAssociation', () => {
const yesButton = screen.getByRole('button', { name: 'Yes' })
await user.click(yesButton)
+ await screen.findByText('Showing 1 Collection Association')
expect(screen.getByText('CIESIN_SEDAC_ESI_2001')).toBeInTheDocument()
})
})
})
describe('when clicking on refresh button', () => {
- test('should call getMetadata() again', async () => {
+ test('should refetch the data', async () => {
const { user } = setup({
- additionalMocks: [toolRecordSearch]
+ overrideMocks: [toolRecordSearch, deletedAssociationResponse]
})
- const refreshButton = await screen.findByRole('link', { name: 'refresh the page' })
-
- await user.click(refreshButton)
-
- expect(screen.getByText('CIESIN_SEDAC_ESI_2000')).toBeInTheDocument()
- })
- })
-
- describe('when clicking on Add Collection Associations button', () => {
- test.skip('should navigate to collection-search', async () => {
- const navigateSpy = vi.fn()
- vi.spyOn(router, 'useNavigate').mockImplementation(() => navigateSpy)
-
- const { user } = setup({})
+ await screen.findByText('Showing 2 Collection Associations')
- const addCollectionAssociationButton = await screen.findByRole('button', { name: 'Add Collection Associations' })
+ const refreshButton = screen.getByRole('button', { name: /refresh the page/i })
+ expect(refreshButton).toBeInTheDocument()
- await user.click(addCollectionAssociationButton)
+ await user.click(refreshButton)
- expect(navigateSpy).toHaveBeenCalledWith('/tools/T1200000-TEST/collection-association-search')
- expect(navigateSpy).toHaveBeenCalledTimes(1)
+ // Check to see that data has been refetched
+ await waitFor(() => {
+ expect(screen.getByText('Showing 1 Collection Association')).toBeInTheDocument()
+ })
})
})
@@ -319,19 +308,19 @@ describe('ManageCollectionAssociation', () => {
collectionsParams: {
limit: 20,
offset: 0,
- sortKey: '-shortName'
+ sortKey: 'shortName'
}
}
},
result: toolRecordSortSearch.result
}],
- overrideInitialEntries: ['/tools/T1200000-TEST/collection-association']
+ overrideInitialEntries: ['/tools/T1200000-TEST/collection-association?sortKey=shortName']
})
- const test = await screen.findByRole('button', { name: /Sort Short Name in ascending order/ })
+ const test = await screen.findByRole('button', { name: /Sort Short Name in descending order/ })
await user.click(test)
- expect(await screen.findByRole('button', { name: /Sort Short Name in ascending order/ })).toHaveClass('d-flex align-items-center text-nowrap button--naked table__sort-button text-secondary d-flex justify-content-center btn')
+ expect(await screen.findByRole('button', { name: /Sort Short Name in descending order/ })).toHaveClass('d-flex align-items-center text-nowrap button--naked table__sort-button text-secondary d-flex justify-content-center btn')
})
})
})
diff --git a/static/src/js/components/ManageCollectionAssociation/__tests__/__mocks__/manageCollectionAssociationResults.js b/static/src/js/components/ManageCollectionAssociation/__tests__/__mocks__/manageCollectionAssociationResults.js
index 866a6d683..aae7e815f 100644
--- a/static/src/js/components/ManageCollectionAssociation/__tests__/__mocks__/manageCollectionAssociationResults.js
+++ b/static/src/js/components/ManageCollectionAssociation/__tests__/__mocks__/manageCollectionAssociationResults.js
@@ -141,6 +141,121 @@ export const toolRecordSearch = {
}
}
}
+export const toolRecordSearchNoAssociatedCollections = {
+ request: {
+ query: GET_TOOL,
+ variables: {
+ params: {
+ conceptId: 'T1200000-TEST'
+ },
+ collectionsParams: {
+ limit: 20,
+ offset: 0
+ }
+ }
+ },
+ result: {
+ data: {
+ tool: {
+ __typename: 'Tool',
+ accessConstraints: null,
+ ancillaryKeywords: null,
+ associationDetails: null,
+ collections: {
+ count: 0,
+ items: []
+ },
+ conceptId: 'T1200000-TEST',
+ contactGroups: null,
+ contactPersons: null,
+ description: '312',
+ doi: null,
+ lastUpdatedDate: null,
+ longName: '123',
+ metadataSpecification: {
+ name: 'UMM-T',
+ url: 'https://cdn.earthdata.nasa.gov/umm/tool/v1.2.0',
+ version: '1.2.0'
+ },
+ name: 'Testing publish with routes',
+ nativeId: 'Test-123',
+ organizations: [{
+ longName: 'Woods Hole Science Center, Coastal and Marine Geology, U.S. Geological Survey, U.S. Department of the Interior',
+ roles: ['DEVELOPER'],
+ shortName: 'DOI/USGS/CMG/WHSC',
+ urlValue: 'http://woodshole.er.usgs.gov/'
+ }],
+ pageTitle: 'Testing publish with routes',
+ potentialAction: null,
+ providerId: 'MMT_2',
+ quality: null,
+ relatedUrls: null,
+ revisionDate: '2024-03-19T17:05:21.642Z',
+ revisionId: '1',
+ revisions: {
+ __typename: 'ToolRevisionList',
+ count: 1,
+ items: [{
+ __typename: 'Tool',
+ conceptId: 'T1200000-TEST',
+ revisionDate: '2024-04-25T17:11:57.611Z',
+ revisionId: '1',
+ userId: 'ECHO_SYS'
+ }]
+ },
+ 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: 'Model',
+ ummMetadata: {
+ Description: '312',
+ LongName: '123',
+ MetadataSpecification: {
+ Name: 'UMM-T',
+ URL: 'https://cdn.earthdata.nasa.gov/umm/tool/v1.2.0',
+ Version: '1.2.0'
+ },
+ Name: 'Testing publish with routes',
+ Organizations: [{
+ LongName: 'Woods Hole Science Center, Coastal and Marine Geology, U.S. Geological Survey, U.S. Department of the Interior',
+ Roles: ['DEVELOPER'],
+ ShortName: 'DOI/USGS/CMG/WHSC',
+ URLValue: 'http://woodshole.er.usgs.gov/'
+ }],
+ ToolKeywords: [{
+ ToolCategory: 'EARTH SCIENCE SERVICES',
+ ToolTerm: 'CALIBRATION/VALIDATION',
+ ToolTopic: 'DATA ANALYSIS AND VISUALIZATION'
+ }],
+ Type: 'Model',
+ URL: {
+ Type: 'DOWNLOAD SOFTWARE',
+ URLContentType: 'DistributionURL',
+ URLValue: '132'
+ },
+ Version: '123'
+ },
+ url: {
+ type: 'DOWNLOAD SOFTWARE',
+ urlContentType: 'DistributionURL',
+ urlValue: '132'
+ },
+ useConstraints: null,
+ version: '123',
+ versionDescription: null
+ }
+ }
+ }
+}
+
export const toolRecordSearchwithPages = {
request: {
query: GET_TOOL,
@@ -150,7 +265,7 @@ export const toolRecordSearchwithPages = {
},
collectionsParams: {
limit: 20,
- offset: 40
+ offset: 20
}
}
},
diff --git a/static/src/js/components/Table/Table.scss b/static/src/js/components/Table/Table.scss
index 5f6563ce6..d7aa4e289 100644
--- a/static/src/js/components/Table/Table.scss
+++ b/static/src/js/components/Table/Table.scss
@@ -54,7 +54,6 @@
th:first-child,
td:first-child {
position: sticky;
- z-index: 1000;
left: 0;
max-width: 20rem;
}
diff --git a/static/src/js/pages/CollectionAssociationFormPage/CollectionAssociationFormPage.jsx b/static/src/js/pages/CollectionAssociationFormPage/CollectionAssociationFormPage.jsx
index 4cce67d1d..44f580c4b 100644
--- a/static/src/js/pages/CollectionAssociationFormPage/CollectionAssociationFormPage.jsx
+++ b/static/src/js/pages/CollectionAssociationFormPage/CollectionAssociationFormPage.jsx
@@ -8,6 +8,7 @@ import pluralize from 'pluralize'
import CollectionAssociationForm from '@/js/components/CollectionAssociationForm/CollectionAssociationForm'
import ErrorBoundary from '@/js/components/ErrorBoundary/ErrorBoundary'
+import LoadingBanner from '@/js/components/LoadingBanner/LoadingBanner'
import Page from '@/js/components/Page/Page'
import PageHeader from '@/js/components/PageHeader/PageHeader'
@@ -46,7 +47,7 @@ const CollectionAssociationFormPageHeader = () => {
return (
{
*
* )
*/
-const CollectionAssociationFormPage = () => {
- const { conceptId } = useParams()
-
- const derivedConceptType = getConceptTypeByConceptId(conceptId)
-
- const { data } = useSuspenseQuery(conceptTypeQueries[derivedConceptType], {
- variables: {
- params: {
- conceptId
- }
- }
- })
-
- const { [camelCase(derivedConceptType)]: concept } = data
-
- return (
- }
- >
-
-
-
-
-
-
- )
-}
+const CollectionAssociationFormPage = () => (
+ }
+ >
+
+ }>
+
+
+
+
+)
export default CollectionAssociationFormPage
diff --git a/static/src/js/pages/CollectionAssociationFormPage/__tests__/CollectionAssociationFormPage.test.jsx b/static/src/js/pages/CollectionAssociationFormPage/__tests__/CollectionAssociationFormPage.test.jsx
index 3af117b77..d874239b4 100644
--- a/static/src/js/pages/CollectionAssociationFormPage/__tests__/CollectionAssociationFormPage.test.jsx
+++ b/static/src/js/pages/CollectionAssociationFormPage/__tests__/CollectionAssociationFormPage.test.jsx
@@ -10,7 +10,6 @@ import {
import { GET_TOOL } from '@/js/operations/queries/getTool'
import { GET_ORDER_OPTION } from '@/js/operations/queries/getOrderOption'
-import CollectionAssociationForm from '@/js/components/CollectionAssociationForm/CollectionAssociationForm'
import CollectionAssociationFormPage from '../CollectionAssociationFormPage'
vi.mock('@/js/components/CollectionAssociationForm/CollectionAssociationForm')
@@ -188,7 +187,7 @@ describe('CollectionAssociationFormPage', () => {
})
expect(await screen.findByText('Tools')).toBeInTheDocument()
- expect(screen.getByRole('heading', { name: 'Mock Tool Collection Associations' })).toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: 'Mock Tool Collection Association Search' })).toBeInTheDocument()
})
})
@@ -254,42 +253,7 @@ describe('CollectionAssociationFormPage', () => {
})
expect(await screen.findByText('Order Options')).toBeInTheDocument()
- expect(screen.getByRole('heading', { name: 'Test Order Option Collection Associations' })).toBeInTheDocument()
- })
- })
-
- describe('when rendering the CollectionAssociationForm', () => {
- test('should pass the correct props', async () => {
- const mocks = [{
- request: {
- query: GET_TOOL,
- variables: { params: { conceptId: 'T1234-MMT' } }
- },
- result: {
- data: {
- tool: mockTool
- }
- }
- }]
-
- setup({
- mocks,
- pageUrl: '/tools/T1234-MMT/collection-association-search'
- })
-
- // Wait for the query to resolve
- await screen.findByText('Mock Tool Collection Associations')
-
- // Check if CollectionAssociationForm is rendered with correct props
- expect(CollectionAssociationForm).toHaveBeenCalledWith(
- expect.objectContaining({
- metadata: expect.objectContaining({
- conceptId: 'T1234-MMT',
- name: 'Mock Tool'
- })
- }),
- expect.anything()
- )
+ expect(screen.getByRole('heading', { name: 'Test Order Option Collection Association Search' })).toBeInTheDocument()
})
})