diff --git a/src/components/EditSections/EditUserRoles/EditUserRoles.js b/src/components/EditSections/EditUserRoles/EditUserRoles.js
index 56d981c2d..e5e581775 100644
--- a/src/components/EditSections/EditUserRoles/EditUserRoles.js
+++ b/src/components/EditSections/EditUserRoles/EditUserRoles.js
@@ -5,11 +5,12 @@ import PropTypes from 'prop-types';
import { isEmpty } from 'lodash';
import { FieldArray } from 'react-final-form-arrays';
import { OnChange } from 'react-final-form-listeners';
+import { useForm } from 'react-final-form';
import { IfPermission, useStripes } from '@folio/stripes/core';
-import { Accordion, Headline, Badge, Row, Col, List, Button, Icon, ConfirmationModal } from '@folio/stripes/components';
+import { Accordion, Headline, Badge, Row, Col, List, Button, Icon, ConfirmationModal, Layout } from '@folio/stripes/components';
-import { useAllRolesData, useUserAffiliations } from '../../../hooks';
+import { useAllRolesData, useUserAffiliationRoles, useUserAffiliations } from '../../../hooks';
import AffiliationsSelect from '../../AffiliationsSelect/AffiliationsSelect';
import IfConsortium from '../../IfConsortium';
import IfConsortiumPermission from '../../IfConsortiumPermission';
@@ -17,8 +18,9 @@ import UserRolesModal from './components/UserRolesModal/UserRolesModal';
import { isAffiliationsEnabled } from '../../util/util';
import { filtersConfig } from './helpers';
-function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds, assignedRoleIds, setTenantId, tenantId }) {
+function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds, assignedRoleIds, setTenantId, tenantId, initialAssignedRoleIds, isLoadingAffiliationRoles }) {
const stripes = useStripes();
+ const form = useForm();
const [isOpen, setIsOpen] = useState(false);
const [unassignModalOpen, setUnassignModalOpen] = useState(false);
const intl = useIntl();
@@ -28,7 +30,19 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds,
isFetching: isAffiliationsFetching,
} = useUserAffiliations({ userId: user.id }, { enabled: isAffiliationsEnabled(user) });
- const { isLoading: isAllRolesDataLoading, allRolesMapStructure, refetch } = useAllRolesData({ tenantId });
+ const {
+ isLoading: isAllRolesDataLoading,
+ isFetching: isAllRolesDataFetching,
+ allRolesMapStructure,
+ refetch,
+ } = useAllRolesData({ tenantId });
+
+ const isLoadingData = (
+ isAffiliationsFetching
+ || isLoadingAffiliationRoles
+ || isAllRolesDataLoading
+ || isAllRolesDataFetching
+ );
useEffect(() => {
if (!affiliations.some(({ tenantId: assigned }) => tenantId === assigned)) {
@@ -38,6 +52,18 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds,
}
}, [affiliations, stripes.okapi.tenant, setTenantId, tenantId, refetch]);
+ // Initialize form field for the current tenant if it doesn't exist yet
+ useEffect(() => {
+ const formState = form.getState();
+ const currentFieldValue = formState.values?.assignedRoleIds?.[tenantId];
+ const hasInitialValue = initialAssignedRoleIds?.[tenantId];
+
+ // If the form field doesn't exist but we have initial values for this tenant, initialize it
+ if (currentFieldValue === undefined && hasInitialValue) {
+ change(`assignedRoleIds.${tenantId}`, initialAssignedRoleIds[tenantId]);
+ }
+ }, [tenantId, initialAssignedRoleIds, form, change]);
+
const changeUserRoles = (roleIds) => {
change(`assignedRoleIds[${tenantId}]`, roleIds);
};
@@ -107,6 +133,14 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds,
};
function renderUserRoles() {
+ if (isLoadingData) {
+ return (
+
+
+
+ );
+ }
+
return (
{renderUserRoles()}
-
-
+
+
diff --git a/src/components/EditSections/EditUserRoles/components/UserRolesModal/UserRolesModal.js b/src/components/EditSections/EditUserRoles/components/UserRolesModal/UserRolesModal.js
index d84840c26..033ebaebf 100644
--- a/src/components/EditSections/EditUserRoles/components/UserRolesModal/UserRolesModal.js
+++ b/src/components/EditSections/EditUserRoles/components/UserRolesModal/UserRolesModal.js
@@ -20,7 +20,7 @@ export default function UserRolesModal({ isOpen,
const [submittedSearchTerm, setSubmittedSearchTerm] = useState('');
const [assignedRoleIds, setAssignedRoleIds] = useState({});
const { filters, onChangeFilter, onClearFilter, resetFilters } = useRolesModalFilters();
- const { data: allRolesData, allRolesMapStructure } = useAllRolesData();
+ const { data: allRolesData, allRolesMapStructure } = useAllRolesData({ tenantId });
useEffect(() => {
setAssignedRoleIds(initialRoleIds);
diff --git a/src/components/Wrappers/withUserRoles.js b/src/components/Wrappers/withUserRoles.js
index 5aa155b69..cb506f3a4 100644
--- a/src/components/Wrappers/withUserRoles.js
+++ b/src/components/Wrappers/withUserRoles.js
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useStripes, useOkapiKy, useCallout } from '@folio/stripes/core';
import isEqual from 'lodash/isEqual';
+import isEmpty from 'lodash/isEmpty';
import { useCreateAuthUserKeycloak, useUserAffiliationRoles } from '../../hooks';
import { KEYCLOAK_USER_EXISTANCE } from '../../constants';
import { showErrorCallout } from '../../views/UserEdit/UserEditHelpers';
@@ -10,8 +11,9 @@ const withUserRoles = (WrappedComponent) => (props) => {
const { okapi } = useStripes();
// eslint-disable-next-line react/prop-types
const userId = props.match.params.id;
- const initialAssignedRoleIds = useUserAffiliationRoles(userId);
+ const [initialAssignedRoleIds, setInitialAssignedRoleIds] = useState({});
const [tenantId, setTenantId] = useState(okapi.tenant);
+ const { userRoleIds, isLoading: isLoadingAffiliationRoles } = useUserAffiliationRoles(userId, tenantId);
const [assignedRoleIds, setAssignedRoleIds] = useState({});
const [isCreateKeycloakUserConfirmationOpen, setIsCreateKeycloakUserConfirmationOpen] = useState(false);
const callout = useCallout();
@@ -26,17 +28,33 @@ const withUserRoles = (WrappedComponent) => (props) => {
}
});
- const stringifiedInitialAssignedRoleIds = JSON.stringify(initialAssignedRoleIds);
-
useEffect(() => {
- setAssignedRoleIds(initialAssignedRoleIds);
- // on each re-render reference to initialAssignedRoleIds are different, so putting initialAssignedRoleIds to deps causes infinite trigger
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [stringifiedInitialAssignedRoleIds]);
+ // No need to set roles if there are empty or loading
+ if (!userRoleIds.length) return;
+
+ setInitialAssignedRoleIds(prev => ({
+ ...prev,
+ [tenantId]: userRoleIds,
+ }));
+
+
+ // Set assigned roles only if they are not set for the tenant yet
+ if (isEmpty(assignedRoleIds[tenantId])) {
+ setAssignedRoleIds(prev => ({
+ ...prev,
+ [tenantId]: userRoleIds,
+ }));
+ }
+ }, [userRoleIds]);
const updateUserRoles = async (roleIds) => {
// to update roles for different tenants, we need to make API requests for each tenant
const requests = Object.keys(roleIds).map((tenantIdKey) => {
+ // No need to make API call if roles didn't change for the tenant
+ if (isEqual(roleIds[tenantIdKey], initialAssignedRoleIds[tenantIdKey])) {
+ return Promise.resolve();
+ }
+
const putApi = ky.extend({
hooks: {
beforeRequest: [(req) => req.headers.set('X-Okapi-Tenant', tenantIdKey)]
@@ -131,6 +149,7 @@ const withUserRoles = (WrappedComponent) => (props) => {
initialAssignedRoleIds={initialAssignedRoleIds}
checkAndHandleKeycloakAuthUser={checkAndHandleKeycloakAuthUser}
confirmCreateKeycloakUser={confirmCreateKeycloakUser}
+ isLoadingAffiliationRoles={isLoadingAffiliationRoles}
/>;
};
diff --git a/src/hooks/useAllRolesData/useAllRolesData.js b/src/hooks/useAllRolesData/useAllRolesData.js
index 913a8cf14..918934dbc 100644
--- a/src/hooks/useAllRolesData/useAllRolesData.js
+++ b/src/hooks/useAllRolesData/useAllRolesData.js
@@ -14,15 +14,18 @@ import { useQuery } from 'react-query';
*/
function useAllRolesData(options = {}) {
- const { tenantId } = options;
+ const {
+ tenantId,
+ enabled = true,
+ } = options;
const stripes = useStripes();
const ky = useOkapiKy({ tenant: tenantId || stripes.okapi.tenant });
- const [namespace] = useNamespace();
+ const [namespace] = useNamespace({ key: 'tenant-roles' });
- const { data, isLoading, isSuccess, refetch } = useQuery([namespace, 'user-roles'], () => {
+ const { data, isLoading, isSuccess, refetch, isFetching } = useQuery([namespace, tenantId], () => {
return ky.get(`roles?limit=${stripes.config.maxUnpagedResourceCount}&query=cql.allRecords=1 sortby name`).json();
- }, { enabled: stripes.hasInterface('roles') });
+ }, { enabled: stripes.hasInterface('roles') && enabled });
const allRolesMapStructure = useMemo(() => {
const rolesMap = new Map();
@@ -33,7 +36,7 @@ function useAllRolesData(options = {}) {
return rolesMap;
}, [data]);
- return { data, isLoading, allRolesMapStructure, isSuccess, refetch };
+ return { data, isLoading, allRolesMapStructure, isSuccess, refetch, isFetching };
}
export default useAllRolesData;
diff --git a/src/hooks/useUserAffiliationRoles/useUserAffiliationRoles.js b/src/hooks/useUserAffiliationRoles/useUserAffiliationRoles.js
index 719d53460..729d2500a 100644
--- a/src/hooks/useUserAffiliationRoles/useUserAffiliationRoles.js
+++ b/src/hooks/useUserAffiliationRoles/useUserAffiliationRoles.js
@@ -1,67 +1,51 @@
-import { useStripes, useOkapiKy } from '@folio/stripes/core';
-import { useQueries } from 'react-query';
+import { useMemo } from 'react';
+import { useQuery } from 'react-query';
-function useUserAffiliationRoles(userId) {
+import { useStripes, useOkapiKy, useNamespace } from '@folio/stripes/core';
+
+import useAllRolesData from '../useAllRolesData/useAllRolesData';
+
+const DEFAULT = [];
+
+function useUserAffiliationRoles(userId, tenantId) {
const stripes = useStripes();
+ const [namespace] = useNamespace({ key: 'user-affiliation-roles' });
+ const ky = useOkapiKy({ tenant: tenantId });
- const searchParams = {
- limit: stripes.config.maxUnpagedResourceCount,
- query: `userId==${userId}`,
- };
+ const hasViewRolesPermission = stripes.hasPerm('ui-users.roles.view');
- // To unify in case if consortium of non-consortium
- let tenants = stripes.user.user?.tenants || [{ id: stripes.okapi.tenant }];
- // Only make API calls if user has permission to view roles
- tenants = stripes.hasPerm('ui-users.roles.view') ? tenants : [];
- const ky = useOkapiKy();
+ const {
+ isLoading: isAllRolesDataLoading,
+ isFetching: isAllRolesDataFetching,
+ allRolesMapStructure,
+ } = useAllRolesData({ tenantId, enabled: hasViewRolesPermission });
- const userTenantRolesQueries = useQueries(
- tenants.map(({ id }) => {
- return {
- queryKey:['userTenantRoles', id],
- queryFn:() => {
- const api = ky.extend({
- hooks: {
- beforeRequest: [(req) => req.headers.set('X-Okapi-Tenant', id)]
- }
- });
- return api.get('roles/users', { searchParams }).json();
- },
- enabled: Boolean(userId)
- };
- })
+ const {
+ data,
+ isLoading: isUserRolesLoading,
+ isFetching: isUserRolesFetching,
+ } = useQuery(
+ [namespace, userId, tenantId],
+ () => ky.get(`roles/users/${userId}`).json(),
+ {
+ enabled: Boolean(userId && tenantId && hasViewRolesPermission),
+ }
);
- // Since `roles/users` return doesn't include names (only ids) for the roles, and we need them sorted by role name,
- // we need to retrieve all the records for roles and use them to determine the sequence of ids.
- const tenantRolesQueries = useQueries(
- tenants.map(({ id }) => {
- return {
- queryKey:['tenantRolesAllRecords', id],
- queryFn:() => {
- const api = ky.extend({
- hooks: {
- beforeRequest: [(req) => req.headers.set('X-Okapi-Tenant', id)]
- }
- });
- return api.get(`roles?limit=${stripes.config.maxUnpagedResourceCount}&query=cql.allRecords=1 sortby name`).json();
- },
- };
- })
- );
+ const userRoleIds = useMemo(() => {
+ if (!data?.userRoles || !allRolesMapStructure.size) return DEFAULT;
+
+ return data.userRoles
+ .map(({ roleId }) => allRolesMapStructure.get(roleId))
+ .toSorted((a, b) => a.name.localeCompare(b.name))
+ .map(({ id }) => id);
+ }, [data?.userRoles, allRolesMapStructure]);
- // result from useQueries doesn’t provide information about the tenants, reach appropriate tenant using index
- // useQueries guarantees that the results come in the same order as provided [queryFns]
- return tenants.reduce((acc, tenant, index) => {
- const roleIds = userTenantRolesQueries[index].data?.userRoles.map(d => d.roleId) || [];
- const assignedRoles = [];
- roleIds.forEach(roleId => {
- const found = tenantRolesQueries[index].data?.roles.find(r => r.id === roleId);
- if (found) assignedRoles.push(found);
- });
- acc[tenant.id] = [...assignedRoles].sort((a, b) => a.name.localeCompare(b.name)).map(({ id }) => id);
- return acc;
- }, {});
+ return {
+ userRoleIds,
+ isLoading: isUserRolesLoading || isAllRolesDataLoading,
+ isFetching: isUserRolesFetching || isAllRolesDataFetching,
+ };
}
export default useUserAffiliationRoles;
diff --git a/src/views/UserEdit/UserEdit.js b/src/views/UserEdit/UserEdit.js
index 4f334f186..8c9011232 100644
--- a/src/views/UserEdit/UserEdit.js
+++ b/src/views/UserEdit/UserEdit.js
@@ -462,6 +462,7 @@ class UserEdit extends React.Component {
setTenantId,
tenantId,
setAssignedRoleIds,
+ isLoadingAffiliationRoles,
assignedRoleIds
} = this.props;
@@ -503,6 +504,7 @@ class UserEdit extends React.Component {
tenantId={tenantId}
setAssignedRoleIds={setAssignedRoleIds}
assignedRoleIds={assignedRoleIds}
+ isLoadingAffiliationRoles={isLoadingAffiliationRoles}
/>
);
}
diff --git a/src/views/UserEdit/UserForm.js b/src/views/UserEdit/UserForm.js
index a1f2689b2..bd025bfcc 100644
--- a/src/views/UserEdit/UserForm.js
+++ b/src/views/UserEdit/UserForm.js
@@ -336,6 +336,7 @@ class UserForm extends React.Component {
uniquenessValidator,
profilePictureConfig,
isCreateKeycloakUserConfirmationOpen,
+ isLoadingAffiliationRoles,
onCancelKeycloakConfirmation
} = this.props;
const selectedPatronGroup = form.getFieldState('patronGroup')?.value;
@@ -485,6 +486,8 @@ class UserForm extends React.Component {
setAssignedRoleIds={this.props.setAssignedRoleIds}
assignedRoleIds={this.props.assignedRoleIds}
accordionId="userRoles"
+ initialAssignedRoleIds={initialValues.assignedRoleIds}
+ isLoadingAffiliationRoles={isLoadingAffiliationRoles}
/>
}
@@ -542,4 +545,5 @@ export default stripesFinalForm({
initialValuesEqual: (a, b) => isEqual(a, b),
navigationCheck: true,
enableReinitialize: true,
+ keepDirtyOnReinitialize: true,
})(injectIntl(UserForm));