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
commit 91d3f954cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 193 additions and 320 deletions

View File

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

View File

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