Skip to content

[Data Object Editor] Restore (inherited) changes - #4026

Draft
ValeriaMaltseva wants to merge 2 commits into
2026.xfrom
feat/1045-restore-inherited-changes
Draft

[Data Object Editor] Restore (inherited) changes#4026
ValeriaMaltseva wants to merge 2 commits into
2026.xfrom
feat/1045-restore-inherited-changes

Conversation

@ValeriaMaltseva

Copy link
Copy Markdown
Contributor

Resolves #1045, the remaining unchecked item of #291.

Adds a per-field Restore action that undoes the change which broke a field's inheritance, so the field takes its value from the origin object again.

Scope

inheritanceData carries only the origin id and a flag per field, never a value, so the value a field would inherit is known only for fields that were inherited when the editor was opened. The action is therefore offered for those, and not for fields that already carried an own value — covering the latter needs a backend endpoint that returns the inherited value plus origin id, which does not exist today.

Field collections, calculated values, consent, URL slugs and reverse relations never get the action, because supportsInheritance() is false for them.

How it works

useRestoreInheritance picks the branch by where the value actually lives.

Plain fields are held by the Ant form store:

  1. form.resetFields([name]) puts the loaded (inherited) value back. Ant does not report a reset through onValuesChange, so the field is not counted as changed again.
  2. The field is separately persisted as empty, because an auto save may already have written the own value into the draft and the draft is cumulative on the server. The empty value comes from the field type (DynamicTypeObjectDataAbstract.getEmptyValue) — null for almost everything, but [] for blocks, whose adapter takes array and would throw a TypeError on null. I checked every data adapter; BlockAdapter is the only one that cannot take null.
  3. The inheritance state goes back to the entry it was loaded with.

Object bricks and the classification store hold their values in a Form.KeyedList, which the Ant form store never sees, and each encodes inherited fields in its own payload — null for bricks, absence for the store. So the owner of the list restores the field instead: it drops its own "field was changed" bookkeeping and re-emits the payload, and the displayed value fixes itself because getMergedValue then reads the field from the loaded data. A synchronous marker (restoredFieldsRef, mirroring the existing changedFieldsRef) is needed because restoring the inheritance state only schedules a React update, which the payload built right after must not wait for.

KeyedList gains one optional onFieldRestore prop, the counterpart of the existing onFieldChange. It is the channel the label uses to reach the owner. components/pipeline/* passes nothing and is unaffected.

Blocks

Block inheritance is all-or-nothing at field level — BlockAdapter does not implement DataInheritanceInterface, so the generic path yields one state for the whole block field. Restore therefore sits on the block's own label, not on its inner fields, which matches that granularity.

Drive-by fix

getMergedValue dropped the inherited values of a classification store group once that group held no own values any more. filterInheritedFields deliberately encodes such a group as an empty list, getFieldList reports that as a field of its own, and the merge then wrote over the values already merged in for the keys of the group. Restoring the last own key of a group made this reachable. getMergedValue had no tests; it has three now.

Verified

  • tsc --noEmit, eslint, and the jest suite (562 tests) pass; npm run dev-sdk builds.
  • New tests: the break → restore cycle in the inheritance state provider incl. a localized path, the payload shape (nested locales, siblings kept, no mutation), both branches of useRestoreInheritance, and the getMergedValue regression.
  • Not verified in a browser. Worth a look at: a plain field, a localized field (only the shown locale must change), a block (the save must not fail), an object brick, and a classification store key — each restored and then the tab reloaded, which is what proves the field was persisted as empty.

Follow-ups, not in here

  • Restoring a field whose own value was already persisted before the editor was opened (needs the backend endpoint above).
  • A store-level Restore for a whole classification store field: inheritance there is tracked per group × language × key, and reverting the field would also have to undo added and removed groups (activeGroups, groupCollectionMapping, DELETED markers). Different semantics, and destructive enough to want a confirmation.
  • Two latent issues found while reading, deliberately left alone: getMergedValue in group-value.ts throws if originalValue holds a group that the current value does not (the sibling brick-value.ts guards this); and DataComponent builds virtualFieldName without the Form.List path, so a field inside a block can pick up the inheritance overlay of a same-named top-level field.

Adds a per-field Restore action that undoes the change which broke a
field's inheritance, so the field takes its value from the origin object
again.

The value a field would inherit is only known for fields that were
inherited when the editor was opened - inheritanceData carries the origin
id and a flag, never a value - so the action is offered for those and not
for fields that already carried an own value.

Where the value lives decides how it is put back:

- Plain fields are held by the Ant form store, so the form resets them to
  the loaded value and the field is separately persisted as empty, since
  an auto save may already have written the own value into the draft. The
  empty value comes from the field type, because the block API takes a
  list of items and rejects null.
- Inside a Form.KeyedList - object bricks and the classification store -
  the value is held by the list, and each of the two encodes inherited
  fields in its own payload (null for bricks, absence for the store), so
  its owner drops its own bookkeeping for the field and re-emits the
  payload. A synchronous marker is needed there because restoring the
  inheritance state only schedules a React update.

Also fixes getMergedValue dropping the inherited values of a
classification store group once that group holds no own values any more.
filterInheritedFields encodes such a group as an empty list, which
getFieldList reports as a field of its own and the merge then wrote over
the values merged in for the keys of the group. Restoring the last own
key of a group made that reachable.

Verified: tsc, eslint and the jest suite pass, and the SDK builds. Not
verified in a browser.

Resolves #1045

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ValeriaMaltseva ValeriaMaltseva added this to the 2026.3.0 milestone Aug 25, 2026
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds per-field inheritance restoration to the Data Object Editor.

Changes:

  • Adds Restore UI, translations, and inheritance-state handling.
  • Supports plain fields, blocks, object bricks, and classification stores.
  • Fixes classification-store merging and adds tests.

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
translations/studio.sv.yaml Adds Swedish label.
translations/studio.no.yaml Adds Norwegian label.
translations/studio.it.yaml Adds Italian label.
translations/studio.fr.yaml Adds French label.
translations/studio.es.yaml Adds Spanish label.
translations/studio.en.yaml Adds English label.
translations/studio.de.yaml Adds German label.
dynamic-type-object-data-block.tsx Defines block empty value.
use-restore-inheritance.ts Implements restore behavior.
use-restore-inheritance.test.tsx Tests restore branches.
field-label.tsx Displays Restore action.
dynamic-type-object-data-abstract.tsx Adds empty-value API.
object-brick.tsx Restores brick fields.
group-value.ts Fixes inherited group merging.
group-value.test.ts Tests group merging.
classification-store.tsx Restores classification keys.
inheritance-state-provider.tsx Adds restore state transition.
inheritance-state-provider.test.tsx Tests state restoration.
clear-form-field.ts Builds cleared payloads.
clear-form-field.test.ts Tests cleared payloads.
edit-form-provider.tsx Exposes field-clearing support.
restore-inheritance-button.tsx Adds translated Restore button.
keyed-list-provider.tsx Exposes restore callback.
keyed-list.tsx Routes field restoration.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// field has to be persisted as empty for the backend to resolve it from the
// parent again.
editFormContext?.clearDataObjectAttribute(name, emptyValue)
editFormContext?.updateDraft().catch((error) => { console.error(error) })

export const InheritanceStateContext = React.createContext<IInheritanceStateContext | undefined>(undefined)

const getStateKey = (name: NamePath): string => Array.isArray(name) ? name.join('.') : name.toString()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Data Object Editor] Restore (inherited) changes

2 participants