Skip to content

Commit

Permalink
UIU-3115 - reading room access accordion on user edit view (#2694)
Browse files Browse the repository at this point in the history
* UIU-3115 - reading room acecss acoordion on user edit view

* UIU-3115 - add test data

* UIU-3115 - cleanup

* UIU-3115 - fix failed tests

* UIU-3115 - fix sonar complaints

* UIU-3115 - refine, add axe tests

* UIU-3115 - fix corner case

* UIU-3115 - add unit test

* UIU-3115 - update test to cover a scenario and increase coverage.

* UIU-3115 - add MCL column widths

* UIU-3115 - fix review comments
  • Loading branch information
Terala-Priyanka authored May 24, 2024
1 parent c666aba commit eb84b32
Show file tree
Hide file tree
Showing 14 changed files with 1,983 additions and 59 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* Create new permission 'Users: Can view, and edit reading room access'. Refs UIU-3117.
* Include DCB in 'User Type' search filter group. Refs UIU-3016.
* Displaying Default Reading Room Access in User Records. Refs UIU-3114.
* Implement Reading Room Access functionality in user profile edit. Refs UIU-3115.

## [10.1.1](https://github.com/folio-org/ui-users/tree/v10.1.1) (2024-05-07)
[Full Changelog](https://github.com/folio-org/ui-users/compare/v10.1.0...v10.1.1)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,7 @@
"@folio/jest-config-stripes": "^2.0.0",
"@folio/stripes": "^9.0.0",
"@folio/stripes-cli": "^3.0.0",
"@folio/stripes-testing": "^4.4.0",
"@formatjs/cli": "^6.1.3",
"core-js": "^3.6.4",
"eslint": "^7.32.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { noop } from 'lodash';

import {
Accordion,
Badge,
Headline,
MultiColumnList,
} from '@folio/stripes/components';

import { rraColumns } from './constants';
import { getFormatter } from './getFormatter';

const EditReadingRoomAccess = ({
expanded,
onToggle,
accordionId,
form,
formData,
}) => {
const columnMapping = {
[rraColumns.ACCESS]: <FormattedMessage id="ui-users.readingRoom.access" />,
[rraColumns.READING_ROOM_NAME]: <FormattedMessage id="ui-users.readingRoom.name" />,
[rraColumns.NOTES]: <FormattedMessage id="ui-users.readingRoom.note" />,
};
const visibleColumns = Object.keys(columnMapping);
const columnWidths = {
[rraColumns.ACCESS]: '15%',
[rraColumns.READING_ROOM_NAME]: '25%',
};

useEffect(() => {
const unregisterReadingRoomAccessList = form.registerField('readingRoomsAccessList', noop, { initialValue: [] });
return () => {
unregisterReadingRoomAccessList();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<Accordion
open={expanded}
id={accordionId}
onToggle={onToggle}
label={<Headline size="large" tag="h3"><FormattedMessage id="ui-users.readingRoom.readingRoomAccess" /></Headline>}
displayWhenClosed={<Badge>{formData.length}</Badge>}
>
<MultiColumnList
striped
contentData={formData}
columnMapping={columnMapping}
visibleColumns={visibleColumns}
formatter={getFormatter(form)}
columnWidths={columnWidths}
/>
</Accordion>
);
};

EditReadingRoomAccess.propTypes = {
expanded: PropTypes.bool,
onToggle: PropTypes.func,
accordionId: PropTypes.string.isRequired,
formData: PropTypes.object,
form: PropTypes.object,
};

export default EditReadingRoomAccess;
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { screen, waitFor, act } from '@folio/jest-config-stripes/testing-library/react';
import userEvent from '@folio/jest-config-stripes/testing-library/user-event';
import { within } from '@folio/jest-config-stripes/testing-library/dom';
import { Form } from 'react-final-form';

import { runAxeTest } from '@folio/stripes-testing';

import renderWithRouter from 'helpers/renderWithRouter';
import '../../../../test/jest/__mock__/matchMedia.mock';

import EditReadingRoomAccess from './EditReadingRoomAccess';

jest.unmock('@folio/stripes/components');

const unregisterFieldMock = jest.fn();
const rraFieldStateMock = {
value: [
{
'id': '2205004b-ca51-4a14-87fd-938eefa8f5df',
'userId': '2205005b-ca51-4a04-87fd-938eefa8f6de',
'readingRoomId': 'ea7ac988-ede1-466b-968c-46a770333b14',
'readingRoomName': 'rr-4',
'access': 'ALLOWED',
'notes': 'Allowed for this reading room...',
'metadata': {
'createdDate': '2024-05-15 18:39:31',
'createdByUserId': '21457ab5-4635-4e56-906a-908f05e9233b',
'updatedDate': '2024-05-15 18:40:27',
'updatedByUserId': '21457ab5-4635-4e56-906a-908f05e9233b'
}
}
]
};
const onSubmit = jest.fn();
const arrayMutators = {
concat: jest.fn(),
move: jest.fn(),
pop: jest.fn(),
push: jest.fn(),
remove: jest.fn(),
removeBatch: jest.fn(),
shift: jest.fn(),
swap: jest.fn(),
unshift: jest.fn(),
update: jest.fn()
};
const renderEditReadingRoomAccess = (props, initialValues) => {
const component = () => (
<>
<EditReadingRoomAccess {...props} />
</>
);
renderWithRouter(
<Form
id="form-user"
mutators={{
...arrayMutators
}}
initialValues={initialValues}
onSubmit={onSubmit}
render={component}
/>
);
};
const props = {
expanded: true,
onToggle: jest.fn(),
accordionId: 'readingRoomAccess',
form: {
change: jest.fn(),
registerField: jest.fn().mockReturnValue(unregisterFieldMock),
getFieldState: jest.fn().mockReturnValue(rraFieldStateMock),
},
formData: [
{
'id': '2205004b-ca51-4a14-87fd-938eefa8f5df',
'userId': '2205005b-ca51-4a04-87fd-938eefa8f6de',
'readingRoomId': 'ea7ac988-ede1-466b-968c-46a770333b14',
'readingRoomName': 'rr-4',
'access': 'ALLOWED',
'notes': 'Allowed for this reading room...',
'metadata': {
'createdDate': '2024-05-15 18:39:31',
'createdByUserId': '21457ab5-4635-4e56-906a-908f05e9233b',
'updatedDate': '2024-05-15 18:40:27',
'updatedByUserId': '21457ab5-4635-4e56-906a-908f05e9233b'
}
},
{
'id': 'fe1d83dc-e3f9-4e57-aa2c-0b245ae7eb19',
'userId': '2205005b-ca51-4a04-87fd-938eefa8f6de',
'readingRoomId': '754c6287-892c-4484-941a-23e050fc8888',
'readingRoomName': 'abc',
'access': 'NOT_ALLOWED',
'notes': '',
'metadata': {
'createdDate': '2024-05-21 07:05:17',
'createdByUserId': '21457ab5-4635-4e56-906a-908f05e9233b',
'updatedDate': '2024-05-21 07:13:11',
'updatedByUserId': '21457ab5-4635-4e56-906a-908f05e9233b'
}
}
],
};
describe('EditReadingRoomAccess', () => {
it('should render with no axe errors', async () => {
await runAxeTest({
rootNode: document.body,
});
});

it('should render component', () => {
renderEditReadingRoomAccess(props);
expect(screen.getByText('ui-users.readingRoom.readingRoomAccess')).toBeDefined();
});

it('should display columns - access, name and note', () => {
renderEditReadingRoomAccess(props);
[
'ui-users.readingRoom.access',
'ui-users.readingRoom.name',
'ui-users.readingRoom.note',
].forEach(col => expect(screen.getByText(col)).toBeDefined());
});

it('should update the notes', async () => {
renderEditReadingRoomAccess(props);
const noteField1 = document.querySelectorAll('[id^=textarea]')[0];
await act(async () => userEvent.type(noteField1, 'note1'));
await waitFor(() => expect(props.form.change).toHaveBeenCalled());
});

it('should update access', async () => {
renderEditReadingRoomAccess(props);
const accessSelectField = document.querySelectorAll('[id=reading-room-access-select]')[1];
await act(async () => userEvent.click(accessSelectField));
const list = screen.getByRole('listbox');
await act(async () => userEvent.click(within(list).getByText('ui-users.readingRoom.notAllowed', { exact: false })));
await waitFor(() => expect(props.form.change).toHaveBeenCalled());
});

it('should update both access and note', async () => {
renderEditReadingRoomAccess(props);
const noteField1 = document.querySelectorAll('[id^=textarea]')[0];
await act(async () => userEvent.type(noteField1, 'note1'));
const accessSelectField = document.querySelectorAll('[id=reading-room-access-select]')[0];
await act(async () => userEvent.click(accessSelectField));
const list = screen.getByRole('listbox');
await act(async () => userEvent.click(within(list).getByText('ui-users.readingRoom.allowed', { exact: false })));
await waitFor(() => expect(props.form.change).toHaveBeenCalled());
});
});
18 changes: 18 additions & 0 deletions src/components/EditSections/EditReadingRoomAccess/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { FormattedMessage } from 'react-intl';

export const rraColumns = {
ACCESS: 'access',
READING_ROOM_NAME: 'readingRoomName',
NOTES: 'notes',
};

export const READING_ROOM_ACCESS_OPTIONS = [
{
label: (<FormattedMessage id="ui-users.readingRoom.allowed" />),
value: 'ALLOWED'
},
{
label: (<FormattedMessage id="ui-users.readingRoom.notAllowed" />),
value: 'NOT_ALLOWED'
}
];
73 changes: 73 additions & 0 deletions src/components/EditSections/EditReadingRoomAccess/getFormatter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* eslint-disable import/prefer-default-export */
import PropTypes from 'prop-types';
import { Field } from 'react-final-form';
import { cloneDeep } from 'lodash';
import { v4 as uuidv4 } from 'uuid';

import { Selection, TextArea } from '@folio/stripes/components';

import { rraColumns, READING_ROOM_ACCESS_OPTIONS } from './constants';

export const getFormatter = (form) => {
const updateRecord = (record, val, name, rowIndex) => {
const fieldState = form.getFieldState('readingRoomsAccessList');
if (fieldState?.value && fieldState.value[rowIndex]) {
form.change(`readingRoomsAccessList[${rowIndex}][${name}]`, val);
} else {
const clonedRecord = cloneDeep(record);
clonedRecord[name] = val;
if (!clonedRecord.id) {
clonedRecord.id = uuidv4();
}
form.change(`readingRoomsAccessList[${rowIndex}]`, clonedRecord);
}
};

return ({
[rraColumns.ACCESS] : Object.assign(
({ rowIndex, ...record }) => (
<Field
name={`${rraColumns.ACCESS}`}
id={`${rraColumns.ACCESS}-${rowIndex}`}
aria-label={`${rraColumns.ACCESS}-${rowIndex}`}
render={({ input }) => {
return (
<Selection
ariaLabel="reading-room access"
dataOptions={READING_ROOM_ACCESS_OPTIONS}
id="reading-room-access-select"
value={record.access}
onChange={(val) => {
updateRecord(record, val, input.name, rowIndex);
}}
/>
);
}}
/>
),
{ rowIndex: PropTypes.number, record: PropTypes.object }
),
[rraColumns.READING_ROOM_NAME] : ({ readingRoomName }) => readingRoomName,
[rraColumns.NOTES] : Object.assign(
({ rowIndex, ...record }) => (
<Field
name={`${rraColumns.NOTES}`}
ariaLabel={`${rraColumns.NOTES}-${rowIndex}`}
id={`${rraColumns.NOTES}-${rowIndex}`}
render={({ input }) => (
<TextArea
{...input}
fullWidth
marginBottom0
value={record?.notes}
onChange={(e) => {
updateRecord(record, e.target.value, input.name, rowIndex);
}}
/>
)}
/>
),
{ rowIndex: PropTypes.number, record: PropTypes.object }
)
});
};
1 change: 1 addition & 0 deletions src/components/EditSections/EditReadingRoomAccess/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './EditReadingRoomAccess';
1 change: 1 addition & 0 deletions src/components/EditSections/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as EditExtendedInfo } from './EditExtendedInfo';
export { default as EditProxy } from './EditProxy';
export { default as EditUserInfo } from './EditUserInfo';
export { default as EditServicePoints } from './EditServicePoints';
export { default as EditReadingRoomAccess } from './EditReadingRoomAccess';
2 changes: 1 addition & 1 deletion src/routes/UserRecordContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ class UserRecordContainer extends React.Component {
return `reading-room-patron-permission/${pathComponents.id}`;
}
}
}
},
});

