From 7889b7dd184da4a4e3759668e66d8d2c81cb58ab Mon Sep 17 00:00:00 2001 From: Priyanka Terala <104053200+Terala-Priyanka@users.noreply.github.com> Date: Thu, 17 Oct 2024 07:06:19 +0530 Subject: [PATCH] UIU-3219 - navigate to pre patron registrations page and render list (#2755) * UIU-3219 - navigate to pre patron registrations page and render list * upgrade upload artifact version * cleanup * update unit test * UIU-3219 - update tests * UIU-3219 - update records count * UIU-3219 - add interface dependency, fix review comments * UIU-3219 - update query to fetch only 'TIER-2' records * UIU-3219 - update test data for staging records with 'TIER-2' status * UIU-3219 - update chanelog to reflect breaking change * UIU-3221 - create new permission 'Users: Can view patron preregistration data' * UIU-3219 - update tests for PatronPreRegistrationRecordsContainer * UIU-3219 - update sub permission of permission 'Users: Can view patron preregistration data' * UIU-3219 - fix rveiew comments * UIU-3219 - fix lint issue --- .github/workflows/build-npm-release.yml | 4 +- .github/workflows/build-npm.yml | 4 +- CHANGELOG.md | 3 + package.json | 11 ++ .../LinkToPatronPreRegistrations.js | 34 ++++ .../LinkToPatronPreRegistrations.test.js | 26 +++ .../LinkToPatronPreRegistrations/index.js | 1 + src/constants.js | 2 + src/index.js | 8 + .../PatronPreRegistrationRecordsContainer.js | 96 ++++++++++ ...ronPreRegistrationRecordsContainer.test.js | 118 ++++++++++++ src/routes/index.js | 1 + .../PatronsPreRegistrationList.js | 75 ++++++++ .../PatronsPreRegistrationList.test.js | 46 +++++ .../PatronsPreRegistrationListContainer.js | 174 ++++++++++++++++++ ...atronsPreRegistrationListContainer.test.js | 111 +++++++++++ .../constants.js | 46 +++++ src/views/UserSearch/UserSearch.js | 2 + src/views/UserSearch/UserSearch.test.js | 21 ++- src/views/index.js | 1 + .../jest/fixtures/preRegistrationRecords.json | 36 ++++ translations/ui-users/en.json | 23 ++- 22 files changed, 836 insertions(+), 7 deletions(-) create mode 100644 src/components/LinkToPatronPreRegistrations/LinkToPatronPreRegistrations.js create mode 100644 src/components/LinkToPatronPreRegistrations/LinkToPatronPreRegistrations.test.js create mode 100644 src/components/LinkToPatronPreRegistrations/index.js create mode 100644 src/routes/PatronPreRegistrationRecordsContainer.js create mode 100644 src/routes/PatronPreRegistrationRecordsContainer.test.js create mode 100644 src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationList.js create mode 100644 src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationList.test.js create mode 100644 src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationListContainer.js create mode 100644 src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationListContainer.test.js create mode 100644 src/views/PatronsPreRegistrationListContainer/constants.js create mode 100644 test/jest/fixtures/preRegistrationRecords.json diff --git a/.github/workflows/build-npm-release.yml b/.github/workflows/build-npm-release.yml index 44dcf781e..928dbafce 100644 --- a/.github/workflows/build-npm-release.yml +++ b/.github/workflows/build-npm-release.yml @@ -147,7 +147,7 @@ jobs: data: ${{ steps.moduleDescriptor.outputs.content }} - name: Upload event file - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Event File path: ${{ github.event_path }} @@ -155,7 +155,7 @@ jobs: - name: Upload Jest results if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: jest-test-results path: ${{ env.JEST_JUNIT_OUTPUT_DIR }}/*.xml diff --git a/.github/workflows/build-npm.yml b/.github/workflows/build-npm.yml index 8c7ca6de3..81a8c2b0d 100644 --- a/.github/workflows/build-npm.yml +++ b/.github/workflows/build-npm.yml @@ -87,7 +87,7 @@ jobs: run: cat module-descriptor.json - name: Upload event file - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Event File path: ${{ github.event_path }} @@ -95,7 +95,7 @@ jobs: - name: Upload Jest results if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: jest-test-results path: ${{ env.JEST_JUNIT_OUTPUT_DIR }}/*.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eb59d2c8..6297f130a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,9 @@ * Update `notes` to `v4.0`. Refs UIU-3229. * Loan details: Print Due date receipt. Refs UIU-3210. * Open loans: `isDcbItem()` must tolerate sparse data. Refs UIU-3230. +* *BREAKING* Add new okapi interface staging-users. Add action button for searching patrons pre registration records. Refs UIU-3219. +* Display patrons pre registration records results. Refs UIU-3222 +* Create new permission 'Users: Can view patron preregistration data'. Refs UIU-3221. ## [10.1.2](https://github.com/folio-org/ui-users/tree/v10.1.2) (2024-09-05) [Full Changelog](https://github.com/folio-org/ui-users/compare/v10.1.1...v10.1.2) diff --git a/package.json b/package.json index fc2f25fc2..b96f3c148 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "permissions": "5.6", "login": "7.3", "users-bl": "6.1", + "staging-users": "1.0", "tags": "1.0" }, "optionalOkapiInterfaces": { @@ -1086,6 +1087,16 @@ "reading-room.patron-permission.item.put" ], "visible": true + }, + { + "permissionName": "ui-users.patron-pre-registrations-view", + "displayName": "Users: Can view patron preregistration data", + "subPermissions": [ + "module.users.enabled", + "ui-users.view", + "staging-users.collection.get" + ], + "visible": true } ] }, diff --git a/src/components/LinkToPatronPreRegistrations/LinkToPatronPreRegistrations.js b/src/components/LinkToPatronPreRegistrations/LinkToPatronPreRegistrations.js new file mode 100644 index 000000000..e6474097a --- /dev/null +++ b/src/components/LinkToPatronPreRegistrations/LinkToPatronPreRegistrations.js @@ -0,0 +1,34 @@ +import { useHistory } from 'react-router-dom'; +import { FormattedMessage } from 'react-intl'; + +import { IfPermission } from '@folio/stripes/core'; +import { + Button, + Icon, +} from '@folio/stripes/components'; + + +const LinkToPatronPreRegistrations = () => { + const history = useHistory(); + + return ( + + + + ); +}; + +export default LinkToPatronPreRegistrations; diff --git a/src/components/LinkToPatronPreRegistrations/LinkToPatronPreRegistrations.test.js b/src/components/LinkToPatronPreRegistrations/LinkToPatronPreRegistrations.test.js new file mode 100644 index 000000000..03755b24b --- /dev/null +++ b/src/components/LinkToPatronPreRegistrations/LinkToPatronPreRegistrations.test.js @@ -0,0 +1,26 @@ +import { + screen, + render, +} from '@folio/jest-config-stripes/testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; + +import '../../../test/jest/__mock__'; + +import LinkToPatronPreRegistrations from './LinkToPatronPreRegistrations'; + +describe('LinkToPatronPreRegistrations', () => { + beforeEach(() => { + const history = createMemoryHistory(); + + render( + + + + ); + }); + + it('should render button', () => { + expect(screen.getByText('ui-users.actionMenu.preRegistrationRecords')).toBeInTheDocument(); + }); +}); diff --git a/src/components/LinkToPatronPreRegistrations/index.js b/src/components/LinkToPatronPreRegistrations/index.js new file mode 100644 index 000000000..8955c20b1 --- /dev/null +++ b/src/components/LinkToPatronPreRegistrations/index.js @@ -0,0 +1 @@ +export { default } from './LinkToPatronPreRegistrations'; diff --git a/src/constants.js b/src/constants.js index dcd9517e6..77067913e 100644 --- a/src/constants.js +++ b/src/constants.js @@ -389,3 +389,5 @@ export const USER_INFO = { export const USER_FIELDS_TO_CHECK = [USER_INFO.FIRST_NAME, USER_INFO.MIDDLE_NAME, USER_INFO.MOBILE_PHONE, USER_INFO.PHONE, USER_INFO.PREFERRED_FIRST_NAME, USER_INFO.BARCODE, USER_INFO.EXTERNAL_SYSTEM_ID, USER_INFO.USERNAME]; + +export const PATRON_PREREGISTRATION_RECORDS_NAME = 'patronPreRegistrationRecords'; diff --git a/src/index.js b/src/index.js index 800ea1b37..ee1ee5d9b 100644 --- a/src/index.js +++ b/src/index.js @@ -215,6 +215,14 @@ class UsersRouting extends React.Component { + ( + + + + )} + /> diff --git a/src/routes/PatronPreRegistrationRecordsContainer.js b/src/routes/PatronPreRegistrationRecordsContainer.js new file mode 100644 index 000000000..ebf268200 --- /dev/null +++ b/src/routes/PatronPreRegistrationRecordsContainer.js @@ -0,0 +1,96 @@ +import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; +import { get } from 'lodash'; + +import { stripesConnect } from '@folio/stripes/core'; +import { + makeQueryFunction, + StripesConnectedSource, +} from '@folio/stripes/smart-components'; +import PatronsPreRegistrationListContainer from '../views/PatronsPreRegistrationListContainer/PatronsPreRegistrationListContainer'; + +import { PATRON_PREREGISTRATION_RECORDS_NAME } from '../constants'; + +const RESULT_COUNT_INCREMENT = 100; +const PAGE_AMOUNT = 100; + +const PatronPreRegistrationRecordsContainer = ({ + resources, + mutator, + stripes, +}) => { + const history = useHistory(); + const source = new StripesConnectedSource({ resources, mutator }, stripes.logger, PATRON_PREREGISTRATION_RECORDS_NAME); + const data = get(resources, `${PATRON_PREREGISTRATION_RECORDS_NAME}.records`, []); + + const queryGetter = () => { + return get(resources, 'query', {}); + }; + + const onNeedMoreData = (askAmount, index) => { + const { resultOffset } = mutator; + + if (source) { + if (resultOffset && index >= 0) { + source.fetchOffset(index); + } else { + source.fetchMore(RESULT_COUNT_INCREMENT); + } + } + }; + + const onClose = () => { + history.push('/users?sort=name'); + }; + + return ( + + ); +}; + +PatronPreRegistrationRecordsContainer.manifest = { + query: { initialValue: {} }, + resultCount: { initialValue: 0 }, + resultOffset: { initialValue: 0 }, + [PATRON_PREREGISTRATION_RECORDS_NAME]: { + type: 'okapi', + records: 'staging_users', + resultOffset: '%{resultOffset}', + resultDensity: 'sparse', + perRequest: PAGE_AMOUNT, + path: 'staging-users', + GET: { + params: { + query: makeQueryFunction( + 'cql.allRecords=1', + '(keywords="%{query.query}*") AND status == "TIER-2"', + { + 'firstName': 'personal.firstName', + 'lastName': 'personal.lastName', + 'middleName': 'personal.middleName', + 'preferredFirsName': 'personal.preferredFirstName', + 'email': 'personal.email', + }, + '', + 2 + ), + }, + staticFallback: { params: {} }, + }, + } +}; + +PatronPreRegistrationRecordsContainer.propTypes = { + mutator: PropTypes.object, + stripes: PropTypes.object, + resources: PropTypes.object, +}; + +export default stripesConnect(PatronPreRegistrationRecordsContainer); diff --git a/src/routes/PatronPreRegistrationRecordsContainer.test.js b/src/routes/PatronPreRegistrationRecordsContainer.test.js new file mode 100644 index 000000000..581d40ee3 --- /dev/null +++ b/src/routes/PatronPreRegistrationRecordsContainer.test.js @@ -0,0 +1,118 @@ +import { render, screen, fireEvent } from '@folio/jest-config-stripes/testing-library/react'; + +import preRegistrationRecords from 'fixtures/preRegistrationRecords'; +import buildStripes from '__mock__/stripes.mock'; + +import { createMemoryHistory } from 'history'; +import { MemoryRouter, useHistory } from 'react-router-dom'; + +import { StripesConnectedSource } from '@folio/stripes/smart-components'; + +import PatronPreRegistrationRecordsContainer from './PatronPreRegistrationRecordsContainer'; + +jest.unmock('@folio/stripes/components'); +jest.unmock('@folio/stripes/smart-components'); +jest.mock('@folio/stripes/components', () => ({ + ...jest.requireActual('@folio/stripes/components'), + SearchField: jest.fn((props) => ( + + )), +})); +jest.mock('../views/PatronsPreRegistrationListContainer/PatronsPreRegistrationListContainer', () => { + return jest.fn(({ onClose, onNeedMoreData }) => ( +
+ Mocked component PatronsPreRegistrationListContainer + + +
+ )); +}); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: jest.fn(), +})); + +const history = createMemoryHistory(); +history.push = jest.fn(); +const props = { + resources: { + patronPreRegistrationRecords: { + records: preRegistrationRecords, + }, + resultCount: 1, + resultOffset: 0, + }, + mutator: { + patronPreRegistrationRecords:{ + GET: jest.fn(), + }, + resultOffset: 100 + }, + stripes: buildStripes(), + history, +}; + +const renderPatronPreRegistrationRecordsContainer = (alteredProps) => render( + + + +); + +describe('PatronPreRegistrationRecordsContainer', () => { + let mockHistory; + let mockSource; + + beforeEach(() => { + // Set up the mock history object + mockHistory = { push: jest.fn() }; + useHistory.mockReturnValue(mockHistory); + + // Mock the StripesConnectedSource instance + mockSource = { + fetchOffset: jest.fn(), + fetchMore: jest.fn(), + }; + + jest.spyOn(StripesConnectedSource.prototype, 'fetchOffset').mockImplementation(mockSource.fetchOffset); + jest.spyOn(StripesConnectedSource.prototype, 'fetchMore').mockImplementation(mockSource.fetchMore); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render', () => { + renderPatronPreRegistrationRecordsContainer(); + expect(screen.getByTestId('mock-PatronsPreRegistrationListContainer')).toBeInTheDocument(); + }); + + it('should call onClose method', () => { + renderPatronPreRegistrationRecordsContainer(); + fireEvent.click(screen.getByTestId('close-button')); + + expect(mockHistory.push).toHaveBeenCalledWith('/users?sort=name'); + }); + + it('should call onNeedMoreData method', () => { + renderPatronPreRegistrationRecordsContainer(); + fireEvent.click(screen.getByTestId('need-more-button')); + + expect(mockSource.fetchOffset).toHaveBeenCalledWith(1); + }); + + it('should call onNeedMoreData method', () => { + const alteredProps = { + ...props, + mutator: { + ...props.mutator, + resultOffset: 0 + }, + }; + renderPatronPreRegistrationRecordsContainer(alteredProps); + fireEvent.click(screen.getByTestId('need-more-button')); + + expect(mockSource.fetchMore).toHaveBeenCalledWith(100); + }); +}); diff --git a/src/routes/index.js b/src/routes/index.js index 9d4965702..b36dfaa75 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -9,4 +9,5 @@ export { default as LoansListingContainer } from './LoansListingContainer'; export { default as LoanDetailContainer } from './LoanDetailContainer'; export { default as AccountDetailsContainer } from './AccountDetailsContainer'; export { default as LostItemsContainer } from './LostItemsContainer'; +export { default as PatronPreRegistrationRecordsContainer } from './PatronPreRegistrationRecordsContainer'; export { default as PatronNoticePrintJobsContainer } from './PatronNoticePrintJobsContainer'; diff --git a/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationList.js b/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationList.js new file mode 100644 index 000000000..f2d34a3d4 --- /dev/null +++ b/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationList.js @@ -0,0 +1,75 @@ +import PropTypes from 'prop-types'; +import { MultiColumnList, Button } from '@folio/stripes/components'; +import { useIntl, FormattedMessage } from 'react-intl'; +import { get, noop } from 'lodash'; + +import { visibleColumns, columnMapping, COLUMNS_NAME } from './constants'; + +const PatronsPreRegistrationList = ({ + data, + isEmptyMessage, + totalCount, + onNeedMoreData +}) => { + const intl = useIntl(); + + const preRegistrationsListFormatter = () => ({ + [COLUMNS_NAME.ACTION]: () => ( + + ), + [COLUMNS_NAME.FIRST_NAME]: user => get(user, ['generalInfo', 'firstName']), + [COLUMNS_NAME.LAST_NAME]: user => get(user, ['generalInfo', 'lastName']), + [COLUMNS_NAME.MIDDLE_NAME]: user => get(user, ['generalInfo', 'middleName']), + [COLUMNS_NAME.PREFERRED_FIRST_NAME]: user => get(user, ['generalInfo', 'preferredFirstName']), + [COLUMNS_NAME.EMAIL]: user => get(user, ['contactInfo', 'email']), + [COLUMNS_NAME.PHONE_NUMBER]: user => get(user, ['contactInfo', 'phone']), + [COLUMNS_NAME.MOBILE_NUMBER]: user => get(user, ['contactInfo', 'mobilePhone']), + [COLUMNS_NAME.ADDRESS]: user => { + const addressInfo = get(user, 'addressInfo'); + return Object.values(addressInfo).join(','); + }, + [COLUMNS_NAME.EMAIL_COMMUNICATION_PREFERENCES]: user => get(user, ['preferredEmailCommunication']).join(','), + [COLUMNS_NAME.SUBMISSION_DATE]: user => { + const submissionDate = get(user, ['metadata', 'updatedDate']); + return `${intl.formatDate(submissionDate)}, ${intl.formatTime(submissionDate)}`; + }, + [COLUMNS_NAME.EMAIL_VERIFICATION]: user => { + const isEmailVerified = get(user, 'isEmailVerified'); + if (isEmailVerified) return intl.formatMessage({ id: 'ui-users.stagingRecords.activated' }); + else return intl.formatMessage({ id: 'ui-users.stagingRecords.notActivated' }); + } + }); + + return ( + + ); +}; + +PatronsPreRegistrationList.propTypes = { + isEmptyMessage: PropTypes.node.isRequired, + totalCount: PropTypes.number.isRequired, + data: PropTypes.arrayOf(PropTypes.object).isRequired, + onNeedMoreData: PropTypes.func.isRequired, +}; + +export default PatronsPreRegistrationList; diff --git a/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationList.test.js b/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationList.test.js new file mode 100644 index 000000000..aa1634ba1 --- /dev/null +++ b/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationList.test.js @@ -0,0 +1,46 @@ +import { + screen, + render, +} from '@folio/jest-config-stripes/testing-library/react'; + +import preRegistrationRecords from 'fixtures/preRegistrationRecords'; + +import { MultiColumnList } from '@folio/stripes/components'; + +import PatronsPreRegistrationList from './PatronsPreRegistrationList'; + +const defaultProps = { + data: [], + isEmptyMessage: 'empty message', + totalCount: 0, + onNeedMoreData: jest.fn(), +}; + +describe('PatronsPreRegistrationList', () => { + it('should render the component', () => { + render(); + + expect(screen.getByTestId('PatronsPreRegistrationsList')).toBeVisible(); + }); + + it('should be called with correct props', () => { + const expectedProps = { + autosize: true, + contentData: preRegistrationRecords, + id: 'PatronsPreRegistrationsList', + pageAmount: 100, + pagingType: 'prev-next', + totalCount: 1, + columnMapping: expect.any(Object), + formatter: expect.any(Object), + }; + const props = { + ...defaultProps, + totalCount: 1, + data: preRegistrationRecords, + }; + + render(); + expect(MultiColumnList).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {}); + }); +}); diff --git a/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationListContainer.js b/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationListContainer.js new file mode 100644 index 000000000..a3081a438 --- /dev/null +++ b/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationListContainer.js @@ -0,0 +1,174 @@ +import { useState } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { noop } from 'lodash'; + +import { + Pane, + PaneMenu, + Paneset, + Button, + Icon, + SearchField, +} from '@folio/stripes/components'; +import { + SearchAndSortQuery, + SearchAndSortNoResultsMessage as NoResultsMessage, + CollapseFilterPaneButton, + ExpandFilterPaneButton, +} from '@folio/stripes/smart-components'; + +import PatronsPreRegistrationList from './PatronsPreRegistrationList'; + +const PatronsPreRegistrationListContainer = ({ + queryGetter, + onClose, + source, + data, + onNeedMoreData +}) => { + const intl = useIntl(); + const [isSearchPaneVisible, setIsSearchPaneVisible] = useState(true); + const query = queryGetter ? queryGetter() || {} : {}; + const count = source ? source.totalCount() : 0; + const emptyMessage = source + ? + : null; + + let resultPaneSub = ; + + if (source && source.loaded()) { + resultPaneSub = ; + } + + const toggleSearchPane = () => { + setIsSearchPaneVisible(prev => !prev); + }; + + const firstMenu = () => { + if (!isSearchPaneVisible) { + return ( + + + + ); + } else { + return null; + } + }; + + return ( + + { + ({ + getSearchHandlers, + onSubmitSearch, + searchValue, + resetAll, + }) => { + const isResetButtonDisabled = !searchValue.query; + + return ( + + { + isSearchPaneVisible && ( + } + lastMenu={ + + + + } + > +
+ { + if (e.target.value) { + getSearchHandlers().query(e); + } else { + getSearchHandlers().reset(); + } + }} + placeholder={intl.formatMessage({ id : 'ui-users.stagingRecords.search.placeholder' })} + value={searchValue.query} + /> + + + +
+ ) + } + } + paneSub={resultPaneSub} + firstMenu={firstMenu()} + defaultWidth="fill" + dismissible + onClose={onClose} + padContent={false} + noOverflow + > + + +
+ ); + } + } +
+ ); +}; + +PatronsPreRegistrationListContainer.propTypes = { + onNeedMoreData: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + queryGetter: PropTypes.func.isRequired, + source: PropTypes.object.isRequired, + data: PropTypes.object.isRequired, +}; + +export default PatronsPreRegistrationListContainer; diff --git a/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationListContainer.test.js b/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationListContainer.test.js new file mode 100644 index 000000000..5c5e7adf2 --- /dev/null +++ b/src/views/PatronsPreRegistrationListContainer/PatronsPreRegistrationListContainer.test.js @@ -0,0 +1,111 @@ +import { + screen, + waitFor, +} from '@folio/jest-config-stripes/testing-library/react'; +import userEvent from '@folio/jest-config-stripes/testing-library/user-event'; + +import renderWithRouter from 'helpers/renderWithRouter'; +import '../../../test/jest/__mock__/matchMedia.mock'; + +import PatronsPreRegistrationListContainer from './PatronsPreRegistrationListContainer'; + +jest.unmock('@folio/stripes/components'); +jest.mock('./PatronsPreRegistrationList', () => jest.fn(() =>
PatronsPreRegistrationList
)); +jest.mock('@folio/stripes/components', () => ({ + ...jest.requireActual('@folio/stripes/components'), + SearchField: jest.fn((props) => ( + + )), +})); + +const defaultProps = { + queryGetter: jest.fn(), + onClose: jest.fn(), + source: { + loaded: () => true, + totalCount: () => 0, + }, + data: [], + onNeedMoreData: jest.fn(), +}; +const renderComponent = (props) => renderWithRouter(); + +describe('PatronsPreRegistrationListContainer', () => { + it('should render component', () => { + renderComponent(); + expect(screen.getByText('ui-users.stagingRecords.list.searchResults')).toBeInTheDocument(); + }); + + it('should display search pane', () => { + renderComponent(); + expect(screen.getByText('ui-users.stagingRecords.list.search')).toBeInTheDocument(); + }); + + it('should toggle search pane', async () => { + renderComponent(); + expect(screen.getByText('ui-users.stagingRecords.list.search')).toBeInTheDocument(); + const collapseButton = document.querySelector('[icon="caret-left"]'); + await userEvent.click(collapseButton); + await waitFor(() => expect(screen.queryByText('ui-users.stagingRecords.list.search')).toBeNull()); + }); + + it('should reset search on clicking reset button', async () => { + renderComponent(); + const resetButton = document.querySelector('[id="preRegistrationListResetAllButton"]'); + expect(resetButton).toBeDisabled(); + await userEvent.type(document.querySelector('[id="stagingRecordsSearch"]'), 'record'); + expect(resetButton).toBeEnabled(); + await userEvent.click(resetButton); + await waitFor(() => expect(resetButton).toBeDisabled()); + }); + + it('should render PatronsPreRegistrationList component', () => { + const props = { + ...defaultProps, + source: { + loaded: () => true, + totalCount: () => 1, + }, + data: [ + { + 'id': 'd717f710-ddf3-4208-9784-f443b74c40cd', + 'active': true, + 'generalInfo': { + 'firstName': 'new-record-2', + 'preferredFirstName': 'New Record', + 'middleName': '', + 'lastName': 'new-record-2' + }, + 'addressInfo': { + 'addressLine0': '123 Main St', + 'addressLine1': 'Apt 4B', + 'city': 'Metropolis', + 'province': 'NY', + 'zip': '12345', + 'country': 'USA' + }, + 'contactInfo': { + 'phone': '555-123456', + 'mobilePhone': '555-5678', + 'email': 'new-record-2@test.com' + }, + 'preferredEmailCommunication': [ + 'Support', + 'Programs' + ], + 'metadata': { + 'createdDate': '2024-04-29T13:29:41.711+00:00', + 'createdByUserId': '6d8a6bf2-5b5f-4168-aa2f-5c4e67b4b65c', + 'updatedDate': '2024-04-29T13:29:41.711+00:00', + 'updatedByUserId': '6d8a6bf2-5b5f-4168-aa2f-5c4e67b4b65c' + } + } + ], + }; + renderComponent(props); + + expect(screen.getByText('PatronsPreRegistrationList')).toBeInTheDocument(); + }); +}); diff --git a/src/views/PatronsPreRegistrationListContainer/constants.js b/src/views/PatronsPreRegistrationListContainer/constants.js new file mode 100644 index 000000000..d78313036 --- /dev/null +++ b/src/views/PatronsPreRegistrationListContainer/constants.js @@ -0,0 +1,46 @@ +import { FormattedMessage } from 'react-intl'; + +export const COLUMNS_NAME = { + ACTION: 'ACTION', + FIRST_NAME: 'personal.firstName', + LAST_NAME: 'personal.lastName', + MIDDLE_NAME: 'middleName', + PREFERRED_FIRST_NAME: 'personal.preferredFirstName', + EMAIL: 'personal.email', + PHONE_NUMBER: 'personal.phone', + MOBILE_NUMBER: 'personal.mobilePhone', + ADDRESS: 'address', + EMAIL_COMMUNICATION_PREFERENCES: 'emailCommunicationPreferences', + SUBMISSION_DATE: 'submissionDate', + EMAIL_VERIFICATION: 'emailVerification' +}; + +export const visibleColumns = [ + COLUMNS_NAME.ACTION, + COLUMNS_NAME.FIRST_NAME, + COLUMNS_NAME.LAST_NAME, + COLUMNS_NAME.MIDDLE_NAME, + COLUMNS_NAME.PREFERRED_FIRST_NAME, + COLUMNS_NAME.EMAIL, + COLUMNS_NAME.PHONE_NUMBER, + COLUMNS_NAME.MOBILE_NUMBER, + COLUMNS_NAME.ADDRESS, + COLUMNS_NAME.EMAIL_COMMUNICATION_PREFERENCES, + COLUMNS_NAME.SUBMISSION_DATE, + COLUMNS_NAME.EMAIL_VERIFICATION +]; + +export const columnMapping = { + [COLUMNS_NAME.ACTION]: , + [COLUMNS_NAME.FIRST_NAME]: , + [COLUMNS_NAME.LAST_NAME]: , + [COLUMNS_NAME.MIDDLE_NAME]: , + [COLUMNS_NAME.PREFERRED_FIRST_NAME]: , + [COLUMNS_NAME.EMAIL]: , + [COLUMNS_NAME.PHONE_NUMBER]: , + [COLUMNS_NAME.MOBILE_NUMBER]: , + [COLUMNS_NAME.ADDRESS]: , + [COLUMNS_NAME.EMAIL_COMMUNICATION_PREFERENCES]: , + [COLUMNS_NAME.SUBMISSION_DATE]: , + [COLUMNS_NAME.EMAIL_VERIFICATION]: , +}; diff --git a/src/views/UserSearch/UserSearch.js b/src/views/UserSearch/UserSearch.js index 6d1648761..925b57e8b 100644 --- a/src/views/UserSearch/UserSearch.js +++ b/src/views/UserSearch/UserSearch.js @@ -45,6 +45,7 @@ import CashDrawerReconciliationReportPDF from '../../components/data/reports/cas import CashDrawerReconciliationReportCSV from '../../components/data/reports/cashDrawerReconciliationReportCSV'; import FinancialTransactionsReport from '../../components/data/reports/FinancialTransactionsReport'; import LostItemsLink from '../../components/LostItemsLink'; +import LinkToPatronPreRegistrations from '../../components/LinkToPatronPreRegistrations'; import PatronNoticePrintJobsLink from '../../components/PatronNoticePrintJobsLink'; import Filters from './Filters'; @@ -293,6 +294,7 @@ class UserSearch extends React.Component { + { useCustomFields: jest.fn(() => [[customField]]), }; }); +jest.mock('../../components/LinkToPatronPreRegistrations', () => { + return () =>
LinkToPatronPreRegistrations
; +}); const defaultProps = { mutator: {}, @@ -28,6 +32,7 @@ const defaultProps = { }, stripes: { hasInterface: jest.fn(), + hasPerm: () => true, }, location: {}, history: {}, @@ -47,6 +52,20 @@ const renderComponent = (props) => renderWithRouter( { it('should render component', () => { renderComponent(); - expect(screen.getByText('ui-users.status')).toBeTruthy(); + expect(screen.getByText('ui-users.userSearchResults')).toBeInTheDocument(); + }); + + it('should render actions menu', () => { + renderComponent(); + expect(screen.getByText('ui-users.actions')).toBeInTheDocument(); + }); + + it('should display "Search patron preregistration records"', async () => { + renderComponent(); + const actionsButton = screen.getByText('ui-users.actions'); + + await userEvent.click(actionsButton); + + expect(screen.getByText('LinkToPatronPreRegistrations')).toBeInTheDocument(); }); }); diff --git a/src/views/index.js b/src/views/index.js index 3012560a8..53525605a 100644 --- a/src/views/index.js +++ b/src/views/index.js @@ -9,3 +9,4 @@ export { default as AccountsListing } from './AccountsListing/AccountsListing'; export { default as NoteCreatePage } from './Notes/NoteCreatePage'; export { default as NoteEditPage } from './Notes/NoteEditPage'; export { default as NoteViewPage } from './Notes/NoteViewPage'; +export { default as PatronsPreRegistrationList } from './PatronsPreRegistrationListContainer/PatronsPreRegistrationListContainer'; diff --git a/test/jest/fixtures/preRegistrationRecords.json b/test/jest/fixtures/preRegistrationRecords.json new file mode 100644 index 000000000..b61479978 --- /dev/null +++ b/test/jest/fixtures/preRegistrationRecords.json @@ -0,0 +1,36 @@ +[ + { + "id": "58fcba1e-87d2-4ae7-a0a3-36d2425837c2", + "isEmailVerified": true, + "status": "TIER-2", + "generalInfo": { + "firstName": "new-record-1", + "preferredFirstName": "New Record", + "middleName": "", + "lastName": "new-record-1" + }, + "addressInfo": { + "addressLine0": "123 Main St", + "addressLine1": "Apt 4B", + "city": "Metropolis", + "province": "NY", + "zip": "12345", + "country": "USA" + }, + "contactInfo": { + "phone": "555-123456", + "mobilePhone": "555-5678", + "email": "new-record-1@test.com" + }, + "preferredEmailCommunication": [ + "Support", + "Programs" + ], + "metadata": { + "createdDate": "2024-04-29T13:29:41.711+00:00", + "createdByUserId": "6d8a6bf2-5b5f-4168-aa2f-5c4e67b4b65c", + "updatedDate": "2024-04-29T13:29:41.711+00:00", + "updatedByUserId": "6d8a6bf2-5b5f-4168-aa2f-5c4e67b4b65c" + } + } +] diff --git a/translations/ui-users/en.json b/translations/ui-users/en.json index bbdef36af..775db9ba1 100644 --- a/translations/ui-users/en.json +++ b/translations/ui-users/en.json @@ -66,6 +66,7 @@ "user.unknown": "Unknown user", "action": "Action", "actionMenu.lostItems": "Lost items requiring actual cost", + "actionMenu.preRegistrationRecords": "Search patron pre-registration records", "actionMenu.patronNoticePrintJobs": "View patron notice print jobs (PDF)", "dueDate": "Due date", "loanDate": "Loan date", @@ -1109,6 +1110,7 @@ "permission.remove-patron-notice-print-jobs": "Users: View and remove patron notice print jobs", "permission.view-reading-room-access": "Users: Can view reading room access", "permission.edit-reading-room-access": "Users: Can view, and edit reading room access", + "permission.patron-pre-registrations-view": "Users: Can view patron pre-registration data", "bulkClaimReturned.item.title": "Title", "bulkClaimReturned.preConfirm": "Confirm claim returned", "bulkClaimReturned.postConfirm": "Claim returned confirmation", @@ -1192,7 +1194,6 @@ "patronNoticePrintJobs.updated": "Updated", "patronNoticePrintJobs.created": "Created", "patronNoticePrintJobs.errors.pdf": "'PDF generation failed", - "roles.userRoles": "User roles", "roles.empty": "No user roles found", "roles.deleteRole": "Delete user role", @@ -1206,5 +1207,23 @@ "roles.modal.unassignAll.label": "You are unassigning all user roles {roles}

Are you sure?

", "roles.modal.search.header": "Search & filter", "roles.modal.filter.status.label": "Role assigment status", - "roles.modal.header": "Select user roles" + "roles.modal.header": "Select user roles", + "stagingRecords.list.search": "Search", + "stagingRecords.search.placeholder": "Search by name or email", + "stagingRecords.search.label": "Staging records search", + "stagingRecords.list.searchResults": "Patron pre-registration record results", + "stagingRecords.list.columnNames.action": "Action", + "stagingRecords.list.columnNames.firstName": "First name", + "stagingRecords.list.columnNames.lastName": "Last name", + "stagingRecords.list.columnNames.middleName": "Middle name", + "stagingRecords.list.columnNames.preferredFirstName": "Preferred first name", + "stagingRecords.list.columnNames.email": "Email", + "stagingRecords.list.columnNames.phoneNumber": "Phone number", + "stagingRecords.list.columnNames.mobileNumber": "Mobile number", + "stagingRecords.list.columnNames.address": "Address", + "stagingRecords.list.columnNames.emailCommunicationPreferences": "Email communication preferences", + "stagingRecords.list.columnNames.submissionDate": "Submission date", + "stagingRecords.list.columnNames.emailVerification": "Email verification", + "stagingRecords.activated": "Activated", + "stagingRecords.notActivated": "Not activated" }