backstage / backstage/backstage

Scaffolder BUI theme EntityPicker: onBlur re-commits display label as entityRef, corrupting valid selections

Open
#35,281 3 comments 0 reactions 0 assignees View on GitHub
area:scaffolder good first issue priority:contrib-needed type:bug
Dominant language
TypeScript
Stars
34.4k
Forks
7.6k
Avg merge
8h 57m
Merged PRs (30d)
50

Description

### 📜 Issue Labels

- [x] Please familiarize yourself with the issue labels used in this project: [LABELS.md](https://github.com/backstage/backstage/blob/master/LABELS.md)

### 🔎 Search Terms

```plain
EntityPicker allowArbitraryValues blur
```

### 🗃️ Project Area

Software Templates

### 🔗 External Integration

N/A

### 📝 Description & Context

When the scaffolder form's experimental Backstage UI theme is enabled (`EXPERIMENTAL_theme="bui"`, toggled via the templates SubPage extension config `enableBackstageUi: true`), the EntityPicker field (`packages/scaffolder-react/src/fields/EntityPicker` in plugin-scaffolder) can silently overwrite a correctly-selected entity reference with a corrupted, kind-less value on blur — with no visible error to the user.

Root cause, in EntityPicker:

1. In the bui theme branch, the visible text of the combobox is tracked via a separate inputValue state, which is populated from the display label of the selected option (presentation?.primaryTitle), not the entity ref:

```tsx
useEffect(() => {
if (formData) {
const opt = buiOptions.find((o) => o.value === formData);
setInputValue(opt?.label || formData); // <-- label, not the ref
} else {
setInputValue("");
}
}, [formData, buiOptions]);
```
2. Selecting a valid option correctly commits the full entity ref via `handleSelectionChange` which sets `formData` (and `lastCommittedRef.current`) to `stringifyEntityRef(entity)` (e.g. `resource:default/my-subscription`).
3. However, `handleBlur` fires independently and unconditionally re-parses inputValue (the label, e.g. "My Subscription") as if it were an entity reference:

```tsx
const handleBlur = useCallback(() => {
if (allowArbitraryValues && inputValue) {
let entityRef = inputValue;
try {
entityRef = stringifyEntityRef(
parseEntityRef(inputValue, { defaultKind, defaultNamespace })
);
} catch {}
if (lastCommittedRef.current !== entityRef) {
lastCommittedRef.current = entityRef;
onChange(entityRef); // <-- clobbers the correct selection
}
}
}, [...]);
```

Since `allowArbitraryValues` defaults to true when not explicitly configured, this branch runs on essentially every field.

I verified both failure modes directly with @backstage/catalog-model@1.10.0:

- Without `ui:options.defaultKind`: `parseEntityRef("My Subscription Name")` throws `Entity reference "My Subscription Name" had missing or empty kind (...)`. The catch {} swallows it, so entityRef falls back to the raw label text (no kind: prefix at all). onChange then commits this bad value, silently replacing the correct `resource:default/my-subscription` with just "My Subscription Name". Any later step (e.g. the catalog:fetch scaffolder action, which does its own unguarded `parseEntityRef(entityRef, { defaultKind, defaultNamespace }))` then throws the same "missing or empty kind" error for real — but at execution time, far from the actual cause.
- With `ui:options.defaultKind/defaultNamespace` set: no exception occurs at all. `parseEntityRef` happily treats the label as an entity name:

```tsx
parseEntityRef('My Subscription Name', { defaultKind: 'resource', defaultNamespace: 'default' })
// => { kind: 'resource', namespace: 'default', name: 'My Subscription Name' }
stringifyEntityRef(...) // => "resource:default/my subscription name"
```
This is silently committed as the field value — pointing to an entity that almost certainly doesn't exist, with no error surfaced anywhere. This is arguably worse than case 1 since it fails silently deep in a later step (or not at all, if nothing consumes the ref).

The only way to avoid this entirely is setting `ui:options.allowArbitraryValues` false, which disables the offending `handleBlur` branch — but this isn't the default, isn't obviously connected to this bug from the option's name/docs, and many existing templates (in our case, ~15 templates / 30 fields) don't set it.

### 👍 Expected Behavior

Blurring an `EntityPicker` field after selecting a valid catalog entity should never change the committed `ormData`. `onBlur` should not re-derive a ref from the visible label text at all — it should only reconcile free-text input against `inputValue` when the user actually typed something themselves (and even then, it should operate on the actual typed text, not a label that was programmatically set from a prior valid selection). At minimum, a failed/ambiguous re-parse should never silently overwrite an already-valid, correctly-typed `formData`.

### 📦 Reproduction Repo

_No response_

### 🥾 Reproduction steps

1. Enable the experimental Backstage UI scaffolder theme:
```yaml
app-config.yaml:
app:
extensions:
- page:scaffolder/templates:
config:
enableBackstageUi: true
````
2. Create/open a template with a field such as:
```yaml
SubID:
ui:field: EntityPicker
ui:options:
catalogFilter:
kind: [Resource]
spec.type: database
```
(i.e. no `allowArbitraryValues: false` and no `defaultKind` set — the default configuration.)
3. Open the "Create" wizard for that template and select a valid entity from the
EntityPicker dropdown (e.g. "My DB").
4. Click/tab away from the field (blur) without changing anything else.
5. Inspect the form's stored parameter value for that field (e.g. via the review
step, or by adding a debug step that outputs `${{ parameters.SubID }}`).
Expected: `resource:default/my-db`
Actual: `My DB` (or, if `defaultKind`/`defaultNamespace` is set,
a mangled ref like `resource:default/my db`)
6. Any downstream step using this parameter as an `entityRef` input to
`catalog:fetch` (or similar) then fails with:
"Entity reference '' had missing or empty kind (e.g. did not start
with 'component:' or similar)" — or, worse, silently resolves nothing.

### Have you read the Code of Conduct?

- [x] I have read the [Code of Conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md)

### Are you willing to submit PR?

Undecided

Contributor guide

Open the contributing guide

Research direction

Read the EntityPicker implementation in packages/scaffolder-react/src/fields/EntityPicker, starting with the BUI inputValue effect, handleSelectionChange, and handleBlur paths. Reproduce the selection-and-blur steps with allowArbitraryValues enabled, then verify that a valid entity reference remains unchanged after blur and that arbitrary text still follows the intended behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.