Skip to content

feat(dashboard): Send only changed fields on detail page updates#4916

Open
grolmus wants to merge 4 commits into
vendurehq:minorfrom
grolmus:mgrolmus/oss-567-dashboard-form-engine-resubmits-full-payload-allowing
Open

feat(dashboard): Send only changed fields on detail page updates#4916
grolmus wants to merge 4 commits into
vendurehq:minorfrom
grolmus:mgrolmus/oss-567-dashboard-form-engine-resubmits-full-payload-allowing

Conversation

@grolmus

@grolmus grolmus commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

The dashboard detail-page form engine now submits only the fields the user actually changed on update, instead of resending the entire form payload. This closes a silent data-loss vector: previously, a field left untouched by admin A would be resubmitted at its stale page-load value and could overwrite a concurrent change made by admin B (or an API integration) — no error, success toast.

Design was signed off by @michaelbromley on OSS-567 (targeting minor). This is Tier 1 of his framing (don't send untouched fields); Tier 2 (touched replace-set element conflicts, beyond the merged stock fix) and Tier 3 (optimistic concurrency) remain separate.

Root cause

useGeneratedForm mapped the whole entity into the form and called onSubmit with the entire payload on every save; useDetailPage forwarded it to the update mutation. For replace-semantics fields (facetValueIds, assetIds, scalar stock config, channelIds, prices, promotion conditions/actions, …) the stale value replaced whatever was there.

Change

  • form-engine/utils.tsdeepEqual (primitives incl. NaN, Date, arrays, plain objects) and getChangedTopLevelFields(submitted, baseline, fields): the top-level keys that differ from the baseline, plus any input field that is non-nullable (FieldInfo.nullable === false, e.g. id, UpdateShippingMethodInput.translations) which is always sent.
  • useGeneratedForm — computes changedFields by deep-comparing the raw submitted values against the form baseline (the values memo) and passes it via a new, backward-compatible 2nd meta arg to onSubmit. RHF dirtyFields is deliberately not used (array-item removal reads as clean → would drop the user's own deletion; assetIds/featuredAssetId are written via setValue with no Controller).
  • useDetailPage — on the update path only, prunes the payload to changedFields before transformUpdateInput. Create path is unchanged. Adds a sendAllFieldsOnUpdate?: boolean opt-out. Composes with the existing getChangedStockLevels transform (fix(dashboard): only send edited stock levels on variant update #4834).
  • No change to transformUpdateInput's signature (avoids the generic-inference hazard from the OSS-554 fix).

Whole-key granularity: a changed top-level key is sent in full (the entire translations array, facetValueIds, customFields object), preserving the "full translation objects per language" invariant.

Behaviour change / extensions

Update mutations move from full-snapshot to patch-of-changed-fields for all detail pages, including third-party dashboard extensions built on useDetailPage. Core update inputs are patch-style by design, so this is safe for core. Extensions whose custom update mutation relies on receiving unchanged fields can set sendAllFieldsOnUpdate: true.

Test plan

Unit (form-engine/utils.spec.ts, 48 pass): deepEqual (NaN, Date, nested arrays/objects); getChangedTopLevelFields — changed scalar kept, untouched pruned, array add/remove, key deletion, nested translation/customFields, non-nullable always kept, undefined-baseline↔value transitions, missing non-nullable field doesn't throw.

E2E (payload assertions, the strongest proof):

  • catalog/product-variants.spec.ts — edit only the SKU → update input is exactly { id, sku } (facetValueIds/assetIds/trackInventory/translations/price omitted). 11/11.
  • catalog/products.spec.ts — edit only the name → update input is exactly { id, translations }. 16/16.

Typecheck clean; existing catalog suites green (no regression).

Relates to OSS-567.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Summary by CodeRabbit

  • Tests

    • Added comprehensive end-to-end tests for collections, products, and product variants to verify that update requests include only the fields modified by the user.
  • Chores

    • Enhanced the form engine to optimize update payloads by sending only changed fields (with an option to revert to sending all fields if needed).

The form engine (useGeneratedForm) submitted the entire mapped payload on
every save, so replace-semantics fields (facetValueIds, assetIds, scalar stock
config, etc.) resent their stale page-load value and could silently overwrite a
concurrent edit made by another admin or via the API.

Compute the set of top-level fields the user actually changed by deep-comparing
the submitted values against the form baseline (plus any non-nullable input
field, which is always sent), and prune update-mutation payloads to those fields
in useDetailPage. RHF dirtyFields is not used as the source (array-item removal
reads as clean; setValue-driven fields have no Controller). Update inputs are
patch-style, so omitting untouched fields leaves them unchanged. Adds a
sendAllFieldsOnUpdate opt-out for update mutations that need the full payload.

Relates to OSS-567
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vendure-storybook Ready Ready Preview, Comment Jul 15, 2026 12:52pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e8fbdee4-31ed-4b15-93fa-faee9fe945f5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds deepEqual and getChangedTopLevelFields utilities to compute changed top-level form fields against a baseline entity. useGeneratedForm now passes computed changedFields metadata to onSubmit. useDetailPage gains a sendAllFieldsOnUpdate option to prune update payloads to only changed fields. E2e tests verify minimized update mutations for products, variants, and collections.

Changes

Send only changed fields on update

Layer / File(s) Summary
Deep equality and diff utilities
packages/dashboard/src/lib/framework/form-engine/utils.ts, utils.spec.ts
Adds deepEqual (NaN-safe recursive equality) and getChangedTopLevelFields (computes dirty keys plus forced non-nullable fields), with unit tests covering scalars, arrays, nested objects, dates, and edge cases.
Generated form submit metadata
packages/dashboard/src/lib/framework/form-engine/use-generated-form.tsx
Adds GeneratedFormSubmitMeta interface and extends onSubmit to receive changedFields, computed by diffing raw submitted values against the form baseline when editing an existing entity.
Detail page update pruning
packages/dashboard/src/lib/framework/page/use-detail-page.ts
Adds sendAllFieldsOnUpdate option to DetailPageOptions; update submissions now prune filteredValues to meta.changedFields before transforming, unless the option is enabled.
E2e tests for minimized payloads
packages/dashboard/e2e/tests/catalog/products.spec.ts, product-variants.spec.ts, collections.spec.ts
Adds Playwright tests (OSS-567) verifying that editing a single field on product, variant, and collection detail pages sends update mutations containing only the id and changed fields.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant GeneratedForm as useGeneratedForm
  participant Utils as getChangedTopLevelFields
  participant DetailPage as useDetailPage
  participant API as GraphQL API

  User->>GeneratedForm: edit single field, submit
  GeneratedForm->>Utils: diff rawValues vs baseline
  Utils-->>GeneratedForm: changedFields set
  GeneratedForm->>DetailPage: onSubmit(processed, meta)
  DetailPage->>DetailPage: prune filteredValues to changedFields
  DetailPage->>API: send Update mutation (id + changed fields only)
  API-->>DetailPage: success response
  DetailPage-->>User: show updated toast
Loading

Possibly related PRs

  • vendurehq/vendure#4339: Both PRs modify the same useGeneratedForm submit wrapper in use-generated-form.tsx, adjacent to the changed-field metadata logic added here.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, specific, and accurately summarizes the main change: sending only changed fields on detail-page updates.
Description check ✅ Passed The description covers the summary, related issue, root cause, implementation, behavior impact, and test plan, though some template sections are implicit.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Adds an e2e payload assertion on the collection detail page (configurable-
operation filters array, no update transform) confirming that editing only the
name submits just id + translations and omits the filters replace-array.

Relates to OSS-567

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (3)
packages/dashboard/e2e/tests/catalog/product-variants.spec.ts (1)

380-384: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Comment format deviates from required #ISSUE_NUMBER convention.

Uses OSS-567 instead of #ISSUE_NUMBER, inconsistent with the #4478/#4608 style already used elsewhere in this same file. As per path instructions, comments above Dashboard E2E tests should be formatted // #ISSUE_NUMBER — description.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/e2e/tests/catalog/product-variants.spec.ts` around lines
380 - 384, The inline comment above the variant update test uses the wrong issue
tag format, so update the leading marker in the catalog product-variants E2E
test to match the required `#ISSUE_NUMBER` convention used elsewhere in this file.
Locate the comment near the variant update describe block and change the OSS-567
style prefix to the Dashboard test comment format while keeping the descriptive
text intact.

Source: Path instructions

packages/dashboard/e2e/tests/catalog/products.spec.ts (1)

124-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Comment format deviates from required #ISSUE_NUMBER convention.

The comment uses OSS-567 instead of #ISSUE_NUMBER. As per path instructions, Dashboard E2E test comments should be formatted as // #ISSUE_NUMBER — description.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/e2e/tests/catalog/products.spec.ts` around lines 124 -
128, The test comment in products.spec.ts uses the wrong issue tag format;
update the leading identifier in the describe block comment from OSS-567 to the
required `#ISSUE_NUMBER` convention. Locate the comment above
test.describe('product update sends only changed fields (OSS-567)') and change
only the issue reference so it matches the Dashboard E2E format while keeping
the rest of the description unchanged.

Source: Path instructions

packages/dashboard/e2e/tests/catalog/collections.spec.ts (1)

370-374: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Comment format deviates from required #ISSUE_NUMBER convention.

As per path instructions, comments above Dashboard E2E tests should be formatted // #ISSUE_NUMBER — description; this uses OSS-567 without the # prefix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/e2e/tests/catalog/collections.spec.ts` around lines 370 -
374, The test description comment in collections.spec.ts uses the wrong
issue-tag format; update the OSS-567 marker to the required Dashboard E2E
convention with a leading #. Keep the surrounding explanatory text the same, and
ensure the comment above the test.describe block follows the `// `#ISSUE_NUMBER` —
description` style consistently.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/dashboard/e2e/tests/catalog/collections.spec.ts`:
- Around line 370-374: The test description comment in collections.spec.ts uses
the wrong issue-tag format; update the OSS-567 marker to the required Dashboard
E2E convention with a leading #. Keep the surrounding explanatory text the same,
and ensure the comment above the test.describe block follows the `//
`#ISSUE_NUMBER` — description` style consistently.

In `@packages/dashboard/e2e/tests/catalog/product-variants.spec.ts`:
- Around line 380-384: The inline comment above the variant update test uses the
wrong issue tag format, so update the leading marker in the catalog
product-variants E2E test to match the required `#ISSUE_NUMBER` convention used
elsewhere in this file. Locate the comment near the variant update describe
block and change the OSS-567 style prefix to the Dashboard test comment format
while keeping the descriptive text intact.

In `@packages/dashboard/e2e/tests/catalog/products.spec.ts`:
- Around line 124-128: The test comment in products.spec.ts uses the wrong issue
tag format; update the leading identifier in the describe block comment from
OSS-567 to the required `#ISSUE_NUMBER` convention. Locate the comment above
test.describe('product update sends only changed fields (OSS-567)') and change
only the issue reference so it matches the Dashboard E2E format while keeping
the rest of the description unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b4879d6c-3cb9-4567-b790-b322df1524fe

📥 Commits

Reviewing files that changed from the base of the PR and between 2b58429 and 5a1a980.

📒 Files selected for processing (7)
  • packages/dashboard/e2e/tests/catalog/collections.spec.ts
  • packages/dashboard/e2e/tests/catalog/product-variants.spec.ts
  • packages/dashboard/e2e/tests/catalog/products.spec.ts
  • packages/dashboard/src/lib/framework/form-engine/use-generated-form.tsx
  • packages/dashboard/src/lib/framework/form-engine/utils.spec.ts
  • packages/dashboard/src/lib/framework/form-engine/utils.ts
  • packages/dashboard/src/lib/framework/page/use-detail-page.ts

@grolmus grolmus requested a review from michaelbromley July 3, 2026 12:08
…hboard-form-engine-resubmits-full-payload-allowing
@grolmus

grolmus commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

CI fix: dashboard e2e — test pollution of shared seed data

Symptom. dashboard e2e (shard 2) failed on translation-placeholders.spec.ts:

expect(locator).toHaveValue(expected)
Expected: "Laptop"
Received: "Laptop OSS567 1784111122452"

Root cause. The new products test added in this PR (product update sends only changed fields (OSS-567)) navigated to the shared seed Laptop product, renamed it to Laptop OSS567 <timestamp> and saved. Dashboard e2e tests run in parallel against a single shared server DB within a shard, so this permanently mutated the Laptop product — and translation-placeholders.spec.ts asserts the Laptop name stays "Laptop". Classic cross-spec pollution.

Notably, this PR's collections OSS-567 test already does it the right way: it creates a dedicated collection via VendureAdminClient in beforeAll and deletes it in afterAll. The products test just didn't follow that pattern.

Fix. Bring the products test in line with the collections one:

  • Create a dedicated product via VendureAdminClient in beforeAll (seeded with a non-empty facetValueIds), edit that product by id, and delete it in afterAll.
  • Seeding a non-empty facetValueIds also strengthens the assertion: it now proves the engine omits an unchanged non-empty replace-array from the update payload (the actual clobber-prevention behaviour), not merely that an empty one is absent.

No change to product/framework code — this is purely test isolation.

Verification. Ran products.spec.ts + translation-placeholders.spec.ts together locally (same shared-DB scenario, parallel workers): 21/21 pass, including the OSS-567 test and the previously-failing translation-placeholders test. Before the fix, translation-placeholders failed with the polluted name.

Note — unrelated flake

The e2e (mariadb) failure is default-scheduler-plugin.e2e-spec.ts:155 (expected "spy" to be called 2 times, but got 1 times) — a timing-sensitive scheduler test in core, unrelated to this PR's changes (1968/1972 passed). Should clear on re-run.

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.

1 participant