diff --git a/awx/ui_next/src/app.scss b/awx/ui_next/src/app.scss index 4f0b81df21..cf8f2d2338 100644 --- a/awx/ui_next/src/app.scss +++ b/awx/ui_next/src/app.scss @@ -97,7 +97,7 @@ width: 600px; .pf-c-modal-box__body { - overflow: visible; + overflow: auto; } .pf-c-modal-box__footer > .pf-c-button:not(:last-child) { diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx index 77c06e99b5..8610bfa779 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx @@ -107,6 +107,7 @@ class SelectResourceStep extends React.Component { itemId={item.id} key={item.id} name={item[displayKey]} + label={item[displayKey]} onSelect={() => onRowClick(item)} /> )} diff --git a/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.jsx b/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.jsx index 903ff83606..cdad44c60e 100644 --- a/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.jsx +++ b/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.jsx @@ -7,42 +7,50 @@ import { DataListCheck, DataListCell, } from '@patternfly/react-core'; - +import DataListRadio from '@components/DataListRadio'; import VerticalSeparator from '../VerticalSeparator'; -const CheckboxListItem = ({ itemId, name, isSelected, onSelect }) => ( - - - - - - , - - - , - ]} - /> - - -); + + , + + + , + ]} + /> + + + ); +}; CheckboxListItem.propTypes = { itemId: PropTypes.number.isRequired, name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, isSelected: PropTypes.bool.isRequired, onSelect: PropTypes.func.isRequired, }; diff --git a/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.test.jsx b/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.test.jsx index ad0603f5f1..a28003b71a 100644 --- a/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.test.jsx +++ b/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.test.jsx @@ -9,6 +9,7 @@ describe('CheckboxListItem', () => { {}} /> diff --git a/awx/ui_next/src/components/DataListRadio/DataListRadio.jsx b/awx/ui_next/src/components/DataListRadio/DataListRadio.jsx new file mode 100644 index 0000000000..c498f42b55 --- /dev/null +++ b/awx/ui_next/src/components/DataListRadio/DataListRadio.jsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { string, bool, func } from 'prop-types'; +import styled from 'styled-components'; + +function DataListRadio({ + className = '', + onChange, + isValid = true, + isDisabled = false, + isChecked = null, + checked = null, + ...props +}) { + return ( +
+
+ onChange(event.currentTarget.checked, event)} + aria-invalid={!isValid} + disabled={isDisabled} + checked={isChecked || checked} + /> +
+
+ ); +} +DataListRadio.propTypes = { + className: string, + isValid: bool, + isDisabled: bool, + isChecked: bool, + checked: bool, + onChange: func, + 'aria-labelledby': string, +}; +DataListRadio.defaultProps = { + className: '', + isValid: true, + isDisabled: false, + isChecked: false, + checked: false, + onChange: () => {}, + 'aria-labelledby': '', +} + +export default DataListRadio; diff --git a/awx/ui_next/src/components/DataListRadio/index.js b/awx/ui_next/src/components/DataListRadio/index.js new file mode 100644 index 0000000000..c8f5b6d345 --- /dev/null +++ b/awx/ui_next/src/components/DataListRadio/index.js @@ -0,0 +1 @@ +export { default } from './DataListRadio'; diff --git a/awx/ui_next/src/components/Lookup/Lookup.jsx b/awx/ui_next/src/components/Lookup/Lookup.jsx index b9a417cb55..b7f60672ca 100644 --- a/awx/ui_next/src/components/Lookup/Lookup.jsx +++ b/awx/ui_next/src/components/Lookup/Lookup.jsx @@ -1,5 +1,13 @@ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; +import { + string, + bool, + arrayOf, + func, + number, + oneOfType, + shape, +} from 'prop-types'; import { withRouter } from 'react-router-dom'; import { SearchIcon } from '@patternfly/react-icons'; import { @@ -20,19 +28,24 @@ import { ChipGroup, Chip } from '../Chip'; import { getQSConfig, parseNamespacedQueryString } from '../../util/qs'; const InputGroup = styled(PFInputGroup)` - ${props => props.multiple && (` + ${props => + props.multiple && + ` --pf-c-form-control--Height: 90px; overflow-y: auto; - `)} + `} `; class Lookup extends React.Component { constructor(props) { super(props); + this.assertCorrectValueType(); this.state = { isModalOpen: false, - lookupSelectedItems: [...props.value] || [], + lookupSelectedItems: props.multiple + ? [...props.value] || [] + : [props.value], results: [], count: 0, error: null, @@ -59,6 +72,18 @@ class Lookup extends React.Component { } } + assertCorrectValueType() { + const { multiple, value } = this.props; + if (!multiple && Array.isArray(value)) { + throw new Error( + 'Lookup value must not be an array unless `multiple` is set' + ); + } + if (multiple && !Array.isArray(value)) { + throw new Error('Lookup value must be an array if `multiple` is set'); + } + } + async getData() { const { getItems, @@ -81,7 +106,7 @@ class Lookup extends React.Component { } toggleSelected(row) { - const { name, onLookupSave } = this.props; + const { name, onLookupSave, multiple } = this.props; const { lookupSelectedItems: updatedSelectedItems, isModalOpen, @@ -91,13 +116,17 @@ class Lookup extends React.Component { selectedRow => selectedRow.id === row.id ); - if (selectedIndex > -1) { - updatedSelectedItems.splice(selectedIndex, 1); - this.setState({ lookupSelectedItems: updatedSelectedItems }); + if (multiple) { + if (selectedIndex > -1) { + updatedSelectedItems.splice(selectedIndex, 1); + this.setState({ lookupSelectedItems: updatedSelectedItems }); + } else { + this.setState(prevState => ({ + lookupSelectedItems: [...prevState.lookupSelectedItems, row], + })); + } } else { - this.setState(prevState => ({ - lookupSelectedItems: [...prevState.lookupSelectedItems, row], - })); + this.setState({ lookupSelectedItems: [row] }); } // Updates the selected items from parent state @@ -110,12 +139,14 @@ class Lookup extends React.Component { handleModalToggle() { const { isModalOpen } = this.state; - const { value } = this.props; + const { value, multiple } = 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: [...value] }); + this.setState({ + lookupSelectedItems: multiple ? [...value] : [value], + }); } this.setState(prevState => ({ isModalOpen: !prevState.isModalOpen, @@ -123,9 +154,10 @@ class Lookup extends React.Component { } saveModal() { - const { onLookupSave, name } = this.props; + const { onLookupSave, name, multiple } = this.props; const { lookupSelectedItems } = this.state; - onLookupSave(lookupSelectedItems, name); + const value = multiple ? lookupSelectedItems : lookupSelectedItems[0]; + onLookupSave(value, name); this.handleModalToggle(); } @@ -137,13 +169,21 @@ class Lookup extends React.Component { results, count, } = this.state; - const { id, lookupHeader, value, columns, i18n } = this.props; + const { + id, + lookupHeader, + value, + columns, + multiple, + name, + i18n, + } = this.props; const header = lookupHeader || i18n._(t`items`); const chips = value ? ( - {value.map(chip => ( + {(multiple ? value : [value]).map(chip => ( this.toggleSelected(chip)}> {chip.name} @@ -197,9 +237,11 @@ class Lookup extends React.Component { i.id === item.id)} onSelect={() => this.toggleSelected(item)} + isRadio={!multiple} /> )} renderToolbar={props => } @@ -220,15 +262,19 @@ class Lookup extends React.Component { } } +const Item = shape({ + id: number.isRequired, +}); + Lookup.propTypes = { - id: PropTypes.string, - getItems: PropTypes.func.isRequired, - lookupHeader: PropTypes.string, - name: PropTypes.string, // TODO: delete, unused ? - onLookupSave: PropTypes.func.isRequired, - value: PropTypes.arrayOf(PropTypes.object).isRequired, - sortedColumnKey: PropTypes.string.isRequired, - multiple: PropTypes.bool, + id: string, + getItems: func.isRequired, + lookupHeader: string, + name: string, + onLookupSave: func.isRequired, + value: oneOfType([Item, arrayOf(Item)]).isRequired, + sortedColumnKey: string.isRequired, + multiple: bool, }; Lookup.defaultProps = { diff --git a/awx/ui_next/src/screens/Template/shared/InventoriesLookup.jsx b/awx/ui_next/src/screens/Template/shared/InventoriesLookup.jsx index 2f44a716db..722a6b5eba 100644 --- a/awx/ui_next/src/screens/Template/shared/InventoriesLookup.jsx +++ b/awx/ui_next/src/screens/Template/shared/InventoriesLookup.jsx @@ -1,5 +1,5 @@ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; +import { string, func } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { FormGroup, Tooltip } from '@patternfly/react-core'; @@ -7,6 +7,7 @@ import { QuestionCircleIcon } from '@patternfly/react-icons'; import { InventoriesAPI } from '@api'; import Lookup from '@components/Lookup'; +import { Inventory } from '../../../types'; const getInventories = async params => InventoriesAPI.read(params); @@ -35,7 +36,6 @@ class InventoriesLookup extends React.Component { value={value} onLookupSave={onChange} getItems={getInventories} - multiple columns={[ { name: i18n._(t`Name`), key: 'name', isSortable: true }, { @@ -59,9 +59,9 @@ class InventoriesLookup extends React.Component { } InventoriesLookup.propTypes = { - value: PropTypes.arrayOf(PropTypes.object).isRequired, - tooltip: PropTypes.string, - onChange: PropTypes.func.isRequired, + value: Inventory.isRequired, + tooltip: string, + onChange: func.isRequired, }; InventoriesLookup.defaultProps = { diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index e823024044..ada2ba4ff3 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -10,10 +10,10 @@ import AnsibleSelect from '@components/AnsibleSelect'; import FormActionGroup from '@components/FormActionGroup'; import FormField from '@components/FormField'; import FormRow from '@components/FormRow'; -import Lookup from '@components/Lookup'; import { required } from '@util/validators'; import styled from 'styled-components'; import { JobTemplate } from '@types'; +import InventoriesLookup from './InventoriesLookup'; const QuestionCircleIcon = styled(PFQuestionCircleIcon)` margin-left: 10px; @@ -34,11 +34,21 @@ class JobTemplateForm extends Component { job_type: 'run', project: '', playbook: '', + summary_fields: {}, }, }; + constructor(props) { + super(props); + + this.state = { + inventory: props.template.summary_fields.inventory, + }; + } + render() { const { handleCancel, handleSubmit, i18n, template } = this.props; + const { inventory } = this.state; const jobTypeOptions = [ { @@ -109,8 +119,19 @@ class JobTemplateForm extends Component { ( - ( + { + form.setFieldValue('inventory', value.id); + this.setState({ inventory: value }); + }} + /> + )} + /> + {/* {console.log(value)}} getItems={() => ({ data: { - results: [{id: 1, name: 'foo'}], - count: 1 + results: [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}], + count: 2 } })} columns={[ @@ -133,9 +154,7 @@ class JobTemplateForm extends Component { ]} sortedColumnsKey="name" /> - - )} - /> + */} {/*