mirror of
https://github.com/ansible/awx.git
synced 2026-05-09 18:37:36 -02:30
Adds organization field to team form. Adds edit button to team list items.
This commit is contained in:
@@ -12,6 +12,7 @@ describe('<TeamAdd />', () => {
|
|||||||
const updatedTeamData = {
|
const updatedTeamData = {
|
||||||
name: 'new name',
|
name: 'new name',
|
||||||
description: 'new description',
|
description: 'new description',
|
||||||
|
organization: 1,
|
||||||
};
|
};
|
||||||
wrapper.find('TeamForm').prop('handleSubmit')(updatedTeamData);
|
wrapper.find('TeamForm').prop('handleSubmit')(updatedTeamData);
|
||||||
expect(TeamsAPI.create).toHaveBeenCalledWith(updatedTeamData);
|
expect(TeamsAPI.create).toHaveBeenCalledWith(updatedTeamData);
|
||||||
@@ -40,11 +41,18 @@ describe('<TeamAdd />', () => {
|
|||||||
const teamData = {
|
const teamData = {
|
||||||
name: 'new name',
|
name: 'new name',
|
||||||
description: 'new description',
|
description: 'new description',
|
||||||
|
organization: 1,
|
||||||
};
|
};
|
||||||
TeamsAPI.create.mockResolvedValueOnce({
|
TeamsAPI.create.mockResolvedValueOnce({
|
||||||
data: {
|
data: {
|
||||||
id: 5,
|
id: 5,
|
||||||
...teamData,
|
...teamData,
|
||||||
|
summary_fields: {
|
||||||
|
organization: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Default',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const wrapper = mountWithContexts(<TeamAdd />, {
|
const wrapper = mountWithContexts(<TeamAdd />, {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ class TeamEdit extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.submitInstanceGroups = this.submitInstanceGroups.bind(this);
|
|
||||||
this.handleCancel = this.handleCancel.bind(this);
|
this.handleCancel = this.handleCancel.bind(this);
|
||||||
this.handleSuccess = this.handleSuccess.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;
|
const { team } = this.props;
|
||||||
try {
|
try {
|
||||||
await TeamsAPI.update(team.id, values);
|
await TeamsAPI.update(team.id, values);
|
||||||
await this.submitInstanceGroups(groupsToAssociate, groupsToDisassociate);
|
|
||||||
this.handleSuccess();
|
this.handleSuccess();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: err });
|
this.setState({ error: err });
|
||||||
@@ -49,24 +47,6 @@ class TeamEdit extends Component {
|
|||||||
history.push(`/teams/${id}/details`);
|
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() {
|
render() {
|
||||||
const { team } = this.props;
|
const { team } = this.props;
|
||||||
const { error } = this.state;
|
const { error } = this.state;
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { string, bool, func } from 'prop-types';
|
import { string, bool, func } from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
DataListItem,
|
DataListItem,
|
||||||
DataListItemRow,
|
DataListItemRow,
|
||||||
DataListItemCells,
|
DataListItemCells,
|
||||||
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
import ActionButtonCell from '@components/ActionButtonCell';
|
||||||
import DataListCell from '@components/DataListCell';
|
import DataListCell from '@components/DataListCell';
|
||||||
import DataListCheck from '@components/DataListCheck';
|
import DataListCheck from '@components/DataListCheck';
|
||||||
|
import ListActionButton from '@components/ListActionButton';
|
||||||
import VerticalSeparator from '@components/VerticalSeparator';
|
import VerticalSeparator from '@components/VerticalSeparator';
|
||||||
import { Team } from '@types';
|
import { Team } from '@types';
|
||||||
|
|
||||||
@@ -22,7 +27,7 @@ class TeamListItem extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { team, isSelected, onSelect, detailUrl } = this.props;
|
const { team, isSelected, onSelect, detailUrl, i18n } = this.props;
|
||||||
const labelId = `check-action-${team.id}`;
|
const labelId = `check-action-${team.id}`;
|
||||||
return (
|
return (
|
||||||
<DataListItem key={team.id} aria-labelledby={labelId}>
|
<DataListItem key={team.id} aria-labelledby={labelId}>
|
||||||
@@ -48,7 +53,9 @@ class TeamListItem extends React.Component {
|
|||||||
<DataListCell key="organization">
|
<DataListCell key="organization">
|
||||||
{team.summary_fields.organization && (
|
{team.summary_fields.organization && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<b style={{ marginRight: '20px' }}>Organization</b>
|
<b style={{ marginRight: '20px' }}>
|
||||||
|
{i18n._(t`Organization`)}
|
||||||
|
</b>
|
||||||
<Link
|
<Link
|
||||||
to={`/organizations/${team.summary_fields.organization.id}/details`}
|
to={`/organizations/${team.summary_fields.organization.id}/details`}
|
||||||
>
|
>
|
||||||
@@ -57,9 +64,19 @@ class TeamListItem extends React.Component {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
<DataListCell lastcolumn="true" key="action">
|
<ActionButtonCell lastcolumn="true" key="action">
|
||||||
edit button goes here
|
{team.summary_fields.user_capabilities.edit && (
|
||||||
</DataListCell>,
|
<Tooltip content={i18n._(t`Edit Team`)} position="top">
|
||||||
|
<ListActionButton
|
||||||
|
variant="plain"
|
||||||
|
component={Link}
|
||||||
|
to={`/teams/${team.id}/edit`}
|
||||||
|
>
|
||||||
|
<PencilAltIcon />
|
||||||
|
</ListActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</ActionButtonCell>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DataListItemRow>
|
</DataListItemRow>
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ describe('<TeamListItem />', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'Team 1',
|
name: 'Team 1',
|
||||||
summary_fields: {
|
summary_fields: {
|
||||||
organization: {
|
user_capabilities: {
|
||||||
id: 1,
|
edit: true,
|
||||||
name: 'Default',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -30,4 +29,50 @@ describe('<TeamListItem />', () => {
|
|||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
test('edit button shown to users with edit capabilities', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<I18nProvider>
|
||||||
|
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
|
||||||
|
<TeamListItem
|
||||||
|
team={{
|
||||||
|
id: 1,
|
||||||
|
name: 'Team',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
detailUrl="/team/1"
|
||||||
|
isSelected
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</I18nProvider>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
test('edit button hidden from users without edit capabilities', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<I18nProvider>
|
||||||
|
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
|
||||||
|
<TeamListItem
|
||||||
|
team={{
|
||||||
|
id: 1,
|
||||||
|
name: 'Team',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
detailUrl="/team/1"
|
||||||
|
isSelected
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
</I18nProvider>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,98 +1,91 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import { Formik } from 'formik';
|
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { Formik, Field } from 'formik';
|
||||||
import { Form } from '@patternfly/react-core';
|
import { Form } from '@patternfly/react-core';
|
||||||
|
|
||||||
import FormRow from '@components/FormRow';
|
|
||||||
import FormField from '@components/FormField';
|
|
||||||
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
|
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';
|
import { required } from '@util/validators';
|
||||||
|
|
||||||
class TeamForm extends Component {
|
function TeamForm(props) {
|
||||||
constructor(props) {
|
const { team, handleCancel, handleSubmit, i18n } = props;
|
||||||
super(props);
|
const [organization, setOrganization] = useState(
|
||||||
|
team.summary_fields ? team.summary_fields.organization : null
|
||||||
|
);
|
||||||
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
return (
|
||||||
|
<Formik
|
||||||
this.state = {
|
initialValues={{
|
||||||
formIsValid: true,
|
description: team.description || '',
|
||||||
};
|
name: team.name || '',
|
||||||
}
|
organization: team.organization || '',
|
||||||
|
}}
|
||||||
isEditingNewTeam() {
|
onSubmit={handleSubmit}
|
||||||
const { team } = this.props;
|
render={formik => (
|
||||||
return !team.id;
|
<Form
|
||||||
}
|
autoComplete="off"
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
handleSubmit(values) {
|
css="padding: 0 24px"
|
||||||
const { handleSubmit } = this.props;
|
>
|
||||||
|
<FormRow>
|
||||||
handleSubmit(values);
|
<FormField
|
||||||
}
|
id="team-name"
|
||||||
|
label={i18n._(t`Name`)}
|
||||||
render() {
|
name="name"
|
||||||
const { team, handleCancel, i18n } = this.props;
|
type="text"
|
||||||
const { formIsValid, error } = this.state;
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
initialValues={{
|
|
||||||
name: team.name,
|
|
||||||
description: team.description,
|
|
||||||
}}
|
|
||||||
onSubmit={this.handleSubmit}
|
|
||||||
render={formik => (
|
|
||||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
|
||||||
<FormRow>
|
|
||||||
<FormField
|
|
||||||
id="team-name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
label={i18n._(t`Name`)}
|
|
||||||
validate={required(null, i18n)}
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
id="team-description"
|
|
||||||
name="description"
|
|
||||||
type="text"
|
|
||||||
label={i18n._(t`Description`)}
|
|
||||||
/>
|
|
||||||
</FormRow>
|
|
||||||
<FormActionGroup
|
|
||||||
onCancel={handleCancel}
|
|
||||||
onSubmit={formik.handleSubmit}
|
|
||||||
submitDisabled={!formIsValid}
|
|
||||||
/>
|
/>
|
||||||
{error ? <div>error</div> : null}
|
<FormField
|
||||||
</Form>
|
id="team-description"
|
||||||
)}
|
label={i18n._(t`Description`)}
|
||||||
/>
|
name="description"
|
||||||
);
|
type="text"
|
||||||
}
|
/>
|
||||||
|
<Field
|
||||||
|
name="organization"
|
||||||
|
validate={required(
|
||||||
|
i18n._(t`Select a value for this field`),
|
||||||
|
i18n
|
||||||
|
)}
|
||||||
|
render={({ form }) => (
|
||||||
|
<OrganizationLookup
|
||||||
|
helperTextInvalid={form.errors.organization}
|
||||||
|
isValid={
|
||||||
|
!form.touched.organization || !form.errors.organization
|
||||||
|
}
|
||||||
|
onBlur={() => form.setFieldTouched('organization')}
|
||||||
|
onChange={value => {
|
||||||
|
form.setFieldValue('organization', value.id);
|
||||||
|
setOrganization(value);
|
||||||
|
}}
|
||||||
|
value={organization}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
<FormActionGroup
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
FormField.propTypes = {
|
|
||||||
label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
TeamForm.propTypes = {
|
TeamForm.propTypes = {
|
||||||
team: PropTypes.shape(),
|
|
||||||
handleSubmit: PropTypes.func.isRequired,
|
|
||||||
handleCancel: PropTypes.func.isRequired,
|
handleCancel: PropTypes.func.isRequired,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
team: PropTypes.shape({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
TeamForm.defaultProps = {
|
TeamForm.defaultProps = {
|
||||||
team: {
|
team: {},
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { TeamForm as _TeamForm };
|
export default withI18n()(TeamForm);
|
||||||
export default withI18n()(withRouter(TeamForm));
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||||
import { sleep } from '@testUtils/testUtils';
|
import { sleep } from '@testUtils/testUtils';
|
||||||
|
|
||||||
import TeamForm from './TeamForm';
|
import TeamForm from './TeamForm';
|
||||||
@@ -8,6 +8,7 @@ import TeamForm from './TeamForm';
|
|||||||
jest.mock('@api');
|
jest.mock('@api');
|
||||||
|
|
||||||
describe('<TeamForm />', () => {
|
describe('<TeamForm />', () => {
|
||||||
|
let wrapper;
|
||||||
const meConfig = {
|
const meConfig = {
|
||||||
me: {
|
me: {
|
||||||
is_superuser: false,
|
is_superuser: false,
|
||||||
@@ -17,14 +18,20 @@ describe('<TeamForm />', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'Foo',
|
name: 'Foo',
|
||||||
description: 'Bar',
|
description: 'Bar',
|
||||||
|
organization: 1,
|
||||||
|
summary_fields: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Default',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('changing inputs should update form values', () => {
|
test('changing inputs should update form values', () => {
|
||||||
const wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<TeamForm
|
<TeamForm
|
||||||
team={mockData}
|
team={mockData}
|
||||||
handleSubmit={jest.fn()}
|
handleSubmit={jest.fn()}
|
||||||
@@ -42,31 +49,39 @@ describe('<TeamForm />', () => {
|
|||||||
target: { value: 'new bar', name: 'description' },
|
target: { value: 'new bar', name: 'description' },
|
||||||
});
|
});
|
||||||
expect(form.state('values').description).toEqual('new bar');
|
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 handleSubmit = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
await act(async () => {
|
||||||
<TeamForm
|
wrapper = mountWithContexts(
|
||||||
team={mockData}
|
<TeamForm
|
||||||
handleSubmit={handleSubmit}
|
team={mockData}
|
||||||
handleCancel={jest.fn()}
|
handleSubmit={handleSubmit}
|
||||||
me={meConfig.me}
|
handleCancel={jest.fn()}
|
||||||
/>
|
me={meConfig.me}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
expect(handleSubmit).not.toHaveBeenCalled();
|
expect(handleSubmit).not.toHaveBeenCalled();
|
||||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||||
await sleep(1);
|
await sleep(1);
|
||||||
expect(handleSubmit).toHaveBeenCalledWith({
|
expect(handleSubmit).toBeCalled();
|
||||||
name: 'Foo',
|
|
||||||
description: 'Bar',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calls "handleCancel" when Cancel button is clicked', () => {
|
test('calls handleCancel when Cancel button is clicked', () => {
|
||||||
const handleCancel = jest.fn();
|
const handleCancel = jest.fn();
|
||||||
|
|
||||||
const wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<TeamForm
|
<TeamForm
|
||||||
team={mockData}
|
team={mockData}
|
||||||
handleSubmit={jest.fn()}
|
handleSubmit={jest.fn()}
|
||||||
|
|||||||
Reference in New Issue
Block a user