mirror of
https://github.com/ansible/awx.git
synced 2026-03-26 21:35:01 -02:30
Merge pull request #6356 from keithjgrant/5899-survey-add-form
Survey add/edit forms Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
41
awx/ui_next/package-lock.json
generated
41
awx/ui_next/package-lock.json
generated
@@ -10625,7 +10625,8 @@
|
|||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
@@ -10646,12 +10647,14 @@
|
|||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -10666,17 +10669,20 @@
|
|||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -10793,7 +10799,8 @@
|
|||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
@@ -10805,6 +10812,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -10819,6 +10827,7 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
@@ -10826,12 +10835,14 @@
|
|||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
@@ -10850,6 +10861,7 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
@@ -10930,7 +10942,8 @@
|
|||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@@ -10942,6 +10955,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
@@ -11027,7 +11041,8 @@
|
|||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@@ -11063,6 +11078,7 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
@@ -11082,6 +11098,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
@@ -11125,12 +11142,14 @@
|
|||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ function FormField(props) {
|
|||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
{...rest}
|
{...rest}
|
||||||
{...field}
|
{...field}
|
||||||
|
type={type}
|
||||||
onChange={(value, event) => {
|
onChange={(value, event) => {
|
||||||
field.onChange(event);
|
field.onChange(event);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ function RoutedTabs(props) {
|
|||||||
if (match) {
|
if (match) {
|
||||||
return match.id;
|
return match.id;
|
||||||
}
|
}
|
||||||
|
const subpathMatch = tabsArray.find(tab =>
|
||||||
|
history.location.pathname.startsWith(tab.link)
|
||||||
|
);
|
||||||
|
if (subpathMatch) {
|
||||||
|
return subpathMatch.id;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ describe('<HostAdd />', () => {
|
|||||||
let history;
|
let history;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
history = createMemoryHistory();
|
history = createMemoryHistory({
|
||||||
|
initialEntries: ['/templates/job_templates/1/survey/edit/foo'],
|
||||||
|
state: { some: 'state' },
|
||||||
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<HostAdd />, {
|
wrapper = mountWithContexts(<HostAdd />, {
|
||||||
context: { router: { history } },
|
context: { router: { history } },
|
||||||
|
|||||||
@@ -95,7 +95,50 @@ function SurveyList({
|
|||||||
</DataList>
|
</DataList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isDeleteModalOpen) {
|
||||||
|
return (
|
||||||
|
<AlertModal
|
||||||
|
variant="danger"
|
||||||
|
title={
|
||||||
|
isAllSelected ? i18n._(t`Delete Survey`) : i18n._(t`Delete Questions`)
|
||||||
|
}
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setSelected([]);
|
||||||
|
}}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
key="delete"
|
||||||
|
variant="danger"
|
||||||
|
aria-label={i18n._(t`confirm delete`)}
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="cancel"
|
||||||
|
variant="secondary"
|
||||||
|
aria-label={i18n._(t`cancel delete`)}
|
||||||
|
onClick={() => {
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setSelected([]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n._(t`Cancel`)}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div>{i18n._(t`This action will delete the following:`)}</div>
|
||||||
|
{selected.map(question => (
|
||||||
|
<span key={question.variable}>
|
||||||
|
<strong>{question.question_name}</strong>
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</AlertModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SurveyToolbar
|
<SurveyToolbar
|
||||||
@@ -3,7 +3,7 @@ import { act } from 'react-dom/test-utils';
|
|||||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
import SurveyList from './SurveyList';
|
import SurveyList from './SurveyList';
|
||||||
import { JobTemplatesAPI } from '@api';
|
import { JobTemplatesAPI } from '@api';
|
||||||
import mockJobTemplateData from './data.job_template.json';
|
import mockJobTemplateData from '../shared/data.job_template.json';
|
||||||
|
|
||||||
jest.mock('@api/models/JobTemplates');
|
jest.mock('@api/models/JobTemplates');
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Button as _Button,
|
Button as _Button,
|
||||||
DataListAction as _DataListAction,
|
DataListAction as _DataListAction,
|
||||||
@@ -75,13 +76,13 @@ function SurveyListItem({
|
|||||||
/>
|
/>
|
||||||
<DataListItemCells
|
<DataListItemCells
|
||||||
dataListCells={[
|
dataListCells={[
|
||||||
<DataListCell key={question.question_name}>
|
<DataListCell key="name">
|
||||||
{question.question_name}
|
<Link to={`survey/edit/${question.variable}`}>
|
||||||
</DataListCell>,
|
{question.question_name}
|
||||||
<DataListCell key={question.type}>{question.type}</DataListCell>,
|
</Link>
|
||||||
<DataListCell key={question.default}>
|
|
||||||
{question.default}
|
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
|
<DataListCell key="type">{question.type}</DataListCell>,
|
||||||
|
<DataListCell key="default">{question.default}</DataListCell>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DataListItemRow>
|
</DataListItemRow>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
|
import SurveyQuestionForm from './SurveyQuestionForm';
|
||||||
|
|
||||||
|
export default function SurveyQuestionAdd({ survey, updateSurvey }) {
|
||||||
|
const [formError, setFormError] = useState(null);
|
||||||
|
const history = useHistory();
|
||||||
|
const match = useRouteMatch();
|
||||||
|
|
||||||
|
const handleSubmit = async question => {
|
||||||
|
try {
|
||||||
|
if (survey.spec?.some(q => q.variable === question.variable)) {
|
||||||
|
setFormError(
|
||||||
|
new Error(
|
||||||
|
`Survey already contains a question with variable named “${question.variable}”`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newSpec = survey.spec ? survey.spec.concat(question) : [question];
|
||||||
|
await updateSurvey(newSpec);
|
||||||
|
history.push(match.url.replace('/add', ''));
|
||||||
|
} catch (err) {
|
||||||
|
setFormError(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.push(match.url.replace('/add', ''));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<SurveyQuestionForm
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
submitError={formError}
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import SurveyQuestionAdd from './SurveyQuestionAdd';
|
||||||
|
|
||||||
|
const survey = {
|
||||||
|
spec: [
|
||||||
|
{
|
||||||
|
question_name: 'What is the foo?',
|
||||||
|
question_description: 'more about the foo',
|
||||||
|
variable: 'foo',
|
||||||
|
required: true,
|
||||||
|
type: 'text',
|
||||||
|
min: 0,
|
||||||
|
max: 1024,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question_name: 'Who shot the sheriff?',
|
||||||
|
question_description: 'they did not shoot the deputy',
|
||||||
|
variable: 'bar',
|
||||||
|
required: true,
|
||||||
|
type: 'textarea',
|
||||||
|
min: 0,
|
||||||
|
max: 1024,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<SurveyQuestionAdd />', () => {
|
||||||
|
let updateSurvey;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
updateSurvey = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render form', () => {
|
||||||
|
let wrapper;
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionAdd survey={survey} updateSurvey={updateSurvey} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('SurveyQuestionForm')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call updateSurvey', () => {
|
||||||
|
let wrapper;
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionAdd survey={survey} updateSurvey={updateSurvey} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('SurveyQuestionForm').invoke('handleSubmit')({
|
||||||
|
question_name: 'new question',
|
||||||
|
variable: 'question',
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(updateSurvey).toHaveBeenCalledWith([
|
||||||
|
...survey.spec,
|
||||||
|
{
|
||||||
|
question_name: 'new question',
|
||||||
|
variable: 'question',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should set formError', async () => {
|
||||||
|
const realConsoleError = global.console.error;
|
||||||
|
global.console.error = jest.fn();
|
||||||
|
const err = new Error('oops');
|
||||||
|
updateSurvey.mockImplementation(() => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
let wrapper;
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionAdd survey={survey} updateSurvey={updateSurvey} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('SurveyQuestionForm').invoke('handleSubmit')({
|
||||||
|
question_name: 'new question',
|
||||||
|
variable: 'question',
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('SurveyQuestionForm').prop('submitError')).toEqual(err);
|
||||||
|
global.console.error = realConsoleError;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate error for duplicate variable names', async () => {
|
||||||
|
const realConsoleError = global.console.error;
|
||||||
|
global.console.error = jest.fn();
|
||||||
|
let wrapper;
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionAdd survey={survey} updateSurvey={updateSurvey} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('SurveyQuestionForm').invoke('handleSubmit')({
|
||||||
|
question_name: 'new question',
|
||||||
|
variable: 'foo',
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
const err = wrapper.find('SurveyQuestionForm').prop('submitError');
|
||||||
|
expect(err.message).toEqual(
|
||||||
|
'Survey already contains a question with variable named “foo”'
|
||||||
|
);
|
||||||
|
global.console.error = realConsoleError;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||||
|
import ContentLoading from '@components/ContentLoading';
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
|
import SurveyQuestionForm from './SurveyQuestionForm';
|
||||||
|
|
||||||
|
export default function SurveyQuestionEdit({ survey, updateSurvey }) {
|
||||||
|
const [formError, setFormError] = useState(null);
|
||||||
|
const history = useHistory();
|
||||||
|
const match = useRouteMatch();
|
||||||
|
|
||||||
|
if (!survey) {
|
||||||
|
return <ContentLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const question = survey.spec.find(q => q.variable === match.params.variable);
|
||||||
|
|
||||||
|
const navigateToList = () => {
|
||||||
|
const index = match.url.indexOf('/edit');
|
||||||
|
history.push(match.url.substr(0, index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async formData => {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
formData.variable !== question.variable &&
|
||||||
|
survey.spec.find(q => q.variable === formData.variable)
|
||||||
|
) {
|
||||||
|
setFormError(
|
||||||
|
new Error(
|
||||||
|
`Survey already contains a question with variable named “${formData.variable}”`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const questionIndex = survey.spec.findIndex(
|
||||||
|
q => q.variable === match.params.variable
|
||||||
|
);
|
||||||
|
if (questionIndex === -1) {
|
||||||
|
throw new Error('Question not found in spec');
|
||||||
|
}
|
||||||
|
await updateSurvey([
|
||||||
|
...survey.spec.slice(0, questionIndex),
|
||||||
|
formData,
|
||||||
|
...survey.spec.slice(questionIndex + 1),
|
||||||
|
]);
|
||||||
|
navigateToList();
|
||||||
|
} catch (err) {
|
||||||
|
setFormError(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<SurveyQuestionForm
|
||||||
|
question={question}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
handleCancel={navigateToList}
|
||||||
|
submitError={formError}
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import { Switch, Route } from 'react-router-dom';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import SurveyQuestionEdit from './SurveyQuestionEdit';
|
||||||
|
|
||||||
|
const survey = {
|
||||||
|
spec: [
|
||||||
|
{
|
||||||
|
question_name: 'What is the foo?',
|
||||||
|
question_description: 'more about the foo',
|
||||||
|
variable: 'foo',
|
||||||
|
required: true,
|
||||||
|
type: 'text',
|
||||||
|
min: 0,
|
||||||
|
max: 1024,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question_name: 'Who shot the sheriff?',
|
||||||
|
question_description: 'they did not shoot the deputy',
|
||||||
|
variable: 'bar',
|
||||||
|
required: true,
|
||||||
|
type: 'textarea',
|
||||||
|
min: 0,
|
||||||
|
max: 1024,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<SurveyQuestionEdit />', () => {
|
||||||
|
let updateSurvey;
|
||||||
|
let history;
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
history = createMemoryHistory({
|
||||||
|
initialEntries: ['/templates/job_templates/1/survey/edit/foo'],
|
||||||
|
});
|
||||||
|
updateSurvey = jest.fn();
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Switch>
|
||||||
|
<Route path="/templates/:templateType/:id/survey/edit/:variable">
|
||||||
|
<SurveyQuestionEdit survey={survey} updateSurvey={updateSurvey} />
|
||||||
|
</Route>
|
||||||
|
</Switch>,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render form', () => {
|
||||||
|
expect(wrapper.find('SurveyQuestionForm')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call updateSurvey', () => {
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('SurveyQuestionForm').invoke('handleSubmit')({
|
||||||
|
question_name: 'new question',
|
||||||
|
variable: 'question',
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(updateSurvey).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
|
question_name: 'new question',
|
||||||
|
variable: 'question',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
survey.spec[1],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should set formError', async () => {
|
||||||
|
const realConsoleError = global.console.error;
|
||||||
|
global.console.error = jest.fn();
|
||||||
|
const err = new Error('oops');
|
||||||
|
updateSurvey.mockImplementation(() => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('SurveyQuestionForm').invoke('handleSubmit')({
|
||||||
|
question_name: 'new question',
|
||||||
|
variable: 'question',
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('SurveyQuestionForm').prop('submitError')).toEqual(err);
|
||||||
|
global.console.error = realConsoleError;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate error for duplicate variable names', async () => {
|
||||||
|
const realConsoleError = global.console.error;
|
||||||
|
global.console.error = jest.fn();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('SurveyQuestionForm').invoke('handleSubmit')({
|
||||||
|
question_name: 'new question',
|
||||||
|
variable: 'bar',
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
const err = wrapper.find('SurveyQuestionForm').prop('submitError');
|
||||||
|
expect(err.message).toEqual(
|
||||||
|
'Survey already contains a question with variable named “bar”'
|
||||||
|
);
|
||||||
|
global.console.error = realConsoleError;
|
||||||
|
});
|
||||||
|
});
|
||||||
231
awx/ui_next/src/screens/Template/Survey/SurveyQuestionForm.jsx
Normal file
231
awx/ui_next/src/screens/Template/Survey/SurveyQuestionForm.jsx
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { func, string, bool, number, shape } from 'prop-types';
|
||||||
|
import { Formik, useField } from 'formik';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Form, FormGroup } from '@patternfly/react-core';
|
||||||
|
import { FormColumnLayout } from '@components/FormLayout';
|
||||||
|
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
|
||||||
|
import FormField, {
|
||||||
|
CheckboxField,
|
||||||
|
PasswordField,
|
||||||
|
FormSubmitError,
|
||||||
|
FieldTooltip,
|
||||||
|
} from '@components/FormField';
|
||||||
|
import AnsibleSelect from '@components/AnsibleSelect';
|
||||||
|
import { required, noWhiteSpace, combine } from '@util/validators';
|
||||||
|
|
||||||
|
function AnswerTypeField({ i18n }) {
|
||||||
|
const [field] = useField({
|
||||||
|
name: 'type',
|
||||||
|
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={i18n._(t`Answer Type`)}
|
||||||
|
isRequired
|
||||||
|
fieldId="question-answer-type"
|
||||||
|
>
|
||||||
|
<FieldTooltip
|
||||||
|
content={i18n._(
|
||||||
|
t`Choose an answer type or format you want as the prompt for the user.
|
||||||
|
Refer to the Ansible Tower Documentation for more additional
|
||||||
|
information about each option.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<AnsibleSelect
|
||||||
|
id="question-type"
|
||||||
|
{...field}
|
||||||
|
data={[
|
||||||
|
{ key: 'text', value: 'text', label: i18n._(t`Text`) },
|
||||||
|
{ key: 'textarea', value: 'textarea', label: i18n._(t`Textarea`) },
|
||||||
|
{ key: 'password', value: 'password', label: i18n._(t`Password`) },
|
||||||
|
{
|
||||||
|
key: 'multiplechoice',
|
||||||
|
value: 'multiplechoice',
|
||||||
|
label: i18n._(t`Multiple Choice (single select)`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'multiselect',
|
||||||
|
value: 'multiselect',
|
||||||
|
label: i18n._(t`Multiple Choice (multiple select)`),
|
||||||
|
},
|
||||||
|
{ key: 'integer', value: 'integer', label: i18n._(t`Integer`) },
|
||||||
|
{ key: 'float', value: 'float', label: i18n._(t`Float`) },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SurveyQuestionForm({
|
||||||
|
question,
|
||||||
|
handleSubmit,
|
||||||
|
handleCancel,
|
||||||
|
submitError,
|
||||||
|
i18n,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
question_name: question?.question_name || '',
|
||||||
|
question_description: question?.question_description || '',
|
||||||
|
required: question ? question?.required : true,
|
||||||
|
type: question?.type || 'text',
|
||||||
|
variable: question?.variable || '',
|
||||||
|
min: question?.min || 0,
|
||||||
|
max: question?.max || 1024,
|
||||||
|
default: question?.default || '',
|
||||||
|
choices: question?.choices || '',
|
||||||
|
new_question: !question,
|
||||||
|
}}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
{formik => (
|
||||||
|
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||||
|
<FormColumnLayout>
|
||||||
|
<FormField
|
||||||
|
id="question-name"
|
||||||
|
name="question_name"
|
||||||
|
type="text"
|
||||||
|
label={i18n._(t`Question`)}
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="question-description"
|
||||||
|
name="question_description"
|
||||||
|
type="text"
|
||||||
|
label={i18n._(t`Description`)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="question-variable"
|
||||||
|
name="variable"
|
||||||
|
type="text"
|
||||||
|
label={i18n._(t`Answer Variable Name`)}
|
||||||
|
validate={combine([noWhiteSpace(i18n), required(null, i18n)])}
|
||||||
|
isRequired
|
||||||
|
tooltip={i18n._(
|
||||||
|
t`The suggested format for variable names is lowercase and
|
||||||
|
underscore-separated (for example, foo_bar, user_id, host_name,
|
||||||
|
etc.). Variable names with spaces are not allowed.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<AnswerTypeField i18n={i18n} />
|
||||||
|
<CheckboxField
|
||||||
|
id="question-required"
|
||||||
|
name="required"
|
||||||
|
label="Required"
|
||||||
|
/>
|
||||||
|
</FormColumnLayout>
|
||||||
|
<FormColumnLayout>
|
||||||
|
{['text', 'textarea', 'password'].includes(formik.values.type) && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
id="question-min"
|
||||||
|
name="min"
|
||||||
|
type="number"
|
||||||
|
label={i18n._(t`Minimum length`)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="question-max"
|
||||||
|
name="max"
|
||||||
|
type="number"
|
||||||
|
label={i18n._(t`Maximum length`)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{['integer', 'float'].includes(formik.values.type) && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
id="question-min"
|
||||||
|
name="min"
|
||||||
|
type="number"
|
||||||
|
label={i18n._(t`Minimum`)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="question-max"
|
||||||
|
name="max"
|
||||||
|
type="number"
|
||||||
|
label={i18n._(t`Maximum`)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{['text', 'integer', 'float'].includes(formik.values.type) && (
|
||||||
|
<FormField
|
||||||
|
id="question-default"
|
||||||
|
name="default"
|
||||||
|
type={formik.values.type === 'text' ? 'text' : 'number'}
|
||||||
|
label={i18n._(t`Default answer`)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{formik.values.type === 'textarea' && (
|
||||||
|
<FormField
|
||||||
|
id="question-default"
|
||||||
|
name="default"
|
||||||
|
type="textarea"
|
||||||
|
label={i18n._(t`Default answer`)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{formik.values.type === 'password' && (
|
||||||
|
<PasswordField
|
||||||
|
id="question-default"
|
||||||
|
name="default"
|
||||||
|
label={i18n._(t`Default answer`)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{['multiplechoice', 'multiselect'].includes(formik.values.type) && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
id="question-options"
|
||||||
|
name="choices"
|
||||||
|
type="textarea"
|
||||||
|
label={i18n._(t`Multiple Choice Options`)}
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="question-default"
|
||||||
|
name="default"
|
||||||
|
type={
|
||||||
|
formik.values.type === 'multiplechoice'
|
||||||
|
? 'text'
|
||||||
|
: 'textarea'
|
||||||
|
}
|
||||||
|
label={i18n._(t`Default answer`)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FormColumnLayout>
|
||||||
|
<FormSubmitError error={submitError} />
|
||||||
|
<FormActionGroup
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SurveyQuestionForm.propTypes = {
|
||||||
|
question: shape({
|
||||||
|
question_name: string.isRequired,
|
||||||
|
question_description: string.isRequired,
|
||||||
|
required: bool,
|
||||||
|
type: string.isRequired,
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
}),
|
||||||
|
handleSubmit: func.isRequired,
|
||||||
|
handleCancel: func.isRequired,
|
||||||
|
submitError: shape({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
SurveyQuestionForm.defaultProps = {
|
||||||
|
question: null,
|
||||||
|
submitError: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(SurveyQuestionForm);
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import SurveyQuestionForm from './SurveyQuestionForm';
|
||||||
|
|
||||||
|
const question = {
|
||||||
|
question_name: 'What is the foo?',
|
||||||
|
question_description: 'more about the foo',
|
||||||
|
variable: 'foo',
|
||||||
|
required: true,
|
||||||
|
type: 'text',
|
||||||
|
min: 0,
|
||||||
|
max: 1024,
|
||||||
|
};
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
|
async function selectType(wrapper, type) {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('AnsibleSelect#question-type').invoke('onChange')({
|
||||||
|
target: {
|
||||||
|
name: 'type',
|
||||||
|
value: type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('<SurveyQuestionForm />', () => {
|
||||||
|
test('should render form', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionForm
|
||||||
|
question={question}
|
||||||
|
handleSubmit={noop}
|
||||||
|
handleCancel={noop}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('FormField#question-name input').prop('value')).toEqual(
|
||||||
|
question.question_name
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('FormField#question-description input').prop('value')
|
||||||
|
).toEqual(question.question_description);
|
||||||
|
expect(
|
||||||
|
wrapper.find('FormField#question-variable input').prop('value')
|
||||||
|
).toEqual(question.variable);
|
||||||
|
expect(
|
||||||
|
wrapper.find('CheckboxField#question-required input').prop('checked')
|
||||||
|
).toEqual(true);
|
||||||
|
expect(wrapper.find('AnsibleSelect#question-type').prop('value')).toEqual(
|
||||||
|
question.type
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should provide fields for text question', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionForm
|
||||||
|
question={question}
|
||||||
|
handleSubmit={noop}
|
||||||
|
handleCancel={noop}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('FormField#question-min').prop('type')).toEqual(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('FormField#question-max').prop('type')).toEqual(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('FormField#question-default').prop('type')).toEqual(
|
||||||
|
'text'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should provide fields for textarea question', async () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionForm
|
||||||
|
question={question}
|
||||||
|
handleSubmit={noop}
|
||||||
|
handleCancel={noop}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await selectType(wrapper, 'textarea');
|
||||||
|
|
||||||
|
expect(wrapper.find('FormField#question-min').prop('type')).toEqual(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('FormField#question-max').prop('type')).toEqual(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('FormField#question-default').prop('type')).toEqual(
|
||||||
|
'textarea'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should provide fields for password question', async () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionForm
|
||||||
|
question={question}
|
||||||
|
handleSubmit={noop}
|
||||||
|
handleCancel={noop}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await selectType(wrapper, 'password');
|
||||||
|
|
||||||
|
expect(wrapper.find('FormField#question-min').prop('type')).toEqual(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('FormField#question-max').prop('type')).toEqual(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#question-default input').prop('type')
|
||||||
|
).toEqual('password');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should provide fields for multiple choice question', async () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionForm
|
||||||
|
question={question}
|
||||||
|
handleSubmit={noop}
|
||||||
|
handleCancel={noop}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await selectType(wrapper, 'multiplechoice');
|
||||||
|
|
||||||
|
expect(wrapper.find('FormField#question-options').prop('type')).toEqual(
|
||||||
|
'textarea'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('FormField#question-default').prop('type')).toEqual(
|
||||||
|
'text'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should provide fields for multi-select question', async () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionForm
|
||||||
|
question={question}
|
||||||
|
handleSubmit={noop}
|
||||||
|
handleCancel={noop}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await selectType(wrapper, 'multiselect');
|
||||||
|
|
||||||
|
expect(wrapper.find('FormField#question-options').prop('type')).toEqual(
|
||||||
|
'textarea'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('FormField#question-default').prop('type')).toEqual(
|
||||||
|
'textarea'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should provide fields for integer question', async () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionForm
|
||||||
|
question={question}
|
||||||
|
handleSubmit={noop}
|
||||||
|
handleCancel={noop}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await selectType(wrapper, 'integer');
|
||||||
|
|
||||||
|
expect(wrapper.find('FormField#question-min').prop('type')).toEqual(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('FormField#question-max').prop('type')).toEqual(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('FormField#question-default input').prop('type')
|
||||||
|
).toEqual('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should provide fields for float question', async () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SurveyQuestionForm
|
||||||
|
question={question}
|
||||||
|
handleSubmit={noop}
|
||||||
|
handleCancel={noop}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await selectType(wrapper, 'float');
|
||||||
|
|
||||||
|
expect(wrapper.find('FormField#question-min').prop('type')).toEqual(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('FormField#question-max').prop('type')).toEqual(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('FormField#question-default input').prop('type')
|
||||||
|
).toEqual('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useRouteMatch } from 'react-router-dom';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ function SurveyToolbar({
|
|||||||
isDeleteDisabled,
|
isDeleteDisabled,
|
||||||
onToggleDeleteModal,
|
onToggleDeleteModal,
|
||||||
}) {
|
}) {
|
||||||
|
const match = useRouteMatch();
|
||||||
return (
|
return (
|
||||||
<DataToolbar id="survey-toolbar">
|
<DataToolbar id="survey-toolbar">
|
||||||
<DataToolbarContent>
|
<DataToolbarContent>
|
||||||
@@ -45,7 +47,7 @@ function SurveyToolbar({
|
|||||||
</DataToolbarItem>
|
</DataToolbarItem>
|
||||||
<DataToolbarGroup>
|
<DataToolbarGroup>
|
||||||
<DataToolbarItem>
|
<DataToolbarItem>
|
||||||
<ToolbarAddButton linkTo="/" />
|
<ToolbarAddButton linkTo={`${match.url}/add`} />
|
||||||
</DataToolbarItem>
|
</DataToolbarItem>
|
||||||
<DataToolbarItem>
|
<DataToolbarItem>
|
||||||
<Button
|
<Button
|
||||||
3
awx/ui_next/src/screens/Template/Survey/index.js
Normal file
3
awx/ui_next/src/screens/Template/Survey/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as SurveyList } from './SurveyList';
|
||||||
|
export { default as SurveyQuestionAdd } from './SurveyQuestionAdd';
|
||||||
|
export { default as SurveyQuestionEdit } from './SurveyQuestionEdit';
|
||||||
@@ -7,7 +7,7 @@ import ContentError from '@components/ContentError';
|
|||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
import ErrorDetail from '@components/ErrorDetail';
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
import useRequest, { useDismissableError } from '@util/useRequest';
|
import useRequest, { useDismissableError } from '@util/useRequest';
|
||||||
import SurveyList from './shared/SurveyList';
|
import { SurveyList, SurveyQuestionAdd, SurveyQuestionEdit } from './Survey';
|
||||||
|
|
||||||
function TemplateSurvey({ template, i18n }) {
|
function TemplateSurvey({ template, i18n }) {
|
||||||
const [surveyEnabled, setSurveyEnabled] = useState(template.survey_enabled);
|
const [surveyEnabled, setSurveyEnabled] = useState(template.survey_enabled);
|
||||||
@@ -37,6 +37,13 @@ function TemplateSurvey({ template, i18n }) {
|
|||||||
[template.id, setSurvey]
|
[template.id, setSurvey]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const updateSurveySpec = spec => {
|
||||||
|
updateSurvey({
|
||||||
|
name: survey.name || '',
|
||||||
|
description: survey.description || '',
|
||||||
|
spec,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const { request: deleteSurvey, error: deleteError } = useRequest(
|
const { request: deleteSurvey, error: deleteError } = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
@@ -64,13 +71,19 @@ function TemplateSurvey({ template, i18n }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/templates/:templateType/:id/survey">
|
<Route path="/templates/:templateType/:id/survey/add">
|
||||||
|
<SurveyQuestionAdd survey={survey} updateSurvey={updateSurveySpec} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/templates/:templateType/:id/survey/edit/:variable">
|
||||||
|
<SurveyQuestionEdit survey={survey} updateSurvey={updateSurveySpec} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/templates/:templateType/:id/survey" exact>
|
||||||
<SurveyList
|
<SurveyList
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
survey={survey}
|
survey={survey}
|
||||||
surveyEnabled={surveyEnabled}
|
surveyEnabled={surveyEnabled}
|
||||||
toggleSurvey={toggleSurvey}
|
toggleSurvey={toggleSurvey}
|
||||||
updateSurvey={spec => updateSurvey({ ...survey, spec })}
|
updateSurvey={updateSurveySpec}
|
||||||
deleteSurvey={deleteSurvey}
|
deleteSurvey={deleteSurvey}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ class Templates extends Component {
|
|||||||
t`Completed Jobs`
|
t`Completed Jobs`
|
||||||
),
|
),
|
||||||
[`/templates/${template.type}/${template.id}/survey`]: i18n._(t`Survey`),
|
[`/templates/${template.type}/${template.id}/survey`]: i18n._(t`Survey`),
|
||||||
|
[`/templates/${template.type}/${template.id}/survey/add`]: i18n._(
|
||||||
|
t`Add Question`
|
||||||
|
),
|
||||||
|
[`/templates/${template.type}/${template.id}/survey/edit`]: i18n._(
|
||||||
|
t`Edit Question`
|
||||||
|
),
|
||||||
[`/templates/${template.type}/${template.id}/schedules`]: i18n._(
|
[`/templates/${template.type}/${template.id}/schedules`]: i18n._(
|
||||||
t`Schedules`
|
t`Schedules`
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -47,3 +47,24 @@ export function requiredEmail(i18n) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function noWhiteSpace(i18n) {
|
||||||
|
return value => {
|
||||||
|
if (/\s/.test(value)) {
|
||||||
|
return i18n._(t`This field must not contain spaces`);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function combine(validators) {
|
||||||
|
return value => {
|
||||||
|
for (let i = 0; i < validators.length; i++) {
|
||||||
|
const error = validators[i](value);
|
||||||
|
if (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { required, maxLength } from './validators';
|
import { required, maxLength, noWhiteSpace, combine } from './validators';
|
||||||
|
|
||||||
const i18n = { _: val => val };
|
const i18n = { _: val => val };
|
||||||
|
|
||||||
@@ -51,4 +51,31 @@ describe('validators', () => {
|
|||||||
values: { max: 8 },
|
values: { max: 8 },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('noWhiteSpace returns error', () => {
|
||||||
|
expect(noWhiteSpace(i18n)('this has spaces')).toEqual({
|
||||||
|
id: 'This field must not contain spaces',
|
||||||
|
});
|
||||||
|
expect(noWhiteSpace(i18n)('this has\twhitespace')).toEqual({
|
||||||
|
id: 'This field must not contain spaces',
|
||||||
|
});
|
||||||
|
expect(noWhiteSpace(i18n)('this\nhas\nnewlines')).toEqual({
|
||||||
|
id: 'This field must not contain spaces',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('noWhiteSpace should accept valid string', () => {
|
||||||
|
expect(noWhiteSpace(i18n)('this_has_no_whitespace')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('combine should run all validators', () => {
|
||||||
|
const validators = [required(null, i18n), noWhiteSpace(i18n)];
|
||||||
|
expect(combine(validators)('')).toEqual({
|
||||||
|
id: 'This field must not be blank',
|
||||||
|
});
|
||||||
|
expect(combine(validators)('one two')).toEqual({
|
||||||
|
id: 'This field must not contain spaces',
|
||||||
|
});
|
||||||
|
expect(combine(validators)('ok')).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user