Merge pull request #9120 from AlexSCorey/9043-InventoryFileField

Inventory File field and playbook field are both now type ahead.

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2021-02-24 14:15:28 +00:00 committed by GitHub
commit 615cc11d0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 126 additions and 81 deletions

View File

@ -161,7 +161,7 @@ const InventorySourceFormFields = ({ source, sourceOptions, i18n }) => {
/>
</FormGroup>
)}
{sourceField.value !== '' && (
{!['', 'custom'].includes(sourceField.value) && (
<SubFormLayout>
<Title size="md" headingLevel="h4">
{i18n._(t`Source details`)}

View File

@ -93,7 +93,8 @@ describe('<InventorySourceForm />', () => {
id: 2,
name: 'mock proj',
});
wrapper.find('AnsibleSelect#source_path').prop('onChange')(null, 'foo');
wrapper.find('Select#source_path').prop('onToggle')();
wrapper.find('Select#source_path').prop('onSelect')(null, 'foo');
wrapper.find('AnsibleSelect#verbosity').prop('onChange')(null, '2');
wrapper.find('button[aria-label="Save"]').simulate('click');
});

View File

@ -1,13 +1,17 @@
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { FormGroup } from '@patternfly/react-core';
import {
FormGroup,
SelectVariant,
Select,
SelectOption,
} from '@patternfly/react-core';
import { ProjectsAPI } from '../../../../api';
import useRequest from '../../../../util/useRequest';
import { required } from '../../../../util/validators';
import AnsibleSelect from '../../../../components/AnsibleSelect';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
import ProjectLookup from '../../../../components/Lookup/ProjectLookup';
import Popover from '../../../../components/Popover';
@ -21,6 +25,8 @@ import {
} from './SharedFields';
const SCMSubForm = ({ autoPopulateProject, i18n }) => {
const [isOpen, setIsOpen] = useState(false);
const [sourcePath, setSourcePath] = useState([]);
const { setFieldValue, setFieldTouched } = useFormikContext();
const [credentialField] = useField('credential');
const [projectField, projectMeta, projectHelpers] = useField({
@ -32,14 +38,10 @@ const SCMSubForm = ({ autoPopulateProject, i18n }) => {
validate: required(i18n._(t`Select a value for this field`), i18n),
});
const {
error: sourcePathError,
request: fetchSourcePath,
result: sourcePath,
} = useRequest(
const { error: sourcePathError, request: fetchSourcePath } = useRequest(
useCallback(async projectId => {
const { data } = await ProjectsAPI.readInventories(projectId);
return [...data, '/ (project root)'];
setSourcePath([...data, '/ (project root)']);
}, []),
[]
);
@ -106,26 +108,40 @@ const SCMSubForm = ({ autoPopulateProject, i18n }) => {
/>
}
>
<AnsibleSelect
{...sourcePathField}
<Select
variant={SelectVariant.typeahead}
onToggle={setIsOpen}
isOpen={isOpen}
selections={sourcePathField.value}
id="source_path"
isValid={
(!sourcePathMeta.error || !sourcePathMeta.touched) &&
!sourcePathError?.message
}
data={[
{
value: '',
key: '',
label: i18n._(t`Choose an inventory file`),
isDisabled: true,
},
...sourcePath.map(value => ({ value, label: value, key: value })),
]}
onChange={(event, value) => {
onSelect={(event, value) => {
setIsOpen(false);
value = value.trim();
if (!value.endsWith('/')) {
value += '/';
}
sourcePathHelpers.setValue(value);
}}
/>
aria-label={i18n._(t`Select source path`)}
placeholder={i18n._(t`Select source path`)}
isCreatable
onCreateOption={value => {
value.trim();
if (!value.endsWith('/')) {
value += '/';
}
setSourcePath([...sourcePath, value]);
}}
>
{sourcePath.map(path => (
<SelectOption key={path} id={path} value={path} />
))}
</Select>
</FormGroup>
<VerbosityField />
<HostFilterField />

View File

@ -89,17 +89,17 @@ describe('<SCMSubForm />', () => {
});
test('changing source project should reset source path dropdown', async () => {
expect(wrapper.find('AnsibleSelect#source_path').prop('value')).toEqual('');
expect(wrapper.find('Select#source_path').prop('selections')).toEqual('');
await act(async () => {
await wrapper.find('AnsibleSelect#source_path').prop('onChange')(
null,
'bar'
);
await wrapper.find('Select#source_path').prop('onToggle')();
});
wrapper.update();
expect(wrapper.find('AnsibleSelect#source_path').prop('value')).toEqual(
'bar'
await act(async () => {
await wrapper.find('Select#source_path').prop('onSelect')(null, 'bar');
});
wrapper.update();
expect(wrapper.find('Select#source_path').prop('selections')).toEqual(
'bar/'
);
await act(async () => {
@ -109,6 +109,37 @@ describe('<SCMSubForm />', () => {
});
});
wrapper.update();
expect(wrapper.find('AnsibleSelect#source_path').prop('value')).toEqual('');
expect(wrapper.find('Select#source_path').prop('selections')).toEqual('');
});
test('should be able to create custom source path', async () => {
const customInitialValues = {
credential: { id: 1, name: 'Credential' },
custom_virtualenv: '',
overwrite: false,
overwrite_vars: false,
source_path: '/path',
source_project: { id: 1, name: 'Source project' },
source_script: null,
source_vars: '---\n',
update_cache_timeout: 0,
update_on_launch: true,
update_on_project_update: false,
verbosity: 1,
};
let customWrapper;
await act(async () => {
customWrapper = mountWithContexts(
<Formik initialValues={customInitialValues}>
<SCMSubForm />
</Formik>
);
});
await act(async () => {
customWrapper.find('Select').invoke('onSelect')({}, 'newPath');
});
customWrapper.update();
expect(customWrapper.find('Select').prop('selections')).toBe('newPath/');
});
});

View File

@ -145,12 +145,9 @@ describe('<JobTemplateAdd />', () => {
summary_fields: { organization: { id: 1, name: 'Org Foo' } },
});
wrapper.update();
wrapper
.find('PlaybookSelect')
.prop('field')
.onChange({
target: { value: 'Baz', name: 'playbook' },
});
wrapper.find('Select#template-playbook').prop('onToggle')();
wrapper.update();
wrapper.find('Select#template-playbook').prop('onSelect')(null, 'Baz');
});
wrapper.update();
act(() => {
@ -207,12 +204,9 @@ describe('<JobTemplateAdd />', () => {
summary_fields: { organization: { id: 1, name: 'Org Foo' } },
});
wrapper.update();
wrapper
.find('PlaybookSelect')
.prop('field')
.onChange({
target: { value: 'Bar', name: 'playbook' },
});
wrapper.find('Select#template-playbook').prop('onToggle')();
wrapper.update();
wrapper.find('Select#template-playbook').prop('onSelect')(null, 'Bar');
});
wrapper.update();
act(() => {

View File

@ -307,9 +307,10 @@ function JobTemplateForm({
}
>
<PlaybookSelect
onChange={playbookHelpers.setValue}
projectId={projectField.value?.id}
isValid={!playbookMeta.touched || !playbookMeta.error}
field={playbookField}
selected={playbookField.value}
onBlur={() => playbookHelpers.setTouched()}
onError={setContentError}
/>

View File

@ -200,9 +200,12 @@ describe('<JobTemplateForm />', () => {
'devel'
);
wrapper.find('TextInputBase#template-limit').prop('onChange')(1234567890);
wrapper.find('AnsibleSelect[name="playbook"]').simulate('change', {
target: { value: 'new baz type', name: 'playbook' },
});
wrapper.find('Select#template-playbook').prop('onToggle')();
wrapper.update();
wrapper.find('Select#template-playbook').prop('onSelect')(
null,
'new baz type'
);
});
await act(async () => {
@ -237,9 +240,9 @@ describe('<JobTemplateForm />', () => {
expect(wrapper.find('input#template-limit').prop('value')).toEqual(
1234567890
);
expect(
wrapper.find('AnsibleSelect[name="playbook"]').prop('value')
).toEqual('new baz type');
expect(wrapper.find('Select#template-playbook').prop('selections')).toEqual(
'new baz type'
);
expect(wrapper.find('MultiCredentialsLookup').prop('value')).toEqual([
{
id: 2,

View File

@ -2,12 +2,21 @@ import React, { useCallback, useEffect, useState } from 'react';
import { number, string, oneOfType } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import AnsibleSelect from '../../../components/AnsibleSelect';
import { SelectVariant, Select, SelectOption } from '@patternfly/react-core';
import { ProjectsAPI } from '../../../api';
import useRequest from '../../../util/useRequest';
function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
function PlaybookSelect({
projectId,
isValid,
selected,
onBlur,
onError,
onChange,
i18n,
}) {
const [isDisabled, setIsDisabled] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const {
result: options,
request: fetchOptions,
@ -20,21 +29,8 @@ function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
}
const { data } = await ProjectsAPI.readPlaybooks(projectId);
const opts = (data || []).map(playbook => ({
value: playbook,
key: playbook,
label: playbook,
isDisabled: false,
}));
opts.unshift({
value: '',
key: '',
label: i18n._(t`Choose a playbook`),
isDisabled: false,
});
return opts;
}, [projectId, i18n]),
return data;
}, [projectId]),
[]
);
@ -52,23 +48,26 @@ function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
}
}, [error, onError]);
const isDisabledData = [
{
value: field.value || '',
label: field.value || '',
key: 1,
isDisabled: true,
},
];
return (
<AnsibleSelect
<Select
isOpen={isOpen}
variant={SelectVariant.typeahead}
selections={selected}
onToggle={setIsOpen}
placeholderText={i18n._(t`Select a playbook`)}
isCreateable={false}
onSelect={(event, value) => {
onChange(value);
}}
id="template-playbook"
data={isDisabled ? isDisabledData : options}
isValid={isValid}
{...field}
onBlur={onBlur}
isDisabled={isLoading || isDisabled}
/>
>
{options.map(opt => (
<SelectOption key={opt} value={opt} />
))}
</Select>
);
}
PlaybookSelect.propTypes = {