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,227 +18,160 @@ 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);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const location = useLocation();
const {
result: { accessRecords, itemCount },
error: contentError,
isLoading,
request: fetchAccessRecords,
} = useRequest(
useCallback(async () => {
const params = parseQueryString(QS_CONFIG, location.search);
const response = await apiModel.readAccessList(resource.id, params);
return {
accessRecords: response.data.results,
itemCount: response.data.count,
};
}, [apiModel, location, resource.id]),
{
accessRecords: [], accessRecords: [],
contentError: null,
hasContentLoading: true,
hasDeletionError: false,
deletionRecord: null,
deletionRole: null,
isAddModalOpen: false,
itemCount: 0, 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() {
this.loadAccessList();
}
componentDidUpdate(prevProps) {
const { location } = this.props;
const prevParams = parseQueryString(QS_CONFIG, prevProps.location.search);
const currentParams = parseQueryString(QS_CONFIG, location.search);
if (encodeQueryString(currentParams) !== encodeQueryString(prevParams)) {
this.loadAccessList();
} }
} );
async loadAccessList() { useEffect(() => {
const { apiModel, resource, location } = this.props; fetchAccessRecords();
const params = parseQueryString(QS_CONFIG, location.search); }, [fetchAccessRecords]);
this.setState({ contentError: null, hasContentLoading: true }); const {
try { isLoading: isDeleteLoading,
const { deleteItems: deleteRole,
data: { results: accessRecords = [], count: itemCount = 0 }, deletionError,
} = await apiModel.readAccessList(resource.id, params); clearDeletionError,
this.setState({ itemCount, accessRecords }); } = useDeleteItems(
} catch (err) { useCallback(async () => {
this.setState({ contentError: err }); if (typeof deletionRole.team_id !== 'undefined') {
} finally { return TeamsAPI.disassociateRole(deletionRole.team_id, deletionRole.id);
this.setState({ hasContentLoading: false }); }
return UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id);
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [deletionRole]),
{
qsConfig: QS_CONFIG,
fetchItems: fetchAccessRecords,
} }
} );
handleDeleteOpen(deletionRole, deletionRecord) { return (
this.setState({ deletionRole, deletionRecord }); <>
} <PaginatedDataList
error={contentError}
handleDeleteCancel() { hasContentLoading={isLoading || isDeleteLoading}
this.setState({ deletionRole: null, deletionRecord: null }); items={accessRecords}
} itemCount={itemCount}
pluralizedItemName={i18n._(t`Roles`)}
handleDeleteErrorClose() { qsConfig={QS_CONFIG}
this.setState({ toolbarSearchColumns={[
hasDeletionError: false, {
deletionRecord: null, name: i18n._(t`Username`),
deletionRole: null, key: 'username',
}); isDefault: true,
} },
{
async handleDeleteConfirm() { name: i18n._(t`First name`),
const { deletionRole, deletionRecord } = this.state; key: 'first_name',
},
if (!deletionRole || !deletionRecord) { {
return; name: i18n._(t`Last name`),
} key: 'last_name',
},
let promise; ]}
if (typeof deletionRole.team_id !== 'undefined') { toolbarSortColumns={[
promise = TeamsAPI.disassociateRole( {
deletionRole.team_id, name: i18n._(t`Username`),
deletionRole.id key: 'username',
); },
} else { {
promise = UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id); name: i18n._(t`First name`),
} key: 'first_name',
},
this.setState({ hasContentLoading: true }); {
try { name: i18n._(t`Last name`),
await promise.then(this.loadAccessList); key: 'last_name',
this.setState({ },
deletionRole: null, ]}
deletionRecord: null, renderToolbar={props => (
}); <DataListToolbar
} catch (error) { {...props}
this.setState({ qsConfig={QS_CONFIG}
hasContentLoading: false, additionalControls={
hasDeletionError: true, resource?.summary_fields?.user_capabilities?.edit
}); ? [
} <ToolbarAddButton
} key="add"
onClick={() => setShowAddModal(true)}
handleAddClose() { />,
this.setState({ isAddModalOpen: false }); ]
} : []
}
handleAddOpen() { />
this.setState({ isAddModalOpen: true }); )}
} renderItem={accessRecord => (
<ResourceAccessListItem
handleAddSuccess() { key={accessRecord.id}
this.setState({ isAddModalOpen: false }); accessRecord={accessRecord}
this.loadAccessList(); onRoleDelete={(role, record) => {
} setDeletionRecord(record);
setDeletionRole(role);
render() { setShowDeleteModal(true);
const { resource, i18n } = this.props; }}
const { />
accessRecords, )}
contentError, />
hasContentLoading, {showAddModal && (
deletionRole, <AddResourceRole
deletionRecord, onClose={() => setShowAddModal(false)}
hasDeletionError, onSave={() => {
itemCount, setShowAddModal(false);
isAddModalOpen, fetchAccessRecords();
} = this.state; }}
const canEdit = resource.summary_fields.user_capabilities.edit; roles={resource.summary_fields.object_roles}
const isDeleteModalOpen =
!hasContentLoading && !hasDeletionError && deletionRole;
return (
<Fragment>
<PaginatedDataList
error={contentError}
hasContentLoading={hasContentLoading}
items={accessRecords}
itemCount={itemCount}
pluralizedItemName={i18n._(t`Roles`)}
qsConfig={QS_CONFIG}
toolbarSearchColumns={[
{
name: i18n._(t`Username`),
key: 'username',
isDefault: true,
},
{
name: i18n._(t`First Name`),
key: 'first_name',
},
{
name: i18n._(t`Last Name`),
key: 'last_name',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Username`),
key: 'username',
},
{
name: i18n._(t`First Name`),
key: 'first_name',
},
{
name: i18n._(t`Last Name`),
key: 'last_name',
},
]}
renderToolbar={props => (
<DataListToolbar
{...props}
qsConfig={QS_CONFIG}
additionalControls={
canEdit
? [
<ToolbarAddButton
key="add"
onClick={this.handleAddOpen}
/>,
]
: []
}
/>
)}
renderItem={accessRecord => (
<ResourceAccessListItem
key={accessRecord.id}
accessRecord={accessRecord}
onRoleDelete={this.handleDeleteOpen}
/>
)}
/> />
{isAddModalOpen && ( )}
<AddResourceRole {showDeleteModal && (
onClose={this.handleAddClose} <DeleteRoleConfirmationModal
onSave={this.handleAddSuccess} role={deletionRole}
roles={resource.summary_fields.object_roles} username={deletionRecord.username}
/> onCancel={() => {
)} setDeletionRecord(null);
{isDeleteModalOpen && ( setDeletionRole(null);
<DeleteRoleConfirmationModal setShowDeleteModal(false);
role={deletionRole} }}
username={deletionRecord.username} onConfirm={async () => {
onCancel={this.handleDeleteCancel} await deleteRole();
onConfirm={this.handleDeleteConfirm} 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);
button.prop('onClick')(); await act(async () => {
button.prop('onClick')();
});
wrapper.update(); wrapper.update();
expect(wrapper.find('DeleteRoleConfirmationModal')).toHaveLength(1);
const component = wrapper.find('ResourceAccessList'); await act(async () => {
expect(component.state('deletionRole')).toEqual( wrapper.find('DeleteRoleConfirmationModal').prop('onCancel')();
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(0);
button.prop('onClick')();
wrapper.update();
wrapper.find('DeleteRoleConfirmationModal').prop('onCancel')();
const component = wrapper.find('ResourceAccessList');
expect(component.state('deletionRole')).toBeNull();
expect(component.state('deletionRecord')).toBeNull();
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);