mirror of
https://github.com/ansible/awx.git
synced 2026-03-04 10:11:05 -03:30
Merge pull request #98 from mabashian/selected-list
Adds selected list to lookup component
This commit is contained in:
@@ -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'
|
||||||
|
}]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
123
__tests__/components/SelectedList.test.jsx
Normal file
123
__tests__/components/SelectedList.test.jsx
Normal 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'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
33
package-lock.json
generated
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
61
src/components/SelectedList/SelectedList.jsx
Normal file
61
src/components/SelectedList/SelectedList.jsx
Normal 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;
|
||||||
3
src/components/SelectedList/index.js
Normal file
3
src/components/SelectedList/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import SelectedList from './SelectedList';
|
||||||
|
|
||||||
|
export default SelectedList;
|
||||||
31
src/components/SelectedList/styles.scss
Normal file
31
src/components/SelectedList/styles.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user