Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ UIIN-3437.
* Fix barcode encoding in ItemForm checkUniqueBarcode function. Fixes UIIN-3490.
* *BREAKING* Migrate config values from mod-configuration. Refs UIIN-3426.
* Make interface dependencies optional (audit-inventory, circulation). Refs UIIN-3592.
* Improve usage of GET `/users-keycloak/_self` requests in "Inventory" app. Fixes UIIN-3585.

## [13.0.10](https://github.com/folio-org/ui-inventory/tree/v13.0.10) (2025-09-01)
[Full Changelog](https://github.com/folio-org/ui-inventory/compare/v13.0.9...v13.0.10)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import {
useContext,
useEffect,
useState,
} from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';

import {
IfInterface,
useStripes,
getUserTenantsPermissions,
} from '@folio/stripes/core';
import {
Accordion,
Expand All @@ -36,7 +34,6 @@ const ConsortialHoldings = ({
const { consortiaTenantsById } = useContext(DataContext);
const { tenants } = useSearchForShadowInstanceTenants({ instanceId });
const [status, updateStatus] = useHoldingsFromStorage({ defaultValue: {} });
const [userTenantPermissions, setUserTenantPermissions] = useState([]);

useEffect(() => {
if (instanceId !== prevInstanceId) {
Expand All @@ -45,10 +42,6 @@ const ConsortialHoldings = ({
}
}, []);

useEffect(() => {
getUserTenantsPermissions(stripes, tenants).then(perms => setUserTenantPermissions(perms));
}, [tenants]);

useEffect(() => {
if (typeof isAllExpanded === 'boolean') {
updateStatus(curStatus => {
Expand Down Expand Up @@ -111,7 +104,6 @@ const ConsortialHoldings = ({
key={`${memberTenant.id}.${instanceId}`}
memberTenant={memberTenant}
instance={instance}
userTenantPermissions={userTenantPermissions}
/>
))}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,72 @@
import React from 'react';
import { useEffect } from 'react';
import PropTypes from 'prop-types';
import { isEmpty } from 'lodash';
import { useQueryClient } from 'react-query';

import {
Accordion,
Loading,
} from '@folio/stripes/components';
import {
checkIfUserInCentralTenant,
useNamespace,
useStripes,
useUserTenantPermissions,
} from '@folio/stripes/core';

import DragAndDropProvider from '../../../../dnd/DragAndDropProvider';
import HoldingsList from '../../HoldingsList';
import { LimitedHoldingsList } from '../LimitedHoldingsList';
import { InstanceNewHolding } from '../../../ViewInstance/components/InstanceDetails/InstanceNewHolding';
import useMemberTenantHoldings from '../../../../hooks/useMemberTenantHoldings';

import useMemberTenantHoldings from '../../../../hooks/useMemberTenantHoldings';
import { useHoldingsFromStorage } from '../../../../hooks';
import { hasMemberTenantPermission } from '../../../../utils';

import css from './MemberTenantHoldings.css';

const MemberTenantHoldings = ({
memberTenant,
instance,
userTenantPermissions,
}) => {
const MemberTenantHoldings = ({ memberTenant, instance }) => {
const {
name,
id: memberTenantId,
} = memberTenant;
const stripes = useStripes();
const queryClient = useQueryClient();

const [namespace] = useNamespace({ key: 'user-self-permissions' });
const accordionId = `${memberTenantId}.${instance?.id}`;
const [accordionStatus, updateAccordionStatus] = useHoldingsFromStorage({ defaultValue: {} });

const isAccordionOpen = accordionStatus[accordionId] || false;

const {
userPermissions,
isFetching: isUserTenantPermissionsLoading,
} = useUserTenantPermissions(
{ tenantId: memberTenantId },
{ enabled: !!memberTenantId && isAccordionOpen },
);

useEffect(() => {
if (!isAccordionOpen) {
queryClient.cancelQueries([namespace, memberTenantId]);
}
}, [isAccordionOpen, memberTenantId, queryClient]);

const pathToHoldingsAccordion = ['consortialHoldings', memberTenantId];
const isUserInCentralTenant = checkIfUserInCentralTenant(stripes);

const canViewHoldingsAndItems = hasMemberTenantPermission('ui-inventory.instance.view', memberTenantId, userTenantPermissions);
const canCreateHoldings = hasMemberTenantPermission('ui-inventory.holdings.create', memberTenantId, userTenantPermissions);
const canViewHoldingsAndItems = hasMemberTenantPermission('ui-inventory.instance.view', memberTenantId, userPermissions);
const canCreateHoldings = hasMemberTenantPermission('ui-inventory.holdings.create', memberTenantId, userPermissions);

const { holdings, isLoading } = useMemberTenantHoldings(instance, memberTenantId, userPermissions);

const { holdings, isLoading } = useMemberTenantHoldings(instance, memberTenantId, userTenantPermissions);
const onToggle = ({ id }) => {
updateAccordionStatus(current => ({
...current,
[id]: !current[id],
}));
};

if (isEmpty(holdings)) return null;

Expand All @@ -61,7 +89,7 @@ const MemberTenantHoldings = ({
instance={instance}
holdings={holdings}
tenantId={memberTenantId}
userTenantPermissions={userTenantPermissions}
userTenantPermissions={userPermissions}
pathToAccordionsState={pathToHoldingsAccordion}
/>
)
Expand All @@ -70,11 +98,13 @@ const MemberTenantHoldings = ({
return (
<Accordion
className={css.memberTenantHoldings}
id={`${memberTenantId}.${instance?.id}`}
id={accordionId}
label={name}
open={isAccordionOpen}
onToggle={onToggle}
>
<div className={css.memberTenantHoldings}>
{isLoading
{isLoading || isUserTenantPermissionsLoading
? <Loading size="large" />
: renderHoldings()
}
Expand All @@ -91,7 +121,6 @@ const MemberTenantHoldings = ({
MemberTenantHoldings.propTypes = {
instance: PropTypes.object.isRequired,
memberTenant: PropTypes.object.isRequired,
userTenantPermissions: PropTypes.arrayOf(PropTypes.object).isRequired,
};

export default MemberTenantHoldings;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BrowserRouter as Router } from 'react-router-dom';

import { screen } from '@folio/jest-config-stripes/testing-library/react';
import { useUserTenantPermissions } from '@folio/stripes/core';

import '../../../../../test/jest/__mock__';
import {
Expand All @@ -9,10 +10,22 @@ import {
} from '../../../../../test/jest/helpers';

import { instance } from '../../../../../test/fixtures';
import { useHoldingsFromStorage } from '../../../../hooks';

import MemberTenantHoldings from './MemberTenantHoldings';

jest.mock('../../../../hooks/useMemberTenantHoldings', () => () => ({ holdings: [{}], isLoading: false }));
jest.mock('../../../../hooks/useMemberTenantHoldings', () => () => ({ holdings: [{ id: 'holdingId' }], isLoading: false }));
jest.mock('@folio/stripes/core', () => ({
...jest.requireActual('@folio/stripes/core'),
useUserTenantPermissions: jest.fn().mockReturnValue({
userPermissions: [],
isFetching: false,
}),
}));
jest.mock('../../../../hooks', () => ({
...jest.requireActual('../../../../hooks'),
useHoldingsFromStorage: jest.fn(),
}));
jest.mock('../../HoldingsList', () => () => <>Holdings</>);
jest.mock('../LimitedHoldingsList', () => ({
...jest.requireActual('../LimitedHoldingsList'),
Expand All @@ -28,32 +41,25 @@ const mockMemberTenant = {
name: 'College',
};

const userTenantFullPermissions = [{
tenantId: 'college',
permissionNames: [{
permissionName: 'ui-inventory.holdings.create',
subPermissions: ['test subPermission 1']
}, {
permissionName: 'ui-inventory.instance.view',
subPermissions: ['test subPermission 1']
}, {
permissionName: 'ui-inventory.item.create',
subPermissions: ['test subPermission 1']
}],
}];
const accordionId = `${mockMemberTenant.id}.${instance.id}`;

const userTenantLimitedPermissions = [{
tenantId: 'college',
permissionNames: [],
const userTenantFullPermissions = [{
permissionName: 'ui-inventory.holdings.create',
subPermissions: ['test subPermission 1']
}, {
permissionName: 'ui-inventory.instance.view',
subPermissions: ['test subPermission 1']
}, {
permissionName: 'ui-inventory.item.create',
subPermissions: ['test subPermission 1']
}];

const renderMemberTenantHoldings = ({ userTenantPermissions }) => {
const renderMemberTenantHoldings = () => {
const component = (
<Router>
<MemberTenantHoldings
instance={instance}
memberTenant={mockMemberTenant}
userTenantPermissions={userTenantPermissions}
/>
</Router>
);
Expand All @@ -62,30 +68,55 @@ const renderMemberTenantHoldings = ({ userTenantPermissions }) => {
};

describe('MemberTenantHoldings', () => {
beforeEach(() => {
useHoldingsFromStorage.mockClear().mockReturnValue(
[{ [accordionId]: true }, jest.fn()]
);
});

afterAll(() => {
jest.clearAllMocks();
});

it('should render member tenant accordion', () => {
renderMemberTenantHoldings({ userTenantPermissions: userTenantFullPermissions });
renderMemberTenantHoldings();

expect(screen.getByText('College')).toBeInTheDocument();
});

describe('tenant with inventory permissions', () => {
it('should render member tenant\'s holdings', () => {
renderMemberTenantHoldings({ userTenantPermissions: userTenantFullPermissions });
useUserTenantPermissions.mockClear().mockReturnValue({
userPermissions: userTenantFullPermissions,
isFetching: false,
});

renderMemberTenantHoldings();

expect(screen.getByText('Holdings')).toBeInTheDocument();
});
});

describe('tenant with limited permissions', () => {
it('should render member tenant\'s holdings with limited information', () => {
renderMemberTenantHoldings({ userTenantPermissions: userTenantLimitedPermissions });
useUserTenantPermissions.mockClear().mockReturnValue({
userPermissions: [],
isFetching: false,
});

renderMemberTenantHoldings();

expect(screen.getByText('LimitedHoldingsList')).toBeInTheDocument();
});
});

it('should render Add holdings button', () => {
renderMemberTenantHoldings({ userTenantPermissions: userTenantFullPermissions });
useUserTenantPermissions.mockClear().mockReturnValue({
userPermissions: userTenantFullPermissions,
isFetching: false,
});

renderMemberTenantHoldings();

expect(screen.getByRole('button', { name: 'Add holdings' })).toBeInTheDocument();
});
Expand Down
2 changes: 1 addition & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ export const isInstanceShadowCopy = (source) => [`${CONSORTIUM_PREFIX}FOLIO`, `$
* @returns true if permissions contains a value matching the given permissionName and tenant
*/
export const hasMemberTenantPermission = (permissionName, tenantId, permissions = []) => {
const tenantPermissions = permissions?.find(permission => permission?.tenantId === tenantId)?.permissionNames || [];
const tenantPermissions = permissions?.find(permission => permission?.tenantId === tenantId)?.permissionNames || permissions || [];

const hasPermission = tenantPermissions?.some(tenantPermission => tenantPermission?.permissionName === permissionName);

Expand Down
Loading