Merge pull request #98 from mabashian/selected-list

Adds selected list to lookup component
This commit is contained in:
Michael Abashian
2019-01-30 15:58:14 -05:00
committed by GitHub
11 changed files with 474 additions and 74 deletions

View File

@@ -3,41 +3,51 @@ import { mount } from 'enzyme';
import { I18nProvider } from '@lingui/react'; import { I18nProvider } from '@lingui/react';
import Lookup from '../../src/components/Lookup'; import Lookup from '../../src/components/Lookup';
let mockData = [{ name: 'foo', id: 0, isChecked: false }]; let mockData = [{ name: 'foo', id: 1, isChecked: false }];
describe('<Lookup />', () => { describe('<Lookup />', () => {
test('initially renders succesfully', () => { test('initially renders succesfully', () => {
mount( mount(
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
lookupChange={() => { }} onLookupSave={() => { }}
data={mockData} data={mockData}
selected={[]}
/> />
); );
}); });
test('calls "onLookup" when search icon is clicked', () => { test('Opens modal when search icon is clicked', () => {
const spy = jest.spyOn(Lookup.prototype, 'onLookup'); const spy = jest.spyOn(Lookup.prototype, 'handleModalToggle');
const mockSelected = [{ name: 'foo', id: 1 }];
const wrapper = mount( const wrapper = mount(
<I18nProvider> <I18nProvider>
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
lookupChange={() => { }} onLookupSave={() => { }}
data={mockData} data={mockData}
selected={mockSelected}
/> />
</I18nProvider> </I18nProvider>
); ).find('Lookup');
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
expect(wrapper.state('lookupSelectedItems')).toEqual([]);
const searchItem = wrapper.find('.pf-c-input-group__text#search'); const searchItem = wrapper.find('.pf-c-input-group__text#search');
searchItem.first().simulate('click'); searchItem.first().simulate('click');
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
expect(wrapper.state('lookupSelectedItems')).toEqual([{
id: 1,
name: 'foo'
}]);
expect(wrapper.state('isModalOpen')).toEqual(true);
}); });
test('calls "onChecked" when a user changes a checkbox', () => { test('calls "toggleSelected" when a user changes a checkbox', () => {
const spy = jest.spyOn(Lookup.prototype, 'onChecked'); const spy = jest.spyOn(Lookup.prototype, 'toggleSelected');
const wrapper = mount( const wrapper = mount(
<I18nProvider> <I18nProvider>
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
lookupChange={() => { }} onLookupSave={() => { }}
data={mockData} data={mockData}
selected={[]}
/> />
</I18nProvider> </I18nProvider>
); );
@@ -46,15 +56,17 @@ describe('<Lookup />', () => {
wrapper.find('input[type="checkbox"]').simulate('change'); wrapper.find('input[type="checkbox"]').simulate('change');
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
test('calls "onRemove" when remove icon is clicked', () => { test('calls "toggleSelected" when remove icon is clicked', () => {
const spy = jest.spyOn(Lookup.prototype, 'onRemove'); const spy = jest.spyOn(Lookup.prototype, 'toggleSelected');
mockData = [{ name: 'foo', id: 0, isChecked: false }, { name: 'bar', id: 1, isChecked: true }]; mockData = [{ name: 'foo', id: 1, isChecked: false }, { name: 'bar', id: 2, isChecked: true }];
const mockSelected = [{ name: 'foo', id: 1 }, { name: 'bar', id: 2 }];
const wrapper = mount( const wrapper = mount(
<I18nProvider> <I18nProvider>
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
lookupChange={() => { }} onLookupSave={() => { }}
data={mockData} data={mockData}
selected={mockSelected}
/> />
</I18nProvider> </I18nProvider>
); );
@@ -69,8 +81,9 @@ describe('<Lookup />', () => {
<I18nProvider> <I18nProvider>
<Lookup <Lookup
lookup_header="Foo Bar" lookup_header="Foo Bar"
lookupChange={() => { }} onLookupSave={() => { }}
data={mockData} data={mockData}
selected={[]}
/> />
</I18nProvider> </I18nProvider>
); );
@@ -78,4 +91,57 @@ describe('<Lookup />', () => {
const pill = wrapper.find('span.awx-c-tag--pill'); const pill = wrapper.find('span.awx-c-tag--pill');
expect(pill).toHaveLength(0); expect(pill).toHaveLength(0);
}); });
test('toggleSelected successfully adds/removes row from lookupSelectedItems state', () => {
mockData = [{ name: 'foo', id: 1 }];
const wrapper = mount(
<I18nProvider>
<Lookup
lookup_header="Foo Bar"
onLookupSave={() => { }}
data={mockData}
selected={[]}
/>
</I18nProvider>
).find('Lookup');
wrapper.instance().toggleSelected({
id: 1,
name: 'foo'
});
expect(wrapper.state('lookupSelectedItems')).toEqual([{
id: 1,
name: 'foo'
}]);
wrapper.instance().toggleSelected({
id: 1,
name: 'foo'
});
expect(wrapper.state('lookupSelectedItems')).toEqual([]);
});
test('saveModal calls callback with selected items', () => {
mockData = [{ name: 'foo', id: 1 }];
const onLookupSaveFn = jest.fn();
const wrapper = mount(
<I18nProvider>
<Lookup
lookup_header="Foo Bar"
onLookupSave={onLookupSaveFn}
data={mockData}
selected={[]}
/>
</I18nProvider>
).find('Lookup');
wrapper.instance().toggleSelected({
id: 1,
name: 'foo'
});
expect(wrapper.state('lookupSelectedItems')).toEqual([{
id: 1,
name: 'foo'
}]);
wrapper.instance().saveModal();
expect(onLookupSaveFn).toHaveBeenCalledWith([{
id: 1,
name: 'foo'
}]);
});
}); });

View File

@@ -0,0 +1,123 @@
import React from 'react';
import { mount } from 'enzyme';
import SelectedList from '../../src/components/SelectedList';
describe('<SelectedList />', () => {
test('initially renders succesfully', () => {
const mockSelected = [
{
id: 1,
name: 'foo'
}, {
id: 2,
name: 'bar'
}
];
mount(
<SelectedList
label="Selected"
selected={mockSelected}
showOverflowAfter={5}
onRemove={() => {}}
/>
);
});
test('showOverflow should set showOverflow state to true', () => {
const wrapper = mount(
<SelectedList
label="Selected"
selected={[]}
showOverflowAfter={5}
onRemove={() => {}}
/>
);
expect(wrapper.state('showOverflow')).toBe(false);
wrapper.instance().showOverflow();
expect(wrapper.state('showOverflow')).toBe(true);
});
test('Overflow chip should be shown when more selected.length exceeds showOverflowAfter', () => {
const mockSelected = [
{
id: 1,
name: 'foo'
}, {
id: 2,
name: 'bar'
}, {
id: 3,
name: 'foobar'
}, {
id: 4,
name: 'baz'
}, {
id: 5,
name: 'foobaz'
}
];
const wrapper = mount(
<SelectedList
label="Selected"
selected={mockSelected}
showOverflowAfter={3}
onRemove={() => {}}
/>
);
expect(wrapper.find('Chip').length).toBe(4);
expect(wrapper.find('[isOverflowChip=true]').length).toBe(1);
});
test('Clicking overflow chip should show all chips', () => {
const mockSelected = [
{
id: 1,
name: 'foo'
}, {
id: 2,
name: 'bar'
}, {
id: 3,
name: 'foobar'
}, {
id: 4,
name: 'baz'
}, {
id: 5,
name: 'foobaz'
}
];
const wrapper = mount(
<SelectedList
label="Selected"
selected={mockSelected}
showOverflowAfter={3}
onRemove={() => {}}
/>
);
expect(wrapper.find('Chip').length).toBe(4);
expect(wrapper.find('[isOverflowChip=true]').length).toBe(1);
wrapper.find('[isOverflowChip=true] button').simulate('click');
expect(wrapper.find('Chip').length).toBe(5);
expect(wrapper.find('[isOverflowChip=true]').length).toBe(0);
});
test('Clicking remove on chip calls onRemove callback prop with correct params', () => {
const onRemove = jest.fn();
const mockSelected = [
{
id: 1,
name: 'foo'
}
];
const wrapper = mount(
<SelectedList
label="Selected"
selected={mockSelected}
showOverflowAfter={3}
onRemove={onRemove}
/>
);
wrapper.find('.pf-c-chip button').first().simulate('click');
expect(onRemove).toBeCalledWith({
id: 1,
name: 'foo'
});
});
});

View File

@@ -92,4 +92,70 @@ describe('<OrganizationAdd />', () => {
done(); done();
}); });
}); });
test('updateSelectedInstanceGroups successfully sets selectedInstanceGroups state', () => {
const wrapper = mount(
<MemoryRouter>
<OrganizationAdd api={{}} />
</MemoryRouter>
).find('OrganizationAdd');
wrapper.instance().updateSelectedInstanceGroups([
{
id: 1,
name: 'foo'
}
]);
expect(wrapper.state('selectedInstanceGroups')).toEqual([
{
id: 1,
name: 'foo'
}
]);
});
test('onSelectChange successfully sets custom_virtualenv state', () => {
const wrapper = mount(
<MemoryRouter>
<OrganizationAdd api={{}} />
</MemoryRouter>
).find('OrganizationAdd');
wrapper.instance().onSelectChange('foobar');
expect(wrapper.state('custom_virtualenv')).toBe('foobar');
});
test('onSubmit posts instance groups from selectedInstanceGroups', async () => {
const createOrganizationFn = jest.fn().mockResolvedValue({
data: {
id: 1,
name: 'mock org',
related: {
instance_groups: '/api/v2/organizations/1/instance_groups'
}
}
});
const createInstanceGroupsFn = jest.fn().mockResolvedValue('done');
const api = {
createOrganization: createOrganizationFn,
createInstanceGroups: createInstanceGroupsFn
};
const wrapper = mount(
<MemoryRouter>
<OrganizationAdd api={api} />
</MemoryRouter>
).find('OrganizationAdd');
wrapper.setState({
name: 'mock org',
selectedInstanceGroups: [{
id: 1,
name: 'foo'
}]
});
await wrapper.instance().onSubmit();
expect(createOrganizationFn).toHaveBeenCalledWith({
custom_virtualenv: '',
description: '',
name: 'mock org'
});
expect(createInstanceGroupsFn).toHaveBeenCalledWith('/api/v2/organizations/1/instance_groups', 1);
});
}); });

