Adds organization field to team form. Adds edit button to team list items.

This commit is contained in:
mabashian 2019-11-01 14:34:35 -04:00
parent 6acd3c98b7
commit 84bce530dc
6 changed files with 185 additions and 127 deletions

View File

@ -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 />, {

View File

@ -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;

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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);

View File

@ -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()}