Skip to content
12 changes: 6 additions & 6 deletions static/src/js/components/Notifications/Notifications.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ const Notifications = () => {
}
show={show}
>
<Toast.Body className="d-flex align-items-center justify-content-between">
<div className="d-flex align-items-center">
<Toast.Body className="d-flex">
<div className="d-flex flex-grow-1 overflow-hidden">
<span
className={
classNames(
Expand All @@ -89,12 +89,12 @@ const Notifications = () => {
>
{iconMap[variant] && iconMap[variant]}
</span>

{message}
<div className="notifications-list__message">
{message}
</div>
</div>

<Button
className="ms-2"
className="notifications-list__close-button ms-2"
onClick={() => hideNotification(id)}
size="sm"
variant="outline-light-dark"
Expand Down
33 changes: 27 additions & 6 deletions static/src/js/components/Notifications/Notifications.scss
Original file line number Diff line number Diff line change
@@ -1,30 +1,51 @@
.notifications-list {
max-inline-size: 90vw; // Limit the maximum width of notifications

.toast {
max-inline-size: 100%; // Allow toasts to take full width of the container
}

&__icon-background {
width: 2rem;
height: 2rem;
flex-shrink: 0;
border-radius: 50%;
block-size: 2rem;
inline-size: 2rem;
}

&__icon {
font-size: 1.2rem;
}

&__message {
flex: 1;
min-inline-size: 0; // Ensures flexbox works correctly
overflow-wrap: break-word; // Additional support for wrapping
padding-inline-end: 0.5rem; // Add some space before the close button
word-wrap: break-word; // Allows long words to break and wrap
}

&__close-button {
flex-shrink: 0;
align-self: flex-start; // Aligns the button to the top
white-space: nowrap;
}

&__timer {
--timer-duration: 3000ms;

width: 0%;
height: 0.325rem;
animation: timer var(--timer-duration) linear;
background-color: var(--bs-gray-600);
block-size: 0.325rem;
inline-size: 0%;
}

@keyframes timer {
0% {
width: 0%;
inline-size: 0%;
}

100% {
width: 100%;
inline-size: 100%;
}
}
}
35 changes: 28 additions & 7 deletions static/src/js/components/Permission/Permission.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import React, { Suspense, useCallback } from 'react'
import { useParams } from 'react-router'
import React, {
Suspense,
useCallback,
useEffect
} from 'react'
import { useNavigate, useParams } from 'react-router'
import { useSuspenseQuery } from '@apollo/client'

import Col from 'react-bootstrap/Col'
Expand All @@ -11,37 +15,54 @@ import LoadingTable from '@/js/components/LoadingTable/LoadingTable'
import PermissionCollectionTable from '@/js/components/PermissionCollectionTable/PermissionCollectionTable'
import Table from '@/js/components/Table/Table'
import validGroupItems from '@/js/utils/validGroupItems'
import useNotificationsContext from '@/js/hooks/useNotificationsContext'

import { GET_COLLECTION_PERMISSION } from '@/js/operations/queries/getCollectionPermission'

import './Permission.scss'

const Permission = () => {
const { conceptId } = useParams()
const navigate = useNavigate()
const { addNotification } = useNotificationsContext()

const { data } = useSuspenseQuery(GET_COLLECTION_PERMISSION, {
const { data = {} } = useSuspenseQuery(GET_COLLECTION_PERMISSION, {
variables: {
conceptId
}
})

const { acl } = data
useEffect(() => {
if (data && conceptId !== 'new') {
const { acl } = data
if (!acl) {
addNotification({
message: `${conceptId} was not found.`,
variant: 'danger'
})

navigate('/permissions')
}
}
}, [data])

const { acl } = data || {}

const {
catalogItemIdentity,
collections,
groups
} = acl
} = acl || {}

// Returns valid group permission items. Invalid items are those without both id and userType.
const groupItems = validGroupItems(groups.items)
const groupItems = validGroupItems(groups?.items) || []

const {
collectionApplicable,
collectionIdentifier,
granuleApplicable,
granuleIdentifier
} = catalogItemIdentity
} = catalogItemIdentity || {}

const {
accessValue: collectionAccessValue,
Expand Down
166 changes: 143 additions & 23 deletions static/src/js/components/Permission/__tests__/Permission.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MockedProvider } from '@apollo/client/testing'
import {
render,
screen,
waitFor,
within
} from '@testing-library/react'
import React, { Suspense } from 'react'
Expand All @@ -10,14 +11,28 @@ import {
Route,
Routes
} from 'react-router'

import * as router from 'react-router'

import userEvent from '@testing-library/user-event'

import Permission from '@/js/components/Permission/Permission'
import PermissionCollectionTable from '@/js/components/PermissionCollectionTable/PermissionCollectionTable'

import { GET_COLLECTION_PERMISSION } from '@/js/operations/queries/getCollectionPermission'
import { GET_GROUPS } from '@/js/operations/queries/getGroups'
import { GET_AVAILABLE_PROVIDERS } from '@/js/operations/queries/getAvailableProviders'

import useAvailableProviders from '@/js/hooks/useAvailableProviders'
import Providers from '@/js/providers/Providers/Providers'
import NotificationsContext from '@/js/context/NotificationsContext'

vi.mock('../../PermissionCollectionTable/PermissionCollectionTable')
vi.mock('@/js/hooks/useAvailableProviders')

useAvailableProviders.mockReturnValue({
providerIds: ['MMT_1', 'MMT_2']
})

const setup = ({
overrideMocks = false
Expand Down Expand Up @@ -85,33 +100,106 @@ const setup = ({

const user = userEvent.setup()

const defaultMocks = [{
request: {
query: GET_AVAILABLE_PROVIDERS,
variables: {
params: {
limit: 500,
permittedUser: undefined,
target: 'PROVIDER_CONTEXT'
}
}
},
result: {
data: {
acls: {
items: [{
conceptId: 'mock-id',
providerIdentity: {
target: 'PROVIDER_CONTEXT',
provider_id: 'MMT_2'
}
}]
}
}
}
},
{
request: {
query: GET_GROUPS,
variables: {
params: {
tags: ['MMT_1', 'MMT_2'],
limit: 500
}
}
},
result: {
data: {
groups: {
__typename: 'GroupList',
count: 1,
items: [
{
__typename: 'Group',
description: 'Test group',
id: '1234-abcd-5678',
members: {
__typename: 'GroupMemberList',
count: 2
},
name: 'Mock group',
tag: 'MMT_2'
}
]
}

}
}
}]
const notificationContext = {
addNotification: vi.fn()
}

render(
<MockedProvider
mocks={overrideMocks || mocks}
>
<MemoryRouter initialEntries={['/permissions/ACL00000-CMR']}>
<Routes>
<Route
path="/permissions"
>
<Route
path=":conceptId"
element={
(
<Suspense>
<Permission />
</Suspense>
)
}
/>
</Route>
</Routes>
</MemoryRouter>
</MockedProvider>
<Providers>
<MockedProvider
mocks={
[
...defaultMocks,
...(overrideMocks || mocks)
]
}
>
<NotificationsContext.Provider value={notificationContext}>
<MemoryRouter initialEntries={['/permissions/ACL00000-CMR']}>
<Routes>
<Route
path="/permissions"
>
<Route
path=":conceptId"
element={
(
<Suspense>
<Permission />
</Suspense>
)
}
/>
</Route>
</Routes>
</MemoryRouter>
</NotificationsContext.Provider>

</MockedProvider>
</Providers>
)

return {
user
user,
notificationContext
}
}

Expand Down Expand Up @@ -837,4 +925,36 @@ describe('Permission', () => {
expect(invalidGroup).not.toBeInTheDocument()
})
})

describe('when the ACL is not found', () => {
test('should show a notification and navigate to permissions page', async () => {
const navigateSpy = vi.fn()

vi.spyOn(router, 'useNavigate').mockImplementation(() => navigateSpy)
vi.spyOn(router, 'useParams').mockReturnValue({ conceptId: 'NOT_FOUND_ACL' })

const { notificationContext } = setup({
overrideMocks: [{
request: {
query: GET_COLLECTION_PERMISSION,
variables: { conceptId: 'NOT_FOUND_ACL' }
},
result: {
data: {
acl: null
}
}
}]
})

await waitFor(() => {
expect(notificationContext.addNotification).toHaveBeenCalledWith({
message: 'NOT_FOUND_ACL was not found.',
variant: 'danger'
})
})

expect(navigateSpy).toHaveBeenCalledWith('/permissions')
})
})
})
Loading