Skip to content

Commit

Permalink
UIU-2943: ECS - Filter users by "User Type" (#2555)
Browse files Browse the repository at this point in the history
* UIU-2943: ECS - Filter users by "User Type"

* tests: add tests

* update: revert UIU-2933 manually

* refactor: fix naming issues and minor improvements

* tests: fix test labeling issue

* tests: improve test cases
  • Loading branch information
alisher-epam committed Sep 20, 2023
1 parent 779a981 commit e6e4da7
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
* *BREAKING* bump `react-intl` to `v6.4.4`. Refs UIU-2946.
* Generate "Create request" url for users without barcode. Refs UIU-2869.
* Add auto focus to textarea on staff and patron info modal. Fixes UIU-2932.
* ECS - Filter users by "User Type". Refs UIU-2943.
* Assign/unassign a users affiliations adjustments. Refs UIU-2942.

## [9.0.0](https://github.com/folio-org/ui-users/tree/v9.0.0) (2023-02-20)
Expand Down
9 changes: 7 additions & 2 deletions src/components/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { every } from 'lodash';
import queryString from 'query-string';

import { NoValue } from '@folio/stripes/components';

import {
Expand Down Expand Up @@ -176,11 +177,15 @@ export function checkUserActive(user) {
export const getContributors = (account, instance) => {
const contributors = account?.contributors || instance?.contributors;

return contributors && contributors.map(({ name }) => name);
return contributors?.map(({ name }) => name);
};

export const isConsortiumEnabled = stripes => {
return stripes.hasInterface('consortia');
return stripes?.hasInterface('consortia');
};

export const getCentralTenantId = stripes => {
return stripes?.user?.user?.consortium?.centralTenantId;
};

export const getRequestUrl = (barcode, userId) => {
Expand Down
38 changes: 38 additions & 0 deletions src/components/util/util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,23 @@ import {
retrieveNoteReferredEntityDataFromLocationState,
getClosedRequestStatusesFilterString,
getOpenRequestStatusesFilterString,
getCentralTenantId,
isConsortiumEnabled,
getRequestUrl,
} from './util';

const STRIPES = {
hasPerm: jest.fn().mockReturnValue(true),
hasInterface: jest.fn().mockReturnValue(true),
user: {
user: {
consortium: {
centralTenantId: 'centralTenantId'
}
}
}
};

describe('accountsMatchStatus', () => {
it('returns true if all accounts match', () => {
const status = 'monkey';
Expand Down Expand Up @@ -366,6 +380,30 @@ describe('getContributors', () => {
});
});

describe('isConsortiumEnabled', () => {
it('should return false', () => {
const data = isConsortiumEnabled();
expect(data).toBeFalsy();
});

it('should return true', () => {
const data = isConsortiumEnabled(STRIPES);
expect(data).toBe(true);
});
});

describe('getCentralTenantId ', () => {
it('should return undefined if consortium object is absent', () => {
const data = getCentralTenantId({ ...STRIPES, user: { user: { } } });
expect(data).toBe(undefined);
});

it('should return centralTenantId if consortium object and id is present', () => {
const data = getCentralTenantId(STRIPES);
expect(data).toBe(STRIPES.user.user.consortium.centralTenantId);
});
});

describe('getRequestUrl', () => {
it('should return url with user barcode', () => {
const userBarcode = 'userBarcode';
Expand Down
15 changes: 4 additions & 11 deletions src/routes/UserSearchContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,22 @@ import {
get,
template,
} from 'lodash';
import { stripesConnect } from '@folio/stripes/core';

import { stripesConnect } from '@folio/stripes/core';
import {
makeQueryFunction,
StripesConnectedSource,
buildUrl,
} from '@folio/stripes/smart-components';

import filterConfig from './filterConfig';
import { UserSearch } from '../views';
import {
MAX_RECORDS,
USER_TYPES,
} from '../constants';
import { MAX_RECORDS } from '../constants';
import filterConfig from './filterConfig';
import { buildFilterConfig } from './utils';

const INITIAL_RESULT_COUNT = 30;
const RESULT_COUNT_INCREMENT = 30;

export const NOT_SHADOW_USER_CQL = `((cql.allRecords=1 NOT type ="") or type<>"${USER_TYPES.SHADOW}")`;

const searchFields = [
'username="%{query}*"',
'personal.firstName="%{query}*"',
Expand All @@ -42,7 +37,7 @@ const compileQuery = template(`(${searchFields.join(' or ')})`, { interpolate: /
export function buildQuery(queryParams, pathComponents, resourceData, logger, props) {
const customFilterConfig = buildFilterConfig(queryParams.filters);

const mainQuery = makeQueryFunction(
return makeQueryFunction(
'cql.allRecords=1',
// TODO: Refactor/remove this after work on FOLIO-2066 and RMB-385 is done
(parsedQuery, _, localProps) => localProps.query.query.trim().replace('*', '').split(/\s+/)
Expand All @@ -59,8 +54,6 @@ export function buildQuery(queryParams, pathComponents, resourceData, logger, pr
[...filterConfig, ...customFilterConfig],
2,
)(queryParams, pathComponents, resourceData, logger, props);

return mainQuery && `${NOT_SHADOW_USER_CQL} and ${mainQuery}`;
}

class UserSearchContainer extends React.Component {
Expand Down
20 changes: 14 additions & 6 deletions src/routes/UserSearchContainer.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
buildQuery,
NOT_SHADOW_USER_CQL,
} from './UserSearchContainer';
import { buildQuery } from './UserSearchContainer';

const queryParams = {
filters: 'active.active',
Expand All @@ -15,9 +12,20 @@ const resourceData = {
const logger = {
log: jest.fn(),
};
const mockHasInterface = jest.fn().mockReturnValue(false);
const props = {
stripes: {
hasInterface: mockHasInterface,
}
};

describe('buildQuery', () => {
it('should exclude shadow users when building CQL query', () => {
expect(buildQuery(queryParams, pathComponents, resourceData, logger)).toEqual(expect.stringContaining(NOT_SHADOW_USER_CQL));
it('should return empty CQL query', () => {
expect(buildQuery({}, pathComponents, { query: {} }, logger, props)).toBeFalsy();
});

it('should include username when building CQL query', () => {
mockHasInterface.mockReturnValue(true);
expect(buildQuery(queryParams, pathComponents, resourceData, logger, props)).toEqual(expect.stringContaining('username="Joe*"'));
});
});
6 changes: 6 additions & 0 deletions src/routes/filterConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ const filterConfig = [
values: [],
operator: '=',
},
{
name: 'userType',
cql: 'type',
values: [],
operator: '=',
}
];

export default filterConfig;
65 changes: 54 additions & 11 deletions src/views/UserSearch/Filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@ import {
FormattedMessage,
injectIntl,
} from 'react-intl';
import {
get,
} from 'lodash';
import { get } from 'lodash';

import {
Accordion,
AccordionSet,
FilterAccordionHeader,
} from '@folio/stripes/components';

import { stripesConnect } from '@folio/stripes/core';
import {
CheckboxFilter,
MultiSelectionFilter,
} from '@folio/stripes/smart-components';

import { statusFilter } from '../../constants';

import CustomFieldsFilters from '../../components/CustomFieldsFilters';
import { isConsortiumEnabled } from '../../components/util';
import { USER_TYPES, statusFilter } from '../../constants';

const ACCORDION_ID_PREFIX = 'users-filter-accordion';

class Filters extends React.Component {
static propTypes = {
Expand All @@ -34,6 +34,7 @@ class Filters extends React.Component {
resultOffset: PropTypes.shape({
replace: PropTypes.func.isRequired,
}),
stripes: PropTypes.object.isRequired,
};

static defaultProps = {
Expand Down Expand Up @@ -85,21 +86,44 @@ class Filters extends React.Component {
onChangeHandlers: { clearGroup },
intl: { formatMessage },
resources,
stripes,
} = this.props;
const {
active = [],
pg = [],
tags = [],
departments = [],
userType,
} = activeFilters;

const departmentsAreNotEmpty = !!resources.departments?.records?.length;

const isConsortium = isConsortiumEnabled(stripes);
const { PATRON, SHADOW, STAFF, SYSTEM } = USER_TYPES;
const userTypeOptions = [
{
value: PATRON,
label: formatMessage({ id: 'ui-users.information.userType.patron' }),
},
{
value: STAFF,
label: formatMessage({ id: 'ui-users.information.userType.staff' }),
},
{
value: SHADOW,
label: formatMessage({ id: 'ui-users.information.userType.shadow' }),
},
{
value: SYSTEM,
label: formatMessage({ id: 'ui-users.information.userType.system' }),
}
];

return (
<AccordionSet>
<Accordion
displayClearButton
id="users-filter-accordion-status"
id={`${ACCORDION_ID_PREFIX}-status`}
header={FilterAccordionHeader}
label={formatMessage({ id: 'ui-users.status' })}
separator={false}
Expand All @@ -114,7 +138,7 @@ class Filters extends React.Component {
</Accordion>
<Accordion
displayClearButton
id="users-filter-accordion-patron-group"
id={`${ACCORDION_ID_PREFIX}-patron-group`}
header={FilterAccordionHeader}
label={formatMessage({ id: 'ui-users.information.patronGroup' })}
separator={false}
Expand All @@ -130,7 +154,7 @@ class Filters extends React.Component {
{departmentsAreNotEmpty && (
<Accordion
displayClearButton
id="users-filter-accordion-departments"
id={`${ACCORDION_ID_PREFIX}-departments`}
header={FilterAccordionHeader}
label={formatMessage({ id: 'ui-users.departments' })}
separator={false}
Expand All @@ -147,7 +171,7 @@ class Filters extends React.Component {
)}
<Accordion
displayClearButton
id="users-filter-accordion-tags"
id={`${ACCORDION_ID_PREFIX}-tags`}
header={FilterAccordionHeader}
label={formatMessage({ id: 'ui-users.tags' })}
separator={false}
Expand All @@ -161,6 +185,25 @@ class Filters extends React.Component {
aria-labelledby="users-filter-accordion-tags"
/>
</Accordion>
{
isConsortium && (
<Accordion
displayClearButton
id={`${ACCORDION_ID_PREFIX}-user-types`}
header={FilterAccordionHeader}
label={formatMessage({ id: 'ui-users.userType' })}
separator={false}
onClearFilter={() => clearGroup('userType')}
>
<CheckboxFilter
dataOptions={userTypeOptions}
name="userType"
selectedValues={userType}
onChange={this.handleFilterChange}
/>
</Accordion>
)
}

<CustomFieldsFilters
activeFilters={activeFilters}
Expand All @@ -172,4 +215,4 @@ class Filters extends React.Component {
}
}

export default injectIntl(Filters);
export default stripesConnect(injectIntl(Filters));
19 changes: 19 additions & 0 deletions src/views/UserSearch/Filters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { fireEvent, render, screen } from '@folio/jest-config-stripes/testing-li
import '../../../test/jest/__mock__/matchMedia.mock';

import Filters from './Filters';
import { isConsortiumEnabled } from '../../components/util';

jest.unmock('@folio/stripes/components');
jest.unmock('@folio/stripes/smart-components');
Expand All @@ -16,6 +17,9 @@ jest.mock('@folio/stripes/smart-components', () => {
useCustomFields: jest.fn(() => [[customField]]),
};
});
jest.mock('../../components/util', () => ({
isConsortiumEnabled: jest.fn(),
}));

const stateMock = jest.fn();
const filterHandlers = {
Expand Down Expand Up @@ -43,6 +47,9 @@ const initialProps = {
resultOffset: {
replace: jest.fn(),
},
stripes: {
hasInterface: jest.fn(),
},
};

describe('Filters', () => {
Expand Down Expand Up @@ -77,4 +84,16 @@ describe('Filters', () => {
renderFilters(initialProps);
expect(screen.getByText('ui-users.departments')).toBeInTheDocument();
});

it('should display user-type filter for consortia tenants', () => {
isConsortiumEnabled.mockReturnValue(true);
renderFilters(initialProps);
expect(screen.getByText('ui-users.userType')).toBeInTheDocument();
});

it('should hide user-types filter for non-consortia tenants', () => {
isConsortiumEnabled.mockReturnValue(false);
renderFilters(initialProps);
expect(screen.queryByText('ui-users.userType')).not.toBeInTheDocument();
});
});
Loading

0 comments on commit e6e4da7

Please sign in to comment.