mirror of
https://github.com/ansible/awx.git
synced 2026-01-18 13:11:19 -03:30
Adds organization field to team form. Adds edit button to team list items.
This commit is contained in:
parent
6acd3c98b7
commit
84bce530dc
@ -12,6 +12,7 @@ describe('<TeamAdd />', () => {
|
||||
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('<TeamAdd />', () => {
|
||||
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(<TeamAdd />, {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
<DataListItem key={team.id} aria-labelledby={labelId}>
|
||||
@ -48,7 +53,9 @@ class TeamListItem extends React.Component {
|
||||
<DataListCell key="organization">
|
||||
{team.summary_fields.organization && (
|
||||
<Fragment>
|
||||
<b style={{ marginRight: '20px' }}>Organization</b>
|
||||
<b style={{ marginRight: '20px' }}>
|
||||
{i18n._(t`Organization`)}
|
||||
</b>
|
||||
<Link
|
||||
to={`/organizations/${team.summary_fields.organization.id}/details`}
|
||||
>
|
||||
@ -57,9 +64,19 @@ class TeamListItem extends React.Component {
|
||||
</Fragment>
|
||||
)}
|
||||
</DataListCell>,
|
||||
<DataListCell lastcolumn="true" key="action">
|
||||
edit button goes here
|
||||
</DataListCell>,
|
||||
<ActionButtonCell lastcolumn="true" key="action">
|
||||
{team.summary_fields.user_capabilities.edit && (
|
||||
<Tooltip content={i18n._(t`Edit Team`)} position="top">
|
||||
<ListActionButton
|
||||
variant="plain"
|
||||
component={Link}
|
||||
to={`/teams/${team.id}/edit`}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</ListActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ActionButtonCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
|
||||
@ -16,9 +16,8 @@ describe('<TeamListItem />', () => {
|
||||
id: 1,
|
||||
name: 'Team 1',
|
||||
summary_fields: {
|
||||
organization: {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
@ -30,4 +29,50 @@ describe('<TeamListItem />', () => {
|
||||
</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 { 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 (
|
||||
<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}
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
description: team.description || '',
|
||||
name: team.name || '',
|
||||
organization: team.organization || '',
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
render={formik => (
|
||||
<Form
|
||||
autoComplete="off"
|
||||
onSubmit={formik.handleSubmit}
|
||||
css="padding: 0 24px"
|
||||
>
|
||||
<FormRow>
|
||||
<FormField
|
||||
id="team-name"
|
||||
label={i18n._(t`Name`)}
|
||||
name="name"
|
||||
type="text"
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
{error ? <div>error</div> : null}
|
||||
</Form>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
<FormField
|
||||
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 = {
|
||||
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);
|
||||
|
||||
@ -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('<TeamForm />', () => {
|
||||
let wrapper;
|
||||
const meConfig = {
|
||||
me: {
|
||||
is_superuser: false,
|
||||
@ -17,14 +18,20 @@ describe('<TeamForm />', () => {
|
||||
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(
|
||||
<TeamForm
|
||||
team={mockData}
|
||||
handleSubmit={jest.fn()}
|
||||
@ -42,31 +49,39 @@ describe('<TeamForm />', () => {
|
||||
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(
|
||||
<TeamForm
|
||||
team={mockData}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={jest.fn()}
|
||||
me={meConfig.me}
|
||||
/>
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<TeamForm
|
||||
team={mockData}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={jest.fn()}
|
||||
me={meConfig.me}
|
||||
/>
|
||||
);
|
||||
});
|
||||
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(
|
||||
<TeamForm
|
||||
team={mockData}
|
||||
handleSubmit={jest.fn()}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user