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
106 changes: 106 additions & 0 deletions src/components/PatronRequests/BackendMultiSelectionFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { MultiSelection } from '@folio/stripes/components';
import { useOkapiQuery } from '@projectreshare/stripes-reshare';

const MIN_SEARCH_LENGTH = 2;

const BackendMultiSelectionFilter = ({
name,
selectedValues = [],
onChange,
endpoint,
searchParamsTemplate,
resultsPath = 'results',
labelKey,
valueKey,
placeholder,
disabled,
...otherProps
}) => {
const intl = useIntl();
const [filterValue, setFilterValue] = useState('');

// Cache all items we've seen in order to display selected items
const itemCache = useRef(new Map());

// Note: MultiSelection already debounces the filter function call at 300ms
const shouldFetch = filterValue.length >= MIN_SEARCH_LENGTH;
const searchParams = shouldFetch
? searchParamsTemplate.replace('{searchTerm}', filterValue)
: '';

const query = useOkapiQuery(endpoint, {
searchParams,
enabled: shouldFetch,
staleTime: 5 * 60 * 1000,
cacheTime: 2 * 60 * 60 * 1000,
});

// Map response data to dataOptions format
const data = query.data?.[resultsPath] || [];
const searchResults = data.map(item => ({
label: item[labelKey],
value: item[valueKey]
}));

// Update cache with new search results
useEffect(() => {
searchResults.forEach(item => {
itemCache.current.set(item.value, item);
});
}, [searchResults]);

// Build dataOptions: include search results + any selected items from cache
const dataOptions = [...searchResults];
selectedValues.forEach(val => {
if (!dataOptions.find(opt => opt.value === val) && itemCache.current.has(val)) {
dataOptions.push(itemCache.current.get(val));
}
});

const onChangeHandler = (selectedDataOptions) => {
onChange({
name,
values: selectedDataOptions.map(opt => opt.value)
});
};

// Handle filter input changes - called by MultiSelection when asyncFiltering is true
const handleFilterChange = (value) => {
setFilterValue(value);
return { renderedItems: dataOptions, exactMatch: false };
};

return (
<MultiSelection
asyncFiltering
dataOptions={dataOptions}
value={selectedValues
.map(val => dataOptions.find(opt => opt.value === val))
.filter(Boolean)}
onChange={onChangeHandler}
filter={handleFilterChange}
showLoading={shouldFetch && query.isLoading}
placeholder={placeholder || intl.formatMessage({ id: 'ui-rs.filter.typeToSearch' })}
disabled={disabled}
{...otherProps}
/>
);
};

BackendMultiSelectionFilter.propTypes = {
name: PropTypes.string.isRequired,
selectedValues: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func.isRequired,
endpoint: PropTypes.string.isRequired,
searchParamsTemplate: PropTypes.string.isRequired,
resultsPath: PropTypes.string,
labelKey: PropTypes.string.isRequired,
valueKey: PropTypes.string.isRequired,
placeholder: PropTypes.string,
disabled: PropTypes.bool,
};

export default BackendMultiSelectionFilter;
26 changes: 20 additions & 6 deletions src/components/PatronRequests/Filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
// import { DateFilter } from '@folio/stripes-erm-components';
import AppNameContext from '../../AppNameContext';
import DateFilter from './DateFilter';
import BackendMultiSelectionFilter from './BackendMultiSelectionFilter';

// import css from './Filters.css';

