Fix Biome lint errors on frontend
Description
Summary
Re-enables full Biome lint enforcement in src/main/frontend/. CI's validate step ran biome check --linter-enabled=false, so nothing was checked and the recommended ruleset had accumulated violations unnoticed.
Clears 271 errors to 0, and settles the warnings too under one policy: a rule either gates the build or is not enabled. Warning is a migration state, not a resting state — a warning nobody actions only teaches people to ignore linter output, which is how the previous ESLint config stayed broken here without anyone noticing.
Final: 0 errors, 0 infos, 261 warnings (all one deliberately-kept rule). npm run validate green, tsc --noEmit clean. 670 files changed.
Scope correction
The counts in the original description were stale — Biome had been upgraded since (schema 2.3.8):
|
original description |
actual |
|
|---|---|---|
|
errors |
657 |
271 |
|
files |
622 |
111 |
|
|
240 |
0 — no longer reported in that form |
|
|
413 |
34 |
Eight rules that error today were missing from the original breakdown (noLabelWithoutControl 37, useKeyWithClickEvents 26, noSwitchDeclarations 7, useAriaPropsSupportedByRole 6, and four single-occurrence rules). Also: only errors fail the build — a warnings-only file exits 0 — so the proposed per-phase CI ratchet was unnecessary.
Delivered as one PR rather than the four proposed phases, because the judgement-call errors sit inside the same files as the mechanical ones (10 of their 30 files carry both, including VoteStepNode, GroupStepNode, PathRenderer, AdfRenderer, ParallelGroup). Splitting by rule family would edit those files twice and make a tester re-check the same screen twice — 38 UI-surface passes against 18.
Rule configuration
25 rules at error. useSemanticElements sits at its recommended default; the other 24 are promoted above their Biome defaults so they gate rather than advise: useImportType, useConst, useTemplate, noExplicitAny, noGlobalIsNan, useParseIntRadix, noEvolvingTypes, noAccumulatingSpread, noUnusedVariables, noUnusedFunctionParameters,noUselessFragments, useOptionalChain, noThisInStatic, noUselessEscapeInRegex, noUselessUndefinedInitialization, useArrowFunction, noUselessSwitchCase, noUselessCatch, useLiteralKeys, noInvalidUseBeforeDeclaration, noUndeclaredVariables, noNodejsModules, noProcessGlobal, noControlCharactersInRegex.
4 rules off, each because the rule does not fit this codebase:
|
rule |
n |
why off |
|---|---|---|
|
|
40 |
banning |
|
|
10 |
all 10 are documentation UI listing the |
|
|
5 |
4 of 5 are react-select |
|
|
2 |
a static-only service class is a deliberate namespace; converting churns every call site |
1 rule at warn, the only exception: useExhaustiveDependencies (261). It finds stale closures, cannot be fixed in bulk — its own fix adds dependencies that break compilation — and its value is as a review aid. Anything promoted later starts here.
Formatter and organizeImports unchanged. Considered and rejected: enabling rule groups wholesale adds ~9,700 diagnostics, including 1,290 noSolidDestructuredProps and 90 noReactSpecificProps (SolidJS rules, wrong for React), 2,504 noUndeclaredDependencies (cannot see Parcel's path aliases) and ~1,400 style-only findings. nursery stays off.
Interactive elements: native control where the markup allows one
useSemanticElements gates the build, so role="button" is a documented exception rather than the default fix for the interaction rules. role + tabIndex is not equivalent to a <button>: it carries no accessible name of its own, and it nests inside another control without complaint.
Seven elements are native <button>s — the sort control and the column-options trigger in ColumnHeader (siblings, not nested), the three PathRenderer result badges, the StepTypeChooser step-type cards, the StepsContainer add-step control, and the built-in template card in AiTemplatesContent. Each carries an explicit UA reset (margin, background, border, font, text-align) so the rendered result matches the div it replaces, and ColumnHeader's sort button takes draggable={false} so the drag gesture resolves to the pragmatic-drag-and-drop <th> behind it. The reassign avatars in EmailStepNode and UserStepNode pass onClick to Atlaskit's AvatarItem, which renders a <button> itself.
Ten elements keep role under a per-site biome-ignore, because a <button> there is invalid HTML or the wrong element:
|
element |
why not a |
|---|---|
|
|
|
|
|
|
|
|
the row wraps a |
|
|
card body is Atlaskit block primitives; a |
|
|
holds its own Edit/Delete buttons |
|
|
holds its own floating action buttons |
|
|
renders inside the filter chip's own |
The rule does not see a role passed through a JSX spread, so a clean run is not proof on its own — audit with a grep for role in both attribute and object-literal form.
common/utils/keyboardActivation.ts (activateOnKey) supplies the Enter/Space handler for the elements that stay role="button". Wrappers whose handler is a propagation guard or a hover measurement take role="none" instead, so they gain no phantom tab stop and claim no semantics they do not have.
Decorative SVGs take aria-hidden, not <title> — all 22 get their accessible name from the surrounding control, and <title>would add a native hover tooltip.
Other notable changes
-
Three href-less anchors became buttons — "Select all"/"Clear all" in the API key privileges form, the open-definition icon link in Condition config usage history, and the Manage Approvals link. Each carries the UA button reset plus
token('color.link')and a hover underline, matching what@atlaskit/css-resetgave the anchors. Verified against the originals in a headless render: computed styles match exceptdisplay: inline-block, which browsers force on form controls and which is not visible. -
renderExpandButtonCell— itsIconButtoncarried no handler, so it was a focusable dead control. The handler now lives on the button (native keyboard support,stopPropagationso the cell's own click cannot toggle twice); the cell keepsrole="none"and its click, preserving the full-cell target. -
Keys use a real identity where the item carries one (label text, component name, step id, action label, viewer type+id,
option.text). Where position is the identity, or no stable id exists, the index key is kept with the reason recorded rather than faked — drop zones and connectors are positions, filter conditions are addressed by index througheditingIndex/handleRemove, palette slots are numbered, parsed markdown and text segments have none, andstep.idis optional outside the editor so the list view guards withstep.id || index. -
Hook fixes are reorders.
useMemo/useCallback/useStatehave no side effects, so hoisting them above an early return cannot change behaviour.InstanceErrorsPageis the exception:useUniversalPageLayoutregisters a header action, so it moved above the guard withenabled: !collectionId, leaving the denied path registering nothing exactly as before. -
Dead code removed in
Approval.tsx:onMouseOver={() => clearBorderColor}returned the function without calling it, so the highlight border has never cleared on hover. Restoring that behaviour is a deliberate UX change and is out of scope here. -
TestRenderPanel'sdangerouslySetInnerHTMLis suppressed, not solved. It renders a tenant-authored email template for an admin, which is the panel's purpose. A dependency-free hardening option is a sandboxed iframe (<iframe sandbox srcDoc=...>) — preserves fidelity, changes preview sizing, wants its own decision. -
Five files carry a file-wide
biome-ignore-allfornoArrayIndexKey(DateFilterManager,StepsFilterManager,StepsList,ResponsiveOverflowContainer,GroupStepNode). Biome anchors that rule on thekeyattribute and no comment can sit inside a JSX opening tag, so per-line suppression is impossible. Cost: a genuinely wrong index key added to those files later is not caught.
Enforcement
-
validatechanged frombiome check --linter-enabled=false .tobiome check . -
pom.xmlbindsnpm run buildtogenerate-resources, andbuildrunsvalidate, somvn verify --projects approval-path— the PR gate — enforces type-check and lint. -
.husky/pre-commitnow lints staged frontend files, check-only. It must not write:node_formatends inxargs git add, so anything the hook fixes is re-staged wholesale — fine for formatting, wrong for a linter fix that can change behaviour. It reports and aborts, pointing atnpm run lint:fixand--no-verify. -
Confirmed the gate bites: a file with a fresh
useTemplateornoEvolvingTypesviolation exits 1, where before it produced only an info or a warning.
Manual testing
One regression pass. Everything outside this list is either provably inert or additive keyboard support (tab to it, press Enter).
-
List page column headers — each sortable header is a tab stop, on top of the column-options trigger. Intended, and the most visible change in the ticket. Sort by clicking the header, open the column menu, and drag a column to reorder starting the drag on the header label.
-
The seven converted controls —
PathRenderer's success/rejection/expiration badges (click to expand), theStepTypeChooserstep-type cards,StepsContainer's add-step and add-parallel-group controls, the built-in template cards in Settings → AI, and the reassign avatars inEmailStepNode/UserStepNode. Visual parity plus Tab/Enter. -
The three converted links — "Select all"/"Clear all" in the API key privileges form, the open-definition icon link in Condition config usage history, the Manage Approvals link.
-
Details panel resize handle (Approvals) — drag to resize, then collapse and click to expand.
-
Definition editor — reorder steps by drag-and-drop, including inside a parallel group, and click a step (its keyboard path calls
onClickdirectly rather than the mouse handler). -
Instance errors page in a collection context — still denied, header action absent.
-
Message template preview (Settings → Messages) — templates still render.
-
Approval panel (Jira + Confluence) and the list pages — visual sanity, plus Tab/Enter on elements that were previously click-only.
autoFocus in the bulk decide dialog was kept (suppressed with a reason), so there is no UX change to sign off there.
Known gaps and follow-ups
|
gap |
note |
|---|---|
|
Forge deploy steps skip |
bitbucket-pipelines.yml:136-139 and 166-169 call |
|
|
TypeScript project with no Biome or ESLint config at all |
|
|
the whole Playwright suite is unlinted; left alone deliberately |
|
|
root-level TS config and scripts are not linted |
|
|
the standing backlog; triage opportunistically, never auto-fix |