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-3115 - reading room access accordion on user edit view #2694

Merged
merged 12 commits into from
May 24, 2024
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
Loading