Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UIU-3179] Add ability to create/edit role assignments for all of a user's affiliations #2851

Merged
merged 10 commits into from
Feb 7, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* *BREAKING* Add `pronouns` field to user edit form. Refs UIU-3119.
* Add `pronouns` field to user view form. Refs UIU-3118.
* Pronouns Field - add Character Limit Warning. Refs UIU-3324.
* Add ability to create/edit role assignments for all of a user's affiliations. Refs UIU-3179.

## [11.0.11](https://github.com/folio-org/ui-users/tree/v11.0.11) (2025-01-15)
[Full Changelog](https://github.com/folio-org/ui-users/compare/v11.0.10...v11.0.11)
Expand Down
71 changes: 56 additions & 15 deletions src/components/EditSections/EditUserRoles/EditUserRoles.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useIntl, FormattedMessage } from 'react-intl';
import { withRouter } from 'react-router';
import PropTypes from 'prop-types';
import { isEmpty } from 'lodash';
import { FieldArray } from 'react-final-form-arrays';
import { OnChange } from 'react-final-form-listeners';
import { IfPermission } from '@folio/stripes/core';

import { IfPermission, useStripes } from '@folio/stripes/core';
import { Accordion, Headline, Badge, Row, Col, List, Button, Icon, ConfirmationModal } from '@folio/stripes/components';
import { useAllRolesData } from '../../../hooks';

import { useAllRolesData, useUserAffiliations } from '../../../hooks';
import AffiliationsSelect from '../../AffiliationsSelect/AffiliationsSelect';
import IfConsortium from '../../IfConsortium';
import IfConsortiumPermission from '../../IfConsortiumPermission';
import UserRolesModal from './components/UserRolesModal/UserRolesModal';
import { isAffiliationsEnabled } from '../../util/util';
import { filtersConfig } from './helpers';

