Repository navigation
fix(app-shell): the embedded item editor saves into the parent's draft (objectui#12027) - #12038
Merged
objectstack-fleet[bot] merged 2 commits intoOct 9, 2026
Merged
Conversation
…t (objectui#12027) EmbeddedItemEditor's save based the parent on its published layers and PUT it in publish mode. For a draft-only parent the layered read's 404 became an empty body, so the save sent a publish-mode stub of the one item and said "Saved.". The save now reads the parent's pending draft first and, when there is one, splices the item into it and writes it back with mode draft. With no draft it reads the published body as before and saves as before. With neither, it refuses with an error state and sends nothing. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…jectui#12027) Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
objectstack-fleet
Bot
deleted the
claude/issue-12027-embedded-draft-save
branch
October 9, 2026 08:12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12027
Clause-②: no
The embedded item editor ("Save into object", opened from a metadata item's Related drawer) now saves into the parent's draft. Only
EmbeddedItemEditor'sdoSavechanges. No export, prop, type member or language-pack key is added.Step 0: the live server's answer, measured
Measured on objectstack
main3054516e, from a separate objectstack worktree that has since been removed.examples/app-showcasewas booted withobjectstack dev --seed-admin --no-watchon its own port and its own SQLite file. Requests were sent two ways: directly over HTTP, and through the REAL editor with a realMetadataClientover HTTP. That second way was a one-off harness that rendered bothmain's editor and this branch's. It was never committed.1. A draft-only parent, with the request the console sends today. The request is
PUT /api/v1/meta/object/qa_sales_orderwith no query string and the body{"fields":{"amount":{"type":"number","label":"Order amount"}}}.INVALID_METADATA, with one issue at pathname: "Invalid input: expected string, received undefined" (invalid_type)./layersstill answer 404. The draft is unchanged and still in the drafts ledger. Nothing is published.main's editor, the same request gets the same 422. The banner reads "Validation failed (1 issue)." The issue pathnameis not underfields.amount., so the editor maps it onto the sub-form unchanged. In the harness's field form, that path is the item's Name control, which then showed "Required text value". So the edit is not saved, the author is shown an error that is not about the item, and every retry sends the same stub.sharingModelis also refused, by the author-time rulesecurity-owd-unset(422). So the stub would be refused even if it carried a name.2. A published parent with a pending draft, saved through
main's editor./layers(200), then sends a publish-modePUTof the effective body with the item spliced in. The server answers 200: the edit goes live at once, without the author publishing.channelfield) are kept. It still holds the OLDamountlabel, so publishing that draft later puts the old label back.This branch, measured live the same way:
GET /_drafts,GET ?state=draft, thenPUT ?mode=draft. The server answers 200 and the editor shows "Saved.". The stored draft row has the newamountlabel and keepsname,label,descriptionandregion. The published read and/layersstill answer 404.PUT ?mode=draftanswers 200. The draft keeps its renamed label and itschannelfield, and gets the newamountlabel. The published row is unchanged.GET /_drafts, then/layers(200), then aPUTwith no query string (200). These are the same request and the same body asmain's editor sends.GET /_drafts, then/layers(404), and noPUT. The banner reads "Failed to load object/qa_live_f: (not found)".nameFieldand_diagnostics. The rows stored after these saves hold only the authored keys. The client strips the read decorations (extractDraftBody), and the server strips the served object columns.What changed
doSavere-reads the parent when the author saves, as before. It now reads the parent's pending draft first, with the client's existinggetDraftandextractDraftBody, both unchanged.{ mode: 'draft' }./layers(effective, elsecode), and the save is sent exactly as before, in publish mode.engine.edit.loadFailedandengine.form.notFound. Nothing is sent and "Saved." is not shown. A missing published version never becomes{}.The one caller,
MetadataDetailDrawer, holds no parent draft, sodoSavereads it itself.MetadataDetailDrawerandRelatedPanelare untouched.Pins
New file:
EmbeddedItemEditor.draftSave-12027.test.tsx. It uses the real editor over a realMetadataClient, whose transport is an in-memory server shaped like the framework (drafts ledger, decorated?state=draft,/layers404, and a PUT that writes the draft row or the published row by mode). Six tests:Changed files, with their existing assertions unchanged:
EmbeddedItemEditor.refusalIssues-11379.test.tsx: its stub client gainsgetDraft: async () => null.EmbeddedItemEditor.layersRead-11799.test.tsx: the header's "Not covered" paragraph now points at the new file. All three of its pins stay green. This card's write side supersedes nothing in them: both of its controls have no draft, so they still reach/layers.Verification (head
3d2bcf0ab)pnpm exec vitest run --maxWorkers=2over the sixEmbeddedItemEditor.*test files gaveTest Files 6 passed (6)andTests 35 passed (35).pnpm exec vitest run --maxWorkers=2 packages/app-shell/gaveTest Files 1229 passed | 1 skipped (1230)andTests 12031 passed | 9 skipped (12040), with the VERDICT linecommand-exit 0. The lock was held for 47 minutes on a shared box.turbo run build --filter='@object-ui/app-shell^...' --concurrency=2(28 of 28 tasks), thenpnpm --filter @object-ui/app-shell type-check(tsc --noEmit && tsc -p tsconfig.test.json), exited 0.tsc -p tsconfig.test.json --listFilesOnlylists the three touched test files.pnpm exec eslint --format jsonon the four touched files: 4 files, 0 errors, 7 warnings. The 7 warnings are exactly the 7 thatmain's copy ofEmbeddedItemEditor.tsxalready has, with the same rules at shifted lines, so none is new. This narrowed run is a measurement. The population is the four touched files (the JSON count is 4).eslint.config.jssets noparserOptions.projectand noprojectService, so linting is not type-aware. Nothing imports a test file, so no untouched file's verdict can move.pnpm lintis left to CI.check:control-bytes,check:test-path-roots,check:changeset-claims,check:pending-changeset-literals,check:new-line-citations,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:metadata-write-doors,check:phantom-deps,check:unreferenced-sources,node scripts/check-changeset-presence.mjs,node scripts/check-changeset-no-major.mjs. No locale pack and no export changed, so the i18n gates andcheck:readme-exportsdo not apply.Reverse verification (one-time, not committed)
Both runs used objectstack's
scripts/ablation-replace.mjsin WRAP mode, under the lock, on the committed fix. The test resolves the editor fromsrc, so nodistleg is needed.{ mode: 'draft' }dropped from the draft-based save. The anchor count went from 1 to 0, and the blob from062bbc87to82ebe33f. Result: 3 failed / 32 passed of 35, in the predicted direction. The draft-only pin, the pending-draft pin and the draft-mode 422 pin went red. The control, the refusal and the draft-read-failure pins stayed green.?? {}fallback put back afterlayered.effective ?? layered.code. The anchor count went from 1 to 0, and the blob from062bbc87toc68f7a78. Result: 1 failed / 34 passed: exactly the refusal pin ("Unable to find … Failed to load object/sales_order: (not found)").062bbc87, andgit diff HEADis empty.Acceptance notes
main's code.If-Match) is not addressed here and remains open.EMBEDDEDPATH.ITEM.) on the sub-form unchanged. Onmain, the stub'snameissue therefore marked the item's own Name control. After this change the stub is never sent, and no other parent-level issue path was measured reaching that mapping.Session:
https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8(domain:uiseat 3 dispatch,os-devsubagent).Generated by Claude Code