From 2f8221297240a62c4148297fb81e63ebffc7ee1a Mon Sep 17 00:00:00 2001 From: Mariia Aloshyna <55138456+mariia-aloshyna@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:50:43 +0200 Subject: [PATCH] Release v10.0.1 (#2329) * UIIN-2453: Instance 3rd pane: Adjust behaviour when returning to instance from holdings/item full screen (#2305) (cherry picked from commit b7c19d1bd14c5306ee40cc3f62cadf8b12910f8f) * UIIN-2629: Consortial holdings accordion is not appearing after the sharing of Instance (#2312) (cherry picked from commit e03476fba5f3321589a7cfc27d910e57cf7b8b0e) * UIIN-2531 Reset CheckboxFacet state.more when user resets search form and fewer facet options are loaded. (#2310) (cherry picked from commit 749fead423792fe9b75df38ffc75733a0de146e8) * UIIN-2588: Edit instance success toast no longer shows the instance HRID (#2313) (cherry picked from commit dc738d222fb766b6d93532b7c7c93646c0b26a1b) * UIIN-2533: Show facet options, if they exist, after clicking the +More button. (#2309) (cherry picked from commit 1b6dca3c391206481dde5d02f680dc27cc0d5191) * UIIN-2608: If Shared & Held by facets were selected in the Browse search, then retain them in the Search lookup after clicking the record. (#2307) (cherry picked from commit 79771fb45f23c92fd0df889658d47ff8dd56df83) * UIIN-2600: Remove error message after switch from Instance Edit screen to another app (#2311) (cherry picked from commit 3411d728c864542ce30a6a91a5affc1af6fec06a) * Release v10.0.1 --------- Co-authored-by: Denys Bohdan Co-authored-by: Dmytro-Melnyshyn <77053927+Dmytro-Melnyshyn@users.noreply.github.com> Co-authored-by: Oleksandr Hladchenko <85172747+oleksandrhladchenko1@users.noreply.github.com> --- CHANGELOG.md | 11 + package.json | 2 +- src/Instance/HoldingsList/Holding/Holding.js | 7 + .../HoldingsList/Holding/Holding.test.js | 2 + .../HoldingsList/Holding/HoldingAccordion.js | 14 +- .../Holding/HoldingAccordion.test.js | 2 + .../HoldingsList/Holding/HoldingContainer.js | 16 ++ .../Holding/HoldingContainer.test.js | 1 + src/Instance/HoldingsList/HoldingsList.js | 4 + .../HoldingsList/HoldingsListContainer.js | 10 +- .../ConsortialHoldings/ConsortialHoldings.js | 31 ++- .../InstanceDetails/InstanceDetails.js | 4 +- .../InstanceDetails/InstanceDetails.test.js | 157 +++++------- .../MemberTenantHoldings.js | 11 +- src/Instance/InstanceEdit/InstanceEdit.js | 4 +- .../HoldingsListMovement.js | 4 + src/ViewInstance.js | 2 + .../BrowseResultsList/BrowseResultsList.js | 4 +- .../BrowseResultsList.test.js | 113 +++++++++ .../getBrowseResultsFormatter.js | 10 +- src/components/BrowseResultsList/utils.js | 53 +++- .../BrowseResultsPane/BrowseResultsPane.js | 1 + src/components/CheckboxFacet/CheckboxFacet.js | 12 +- .../CheckboxFacet/CheckboxFacet.test.js | 236 ++++++++++-------- .../CheckboxFacet/CheckboxFacetList.js | 36 +-- .../CheckboxFacet/CheckboxFacetList.test.js | 21 ++ .../InstanceFilters/InstanceFilters.js | 3 + src/edit/InstanceForm.js | 4 +- src/facetUtils.js | 5 + src/facetUtils.test.js | 1 + src/hooks/index.js | 1 + src/hooks/useHoldingsAccordionState/index.js | 1 + .../useHoldingsAccordionState.js | 42 ++++ .../useHoldingsAccordionState.test.js | 43 ++++ 34 files changed, 622 insertions(+), 246 deletions(-) create mode 100644 src/hooks/useHoldingsAccordionState/index.js create mode 100644 src/hooks/useHoldingsAccordionState/useHoldingsAccordionState.js create mode 100644 src/hooks/useHoldingsAccordionState/useHoldingsAccordionState.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 047214f03..157c81478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change history for ui-inventory +## [10.0.1](https://github.com/folio-org/ui-inventory/tree/v10.0.1) (2023-11-03) +[Full Changelog](https://github.com/folio-org/ui-inventory/compare/v10.0.0...v10.0.1) + +* Instance 3rd pane: Adjust behavior when returning to instance from holdings/item full screen. Refs UIIN-2453. +* Consortial holdings accordion is not appearing after the sharing of Instance. Fixes UIIN-2629. +* Reset CheckboxFacet state.more when user resets search form and fewer facet options are loaded. Fixes UIIN-2531. +* Edit instance success toast no longer shows the instance HRID. Fixes UIIN-2588. +* Show facet options, if they exist, after clicking the +More button. Refs UIIN-2533. +* If Shared & Held by facets were selected in the Browse search, then retain them in the Search lookup after clicking the record. Refs UIIN-2608. +* Remove error message after switch from Instance Edit screen to another app. Fixes UIIN-2600. + ## [10.0.0](https://github.com/folio-org/ui-inventory/tree/v10.0.0) (2023-10-13) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v9.4.12...v10.0.0) diff --git a/package.json b/package.json index c114a0879..582746d2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@folio/inventory", - "version": "10.0.0", + "version": "10.0.1", "description": "Inventory manager", "repository": "folio-org/ui-inventory", "publishConfig": { diff --git a/src/Instance/HoldingsList/Holding/Holding.js b/src/Instance/HoldingsList/Holding/Holding.js index b04382bb0..aea2e7d98 100644 --- a/src/Instance/HoldingsList/Holding/Holding.js +++ b/src/Instance/HoldingsList/Holding/Holding.js @@ -20,6 +20,8 @@ const Holding = ({ isDraggable, isItemsDroppable, tenantId, + instanceId, + pathToAccordionsState, }) => { return (
@@ -49,6 +51,8 @@ const Holding = ({ onViewHolding={onViewHolding} onAddItem={onAddItem} tenantId={tenantId} + instanceId={instanceId} + pathToAccordionsState={pathToAccordionsState} > ( ( isHoldingDragSelected={isHoldingDragSelected} isDraggable={isDraggable} isItemsDroppable={isItemsDroppable} + pathToAccordionsState={['holdings']} /> ); diff --git a/src/Instance/HoldingsList/Holding/HoldingAccordion.js b/src/Instance/HoldingsList/Holding/HoldingAccordion.js index 8fb96e77b..02b4c2ff4 100644 --- a/src/Instance/HoldingsList/Holding/HoldingAccordion.js +++ b/src/Instance/HoldingsList/Holding/HoldingAccordion.js @@ -13,7 +13,10 @@ import { import { callNumberLabel } from '../../../utils'; import HoldingButtonsGroup from './HoldingButtonsGroup'; import useHoldingItemsQuery from '../../../hooks/useHoldingItemsQuery'; -import { useLocationsQuery } from '../../../hooks'; +import { + useHoldingsAccordionState, + useLocationsQuery, +} from '../../../hooks'; const HoldingAccordion = ({ children, @@ -23,13 +26,16 @@ const HoldingAccordion = ({ onAddItem, withMoveDropdown, tenantId, + instanceId, + pathToAccordionsState, }) => { const searchParams = { limit: 0, offset: 0, }; - const [open, setOpen] = useState(false); + const pathToAccordion = [...pathToAccordionsState, holding?.id]; + const [open, setOpen] = useHoldingsAccordionState({ instanceId, pathToAccordion }); const [openFirstTime, setOpenFirstTime] = useState(false); const { totalRecords, isFetching } = useHoldingItemsQuery(holding.id, { searchParams, key: 'itemCount', tenantId }); const { data: locations } = useLocationsQuery({ tenantId }); @@ -113,10 +119,14 @@ HoldingAccordion.propTypes = { holding: PropTypes.object.isRequired, onViewHolding: PropTypes.func.isRequired, onAddItem: PropTypes.func.isRequired, + instanceId: PropTypes.string.isRequired, holdings: PropTypes.arrayOf(PropTypes.object), withMoveDropdown: PropTypes.bool, children: PropTypes.object, tenantId: PropTypes.string, + pathToAccordionsState: PropTypes.arrayOf(PropTypes.string), }; +HoldingAccordion.defaultProps = { pathToAccordionsState: [] }; + export default HoldingAccordion; diff --git a/src/Instance/HoldingsList/Holding/HoldingAccordion.test.js b/src/Instance/HoldingsList/Holding/HoldingAccordion.test.js index 45df04f4c..b8aa058f9 100644 --- a/src/Instance/HoldingsList/Holding/HoldingAccordion.test.js +++ b/src/Instance/HoldingsList/Holding/HoldingAccordion.test.js @@ -37,6 +37,8 @@ const HoldingAccordionSetup = () => ( onViewHolding={noop} onAddItem={noop} withMoveDropdown={false} + instanceId="instanceId" + pathToAccordionsState={['holdings']} > <> diff --git a/src/Instance/HoldingsList/Holding/HoldingContainer.js b/src/Instance/HoldingsList/Holding/HoldingContainer.js index 93f77f883..9a907bd49 100644 --- a/src/Instance/HoldingsList/Holding/HoldingContainer.js +++ b/src/Instance/HoldingsList/Holding/HoldingContainer.js @@ -36,6 +36,8 @@ const DraggableHolding = ({ onViewHolding, onAddItem, tenantId, + instanceId, + pathToAccordionsState, ...rest }) => { const rowStyles = useMemo(() => ( @@ -68,6 +70,8 @@ const DraggableHolding = ({ onViewHolding={onViewHolding} onAddItem={onAddItem} tenantId={tenantId} + instanceId={instanceId} + pathToAccordionsState={pathToAccordionsState} /> ) } @@ -83,12 +87,16 @@ DraggableHolding.propTypes = { draggingHoldingsCount: PropTypes.number, provided: PropTypes.object.isRequired, snapshot: PropTypes.object.isRequired, + instanceId: PropTypes.string.isRequired, holding: PropTypes.object, onViewHolding: PropTypes.func, onAddItem: PropTypes.func, tenantId: PropTypes.string, + pathToAccordionsState: PropTypes.arrayOf(PropTypes.string), }; +DraggableHolding.defaultProps = { pathToAccordionsState: [] }; + const HoldingContainer = ({ location, history, @@ -99,6 +107,7 @@ const HoldingContainer = ({ holdingIndex, draggingHoldingsCount, tenantId, + pathToAccordionsState, ...rest }) => { const onViewHolding = useCallback(() => { @@ -130,6 +139,8 @@ const HoldingContainer = ({ onViewHolding={onViewHolding} onAddItem={onAddItem} tenantId={tenantId} + instanceId={instance?.id} + pathToAccordionsState={pathToAccordionsState} {...rest} /> )} @@ -141,6 +152,8 @@ const HoldingContainer = ({ onViewHolding={onViewHolding} onAddItem={onAddItem} tenantId={tenantId} + instanceId={instance?.id} + pathToAccordionsState={pathToAccordionsState} /> ); }; @@ -157,6 +170,9 @@ HoldingContainer.propTypes = { isDraggable: PropTypes.bool, draggingHoldingsCount: PropTypes.number, tenantId: PropTypes.string, + pathToAccordionsState: PropTypes.arrayOf(PropTypes.string), }; +HoldingContainer.defaultProps = { pathToAccordionsState: [] }; + export default withRouter(HoldingContainer); diff --git a/src/Instance/HoldingsList/Holding/HoldingContainer.test.js b/src/Instance/HoldingsList/Holding/HoldingContainer.test.js index 1cb2c6129..d053d5e37 100644 --- a/src/Instance/HoldingsList/Holding/HoldingContainer.test.js +++ b/src/Instance/HoldingsList/Holding/HoldingContainer.test.js @@ -54,6 +54,7 @@ const renderHoldingContainer = (props = {}) => renderWithIntl( provided={{ draggableProps: { style: true } }} onViewHolding={jest.fn()} onAddItem={jest.fn()} + pathToAccordionsState={['holdings']} {...props} /> diff --git a/src/Instance/HoldingsList/HoldingsList.js b/src/Instance/HoldingsList/HoldingsList.js index 97c01fff5..b378b7d17 100644 --- a/src/Instance/HoldingsList/HoldingsList.js +++ b/src/Instance/HoldingsList/HoldingsList.js @@ -7,6 +7,7 @@ const HoldingsList = ({ instance, holdings, tenantId, + pathToAccordionsState, draggable, droppable, @@ -19,6 +20,7 @@ const HoldingsList = ({ droppable={droppable} holdings={holdings} tenantId={tenantId} + pathToAccordionsState={pathToAccordionsState} /> )); @@ -26,6 +28,7 @@ HoldingsList.propTypes = { instance: PropTypes.object.isRequired, holdings: PropTypes.arrayOf(PropTypes.object), tenantId: PropTypes.string, + pathToAccordionsState: PropTypes.arrayOf(PropTypes.string), draggable: PropTypes.bool, droppable: PropTypes.bool, @@ -33,6 +36,7 @@ HoldingsList.propTypes = { HoldingsList.defaultProps = { holdings: [], + pathToAccordionsState: [], }; export default HoldingsList; diff --git a/src/Instance/HoldingsList/HoldingsListContainer.js b/src/Instance/HoldingsList/HoldingsListContainer.js index be19ea8ee..343284e36 100644 --- a/src/Instance/HoldingsList/HoldingsListContainer.js +++ b/src/Instance/HoldingsList/HoldingsListContainer.js @@ -1,9 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - Loading, -} from '@folio/stripes/components'; +import { Loading } from '@folio/stripes/components'; import HoldingsList from './HoldingsList'; import { HoldingsListMovement } from '../InstanceMovement/HoldingMovementList'; @@ -13,6 +11,7 @@ const HoldingsListContainer = ({ instance, isHoldingsMove, tenantId, + pathToAccordionsState, ...rest }) => { const { holdingsRecords: holdings, isLoading } = useInstanceHoldingsQuery(instance.id, { tenantId }); @@ -26,6 +25,7 @@ const HoldingsListContainer = ({ holdings={holdings} instance={instance} tenantId={tenantId} + pathToAccordionsState={pathToAccordionsState} /> ) : ( ) ); @@ -42,6 +43,9 @@ HoldingsListContainer.propTypes = { instance: PropTypes.object.isRequired, isHoldingsMove: PropTypes.bool, tenantId: PropTypes.string, + pathToAccordionsState: PropTypes.arrayOf(PropTypes.string), }; +HoldingsListContainer.defaultProps = { pathToAccordionsState: [] }; + export default HoldingsListContainer; diff --git a/src/Instance/InstanceDetails/ConsortialHoldings/ConsortialHoldings.js b/src/Instance/InstanceDetails/ConsortialHoldings/ConsortialHoldings.js index d3623b753..e2a220c2d 100644 --- a/src/Instance/InstanceDetails/ConsortialHoldings/ConsortialHoldings.js +++ b/src/Instance/InstanceDetails/ConsortialHoldings/ConsortialHoldings.js @@ -1,4 +1,8 @@ -import React, { useContext } from 'react'; +import React, { + useContext, + useEffect, + useRef, +} from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; @@ -13,13 +17,29 @@ import { import { MemberTenantHoldings } from '../MemberTenantHoldings'; import { DataContext } from '../../../contexts'; -import { useSearchForShadowInstanceTenants } from '../../../hooks'; +import { + useHoldingsAccordionState, + useSearchForShadowInstanceTenants, +} from '../../../hooks'; const ConsortialHoldings = ({ instance }) => { + const pathToAccordion = ['consortialHoldings', '_state']; + const instanceId = instance?.id; + const stripes = useStripes(); + const prevInstanceId = useRef(instanceId); const { consortiaTenantsById } = useContext(DataContext); + const { tenants } = useSearchForShadowInstanceTenants({ instanceId }); + const [isConsortialAccOpen, setConsortialAccOpen] = useHoldingsAccordionState({ instanceId, pathToAccordion }); + + useEffect(() => { + if (instanceId !== prevInstanceId.current) { + setConsortialAccOpen(false); + prevInstanceId.current = instanceId; + } + }, [instanceId]); - const { tenants } = useSearchForShadowInstanceTenants({ instanceId: instance?.id }); + if (!consortiaTenantsById) return null; const memberTenants = tenants .map(tenant => consortiaTenantsById[tenant.id]) @@ -31,7 +51,8 @@ const ConsortialHoldings = ({ instance }) => { } - closedByDefault + open={isConsortialAccOpen} + onToggle={() => setConsortialAccOpen(prevState => !prevState)} > {!memberTenants.length ? @@ -39,7 +60,7 @@ const ConsortialHoldings = ({ instance }) => { {memberTenants.map(memberTenant => ( diff --git a/src/Instance/InstanceDetails/InstanceDetails.js b/src/Instance/InstanceDetails/InstanceDetails.js index 36df56a26..a1d1a40f6 100644 --- a/src/Instance/InstanceDetails/InstanceDetails.js +++ b/src/Instance/InstanceDetails/InstanceDetails.js @@ -123,6 +123,8 @@ const InstanceDetails = forwardRef(({ ); } + const isConsortialHoldingsVisible = instance?.shared || isInstanceShadowCopy(instance?.source); + const renderPaneTitle = () => { const isInstanceShared = Boolean(isShared || isInstanceShadowCopy(instance?.source)); @@ -196,7 +198,7 @@ const InstanceDetails = forwardRef(({ )} - {instance?.shared && ( + {isConsortialHoldingsVisible && ( )} diff --git a/src/Instance/InstanceDetails/InstanceDetails.test.js b/src/Instance/InstanceDetails/InstanceDetails.test.js index 2898391a2..daf483dcd 100644 --- a/src/Instance/InstanceDetails/InstanceDetails.test.js +++ b/src/Instance/InstanceDetails/InstanceDetails.test.js @@ -17,9 +17,14 @@ jest.mock('../InstanceDetails/ControllableDetail/ControllableDetail', () => jest jest.mock('../InstanceDetails/SubInstanceGroup/SubInstanceGroup', () => jest.fn().mockReturnValue('SubInstanceGroup')); jest.mock('../InstanceDetails/InstanceAcquisition/InstanceAcquisition', () => jest.fn().mockReturnValue('InstanceAcquisition')); jest.mock('../InstanceDetails/InstanceTitleData/InstanceTitleData', () => jest.fn().mockReturnValue('InstanceTitleData')); +jest.mock('./ConsortialHoldings', () => ({ + ...jest.requireActual('./ConsortialHoldings'), + ConsortialHoldings: () => , +})); const instance = { title: 'Test Title', + source: 'FOLIO', contributors: [], identifiers: [], instanceTypeId: '1234', @@ -30,6 +35,7 @@ const instance = { notes: [], staffSuppress: false, discoverySuppress: false, + shared: false, }; const mockReferenceData = { @@ -48,23 +54,31 @@ const queryClient = new QueryClient(); const actionMenu = jest.fn(); const onClose = jest.fn(); const tagsEnabled = true; + +const renderInstanceDetails = (props) => { + const component = ( + + + + + + + + ); + + return renderWithIntl(component, translationsProperties); +}; + describe('InstanceDetails', () => { it('renders the InstanceDetails component', () => { - renderWithIntl( - - - - , - - - , - translationsProperties - ); + renderInstanceDetails(); + expect(screen.getByText('InstanceTitle')).toBeInTheDocument(); expect(screen.getByText('Add holdings')).toBeInTheDocument(); expect(screen.getByText('Administrative data')).toBeInTheDocument(); @@ -103,21 +117,7 @@ describe('InstanceDetails', () => { ...instance, staffSuppress: true, }; - renderWithIntl( - - - - , - - - , - translationsProperties - ); + renderInstanceDetails({ instance: staffSuppressedInstance }); expect(screen.getByText('Warning: Instance is marked staff suppressed')).toBeInTheDocument(); expect(screen.getByText('Staff suppressed')).toBeInTheDocument(); @@ -128,21 +128,7 @@ describe('InstanceDetails', () => { ...instance, discoverySuppress: true, }; - renderWithIntl( - - - - , - - - , - translationsProperties - ); + renderInstanceDetails({ instance: discoverySuppressedInstance }); expect(screen.getByText('Warning: Instance is marked suppressed from discovery')).toBeInTheDocument(); expect(screen.getByText('Suppressed from discovery')).toBeInTheDocument(); @@ -153,48 +139,21 @@ describe('InstanceDetails', () => { staffSuppress: true, discoverySuppress: true, }; - renderWithIntl( - - - - , - - - , - translationsProperties - ); + renderInstanceDetails({ instance: bothSuppressedInstance }); expect(screen.getByText('Warning: Instance is marked suppressed from discovery and staff suppressed')).toBeInTheDocument(); }); it('expands and collapses the accordion sections', () => { - renderWithIntl( - - - - , - - - , - translationsProperties - ); + renderInstanceDetails(); const expandAllButtons = screen.getByText('Expand all'); const firstAccordionSection = screen.getByRole('button', { name: /Administrative data/i }); const secondAccordionSection = screen.getByRole('button', { name: /Instance notes/i }); const thirdAccordionSection = screen.getByRole('button', { name: /Electronic access/i }); const fourthAccordionSection = screen.getByRole('button', { name: /Classification/i }); - expect(firstAccordionSection.getAttribute('aria-expanded')).toBe('false'); + // Administrative data is open because it has initial data inside it + expect(firstAccordionSection.getAttribute('aria-expanded')).toBe('true'); expect(secondAccordionSection.getAttribute('aria-expanded')).toBe('false'); expect(thirdAccordionSection.getAttribute('aria-expanded')).toBe('false'); expect(fourthAccordionSection.getAttribute('aria-expanded')).toBe('false'); @@ -212,23 +171,39 @@ describe('InstanceDetails', () => { }); it('renders tags button if tagsEnabled is true', () => { - renderWithIntl( - - - - , - - - , - translationsProperties - ); + renderInstanceDetails(); + const button = screen.getAllByRole('button', { id: 'clickable-show-tags' }); fireEvent.click(button[1]); expect(button[1]).toBeEnabled(); }); + + describe('Consortial holdings accordion', () => { + it('should be visible for shared instances', () => { + const sharedInstance = { + ...instance, + shared: true, + }; + renderInstanceDetails({ instance: sharedInstance }); + + expect(screen.getByRole('button', { name: 'Consortial holdings' })).toBeInTheDocument(); + }); + + it('should be visible for shadow instances', () => { + const shadowInstance = { + ...instance, + shared: false, + source: 'CONSORTIUM-FOLIO', + }; + renderInstanceDetails({ instance: shadowInstance }); + + expect(screen.getByRole('button', { name: 'Consortial holdings' })).toBeInTheDocument(); + }); + + it('should not be visible for local instances', () => { + renderInstanceDetails(); + + expect(screen.queryByRole('button', { name: 'Consortial holdings' })).not.toBeInTheDocument(); + }); + }); }); diff --git a/src/Instance/InstanceDetails/MemberTenantHoldings/MemberTenantHoldings.js b/src/Instance/InstanceDetails/MemberTenantHoldings/MemberTenantHoldings.js index 0ada3eff2..47fc6aa2f 100644 --- a/src/Instance/InstanceDetails/MemberTenantHoldings/MemberTenantHoldings.js +++ b/src/Instance/InstanceDetails/MemberTenantHoldings/MemberTenantHoldings.js @@ -16,6 +16,7 @@ import { InstanceNewHolding } from '../InstanceNewHolding'; import { MoveItemsContext } from '../../MoveItemsContext'; import { useInstanceHoldingsQuery } from '../../../providers'; +import { useHoldingsAccordionState } from '../../../hooks'; import css from './MemberTenantHoldings.css'; @@ -27,7 +28,13 @@ const MemberTenantHoldings = ({ name, id, } = memberTenant; + const instanceId = instance?.id; const stripes = useStripes(); + + const pathToAccordion = ['consortialHoldings', id, '_state']; + const pathToHoldingsAccordion = ['consortialHoldings', id]; + const [isMemberTenantAccOpen, setMemberTenantAccOpen] = useHoldingsAccordionState({ instanceId, pathToAccordion }); + const { holdingsRecords, isLoading } = useInstanceHoldingsQuery(instance?.id, { tenantId: id }); const isUserInCentralTenant = checkIfUserInCentralTenant(stripes); @@ -38,7 +45,8 @@ const MemberTenantHoldings = ({ className={css.memberTenantHoldings} id={`${name}-holdings`} label={name} - closedByDefault + open={isMemberTenantAccOpen} + onToggle={() => setMemberTenantAccOpen(prevValue => !prevValue)} >
{isLoading @@ -51,6 +59,7 @@ const MemberTenantHoldings = ({ tenantId={id} draggable={false} droppable={false} + pathToAccordionsState={pathToHoldingsAccordion} /> )} diff --git a/src/Instance/InstanceEdit/InstanceEdit.js b/src/Instance/InstanceEdit/InstanceEdit.js index f0ecda89b..ed10ba12c 100644 --- a/src/Instance/InstanceEdit/InstanceEdit.js +++ b/src/Instance/InstanceEdit/InstanceEdit.js @@ -54,13 +54,13 @@ const InstanceEdit = ({ const goBack = useGoBack(`/inventory/view/${instanceId}`); - const onSuccess = useCallback((updatedInstance) => { + const onSuccess = useCallback(() => { const message = instance?.shared ? ( ) : ( ); diff --git a/src/Instance/InstanceMovement/HoldingMovementList/HoldingsListMovement.js b/src/Instance/InstanceMovement/HoldingMovementList/HoldingsListMovement.js index 5c0788920..825a8b514 100644 --- a/src/Instance/InstanceMovement/HoldingMovementList/HoldingsListMovement.js +++ b/src/Instance/InstanceMovement/HoldingMovementList/HoldingsListMovement.js @@ -23,6 +23,7 @@ const HoldingsListMovement = ({ draggable, droppable, tenantId, + pathToAccordionsState, }) => { const { selectItemsForDrag, @@ -58,6 +59,7 @@ const HoldingsListMovement = ({ holdingIndex={index} draggingHoldingsCount={draggingHoldingsCount} tenantId={tenantId} + pathToAccordionsState={pathToAccordionsState} /> )) ) : ( @@ -80,10 +82,12 @@ HoldingsListMovement.propTypes = { draggable: PropTypes.bool, droppable: PropTypes.bool, tenantId: PropTypes.string, + pathToAccordionsState: PropTypes.arrayOf(PropTypes.string), }; HoldingsListMovement.defaultProps = { holdings: [], + pathToAccordionsState: [], }; export default HoldingsListMovement; diff --git a/src/ViewInstance.js b/src/ViewInstance.js index 67946a013..f67977944 100644 --- a/src/ViewInstance.js +++ b/src/ViewInstance.js @@ -900,6 +900,7 @@ class ViewInstance extends React.Component { }, ]; const isInstanceLoading = this.state.isLoading || !instance || isCentralTenantPermissionsLoading; + const keyInStorageToHoldingsAccsState = ['holdings']; return ( @@ -927,6 +928,7 @@ class ViewInstance extends React.Component { instance={instance} draggable={this.state.isItemsMovement} tenantId={okapi.tenant} + pathToAccordionsState={keyInStorageToHoldingsAccsState} droppable /> diff --git a/src/components/BrowseResultsList/BrowseResultsList.js b/src/components/BrowseResultsList/BrowseResultsList.js index 9a3f068d5..d387e4be2 100644 --- a/src/components/BrowseResultsList/BrowseResultsList.js +++ b/src/components/BrowseResultsList/BrowseResultsList.js @@ -49,6 +49,7 @@ const BrowseResultsList = ({ onNeedMoreData, }, totalRecords, + filters, }) => { const data = useContext(DataContext); const { search } = useLocation(); @@ -75,7 +76,7 @@ const BrowseResultsList = ({ id={listId} totalCount={totalRecords} contentData={browseData} - formatter={getBrowseResultsFormatter({ data, browseOption })} + formatter={getBrowseResultsFormatter({ data, browseOption, filters })} visibleColumns={VISIBLE_COLUMNS_MAP[browseOption]} isEmptyMessage={isEmptyMessage} isSelected={isSelected} @@ -101,6 +102,7 @@ const BrowseResultsList = ({ BrowseResultsList.propTypes = { browseData: PropTypes.arrayOf(PropTypes.object), + filters: PropTypes.object.isRequired, isEmptyMessage: PropTypes.node.isRequired, isLoading: PropTypes.bool, pagination: PropTypes.shape({ diff --git a/src/components/BrowseResultsList/BrowseResultsList.test.js b/src/components/BrowseResultsList/BrowseResultsList.test.js index ade758be1..9c5833edc 100644 --- a/src/components/BrowseResultsList/BrowseResultsList.test.js +++ b/src/components/BrowseResultsList/BrowseResultsList.test.js @@ -15,6 +15,8 @@ import { browseModeOptions, BROWSE_INVENTORY_ROUTE, INVENTORY_ROUTE, + browseCallNumberOptions, + FACETS, } from '../../constants'; import { DataContext } from '../../contexts'; import BrowseResultsList from './BrowseResultsList'; @@ -59,14 +61,37 @@ const defaultProps = { pageConfig: [0, null, null], }, totalRecords: 1, + filters: {}, }; const mockContext = { contributorNameTypes: [{ id: '2b94c631-fca9-4892-a730-03ee529ffe2a', }], + contributorTypes: [{ + id: '6e09d47d-95e2-4d8a-831b-f777b8ef6d81', + name: 'Author', + }], }; +const contributorsData = [ + { + 'name': 'Toth, Josh', + 'contributorTypeId': [ + '6e09d47d-95e2-4d8a-831b-f777b8ef6d81' + ], + 'contributorNameTypeId': '2b94c631-fca9-4892-a730-03ee529ffe2a', + 'isAnchor': false, + 'totalRecords': 1 + }, +]; +const subjectsData = [ + { + 'value': 'Trivia and miscellanea', + 'totalRecords': 8 + }, +]; + const renderBrowseResultsList = (props = {}) => renderWithIntl( @@ -103,6 +128,94 @@ describe('BrowseResultsList', () => { ); }); + describe.each([ + { searchOption: browseCallNumberOptions.CALL_NUMBERS, shared: FACETS.SHARED, heldBy: FACETS.CALL_NUMBERS_HELD_BY }, + { searchOption: browseCallNumberOptions.DEWEY, shared: FACETS.SHARED, heldBy: FACETS.CALL_NUMBERS_HELD_BY }, + { searchOption: browseCallNumberOptions.LIBRARY_OF_CONGRESS, shared: FACETS.SHARED, heldBy: FACETS.CALL_NUMBERS_HELD_BY }, + { searchOption: browseCallNumberOptions.LOCAL, shared: FACETS.SHARED, heldBy: FACETS.CALL_NUMBERS_HELD_BY }, + { searchOption: browseCallNumberOptions.NATIONAL_LIBRARY_OF_MEDICINE, shared: FACETS.SHARED, heldBy: FACETS.CALL_NUMBERS_HELD_BY }, + { searchOption: browseCallNumberOptions.OTHER, shared: FACETS.SHARED, heldBy: FACETS.CALL_NUMBERS_HELD_BY }, + { searchOption: browseCallNumberOptions.SUPERINTENDENT, shared: FACETS.SHARED, heldBy: FACETS.CALL_NUMBERS_HELD_BY }, + ])('when the search option is $searchOption and the Shared and/or HeldBy facets are selected', ({ searchOption, shared, heldBy }) => { + describe('and the user clicks on a record in the list', () => { + it('should be navigated to the Search lookup with those filters', async () => { + history = createMemoryHistory({ + initialEntries: [{ + pathname: BROWSE_INVENTORY_ROUTE, + search: `${heldBy}=college&qindex=${searchOption}&query=a&${shared}=true&${shared}=false`, + }], + }); + + renderBrowseResultsList({ + filters: { + qindex: searchOption, + query: 'a', + [shared]: ['true', 'false'], + [heldBy]: ['college'], + }, + }); + + fireEvent.click(screen.getByText(defaultProps.browseData[2].fullCallNumber)); + + expect(history.location.search).toContain('?filters=shared.true%2Cshared.false%2CtenantId.college'); + }); + }); + }); + + describe('when the search option is Contributors and the Shared and/or HeldBy facets are selected', () => { + describe('and the user clicks on a record in the list', () => { + it('should be navigated to the Search lookup with those filters', async () => { + history = createMemoryHistory({ + initialEntries: [{ + pathname: BROWSE_INVENTORY_ROUTE, + search: 'contributorsShared=true&contributorsShared=false&contributorsTenantId=college&qindex=contributors', + }], + }); + + renderBrowseResultsList({ + filters: { + qindex: 'contributors', + contributorsShared: ['true', 'false'], + contributorsTenantId: ['college'], + }, + browseData: contributorsData, + }); + + fireEvent.click(screen.getByText(contributorsData[0].name)); + + expect(history.location.search).toContain( + '?filters=searchContributors.2b94c631-fca9-4892-a730-03ee529ffe2a%2Cshared.true%2Cshared.false%2CtenantId.college' + ); + }); + }); + }); + + describe('when the search option is Subjects and the Shared and/or HeldBy facets are selected', () => { + describe('and the user clicks on a record in the list', () => { + it('should be navigated to the Search lookup with those filters', async () => { + history = createMemoryHistory({ + initialEntries: [{ + pathname: BROWSE_INVENTORY_ROUTE, + search: 'qindex=browseSubjects&subjectsShared=true&subjectsShared=false&subjectsTenantId=college', + }], + }); + + renderBrowseResultsList({ + filters: { + qindex: 'browseSubjects', + subjectsShared: ['true', 'false'], + subjectsTenantId: ['college'], + }, + browseData: subjectsData, + }); + + fireEvent.click(screen.getByText(subjectsData[0].value)); + + expect(history.location.search).toContain('?filters=shared.true%2Cshared.false%2CtenantId.college'); + }); + }); + }); + describe('when Instance record is linked to an authority record', () => { describe('by clicking on the icon of an authority app', () => { const record = { diff --git a/src/components/BrowseResultsList/getBrowseResultsFormatter.js b/src/components/BrowseResultsList/getBrowseResultsFormatter.js index 101fa6526..723a3975d 100644 --- a/src/components/BrowseResultsList/getBrowseResultsFormatter.js +++ b/src/components/BrowseResultsList/getBrowseResultsFormatter.js @@ -31,9 +31,10 @@ const getTargetRecord = ( item, row, browseOption, + filters, ) => { const record = getFullMatchRecord(item, row.isAnchor); - const searchParams = getSearchParams(row, browseOption); + const searchParams = getSearchParams(row, browseOption, filters); const isNotClickable = isRowPreventsClick(row, browseOption); if (isNotClickable) return record; @@ -98,12 +99,13 @@ const renderMarcAuthoritiesLink = (authorityId, content) => { const getBrowseResultsFormatter = ({ data, browseOption, + filters, }) => { return { title: r => getFullMatchRecord(r.instance?.title, r.isAnchor), subject: r => { if (r?.totalRecords) { - const subject = getTargetRecord(r?.value, r, browseOption); + const subject = getTargetRecord(r?.value, r, browseOption, filters); if (browseOption === browseModeOptions.SUBJECTS && r.authorityId) { return renderMarcAuthoritiesLink(r.authorityId, subject); } @@ -114,13 +116,13 @@ const getBrowseResultsFormatter = ({ }, callNumber: r => { if (r?.instance || r?.totalRecords) { - return getTargetRecord(r?.fullCallNumber, r, browseOption); + return getTargetRecord(r?.fullCallNumber, r, browseOption, filters); } return ; }, contributor: r => { if (r?.totalRecords) { - const fullMatchRecord = getTargetRecord(r.name, r, browseOption); + const fullMatchRecord = getTargetRecord(r.name, r, browseOption, filters); if (browseOption === browseModeOptions.CONTRIBUTORS && r.authorityId) { return renderMarcAuthoritiesLink(r.authorityId, fullMatchRecord); diff --git a/src/components/BrowseResultsList/utils.js b/src/components/BrowseResultsList/utils.js index ab55c744d..6a38054ee 100644 --- a/src/components/BrowseResultsList/utils.js +++ b/src/components/BrowseResultsList/utils.js @@ -1,4 +1,7 @@ +import omit from 'lodash/omit'; + import { + browseCallNumberOptions, browseModeOptions, FACETS, queryIndexes, @@ -14,45 +17,89 @@ export const isRowPreventsClick = (row, browseOption) => { ); }; -export const getSearchParams = (row, qindex) => { +const facetsToString = (filters, facetNameInBrowse, facetNameInSearch) => { + return filters[facetNameInBrowse]?.map(value => `${facetNameInSearch}.${value}`).join(','); +}; + +const getExtraFilters = (row, qindex, allFilters) => { + const filtersOnly = omit(allFilters, 'qindex', 'query'); + const extraFacets = []; + + let sharedFacetName; + let heldByFacetName; + + if (qindex === browseModeOptions.SUBJECTS) { + sharedFacetName = FACETS.SUBJECTS_SHARED; + heldByFacetName = FACETS.SUBJECTS_HELD_BY; + + if (row.authorityId) { + extraFacets.push(`${FACETS.AUTHORITY_ID}.${row.authorityId}`); + } + } else if (qindex === browseModeOptions.CONTRIBUTORS) { + sharedFacetName = FACETS.CONTRIBUTORS_SHARED; + heldByFacetName = FACETS.CONTRIBUTORS_HELD_BY; + + extraFacets.push(`${FACETS.SEARCH_CONTRIBUTORS}.${row.contributorNameTypeId}`); + } else if (Object.values(browseCallNumberOptions).includes(qindex)) { + sharedFacetName = FACETS.SHARED; + heldByFacetName = FACETS.CALL_NUMBERS_HELD_BY; + } + + const sharedExtraFacets = facetsToString(filtersOnly, sharedFacetName, FACETS.SHARED); + const heldByExtraFacets = facetsToString(filtersOnly, heldByFacetName, FACETS.HELD_BY); + const extraFacetsString = [...extraFacets, sharedExtraFacets, heldByExtraFacets].filter(Boolean).join(','); + + return extraFacetsString ? { filters: extraFacetsString } : {}; +}; + +export const getSearchParams = (row, qindex, allFilters) => { + const filters = getExtraFilters(row, qindex, allFilters); + const optionsMap = { [browseModeOptions.CALL_NUMBERS]: { qindex: queryIndexes.CALL_NUMBER, query: row.shelfKey, + ...filters, }, [browseModeOptions.DEWEY]: { qindex: queryIndexes.CALL_NUMBER, query: row.shelfKey, + ...filters, }, [browseModeOptions.LIBRARY_OF_CONGRESS]: { qindex: queryIndexes.CALL_NUMBER, query: row.shelfKey, + ...filters, }, [browseModeOptions.LOCAL]: { qindex: queryIndexes.CALL_NUMBER, query: row.shelfKey, + ...filters, }, [browseModeOptions.NATIONAL_LIBRARY_OF_MEDICINE]: { qindex: queryIndexes.CALL_NUMBER, query: row.shelfKey, + ...filters, }, [browseModeOptions.OTHER]: { qindex: queryIndexes.CALL_NUMBER, query: row.shelfKey, + ...filters, }, [browseModeOptions.SUPERINTENDENT]: { qindex: queryIndexes.CALL_NUMBER, query: row.shelfKey, + ...filters, }, [browseModeOptions.CONTRIBUTORS]: { qindex: queryIndexes.CONTRIBUTOR, query: row.name, - filters: `${FACETS.SEARCH_CONTRIBUTORS}.${row.contributorNameTypeId}`, + ...filters, }, [browseModeOptions.SUBJECTS]: { qindex: queryIndexes.SUBJECT, query: row.value, - ...(row.authorityId && { filters: `${FACETS.AUTHORITY_ID}.${row.authorityId}` }), + ...filters, }, }; diff --git a/src/components/BrowseResultsPane/BrowseResultsPane.js b/src/components/BrowseResultsPane/BrowseResultsPane.js index 3c93d14b3..a9d64aca1 100644 --- a/src/components/BrowseResultsPane/BrowseResultsPane.js +++ b/src/components/BrowseResultsPane/BrowseResultsPane.js @@ -91,6 +91,7 @@ const BrowseResultsPane = ({ isLoading={isFetching} pagination={pagination} totalRecords={totalRecords} + filters={filters} /> ); diff --git a/src/components/CheckboxFacet/CheckboxFacet.js b/src/components/CheckboxFacet/CheckboxFacet.js index aa169ecfc..b266aad20 100644 --- a/src/components/CheckboxFacet/CheckboxFacet.js +++ b/src/components/CheckboxFacet/CheckboxFacet.js @@ -42,12 +42,12 @@ export default class CheckboxFacet extends React.Component { const prevDataLength = prevProps.dataOptions.length; const currentDataLength = this.props.dataOptions.length; - if ( - this.state.isMoreClicked && - prevDataLength === DEFAULT_FILTERS_NUMBER && - currentDataLength > DEFAULT_FILTERS_NUMBER - ) { - this.updateMore(); + if (this.state.isMoreClicked) { + if (prevDataLength === DEFAULT_FILTERS_NUMBER && currentDataLength > DEFAULT_FILTERS_NUMBER) { + this.updateMore(); + } else if (currentDataLength < prevDataLength) { // if filters were reset we need to reset state.more to show +More button + this.setState({ more: SHOW_OPTIONS_COUNT }); + } } } diff --git a/src/components/CheckboxFacet/CheckboxFacet.test.js b/src/components/CheckboxFacet/CheckboxFacet.test.js index bb1b13ced..b07fd1588 100644 --- a/src/components/CheckboxFacet/CheckboxFacet.test.js +++ b/src/components/CheckboxFacet/CheckboxFacet.test.js @@ -61,8 +61,67 @@ const defaultProps = { isFilterable: true }; -const renderCheckboxFacet = (props, renderer = render) => renderWithIntl( - , +const moreOptions = [ + { + disabled: false, + label: 'TestOption1', + readOnly: false, + value: 1, + count: 5 + }, + { + disabled: false, + label: 'TestOption2', + readOnly: false, + value: 2, + count: 12 + }, + { + disabled: false, + label: 'TestOption3', + readOnly: false, + value: 3, + count: 4 + }, + { + disabled: false, + label: 'TestOption4', + readOnly: false, + value: 4, + count: 6 + }, + { + disabled: false, + label: 'TestOption5', + readOnly: false, + value: 5, + count: 8 + }, + { + disabled: false, + label: 'TestOption6', + readOnly: false, + value: 6, + count: 10 + }, + { + disabled: false, + label: 'TestOption7', + readOnly: false, + value: 7, + count: 19 + }, + { + disabled: false, + label: 'TestOption8', + readOnly: false, + value: 8, + count: 17 + } +]; + +const renderCheckboxFacet = (props = {}, renderer = render) => renderWithIntl( + , translationsProperties, renderer ); @@ -72,119 +131,82 @@ describe('CheckboxFacet', () => { facetsStore.getState().resetFacetSettings(); }); - it('Component should render', () => { - renderCheckboxFacet(defaultProps); + it('should render component', () => { + renderCheckboxFacet(); + expect(screen.getByRole('searchbox', { name: 'Test Name-field' })).toBeInTheDocument(); expect(screen.getAllByRole('checkbox')).toHaveLength(5); expect(screen.getByRole('button', { name: 'More' })).toBeInTheDocument(); }); - it('More options should render when More button is click', () => { - renderCheckboxFacet(defaultProps); - fireEvent.click(screen.getByRole('button', { name: 'More' })); - expect(screen.getAllByRole('checkbox')).toHaveLength(6); + + describe('when more button is clicked', () => { + it('should render more options', () => { + renderCheckboxFacet(); + + fireEvent.click(screen.getByRole('button', { name: 'More' })); + expect(screen.getAllByRole('checkbox')).toHaveLength(6); + }); + + describe('and then component re-renders with fewer options', () => { + it('should show More button', () => { + const { rerender } = renderCheckboxFacet(); + + fireEvent.click(screen.getByRole('button', { name: 'More' })); + renderCheckboxFacet({ dataOptions: moreOptions }, rerender); + + renderCheckboxFacet(defaultProps, rerender); + expect(screen.getByRole('button', { name: 'More' })).toBeInTheDocument(); + }); + }); }); - it('components.readonly should be render when readonly property is true', () => { - const props = { - dataOptions: [ - { - disabled: false, - label: 'Options1', - readOnly: true, - value: 1, - count: 4 - } - ], - name: 'Test Name', - onChange: jest.fn(), - isPending: false - }; - renderCheckboxFacet(props); - expect(screen.getByText('Read-only')).toBeInTheDocument(); + + describe('when readonly property is true', () => { + it('should render components.readonly ', () => { + renderCheckboxFacet({ + dataOptions: [ + { + disabled: false, + label: 'Options1', + readOnly: true, + value: 1, + count: 4 + }, + ], + }); + + expect(screen.getByText('Read-only')).toBeInTheDocument(); + }); }); - it('No matching options should be render when required search is not found', () => { - const { rerender } = renderCheckboxFacet(defaultProps); - fireEvent.change(screen.getByRole('searchbox', { name: 'Test Name-field' }), { target: { value: 'test search' } }); - renderCheckboxFacet(defaultProps, rerender); + describe('when required search is not found', () => { + it('should render "No matching options" message', () => { + const { rerender } = renderCheckboxFacet(); + + fireEvent.change(screen.getByRole('searchbox', { name: 'Test Name-field' }), { target: { value: 'test search' } }); - expect(screen.getByText('No matching options')).toBeInTheDocument(); + renderCheckboxFacet(defaultProps, rerender); + + expect(screen.getByText('No matching options')).toBeInTheDocument(); + }); }); - it('component should re-render ', () => { - const props = { - dataOptions: [ - { - disabled: false, - label: 'TestOption1', - readOnly: false, - value: 1, - count: 5 - }, - { - disabled: false, - label: 'TestOption2', - readOnly: false, - value: 2, - count: 12 - }, - { - disabled: false, - label: 'TestOption3', - readOnly: false, - value: 3, - count: 4 - }, - { - disabled: false, - label: 'TestOption4', - readOnly: false, - value: 4, - count: 6 - }, - { - disabled: false, - label: 'TestOption5', - readOnly: false, - value: 5, - count: 8 - }, - { - disabled: false, - label: 'TestOption6', - readOnly: false, - value: 6, - count: 10 - }, - { - disabled: false, - label: 'TestOption7', - readOnly: false, - value: 7, - count: 19 - }, - { - disabled: false, - label: 'TestOption8', - readOnly: false, - value: 8, - count: 17 - } - ], - onFetch: jest.fn(), - onSearch: jest.fn(), - name: 'Test Name', - onChange: jest.fn(), - isPending: false, - selectedValues: [7, 8, 6], - isFilterable: true - }; - const { rerender } = renderCheckboxFacet(defaultProps); - - fireEvent.click(screen.getByRole('button', { name: 'More' })); - renderCheckboxFacet(props, rerender); - - fireEvent.click(screen.getByRole('checkbox', { name: 'TestOption3 4' })); - fireEvent.click(screen.getByRole('checkbox', { name: 'TestOption7 19' })); - - expect(screen.getAllByRole('checkbox')).toHaveLength(8); + + describe('when dataOptions change', () => { + describe('when new options are added', () => { + it('should render new options', () => { + const newProps = { + dataOptions: moreOptions, + selectedValues: [7, 8, 6], + }; + const { rerender } = renderCheckboxFacet(); + + fireEvent.click(screen.getByRole('button', { name: 'More' })); + renderCheckboxFacet(newProps, rerender); + + fireEvent.click(screen.getByRole('checkbox', { name: 'TestOption3 4' })); + fireEvent.click(screen.getByRole('checkbox', { name: 'TestOption7 19' })); + + expect(screen.getAllByRole('checkbox')).toHaveLength(8); + }); + }); }); }); diff --git a/src/components/CheckboxFacet/CheckboxFacetList.js b/src/components/CheckboxFacet/CheckboxFacetList.js index 0212566e4..d1b76bb59 100644 --- a/src/components/CheckboxFacet/CheckboxFacetList.js +++ b/src/components/CheckboxFacet/CheckboxFacetList.js @@ -52,23 +52,25 @@ function CheckboxFacetList({ } - {!isPending && dataOptions.map(({ count, value, label, disabled, readOnly }) => { - const name = typeof label === 'string' ? label : value; - return ( - - ); - })} + {!isPending && dataOptions + .filter(({ isDeleted }) => !isDeleted) + .map(({ count, value, label, disabled, readOnly }) => { + const name = typeof label === 'string' ? label : value; + return ( + + ); + })}
{!isPending && showMore && ( diff --git a/src/components/CheckboxFacet/CheckboxFacetList.test.js b/src/components/CheckboxFacet/CheckboxFacetList.test.js index 917dbf1fb..a436fdd0c 100644 --- a/src/components/CheckboxFacet/CheckboxFacetList.test.js +++ b/src/components/CheckboxFacet/CheckboxFacetList.test.js @@ -38,6 +38,7 @@ const dataOptions = [ label: 'Check Box 2', value: 'checkBox2', }, + { id: 'fakeId', isDeleted: true }, ]; const selectedValues = ['checkBox1']; const fieldName = 'testFacet'; @@ -123,4 +124,24 @@ describe('CheckboxFacetList', () => { const checkBoxs = screen.findAllByRole('checkbox'); expect(checkBoxs).toMatchObject({}); }); + + it('should not render deleted options', () => { + render( + , + ); + + const allOptions = screen.getAllByRole('checkbox').length; + + expect(allOptions).toBe(2); + }); }); diff --git a/src/components/InstanceFilters/InstanceFilters.js b/src/components/InstanceFilters/InstanceFilters.js index c74fec2ee..250f50707 100644 --- a/src/components/InstanceFilters/InstanceFilters.js +++ b/src/components/InstanceFilters/InstanceFilters.js @@ -444,6 +444,9 @@ const InstanceFilters = props => { selectedValues={activeFilters[FACETS.STATUS]} isPending={getIsPending(FACETS.STATUS)} onChange={onChange} + isFilterable + onSearch={handleFilterSearch} + onFetch={handleFetchFacets} />
({ label: refLookup(referenceTables.statisticalCodeTypes, code.statisticalCodeTypeId).name + ': ' + code.code + ' - ' + code.name, @@ -349,7 +349,7 @@ class InstanceForm extends React.Component { selected: code.id === initialValues.statisticalCodeId, }) ) - .sort((a, b) => a.label.localeCompare(b.label)); + .sort((a, b) => a.label.localeCompare(b.label)) : []; // Since preceding/succeeding title relationships are split out from other parent/child instances, // we don't want the type selection box for parent/child to include the preceding-succeeding type diff --git a/src/facetUtils.js b/src/facetUtils.js index c96efd387..9b095f986 100644 --- a/src/facetUtils.js +++ b/src/facetUtils.js @@ -62,6 +62,11 @@ export const getFacetOptions = (selectedFiltersId, entries, facetData, key, pars if (facet) { const option = parse(facetDataMap.get(entry.id), entry.totalRecords); accum.push(option); + } else { + accum.push({ + id: entry.id, + isDeleted: true, + }); } return accum; }, []); diff --git a/src/facetUtils.test.js b/src/facetUtils.test.js index ee384f70d..cacdce94e 100644 --- a/src/facetUtils.test.js +++ b/src/facetUtils.test.js @@ -43,6 +43,7 @@ describe('getFacetOptions', () => { const key = 'id'; const expectedOptions = [ { label: 'Filter 1', value: 'filter1', count: 2 }, + { id: 'invalid', isDeleted: true }, { label: 'Filter 2', value: 'filter2', count: 0 }, ]; expect(getFacetOptions(selectedFiltersId, entries, facetData, key)).toEqual(expectedOptions); diff --git a/src/hooks/index.js b/src/hooks/index.js index 8d61406b9..51310d19e 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -3,6 +3,7 @@ export { default as useBrowseValidation } from './useBrowseValidation'; export { default as useCallout } from './useCallout'; export { default as useHoldingItemsQuery } from './useHoldingItemsQuery'; export { default as useHoldingMutation } from './useHoldingMutation'; +export { default as useHoldingsAccordionState } from './useHoldingsAccordionState'; export { default as useInstanceMutation } from './useInstanceMutation'; export { default as useHoldingsQueryByHrids } from './useHoldingsQueryByHrids'; export { default as useInventoryBrowse } from './useInventoryBrowse'; diff --git a/src/hooks/useHoldingsAccordionState/index.js b/src/hooks/useHoldingsAccordionState/index.js new file mode 100644 index 000000000..83af4d1a1 --- /dev/null +++ b/src/hooks/useHoldingsAccordionState/index.js @@ -0,0 +1 @@ +export { default } from './useHoldingsAccordionState'; diff --git a/src/hooks/useHoldingsAccordionState/useHoldingsAccordionState.js b/src/hooks/useHoldingsAccordionState/useHoldingsAccordionState.js new file mode 100644 index 000000000..86cad1abb --- /dev/null +++ b/src/hooks/useHoldingsAccordionState/useHoldingsAccordionState.js @@ -0,0 +1,42 @@ +import { + useEffect, + useState, +} from 'react'; +import { + cloneDeep, + get, + set, +} from 'lodash'; + +import { useNamespace } from '@folio/stripes/core'; + +import { + getItem, + setItem, +} from '../../storage'; + +const useHoldingsAccordionState = ({ instanceId, pathToAccordion = [] }) => { + const [namespace] = useNamespace(); + const key = `${namespace}.instanceHoldingsAccordionsState`; + + const instanceHoldingsAccordionsState = getItem(key) ?? {}; + const currentAccState = get(instanceHoldingsAccordionsState, [instanceId, ...pathToAccordion], false); + + const [isOpen, setIsOpen] = useState(currentAccState); + + useEffect(() => { + let newState = { + [instanceId]: { + ...cloneDeep(instanceHoldingsAccordionsState[instanceId]), + } + }; + + newState = set(newState, [instanceId, ...pathToAccordion], isOpen); + + setItem(key, newState); + }, [instanceHoldingsAccordionsState, isOpen, instanceId]); + + return [isOpen, setIsOpen]; +}; + +export default useHoldingsAccordionState; diff --git a/src/hooks/useHoldingsAccordionState/useHoldingsAccordionState.test.js b/src/hooks/useHoldingsAccordionState/useHoldingsAccordionState.test.js new file mode 100644 index 000000000..8e60b2ffa --- /dev/null +++ b/src/hooks/useHoldingsAccordionState/useHoldingsAccordionState.test.js @@ -0,0 +1,43 @@ +import { + renderHook, + waitFor, + act, +} from '@folio/jest-config-stripes/testing-library/react'; + +import '../../../test/jest/__mock__'; + +import useHoldingsAccordionState from './useHoldingsAccordionState'; + +import { getItem } from '../../storage'; + +describe('useHoldingsAccordionState', () => { + it('should save initial holdings state in storage', () => { + const { result } = renderHook(() => useHoldingsAccordionState({ + instanceId: 'instanceId', + pathToAccordion: ['holdings', '_self'], + })); + + const expectedResult = { instanceId: { holdings: { _self: false } } }; + + expect(getItem('@folio/inventory.instanceHoldingsAccordionsState')).toEqual(expectedResult); + expect(result.current[0]).toEqual(false); + }); + + it('should set new state on accordion toggle', () => { + const { result } = renderHook(() => useHoldingsAccordionState({ + instanceId: 'instanceId', + pathToAccordion: ['holdings', '_self'], + })); + + const [isOpen, setOpen] = result.current; + + act(() => { + setOpen(true); + }); + + const expectedResult = { instanceId: { holdings: { _self: true } } }; + + waitFor(() => expect(isOpen).toBeTruthy()); + waitFor(() => expect(getItem('@folio/inventory.instanceHoldingsAccordionsState')).toEqual(expectedResult)); + }); +});