Skip to content

Commit 7dac8aa

Browse files
authored
MMT collections permission form should surface errors properly (#1413)
* MMT collections permission form should surface errors properly
1 parent c7d9ddc commit 7dac8aa

File tree

9 files changed

+613
-98
lines changed

9 files changed

+613
-98
lines changed

static/src/js/components/Notifications/Notifications.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ const Notifications = () => {
7474
}
7575
show={show}
7676
>
77-
<Toast.Body className="d-flex align-items-center justify-content-between">
78-
<div className="d-flex align-items-center">
77+
<Toast.Body className="d-flex">
78+
<div className="d-flex flex-grow-1 overflow-hidden">
7979
<span
8080
className={
8181
classNames(
@@ -89,12 +89,12 @@ const Notifications = () => {
8989
>
9090
{iconMap[variant] && iconMap[variant]}
9191
</span>
92-
93-
{message}
92+
<div className="notifications-list__message">
93+
{message}
94+
</div>
9495
</div>
95-
9696
<Button
97-
className="ms-2"
97+
className="notifications-list__close-button ms-2"
9898
onClick={() => hideNotification(id)}
9999
size="sm"
100100
variant="outline-light-dark"
Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,51 @@
11
.notifications-list {
2+
max-inline-size: 90vw; // Limit the maximum width of notifications
3+
4+
.toast {
5+
max-inline-size: 100%; // Allow toasts to take full width of the container
6+
}
7+
28
&__icon-background {
3-
width: 2rem;
4-
height: 2rem;
9+
flex-shrink: 0;
510
border-radius: 50%;
11+
block-size: 2rem;
12+
inline-size: 2rem;
613
}
714

815
&__icon {
916
font-size: 1.2rem;
1017
}
1118

19+
&__message {
20+
flex: 1;
21+
min-inline-size: 0; // Ensures flexbox works correctly
22+
overflow-wrap: break-word; // Additional support for wrapping
23+
padding-inline-end: 0.5rem; // Add some space before the close button
24+
word-wrap: break-word; // Allows long words to break and wrap
25+
}
26+
27+
&__close-button {
28+
flex-shrink: 0;
29+
align-self: flex-start; // Aligns the button to the top
30+
white-space: nowrap;
31+
}
32+
1233
&__timer {
1334
--timer-duration: 3000ms;
1435

15-
width: 0%;
16-
height: 0.325rem;
1736
animation: timer var(--timer-duration) linear;
1837
background-color: var(--bs-gray-600);
38+
block-size: 0.325rem;
39+
inline-size: 0%;
1940
}
2041

2142
@keyframes timer {
2243
0% {
23-
width: 0%;
44+
inline-size: 0%;
2445
}
2546

2647
100% {
27-
width: 100%;
48+
inline-size: 100%;
2849
}
2950
}
3051
}

static/src/js/components/Permission/Permission.jsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import React, { Suspense, useCallback } from 'react'
2-
import { useParams } from 'react-router'
1+
import React, {
2+
Suspense,
3+
useCallback,
4+
useEffect
5+
} from 'react'
6+
import { useNavigate, useParams } from 'react-router'
37
import { useSuspenseQuery } from '@apollo/client'
48

59
import Col from 'react-bootstrap/Col'
@@ -11,37 +15,54 @@ import LoadingTable from '@/js/components/LoadingTable/LoadingTable'
1115
import PermissionCollectionTable from '@/js/components/PermissionCollectionTable/PermissionCollectionTable'
1216
import Table from '@/js/components/Table/Table'
1317
import validGroupItems from '@/js/utils/validGroupItems'
18+
import useNotificationsContext from '@/js/hooks/useNotificationsContext'
1419

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

1722
import './Permission.scss'
1823

1924
const Permission = () => {
2025
const { conceptId } = useParams()
26+
const navigate = useNavigate()
27+
const { addNotification } = useNotificationsContext()
2128

22-
const { data } = useSuspenseQuery(GET_COLLECTION_PERMISSION, {
29+
const { data = {} } = useSuspenseQuery(GET_COLLECTION_PERMISSION, {
2330
variables: {
2431
conceptId
2532
}
2633
})
2734

28-
const { acl } = data
35+
useEffect(() => {
36+
if (data && conceptId !== 'new') {
37+
const { acl } = data
38+
if (!acl) {
39+
addNotification({
40+
message: `${conceptId} was not found.`,
41+
variant: 'danger'
42+
})
43+
44+
navigate('/permissions')
45+
}
46+
}
47+
}, [data])
48+
49+
const { acl } = data || {}
2950

3051
const {
3152
catalogItemIdentity,
3253
collections,
3354
groups
34-
} = acl
55+
} = acl || {}
3556

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

3960
const {
4061
collectionApplicable,
4162
collectionIdentifier,
4263
granuleApplicable,
4364
granuleIdentifier
44-
} = catalogItemIdentity
65+
} = catalogItemIdentity || {}
4566

4667
const {
4768
accessValue: collectionAccessValue,

static/src/js/components/Permission/__tests__/Permission.test.jsx

Lines changed: 143 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { MockedProvider } from '@apollo/client/testing'
22
import {
33
render,
44
screen,
5+
waitFor,
56
within
67
} from '@testing-library/react'
78
import React, { Suspense } from 'react'
@@ -10,14 +11,28 @@ import {
1011
Route,
1112
Routes
1213
} from 'react-router'
14+
15+
import * as router from 'react-router'
16+
1317
import userEvent from '@testing-library/user-event'
1418

1519
import Permission from '@/js/components/Permission/Permission'
1620
import PermissionCollectionTable from '@/js/components/PermissionCollectionTable/PermissionCollectionTable'
1721

1822
import { GET_COLLECTION_PERMISSION } from '@/js/operations/queries/getCollectionPermission'
23+
import { GET_GROUPS } from '@/js/operations/queries/getGroups'
24+
import { GET_AVAILABLE_PROVIDERS } from '@/js/operations/queries/getAvailableProviders'
25+
26+
import useAvailableProviders from '@/js/hooks/useAvailableProviders'
27+
import Providers from '@/js/providers/Providers/Providers'
28+
import NotificationsContext from '@/js/context/NotificationsContext'
1929

2030
vi.mock('../../PermissionCollectionTable/PermissionCollectionTable')
31+
vi.mock('@/js/hooks/useAvailableProviders')
32+
33+
useAvailableProviders.mockReturnValue({
34+
providerIds: ['MMT_1', 'MMT_2']
35+
})
2136

2237
const setup = ({
2338
overrideMocks = false
@@ -85,33 +100,106 @@ const setup = ({
85100

86101
const user = userEvent.setup()
87102

103+
const defaultMocks = [{
104+
request: {
105+
query: GET_AVAILABLE_PROVIDERS,
106+
variables: {
107+
params: {
108+
limit: 500,
109+
permittedUser: undefined,
110+
target: 'PROVIDER_CONTEXT'
111+
}
112+
}
113+
},
114+
result: {
115+
data: {
116+
acls: {
117+
items: [{
118+
conceptId: 'mock-id',
119+
providerIdentity: {
120+
target: 'PROVIDER_CONTEXT',
121+
provider_id: 'MMT_2'
122+
}
123+
}]
124+
}
125+
}
126+
}
127+
},
128+
{
129+
request: {
130+
query: GET_GROUPS,
131+
variables: {
132+
params: {
133+
tags: ['MMT_1', 'MMT_2'],
134+
limit: 500
135+
}
136+
}
137+
},
138+
result: {
139+
data: {
140+
groups: {
141+
__typename: 'GroupList',
142+
count: 1,
143+
items: [
144+
{
145+
__typename: 'Group',
146+
description: 'Test group',
147+
id: '1234-abcd-5678',
148+
members: {
149+
__typename: 'GroupMemberList',
150+
count: 2
151+
},
152+
name: 'Mock group',
153+
tag: 'MMT_2'
154+
}
155+
]
156+
}
157+
158+
}
159+
}
160+
}]
161+
const notificationContext = {
162+
addNotification: vi.fn()
163+
}
164+
88165
render(
89-
<MockedProvider
90-
mocks={overrideMocks || mocks}
91-
>
92-
<MemoryRouter initialEntries={['/permissions/ACL00000-CMR']}>
93-
<Routes>
94-
<Route
95-
path="/permissions"
96-
>
97-
<Route
98-
path=":conceptId"
99-
element={
100-
(
101-
<Suspense>
102-
<Permission />
103-
</Suspense>
104-
)
105-
}
106-
/>
107-
</Route>
108-
</Routes>
109-
</MemoryRouter>
110-
</MockedProvider>
166+
<Providers>
167+
<MockedProvider
168+
mocks={
169+
[
170+
...defaultMocks,
171+
...(overrideMocks || mocks)
172+
]
173+
}
174+
>
175+
<NotificationsContext.Provider value={notificationContext}>
176+
<MemoryRouter initialEntries={['/permissions/ACL00000-CMR']}>
177+
<Routes>
178+
<Route
179+
path="/permissions"
180+
>
181+
<Route
182+
path=":conceptId"
183+
element={
184+
(
185+
<Suspense>
186+
<Permission />
187+
</Suspense>
188+
)
189+
}
190+
/>
191+
</Route>
192+
</Routes>
193+
</MemoryRouter>
194+
</NotificationsContext.Provider>
195+
196+
</MockedProvider>
197+
</Providers>
111198
)
112199

113200
return {
114-
user
201+
user,
202+
notificationContext
115203
}
116204
}
117205

@@ -837,4 +925,36 @@ describe('Permission', () => {
837925
expect(invalidGroup).not.toBeInTheDocument()
838926
})
839927
})
928+
929+
describe('when the ACL is not found', () => {
930+
test('should show a notification and navigate to permissions page', async () => {
931+
const navigateSpy = vi.fn()
932+
933+
vi.spyOn(router, 'useNavigate').mockImplementation(() => navigateSpy)
934+
vi.spyOn(router, 'useParams').mockReturnValue({ conceptId: 'NOT_FOUND_ACL' })
935+
936+
const { notificationContext } = setup({
937+
overrideMocks: [{
938+
request: {
939+
query: GET_COLLECTION_PERMISSION,
940+
variables: { conceptId: 'NOT_FOUND_ACL' }
941+
},
942+
result: {
943+
data: {
944+
acl: null
945+
}
946+
}
947+
}]
948+
})
949+
950+
await waitFor(() => {
951+
expect(notificationContext.addNotification).toHaveBeenCalledWith({
952+
message: 'NOT_FOUND_ACL was not found.',
953+
variant: 'danger'
954+
})
955+
})
956+
957+
expect(navigateSpy).toHaveBeenCalledWith('/permissions')
958+
})
959+
})
840960
})

0 commit comments

Comments
 (0)