backstage / backstage/backstage
Scaffolder BUI theme EntityPicker: onBlur re-commits display label as entityRef, corrupting valid selections
- 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
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