function EditUserRoles({ accordionId, form:{ change }, setAssignedRoleIds, assignedRoleIds }) {
function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds, assignedRoleIds, setTenantId, tenantId }) {
const stripes = useStripes();
const [isOpen, setIsOpen] = useState(false);
const [unassignModalOpen, setUnassignModalOpen] = useState(false);
const intl = useIntl();

const { isLoading: isAllRolesDataLoading, allRolesMapStructure } = useAllRolesData();
const {
affiliations,
isFetching: isAffiliationsFetching,
} = useUserAffiliations({ userId: user.id }, { enabled: isAffiliationsEnabled(user) });

const { isLoading: isAllRolesDataLoading, allRolesMapStructure, refetch } = useAllRolesData({ tenantId });

useEffect(() => {
if (!affiliations.some(({ tenantId: assigned }) => tenantId === assigned)) {
setTenantId(stripes.okapi.tenant);
} else {
refetch();
}
}, [affiliations, stripes.okapi.tenant, setTenantId, tenantId, refetch]);

const changeUserRoles = (roleIds) => {
change('assignedRoleIds', roleIds);
change(`assignedRoleIds[${tenantId}]`, roleIds);
};

const handleUnassignAllRoles = () => {
Expand All @@ -28,24 +48,29 @@ function EditUserRoles({ accordionId, form:{ change }, setAssignedRoleIds, assig
};

const listItemsData = useMemo(() => {
if (isEmpty(assignedRoleIds) || isAllRolesDataLoading) return [];
if (isEmpty(assignedRoleIds[tenantId]) || isAllRolesDataLoading) return [];

return assignedRoleIds.map(roleId => {
const mappedRoleIds = [];
assignedRoleIds[tenantId].forEach(roleId => {
const foundUserRole = allRolesMapStructure.get(roleId);

return { name: foundUserRole?.name, id: foundUserRole?.id };
if (foundUserRole) {
mappedRoleIds.push({ name: foundUserRole.name, id: foundUserRole.id });
}
});
}, [assignedRoleIds, isAllRolesDataLoading, allRolesMapStructure]);
return !isEmpty(mappedRoleIds) ? mappedRoleIds.sort((a, b) => a.name.localeCompare(b.name)) : [];
}, [assignedRoleIds, isAllRolesDataLoading, allRolesMapStructure, tenantId]);

const unassignAllMessage = <FormattedMessage
id="ui-users.roles.modal.unassignAll.label"
values={{ roles: listItemsData.map(d => d.name).join(', ') }}
/>;

const renderRoleComponent = (fields) => (_, index) => {
if (isEmpty(fields.value)) return null;
const tenantValue = fields.value;
if (isEmpty(tenantValue)) return null;

const roleId = fields.value[index];
const roleId = tenantValue[index];
const role = allRolesMapStructure.get(roleId);

if (!role) return null;
Expand Down Expand Up @@ -85,7 +110,7 @@ function EditUserRoles({ accordionId, form:{ change }, setAssignedRoleIds, assig
return (
<Col xs={12}>
<FieldArray
name="assignedRoleIds"
name={`assignedRoleIds.${tenantId}`}
component={renderUserRolesComponent}
/>
</Col>
Expand All @@ -98,9 +123,21 @@ function EditUserRoles({ accordionId, form:{ change }, setAssignedRoleIds, assig
<Accordion
label={<Headline size="large" tag="h3"><FormattedMessage id="ui-users.roles.userRoles" /></Headline>}
id={accordionId}
displayWhenClosed={<Badge>{assignedRoleIds.length}</Badge>}
displayWhenClosed={<Badge>{assignedRoleIds[tenantId]?.length}</Badge>}
>
<Row>
<IfConsortium>
<IfConsortiumPermission perm="consortia.user-tenants.collection.get">
{Boolean(affiliations?.length) && (
<AffiliationsSelect
affiliations={affiliations}
onChange={setTenantId}
isLoading={isAllRolesDataLoading || isAffiliationsFetching}
value={tenantId}
/>
)}
</IfConsortiumPermission>
</IfConsortium>
{renderUserRoles()}
<IfPermission perm="ui-authorization-roles.users.settings.manage">
<Button data-testid="add-roles-button" onClick={() => setIsOpen(true)}><FormattedMessage id="ui-users.roles.addRoles" /></Button>
Expand All @@ -114,6 +151,7 @@ function EditUserRoles({ accordionId, form:{ change }, setAssignedRoleIds, assig
onClose={() => setIsOpen(false)}
initialRoleIds={assignedRoleIds}
changeUserRoles={changeUserRoles}
tenantId={tenantId}
/>
<ConfirmationModal
open={unassignModalOpen}
Expand All @@ -139,8 +177,11 @@ EditUserRoles.propTypes = {
match: PropTypes.shape({ params: { id: PropTypes.string } }),
accordionId: PropTypes.string,
form: PropTypes.object.isRequired,
assignedRoleIds: PropTypes.arrayOf(PropTypes.string).isRequired,
user: PropTypes.object.isRequired,
assignedRoleIds: PropTypes.object.isRequired,
setAssignedRoleIds: PropTypes.func.isRequired,
tenantId: PropTypes.string.isRequired,
setTenantId: PropTypes.func.isRequired
};

export default withRouter(EditUserRoles);
38 changes: 32 additions & 6 deletions src/components/EditSections/EditUserRoles/EditUserRoles.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,24 @@ import {
useStripes,
} from '@folio/stripes/core';
import { Form } from 'react-final-form';
import affiliations from 'fixtures/affiliations';
import EditUserRoles from './EditUserRoles';

import { useAllRolesData } from '../../../hooks';
import {
useAllRolesData,
useConsortiumTenants,
useUserAffiliations
} from '../../../hooks';


jest.mock('../../IfConsortium', () => jest.fn(({ children }) => <>{children}</>));
jest.mock('../../IfConsortiumPermission', () => jest.fn().mockReturnValue(null));

jest.mock('../../../hooks', () => ({
...jest.requireActual('../../../hooks'),
useAllRolesData: jest.fn()
useAllRolesData: jest.fn(),
useConsortiumTenants: jest.fn(),
useUserAffiliations: jest.fn(),
}));

jest.mock('@folio/stripes/core', () => ({
Expand All @@ -25,10 +36,10 @@ jest.mock('@folio/stripes/core', () => ({

jest.unmock('@folio/stripes/components');


const STRIPES = {
config: {},
hasPerm: jest.fn().mockReturnValue(true),
hasInterface: jest.fn().mockReturnValue(true),
okapi: {
tenant: 'diku',
},
Expand Down Expand Up @@ -74,7 +85,7 @@ const arrayMutators = {
const renderEditRolesAccordion = (props) => {
const component = () => <EditUserRoles {...props} />;
return renderWithRouter(<Form
initialValues={{ assignedRoleIds: ['1', '2'] }}
initialValues={{ assignedRoleIds: { 'consortium': ['1', '2'] } }}
id="form-user"
mutators={{
...arrayMutators
Expand All @@ -89,12 +100,27 @@ const propsData = {
form: {
change: mockChangeFunction,
},
assignedRoleIds: ['1', '2', '3'],
assignedRoleIds: { 'consortium': ['1', '2'] },
setAssignedRoleIds: jest.fn(),
user: {
id: '1'
},
setTenantId: jest.fn(),
tenantId: 'consortium'
};

describe('EditUserRoles Component', () => {
beforeEach(() => {
useConsortiumTenants
.mockClear()
.mockReturnValue({
tenants: affiliations.map(({ tenantId, tenantName }) => ({ id: tenantId, name: tenantName })),
isLoading: false,
});
useUserAffiliations
.mockClear()
.mockReturnValue({ isLoading: false, affiliations });

useStripes.mockClear().mockReturnValue(STRIPES);
useAllRolesData.mockClear().mockReturnValue(mockAllRolesData);
IfPermission.mockImplementation(({ children }) => children);
Expand Down Expand Up @@ -167,6 +193,6 @@ describe('EditUserRoles Component', () => {
const confirmButton = document.querySelector('[data-test-confirmation-modal-confirm-button="true"]');
await userEvent.click(confirmButton);

expect(mockChangeFunction).toHaveBeenCalledWith('assignedRoleIds', []);
expect(mockChangeFunction).toHaveBeenCalledWith('assignedRoleIds[consortium]', []);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ const visibleColumns = ['selected', 'roleName', 'status'];
const UserRolesList = ({ assignedUserRoleIds,
filteredRoles,
toggleRole,
toggleAllRoles }) => {
const allChecked = filteredRoles.every(filteredRole => assignedUserRoleIds.includes(filteredRole.id));
toggleAllRoles,
tenantId }) => {
const allChecked = filteredRoles.every(filteredRole => assignedUserRoleIds[tenantId]?.includes(filteredRole.id));

const handleToggleAllRoles = (event) => toggleAllRoles(event.target.checked);

Expand Down Expand Up @@ -45,7 +46,7 @@ const UserRolesList = ({ assignedUserRoleIds,
permissionName={role.permissionName}
value={role.id}
// eslint-disable-next-line react/prop-types
checked={assignedUserRoleIds.includes(role.id)}
checked={assignedUserRoleIds[tenantId]?.includes(role.id)}
onChange={() => toggleRole(role.id)}
/>
),
Expand All @@ -58,7 +59,7 @@ const UserRolesList = ({ assignedUserRoleIds,
status: role => {
const statusText = `ui-users.roles.modal.${
// eslint-disable-next-line react/prop-types
assignedUserRoleIds.includes(role.id)
assignedUserRoleIds[tenantId]?.includes(role.id)
? 'assigned'
: 'unassigned'
}`;
Expand All @@ -72,7 +73,7 @@ const UserRolesList = ({ assignedUserRoleIds,
};

UserRolesList.propTypes = {
assignedUserRoleIds: PropTypes.arrayOf(PropTypes.string).isRequired,
assignedUserRoleIds: PropTypes.object.isRequired,
filteredRoles: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
Expand All @@ -81,6 +82,7 @@ UserRolesList.propTypes = {
).isRequired,
toggleRole: PropTypes.func.isRequired,
toggleAllRoles: PropTypes.func.isRequired,
tenantId: PropTypes.string.isRequired
};

export default UserRolesList;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import UserRolesList from './UserRolesList';

jest.unmock('@folio/stripes/components');

const assignedUserRoleIds = ['1', '2'];
const tenantId = 'consortium';
const assignedUserRoleIds = { 'consortium': ['1', '2'] };
const filteredRoles = [{ id: '1', name: 'role1' }];
const mockToggleRole = jest.fn();
const mockToggleAllRoles = jest.fn();
Expand All @@ -13,7 +14,7 @@ const renderComponent = (props) => render(<UserRolesList {...props} />);

describe('UserRolesList', () => {
beforeEach(() => {
renderComponent({ assignedUserRoleIds, filteredRoles, toggleRole:mockToggleRole, toggleAllRoles:mockToggleAllRoles });
renderComponent({ assignedUserRoleIds, filteredRoles, toggleRole:mockToggleRole, toggleAllRoles:mockToggleAllRoles, tenantId });
});
afterAll(() => {
cleanup();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import useRolesModalFilters from './useRolesModalFilters';
export default function UserRolesModal({ isOpen,
onClose,
changeUserRoles,
initialRoleIds }) {
initialRoleIds,
tenantId }) {
const [filterPaneIsVisible, setFilterPaneIsVisible] = useState(true);
const [submittedSearchTerm, setSubmittedSearchTerm] = useState('');
const [assignedRoleIds, setAssignedRoleIds] = useState([]);
const [assignedRoleIds, setAssignedRoleIds] = useState({});
const { filters, onChangeFilter, onClearFilter, resetFilters } = useRolesModalFilters();

const { data: allRolesData, allRolesMapStructure } = useAllRolesData();

useEffect(() => {
Expand All @@ -39,23 +39,26 @@ export default function UserRolesModal({ isOpen,
let filtered = cloneDeep(allRolesData.roles);
[filtersConfig].forEach((filterData) => {
// eslint-disable-next-line no-unused-vars
filtered = filterData.filter(filtered, filters, assignedRoleIds);
filtered = filterData.filter(filtered, filters, assignedRoleIds, tenantId);
});

return filtered.filter(role => role.name.trim().toLowerCase().includes(submittedSearchTerm.trim().toLowerCase()));
};

const toggleRole = (id) => {
if (assignedRoleIds.includes(id)) {
setAssignedRoleIds(assignedRoleIds.filter(roleId => roleId !== id));
if (assignedRoleIds[tenantId]?.includes(id)) {
setAssignedRoleIds({ ...assignedRoleIds, [tenantId]: assignedRoleIds[tenantId].filter(role => role !== id) });
} else {
setAssignedRoleIds([...assignedRoleIds, id]);
setAssignedRoleIds({ ...assignedRoleIds, [tenantId]: assignedRoleIds[tenantId].concat(id) });
}
};

const toggleAllRoles = (checked) => {
if (checked) setAssignedRoleIds(allRolesData?.roles.map(role => role.id));
else setAssignedRoleIds([]);
if (checked) {
setAssignedRoleIds({ ...assignedRoleIds, [tenantId]: allRolesData?.roles.map(role => role.id) });
} else {
setAssignedRoleIds({ ...assignedRoleIds, [tenantId]: [] });
}
};

const filteredRoles = getFilteredRoles();
Expand All @@ -69,12 +72,12 @@ export default function UserRolesModal({ isOpen,
};

const handleSaveClick = () => {
const sortedAlphabetically = assignedRoleIds
const sortedAlphabetically = assignedRoleIds[tenantId]
.map(id => {
const foundRole = allRolesMapStructure.get(id);
return { name: foundRole?.name, id: foundRole?.id };
})
.sort((a, b) => a.name.localeCompare(b.name))
.sort((a, b) => a.name?.localeCompare(b.name))
.map(r => r.id);
changeUserRoles(sortedAlphabetically);
onClose();
Expand Down Expand Up @@ -103,7 +106,7 @@ export default function UserRolesModal({ isOpen,
<div>
<FormattedMessage
id="ui-users.permissions.modal.total"
values={{ count: assignedRoleIds.length }}
values={{ count: assignedRoleIds[tenantId]?.length }}
/>
</div>
<Button
Expand Down Expand Up @@ -170,6 +173,7 @@ export default function UserRolesModal({ isOpen,
filteredRoles={filteredRoles}
toggleRole={toggleRole}
toggleAllRoles={toggleAllRoles}
tenantId={tenantId}
/>
</Pane>
</Paneset>
Expand All @@ -181,6 +185,7 @@ export default function UserRolesModal({ isOpen,
UserRolesModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
initialRoleIds: PropTypes.arrayOf(PropTypes.string),
initialRoleIds: PropTypes.object,
changeUserRoles: PropTypes.func.isRequired,
tenantId: PropTypes.string.isRequired,
};
Loading
Loading