From fe59adf2b0ba13ce143c138aa65a8a89d70ef265 Mon Sep 17 00:00:00 2001 From: Ryan Berger Date: Fri, 24 Jan 2025 15:32:57 -0500 Subject: [PATCH 1/7] Add ability to add/remove roles for user's affiliations --- CHANGELOG.md | 1 + .../EditUserRoles/EditUserRoles.js | 41 ++++++++++++++++--- src/components/Wrappers/withUserRoles.js | 9 ++-- src/hooks/useAllRolesData/useAllRolesData.js | 9 ++-- src/views/UserEdit/UserEdit.js | 4 ++ src/views/UserEdit/UserForm.js | 3 ++ 6 files changed, 55 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ee6b50b2..3715c298e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * Update fee/fine actions column UX for accessibility. Refs UIU-3027. * Rename permission after BE changes. Refs UIU-3309. * Change import of `exportToCsv` from `stripes-util` to `stripes-components`. Refs UIU-3202. +* Add ability to create/edit role assignments for all of a user's affiliations. Refs UIU-3179. ## [11.0.10](https://github.com/folio-org/ui-users/tree/v11.0.10) (2025-01-10) [Full Changelog](https://github.com/folio-org/ui-users/compare/v11.0.9...v11.0.10) diff --git a/src/components/EditSections/EditUserRoles/EditUserRoles.js b/src/components/EditSections/EditUserRoles/EditUserRoles.js index 9ad7a544b..cb2cda71c 100644 --- a/src/components/EditSections/EditUserRoles/EditUserRoles.js +++ b/src/components/EditSections/EditUserRoles/EditUserRoles.js @@ -1,22 +1,41 @@ -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); + } + refetch(); + }, [affiliations, stripes.okapi.tenant, tenantId]); const changeUserRoles = (roleIds) => { change('assignedRoleIds', roleIds); @@ -101,6 +120,18 @@ function EditUserRoles({ accordionId, form:{ change }, setAssignedRoleIds, assig displayWhenClosed={{assignedRoleIds.length}} > + + + {Boolean(affiliations?.length) && ( + + )} + + {renderUserRoles()} diff --git a/src/components/Wrappers/withUserRoles.js b/src/components/Wrappers/withUserRoles.js index a25317728..9f6693b42 100644 --- a/src/components/Wrappers/withUserRoles.js +++ b/src/components/Wrappers/withUserRoles.js @@ -10,13 +10,14 @@ const withUserRoles = (WrappedComponent) => (props) => { const { okapi, config } = useStripes(); // eslint-disable-next-line react/prop-types const userId = props.match.params.id; + const [tenantId, setTenantId] = useState(okapi.tenant); const [assignedRoleIds, setAssignedRoleIds] = useState([]); const [initialAssignedRoleIds, setInitialAssignedRoleIds] = useState([]); const [isCreateKeycloakUserConfirmationOpen, setIsCreateKeycloakUserConfirmationOpen] = useState(false); const callout = useCallout(); const sendErrorCallout = error => showErrorCallout(error, callout.sendCallout); - const { mutateAsync: createKeycloakUser } = useCreateAuthUserKeycloak(sendErrorCallout, { tenantId: okapi.tenant }); + const { mutateAsync: createKeycloakUser } = useCreateAuthUserKeycloak(sendErrorCallout, { tenantId }); const { isLoading: isAllRolesDataLoading, allRolesMapStructure } = useAllRolesData(); @@ -28,7 +29,7 @@ const withUserRoles = (WrappedComponent) => (props) => { const ky = useOkapiKy(); const api = ky.extend({ hooks: { - beforeRequest: [(req) => req.headers.set('X-Okapi-Tenant', okapi.tenant)] + beforeRequest: [(req) => req.headers.set('X-Okapi-Tenant', tenantId)] } }); @@ -57,7 +58,7 @@ const withUserRoles = (WrappedComponent) => (props) => { }, // Adding api, searchParams to deps causes infinite callback call. Listed deps are enough to track changes. // eslint-disable-next-line react-hooks/exhaustive-deps - [userId, isAllRolesDataLoading, setAssignedRoleIdsOnLoad]); + [userId, isAllRolesDataLoading, setAssignedRoleIdsOnLoad, tenantId]); const updateUserRoles = (roleIds) => api.put( `roles/users/${userId}`, { json: { @@ -116,6 +117,8 @@ const withUserRoles = (WrappedComponent) => (props) => { return { + const { data, isLoading, isSuccess, refetch } = useQuery([namespace, 'user-roles'], () => { return ky.get(`roles?limit=${stripes.config.maxUnpagedResourceCount}&query=cql.allRecords=1 sortby name`).json(); }, { enabled: stripes.hasInterface('roles') }); @@ -32,7 +33,7 @@ function useAllRolesData() { return rolesMap; }, [data]); - return { data, isLoading, allRolesMapStructure, isSuccess }; + return { data, isLoading, allRolesMapStructure, isSuccess, refetch }; } export default useAllRolesData; diff --git a/src/views/UserEdit/UserEdit.js b/src/views/UserEdit/UserEdit.js index 72dde2f43..63ae4ba32 100644 --- a/src/views/UserEdit/UserEdit.js +++ b/src/views/UserEdit/UserEdit.js @@ -459,6 +459,8 @@ class UserEdit extends React.Component { location, match: { params }, isCreateKeycloakUserConfirmationOpen, + setTenantId, + tenantId, setAssignedRoleIds, assignedRoleIds } = this.props; @@ -497,6 +499,8 @@ class UserEdit extends React.Component { isCreateKeycloakUserConfirmationOpen={isCreateKeycloakUserConfirmationOpen} onCancelKeycloakConfirmation={this.onCompleteEdit} confirmCreateKeycloakUser={() => this.props.confirmCreateKeycloakUser(this.onCompleteEdit)} + setTenantId={setTenantId} + tenantId={tenantId} setAssignedRoleIds={setAssignedRoleIds} assignedRoleIds={assignedRoleIds} /> diff --git a/src/views/UserEdit/UserForm.js b/src/views/UserEdit/UserForm.js index 5cb0118e2..e58607ce7 100644 --- a/src/views/UserEdit/UserForm.js +++ b/src/views/UserEdit/UserForm.js @@ -472,6 +472,9 @@ class UserForm extends React.Component { setButtonRef={this.setButtonRef} /> : Date: Thu, 30 Jan 2025 17:22:54 -0500 Subject: [PATCH 2/7] Further progress in multi-tenant --- .../EditUserRoles/EditUserRoles.js | 21 +++++++------ .../components/UserRolesList/UserRolesList.js | 11 ++++--- .../UserRolesModal/UserRolesModal.js | 29 ++++++++++------- .../EditUserRoles/helpers/filtersConfig.js | 4 +-- src/components/Wrappers/withUserRoles.js | 31 ++++++++++++------- src/views/UserEdit/UserEdit.js | 2 +- src/views/UserEdit/UserForm.js | 2 +- 7 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/components/EditSections/EditUserRoles/EditUserRoles.js b/src/components/EditSections/EditUserRoles/EditUserRoles.js index cb2cda71c..328709883 100644 --- a/src/components/EditSections/EditUserRoles/EditUserRoles.js +++ b/src/components/EditSections/EditUserRoles/EditUserRoles.js @@ -33,12 +33,13 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds, useEffect(() => { if (!affiliations.some(({ tenantId: assigned }) => tenantId === assigned)) { setTenantId(stripes.okapi.tenant); + } else { + refetch(); } - refetch(); }, [affiliations, stripes.okapi.tenant, tenantId]); const changeUserRoles = (roleIds) => { - change('assignedRoleIds', roleIds); + change(`assignedRoleIds[${tenantId}]`, roleIds); }; const handleUnassignAllRoles = () => { @@ -47,10 +48,10 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds, }; const listItemsData = useMemo(() => { - if (isEmpty(assignedRoleIds) || isAllRolesDataLoading) return []; + if (isEmpty(assignedRoleIds[tenantId]) || isAllRolesDataLoading) return []; - return assignedRoleIds.map(roleId => { - const foundUserRole = allRolesMapStructure.get(roleId); + return assignedRoleIds[tenantId].map(i => { + const foundUserRole = allRolesMapStructure.get(i.roleId); return { name: foundUserRole?.name, id: foundUserRole?.id }; }); @@ -62,9 +63,10 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds, />; const renderRoleComponent = (fields) => (_, index) => { - if (isEmpty(fields.value)) return null; + const tenantValue = fields.value[tenantId]; + if (isEmpty(tenantValue)) return null; - const roleId = fields.value[index]; + const roleId = tenantValue[index]; const role = allRolesMapStructure.get(roleId); if (!role) return null; @@ -117,7 +119,7 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds, } id={accordionId} - displayWhenClosed={{assignedRoleIds.length}} + displayWhenClosed={{assignedRoleIds[tenantId]?.length}} > @@ -145,6 +147,7 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds, onClose={() => setIsOpen(false)} initialRoleIds={assignedRoleIds} changeUserRoles={changeUserRoles} + tenantId={tenantId} /> { - 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); @@ -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)} /> ), @@ -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' }`; @@ -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, diff --git a/src/components/EditSections/EditUserRoles/components/UserRolesModal/UserRolesModal.js b/src/components/EditSections/EditUserRoles/components/UserRolesModal/UserRolesModal.js index f43c00986..48a6d57b9 100644 --- a/src/components/EditSections/EditUserRoles/components/UserRolesModal/UserRolesModal.js +++ b/src/components/EditSections/EditUserRoles/components/UserRolesModal/UserRolesModal.js @@ -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(() => { @@ -39,23 +39,27 @@ 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(); @@ -69,7 +73,7 @@ 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 }; @@ -103,7 +107,7 @@ export default function UserRolesModal({ isOpen,
- +