diff --git a/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.ORIGINAL.jsx b/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.ORIGINAL.jsx
deleted file mode 100644
index 8666c838ea..0000000000
--- a/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.ORIGINAL.jsx
+++ /dev/null
@@ -1,223 +0,0 @@
-import React, { useState, useCallback, useEffect } from 'react';
-import { Wizard } from '@patternfly/react-core';
-import { withI18n } from '@lingui/react';
-import { t } from '@lingui/macro';
-import { Formik } from 'formik';
-import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
-import useRequest from '@util/useRequest';
-import ContentError from '@components/ContentError';
-import ContentLoading from '@components/ContentLoading';
-import { required } from '@util/validators';
-import InventoryStep from './InventoryStep';
-import CredentialsStep from './CredentialsStep';
-import OtherPromptsStep from './OtherPromptsStep';
-import SurveyStep from './SurveyStep';
-import PreviewStep from './PreviewStep';
-import PromptFooter from './PromptFooter';
-import mergeExtraVars from './mergeExtraVars';
-
-const STEPS = {
- INVENTORY: 'inventory',
- CREDENTIALS: 'credentials',
- PASSWORDS: 'passwords',
- OTHER_PROMPTS: 'other',
- SURVEY: 'survey',
- PREVIEW: 'preview',
-};
-
-function showOtherPrompts(config) {
- return (
- config.ask_job_type_on_launch ||
- config.ask_limit_on_launch ||
- config.ask_verbosity_on_launch ||
- config.ask_tags_on_launch ||
- config.ask_skip_tags_on_launch ||
- config.ask_variables_on_launch ||
- config.ask_scm_branch_on_launch ||
- config.ask_diff_mode_on_launch
- );
-}
-
-function getInitialVisitedSteps(config) {
- const visited = {};
- if (config.ask_inventory_on_launch) {
- visited[STEPS.INVENTORY] = false;
- }
- if (config.ask_credential_on_launch) {
- visited[STEPS.CREDENTIALS] = false;
- }
- if (showOtherPrompts(config)) {
- visited[STEPS.OTHER_PROMPTS] = false;
- }
- if (config.survey_enabled) {
- visited[STEPS.SURVEY] = false;
- }
- return visited;
-}
-
-function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
- const [formErrors, setFormErrors] = useState({});
- const [visitedSteps, setVisitedSteps] = useState(
- getInitialVisitedSteps(config)
- );
-
- const {
- result: survey,
- request: fetchSurvey,
- error: surveyError,
- } = useRequest(
- useCallback(async () => {
- if (!config.survey_enabled) {
- return {};
- }
- const { data } =
- resource.type === 'workflow_job_template'
- ? await WorkflowJobTemplatesAPI.readSurvey(resource.id)
- : await JobTemplatesAPI.readSurvey(resource.id);
- return data;
- }, [config.survey_enabled, resource])
- );
- useEffect(() => {
- fetchSurvey();
- }, [fetchSurvey]);
-
- if (surveyError) {
- return ;
- }
- if (config.survey_enabled && !survey) {
- return ;
- }
-
- const steps = [];
- const initialValues = {};
- if (config.ask_inventory_on_launch) {
- initialValues.inventory = resource?.summary_fields?.inventory || null;
- steps.push({
- id: STEPS.INVENTORY,
- name: i18n._(t`Inventory`),
- component: ,
- });
- }
- if (config.ask_credential_on_launch) {
- initialValues.credentials = resource?.summary_fields?.credentials || [];
- steps.push({
- id: STEPS.CREDENTIALS,
- name: i18n._(t`Credentials`),
- component: ,
- });
- }
-
- // TODO: Add Credential Passwords step
-
- if (config.ask_job_type_on_launch) {
- initialValues.job_type = resource.job_type || '';
- }
- if (config.ask_limit_on_launch) {
- initialValues.limit = resource.limit || '';
- }
- if (config.ask_verbosity_on_launch) {
- initialValues.verbosity = resource.verbosity || 0;
- }
- if (config.ask_tags_on_launch) {
- initialValues.job_tags = resource.job_tags || '';
- }
- if (config.ask_skip_tags_on_launch) {
- initialValues.skip_tags = resource.skip_tags || '';
- }
- if (config.ask_variables_on_launch) {
- initialValues.extra_vars = resource.extra_vars || '---';
- }
- if (config.ask_scm_branch_on_launch) {
- initialValues.scm_branch = resource.scm_branch || '';
- }
- if (config.ask_diff_mode_on_launch) {
- initialValues.diff_mode = resource.diff_mode || false;
- }
- if (showOtherPrompts(config)) {
- steps.push({
- id: STEPS.OTHER_PROMPTS,
- name: i18n._(t`Other Prompts`),
- component: ,
- });
- }
- if (config.survey_enabled) {
- initialValues.survey = {};
- // survey.spec.forEach(question => {
- // initialValues[`survey_${question.variable}`] = question.default;
- // })
- steps.push({
- id: STEPS.SURVEY,
- name: i18n._(t`Survey`),
- component: ,
- });
- }
- steps.push({
- id: STEPS.PREVIEW,
- name: i18n._(t`Preview`),
- component: (
-
- ),
- enableNext: Object.keys(formErrors).length === 0,
- nextButtonText: i18n._(t`Launch`),
- });
-
- const validate = values => {
- // return {};
- return { limit: ['required field'] };
- };
-
- const submit = values => {
- const postValues = {};
- const setValue = (key, value) => {
- if (typeof value !== 'undefined' && value !== null) {
- postValues[key] = value;
- }
- };
- setValue('inventory_id', values.inventory?.id);
- setValue('credentials', values.credentials?.map(c => c.id));
- setValue('job_type', values.job_type);
- setValue('limit', values.limit);
- setValue('job_tags', values.job_tags);
- setValue('skip_tags', values.skip_tags);
- setValue('extra_vars', mergeExtraVars(values.extra_vars, values.survey));
- onLaunch(postValues);
- };
- console.log('formErrors:', formErrors);
-
- return (
-
- {({ errors, values, touched, validateForm, handleSubmit }) => (
- {
- // console.log(`${prevStep.prevName} -> ${nextStep.name}`);
- // console.log('errors', errors);
- // console.log('values', values);
- const newErrors = await validateForm();
- setFormErrors(newErrors);
- // console.log('new errors:', newErrors);
- }}
- onGoToStep={async (newStep, prevStep) => {
- // console.log('errors', errors);
- // console.log('values', values);
- const newErrors = await validateForm();
- setFormErrors(newErrors);
- }}
- title={i18n._(t`Prompts`)}
- steps={steps}
- // footer={}
- />
- )}
-
- );
-}
-
-export { LaunchPrompt as _LaunchPrompt };
-export default withI18n()(LaunchPrompt);
diff --git a/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.jsx b/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.jsx
index 1134d20ff0..6727262cb8 100644
--- a/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.jsx
@@ -1,33 +1,24 @@
-import React, { useState, useCallback, useEffect } from 'react';
+import React from 'react';
import { Wizard } from '@patternfly/react-core';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik } from 'formik';
-// import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
-// import useRequest from '@util/useRequest';
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
-// import { required } from '@util/validators';
-// import InventoryStep from './InventoryStep';
-// import CredentialsStep from './CredentialsStep';
-// import OtherPromptsStep from './OtherPromptsStep';
-// import SurveyStep from './SurveyStep';
-// import PreviewStep from './PreviewStep';
-// import PromptFooter from './PromptFooter';
import mergeExtraVars from './mergeExtraVars';
-import { useSteps, useVisitedSteps } from './hooks';
+import useSteps from './useSteps';
function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
- // const [formErrors, setFormErrors] = useState({});
const {
steps,
initialValues,
isReady,
validate,
- formErrors,
+ visitStep,
+ visitAllSteps,
+ // formErrors,
contentError,
} = useSteps(config, resource, i18n);
- const [visitedSteps, visitStep] = useVisitedSteps(config);
if (contentError) {
return ;
@@ -36,13 +27,6 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
return ;
}
- // TODO move into hook?
- // const validate = values => {
- // // return {};
- // return { limit: ['required field'] };
- // };
-
- // TODO move into hook?
const submit = values => {
const postValues = {};
const setValue = (key, value) => {
@@ -62,22 +46,26 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
return (
- {({ errors, values, touched, validateForm, handleSubmit }) => (
+ {({ validateForm, handleSubmit }) => (
{
- console.log(prevStep);
- visitStep(prevStep.id);
- const newErrors = await validateForm();
- // updatePromptErrors(prevStep.prevName, newErrors);
+ if (nextStep.id === 'preview') {
+ visitAllSteps();
+ } else {
+ visitStep(prevStep.prevId);
+ }
+ await validateForm();
}}
onGoToStep={async (newStep, prevStep) => {
- console.log(prevStep);
- visitStep(prevStep.id);
- const newErrors = await validateForm();
- // updatePromptErrors(prevStep.prevName, newErrors);
+ if (newStep.id === 'preview') {
+ visitAllSteps();
+ } else {
+ visitStep(prevStep.prevId);
+ }
+ await validateForm();
}}
title={i18n._(t`Prompts`)}
steps={steps}
diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/OtherPromptsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/OtherPromptsStep.jsx
index 7b552b8b23..1196e5998e 100644
--- a/awx/ui_next/src/components/LaunchPrompt/steps/OtherPromptsStep.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/steps/OtherPromptsStep.jsx
@@ -8,7 +8,6 @@ import { TagMultiSelect } from '@components/MultiSelect';
import AnsibleSelect from '@components/AnsibleSelect';
import { VariablesField } from '@components/CodeMirrorInput';
import styled from 'styled-components';
-import { required } from '@util/validators';
const FieldHeader = styled.div`
display: flex;
@@ -33,9 +32,7 @@ function OtherPromptsStep({ config, i18n }) {
of hosts that will be managed or affected by the playbook. Multiple
patterns are allowed. Refer to Ansible documentation for more
information and examples on patterns.`)}
- // TODO: remove this validator (for testing only)
isRequired
- validate={required(null, i18n)}
/>
)}
{config.ask_verbosity_on_launch && }
diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/StepName.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/StepName.jsx
new file mode 100644
index 0000000000..28bf5f0414
--- /dev/null
+++ b/awx/ui_next/src/components/LaunchPrompt/steps/StepName.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import styled from 'styled-components';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { Tooltip } from '@patternfly/react-core';
+import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
+
+const AlertText = styled.div`
+ color: var(--pf-global--danger-color--200);
+ font-weight: var(--pf-global--FontWeight--bold);
+`;
+
+const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
+ margin-left: 10px;
+`;
+
+function StepName({ hasErrors, children, i18n }) {
+ if (!hasErrors) {
+ return children;
+ }
+ return (
+ <>
+
+ {children}
+
+
+
+
+ >
+ );
+}
+
+export default withI18n()(StepName);
diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/useCredentialsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/useCredentialsStep.jsx
index a8d5d0053b..e17a9861a1 100644
--- a/awx/ui_next/src/components/LaunchPrompt/steps/useCredentialsStep.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/steps/useCredentialsStep.jsx
@@ -4,13 +4,14 @@ import CredentialsStep from './CredentialsStep';
const STEP_ID = 'credentials';
-export default function useCredentialsStep(config, resource, i18n) {
- const validate = values => {
- const errors = {};
- if (!values.credentials || !values.credentials.length) {
- errors.credentials = i18n._(t`Credentials must be selected`);
- }
- return errors;
+export default function useCredentialsStep(
+ config,
+ resource,
+ visitedSteps,
+ i18n
+) {
+ const validate = () => {
+ return {};
};
return {
diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx
index 91988f83d0..3098f2b0f6 100644
--- a/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx
@@ -1,20 +1,26 @@
-import React from 'react';
+import React, { useState } from 'react';
import { t } from '@lingui/macro';
import InventoryStep from './InventoryStep';
+import StepName from './StepName';
const STEP_ID = 'inventory';
-export default function useInventoryStep(config, resource, i18n) {
+export default function useInventoryStep(config, resource, visitedSteps, i18n) {
+ const [stepErrors, setStepErrors] = useState({});
+
const validate = values => {
const errors = {};
if (!values.inventory) {
errors.inventory = i18n._(t`An inventory must be selected`);
}
+ setStepErrors(errors);
return errors;
};
+ const hasErrors = visitedSteps[STEP_ID] && Object.keys(stepErrors).length > 0;
+
return {
- step: getStep(config, i18n),
+ step: getStep(config, hasErrors, i18n),
initialValues: getInitialValues(config, resource),
validate,
isReady: true,
@@ -22,13 +28,13 @@ export default function useInventoryStep(config, resource, i18n) {
};
}
-function getStep(config, i18n) {
+function getStep(config, hasErrors, i18n) {
if (!config.ask_inventory_on_launch) {
return null;
}
return {
id: STEP_ID,
- name: i18n._(t`Inventory`),
+ name: {i18n._(t`Inventory`)},
component: ,
};
}
diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/useOtherPromptsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/useOtherPromptsStep.jsx
index 1977070e28..b92f1e2eb9 100644
--- a/awx/ui_next/src/components/LaunchPrompt/steps/useOtherPromptsStep.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/steps/useOtherPromptsStep.jsx
@@ -1,20 +1,26 @@
-import React from 'react';
+import React, { useState } from 'react';
import { t } from '@lingui/macro';
import OtherPromptsStep from './OtherPromptsStep';
+import StepName from './StepName';
const STEP_ID = 'other';
-export default function useOtherPrompt(config, resource, i18n) {
+export default function useOtherPrompt(config, resource, visitedSteps, i18n) {
+ const [stepErrors, setStepErrors] = useState({});
+
const validate = values => {
const errors = {};
if (config.ask_job_type_on_launch && !values.job_type) {
errors.job_type = i18n._(t`This field must not be blank`);
}
+ setStepErrors(errors);
return errors;
};
+ const hasErrors = visitedSteps[STEP_ID] && Object.keys(stepErrors).length > 0;
+
return {
- step: getStep(config, i18n),
+ step: getStep(config, hasErrors, i18n),
initialValues: getInitialValues(config, resource),
validate,
isReady: true,
@@ -22,13 +28,13 @@ export default function useOtherPrompt(config, resource, i18n) {
};
}
-function getStep(config, i18n) {
+function getStep(config, hasErrors, i18n) {
if (!shouldShowPrompt(config)) {
return null;
}
return {
id: STEP_ID,
- name: i18n._(t`Other Prompts`),
+ name: {i18n._(t`Other Prompts`)},
component: ,
};
}
diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/useSurveyStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/useSurveyStep.jsx
index 6e52748ee9..9ef75b6029 100644
--- a/awx/ui_next/src/components/LaunchPrompt/steps/useSurveyStep.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/steps/useSurveyStep.jsx
@@ -1,12 +1,15 @@
-import React, { useEffect, useCallback } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import { t } from '@lingui/macro';
import useRequest from '@util/useRequest';
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
import SurveyStep from './SurveyStep';
+import StepName from './StepName';
const STEP_ID = 'survey';
-export default function useSurveyStep(config, resource, i18n) {
+export default function useSurveyStep(config, resource, visitedSteps, i18n) {
+ const [stepErrors, setStepErrors] = useState({});
+
const { result: survey, request: fetchSurvey, isLoading, error } = useRequest(
useCallback(async () => {
if (!config.survey_enabled) {
@@ -24,23 +27,68 @@ export default function useSurveyStep(config, resource, i18n) {
fetchSurvey();
}, [fetchSurvey]);
+ const validate = values => {
+ if (!config.survey_enabled || !survey || !survey.spec) {
+ return {};
+ }
+ const errors = {};
+ survey.spec.forEach(question => {
+ const errMessage = validateField(
+ question,
+ values[question.variable],
+ i18n
+ );
+ if (errMessage) {
+ errors[`survey_${question.variable}`] = errMessage;
+ }
+ });
+ setStepErrors(errors);
+ return errors;
+ };
+
+ const hasErrors = visitedSteps[STEP_ID] && Object.keys(stepErrors).length > 0;
+
return {
- step: getStep(config, survey, i18n),
+ step: getStep(config, survey, hasErrors, i18n),
initialValues: getInitialValues(config, survey),
- validate: getValidate(config, survey, i18n),
+ validate,
survey,
isReady: !isLoading && !!survey,
error,
};
}
-function getStep(config, survey, i18n) {
+function validateField(question, value, i18n) {
+ const isTextField = ['text', 'textarea'].includes(question.type);
+ const isNumeric = ['integer', 'float'].includes(question.type);
+ if (isTextField && (value || value === 0)) {
+ if (question.min && value.length < question.min) {
+ return i18n._(t`This field must be at least ${question.min} characters`);
+ }
+ if (question.max && value.length > question.max) {
+ return i18n._(t`This field must not exceed ${question.max} characters`);
+ }
+ }
+ if (isNumeric && (value || value === 0)) {
+ if (value < question.min || value > question.max) {
+ return i18n._(
+ t`This field must be a number and have a value between ${question.min} and ${question.max}`
+ );
+ }
+ }
+ if (question.required && !value && value !== 0) {
+ return i18n._(t`This field must not be blank`);
+ }
+ return null;
+}
+
+function getStep(config, survey, hasErrors, i18n) {
if (!config.survey_enabled) {
return null;
}
return {
id: STEP_ID,
- name: i18n._(t`Survey`),
+ name: {i18n._(t`Survey`)},
component: ,
};
}
@@ -59,22 +107,3 @@ function getInitialValues(config, survey) {
});
return values;
}
-
-function getValidate(config, survey, i18n) {
- return values => {
- if (!config.survey_enabled || !survey || !survey.spec) {
- return {};
- }
- const errors = {};
- survey.spec.forEach(question => {
- // TODO validate min/max
- // TODO allow 0
- if (question.required && !values[question.variable]) {
- errors[`survey_${question.variable}`] = i18n._(
- t`This field must not be blank`
- );
- }
- });
- return errors;
- };
-}
diff --git a/awx/ui_next/src/components/LaunchPrompt/hooks.js b/awx/ui_next/src/components/LaunchPrompt/useSteps.js
similarity index 57%
rename from awx/ui_next/src/components/LaunchPrompt/hooks.js
rename to awx/ui_next/src/components/LaunchPrompt/useSteps.js
index 872133f782..871ce8c5a1 100644
--- a/awx/ui_next/src/components/LaunchPrompt/hooks.js
+++ b/awx/ui_next/src/components/LaunchPrompt/useSteps.js
@@ -5,17 +5,17 @@ import useOtherPromptsStep from './steps/useOtherPromptsStep';
import useSurveyStep from './steps/useSurveyStep';
import usePreviewStep from './steps/usePreviewStep';
-export function useSteps(config, resource, i18n) {
- const [formErrors, setFormErrors] = useState({});
- const inventory = useInventoryStep(config, resource, i18n);
- const credentials = useCredentialsStep(config, resource, i18n);
- const otherPrompts = useOtherPromptsStep(config, resource, i18n);
- const survey = useSurveyStep(config, resource, i18n);
+export default function useSteps(config, resource, i18n) {
+ const [visited, setVisited] = useState({});
+ const inventory = useInventoryStep(config, resource, visited, i18n);
+ const credentials = useCredentialsStep(config, resource, visited, i18n);
+ const otherPrompts = useOtherPromptsStep(config, resource, visited, i18n);
+ const survey = useSurveyStep(config, resource, visited, i18n);
const preview = usePreviewStep(
config,
resource,
survey.survey,
- formErrors,
+ {}, // TODO: formErrors ?
i18n
);
@@ -46,6 +46,9 @@ export function useSteps(config, resource, i18n) {
survey.error ||
preview.error;
+ // TODO: store error state in each step's hook.
+ // but continue to return values here (async?) so form errors can be returned
+ // out and set into Formik
const validate = values => {
const errors = {
...inventory.validate(values),
@@ -53,24 +56,29 @@ export function useSteps(config, resource, i18n) {
...otherPrompts.validate(values),
...survey.validate(values),
};
- setFormErrors(errors);
+ // setFormErrors(errors);
if (Object.keys(errors).length) {
return errors;
}
return false;
};
- return { steps, initialValues, isReady, validate, formErrors, contentError };
-}
-
-export function usePromptErrors(config) {
- const [promptErrors, setPromptErrors] = useState({});
- const updatePromptErrors = () => {};
- return [promptErrors, updatePromptErrors];
-}
-
-// TODO this interrelates with usePromptErrors
-// merge? or pass result from one into the other?
-export function useVisitedSteps(config) {
- return [[], () => {}];
+ // TODO move visited flags into each step hook
+ return {
+ steps,
+ initialValues,
+ isReady,
+ validate,
+ visitStep: stepId => setVisited({ ...visited, [stepId]: true }),
+ visitAllSteps: () => {
+ setVisited({
+ inventory: true,
+ credentials: true,
+ other: true,
+ survey: true,
+ preview: true,
+ });
+ },
+ contentError,
+ };
}