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
46 changes: 40 additions & 6 deletions src/components/EditSections/EditUserRoles/EditUserRoles.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@
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';

Check warning on line 13 in src/components/EditSections/EditUserRoles/EditUserRoles.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'useUserAffiliationRoles' is defined but never used. Allowed unused vars must match /React/u
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 }, 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();
Expand All @@ -28,7 +30,19 @@
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)) {
Expand All @@ -38,6 +52,18 @@
}
}, [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);
};
Expand Down Expand Up @@ -107,6 +133,14 @@
};

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 +174,8 @@
</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
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
33 changes: 26 additions & 7 deletions src/components/Wrappers/withUserRoles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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';
Expand All @@ -10,8 +11,9 @@
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();
Expand All @@ -26,17 +28,33 @@
}
});

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]);

Check warning on line 48 in src/components/Wrappers/withUserRoles.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

React Hook useEffect has missing dependencies: 'assignedRoleIds' and 'tenantId'. Either include them or remove the dependency array. You can also replace multiple useState variables with useReducer if 'setInitialAssignedRoleIds' needs the current value of 'tenantId'

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 +149,7 @@
initialAssignedRoleIds={initialAssignedRoleIds}
checkAndHandleKeycloakAuthUser={checkAndHandleKeycloakAuthUser}
confirmCreateKeycloakUser={confirmCreateKeycloakUser}
isLoadingAffiliationRoles={isLoadingAffiliationRoles}
/>;
};

Expand Down
13 changes: 8 additions & 5 deletions src/hooks/useAllRolesData/useAllRolesData.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
96 changes: 40 additions & 56 deletions src/hooks/useUserAffiliationRoles/useUserAffiliationRoles.js
Original file line number Diff line number Diff line change
@@ -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;
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
4 changes: 4 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,8 @@ class UserForm extends React.Component {
setAssignedRoleIds={this.props.setAssignedRoleIds}
assignedRoleIds={this.props.assignedRoleIds}
accordionId="userRoles"
initialAssignedRoleIds={initialValues.assignedRoleIds}
isLoadingAffiliationRoles={isLoadingAffiliationRoles}
/>
</IfPermission>
}
Expand Down Expand Up @@ -542,4 +545,5 @@ export default stripesFinalForm({
initialValuesEqual: (a, b) => isEqual(a, b),
navigationCheck: true,
enableReinitialize: true,
keepDirtyOnReinitialize: true,
})(injectIntl(UserForm));
Loading