Experience Studio: a refused save hangs silently and an empty layout cannot be previewed
- Dominant language
- PHP
- Stars
- 3.4k
- Forks
- 1.2k
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 433
Description
Three defects found by driving the Experience Studio end to end in a browser. All three predate the model split (#19663) and are not regressions from it. The first two together make a refused save indistinguishable from a hung one: the user sees a spinner and nothing else, forever.
## 1. A refused save gives no signal and never finishes
`sw-experience-studio-detail/index.ts::onSave()` awaits `layoutRepository.save()` with no `catch` and no `finally`. `isLoading` is set before the await and cleared only on the success path, past the success notification. A rejected save leaves the Save button spinning with no notification.
The path is reachable through the editor's own affordances: add an `Sw:Media:Image` element, leave its media unset, save.
```
PATCH /api/content-layout/{id} -> 400
{"errors":[{"code":"unfilled_required_input","status":"400",
"detail":"Required property \"media\" is wired from \"mediaId\", which has no value.", ...}]}
```
Reproduced twice, the second time after a clean page reload. The rejection itself is correct. The preview route runs only the intrinsic diagnostics subset, while resolvability is a write-time gate, so a draft that previews cleanly can still be refused at save. That split is deliberate, which makes the failure branch a permanent requirement of the save path, not a defensive extra.
Expected: `onSave()` catches the rejection, clears `isLoading` in a `finally`, surfaces the API error `detail` to the user, and leaves the draft intact for repair.
## 2. `ErrorResolver` crashes on a pointer segment that is not a field
`src/core/data/error-resolver.data.js:132` reads
```js
this.errorStore.addSystemError(error);
```
but `this.errorStore` is never assigned anywhere in the class; every other call site reaches the store via `Shopware.Store.get('error')`. Line 132 therefore throws `TypeError: Cannot read properties of undefined (reading 'addSystemError')`.
The branch runs whenever a write error's pointer carries a segment that is not a field of the written entity. The content-layout pointer `/0//media` is such a case: `` is not a field of `content_layout`, so `definition.getField()` returns undefined and the `!field` branch executes line 132. This is a platform defect, not a content-system one; any entity write with a non-field pointer segment hits it. The file was last changed by `cb265e7ae21`, untouched by the content-system work.
Expected: the `!field` branch reaches the error store the same way the rest of the class does.
## 3. Preview refuses an empty layout
`ContentPreviewRequest` declares `#[Assert\NotBlank]` on `array $layout`. `NotBlank` rejects `[]`. The studio requests a preview when a layout opens, so a freshly created layout's first screen is Symfony's generic "This value should not be blank." error.
It is the only one of the eleven content request DTOs that constrains `layout` this way; the mutation DTOs declare `#[Assert\Type('array')]` with a `[]` default and accept an empty layout today. The constraint dates to `3434e63d833`, the commit that introduced the preview action.
Expected: `#[Assert\Type('array')]` alone, matching the siblings, so an empty draft mints a preview token that renders the page with no layout elements. Precondition: confirm `ContentPreviewPageBuilder::build()` completes on an empty tree, or answer the empty case before the build.
## Out of scope
Concurrent editing: the save carries no concurrency token, so two editors are last-write-wins. That is owned by #19585. The failure branch from section 1 handles a future 409 from that work the same way it handles the 400 above.
Contributor guide
Assessment
This issue has not been assessed yet.