33
package-lock.json generated
View File

@@ -1311,22 +1311,27 @@
"integrity": "sha512-sIRfo/tk4NSnaRwHIHLUf4XoqzNNa4MMa8ZWivzzSfdZ5pCbgvZtyEUqKnQAEH6zuaCM9S8HyhxcNXnm/xaYaQ==" "integrity": "sha512-sIRfo/tk4NSnaRwHIHLUf4XoqzNNa4MMa8ZWivzzSfdZ5pCbgvZtyEUqKnQAEH6zuaCM9S8HyhxcNXnm/xaYaQ=="
}, },
"@patternfly/react-core": { "@patternfly/react-core": {
"version": "1.43.5", "version": "1.49.5",
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-1.43.5.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-1.49.5.tgz",
"integrity": "sha512-xO37/q5BJEdGvAoPllm/gbcwCtW7t0Ae7mKm5UU7d4i5bycEjd0UwJacYxCA6GFTwxN5kzn61XEpAUPY49U3pA==", "integrity": "sha512-bb62fkL8nB6F1cUd/szfpLOIAjaa5HBzAoOa4Vc1AjdagwZ6w4MsU7xBPtC0Sp937CpGckRGiVOf0XHWEiSL2g==",
"requires": { "requires": {
"@patternfly/react-icons": "^2.9.5", "@patternfly/react-icons": "^2.10.1",
"@patternfly/react-styles": "^2.3.0", "@patternfly/react-styles": "^2.3.0",
"@patternfly/react-tokens": "^1.0.0", "@patternfly/react-tokens": "^1.10.0",
"@tippy.js/react": "^1.1.1", "@tippy.js/react": "^1.1.1",
"exenv": "^1.2.2", "exenv": "^1.2.2",
"focus-trap-react": "^4.0.1" "focus-trap-react": "^4.0.1"
}, },
"dependencies": { "dependencies": {
"@patternfly/react-icons": { "@patternfly/react-icons": {
"version": "2.9.5", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-2.9.5.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-2.10.1.tgz",
"integrity": "sha512-5e/BD2ER5jifUjUgbIilApOfhVldlAjhQdh7EwH/M3M+qzIb+2qKxV/xQ6hWD3AA71lcYIxvPMMHgdWIAl5oPQ==" "integrity": "sha512-d3uWfQQeCgCLel2DVlF1SSlyOI0Z12tT1YjSLDE091E2uCB582DUQQ4HfmuV51nH5aTXg+en35QG7JP5jzYlvA=="
},
"@patternfly/react-tokens": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-1.10.0.tgz",
"integrity": "sha512-jslQPSRgwbSXAGszA22prGSVye6ri3sRFkaF3BUdWBa8fO6Z2MDFB59x4d6BGK9iW7S+3U/Qkden6myP1CgXdA=="
} }
} }
}, },
@@ -13808,9 +13813,9 @@
"integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=" "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY="
}, },
"tabbable": { "tabbable": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-3.1.1.tgz", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-3.1.2.tgz",
"integrity": "sha512-583MHIOwictf7+zbxqO/L5fBqMN6Li4SJ1XTKQA9WzHRA7c2BB+D+Ny7Y6kGqU2u+rHK59+oRzrBvMU53aZz+A==" "integrity": "sha512-wjB6puVXTYO0BSFtCmWQubA/KIn7Xvajw0x0l6eJUudMG/EAiJvIUnyNX6xO4NpGrJ16lbD0eUseB9WxW0vlpQ=="
}, },
"table": { "table": {
"version": "5.1.0", "version": "5.1.0",
@@ -13927,9 +13932,9 @@
} }
}, },
"tippy.js": { "tippy.js": {
"version": "3.3.0", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-3.3.0.tgz", "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-3.4.1.tgz",
"integrity": "sha512-2gIQg57EFSCBqE97NZbakSkGBJF0GzdOhx/lneGQGMzJiJyvbpyKgNy4l4qofq0nEbXACl7C/jW/ErsdQa21aQ==", "integrity": "sha512-ZiyGP9WZyCCcjxKM4G88cm4U1r1ytjeMDGa5FSKPaPzwc/3yZJVZsb1ffcmqUMCpryRp5LNxRNGKLzbs11sb/Q==",
"requires": { "requires": {
"popper.js": "^1.14.6" "popper.js": "^1.14.6"
} }

View File

@@ -48,7 +48,7 @@
"dependencies": { "dependencies": {
"@lingui/react": "^2.7.2", "@lingui/react": "^2.7.2",
"@patternfly/patternfly-next": "^1.0.84", "@patternfly/patternfly-next": "^1.0.84",
"@patternfly/react-core": "^1.43.5", "@patternfly/react-core": "^1.49.5",
"@patternfly/react-icons": "^2.9.1", "@patternfly/react-icons": "^2.9.1",
"@patternfly/react-styles": "^2.3.0", "@patternfly/react-styles": "^2.3.0",
"@patternfly/react-tokens": "^1.9.0", "@patternfly/react-tokens": "^1.9.0",

View File

@@ -8,47 +8,66 @@ import {
Toolbar, Toolbar,
ToolbarGroup, ToolbarGroup,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { I18n } from '@lingui/react';
import { t, Trans } from '@lingui/macro';
import CheckboxListItem from '../ListItem'; import CheckboxListItem from '../ListItem';
import SelectedList from '../SelectedList';
class Lookup extends React.Component { class Lookup extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
this.state = { this.state = {
isModalOpen: false, isModalOpen: false,
lookupSelectedItems: []
}; };
this.handleModalToggle = this.handleModalToggle.bind(this); this.handleModalToggle = this.handleModalToggle.bind(this);
this.onLookup = this.onLookup.bind(this);
this.onChecked = this.onChecked.bind(this);
this.wrapTags = this.wrapTags.bind(this); this.wrapTags = this.wrapTags.bind(this);
this.onRemove = this.onRemove.bind(this); this.toggleSelected = this.toggleSelected.bind(this);
this.saveModal = this.saveModal.bind(this);
} }
onLookup () { toggleSelected (row) {
this.handleModalToggle(); const { lookupSelectedItems } = this.state;
} const selectedIndex = lookupSelectedItems
.findIndex(selectedRow => selectedRow.id === row.id);
onChecked (_, evt) { if (selectedIndex > -1) {
const { lookupChange } = this.props; lookupSelectedItems.splice(selectedIndex, 1);
lookupChange(evt.target.value); this.setState({ lookupSelectedItems });
} } else {
this.setState(prevState => ({
onRemove (evt) { lookupSelectedItems: [...prevState.lookupSelectedItems, row]
const { lookupChange } = this.props; }));
lookupChange(evt.target.id); }
} }
handleModalToggle () { handleModalToggle () {
const { isModalOpen } = this.state;
const { selected } = this.props;
// Resets the selected items from parent state whenever modal is opened
// This handles the case where the user closes/cancels the modal and
// opens it again
if (!isModalOpen) {
this.setState({ lookupSelectedItems: [...selected] });
}
this.setState((prevState) => ({ this.setState((prevState) => ({
isModalOpen: !prevState.isModalOpen, isModalOpen: !prevState.isModalOpen,
})); }));
} }
saveModal () {
const { onLookupSave } = this.props;
const { lookupSelectedItems } = this.state;
onLookupSave(lookupSelectedItems);
this.handleModalToggle();
}
wrapTags (tags) { wrapTags (tags) {
return tags.filter(tag => tag.isChecked).map((tag) => ( return tags.map(tag => (
<span className="awx-c-tag--pill" key={tag.id}> <span className="awx-c-tag--pill" key={tag.id}>
{tag.name} {tag.name}
<Button className="awx-c-icon--remove" id={tag.id} onClick={this.onRemove}> <Button className="awx-c-icon--remove" id={tag.id} onClick={() => this.toggleSelected(tag)}>
x x
</Button> </Button>
</span> </span>
@@ -56,14 +75,14 @@ class Lookup extends React.Component {
} }
render () { render () {
const { isModalOpen } = this.state; const { isModalOpen, lookupSelectedItems } = this.state;
const { data, lookupHeader } = this.props; const { data, lookupHeader, selected } = this.props;
return ( return (
<div className="pf-c-input-group awx-lookup"> <div className="pf-c-input-group awx-lookup">
<Button className="pf-c-input-group__text" aria-label="search" id="search" onClick={this.onLookup}> <Button className="pf-c-input-group__text" aria-label="search" id="search" onClick={this.handleModalToggle}>
<SearchIcon /> <SearchIcon />
</Button> </Button>
<div className="pf-c-form-control">{this.wrapTags(data)}</div> <div className="pf-c-form-control">{this.wrapTags(selected)}</div>
<Modal <Modal
className="awx-c-modal" className="awx-c-modal"
title={`Select ${lookupHeader}`} title={`Select ${lookupHeader}`}
@@ -76,18 +95,34 @@ class Lookup extends React.Component {
key={i.id} key={i.id}
itemId={i.id} itemId={i.id}
name={i.name} name={i.name}
isSelected={i.isChecked} isSelected={lookupSelectedItems.some(item => item.id === i.id)}
onSelect={this.onChecked} onSelect={() => this.toggleSelected(i)}
/> />
))} ))}
</ul> </ul>
{lookupSelectedItems.length > 0 && (
<I18n>
{({ i18n }) => (
<SelectedList
label={i18n._(t`Selected`)}
selected={lookupSelectedItems}
showOverflowAfter={5}
onRemove={this.toggleSelected}
/>
)}
</I18n>
)}
<ActionGroup className="at-align-right"> <ActionGroup className="at-align-right">
<Toolbar> <Toolbar>
<ToolbarGroup> <ToolbarGroup>
<Button className="at-C-SubmitButton" variant="primary" onClick={this.handleModalToggle}>Select</Button> <Button className="at-C-SubmitButton" variant="primary" onClick={this.saveModal}>
<Trans>Select</Trans>
</Button>
</ToolbarGroup> </ToolbarGroup>
<ToolbarGroup> <ToolbarGroup>
<Button className="at-C-CancelButton" variant="secondary" onClick={this.handleModalToggle}>Cancel</Button> <Button className="at-C-CancelButton" variant="secondary" onClick={this.handleModalToggle}>
<Trans>Cancel</Trans>
</Button>
</ToolbarGroup> </ToolbarGroup>
</Toolbar> </Toolbar>
</ActionGroup> </ActionGroup>

View File

@@ -0,0 +1,61 @@
import React, { Component } from 'react';
import {
Chip
} from '@patternfly/react-core';
class SelectedList extends Component {
constructor (props) {
super(props);
this.state = {
showOverflow: false
};
this.showOverflow = this.showOverflow.bind(this);
}
showOverflow = () => {
this.setState({ showOverflow: true });
};
render () {
const { label, selected, showOverflowAfter, onRemove } = this.props;
const { showOverflow } = this.state;
return (
<div className="awx-selectedList">
<div className="pf-l-split">
<div className="pf-l-split__item pf-u-align-items-center">
{label}
</div>
<div className="pf-l-split__item">
<div className="pf-c-chip-group">
{selected
.slice(0, showOverflow ? selected.length : showOverflowAfter)
.map(selectedItem => (
<Chip
key={selectedItem.id}
onClick={() => onRemove(selectedItem)}
>
{selectedItem.name}
</Chip>
))}
{(
!showOverflow
&& selected.length > showOverflowAfter
) && (
<Chip
isOverflowChip
onClick={() => this.showOverflow()}
>
{`${(selected.length - showOverflowAfter).toString()} more`}
</Chip>
)}
</div>
</div>
</div>
</div>
);
}
}
export default SelectedList;

View File

@@ -0,0 +1,3 @@
import SelectedList from './SelectedList';
export default SelectedList;

View File

@@ -0,0 +1,31 @@
.awx-selectedList {
--awx-selectedList--BackgroundColor: var(--pf-global--BackgroundColor--light-100);
--awx-selectedList--BorderColor: #d7d7d7;
--awx-selectedList--BorderWidth: var(--pf-global--BorderWidth--sm);
--awx-selectedList--FontSize: var(--pf-c-chip__text--FontSize);
.pf-l-split {
padding-top: 20px;
padding-bottom: 10px;
border-bottom: var(--awx-selectedList--BorderWidth) solid var(--awx-selectedList--BorderColor);
}
.pf-l-split__item:first-child {
display: flex;
white-space: nowrap;
height: 30px;
}
.pf-l-split__item:not(:last-child):after {
content: "";
background-color: var(--awx-selectedList--BorderColor);
width: 1px;
height: 30px;
display: block;
margin-left: 20px;
margin-right: 20px;
}
.pf-c-chip {
margin-right: 10px;
margin-bottom: 10px;
}
}

View File

@@ -18,6 +18,7 @@ import '@patternfly/patternfly-next/patternfly.css';
import './app.scss'; import './app.scss';
import './components/Pagination/styles.scss'; import './components/Pagination/styles.scss';
import './components/DataListToolbar/styles.scss'; import './components/DataListToolbar/styles.scss';
import './components/SelectedList/styles.scss';
import APIClient from './api'; import APIClient from './api';

View File

@@ -34,20 +34,20 @@ class OrganizationAdd extends React.Component {
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
this.onSelectChange = this.onSelectChange.bind(this); this.onSelectChange = this.onSelectChange.bind(this);
this.onLookupChange = this.onLookupChange.bind(this);
this.onSubmit = this.onSubmit.bind(this); this.onSubmit = this.onSubmit.bind(this);
this.resetForm = this.resetForm.bind(this); this.resetForm = this.resetForm.bind(this);
this.onSuccess = this.onSuccess.bind(this); this.onSuccess = this.onSuccess.bind(this);
this.onCancel = this.onCancel.bind(this); this.onCancel = this.onCancel.bind(this);
this.updateSelectedInstanceGroups = this.updateSelectedInstanceGroups.bind(this);
} }
state = { state = {
name: '', name: '',
description: '', description: '',
results: [], results: [],
instance_groups: [],
custom_virtualenv: '', custom_virtualenv: '',
error: '', error: '',
selectedInstanceGroups: []
}; };
async componentDidMount () { async componentDidMount () {
@@ -57,7 +57,7 @@ class OrganizationAdd extends React.Component {
const results = format(data); const results = format(data);
this.setState({ results }); this.setState({ results });
} catch (error) { } catch (error) {
this.setState({ getInstanceGroupsError: error }); this.setState({ error });
} }
} }
@@ -65,36 +65,32 @@ class OrganizationAdd extends React.Component {
this.setState({ custom_virtualenv: value }); this.setState({ custom_virtualenv: value });
} }
onLookupChange (id) {
const { results } = this.state;
const selected = { ...results };
const index = id - 1;
selected[index].isChecked = !selected[index].isChecked;
this.setState({ selected });
}
async onSubmit () { async onSubmit () {
const { api } = this.props; const { api } = this.props;
const data = Object.assign({}, { ...this.state }); const { name, description, custom_virtualenv } = this.state;
const { results } = this.state; const data = {
name,
description,
custom_virtualenv
};
const { selectedInstanceGroups } = this.state;
try { try {
const { data: response } = await api.createOrganization(data); const { data: response } = await api.createOrganization(data);
const url = response.related.instance_groups; const url = response.related.instance_groups;
const selected = results.filter(group => group.isChecked);
try { try {
if (selected.length > 0) { if (selectedInstanceGroups.length > 0) {
selected.forEach(async (select) => { selectedInstanceGroups.forEach(async (select) => {
await api.createInstanceGroups(url, select.id); await api.createInstanceGroups(url, select.id);
}); });
} }
} catch (err) { } catch (err) {
this.setState({ createInstanceGroupsError: err }); this.setState({ error: err });
} finally { } finally {
this.resetForm(); this.resetForm();
this.onSuccess(response.id); this.onSuccess(response.id);
} }
} catch (err) { } catch (err) {
this.setState({ onSubmitError: err }); this.setState({ error: err });
} }
} }
@@ -108,6 +104,10 @@ class OrganizationAdd extends React.Component {
history.push(`/organizations/${id}`); history.push(`/organizations/${id}`);
} }
updateSelectedInstanceGroups (selectedInstanceGroups) {
this.setState({ selectedInstanceGroups });
}
handleChange (_, evt) { handleChange (_, evt) {
this.setState({ [evt.target.name]: evt.target.value }); this.setState({ [evt.target.name]: evt.target.value });
} }
@@ -123,7 +123,14 @@ class OrganizationAdd extends React.Component {
} }
render () { render () {
const { name, results, description, custom_virtualenv } = this.state; const {
name,
results,
description,
custom_virtualenv,
selectedInstanceGroups,
error
} = this.state;
const enabled = name.length > 0; // TODO: add better form validation const enabled = name.length > 0; // TODO: add better form validation
return ( return (
@@ -157,8 +164,9 @@ class OrganizationAdd extends React.Component {
<FormGroup label="Instance Groups" fieldId="simple-form-instance-groups"> <FormGroup label="Instance Groups" fieldId="simple-form-instance-groups">
<Lookup <Lookup
lookupHeader="Instance Groups" lookupHeader="Instance Groups"
lookupChange={this.onLookupChange} onLookupSave={this.updateSelectedInstanceGroups}
data={results} data={results}
selected={selectedInstanceGroups}
/> />
</FormGroup> </FormGroup>
<ConfigContext.Consumer> <ConfigContext.Consumer>
@@ -182,6 +190,7 @@ class OrganizationAdd extends React.Component {
</ToolbarGroup> </ToolbarGroup>
</Toolbar> </Toolbar>
</ActionGroup> </ActionGroup>
{ error ? <div>error</div> : '' }
</Form> </Form>
</CardBody> </CardBody>
</Card> </Card>