Skip to content

Commit

Permalink
[UIU-3179] Add ability to create/edit role assignments for all of a u…
Browse files Browse the repository at this point in the history
…ser's affiliations (#2851)

* Add ability to add/remove roles for user's affiliations

* Further progress in multi-tenant

* Minor fixes

* Fix unit tests and lint

* Cleanup

* Fix tenantId and tests

* Fix role listing and sorting.
  • Loading branch information
ryandberger authored Feb 7, 2025
1 parent 92f5a4a commit 2984439
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 93 deletions.
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

0 comments on commit 2984439

Please sign in to comment.