From 908c3fbec6bbf0a0d0160d9c159d739b2fa3c35b Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 26 Aug 2026 17:32:14 -0300 Subject: [PATCH 1/5] feat(value-editor): add a Format action for JSON values The .json toggle only validates and highlights; the stored value is rendered verbatim, so pasted minified JSON stays unreadable. Add a Format action next to copy, shown only when the language is JSON and there is a value, that pretty-prints via JSON.stringify(parse, 2). It calls the same onChange the editor already uses for typing, so every caller (feature value, variations, segment overrides) gets it for free. Invalid JSON is left untouched with a toast. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/web/components/ValueEditor.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/frontend/web/components/ValueEditor.js b/frontend/web/components/ValueEditor.js index 0ae271633957..b9eb921b6325 100644 --- a/frontend/web/components/ValueEditor.js +++ b/frontend/web/components/ValueEditor.js @@ -141,6 +141,15 @@ class ValueEditor extends Component { /> ) + formatJson = () => { + if (this.props.disabled || !this.props.onChange) return + try { + this.props.onChange(JSON.stringify(JSON.parse(this.props.value), null, 2)) + } catch (e) { + toast('Cannot format invalid JSON', 'danger') + } + } + render() { const { ...rest } = this.props return ( @@ -207,6 +216,21 @@ class ValueEditor extends Component { > .yaml {this.state.language === 'yaml' && this.renderValidation()} + {this.state.language === 'json' && + !this.props.disabled && + !!this.props.value && ( + { + e.preventDefault() + e.stopPropagation() + this.formatJson() + }} + className={cx('txt primary')} + > + + format + + )} { const res = Clipboard.setString(this.props.value) From 63fa9bcc063549917a613f17f158f3f9af546ff5 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 26 Aug 2026 17:39:59 -0300 Subject: [PATCH 2/5] refactor(value-editor): let the format icon inherit currentColor The .primary span already sets color: var(--color-text-action); Icon falls back to currentColor, so the hardcoded #6837fc was redundant and not theme-aware. Dropping it keeps the same colour and adapts in dark mode. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/web/components/ValueEditor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/web/components/ValueEditor.js b/frontend/web/components/ValueEditor.js index b9eb921b6325..78413352270e 100644 --- a/frontend/web/components/ValueEditor.js +++ b/frontend/web/components/ValueEditor.js @@ -227,7 +227,7 @@ class ValueEditor extends Component { }} className={cx('txt primary')} > - + format )} From 714087f4713926eaa95b6254a381804511c3c10c Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 26 Aug 2026 17:44:54 -0300 Subject: [PATCH 3/5] feat(value-editor): make Format a BareButton and animate its reveal Replace the clickable span with BareButton so the action is a real button: keyboard focusable, focus-visible outline, disabled semantics. Extend the .select-language selector to style .bare-btn alongside span. Animate the show/hide with a max-width + opacity transition (collapsing out of the row when not JSON) instead of popping in, and honour prefers-reduced-motion. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/web/components/ValueEditor.js | 35 ++++++++++++++----------- frontend/web/styles/3rdParty/_hljs.scss | 21 ++++++++++++++- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/frontend/web/components/ValueEditor.js b/frontend/web/components/ValueEditor.js index 78413352270e..ff278d39e582 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' @@ -152,6 +153,10 @@ class ValueEditor extends Component { 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()} - {this.state.language === 'json' && - !this.props.disabled && - !!this.props.value && ( - { - e.preventDefault() - e.stopPropagation() - this.formatJson() - }} - className={cx('txt primary')} - > - - format - - )} + { + // Keep the editor focused; the click still fires the format. + e.preventDefault() + e.stopPropagation() + }} + 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 { From cc5344e9efa4b77a44e3c5c40ad248a5c4edc91d Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 26 Aug 2026 17:48:32 -0300 Subject: [PATCH 4/5] refactor(value-editor): drop stopPropagation from the format button Only preventDefault is needed, to keep the editor from blurring (which would fire its onBlur commit). stopPropagation was copied from the tab spans with no handler here to stop, and can interfere with click-away. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/web/components/ValueEditor.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/frontend/web/components/ValueEditor.js b/frontend/web/components/ValueEditor.js index ff278d39e582..8c6ed23aec47 100644 --- a/frontend/web/components/ValueEditor.js +++ b/frontend/web/components/ValueEditor.js @@ -223,11 +223,9 @@ class ValueEditor extends Component { { - // Keep the editor focused; the click still fires the format. - e.preventDefault() - e.stopPropagation() - }} + // Don't blur the editor (which would fire its onBlur commit); + // onClick still runs the format. + onMouseDown={(e) => e.preventDefault()} onClick={this.formatJson} className={cx('primary format-action', { 'is-visible': showFormat, From 73f06069776b9ed59c974ba46a06b44eae151a1b Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 26 Aug 2026 17:51:57 -0300 Subject: [PATCH 5/5] fix(value-editor): don't dirty the form when JSON is already formatted formatJson emitted onChange unconditionally, and the feature modal flags the value dirty on any onChange without comparing. So formatting an already-formatted value showed a spurious pending change. Skip onChange when the reformatted string is identical. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/web/components/ValueEditor.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/web/components/ValueEditor.js b/frontend/web/components/ValueEditor.js index 8c6ed23aec47..6c6072d0a5dc 100644 --- a/frontend/web/components/ValueEditor.js +++ b/frontend/web/components/ValueEditor.js @@ -145,7 +145,11 @@ class ValueEditor extends Component { formatJson = () => { if (this.props.disabled || !this.props.onChange) return try { - this.props.onChange(JSON.stringify(JSON.parse(this.props.value), null, 2)) + 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') }