diff --git a/frontend/web/components/ValueEditor.js b/frontend/web/components/ValueEditor.js index 0ae271633957..6c6072d0a5dc 100644 --- a/frontend/web/components/ValueEditor.js +++ b/frontend/web/components/ValueEditor.js @@ -4,6 +4,7 @@ import Highlight from './Highlight' import ConfigProvider from 'common/providers/ConfigProvider' import { Clipboard } from 'polyfill-react-native' import Icon from './icons/Icon' +import BareButton from './base/forms/BareButton' import { IonIcon } from '@ionic/react' import { checkmarkCircle, warning } from 'ionicons/icons' @@ -141,8 +142,25 @@ class ValueEditor extends Component { /> ) + formatJson = () => { + if (this.props.disabled || !this.props.onChange) return + try { + const formatted = JSON.stringify(JSON.parse(this.props.value), null, 2) + // Don't emit a no-op change; the parent flags dirty on any onChange. + if (formatted !== this.props.value) { + this.props.onChange(formatted) + } + } catch (e) { + toast('Cannot format invalid JSON', 'danger') + } + } + render() { const { ...rest } = this.props + const showFormat = + this.state.language === 'json' && + !this.props.disabled && + !!this.props.value return (
.yaml {this.state.language === 'yaml' && this.renderValidation()} + e.preventDefault()} + onClick={this.formatJson} + className={cx('primary format-action', { + 'is-visible': showFormat, + })} + > + + format + { const res = Clipboard.setString(this.props.value) diff --git a/frontend/web/styles/3rdParty/_hljs.scss b/frontend/web/styles/3rdParty/_hljs.scss index d7027cdcd371..c1ff56016abe 100644 --- a/frontend/web/styles/3rdParty/_hljs.scss +++ b/frontend/web/styles/3rdParty/_hljs.scss @@ -65,7 +65,8 @@ color: var(--color-text-default); } } - span { + span, + .bare-btn { display: flex; align-items: center; gap: 2px; @@ -81,6 +82,24 @@ color: var(--color-text-action); } } + // Slides and fades in when JSON is selected, collapses out otherwise. + .format-action { + max-width: 0; + overflow: hidden; + opacity: 0; + white-space: nowrap; + pointer-events: none; + transition: max-width var(--duration-normal, 200ms) ease, + opacity var(--duration-normal, 200ms) ease; + @media (prefers-reduced-motion: reduce) { + transition: none; + } + &.is-visible { + max-width: 6rem; + opacity: 1; + pointer-events: auto; + } + } } } .hljs {