Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UIU-2943: ECS - Filter users by "User Type" #2555

Merged
merged 9 commits into from
Sep 19, 2023
Merged
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.

## [9.0.0](https://github.com/folio-org/ui-users/tree/v9.0.0) (2023-02-20)
[Full Changelog](https://github.com/folio-org/ui-users/compare/v8.1.0...v9.0.0)
Expand Down
11 changes: 8 additions & 3 deletions src/components/util/util.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { every } from 'lodash';
import { every, get } 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: 'test'
}
}
}
};

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 empty string', () => {
const data = isConsortiumEnabled();
expect(data).toBeFalsy();
});

it('should return centralTenantId', () => {
const data = isConsortiumEnabled(STRIPES);
expect(data).toBe(true);
});
});
alisher-epam marked this conversation as resolved.
Show resolved Hide resolved

describe('getCentralTenantId', () => {
it('should return empty string', () => {
const data = getCentralTenantId({});
expect(data).toBe('');
});

it('should return centralTenantId', () => {
const data = getCentralTenantId(STRIPES);
expect(data).toBe('test');
});
});
alisher-epam marked this conversation as resolved.
Show resolved Hide resolved

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}")`;

zburke marked this conversation as resolved.
Show resolved Hide resolved
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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constants, ❤️ . But please keep the same value that was there previously, with hyphens instead of underscores. I know this didn't break any tests, but who knows what else might rely on that value; if we can preserve it then we should.


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`}
alisher-epam marked this conversation as resolved.
Show resolved Hide resolved
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`}
alisher-epam marked this conversation as resolved.
Show resolved Hide resolved
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`}
alisher-epam marked this conversation as resolved.
Show resolved Hide resolved
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`}
alisher-epam marked this conversation as resolved.
Show resolved Hide resolved
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}_userTypes`}
alisher-epam marked this conversation as resolved.
Show resolved Hide resolved
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('Checking presence of user types filter', () => {
isConsortiumEnabled.mockReturnValue(true);
renderFilters(initialProps);
expect(screen.getByText('ui-users.userType')).toBeInTheDocument();
});

it('Checking presence of user types filter', () => {
isConsortiumEnabled.mockReturnValue(false);
renderFilters(initialProps);
expect(screen.queryByText('ui-users.userType')).not.toBeInTheDocument();
});
alisher-epam marked this conversation as resolved.
Show resolved Hide resolved
});
Loading
Loading