Merge pull request #7772 from mabashian/convert-ResourceAccessList-functional

Converts ResourceAccessList to functional component

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-07-30 18:10:29 +00:00
committed by GitHub
2 changed files with 193 additions and 320 deletions

View File

@@ -1,19 +1,14 @@
import React, { Fragment } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { withRouter } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { TeamsAPI, UsersAPI } from '../../api'; import { TeamsAPI, UsersAPI } from '../../api';
import AddResourceRole from '../AddRole/AddResourceRole'; import AddResourceRole from '../AddRole/AddResourceRole';
import AlertModal from '../AlertModal'; import AlertModal from '../AlertModal';
import DataListToolbar from '../DataListToolbar'; import DataListToolbar from '../DataListToolbar';
import PaginatedDataList, { ToolbarAddButton } from '../PaginatedDataList'; import PaginatedDataList, { ToolbarAddButton } from '../PaginatedDataList';
import { import { getQSConfig, parseQueryString } from '../../util/qs';
getQSConfig, import useRequest, { useDeleteItems } from '../../util/useRequest';
encodeQueryString,
parseQueryString,
} from '../../util/qs';
import DeleteRoleConfirmationModal from './DeleteRoleConfirmationModal'; import DeleteRoleConfirmationModal from './DeleteRoleConfirmationModal';
import ResourceAccessListItem from './ResourceAccessListItem'; import ResourceAccessListItem from './ResourceAccessListItem';
@@ -23,143 +18,61 @@ const QS_CONFIG = getQSConfig('access', {
order_by: 'first_name', order_by: 'first_name',
}); });
class ResourceAccessList extends React.Component { function ResourceAccessList({ i18n, apiModel, resource }) {
constructor(props) { const [deletionRecord, setDeletionRecord] = useState(null);
super(props); const [deletionRole, setDeletionRole] = useState(null);
this.state = { const [showAddModal, setShowAddModal] = useState(false);
accessRecords: [], const [showDeleteModal, setShowDeleteModal] = useState(false);
contentError: null, const location = useLocation();
hasContentLoading: true,
hasDeletionError: false,
deletionRecord: null,
deletionRole: null,
isAddModalOpen: false,
itemCount: 0,
};
this.loadAccessList = this.loadAccessList.bind(this);
this.handleAddClose = this.handleAddClose.bind(this);
this.handleAddOpen = this.handleAddOpen.bind(this);
this.handleAddSuccess = this.handleAddSuccess.bind(this);
this.handleDeleteCancel = this.handleDeleteCancel.bind(this);
this.handleDeleteConfirm = this.handleDeleteConfirm.bind(this);
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
this.handleDeleteOpen = this.handleDeleteOpen.bind(this);
}
componentDidMount() { const {
this.loadAccessList(); result: { accessRecords, itemCount },
} error: contentError,
isLoading,
componentDidUpdate(prevProps) { request: fetchAccessRecords,
const { location } = this.props; } = useRequest(
useCallback(async () => {
const prevParams = parseQueryString(QS_CONFIG, prevProps.location.search);
const currentParams = parseQueryString(QS_CONFIG, location.search);
if (encodeQueryString(currentParams) !== encodeQueryString(prevParams)) {
this.loadAccessList();
}
}
async loadAccessList() {
const { apiModel, resource, location } = this.props;
const params = parseQueryString(QS_CONFIG, location.search); const params = parseQueryString(QS_CONFIG, location.search);
const response = await apiModel.readAccessList(resource.id, params);
this.setState({ contentError: null, hasContentLoading: true }); return {
try { accessRecords: response.data.results,
const { itemCount: response.data.count,
data: { results: accessRecords = [], count: itemCount = 0 }, };
} = await apiModel.readAccessList(resource.id, params); }, [apiModel, location, resource.id]),
this.setState({ itemCount, accessRecords }); {
} catch (err) { accessRecords: [],
this.setState({ contentError: err }); itemCount: 0,
} finally {
this.setState({ hasContentLoading: false });
} }
}
handleDeleteOpen(deletionRole, deletionRecord) {
this.setState({ deletionRole, deletionRecord });
}
handleDeleteCancel() {
this.setState({ deletionRole: null, deletionRecord: null });
}
handleDeleteErrorClose() {
this.setState({
hasDeletionError: false,
deletionRecord: null,
deletionRole: null,
});
}
async handleDeleteConfirm() {
const { deletionRole, deletionRecord } = this.state;
if (!deletionRole || !deletionRecord) {
return;
}
let promise;
if (typeof deletionRole.team_id !== 'undefined') {
promise = TeamsAPI.disassociateRole(
deletionRole.team_id,
deletionRole.id
); );
} else {
promise = UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id);
}
this.setState({ hasContentLoading: true }); useEffect(() => {
try { fetchAccessRecords();
await promise.then(this.loadAccessList); }, [fetchAccessRecords]);
this.setState({
deletionRole: null,
deletionRecord: null,
});
} catch (error) {
this.setState({
hasContentLoading: false,
hasDeletionError: true,
});
}
}
handleAddClose() {
this.setState({ isAddModalOpen: false });
}
handleAddOpen() {
this.setState({ isAddModalOpen: true });
}
handleAddSuccess() {
this.setState({ isAddModalOpen: false });
this.loadAccessList();
}
render() {
const { resource, i18n } = this.props;
const { const {
accessRecords, isLoading: isDeleteLoading,
contentError, deleteItems: deleteRole,
hasContentLoading, deletionError,
deletionRole, clearDeletionError,
deletionRecord, } = useDeleteItems(
hasDeletionError, useCallback(async () => {
itemCount, if (typeof deletionRole.team_id !== 'undefined') {
isAddModalOpen, return TeamsAPI.disassociateRole(deletionRole.team_id, deletionRole.id);
} = this.state; }
const canEdit = resource.summary_fields.user_capabilities.edit; return UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id);
const isDeleteModalOpen = /* eslint-disable-next-line react-hooks/exhaustive-deps */
!hasContentLoading && !hasDeletionError && deletionRole; }, [deletionRole]),
{
qsConfig: QS_CONFIG,
fetchItems: fetchAccessRecords,
}
);
return ( return (
<Fragment> <>
<PaginatedDataList <PaginatedDataList
error={contentError} error={contentError}
hasContentLoading={hasContentLoading} hasContentLoading={isLoading || isDeleteLoading}
items={accessRecords} items={accessRecords}
itemCount={itemCount} itemCount={itemCount}
pluralizedItemName={i18n._(t`Roles`)} pluralizedItemName={i18n._(t`Roles`)}
@@ -171,11 +84,11 @@ class ResourceAccessList extends React.Component {
isDefault: true, isDefault: true,
}, },
{ {
name: i18n._(t`First Name`), name: i18n._(t`First name`),
key: 'first_name', key: 'first_name',
}, },
{ {
name: i18n._(t`Last Name`), name: i18n._(t`Last name`),
key: 'last_name', key: 'last_name',
}, },
]} ]}
@@ -185,11 +98,11 @@ class ResourceAccessList extends React.Component {
key: 'username', key: 'username',
}, },
{ {
name: i18n._(t`First Name`), name: i18n._(t`First name`),
key: 'first_name', key: 'first_name',
}, },
{ {
name: i18n._(t`Last Name`), name: i18n._(t`Last name`),
key: 'last_name', key: 'last_name',
}, },
]} ]}
@@ -198,11 +111,11 @@ class ResourceAccessList extends React.Component {
{...props} {...props}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={ additionalControls={
canEdit resource?.summary_fields?.user_capabilities?.edit
? [ ? [
<ToolbarAddButton <ToolbarAddButton
key="add" key="add"
onClick={this.handleAddOpen} onClick={() => setShowAddModal(true)}
/>, />,
] ]
: [] : []
@@ -213,37 +126,52 @@ class ResourceAccessList extends React.Component {
<ResourceAccessListItem <ResourceAccessListItem
key={accessRecord.id} key={accessRecord.id}
accessRecord={accessRecord} accessRecord={accessRecord}
onRoleDelete={this.handleDeleteOpen} onRoleDelete={(role, record) => {
setDeletionRecord(record);
setDeletionRole(role);
setShowDeleteModal(true);
}}
/> />
)} )}
/> />
{isAddModalOpen && ( {showAddModal && (
<AddResourceRole <AddResourceRole
onClose={this.handleAddClose} onClose={() => setShowAddModal(false)}
onSave={this.handleAddSuccess} onSave={() => {
setShowAddModal(false);
fetchAccessRecords();
}}
roles={resource.summary_fields.object_roles} roles={resource.summary_fields.object_roles}
/> />
)} )}
{isDeleteModalOpen && ( {showDeleteModal && (
<DeleteRoleConfirmationModal <DeleteRoleConfirmationModal
role={deletionRole} role={deletionRole}
username={deletionRecord.username} username={deletionRecord.username}
onCancel={this.handleDeleteCancel} onCancel={() => {
onConfirm={this.handleDeleteConfirm} setDeletionRecord(null);
setDeletionRole(null);
setShowDeleteModal(false);
}}
onConfirm={async () => {
await deleteRole();
setShowDeleteModal(false);
setDeletionRecord(null);
setDeletionRole(null);
}}
/> />
)} )}
{deletionError && (
<AlertModal <AlertModal
isOpen={hasDeletionError} isOpen={deletionError}
variant="error" variant="error"
title={i18n._(t`Error!`)} title={i18n._(t`Error!`)}
onClose={this.handleDeleteErrorClose} onClose={clearDeletionError}
> >
{i18n._(t`Failed to delete role`)} {i18n._(t`Failed to delete role`)}
</AlertModal> </AlertModal>
</Fragment> )}
</>
); );
} }
} export default withI18n()(ResourceAccessList);
export { ResourceAccessList as _ResourceAccessList };
export default withI18n()(withRouter(ResourceAccessList));

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { sleep } from '../../../testUtils/testUtils';
import { import {
mountWithContexts, mountWithContexts,
waitForElement, waitForElement,
@@ -13,6 +12,7 @@ import ResourceAccessList from './ResourceAccessList';
jest.mock('../../api'); jest.mock('../../api');
describe('<ResourceAccessList />', () => { describe('<ResourceAccessList />', () => {
let wrapper;
const organization = { const organization = {
id: 1, id: 1,
name: 'Default', name: 'Default',
@@ -74,108 +74,68 @@ describe('<ResourceAccessList />', () => {
], ],
}; };
beforeEach(() => { beforeEach(async () => {
OrganizationsAPI.readAccessList.mockResolvedValue({ data }); OrganizationsAPI.readAccessList.mockResolvedValue({ data });
TeamsAPI.disassociateRole.mockResolvedValue({}); TeamsAPI.disassociateRole.mockResolvedValue({});
UsersAPI.disassociateRole.mockResolvedValue({}); UsersAPI.disassociateRole.mockResolvedValue({});
await act(async () => {
wrapper = mountWithContexts(
<ResourceAccessList
resource={organization}
apiModel={OrganizationsAPI}
/>
);
});
wrapper.update();
}); });
afterEach(() => { afterEach(() => {
wrapper.unmount();
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('initially renders succesfully', () => { test('initially renders succesfully', () => {
const wrapper = mountWithContexts(
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
);
expect(wrapper.find('PaginatedDataList')).toHaveLength(1); expect(wrapper.find('PaginatedDataList')).toHaveLength(1);
}); });
test('should fetch and display access records on mount', async done => { test('should fetch and display access records on mount', async done => {
const wrapper = mountWithContexts( await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} /> expect(OrganizationsAPI.readAccessList).toHaveBeenCalled();
); expect(wrapper.find('ResourceAccessListItem').length).toBe(2);
await waitForElement(
wrapper,
'ResourceAccessListItem',
el => el.length === 2
);
expect(wrapper.find('PaginatedDataList').prop('items')).toEqual(
data.results
);
expect(wrapper.find('ResourceAccessList').state('hasContentLoading')).toBe(
false
);
expect(wrapper.find('ResourceAccessList').state('contentError')).toBe(null);
done(); done();
}); });
test('should open confirmation dialog when deleting role', async done => { test('should open and close confirmation dialog when deleting role', async done => {
const wrapper = mountWithContexts( await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} /> expect(wrapper.find('DeleteRoleConfirmationModal')).toHaveLength(0);
);
await sleep(0);
wrapper.update();
const button = wrapper.find('Chip Button').at(0); const button = wrapper.find('Chip Button').at(0);
await act(async () => {
button.prop('onClick')(); button.prop('onClick')();
wrapper.update();
const component = wrapper.find('ResourceAccessList');
expect(component.state('deletionRole')).toEqual(
data.results[0].summary_fields.direct_access[0].role
);
expect(component.state('deletionRecord')).toEqual(data.results[0]);
expect(component.find('DeleteRoleConfirmationModal')).toHaveLength(1);
done();
}); });
it('should close dialog when cancel button clicked', async done => {
const wrapper = mountWithContexts(
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
);
await sleep(0);
wrapper.update(); wrapper.update();
const button = wrapper.find('Chip Button').at(0); expect(wrapper.find('DeleteRoleConfirmationModal')).toHaveLength(1);
button.prop('onClick')(); await act(async () => {
wrapper.update();
wrapper.find('DeleteRoleConfirmationModal').prop('onCancel')(); wrapper.find('DeleteRoleConfirmationModal').prop('onCancel')();
const component = wrapper.find('ResourceAccessList'); });
expect(component.state('deletionRole')).toBeNull(); wrapper.update();
expect(component.state('deletionRecord')).toBeNull(); expect(wrapper.find('DeleteRoleConfirmationModal')).toHaveLength(0);
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled(); expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled(); expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
done(); done();
}); });
it('should delete user role', async done => { it('should delete user role', async done => {
const wrapper = mountWithContexts( await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} /> const button = wrapper.find('Chip Button').at(0);
); await act(async () => {
const button = await waitForElement( button.prop('onClick')();
wrapper, });
'Chip Button',
el => el.length === 2
);
button.at(0).prop('onClick')();
const confirmation = await waitForElement(
wrapper,
'DeleteRoleConfirmationModal'
);
confirmation.prop('onConfirm')();
await waitForElement(
wrapper,
'DeleteRoleConfirmationModal',
el => el.length === 0
);
await sleep(0);
wrapper.update(); wrapper.update();
const component = wrapper.find('ResourceAccessList'); await act(async () => {
expect(component.state('deletionRole')).toBeNull(); wrapper.find('DeleteRoleConfirmationModal').prop('onConfirm')();
expect(component.state('deletionRecord')).toBeNull(); });
wrapper.update();
expect(wrapper.find('DeleteRoleConfirmationModal')).toHaveLength(0);
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled(); expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
expect(UsersAPI.disassociateRole).toHaveBeenCalledWith(1, 1); expect(UsersAPI.disassociateRole).toHaveBeenCalledWith(1, 1);
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2); expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
@@ -183,32 +143,17 @@ describe('<ResourceAccessList />', () => {
}); });
it('should delete team role', async done => { it('should delete team role', async done => {
const wrapper = mountWithContexts( await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} /> const button = wrapper.find('Chip Button').at(1);
); await act(async () => {
const button = await waitForElement( button.prop('onClick')();
wrapper, });
'Chip Button',
el => el.length === 2
);
button.at(1).prop('onClick')();
const confirmation = await waitForElement(
wrapper,
'DeleteRoleConfirmationModal'
);
confirmation.prop('onConfirm')();
await waitForElement(
wrapper,
'DeleteRoleConfirmationModal',
el => el.length === 0
);
await sleep(0);
wrapper.update(); wrapper.update();
const component = wrapper.find('ResourceAccessList'); await act(async () => {
expect(component.state('deletionRole')).toBeNull(); wrapper.find('DeleteRoleConfirmationModal').prop('onConfirm')();
expect(component.state('deletionRecord')).toBeNull(); });
wrapper.update();
expect(wrapper.find('DeleteRoleConfirmationModal')).toHaveLength(0);
expect(TeamsAPI.disassociateRole).toHaveBeenCalledWith(5, 3); expect(TeamsAPI.disassociateRole).toHaveBeenCalledWith(5, 3);
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled(); expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2); expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);