diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2298f44e..535a9ca62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,6 @@
## [10.2.0] IN PROGRESS
-
## [10.1.0](https://github.com/folio-org/ui-users/tree/v10.1.0) (2024-03-20)
[Full Changelog](https://github.com/folio-org/ui-users/compare/v10.0.4...v10.1.0)
@@ -49,6 +48,7 @@
* Add `users-keycloak` permissions. Refs UIU-3068.
* Omit permissions accordions and queries when `roles` interface is present. Refs UIU-3061, UIU-3062.
* Retrieve user's central-tenant permission from `users-keycloak` endpoints when available. Refs UIU-3054.
+* Show Roles assigned to users. Refs UIU-3110.
## [10.0.4](https://github.com/folio-org/ui-users/tree/v10.0.4) (2023-11-10)
[Full Changelog](https://github.com/folio-org/ui-users/compare/v10.0.3...v10.0.4)
diff --git a/src/components/RenderRoles/RenderRoles.js b/src/components/RenderRoles/RenderRoles.js
new file mode 100644
index 000000000..3883c60f8
--- /dev/null
+++ b/src/components/RenderRoles/RenderRoles.js
@@ -0,0 +1,109 @@
+import _ from 'lodash';
+import React from 'react';
+import { FormattedMessage, injectIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import {
+ List,
+ Accordion,
+ Badge,
+ Headline,
+ Loading
+} from '@folio/stripes/components';
+
+import AffiliationsSelect from '../AffiliationsSelect/AffiliationsSelect';
+import IfConsortium from '../IfConsortium';
+import IfConsortiumPermission from '../IfConsortiumPermission';
+import { affiliationsShape } from '../../shapes';
+
+class RenderRoles extends React.Component {
+ static propTypes = {
+ accordionId: PropTypes.string,
+ affiliations: affiliationsShape,
+ expanded: PropTypes.bool,
+ heading: PropTypes.node.isRequired,
+ intl: PropTypes.shape({
+ formatMessage: PropTypes.func.isRequired,
+ }),
+ isLoading: PropTypes.bool,
+ listedRoles: PropTypes.arrayOf(PropTypes.object),
+ onChangeAffiliation: PropTypes.func,
+ onToggle: PropTypes.func,
+ permToRead: PropTypes.string.isRequired,
+ selectedAffiliation: PropTypes.string,
+ stripes: PropTypes.shape({
+ hasPerm: PropTypes.func.isRequired,
+ config: PropTypes.shape({
+ showPerms: PropTypes.bool,
+ listInvisiblePerms: PropTypes.bool,
+ }).isRequired,
+ }).isRequired,
+ };
+
+ static defaultProps = {
+ onChangeAffiliation: _.noop,
+ isLoading: false,
+ }
+
+ renderList() {
+ const {
+ listedRoles,
+ } = this.props;
+ const listFormatter = item =>
{item.name};
+ const noPermissionsFound = ;
+
+ return (
+ a.name.localeCompare(b.name))}
+ itemFormatter={listFormatter}
+ isEmptyMessage={noPermissionsFound}
+ />
+ );
+ }
+
+ render() {
+ const {
+ affiliations,
+ accordionId,
+ expanded,
+ isLoading,
+ onChangeAffiliation,
+ onToggle,
+ listedRoles,
+ stripes,
+ permToRead,
+ selectedAffiliation,
+ heading,
+ } = this.props;
+
+ if (!stripes.hasPerm(permToRead)) { return null; }
+
+ return (
+ {heading}}
+ displayWhenClosed={
+ isLoading ? : {listedRoles.length}
+ }
+ >
+
+
+ {Boolean(affiliations?.length) && (
+
+ )}
+
+
+
+ {this.renderList()}
+
+ );
+ }
+}
+
+export default injectIntl(RenderRoles);
diff --git a/src/components/RenderRoles/RenderRoles.test.js b/src/components/RenderRoles/RenderRoles.test.js
new file mode 100644
index 000000000..f38e3dcca
--- /dev/null
+++ b/src/components/RenderRoles/RenderRoles.test.js
@@ -0,0 +1,74 @@
+import renderWithRouter from 'helpers/renderWithRouter';
+import RenderRoles from './RenderRoles';
+
+
+jest.unmock('@folio/stripes/components');
+
+const renderRenderRoles = (props) => renderWithRouter();
+const STRIPES = {
+ config: {},
+ hasPerm: jest.fn().mockReturnValue(true),
+};
+
+const STRIPESWITHOUTPERMISSION = {
+ config: {},
+ hasPerm: jest.fn().mockReturnValue(false),
+};
+
+describe('render RenderRoles component', () => {
+ it('Component must be rendered', () => {
+ const props = {
+ accordionId: 'assignedRoles',
+ expanded: true,
+ onToggle: jest.fn(),
+ heading: Assigned roles
,
+ permToRead: 'perms.permissions.get',
+ listedRoles: [
+ {
+ 'id': '024f7895-45fa-4ea7-ba06-a6a51758559f',
+ 'name': 'funky',
+ 'description': 'get down get down'
+ },
+ {
+ 'id': '27b6cf82-303a-4737-b2d8-c5bc807f077f',
+ 'name': 'chicken',
+ 'description': 'look up look up, the sky is falling!'
+ }
+ ],
+ intl: {},
+ stripes: STRIPES,
+ };
+ renderRenderRoles(props);
+ expect(renderRenderRoles(props)).toBeTruthy();
+ });
+
+ it('Checking for roles', () => {
+ const props = {
+ accordionId: 'assignedRoles',
+ expanded: true,
+ onToggle: jest.fn(),
+ heading: Assigned Permissions
,
+ permToRead: 'perms.permissions.get',
+ listedPermissions: [],
+ intl: {},
+ stripes: STRIPESWITHOUTPERMISSION,
+ };
+ renderRenderRoles(props);
+ expect(renderRenderRoles(props)).toBeTruthy();
+ });
+
+ it('Passing empty props', () => {
+ const props = {
+ accordionId: 'assignedRoles',
+ expanded: true,
+ onToggle: jest.fn(),
+ heading: Assigned Permissions
,
+ permToRead: 'perms.permissions.get',
+ listedRoles: [],
+ intl: {},
+ stripes: STRIPES,
+ };
+ renderRenderRoles(props);
+ expect(renderRenderRoles(props)).toBeTruthy();
+ });
+});
diff --git a/src/components/RenderRoles/index.js b/src/components/RenderRoles/index.js
new file mode 100644
index 000000000..5bbd736ca
--- /dev/null
+++ b/src/components/RenderRoles/index.js
@@ -0,0 +1 @@
+export { default } from './RenderRoles';
diff --git a/src/components/UserDetailSections/UserRoles/UserRoles.js b/src/components/UserDetailSections/UserRoles/UserRoles.js
new file mode 100644
index 000000000..cedcf2002
--- /dev/null
+++ b/src/components/UserDetailSections/UserRoles/UserRoles.js
@@ -0,0 +1,55 @@
+import React, { useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+
+import {
+ useUserAffiliations,
+ useUserTenantRoles,
+} from '../../../hooks';
+import RenderRoles from '../../RenderRoles';
+import { isAffiliationsEnabled } from '../../util';
+
+const propTypes = {
+ stripes: PropTypes.object.isRequired,
+ user: PropTypes.object.isRequired,
+};
+
+const UserRoles = (props) => {
+ const { stripes, user } = props;
+ const { id: userId } = useParams();
+ const [tenantId, setTenantId] = useState(stripes.okapi.tenant);
+
+ const {
+ affiliations,
+ isFetching: isAffiliationsFetching,
+ } = useUserAffiliations({ userId }, { enabled: isAffiliationsEnabled(user) });
+
+ const {
+ userRoles,
+ isFetching: isPermissionsFetching,
+ } = useUserTenantRoles({ userId, tenantId });
+
+ const isLoading = isAffiliationsFetching || isPermissionsFetching;
+
+ useEffect(() => {
+ if (!affiliations.some(({ tenantId: assigned }) => tenantId === assigned)) {
+ setTenantId(stripes.okapi.tenant);
+ }
+ }, [affiliations, stripes.okapi.tenant, tenantId]);
+
+ return (}
+ permToRead="roles.users.collection.get"
+ affiliations={affiliations}
+ selectedAffiliation={tenantId}
+ isLoading={isLoading}
+ onChangeAffiliation={setTenantId}
+ listedRoles={userRoles || []}
+ />);
+};
+
+UserRoles.propTypes = propTypes;
+
+export default UserRoles;
diff --git a/src/components/UserDetailSections/UserRoles/UserRoles.test.js b/src/components/UserDetailSections/UserRoles/UserRoles.test.js
new file mode 100644
index 000000000..f57cf7e6a
--- /dev/null
+++ b/src/components/UserDetailSections/UserRoles/UserRoles.test.js
@@ -0,0 +1,87 @@
+import userEvent from '@folio/jest-config-stripes/testing-library/user-event';
+import { screen } from '@folio/jest-config-stripes/testing-library/react';
+
+import '__mock__/matchMedia.mock';
+import renderWithRouter from 'helpers/renderWithRouter';
+import affiliations from 'fixtures/affiliations';
+import roles from 'fixtures/roles';
+import {
+ useUserAffiliations,
+ useUserTenantRoles,
+} from '../../../hooks';
+import IfConsortiumPermission from '../../IfConsortiumPermission';
+import UserRoles from './UserRoles';
+
+jest.unmock('@folio/stripes/components');
+jest.mock('../../../hooks', () => ({
+ useUserAffiliations: jest.fn(),
+ useUserTenantRoles: jest.fn(),
+}));
+jest.mock('../../IfConsortium', () => jest.fn(({ children }) => <>{children}>));
+jest.mock('../../IfConsortiumPermission', () => jest.fn());
+
+const STRIPES = {
+ config: {},
+ hasPerm: jest.fn().mockReturnValue(true),
+ okapi: {
+ tenant: 'diku',
+ },
+ user: {
+ user: {
+ consortium: {},
+ },
+ },
+};
+
+const defaultProps = {
+ accordionId: 'assignedRoles',
+ expanded: true,
+ onToggle: jest.fn(),
+ heading: User roles
,
+ permToRead: 'perms.permissions.get',
+ intl: {},
+ stripes: STRIPES,
+ user: {},
+};
+
+const renderUserRoles = (props = {}) => renderWithRouter(
+
+);
+
+describe('UserRoles component', () => {
+ beforeEach(() => {
+ useUserAffiliations.mockClear().mockReturnValue({ isFetching: false, affiliations });
+ useUserTenantRoles.mockClear().mockImplementation(({ tenantId }) => ({
+ isFetching: false,
+ userRoles: tenantId === 'diku' ? roles : [],
+ }));
+ });
+
+ it('should render user roles accordion', () => {
+ IfConsortiumPermission.mockReturnValue(null);
+ renderUserRoles();
+
+ expect(screen.getByText('ui-users.roles.userRoles')).toBeInTheDocument();
+ expect(screen.getByText('funky')).toBeInTheDocument();
+ expect(screen.getByText('chicken')).toBeInTheDocument();
+ });
+
+ // describe('Consortia', () => {
+ // it('should update roles list after selecting another affiliation', async () => {
+ // IfConsortiumPermission.mockImplementation(({ children }) => children);
+ // renderUserRoles();
+
+ // expect(screen.getByText('funky')).toBeInTheDocument();
+ // expect(screen.getByText('chicken')).toBeInTheDocument();
+
+ // await userEvent.click(screen.getByText(affiliations[1].tenantName));
+
+ // expect(screen.getByText('funky')).not.toBeInTheDocument();
+ // expect(screen.getByText('chicken')).not.toBeInTheDocument();
+ // expect(screen.getByText('ui-users.roles.empty')).toBeInTheDocument();
+ // });
+ // });
+});
diff --git a/src/components/UserDetailSections/UserRoles/index.js b/src/components/UserDetailSections/UserRoles/index.js
new file mode 100644
index 000000000..0311fe588
--- /dev/null
+++ b/src/components/UserDetailSections/UserRoles/index.js
@@ -0,0 +1 @@
+export { default } from './UserRoles';
diff --git a/src/components/UserDetailSections/index.js b/src/components/UserDetailSections/index.js
index 8daff5148..6d8bbbcec 100644
--- a/src/components/UserDetailSections/index.js
+++ b/src/components/UserDetailSections/index.js
@@ -8,5 +8,6 @@ export { default as UserRequests } from './UserRequests';
export { default as UserAccounts } from './UserAccounts';
export { default as UserAffiliations } from './UserAffiliations';
export { default as UserPermissions } from './UserPermissions';
+export { default as UserRoles } from './UserRoles';
export { default as UserServicePoints } from './UserServicePoints';
export { default as RequestPreferencesView } from './ExtendedInfo/components/RequestPreferencesView';
diff --git a/src/hooks/index.js b/src/hooks/index.js
index 8053b27d3..e14862bec 100644
--- a/src/hooks/index.js
+++ b/src/hooks/index.js
@@ -5,6 +5,7 @@ export { default as useToggle } from './useToggle';
export { default as useUserAffiliations } from './useUserAffiliations';
export { default as useUserAffiliationsMutation } from './useUserAffiliationsMutation';
export { default as useUserTenantPermissions } from './useUserTenantPermissions';
+export { default as useUserTenantRoles } from './useUserTenantRoles';
export { default as useProfilePicture } from './useProfilePicture';
export { default as useLocalizedCurrency } from './useLocalizedCurrency';
diff --git a/src/hooks/useUserTenantRoles/index.js b/src/hooks/useUserTenantRoles/index.js
new file mode 100644
index 000000000..1f6a8f059
--- /dev/null
+++ b/src/hooks/useUserTenantRoles/index.js
@@ -0,0 +1 @@
+export { default } from './useUserTenantRoles';
diff --git a/src/hooks/useUserTenantRoles/useUserTenantRoles.js b/src/hooks/useUserTenantRoles/useUserTenantRoles.js
new file mode 100644
index 000000000..ca2460ec2
--- /dev/null
+++ b/src/hooks/useUserTenantRoles/useUserTenantRoles.js
@@ -0,0 +1,84 @@
+import { useQuery } from 'react-query';
+
+import {
+ useChunkedCQLFetch,
+ useNamespace,
+ useStripes,
+ useOkapiKy,
+} from '@folio/stripes/core';
+
+/**
+ * chunkedRolesReducer
+ * reducer for useChunkedCQLFetch. Given input
+ * [
+ * { data: { roles: [1, 2, 3] } },
+ * { data: { roles: [4, 5, 6] } },
+ * ]
+ * return
+ * [1, 2, 3, 4, 5, 6]
+ *
+ * @param {Array} list of chunks, each item shaped like { data: { roles: [] }}
+ * @returns Array flattened array of role data
+ */
+export const chunkedRolesReducer = (list) => (
+ list.reduce((acc, cur) => {
+ return [...acc, ...(cur?.data?.roles ?? [])];
+ }, []));
+
+const useUserTenantRoles = (
+ { userId, tenantId },
+) => {
+ const stripes = useStripes();
+ const ky = useOkapiKy();
+ const api = ky.extend({
+ hooks: {
+ beforeRequest: [(req) => req.headers.set('X-Okapi-Tenant', tenantId)]
+ }
+ });
+ const [namespace] = useNamespace({ key: 'user-affiliation-roles' });
+
+ const searchParams = {
+ limit: stripes.config.maxUnpagedResourceCount,
+ query: `userId==${userId}`,
+ };
+
+ // retrieve roles assigned to the user to get their IDs...
+ const { data, isSuccess, refetch } = useQuery(
+ [namespace, userId, tenantId],
+ ({ signal }) => {
+ return api.get(
+ 'roles/users',
+ {
+ searchParams,
+ signal,
+ },
+ ).json();
+ },
+ {
+ enabled: Boolean(userId && tenantId),
+ }
+ );
+
+ // ... then retrieve corresponding role objects via chunked fetch
+ // since the list may be long.
+ const ids = isSuccess ? data.userRoles.map(i => i.roleId) : [];
+ const {
+ isFetching,
+ isLoading,
+ items: roles
+ } = useChunkedCQLFetch({
+ endpoint: 'roles',
+ ids,
+ queryEnabled: isSuccess,
+ reduceFunction: chunkedRolesReducer,
+ });
+
+ return {
+ userRoles: roles || [],
+ isFetching,
+ isLoading,
+ refetch
+ };
+};
+
+export default useUserTenantRoles;
diff --git a/src/hooks/useUserTenantRoles/useUserTenantRoles.test.js b/src/hooks/useUserTenantRoles/useUserTenantRoles.test.js
new file mode 100644
index 000000000..ac8ffe0b8
--- /dev/null
+++ b/src/hooks/useUserTenantRoles/useUserTenantRoles.test.js
@@ -0,0 +1,162 @@
+import { QueryClient, QueryClientProvider } from 'react-query';
+import { act, renderHook } from '@folio/jest-config-stripes/testing-library/react';
+
+import {
+ useChunkedCQLFetch,
+ useOkapiKy,
+} from '@folio/stripes/core';
+
+import useUserTenantRoles, { chunkedRolesReducer } from './useUserTenantRoles';
+
+const queryClient = new QueryClient();
+const wrapper = ({ children }) => (
+
+ {children}
+
+);
+
+const userRoleData = {
+ 'userRoles': [
+ {
+ 'userId': 'cchase',
+ 'roleId': 'amigo',
+ },
+ {
+ 'userId': 'cchase',
+ 'roleId': 'clark',
+ },
+ ],
+ 'totalRecords': 2
+};
+
+const roleData = {
+ 'roles': [
+ {
+ 'id': 'clark',
+ 'name': 'Clark Griswold',
+ 'description': 'Father of the year',
+ },
+ {
+ 'id': 'amigo',
+ 'name': 'Bandit',
+ 'description': 'A very thirsty man',
+ }
+ ],
+ 'totalRecords': 2,
+ 'resultInfo': {
+ 'totalRecords': 2
+ }
+};
+
+describe('useUserTenantRoles', () => {
+ const mockUsersTenantRolesGet = jest.fn(() => ({
+ json: () => Promise.resolve(userRoleData),
+ }));
+ const mockRolesGet = jest.fn(() => ({
+ items: roleData,
+ isLoading: false,
+ }));
+
+ beforeEach(() => {
+ queryClient.clear();
+ mockUsersTenantRolesGet.mockClear();
+ mockRolesGet.mockClear();
+ useOkapiKy.mockClear().mockReturnValue({
+ extend: () => ({
+ get: mockUsersTenantRolesGet,
+ })
+ });
+ useChunkedCQLFetch.mockClear().mockReturnValue({
+ items: roleData,
+ isLoading: false,
+ });
+ });
+
+ it('fetches roles assigned to a user', async () => {
+ const { result } = renderHook(() => useUserTenantRoles({ userId: 'u', tenantId: 't' }), { wrapper });
+ await act(() => !result.current.isFetching);
+
+ expect(result.current.isLoading).toBe(false);
+ expect(result.current.userRoles).toEqual(roleData);
+ });
+});
+
+describe('chunkedRolesReducer', () => {
+ it('assembles chunks', () => {
+ const list = [
+ { data: { roles: [1, 2, 3] } },
+ { data: { roles: [4, 5, 6] } },
+ ];
+
+ const result = chunkedRolesReducer(list);
+ expect(result.length).toBe(6);
+ });
+});
+
+
+
+// import { renderHook, waitFor } from '@folio/jest-config-stripes/testing-library/react';
+// import {
+// QueryClient,
+// QueryClientProvider,
+// } from 'react-query';
+
+// import { useOkapiKy, useStripes } from '@folio/stripes/core';
+
+// import roles from 'fixtures/roles';
+// import useUserTenantRoles from './useUserTenantRoles';
+
+// const queryClient = new QueryClient();
+
+// // eslint-disable-next-line react/prop-types
+// const wrapper = ({ children }) => (
+//
+// {children}
+//
+// );
+
+// const response = {
+// roles,
+// totalRecords: roles.length,
+// };
+
+// describe('useUserTenantRoles', () => {
+// const getMock = jest.fn(() => ({
+// json: () => Promise.resolve(response),
+// }));
+// const setHeaderMock = jest.fn();
+// const kyMock = {
+// extend: jest.fn(({ hooks: { beforeRequest } }) => {
+// beforeRequest.forEach(handler => handler({ headers: { set: setHeaderMock } }));
+
+// return {
+// get: getMock,
+// };
+// }),
+// };
+
+// beforeEach(() => {
+// getMock.mockClear();
+
+// useStripes.mockClear().mockReturnValue({
+// okapi: {},
+// config: {
+// maxUnpagedResourceCount: 1000,
+// }
+// });
+// useOkapiKy.mockClear().mockReturnValue(kyMock);
+// });
+
+// it('should fetch user roles for specified tenant', async () => {
+// const options = {
+// userId: 'userId',
+// tenantId: 'tenantId',
+// };
+// const { result } = renderHook(() => useUserTenantRoles(options), { wrapper });
+
+// await waitFor(() => !result.current.isLoading);
+
+// expect(setHeaderMock).toHaveBeenCalledWith('X-Okapi-Tenant', options.tenantId);
+// expect(getMock).toHaveBeenCalledWith('roles/users', expect.objectContaining({}));
+// });
+// });
diff --git a/src/routes/UserRecordContainer.js b/src/routes/UserRecordContainer.js
index 1aeb55468..163b673e5 100644
--- a/src/routes/UserRecordContainer.js
+++ b/src/routes/UserRecordContainer.js
@@ -203,7 +203,12 @@ class UserRecordContainer extends React.Component {
},
settings: {
type: 'okapi',
- path: 'users/configurations/entry',
+ path: (queryParams, pathComponents, resourceData, config, props) => {
+ if (props.stripes.hasInterface('users', '16.1')) {
+ return 'users/configurations/entry';
+ }
+ return null;
+ },
},
requestPreferences: {
type: 'okapi',
diff --git a/src/views/UserDetail/UserDetail.js b/src/views/UserDetail/UserDetail.js
index 75552266c..28bd07d33 100644
--- a/src/views/UserDetail/UserDetail.js
+++ b/src/views/UserDetail/UserDetail.js
@@ -44,6 +44,7 @@ import {
ProxyPermissions,
PatronBlock,
UserPermissions,
+ UserRoles,
UserLoans,
UserRequests,
UserAccounts,
@@ -193,6 +194,7 @@ class UserDetail extends React.Component {
requestsSection: false,
accountsSection: false,
permissionsSection: false,
+ rolesSection: false,
servicePointsSection: false,
notesAccordion: false,
customFields: false,
@@ -857,6 +859,16 @@ class UserDetail extends React.Component {
}
+ { !this.showPermissionsAccordion() &&
+
+ }
+
null),
PatronBlock: jest.fn(() => null),
UserPermissions: jest.fn(() => Permissions accordion
),
+ UserRoles: jest.fn(() => Roles accordion
),
UserLoans: jest.fn(() => null),
UserRequests: jest.fn(() => null),
UserAccounts: jest.fn(() => null),
@@ -284,14 +285,23 @@ describe('UserDetail', () => {
expect(screen.getByRole('button', { name: 'ui-users.details.checkDelete' })).toBeVisible();
});
- describe('permissions', () => {
- it('shows permissions accordion in legacy mode', async () => {
+ describe('when roles interface is absent', () => {
+ it('shows permissions accordion', async () => {
stripes.hasInterface = () => false;
renderUserDetail(stripes);
expect(screen.getByText('Permissions accordion')).toBeTruthy();
});
+ });
+
+ describe('when roles interface is present', () => {
+ it('shows roles accordion', async () => {
+ stripes.hasInterface = () => true;
+ renderUserDetail(stripes);
+ expect(screen.queryByText('Permissions accordion')).not.toBeTruthy();
+ expect(screen.getByText('Roles accordion')).toBeTruthy();
+ });
- it('omits permissions pane when "roles" interface is present', async () => {
+ it('omits permissions accordion', async () => {
stripes.hasInterface = (i) => {
return (i === 'roles');
};
diff --git a/test/jest/__mock__/stripesCore.mock.js b/test/jest/__mock__/stripesCore.mock.js
index c4c3d4210..303efae07 100644
--- a/test/jest/__mock__/stripesCore.mock.js
+++ b/test/jest/__mock__/stripesCore.mock.js
@@ -147,7 +147,8 @@ jest.mock('@folio/stripes/core', () => {
stripesConnect,
useCallout: jest.fn(() => ({ sendCallout: jest.fn() })),
useStripes: jest.fn(() => STRIPES),
- useOkapiKy: jest.fn(() => {}),
+ useOkapiKy: jest.fn(() => ({ extend: jest.fn() })),
+ useChunkedCQLFetch: jest.fn(),
useNamespace: jest.fn(() => ['@folio/users']),
withOkapiKy: jest.fn((Component) => (props) => ),
withStripes:
diff --git a/test/jest/fixtures/roles.json b/test/jest/fixtures/roles.json
new file mode 100644
index 000000000..4426ffc69
--- /dev/null
+++ b/test/jest/fixtures/roles.json
@@ -0,0 +1,12 @@
+[
+ {
+ "id": "024f7895-45fa-4ea7-ba06-a6a51758559f",
+ "name": "funky",
+ "description": "get down get down"
+ },
+ {
+ "id": "27b6cf82-303a-4737-b2d8-c5bc807f077f",
+ "name": "chicken",
+ "description": "look up look up, the sky is falling!"
+ }
+]
diff --git a/translations/ui-users/en.json b/translations/ui-users/en.json
index f719545bf..38af2bba3 100644
--- a/translations/ui-users/en.json
+++ b/translations/ui-users/en.json
@@ -1169,5 +1169,8 @@
"patronNoticePrintJobs.email": "Email",
"patronNoticePrintJobs.updated": "Updated",
"patronNoticePrintJobs.created": "Created",
- "patronNoticePrintJobs.errors.pdf": "'PDF generation failed"
+ "patronNoticePrintJobs.errors.pdf": "'PDF generation failed",
+
+ "roles.userRoles": "Roles",
+ "roles.empty": "No roles found"
}