Skip to content

refactor(value-editor): migrate to TypeScript and split by concern - #8447

Open
talissoncosta wants to merge 21 commits into
mainfrom
refactor/value-editor-typescript
Open

refactor(value-editor): migrate to TypeScript and split by concern#8447
talissoncosta wants to merge 21 commits into
mainfrom
refactor/value-editor-typescript

Conversation

@talissoncosta

@talissoncosta talissoncosta commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Thanks for submitting a PR! Please check the boxes below:

  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Closes #8478.

ValueEditor was a 251 line untyped class with no props contract, which is how placeholder and readOnly came to be passed by callers and silently ignored. This migrates it to TypeScript and splits it by concern:

ValueEditor/
  ValueEditor.tsx    orchestrates
  types.ts           language union and display order
  validate.ts        parse checks, no React
  components/        LanguageSelector, LanguageValidation, CopyValueButton

The folder resolves where the file used to, so no import changed.

Also in here:

  • The component owns its label. Three treatments had grown up around it (FieldLabel, a raw <label>, a bespoke div), none passed htmlFor, and ValueEditor accepted no id, so none of them actually named the editor. It now renders the FieldLabel itself and generates both ids. The contenteditable gets role="textbox" and aria-labelledby.
  • The format labels are real buttons. They were <span onMouseDown>: not focusable, no role, no keyboard path. Now BareButtons in a role="group" labelled "Value format", carrying aria-pressed. mousedown was the reason they were spans, since pressing down on a button blurs the contenteditable and loses the caret, so preventDefault keeps the caret and onClick does the work.
  • Layout and colour use utilities, Bootstrap for spacing and position, token utilities for colour. With ~90 lines of first party styles reclaimed from web/styles/3rdParty/_hljs.scss, the component's own SCSS is 99 lines.
  • Copy uses navigator.clipboard instead of polyfill-react-native's wrapper around the deprecated execCommand. copyToClipboard moves to common/utils; Utils keeps the key and delegates, so its existing callers are untouched. Note this narrows the copy button in insecure contexts, where navigator.clipboard is undefined: deliberate, since the other 18 callers of Utils.copyToClipboard already behaved that way and no deployment we document is served over plain http. If that turns out to be a supported mode it wants fixing for all 19 callers, not here.
  • One behaviour fix, in its own commit: a validation error rendered in body colour, because .select-language .active .language-icon out specified .text-danger.
  • E2E drives the real editor. ValueEditor rendered E2E ? <textarea> : <Highlight>, a 2022 TestCafe workaround (Chores/read only feature #1642), so no test at any level touched the contenteditable users type into. The fork goes, and the three value editor selectors move to role and accessible name, which this PR's label work made possible. placeholder and readOnly only ever reached that textarea, so they go with it, along with name.

SAML moves to InputGroup, since it only used ValueEditor to get an XML textarea.

How did you test this code?

13 unit tests over validate.ts (every language's valid and invalid paths) and copyToClipboard.ts. Writing them corrected an assumption of mine: a:\n- b\n - c is valid YAML, so the failing case is an unclosed quote instead.

Drove the Storybook stories in light and dark and asserted on the rendered DOM: label and format row on one row, active language, validation icon tone, copy still a <button> with its aria-label, no ion-icon anywhere. That caught two regressions I had introduced, alphabetically ordered format labels and a validation icon that vanished.

E2E against the real editor, run on staging with a locally served bundle: flag-tests, mv-options-tests, change-request-test, segment-test and versioning-tests. That is what turned up a strict mode violation in the override selector: getByRole matches the name as a substring, and an override row also holds Segment Control Value and read-only Variation Value editors.

Not covered: the components in isolation. Jest runs testEnvironment: 'node' with no React Testing Library, so validate.ts and copyToClipboard.ts are the only unit testable parts, and the xml branch is it.todo for want of DOMParser.

Reviewing this

  1. Icon drops className for most of its cases and hardcodes #FF9F43 on warning, so the colour class goes on a wrapper span here rather than changing Icon and taking the blast radius. Deserves its own issue.
  2. aria-pressed toggles, not a radiogroup. A radiogroup is more precise for a mutually exclusive set but wants roving tabindex and arrow key navigation. Tab plus Enter or Space is accessible today; say if you would rather have the full radiogroup.
  3. The E2E work was test(e2e): drop the textarea fork and select the value editors by role #8448 until Themis pointed out that merging this without it leaves the production editor untested. Its two commits are now the top of this branch and can be reverted together.

@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
flagsmith-frontend-preview Ready Ready Preview Sep 8, 2026 2:36pm UTC
flagsmith-frontend-staging Ready Ready Preview Sep 8, 2026 2:36pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
docs Ignored Ignored Preview Sep 8, 2026 2:36pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • No new commits to review - use @coderabbitai full review for a full pass

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: f81a02eb-edfa-44dc-abd1-57d662f001a6

📥 Commits

Reviewing files that changed from the base of the PR and between 48ad82d and fe024c6.

📒 Files selected for processing (1)
  • frontend/common/utils/__tests__/copyToClipboard.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a modular ValueEditor with language selection, validation, clipboard support, labels, and accessibility updates. It extracts clipboard handling into a tested utility. Feature, variation, segment, and documentation components now use the new editor API and control-weight chips. SAML metadata editing now uses a textarea with a separate download button. Obsolete value-editor styling is removed.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🟡 Moderate · up to fe024

This migration updates value editing, clipboard behavior, and SAML metadata editing. The IdP metadata download control may be unavailable immediately after creating a SAML configuration, and outstanding frontend contract issues remain, so the change should be resolved before merge.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the front-end Issue related to the React Front End Dashboard label Sep 2, 2026
@talissoncosta
talissoncosta force-pushed the refactor/value-editor-typescript branch from 9930bc1 to 61e17f1 Compare September 2, 2026 18:46
@talissoncosta
talissoncosta force-pushed the refactor/value-editor-typescript branch from b129ad3 to 7f4289b Compare September 2, 2026 21:12
@talissoncosta
talissoncosta force-pushed the refactor/value-editor-typescript branch from 7f4289b to 1a381c2 Compare September 2, 2026 21:19
@talissoncosta

Copy link
Copy Markdown
Contributor Author

@themis-blindfold review

Comment thread frontend/web/components/ValueEditor/validate.ts
@themis-blindfold

Copy link
Copy Markdown
Contributor

⚖️ Themis review: 🟠 Fix before merge

The editor is now typed, labelled, keyboard-operable, and exercised through the production contenteditable path. One regression gap remains in the browser-only XML validation branch. Completed unit, lint, static-analysis, and three E2E jobs passed; one private-cloud E2E job was still running.

Area Score
🎯 Correctness 4/5
🧪 Test coverage 3/5
📐 Code quality 5/5
🚀 Product impact 4/5

🟠 Majors

  • frontend/web/components/ValueEditor/validate.ts:10 — add automated coverage for XML validation in a browser-capable environment.

⚖️ Acknowledged

  • Use the common/ alias for this import. — thread resolved by @coderabbitai[bot]
  • Move ControlWeightChip into the required component structure. — thread resolved by @coderabbitai[bot]
  • Place the new component in its required folder structure. — thread resolved by @coderabbitai[bot]
  • Move each new subcomponent into the required component layout. — thread resolved by @coderabbitai[bot]
  • Replace relative module specifiers with approved aliases. — thread resolved by @coderabbitai[bot]
  • Restore a visible keyboard focus indicator. — thread resolved by @coderabbitai[bot]
  • Fix the reported Stylelint errors. — thread resolved by @coderabbitai[bot]
  • Extract the inline union types into named types. — thread resolved by @coderabbitai[bot]
  • Add the required blank line before the comment. — thread resolved by @coderabbitai[bot]
  • Use approved aliases for local module imports. — thread resolved by @coderabbitai[bot]
📝 Walkthrough
  • ValueEditor - replaces the untyped class with a composed TypeScript editor, including labels, keyboard format controls, copy handling, and validation.
  • Feature and segment callers - adopt the string callback and move labels/weight chips into the shared editor.
  • Browser tests - target the actual labelled contenteditable instead of the old test-only textarea.
  • SAML configuration - uses a normal textarea and preserves the ability to clear stored IdP metadata.
🧪 How to verify
  1. Run cd frontend && npm run test:unit -- --runInBand common/utils/__tests__/copyToClipboard.test.ts web/components/ValueEditor/__tests__/validate.test.ts.
  2. Run cd frontend && npx playwright test e2e/tests/mv-options-tests.pw.ts e2e/tests/change-request-test.pw.ts e2e/tests/segment-test.pw.ts e2e/tests/versioning-tests.pw.ts.
  3. In a feature drawer, switch between each format, enter malformed XML, and confirm the validation warning appears; then enter a well-formed document and confirm it clears.
  4. Edit a SAML configuration, clear IdP metadata, save, and reload to confirm the cleared value persists.
    Automate: add the valid and malformed XML checks to browser coverage so the DOMParser branch is exercised in CI.

Product take: A solid reliability and accessibility improvement to a heavily reused editor. The remaining XML regression coverage is important because SAML and remote-config users depend on it.

🧭 Assumptions & unverified claims

The focused Jest command could not run in this checkout because the Jest binary is absent; the completed Unit Tests job passed.

A tidier editor deserves one XML safety net · reviewed at db237f4

@talissoncosta

Copy link
Copy Markdown
Contributor Author

@themis-blindfold review

@themis-blindfold

Copy link
Copy Markdown
Contributor

⚖️ Themis review: 🟠 Fix before merge

The ValueEditor migration preserves its callers, gives the production contenteditable path browser coverage, and improves its accessible controls. One SAML persistence correction still lacks regression coverage. CI is still in progress; the completed unit, lint, and visual checks passed.

Area Score
🎯 Correctness 4/5
🧪 Test coverage 3/5
📐 Code quality 4/5
🚀 Product impact 3/5

🟠 Majors

  • CreateSAML.tsx:244 — cover clearing existing IdP metadata before relying on the new request branch.

⚖️ Acknowledged

  • Keep the local copyToClipboard sibling import — thread resolved by @coderabbitai[bot]
  • Keep the unstyled ControlWeightChip as a file-local component — thread resolved by @coderabbitai[bot]
  • Keep private ValueEditor subcomponents in their parent components/ directory — thread resolved by @coderabbitai[bot]
  • Keep local ValueEditor module imports relative — thread resolved by @coderabbitai[bot]
  • Restore a visible keyboard focus indicator — thread resolved by @coderabbitai[bot]
  • Correct the reported ValueEditor SCSS lint issues — thread resolved by @coderabbitai[bot]
  • Extract ValueEditor’s inline union types — thread resolved by @coderabbitai[bot]
  • Add the required SCSS comment spacing — thread resolved by @coderabbitai[bot]
  • Keep approved aliases out of local ValueEditor imports — thread resolved by @coderabbitai[bot]
  • Use the configured local-import convention in utils.tsx — thread resolved by @coderabbitai[bot]
📝 Walkthrough
  • ValueEditor — migrates the editor to typed, focused components and owns its label, controls, validation, and copy action.
  • Highlight — exposes the real contenteditable editor as a labelled textbox and updates its read-only accessibility state.
  • E2E helpers and specs — remove the test-only textarea path and address editors by role and accessible name.
  • SAML configuration — replaces the XML code editor with a textarea and permits an administrator to submit an intentional empty metadata value.
🧪 How to verify
  1. Edit an existing SAML configuration, clear IdP metadata, update, and reopen it to confirm the stored value is empty.
  2. Run cd frontend && npm run test:unit -- ValueEditor to exercise parser and clipboard helpers.
  3. Run cd frontend && npx playwright test e2e/tests/flag-tests.pw.ts -g "Value editor validates" to cover XML and JSON validation in a browser.
  4. Run the affected Playwright specs for feature values, variations, overrides, and change requests.
    Automate: Add an enterprise SAML clear-save-reload regression test that asserts the update request contains idp_metadata_xml: ''.

Product take: A solid reliability and accessibility improvement to a frequently used configuration editor; the untested SAML clearing path is the only material gap.

🧭 Assumptions & unverified claims
  • The enterprise SAML update endpoint preserves omitted optional metadata on PUT; this behaviour is not implemented in this checkout.

The editor has finally met the editor in the tests · reviewed at 4fd83c7

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 2a1a668a-f712-4dd9-acdf-39ef2d5eb445

📥 Commits

Reviewing files that changed from the base of the PR and between fe024c6 and c030547.

📒 Files selected for processing (16)
  • frontend/common/utils/__tests__/copyToClipboard.test.ts
  • frontend/common/utils/copyToClipboard.ts
  • frontend/documentation/components/ValueEditor.stories.tsx
  • frontend/e2e/helpers/e2e-helpers.playwright.ts
  • frontend/e2e/tests/change-request-test.pw.ts
  • frontend/e2e/tests/flag-tests.pw.ts
  • frontend/e2e/tests/mv-options-tests.pw.ts
  • frontend/e2e/tests/segment-test.pw.ts
  • frontend/e2e/tests/versioning-tests.pw.ts
  • frontend/web/components/Highlight.js
  • frontend/web/components/SegmentOverrides.js
  • frontend/web/components/ValueEditor/ValueEditor.scss
  • frontend/web/components/ValueEditor/ValueEditor.tsx
  • frontend/web/components/modals/create-feature/tabs/FeatureValueTab.tsx
  • frontend/web/components/mv/VariationValueInput/VariationValueInput.tsx
  • frontend/web/components/pages/organisation-settings/tabs/sso/saml/modals/CreateSAML.tsx

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread frontend/e2e/helpers/e2e-helpers.playwright.ts
Comment thread frontend/e2e/helpers/e2e-helpers.playwright.ts Outdated
Comment thread frontend/web/components/SegmentOverrides.js
Comment thread frontend/web/components/ValueEditor/ValueEditor.tsx Outdated
Comment thread frontend/web/components/ValueEditor/ValueEditor.tsx Outdated
talissoncosta and others added 21 commits September 8, 2026 11:34
ValueEditor was a 254-line untyped class with no props contract, which is
how `placeholder` and `readOnly` came to be passed at three call sites and
silently ignored. Renaming the file to .tsx produced 43 strict errors; all
but two were missing interfaces and implicit any.

The two that were not:
- renderValidation() was called with an argument it does not accept
- <Tooltip position='top'> passes a prop TooltipProps does not declare

Both are gone. `value` is typed FlagsmithValue rather than string, which is
what the MV and segment call sites actually pass.

Split into a folder by concern, so each piece is small and the parsing is
testable on its own for the first time:

  ValueEditor/
    ValueEditor.tsx              orchestrates, function component
    types.ts                     language union and display order
    validate.ts                  pure parse checks, no React
    components/LanguageSelector  the .txt/.json/.xml/.toml/.yaml row
    components/LanguageValidation the tick or warning
    components/CopyValueButton   the copy control
    index.ts

Imports are unchanged: the folder resolves where the file used to.

Drop ionicons from this component. We already own `warning` and
`checkmark-circle`, so it is a straight swap. Icon forwards className for
only 12 of its ~100 cases and `warning` hardcodes orange, so the tone class
goes on a wrapper and the icon inherits through currentColor.

Copying no longer goes through polyfill-react-native's Clipboard, which
wraps the deprecated document.execCommand. Extract copyToClipboard out of
the Utils object into common/utils/copyToClipboard, so a component can
reach it without pulling in Utils; Utils keeps the key and delegates, so
its nineteen existing callers are untouched. The Storybook stub for Utils
asks new components not to import it, and this is what that looks like.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Chrome puts <parsererror> in the XHTML namespace, so it is an HTMLElement
with innerText and the old instanceof check held. Firefox uses its own
namespace and returns a plain Element with neither, so malformed XML
reported no error and the editor showed a green tick. Our E2E runs on
Firefox.

Reading textContent off Element works in both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
validate.ts and copyToClipboard.ts are plain .ts with no React, which is
the only thing this project can unit test today: jest runs with
testEnvironment 'node' and testMatch picks up .ts but not .tsx, and there
is no React Testing Library.

The xml branch is left as it.todo. It needs DOMParser, which the node
environment does not provide, and pulling in jest-environment-jsdom is a
wider decision than this file.

Writing these caught a wrong assumption: 'a:\n- b\n  - c' parses fine, so
the yaml failure case now uses an unclosed quote and a bad indent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The .txt/.json/.xml/.toml/.yaml labels were <span onMouseDown>: not
focusable, no role, no keyboard path. That is the same defect the copy
control had, and leaving four of them beside a fixed fifth made no sense.

They are BareButtons now, in a role=group labelled "Value format", with
aria-pressed carrying the selection.

Keeping mousedown was the reason they were spans. The editor is
contenteditable and pressing down on a button blurs it, losing the caret.
preventDefault on mousedown stops the blur without stopping the click, so
onClick can do the work and the keyboard path comes for free.

The styles keyed off the `span` selector, so they move to `.bare-btn`.
BareButton resets at zero specificity, so these rules win without having
to out-specify it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three label treatments had grown up around one component: FieldLabel at
two call sites, a raw <label> at two, a bespoke ValueRowLabel div at three,
and nothing at all in the SAML modal. None of them passed htmlFor, and
ValueEditor accepted no id, so no version named the editor.

ValueEditor now takes `label` and `labelTooltip` and renders the FieldLabel
itself, generating both ids so a caller cannot get the association wrong.
The editor carries aria-labelledby, and the contenteditable gets
role=textbox with aria-multiline, without which a screen reader announces
it as plain text and aria-labelledby has nothing to name.

That also puts the label and the format buttons in one flex row. They used
to be separate layers, the button row absolutely positioned over whatever
label the caller happened to render, so nothing stopped a long label
running underneath it. The multivariate control value is the widest case
(label, weight chip and tooltip); at 380px, narrower than the drawer gets,
it now leaves a 56px gap instead of relying on there being room.

The copy button moves into a __field wrapper so it stays positioned
against the editor rather than the whole component, which now includes the
header.

Two onChange handlers were annotated as taking a ChangeEvent when
ValueEditor has only ever passed a string; safeParseEventValue returns
non-events unchanged, which is why nobody noticed. Typed properly and the
wrapper dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ValueEditor was the wrong component here. onlyOneLang turned off the
format buttons, the copy control and, because validation renders beside
the active format label, the XML validation too. What was left was syntax
highlighting on a blob users paste and rarely read, bought at the cost of
a contenteditable standing in for a form control.

InputGroup with textarea gives what the field needs: a real <textarea>
with an id, a FieldLabel wired to it with htmlFor, aria-invalid and
aria-describedby, FieldError, and a placeholder that is honoured. The call
site has been passing one all along that ValueEditor dropped.

The download action moves to the footer beside the sibling "Download
Service Provider Metadata" button, rather than hanging off the field's
label. It also sends the saved config rather than what the field shows, so
in the header it read as "download this field" and would have handed you
something else once you typed.

data-test and name were 'featureValue', copy-pasted from the feature form,
so an unrelated field shared a test id.

onlyOneLang existed for this one caller, so it goes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The feature drawer rendered "Control Value" with a weight chip; segment
overrides rendered the same idea as "Segment Control Value - 50%". Same
concept, two treatments, and only one of them a chip.

ControlWeightChip renders it once. It uses the canonical Chip rather than
the legacy .chip class the feature drawer had inline: the legacy one
carries a hand-written .dark block, and new code should not add a 36th
usage of what #6606 is trying to retire.

The chip sits after the tooltip icon, so the label reads
"Control Value (i) 100%". It is a sibling of FieldLabel inside a flex row
rather than label content, which leaves FieldLabel alone and keeps the
weight out of the editor's accessible name: that name is now a stable
"Control Value" instead of shifting with the split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ides

web/styles/3rdParty/_hljs.scss is for overriding highlight.js and Pylon's
chat widget. About 130 of its 387 lines were first-party ValueEditor
styles: the component block, its header, field and copy button, the format
row, and code.txt. They now live in ValueEditor.scss beside the component,
which drops _hljs.scss to 256 lines of what it says it is.

The selectors are unchanged, so specificity and cascade behave as before.
Verified in the browser: copy still 18px/12px inside the field and 11px in
code-medium, padding still 16px 44px 16px 20px, code-medium still holds
44px, the format buttons still 12px, and code.txt keeps its 14px/500
OpenSans on a white surface.

It is the only component stylesheet that imports _variables. The six input
sizing values it needs have no token equivalent, and inlining them would
lose the link to $input-height that code-medium exists to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ount

The language check ran in a mount-only effect behind a disabled
exhaustive-deps rule. Values load after mount, and when one did the effect
had already returned at `if (!value)`, so a JSON value rendered as
plaintext for the life of the editor.

It now waits for a value and runs once, tracked by a ref that a manual
pick also sets, so detection never overrides a choice. The caller's
`language` seeds the state directly and counts as already settled. No lint
suppression needed.

Alongside, from the review pass:

Scope `code.txt`. It was a global selector, so any <code class="txt"> in
the app picked up a white surface, a border and 14px OpenSans.

Finish moving the styles out of the globals: a `.value-editor` block was
still in styles.scss. Its disabled-state cursor and opacity move here; the
rule hiding the format row is dead, since the row is no longer rendered
while disabled.

Drop `editorId`. Nothing referenced it: the label association goes through
aria-labelledby, because a <code> is not labelable and htmlFor would do
nothing. Highlight's id passthrough goes with it.

Import Row and Tooltip rather than reading them off the window, which is
why Storybook needed stubs for them.

Adds a story for a value that arrives after mount, covering the bug above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The parse check lived inside LanguageValidation, which renders inside
LanguageSelector, so validity only existed when the format row did. That
is why the SAML field could ask for language='xml' and get no XML
validation: hiding the row hid the check with it.

ValueEditor computes it now and passes it down, so LanguageValidation is
presentational and can be rendered anywhere. Callers can read it too, via
onValidityChange.

saveFeatureWithValidation still reads 'language-validation-error' off the
DOM, so that id stays. Moving it onto the callback needs a decision this
change should not make on its own: the DOM query is global, so today a
parse error in a segment override also blocks saving the feature value,
and a callback has to say which editors gate which save.

Also documents why onChange hands back a string rather than a
FlagsmithValue: this edits text, and deciding "123" is a number is domain
logic that belongs to the caller. Two SegmentOverrides handlers were still
annotated as taking events and wrapping in safeParseEventValue, which has
been a no-op since ValueEditor started passing strings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Layout and spacing move to Bootstrap utilities, the resting colour of the
format buttons to a token utility, matching how the copy button already
works: utility for the resting state, SCSS for hover and active.

Deleted from the SCSS: the __header flex row, the __label block, the
__field position, select-language's margin-left, and the flex on the
format buttons and the validation icon. `.control-label { margin-bottom:
0 }` becomes mb-0 on FieldLabel, which stops this component reaching into
another one's class.

`.active .language-icon` also set margin and opacity. On main that rule
was `.active span`, aimed at the old label spans; nothing sets either
property on the icon, so those two declarations were dead.

What stays needs to: offsets and gaps off Bootstrap's 4px scale, hover
and focus states, the :has() and ancestor selectors, and everything on
the pre/code/hljs elements Highlight renders, where there is no JSX here
to carry a class.

Storybook, light and dark: the header, field and format row keep the same
computed styles and the same bounding boxes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The icon carries .text-success or .text-danger, but
`.select-language .active .language-icon` set a colour at 0-3-0 against
the utility's 0-1-0, and validation only ever renders on the active
label, so that rule always won. A parse error showed a warning triangle
in body colour.

Verified in Storybook: the icon computes to rgb(239, 77, 86) in dark and
the danger token in light.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`string | false` appeared at four boundaries: validateValue's return,
onValidityChange, and both subcomponents' props. One exported
ValueEditorError now covers them, and the icon's tone gets a named type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`.hljs` sets `outline: none` and this component suppresses the outline
colour on top, for every language. Only `code.txt` restyled its border on
focus, so focusing a JSON, XML, TOML or YAML editor showed nothing at
all. `.hljs` does carry a border, so focus-visible can recolour it the
way inputs do.

Also drops `word-break: break-word`, deprecated in favour of
`overflow-wrap`, and reuses $font-family rather than repeating its value.

Verified in Storybook: focusing the JSON editor takes the border to
var(--color-border-action).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stylelint's scss/double-slash-comment-empty-line-before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The E2E ? textarea : Highlight fork was added in 2022 as a TestCafe
workaround. Playwright landed in March and drives a contenteditable with
fill(), so every E2E assertion about this editor has been running against
a textarea that only exists during tests.

placeholder and readOnly only ever worked on that textarea, so they go
with it, along with the three call sites passing them.

Highlight takes role and aria-readonly from the caller rather than
deriving them from onChange, so ValueEditor can name its read-only
editors while the code blocks that also use Highlight stay unlabelled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ValueEditor no longer takes a data-test. Three accessors replace it, and
setText/waitForElementVisible accept a Locator so callers can pass one.

  featureValueField()            role=textbox, /^(Value|Control Value)/
  variationValueField(i)         role=textbox, "Variation Value", nth(i)
  segmentOverrideValueField(i)   scoped to the override, then role+name

The feature value alternation is not cosmetic: the label becomes
"Control Value <weight>%" once a feature has variations.

This also retires a selector that encoded its own value:

  data-test={`featureVariationValue${featureStateToValue(value) || index}`}

The id was featureVariationValue1 only while the field was empty, and
became featureVariationValueadded once you typed. The tests passed because
they addressed it before typing.

toHaveValue becomes toHaveText in change-request-test: the editor is a
contenteditable, not a textarea, so there is no value to assert on.

No new type errors in e2e (3 before, 3 after, all pre-existing).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- `featureValueField` was page-wide and prefix-matched, so it relied on
  `Control Value` and VariationOptions' `Control value` differing only in
  case. Scoped to the drawer and anchored.
- `Highlight.shouldComponentUpdate` listed neither `role`,
  `aria-readonly`, `aria-labelledby`, `disabled` nor `onChange`, so a
  value editor that turned read-only kept its `contentEditable` and never
  gained `aria-readonly`.
- `ValueEditor.scss` imported `_variables`, which is not variables-only:
  it emits `.dark hr`, `.bg-*` and a `.fill-*` loop, duplicating them
  into the component chunk. The seven scalars it needs move to
  `_metrics.scss`, imported after bootstrap so bootstrap's own rem
  defaults still win. Component CSS drops from 67 rule blocks to 20 and
  the global stylesheet compiles to an identical hash.
- Dropped the stray `placeholder` on a SegmentOverrides editor that no
  longer takes one, and three dead destructured E2E helpers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`validate.ts` has unit tests for every format except xml, where both
cases are `it.todo`: jest runs in node and `xmlError` needs a real
DOMParser, whose parser-error output differs between browsers. Nothing
exercised format switching either.

Drives it through the drawer instead: malformed XML raises the error
against `.xml`, a well-formed document clears it, then the same for
`.json`. Closes without saving, so it leaves no data behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Swapping the IdP metadata field to InputGroup lost syntax highlighting,
which is the reason SAML used ValueEditor in the first place: an
administrator pastes a metadata document and has to be able to read it.
Reverted, so SAML keeps the editor it has on main.

`onlyOneLang` goes instead. A caller that pins `language` has nothing to
switch to, so pinning now hides the format row and the copy control,
which is all that boolean ever did. The SAML call site loses one prop
rather than gaining one.

The InvalidJson story went with it: the warning renders against the
active format label, so a pinned story has nowhere to show it. The
browser test added in this PR covers that path instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
From a review pass over the split:

- `label` is required, since the accessible name is the point. That
  caught the story wrapper typed as `Record<string, any>`; it takes the
  component's own props now. ReactNode admits null, so the runtime guard
  still does the work of not rendering a nameless editor.
- `language` is derived from the prop, the user's pick, then detection.
  It was read into useState on mount, so a caller changing the prop kept
  the old format for highlighting and validation. No caller does that
  today.
- Validity is only computed alongside the format row, so a caller that
  pins the format no longer runs a DOMParser parse per keystroke.
- LanguageValidation renders beside the format button rather than inside
  it: Tooltip mounts a div, which a button cannot contain, and it joined
  the button's accessible name while shown.
- The SAML download control is a BareButton with an aria-label, wrapped
  by its tooltip rather than wrapping it. It was a clickable div with no
  keyboard path and no name.
- An invalid-JSON story is back, so the danger tone has visual coverage
  again. The format is clicked rather than pinned, since a pinned editor
  has no row to render the warning against.
- The helper comment claimed the multivariate label reads
  "Control Value <weight>%". It does not: the weight chip is a labelAfter
  sibling, outside the label. Named the `string | Locator` union too.

Verified in Storybook: a value arriving after mount still detects as
json, clicking .xml overrides it and raises the warning, a pinned format
still hides the row, and the warning renders red beside the active label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@talissoncosta

Copy link
Copy Markdown
Contributor Author

@themis-blindfold review

@themis-blindfold

Copy link
Copy Markdown
Contributor

⚖️ Themis review: ✅ Ship it

The refactor keeps the editor’s callers aligned with its typed, labelled textbox contract, and the real contenteditable path is now exercised by browser coverage. All recorded checks passed, including unit, lint, browser, and E2E suites.

Area Score
🎯 Correctness 5/5
🧪 Test coverage 5/5
📐 Code quality 5/5
🚀 Product impact 3/5

⚖️ Acknowledged

  • Local sibling import convention — thread resolved by @coderabbitai[bot]
  • Control-weight chip file layout — thread resolved by @coderabbitai[bot]
  • Private copy-button component layout — thread resolved by @coderabbitai[bot]
  • Private language-component layout — thread resolved by @coderabbitai[bot]
  • Local validation-module imports — thread resolved by @coderabbitai[bot]
  • Keyboard focus indicator for highlighted editors — thread resolved by @coderabbitai[bot]
  • Value-editor stylesheet lint fixes — thread resolved by @coderabbitai[bot]
  • Shared validation type contract — thread resolved by @coderabbitai[bot]
  • Stylesheet formatting — thread resolved by @coderabbitai[bot]
  • Same-folder import aliasing — thread resolved by @coderabbitai[bot]
  • The real editor was not covered under E2E — thread resolved by @themis-blindfold
  • XML parser validation coverage — thread resolved by @talissoncosta
  • SAML metadata clearing scope — thread resolved by @talissoncosta
  • Exact accessible-name selector for the control value — thread resolved by @coderabbitai[bot]
  • Locator support in the E2E helper layer — thread resolved by @coderabbitai[bot]
  • Keyboard-accessible IdP metadata download control — thread resolved by @coderabbitai[bot]
  • Segment-override descendant import convention — thread resolved by @coderabbitai[bot]
  • Required editor label contract — thread resolved by @coderabbitai[bot]
  • Runtime language-prop updates — thread resolved by @coderabbitai[bot]
  • Clearing loaded SAML metadata no longer introduces an unpersisted empty state — thread resolved by @themis-blindfold
📝 Walkthrough
  • ValueEditor - replaces the untyped class with a labelled, typed editor and separates format selection, validation, and clipboard handling.
  • Highlight and E2E helpers - expose the production contenteditable as an accessible textbox and drive it by role and name.
  • Feature, segment, variation, and SAML callers - supply labels and preserve their specialised read-only and pinned-format behaviour.
  • Validation coverage - exercises JSON/XML browser validation and parser-specific unit paths.
🧪 How to verify
  1. Run cd frontend && npm run test:unit -- ValueEditor.
  2. Run cd frontend && npm run typecheck.
  3. Run cd frontend && npm run test:run -- e2e/tests/flag-tests.pw.ts and create a remote config with malformed then valid XML and JSON.
  4. Open feature, segment-override, multivariate, and SAML editors; verify labels, read-only fields, copy controls, and format selection.
    Automate: Retain the browser validation scenario alongside the role-based editor E2E coverage.

Product take: A solid accessibility and regression-safety improvement to a widely used configuration control. It is mostly refactoring, but it removes an important gap between test and production editing behaviour.

🧭 Assumptions & unverified claims

No unverified assumptions or claims.

The editor has graduated from its test-only stunt double · reviewed at 41d8eba

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

front-end Issue related to the React Front End Dashboard refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor and migrate ValueEditor to TypeScript

2 participants