Expand Down Expand Up @@ -77,12 +78,25 @@ const Filters = ({ activeFilters, filterHandlers, options, appDetails }) => {
displayClearButton={activeFilters?.[institutionFilterId]?.length > 0}
onClearFilter={() => filterHandlers.clearGroup(institutionFilterId)}
>
<MultiSelectionFilter
name={institutionFilterId}
dataOptions={options.institution}
selectedValues={activeFilters[institutionFilterId]}
onChange={onChangeHandler}
/>
{options.institution ? (
<MultiSelectionFilter
name={institutionFilterId}
dataOptions={options.institution}
selectedValues={activeFilters[institutionFilterId]}
onChange={onChangeHandler}
/>
) : (
<BackendMultiSelectionFilter
name={institutionFilterId}
selectedValues={activeFilters[institutionFilterId]}
onChange={onChangeHandler}
endpoint="directory/entry"
searchParamsTemplate="filters=type.value=institution&match=name&term={searchTerm}&perPage=100&stats=true"
resultsPath="results"
labelKey="name"
valueKey="id"
/>
)}
</Accordion>
{appName === 'supply' &&
<>
Expand Down
6 changes: 5 additions & 1 deletion src/components/PatronRequests/PatronRequests.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ const PatronRequests = ({ requestsQuery, queryGetter, querySetter, filterOptions
const stripes = useStripes();
const [offset, setOffset] = useState(0);

// Reset pagination when filters/search changes
useEffect(() => {
setOffset(0);
}, [location.search]);

const requests = requestsQuery?.data?.pages?.[offset / perPage]?.results;
const sparseRequests = (new Array(offset)).concat(requests);
const totalCount = requestsQuery?.data?.pages?.[0]?.total;
Expand Down Expand Up @@ -120,7 +125,6 @@ const PatronRequests = ({ requestsQuery, queryGetter, querySetter, filterOptions
<SearchAndSortQuery
initialSearch={initialSearch}
initialSearchState={{ query: '' }}
key={location.search}
queryGetter={queryGetter}
querySetter={querySetter}
>
Expand Down
61 changes: 52 additions & 9 deletions src/routes/PatronRequestsRoute.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import React, { useEffect } from 'react';
import { useInfiniteQuery } from 'react-query';
import { useIntl } from 'react-intl';
import { useHistory, useLocation } from 'react-router-dom';
import queryString from 'query-string';
import { useOkapiKy } from '@folio/stripes/core';
import { generateKiwtQuery, useKiwtSASQuery } from '@k-int/stripes-kint-components';
import { useOkapiQuery } from '@projectreshare/stripes-reshare';
import { useOkapiQuery, useSetting } from '@projectreshare/stripes-reshare';
import PatronRequests from '../components/PatronRequests';

const PER_PAGE = 100;
Expand All @@ -22,10 +24,44 @@ const SUPPLIER = 'supply';

const PatronRequestsRoute = ({ appName, children }) => {
const intl = useIntl();
const location = useLocation();
const history = useHistory();
const { query, queryGetter, querySetter } = useKiwtSASQuery();
const ky = useOkapiKy();
const isSupplier = appName === SUPPLIER;

// Sync query state when location.search changes (e.g., reset filters link clicked)
useEffect(() => {
const parsedQuery = queryString.parse(location.search);

// Ensure all standard query fields are present (set to empty string if missing)
// This is important because querySetter merges rather than replaces
const fullQuery = {
query: '',
filters: '',
sort: '',
...parsedQuery
};

const currentQueryString = queryString.stringify(query);
const newQueryString = queryString.stringify(fullQuery);

if (currentQueryString !== newQueryString) {
querySetter({
nsValues: fullQuery,
location,
history,
state: { changeType: 'external-location' } // Prevents duplicate URL being pushed to history
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.search]); // Only depend on location.search (not query) to avoid infinite loop

// Check if backend institution filter should be used (SLNP mode)
const stateModelSetting = useSetting('requester_returnables_state_model');
const useBackendFilter = stateModelSetting.isSuccess &&
stateModelSetting.value?.startsWith('SLNP');

const SASQ_MAP = {
searchKey: 'id,hrid,patronGivenName,patronSurname,patronIdentifier,title,author,issn,isbn,volumes.itemId,selectedItemBarcode',
// Omitting the date and unread filter keys here causes it to include their value verbatim
Expand Down Expand Up @@ -122,7 +158,8 @@ const PatronRequestsRoute = ({ appName, children }) => {
perPage: '1000',
stats: 'true',
},
staleTime: 2 * 60 * 60 * 1000
staleTime: 2 * 60 * 60 * 1000,
enabled: !useBackendFilter // Only load static institutions when backend filter is disabled
}),
useOkapiQuery('rs/settings/appSettings', {
searchParams: {
Expand All @@ -140,17 +177,17 @@ const PatronRequestsRoute = ({ appName, children }) => {
];

let filterOptions;
if (filterQueries.every(x => x.isSuccess)) {
const [batches, lmsLocations, shelvingLocations, { results: institutions }, settings, refDataRequestServiceType] = filterQueries.map(x => x.data);
// Check if queries are ready (successful or disabled/idle)
const queriesReady = filterQueries.every(x => x.isSuccess || x.status === 'idle');

if (queriesReady) {
const [batches, lmsLocations, shelvingLocations, dirQueryData, settings, refDataRequestServiceType] = filterQueries.map(x => x.data);
filterOptions = {
batch: batches
.sort(compareCreated)
.map(x => ({ label: x.description, value: x.id, dateCreated: x.dateCreated })),
hasLocalNote: [({ label: intl.formatMessage({ id: 'stripes-reshare.hasLocalNote' }), value: 'localNote ISNOTNULL' })],
hasUnread: [({ label: intl.formatMessage({ id: 'ui-rs.unread' }), value: 'hasUnreadMessages=true' })],
institution: institutions
.map(x => ({ label: x.name, value: x.id }))
.sort(compareLabel),
location: lmsLocations
.map(x => ({ label: x.name, value: x.id }))
.sort(compareLabel),
Expand All @@ -162,6 +199,13 @@ const PatronRequestsRoute = ({ appName, children }) => {
terminal: [({ label: intl.formatMessage({ id: 'ui-rs.hideComplete' }), value: 'false' })],
serviceType: getRequestServiceTypes(refDataRequestServiceType)
};

// Only add static institutions if NOT using backend filter
if (!useBackendFilter && dirQueryData?.results) {
filterOptions.institution = dirQueryData.results
.map(x => ({ label: x.name, value: x.id }))
.sort(compareLabel);
}
}

return (
Expand All @@ -172,7 +216,6 @@ const PatronRequestsRoute = ({ appName, children }) => {
filterOptions={filterOptions}
searchParams={generateKiwtQuery(SASQ_MAP, query)}
perPage={PER_PAGE}
key={JSON.stringify(query)}
>
{children}
</PatronRequests>
Expand Down
1 change: 1 addition & 0 deletions translations/ui-rs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@
"filter.dateSubmitted": "Date created",
"filter.dateNeeded": "Date needed",
"filter.serviceType": "Service Type",
"filter.typeToSearch": "Type to search...",
"hideComplete": "Hide completed",
"unread": "Unread messages",
"needsAttention": "Needs attention",
Expand Down