Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions src/components/EditSections/EditUserRoles/EditUserRoles.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { FieldArray } from 'react-final-form-arrays';
import { OnChange } from 'react-final-form-listeners';

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 AffiliationsSelect from '../../AffiliationsSelect/AffiliationsSelect';
Expand All @@ -17,7 +17,7 @@ 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, isLoadingAffiliationRoles }) {
const stripes = useStripes();
const [isOpen, setIsOpen] = useState(false);
const [unassignModalOpen, setUnassignModalOpen] = useState(false);
Expand All @@ -28,7 +28,14 @@ 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, allRolesMapStructure, refetch, isFetching: isAllRolesDataFetching } = useAllRolesData({ tenantId });

const isLoadingData = (
isAffiliationsFetching
|| isLoadingAffiliationRoles
|| isAllRolesDataLoading
|| isAllRolesDataFetching
);

useEffect(() => {
if (!affiliations.some(({ tenantId: assigned }) => tenantId === assigned)) {
Expand Down Expand Up @@ -66,12 +73,11 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds,
values={{ roles: listItemsData.map(d => d.name).join(', ') }}
/>;

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

const roleId = tenantValue[index];
const role = allRolesMapStructure.get(roleId);
const fieldIndex = tenantValue.indexOf(role.id);

if (!role) return null;
return (
Expand All @@ -87,7 +93,7 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds,
type="button"
id={`clickable-remove-user-role-${role.id}`}
aria-label={`${intl.formatMessage({ id:'ui-users.roles.deleteRole' })}: ${role.name}`}
onClick={() => fields.remove(index)}
onClick={() => fields.remove(fieldIndex)}
>
<Icon icon="times-circle" />
</Button>
Expand All @@ -107,6 +113,14 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds,
};

function renderUserRoles() {
if (isLoadingData) {
return (
<Layout className="full padding-bottom-gutter">
<Icon icon="spinner-ellipsis" />
</Layout>
);
}

return (
<Col xs={12}>
<FieldArray
Expand Down Expand Up @@ -140,8 +154,8 @@ function EditUserRoles({ accordionId, form:{ change }, user, setAssignedRoleIds,
</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>
<Button data-testid="unassign-all-roles-button" disabled={isEmpty(listItemsData)} onClick={() => setUnassignModalOpen(true)}><FormattedMessage id="ui-users.roles.unassignAllRoles" /></Button>
<Button disabled={isLoadingData} data-testid="add-roles-button" onClick={() => setIsOpen(true)}><FormattedMessage id="ui-users.roles.addRoles" /></Button>
<Button data-testid="unassign-all-roles-button" disabled={isEmpty(listItemsData) || isLoadingData} onClick={() => setUnassignModalOpen(true)}><FormattedMessage id="ui-users.roles.unassignAllRoles" /></Button>
</IfPermission>
</Row>
</Accordion>
Expand Down
14 changes: 13 additions & 1 deletion src/components/Wrappers/withUserRoles.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ import { showErrorCallout } from '../../views/UserEdit/UserEditHelpers';
const withUserRoles = (WrappedComponent) => (props) => {
const { okapi } = useStripes();
// eslint-disable-next-line react/prop-types
const user = props.resources?.selUser?.records?.[0] || {};
// eslint-disable-next-line react/prop-types
const userId = props.match.params.id;
const initialAssignedRoleIds = useUserAffiliationRoles(userId);
const {
userRoles: initialAssignedRoleIds,
isLoading: isLoadingAffiliationRoles,
} = useUserAffiliationRoles(userId, user);

const [tenantId, setTenantId] = useState(okapi.tenant);
const [assignedRoleIds, setAssignedRoleIds] = useState({});
const [isCreateKeycloakUserConfirmationOpen, setIsCreateKeycloakUserConfirmationOpen] = useState(false);
Expand All @@ -37,6 +43,11 @@ const withUserRoles = (WrappedComponent) => (props) => {
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)]
Expand Down Expand Up @@ -131,6 +142,7 @@ const withUserRoles = (WrappedComponent) => (props) => {
initialAssignedRoleIds={initialAssignedRoleIds}
checkAndHandleKeycloakAuthUser={checkAndHandleKeycloakAuthUser}
confirmCreateKeycloakUser={confirmCreateKeycloakUser}
isLoadingAffiliationRoles={isLoadingAffiliationRoles}
/>;
};

Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useAllRolesData/useAllRolesData.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function useAllRolesData(options = {}) {

const [namespace] = useNamespace();

const { data, isLoading, isSuccess, refetch } = useQuery([namespace, 'user-roles'], () => {
const { data, isLoading, isSuccess, refetch, isFetching } = useQuery([namespace, 'user-roles'], () => {
return ky.get(`roles?limit=${stripes.config.maxUnpagedResourceCount}&query=cql.allRecords=1 sortby name`).json();
}, { enabled: stripes.hasInterface('roles') });

Expand All @@ -33,7 +33,7 @@ function useAllRolesData(options = {}) {
return rolesMap;
}, [data]);

return { data, isLoading, allRolesMapStructure, isSuccess, refetch };
return { data, isLoading, allRolesMapStructure, isSuccess, refetch, isFetching };
}

export default useAllRolesData;
62 changes: 23 additions & 39 deletions src/hooks/useUserAffiliationRoles/useUserAffiliationRoles.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { useStripes, useOkapiKy } from '@folio/stripes/core';
import { useQueries } from 'react-query';

function useUserAffiliationRoles(userId) {
import useUserAffiliations from '../useUserAffiliations';
import { isAffiliationsEnabled } from '../../components/util/util';

function useUserAffiliationRoles(userId, user) {
const stripes = useStripes();
const hasPerm = stripes.hasPerm('ui-users.roles.view');

const searchParams = {
limit: stripes.config.maxUnpagedResourceCount,
query: `userId==${userId}`,
};
const {
affiliations,
} = useUserAffiliations({
userId: user.id,
}, {
enabled: Boolean(hasPerm && isAffiliationsEnabled(user)),
});

// 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 userTenantRolesQueries = useQueries(
tenants.map(({ id }) => {
affiliations.map(({ tenantId: id }) => {
return {
queryKey:['userTenantRoles', id],
queryFn:() => {
Expand All @@ -25,43 +28,24 @@ function useUserAffiliationRoles(userId) {
beforeRequest: [(req) => req.headers.set('X-Okapi-Tenant', id)]
}
});
return api.get('roles/users', { searchParams }).json();
return api.get(`roles/users/${userId}`).json();
},
enabled: Boolean(userId)
};
})
);

// 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();
},
};
})
);

// 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);
const userRoles = affiliations.reduce((acc, { tenantId }, index) => {
acc[tenantId] = userTenantRolesQueries[index]?.data?.userRoles?.map(({ roleId }) => roleId) || [];
return acc;
}, {});

const isLoading = userTenantRolesQueries.some(query => query.isLoading);

return {
userRoles,
isLoading,
};
}

export default useUserAffiliationRoles;
2 changes: 2 additions & 0 deletions src/views/UserEdit/UserEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ class UserEdit extends React.Component {
setTenantId,
tenantId,
setAssignedRoleIds,
isLoadingAffiliationRoles,
assignedRoleIds
} = this.props;

Expand Down Expand Up @@ -503,6 +504,7 @@ class UserEdit extends React.Component {
tenantId={tenantId}
setAssignedRoleIds={setAssignedRoleIds}
assignedRoleIds={assignedRoleIds}
isLoadingAffiliationRoles={isLoadingAffiliationRoles}
/>
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/views/UserEdit/UserForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ class UserForm extends React.Component {
uniquenessValidator,
profilePictureConfig,
isCreateKeycloakUserConfirmationOpen,
isLoadingAffiliationRoles,
onCancelKeycloakConfirmation
} = this.props;
const selectedPatronGroup = form.getFieldState('patronGroup')?.value;
Expand Down Expand Up @@ -485,6 +486,7 @@ class UserForm extends React.Component {
setAssignedRoleIds={this.props.setAssignedRoleIds}
assignedRoleIds={this.props.assignedRoleIds}
accordionId="userRoles"
isLoadingAffiliationRoles={isLoadingAffiliationRoles}
/>
</IfPermission>
}
Expand Down