diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
index 83533f1168..f561a6f74e 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
@@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react';
import { string, node, number } from 'prop-types';
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
import { DetailName, DetailValue } from '@components/DetailList';
+import MultiButtonToggle from '@components/MultiButtonToggle';
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
import CodeMirrorInput from './CodeMirrorInput';
-import YamlJsonToggle from './YamlJsonToggle';
import { JSON_MODE, YAML_MODE } from './constants';
function getValueAsMode(value, mode) {
@@ -50,8 +50,9 @@ function VariablesDetail({ value, label, rows }) {
- {
try {
setCurrentValue(getValueAsMode(currentValue, newMode));
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx
index 05548f8aa8..5ffdaeb1e7 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx
@@ -31,12 +31,12 @@ describe('', () => {
const wrapper = shallow(
);
- wrapper.find('YamlJsonToggle').invoke('onChange')('javascript');
+ wrapper.find('MultiButtonToggle').invoke('onChange')('javascript');
const input = wrapper.find('Styled(CodeMirrorInput)');
expect(input.prop('mode')).toEqual('javascript');
expect(input.prop('value')).toEqual('{\n "foo": "bar"\n}');
- wrapper.find('YamlJsonToggle').invoke('onChange')('yaml');
+ wrapper.find('MultiButtonToggle').invoke('onChange')('yaml');
const input2 = wrapper.find('Styled(CodeMirrorInput)');
expect(input2.prop('mode')).toEqual('yaml');
expect(input2.prop('value')).toEqual('foo: bar\n');
@@ -53,7 +53,7 @@ describe('', () => {
);
act(() => {
- wrapper.find('YamlJsonToggle').invoke('onChange')('javascript');
+ wrapper.find('MultiButtonToggle').invoke('onChange')('javascript');
});
wrapper.setProps({
value: '---bar: baz',
@@ -73,7 +73,7 @@ describe('', () => {
test('should default empty json to "{}"', () => {
const wrapper = mount();
act(() => {
- wrapper.find('YamlJsonToggle').invoke('onChange')('javascript');
+ wrapper.find('MultiButtonToggle').invoke('onChange')('javascript');
});
wrapper.setProps({ value: '' });
const input = wrapper.find('Styled(CodeMirrorInput)');
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
index 336a3fa996..d27b85d328 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
@@ -6,9 +6,9 @@ import { useField } from 'formik';
import styled from 'styled-components';
import { Split, SplitItem } from '@patternfly/react-core';
import { CheckboxField } from '@components/FormField';
+import MultiButtonToggle from '@components/MultiButtonToggle';
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
import CodeMirrorInput from './CodeMirrorInput';
-import YamlJsonToggle from './YamlJsonToggle';
import { JSON_MODE, YAML_MODE } from './constants';
const FieldHeader = styled.div`
@@ -34,8 +34,9 @@ function VariablesField({ i18n, id, name, label, readOnly, promptId }) {
- {
try {
const newVal =
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
index faddd2bc13..cf7758b53e 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
@@ -1,21 +1,16 @@
import React, { useState } from 'react';
import { string, func, bool, number } from 'prop-types';
-import { Button, Split, SplitItem } from '@patternfly/react-core';
+import { Split, SplitItem } from '@patternfly/react-core';
import styled from 'styled-components';
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
+import MultiButtonToggle from '@components/MultiButtonToggle';
import CodeMirrorInput from './CodeMirrorInput';
-import ButtonGroup from './ButtonGroup';
import { JSON_MODE, YAML_MODE } from './constants';
function formatJson(jsonString) {
return JSON.stringify(JSON.parse(jsonString), null, 2);
}
-const SmallButton = styled(Button)`
- padding: 3px 8px;
- font-size: var(--pf-global--FontSize--xs);
-`;
-
const SplitItemRight = styled(SplitItem)`
margin-bottom: 5px;
`;
@@ -47,40 +42,22 @@ function VariablesInput(props) {
-
- {
- if (mode === YAML_MODE) {
- return;
- }
- try {
- onChange(jsonToYaml(value));
- setMode(YAML_MODE);
- } catch (err) {
- onError(err.message);
- }
- }}
- variant={mode === YAML_MODE ? 'primary' : 'secondary'}
- >
- YAML
-
- {
+ {
+ try {
if (mode === JSON_MODE) {
- return;
- }
- try {
+ onChange(jsonToYaml(value));
+ } else {
onChange(yamlToJson(value));
- setMode(JSON_MODE);
- } catch (err) {
- onError(err.message);
}
- }}
- variant={mode === JSON_MODE ? 'primary' : 'secondary'}
- >
- JSON
-
-
+ setMode(newMode);
+ } catch (err) {
+ onError(err.message);
+ }
+ }}
+ />
{
- if (mode !== newMode) {
- onChange(newMode);
- }
- };
-
- return (
-
- setMode(YAML_MODE)}
- variant={mode === YAML_MODE ? 'primary' : 'secondary'}
- >
- YAML
-
- setMode(JSON_MODE)}
- variant={mode === JSON_MODE ? 'primary' : 'secondary'}
- >
- JSON
-
-
- );
-}
-YamlJsonToggle.propTypes = {
- mode: oneOf([YAML_MODE, JSON_MODE]).isRequired,
- onChange: func.isRequired,
-};
-
-export default YamlJsonToggle;
diff --git a/awx/ui_next/src/components/CodeMirrorInput/ButtonGroup.jsx b/awx/ui_next/src/components/MultiButtonToggle/ButtonGroup.jsx
similarity index 100%
rename from awx/ui_next/src/components/CodeMirrorInput/ButtonGroup.jsx
rename to awx/ui_next/src/components/MultiButtonToggle/ButtonGroup.jsx
diff --git a/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.jsx b/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.jsx
new file mode 100644
index 0000000000..9c72cfbab3
--- /dev/null
+++ b/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.jsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { func, string } from 'prop-types';
+import styled from 'styled-components';
+import { Button } from '@patternfly/react-core';
+import ButtonGroup from './ButtonGroup';
+
+const SmallButton = styled(Button)`
+ padding: 3px 8px;
+ font-size: var(--pf-global--FontSize--xs);
+`;
+
+function MultiButtonToggle({ buttons, value, onChange }) {
+ const setValue = newValue => {
+ if (value !== newValue) {
+ onChange(newValue);
+ }
+ };
+
+ return (
+
+ {buttons &&
+ buttons.map(([buttonValue, buttonLabel]) => (
+ setValue(buttonValue)}
+ variant={buttonValue === value ? 'primary' : 'secondary'}
+ >
+ {buttonLabel}
+
+ ))}
+
+ );
+}
+
+const buttonsPropType = {
+ isRequired: ({ buttons }) => {
+ if (!buttons) {
+ return new Error(
+ `The prop buttons is marked as required in MultiButtonToggle, but its value is '${buttons}'`
+ );
+ }
+ // We expect this data structure to look like:
+ // [[value(unrestricted type), label(string)], [value(unrestricted type), label(string)], ...]
+ if (
+ !Array.isArray(buttons) ||
+ buttons.length < 2 ||
+ buttons.reduce(
+ (prevVal, button) => prevVal || typeof button[1] !== 'string',
+ false
+ )
+ ) {
+ return new Error(
+ `Invalid prop buttons supplied to MultiButtonToggle. Validation failed.`
+ );
+ }
+
+ return null;
+ },
+};
+
+MultiButtonToggle.propTypes = {
+ buttons: buttonsPropType.isRequired,
+ value: string.isRequired,
+ onChange: func.isRequired,
+};
+
+export default MultiButtonToggle;
diff --git a/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.test.jsx b/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.test.jsx
new file mode 100644
index 0000000000..15c65e3a29
--- /dev/null
+++ b/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.test.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import MultiButtonToggle from './MultiButtonToggle';
+
+describe('', () => {
+ let wrapper;
+ const onChange = jest.fn();
+ beforeAll(() => {
+ wrapper = mount(
+
+ );
+ });
+ afterAll(() => {
+ wrapper.unmount();
+ });
+ it('should render buttons successfully', () => {
+ const buttons = wrapper.find('Button');
+ expect(buttons.length).toBe(2);
+ expect(buttons.at(0).props().variant).toBe('primary');
+ expect(buttons.at(1).props().variant).toBe('secondary');
+ });
+ it('should call onChange function when button clicked', () => {
+ const buttons = wrapper.find('Button');
+ buttons.at(1).simulate('click');
+ expect(onChange).toHaveBeenCalledWith('json');
+ });
+});
diff --git a/awx/ui_next/src/components/MultiButtonToggle/index.js b/awx/ui_next/src/components/MultiButtonToggle/index.js
new file mode 100644
index 0000000000..9332082446
--- /dev/null
+++ b/awx/ui_next/src/components/MultiButtonToggle/index.js
@@ -0,0 +1 @@
+export { default } from './MultiButtonToggle';