Context
Follow-up to PEES-1560 / pimcore/service-operations#1244, fixed minimally in pimcore/asset-metadata-class-definitions#442. That fix addresses the root cause in the metadata form's draft sync, but the investigation showed the asset save flow in studio-ui-bundle turns any late/lost draft sync into permanent, invisible data loss. The same trap exists for any current or future tab that syncs its draft asynchronously.
Defects in studio-ui-bundle (save-button.tsx + draft slices)
- Empty save requests are sent by design. The save button builds the payload from
asset.changes flags and calls the mutation unconditionally — with no tracked changes it sends PUT /pimcore-studio/api/assets/{id} with data: {} (also for schedules-only saves, since schedules go to their own endpoint). The backend runs a full $asset->save() for it: a new version and a modification-date bump for nothing.
- Success wipes all change tracking. On success the button calls
removeTrackedChanges(), which resets every change flag — including changes tracked while the request was in flight. Combined with (1): an empty save "succeeds", clears the flags the async draft sync just set, the query invalidation resets the draft to server values, and the user's edits vanish with a success toast.
- Latent empty-payload path: a change flag whose draft data is
undefined produces e.g. metadata: undefined, which serializes to an empty payload.
Proposed changes (validated on a 2026.x working branch, incl. jest specs)
- Extract payload assembly into a pure
collectAssetUpdateData() (testable; omits sections whose draft data is undefined).
- After the save-data processors ran: skip the asset PUT when the update is empty — schedules-only saves call only the schedules endpoint; a click with nothing to save just shows the toast.
- New
resetChangesForTypes reducer in use-trackable-changes.ts; on success clear only the change types snapshotted at request time (removeTrackedChanges(types?), backward compatible — no argument keeps full-reset behavior).
- Regression specs for the collector and the reducer.
Notes
- Observable behavior change to review: schedules-only saves will no longer create an asset version / bump the modification date (previously caused by the empty PUT).
- Optional backend hardening (separate decision):
UpdateService::update() in studio-backend-bundle saves unconditionally even for an empty data payload — it could short-circuit, but task-based flows need a BC check; the primary fix belongs in the UI.
- Residual edge to consider while implementing: saving via keyboard shortcut while focus is still inside a form field doesn't blur, so an async draft sync can still lag one save behind — with the changes above this is no longer lossy (no empty PUT, no flag wipe; the next save picks it up). A synchronous pre-save flush hook via the
AssetSaveDataProcessorRegistry would close it completely.
🤖 Generated with Claude Code
Context
Follow-up to PEES-1560 / pimcore/service-operations#1244, fixed minimally in pimcore/asset-metadata-class-definitions#442. That fix addresses the root cause in the metadata form's draft sync, but the investigation showed the asset save flow in studio-ui-bundle turns any late/lost draft sync into permanent, invisible data loss. The same trap exists for any current or future tab that syncs its draft asynchronously.
Defects in studio-ui-bundle (
save-button.tsx+ draft slices)asset.changesflags and calls the mutation unconditionally — with no tracked changes it sendsPUT /pimcore-studio/api/assets/{id}withdata: {}(also for schedules-only saves, since schedules go to their own endpoint). The backend runs a full$asset->save()for it: a new version and a modification-date bump for nothing.removeTrackedChanges(), which resets every change flag — including changes tracked while the request was in flight. Combined with (1): an empty save "succeeds", clears the flags the async draft sync just set, the query invalidation resets the draft to server values, and the user's edits vanish with a success toast.undefinedproduces e.g.metadata: undefined, which serializes to an empty payload.Proposed changes (validated on a 2026.x working branch, incl. jest specs)
collectAssetUpdateData()(testable; omits sections whose draft data isundefined).resetChangesForTypesreducer inuse-trackable-changes.ts; on success clear only the change types snapshotted at request time (removeTrackedChanges(types?), backward compatible — no argument keeps full-reset behavior).Notes
UpdateService::update()in studio-backend-bundle saves unconditionally even for an emptydatapayload — it could short-circuit, but task-based flows need a BC check; the primary fix belongs in the UI.AssetSaveDataProcessorRegistrywould close it completely.🤖 Generated with Claude Code