diff --git a/awx/ui_next/src/screens/Team/TeamAdd/TeamAdd.test.jsx b/awx/ui_next/src/screens/Team/TeamAdd/TeamAdd.test.jsx index ca8c7caa56..20eb762710 100644 --- a/awx/ui_next/src/screens/Team/TeamAdd/TeamAdd.test.jsx +++ b/awx/ui_next/src/screens/Team/TeamAdd/TeamAdd.test.jsx @@ -12,6 +12,7 @@ describe('', () => { const updatedTeamData = { name: 'new name', description: 'new description', + organization: 1, }; wrapper.find('TeamForm').prop('handleSubmit')(updatedTeamData); expect(TeamsAPI.create).toHaveBeenCalledWith(updatedTeamData); @@ -40,11 +41,18 @@ describe('', () => { const teamData = { name: 'new name', description: 'new description', + organization: 1, }; TeamsAPI.create.mockResolvedValueOnce({ data: { id: 5, ...teamData, + summary_fields: { + organization: { + id: 1, + name: 'Default', + }, + }, }, }); const wrapper = mountWithContexts(, { diff --git a/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx b/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx index a8c35e7039..a6580b4ce9 100644 --- a/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx +++ b/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx @@ -13,7 +13,6 @@ class TeamEdit extends Component { super(props); this.handleSubmit = this.handleSubmit.bind(this); - this.submitInstanceGroups = this.submitInstanceGroups.bind(this); this.handleCancel = this.handleCancel.bind(this); this.handleSuccess = this.handleSuccess.bind(this); @@ -22,11 +21,10 @@ class TeamEdit extends Component { }; } - async handleSubmit(values, groupsToAssociate, groupsToDisassociate) { + async handleSubmit(values) { const { team } = this.props; try { await TeamsAPI.update(team.id, values); - await this.submitInstanceGroups(groupsToAssociate, groupsToDisassociate); this.handleSuccess(); } catch (err) { this.setState({ error: err }); @@ -49,24 +47,6 @@ class TeamEdit extends Component { history.push(`/teams/${id}/details`); } - async submitInstanceGroups(groupsToAssociate, groupsToDisassociate) { - const { team } = this.props; - try { - await Promise.all( - groupsToAssociate.map(id => - TeamsAPI.associateInstanceGroup(team.id, id) - ) - ); - await Promise.all( - groupsToDisassociate.map(id => - TeamsAPI.disassociateInstanceGroup(team.id, id) - ) - ); - } catch (err) { - this.setState({ error: err }); - } - } - render() { const { team } = this.props; const { error } = this.state; diff --git a/awx/ui_next/src/screens/Team/TeamList/TeamListItem.jsx b/awx/ui_next/src/screens/Team/TeamList/TeamListItem.jsx index c77ed7dd31..73ddbcad14 100644 --- a/awx/ui_next/src/screens/Team/TeamList/TeamListItem.jsx +++ b/awx/ui_next/src/screens/Team/TeamList/TeamListItem.jsx @@ -1,15 +1,20 @@ import React, { Fragment } from 'react'; import { string, bool, func } from 'prop-types'; import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; import { DataListItem, DataListItemRow, DataListItemCells, + Tooltip, } from '@patternfly/react-core'; import { Link } from 'react-router-dom'; +import { PencilAltIcon } from '@patternfly/react-icons'; +import ActionButtonCell from '@components/ActionButtonCell'; import DataListCell from '@components/DataListCell'; import DataListCheck from '@components/DataListCheck'; +import ListActionButton from '@components/ListActionButton'; import VerticalSeparator from '@components/VerticalSeparator'; import { Team } from '@types'; @@ -22,7 +27,7 @@ class TeamListItem extends React.Component { }; render() { - const { team, isSelected, onSelect, detailUrl } = this.props; + const { team, isSelected, onSelect, detailUrl, i18n } = this.props; const labelId = `check-action-${team.id}`; return ( @@ -48,7 +53,9 @@ class TeamListItem extends React.Component { {team.summary_fields.organization && ( - Organization + + {i18n._(t`Organization`)} + @@ -57,9 +64,19 @@ class TeamListItem extends React.Component { )} , - - edit button goes here - , + + {team.summary_fields.user_capabilities.edit && ( + + + + + + )} + , ]} /> diff --git a/awx/ui_next/src/screens/Team/TeamList/TeamListItem.test.jsx b/awx/ui_next/src/screens/Team/TeamList/TeamListItem.test.jsx index b4d10fa56e..73d9329c7e 100644 --- a/awx/ui_next/src/screens/Team/TeamList/TeamListItem.test.jsx +++ b/awx/ui_next/src/screens/Team/TeamList/TeamListItem.test.jsx @@ -16,9 +16,8 @@ describe('', () => { id: 1, name: 'Team 1', summary_fields: { - organization: { - id: 1, - name: 'Default', + user_capabilities: { + edit: true, }, }, }} @@ -30,4 +29,50 @@ describe('', () => { ); }); + test('edit button shown to users with edit capabilities', () => { + const wrapper = mountWithContexts( + + + {}} + /> + + + ); + expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); + }); + test('edit button hidden from users without edit capabilities', () => { + const wrapper = mountWithContexts( + + + {}} + /> + + + ); + expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); + }); }); diff --git a/awx/ui_next/src/screens/Team/shared/TeamForm.jsx b/awx/ui_next/src/screens/Team/shared/TeamForm.jsx index 9259b008bb..65a3a12cbd 100644 --- a/awx/ui_next/src/screens/Team/shared/TeamForm.jsx +++ b/awx/ui_next/src/screens/Team/shared/TeamForm.jsx @@ -1,98 +1,91 @@ -import React, { Component } from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; - -import { withRouter } from 'react-router-dom'; -import { Formik } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; - +import { Formik, Field } from 'formik'; import { Form } from '@patternfly/react-core'; - -import FormRow from '@components/FormRow'; -import FormField from '@components/FormField'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; +import FormField from '@components/FormField'; +import FormRow from '@components/FormRow'; +import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import { required } from '@util/validators'; -class TeamForm extends Component { - constructor(props) { - super(props); +function TeamForm(props) { + const { team, handleCancel, handleSubmit, i18n } = props; + const [organization, setOrganization] = useState( + team.summary_fields ? team.summary_fields.organization : null + ); - this.handleSubmit = this.handleSubmit.bind(this); - - this.state = { - formIsValid: true, - }; - } - - isEditingNewTeam() { - const { team } = this.props; - return !team.id; - } - - handleSubmit(values) { - const { handleSubmit } = this.props; - - handleSubmit(values); - } - - render() { - const { team, handleCancel, i18n } = this.props; - const { formIsValid, error } = this.state; - - return ( - ( -
- - - - - ( + + + - {error ?
error
: null} - - )} - /> - ); - } + + ( + form.setFieldTouched('organization')} + onChange={value => { + form.setFieldValue('organization', value.id); + setOrganization(value); + }} + value={organization} + required + /> + )} + /> +
+ + + )} + /> + ); } -FormField.propTypes = { - label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired, -}; - TeamForm.propTypes = { - team: PropTypes.shape(), - handleSubmit: PropTypes.func.isRequired, handleCancel: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + team: PropTypes.shape({}), }; TeamForm.defaultProps = { - team: { - name: '', - description: '', - }, + team: {}, }; -export { TeamForm as _TeamForm }; -export default withI18n()(withRouter(TeamForm)); +export default withI18n()(TeamForm); diff --git a/awx/ui_next/src/screens/Team/shared/TeamForm.test.jsx b/awx/ui_next/src/screens/Team/shared/TeamForm.test.jsx index 1aaca85393..0d8a483417 100644 --- a/awx/ui_next/src/screens/Team/shared/TeamForm.test.jsx +++ b/awx/ui_next/src/screens/Team/shared/TeamForm.test.jsx @@ -1,6 +1,6 @@ import React from 'react'; - -import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { sleep } from '@testUtils/testUtils'; import TeamForm from './TeamForm'; @@ -8,6 +8,7 @@ import TeamForm from './TeamForm'; jest.mock('@api'); describe('', () => { + let wrapper; const meConfig = { me: { is_superuser: false, @@ -17,14 +18,20 @@ describe('', () => { id: 1, name: 'Foo', description: 'Bar', + organization: 1, + summary_fields: { + id: 1, + name: 'Default', + }, }; afterEach(() => { + wrapper.unmount(); jest.clearAllMocks(); }); test('changing inputs should update form values', () => { - const wrapper = mountWithContexts( + wrapper = mountWithContexts( ', () => { target: { value: 'new bar', name: 'description' }, }); expect(form.state('values').description).toEqual('new bar'); + act(() => { + wrapper.find('OrganizationLookup').invoke('onBlur')(); + wrapper.find('OrganizationLookup').invoke('onChange')({ + id: 2, + name: 'Other Org', + }); + }); + expect(form.state('values').organization).toEqual(2); }); - test('calls handleSubmit when form submitted', async () => { + test('should call handleSubmit when Submit button is clicked', async () => { const handleSubmit = jest.fn(); - const wrapper = mountWithContexts( - - ); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); expect(handleSubmit).not.toHaveBeenCalled(); wrapper.find('button[aria-label="Save"]').simulate('click'); await sleep(1); - expect(handleSubmit).toHaveBeenCalledWith({ - name: 'Foo', - description: 'Bar', - }); + expect(handleSubmit).toBeCalled(); }); - test('calls "handleCancel" when Cancel button is clicked', () => { + test('calls handleCancel when Cancel button is clicked', () => { const handleCancel = jest.fn(); - const wrapper = mountWithContexts( + wrapper = mountWithContexts(