diff --git a/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx index 797c10673d..5063ab033f 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx @@ -1,5 +1,5 @@ import 'styled-components/macro'; -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { node, number, oneOfType, shape, string, arrayOf } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -23,39 +23,42 @@ import { import CodeEditor from './CodeEditor'; import { JSON_MODE, YAML_MODE } from './constants'; -function getValueAsMode(value, mode) { - if (!value) { - if (mode === JSON_MODE) { - return '{}'; - } - return '---'; - } - const modeMatches = isJsonString(value) === (mode === JSON_MODE); - if (modeMatches) { - return value; - } - return mode === YAML_MODE ? jsonToYaml(value) : yamlToJson(value); -} - -function VariablesDetail({ dataCy, helpText, value, label, rows, i18n }) { +function VariablesDetail({ + dataCy, + helpText, + value, + label, + rows, + fullHeight, + i18n, +}) { const [mode, setMode] = useState( isJsonObject(value) || isJsonString(value) ? JSON_MODE : YAML_MODE ); - const [currentValue, setCurrentValue] = useState( - isJsonObject(value) ? JSON.stringify(value, null, 2) : value || '---' - ); const [isExpanded, setIsExpanded] = useState(false); - const [error, setError] = useState(null); - useEffect(() => { - setCurrentValue( - getValueAsMode( - isJsonObject(value) ? JSON.stringify(value, null, 2) : value, - mode - ) - ); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [value]); + let currentValue = value; + let error; + + const getValueInCurrentMode = () => { + if (!value) { + if (mode === JSON_MODE) { + return '{}'; + } + return '---'; + } + const modeMatches = isJsonString(value) === (mode === JSON_MODE); + if (modeMatches) { + return value; + } + return mode === YAML_MODE ? jsonToYaml(value) : yamlToJson(value); + }; + + try { + currentValue = getValueInCurrentMode(); + } catch (err) { + error = err; + } const labelCy = dataCy ? `${dataCy}-label` : null; const valueCy = dataCy ? `${dataCy}-value` : null; @@ -76,8 +79,6 @@ function VariablesDetail({ dataCy, helpText, value, label, rows, i18n }) { mode={mode} setMode={setMode} currentValue={currentValue} - setCurrentValue={setCurrentValue} - setError={setError} onExpand={() => setIsExpanded(true)} i18n={i18n} /> @@ -93,6 +94,7 @@ function VariablesDetail({ dataCy, helpText, value, label, rows, i18n }) { value={currentValue} readOnly rows={rows} + fullHeight={fullHeight} css="margin-top: 10px" /> {error && ( @@ -129,8 +131,6 @@ function VariablesDetail({ dataCy, helpText, value, label, rows, i18n }) { mode={mode} setMode={setMode} currentValue={currentValue} - setCurrentValue={setCurrentValue} - setError={setError} i18n={i18n} /> { - try { - setCurrentValue(getValueAsMode(currentValue, newMode)); - setMode(newMode); - } catch (err) { - setError(err); - } + setMode(newMode); }} /> diff --git a/awx/ui_next/src/components/CodeEditor/VariablesDetail.test.jsx b/awx/ui_next/src/components/CodeEditor/VariablesDetail.test.jsx index 2705f2b2cf..e616c1beaf 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesDetail.test.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesDetail.test.jsx @@ -39,7 +39,7 @@ describe('', () => { wrapper.find('MultiButtonToggle').invoke('onChange')('yaml'); const input2 = wrapper.find('VariablesDetail___StyledCodeEditor'); expect(input2.prop('mode')).toEqual('yaml'); - expect(input2.prop('value')).toEqual('foo: bar\n'); + expect(input2.prop('value')).toEqual('---foo: bar'); }); test('should render label and value= --- when there are no values', () => { diff --git a/awx/ui_next/src/components/CodeEditor/VariablesField.jsx b/awx/ui_next/src/components/CodeEditor/VariablesField.jsx index ed56f5376c..58e9960b82 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesField.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesField.jsx @@ -73,10 +73,43 @@ function VariablesField({ }, [shouldValidate, validate] // eslint-disable-line react-hooks/exhaustive-deps ); + const [lastYamlValue, setLastYamlValue] = useState( + mode === YAML_MODE ? field.value : null + ); + const [isJsonEdited, setIsJsonEdited] = useState(false); const [isExpanded, setIsExpanded] = useState(false); + const handleModeChange = newMode => { + if (newMode === YAML_MODE && !isJsonEdited && lastYamlValue !== null) { + helpers.setValue(lastYamlValue, false); + setMode(newMode); + return; + } + + try { + const newVal = + newMode === YAML_MODE + ? jsonToYaml(field.value) + : yamlToJson(field.value); + helpers.setValue(newVal, false); + setMode(newMode); + } catch (err) { + helpers.setError(err.message); + } + }; + + const handleChange = newVal => { + helpers.setValue(newVal); + if (mode === JSON_MODE) { + setIsJsonEdited(true); + } else { + setLastYamlValue(newVal); + setIsJsonEdited(false); + } + }; + return ( - <> +
setIsExpanded(true)} mode={mode} - setMode={setMode} + setMode={handleModeChange} setShouldValidate={setShouldValidate} + handleChange={handleChange} />
@@ -128,7 +163,7 @@ function VariablesField({ {meta.error} ) : null} - + ); } VariablesField.propTypes = { @@ -156,8 +191,9 @@ function VariablesFieldInternals({ setMode, onExpand, setShouldValidate, + handleChange, }) { - const [field, meta, helpers] = useField(name); + const [field, meta] = useField(name); return (
@@ -176,18 +212,7 @@ function VariablesFieldInternals({ [JSON_MODE, 'JSON'], ]} value={mode} - onChange={newMode => { - try { - const newVal = - newMode === YAML_MODE - ? jsonToYaml(field.value) - : yamlToJson(field.value); - helpers.setValue(newVal); - setMode(newMode); - } catch (err) { - helpers.setError(err.message); - } - }} + onChange={setMode} /> @@ -213,9 +238,7 @@ function VariablesFieldInternals({ mode={mode} readOnly={readOnly} {...field} - onChange={newVal => { - helpers.setValue(newVal); - }} + onChange={handleChange} fullHeight={fullHeight} onFocus={() => setShouldValidate(false)} onBlur={() => setShouldValidate(true)} diff --git a/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx b/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx index 24c896069e..92ead2400c 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx @@ -51,11 +51,90 @@ describe('VariablesField', () => { }); wrapper.update(); expect(wrapper.find('CodeEditor').prop('mode')).toEqual('yaml'); + expect(wrapper.find('CodeEditor').prop('value')).toEqual( + '---\nfoo: bar\nbaz: 3' + ); + }); + + it('should retain non-expanded yaml if JSON value not edited', async () => { + const value = '---\na: &aa [a,b,c]\nb: *aa'; + const wrapper = mountWithContexts( + + {() => ( + + )} + + ); + const jsButton = wrapper.find('Button.toggle-button-javascript'); + await act(async () => { + jsButton.simulate('click'); + }); + wrapper.update(); + const yamlButton = wrapper.find('Button.toggle-button-yaml'); + await act(async () => { + yamlButton.simulate('click'); + }); + wrapper.update(); + expect(wrapper.find('CodeEditor').prop('mode')).toEqual('yaml'); + expect(wrapper.find('CodeEditor').prop('value')).toEqual(value); + }); + + it('should retain expanded yaml if JSON value is edited', async () => { + const value = '---\na: &aa [a,b,c]\nb: *aa'; + const wrapper = mountWithContexts( + + {() => ( + + )} + + ); + const jsButton = wrapper.find('Button.toggle-button-javascript'); + await act(async () => { + jsButton.simulate('click'); + }); + wrapper.update(); + wrapper.find('CodeEditor').invoke('onChange')( + '{\n "foo": "bar",\n "baz": 3\n}' + ); + const yamlButton = wrapper.find('Button.toggle-button-yaml'); + await act(async () => { + yamlButton.simulate('click'); + }); + wrapper.update(); + expect(wrapper.find('CodeEditor').prop('mode')).toEqual('yaml'); expect(wrapper.find('CodeEditor').prop('value')).toEqual( 'foo: bar\nbaz: 3\n' ); }); + it('should retain non-expanded yaml if YAML value is edited', async () => { + const value = '---\na: &aa [a,b,c]\nb: *aa'; + const wrapper = mountWithContexts( + + {() => ( + + )} + + ); + wrapper.find('CodeEditor').invoke('onChange')( + '---\na: &aa [a,b,c]\nb: *aa\n' + ); + const buttons = wrapper.find('Button'); + await act(async () => { + buttons.at(1).simulate('click'); + }); + wrapper.update(); + const buttons2 = wrapper.find('Button'); + await act(async () => { + buttons2.at(0).simulate('click'); + }); + wrapper.update(); + expect(wrapper.find('CodeEditor').prop('mode')).toEqual('yaml'); + expect(wrapper.find('CodeEditor').prop('value')).toEqual( + '---\na: &aa [a,b,c]\nb: *aa\n' + ); + }); + it('should set Formik error if yaml is invalid', async () => { const value = '---\nfoo bar\n'; const wrapper = mountWithContexts( diff --git a/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.jsx b/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.jsx index efc1cb3d92..8ce9038682 100644 --- a/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.jsx +++ b/awx/ui_next/src/components/MultiButtonToggle/MultiButtonToggle.jsx @@ -25,6 +25,7 @@ function MultiButtonToggle({ buttons, value, onChange }) { setValue(buttonValue)} variant={buttonValue === value ? 'primary' : 'secondary'} >