static propTypes = {
Expand Down
17 changes: 15 additions & 2 deletions src/views/UserEdit/UserEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class UserEdit extends React.Component {
'addressTypes',
'servicePoints',
'departments',
'userReadingRoomPermissions'
);

return formData;
Expand Down Expand Up @@ -201,7 +202,14 @@ class UserEdit extends React.Component {
return copiedCustomFields;
}

update({ requestPreferences, ...userFormData }) {
updateUserReadingRoomAccess(list) {
const { mutator } = this.props;
const payload = list.filter(Boolean);

mutator.userReadingRoomPermissions.PUT(payload);
}

update({ requestPreferences, readingRoomsAccessList, ...userFormData }) {
const {
updateProxies,
updateSponsors,
Expand All @@ -220,6 +228,11 @@ class UserEdit extends React.Component {
const user = cloneDeep(userFormData);
const prevUser = resources?.selUser?.records?.[0] ?? {};

// update user reading room access
if (get(resources, 'userReadingRoomPermissions') && readingRoomsAccessList?.length) {
this.updateUserReadingRoomAccess(readingRoomsAccessList);
}

if (get(resources, 'requestPreferences.records[0].totalRecords')) {
this.updateRequestPreferences(requestPreferences);
} else {
Expand All @@ -246,7 +259,7 @@ class UserEdit extends React.Component {
updateServicePoints(servicePoints, preferredServicePoint);
}

const data = omit(user, ['creds', 'proxies', 'sponsors', 'permissions', 'servicePoints', 'preferredServicePoint']);
const data = omit(user, ['creds', 'proxies', 'sponsors', 'permissions', 'servicePoints', 'preferredServicePoint', 'readingRoomsAccessList']);
const today = moment().endOf('day');
const curActive = user.active;
const prevActive = prevUser.active;
Expand Down
Loading

0 comments on commit eb84b32

Please sign in to comment.