Skip to content

Commit

Permalink
UIPFU-91 - React v19: refactor away from default props for functional…
Browse files Browse the repository at this point in the history
… components (#285)

* UIPFU-82 - add jest test cases for PluginFindUser.js

* UIPFU-82: refactor

* UIPFU-82: refactor

* UIPFU-82: refactor

* UIPFU-91 - initial commit

* update test

* remove default parameters on Filters component

* add unit tests for UserSearchModal component from scratch

* add props on test file

* fix test

* fix tests

* fix test

* refine

* fix test failures

* refactor couple of components to remove default props.

* fix default prop value

---------

Co-authored-by: manvendra-s-rathore <[email protected]>
Co-authored-by: nikolai <[email protected]>
  • Loading branch information
3 people authored Jan 15, 2025
1 parent d98e258 commit 0935663
Show file tree
Hide file tree
Showing 8 changed files with 600 additions and 600 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change history for ui-plugin-find-user

## In progress

* React v19: refactor away from default props for functional components. Refs UIPFU-91.

## [7.2.0] (https://github.com/folio-org/ui-plugin-find-user/tree/v7.2.0) (2024-09-05)
[Full Changelog](https://github.com/folio-org/ui-plugin-find-user/compare/v7.1.2...v7.2.0)

Expand Down
73 changes: 32 additions & 41 deletions src/Filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,41 @@ import {
FilterGroups,
} from '@folio/stripes/components';

export default class Filters extends React.Component {
static propTypes = {
activeFilters: PropTypes.object,
onChangeHandlers: PropTypes.object.isRequired,
config: PropTypes.arrayOf(PropTypes.object),
resultOffset: PropTypes.shape({
replace: PropTypes.func.isRequired,
}),
};

static defaultProps = {
activeFilters: {},
}

handleFilterChange = e => {
const {
resultOffset,
onChangeHandlers,
} = this.props;

const Filters = ({
activeFilters,
onChangeHandlers,
config,
resultOffset,
}) => {
const { clearGroup } = onChangeHandlers;

const handleFilterChange = e => {
if (resultOffset) {
resultOffset.replace(0);
}

onChangeHandlers.checkbox(e);
}

render() {
const {
activeFilters,
onChangeHandlers: { clearGroup },
config,
} = this.props;

const groupFilters = {};
activeFilters.string.split(',').forEach(m => { groupFilters[m] = true; });
};

return (
<FilterGroups
config={config}
filters={groupFilters}
onChangeFilter={this.handleFilterChange}
onClearFilter={clearGroup}
/>
);
}
}
const groupFilters = {};
activeFilters.string.split(',').forEach(m => { groupFilters[m] = true; });

return (
<FilterGroups
config={config}
filters={groupFilters}
onChangeFilter={handleFilterChange}
onClearFilter={clearGroup}
/>
);
};

Filters.propTypes = {
activeFilters: PropTypes.object,
onChangeHandlers: PropTypes.object.isRequired,
config: PropTypes.arrayOf(PropTypes.object),
resultOffset: PropTypes.shape({
replace: PropTypes.func.isRequired,
}),
};
export default Filters;
6 changes: 4 additions & 2 deletions src/Filters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ const props = {
checkbox: jest.fn(),
},
activeFilters: {
state: {},
string: '',
state: {
active : ['active'],
},
string: 'active.active',
},
resultOffset: {
replace: jest.fn(),
Expand Down
179 changes: 85 additions & 94 deletions src/PluginFindUser.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import _omit from 'lodash/omit';
import className from 'classnames';
Expand All @@ -11,106 +11,97 @@ import UserSearchModal from './UserSearchModal';

import css from './UserSearch.css';

class PluginFindUser extends Component {
constructor(props) {
super(props);

this.state = {
openModal: false,
};

this.openModal = this.openModal.bind(this);
this.closeModal = this.closeModal.bind(this);
this.modalTrigger = React.createRef();
this.modalRef = React.createRef();
}

getStyle() {
const { marginBottom0, marginTop0 } = this.props;
return className(
css.searchControl,
{ [css.marginBottom0]: marginBottom0 },
{ [css.marginTop0]: marginTop0 },
);
}

openModal() {
this.setState({
openModal: true,
});
}

closeModal() {
const {
afterClose
} = this.props;

this.setState({
openModal: false,
}, () => {
const PluginFindUser = (props) => {
const {
afterClose,
id = 'clickable-plugin-find-user',
searchLabel,
searchButtonStyle = 'primary noRightRadius',
marginBottom0,
marginTop0,
// eslint-disable-next-line no-unused-vars
onModalClose,
renderTrigger,
// eslint-disable-next-line no-unused-vars
dataKey = 'find_patron',
// eslint-disable-next-line no-unused-vars
initialSelectedUsers,
} = props;

const [isModalOpen, setIsModalOpen] = useState(false);
const modalTrigger = useRef();
const modalRef = useRef();
const prevOpenModalRef = useRef(); // Reference to track the previous value of openModal

// don't inadvertently pass in other resources which could result in resource confusion.
const isolatedProps = _omit(props, ['parentResources', 'resources', 'mutator', 'parentMutator']);

useEffect(() => {
if (prevOpenModalRef.current && !isModalOpen) {
if (afterClose) {
afterClose();
}

if (this.modalRef.current && this.modalTrigger.current) {
if (contains(this.modalRef.current, document.activeElement)) {
this.modalTrigger.current.focus();
if (modalRef.current && modalTrigger.current) {
if (contains(modalRef.current, document.activeElement)) {
modalTrigger.current.focus();
}
}
});
}

renderTriggerButton() {
const {
renderTrigger,
} = this.props;

return renderTrigger({
buttonRef: this.modalTrigger,
onClick: this.openModal,
});
}

render() {
const { id, marginBottom0, renderTrigger, searchButtonStyle, searchLabel } = this.props;
// don't inadvertently pass in other resources which could result in resource confusion.
const isolatedProps = _omit(this.props, ['parentResources', 'resources', 'mutator', 'parentMutator']);
}
}, [isModalOpen, afterClose]);

return (
<div className={this.getStyle()} data-test-plugin-find-user>
{renderTrigger ?
this.renderTriggerButton() :
<FormattedMessage id="ui-plugin-find-user.searchButton.title">
{ariaLabel => (
<Button
id={id}
key="searchButton"
buttonStyle={searchButtonStyle}
aria-label={ariaLabel}
onClick={this.openModal}
buttonRef={this.modalTrigger}
marginBottom0={marginBottom0}
data-testid="searchButton"
>
{searchLabel || <Icon icon="search" color="#fff" />}
</Button>
)}
</FormattedMessage>}
<UserSearchModal
openWhen={this.state.openModal}
closeCB={this.closeModal}
modalRef={this.modalRef}
{...isolatedProps}
/>
</div>
);
}
}

PluginFindUser.defaultProps = {
id: 'clickable-plugin-find-user',
searchButtonStyle: 'primary noRightRadius',
dataKey: 'find_patron',
const getStyle = () => (
className(
css.searchControl,
{ [css.marginBottom0]: marginBottom0 },
{ [css.marginTop0]: marginTop0 },
)
);

const openModal = () => {
setIsModalOpen(true);
};

const closeModal = () => {
prevOpenModalRef.current = isModalOpen;
setIsModalOpen(false);
};

const renderTriggerButton = () => (
renderTrigger({
buttonRef: modalTrigger,
onClick: openModal,
})
);

return (
<div className={getStyle()} data-test-plugin-find-user>
{renderTrigger ?
renderTriggerButton() :
<FormattedMessage id="ui-plugin-find-user.searchButton.title">
{ariaLabel => (
<Button
id={id}
key="searchButton"
buttonStyle={searchButtonStyle}
aria-label={ariaLabel}
onClick={openModal}
buttonRef={modalTrigger}
marginBottom0={marginBottom0}
data-testid="searchButton"
>
{searchLabel || <Icon icon="search" color="#fff" />}
</Button>
)}
</FormattedMessage>}
<UserSearchModal
openWhen={isModalOpen}
closeCB={closeModal}
modalRef={modalRef}
{...isolatedProps}
/>
</div>
);
};

PluginFindUser.propTypes = {
Expand Down
21 changes: 6 additions & 15 deletions src/PluginFindUser.test.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { screen } from '@folio/jest-config-stripes/testing-library/react';
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 PluginFindUser from './PluginFindUser';

jest.unmock('@folio/stripes/components');
jest.mock('./UserSearchModal', () => ({ closeCB, openWhen, modalRef }) => {
return (
<div>
{openWhen && (
<div ref={modalRef}>
User Search Modal
<button type="button" onClick={closeCB}>Close Modal</button>
</div>
)}
</div>
);
jest.mock('./UserSearchContainer', () => {
return jest.fn(() => <div>UserSearchContainer</div>);
});

const afterCloseMock = jest.fn();
Expand Down Expand Up @@ -86,14 +77,14 @@ describe('PluginFindUser', () => {
renderPluginFindUser(props);
const searchBtn = screen.getByTestId('searchButton');
await userEvent.click(searchBtn);
expect(screen.getByText('User Search Modal')).toBeInTheDocument();
expect(screen.getByText('ui-plugin-find-user.modal.label')).toBeInTheDocument();
});

it('should trigger afterClose() when the modal is closed', async () => {
renderPluginFindUser(props);
const searchBtn = screen.getByTestId('searchButton');
await userEvent.click(searchBtn);
await userEvent.click(screen.getByText('Close Modal'));
expect(afterCloseMock).toHaveBeenCalledTimes(1);
await userEvent.click(screen.getByRole('button', { name : 'stripes-components.dismissModal' }));
await waitFor(() => expect(afterCloseMock).toHaveBeenCalledTimes(1));
});
});
Loading

0 comments on commit 0935663

Please sign in to comment.