Skip to content

fix(app-shell): creating an item or opening one from Pending changes asks before it drops a held Studio edit - #26927

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-fu4yi5
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-fu4yi5

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread

Fixes #26845

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 的 Data、Automations、Interfaces 支柱有暂存未发的编辑时,点「新建对象/流程/仪表盘等」会先弹出同一句离开确认;从 Pending changes 里打开同一支柱的另一个对象也先问。取消则对话框不打开、暂存编辑保留。
  • 为什么改:fix(app-shell): an edit a Studio pillar holds unsent guards departure like a dirty pillar #26839 只拦了离开支柱、左侧栏切换和刷新;新建和 Pending changes 跳转同样会换掉暂存所在的缓冲区,之前一点就静默丢失。分诊裁决是「两条路径都走同一个离开守卫」。
  • 风险与回滚:只多问一句,复用 confirmDropUnsavedWork 与原有文案,不改保存、发布和暂存判定。有问题直接 revert 本 PR。
  • 先看:StudioDesignSurface.tsx 中 DataPillar 挪到 objHeld 之后的 live request effect(唯一的行为改动点),然后三处新建入口(openCreate、openCreateObject、openCreateFlow),最后 StudioDesignSurface.heldDataCreate-26845.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/StudioDesignSurface.heldDataCreate-26845.test.tsx,预期 3 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

Base 94e29cd1. One shared guard, now read by the two departures #26705 left open:

  • Create while held. Each pillar asks confirmDropUnsavedWork before its New dialog opens, because a created object, flow or Interfaces item is opened at once (setCurrent in doCreateObject, doCreateFlow and doCreateSurface), and that load replaces the one buffer the held edit lives in. Asking before the dialog means a decline costs nothing and the create itself never asks again.
    • Interfaces: openCreate (the "+" menu's New dashboard / report / page) reads leafHeld.
    • Data: a new openCreateObject, declared below objHeld, wraps openCreateDialog for the rail's New and the empty state's New (the empty state can hold nothing; it goes through the same door so every create asks the same way).
    • Automations: openCreateFlow reads flowHeld.
  • Same-pillar Pending changes request. Measured: the Data pillar is the only subscriber of the live surface channel (useRequestedSurface), and the sheet's security block is the only producer, so a same-pillar request is always an object request applied by the Data pillar. Its apply effect moves below objHeld and asks, like the rail, only when the requested object is not the open one. Declined, the request id is spent (as before, at most once) and the held edit stays. A request for the open object asks nothing.
  • Cross-pillar request: unchanged. requestSurface already asks through confirmLeavePillar (which reads heldEdit since fix(app-shell): an edit a Studio pillar holds unsent guards departure like a dirty pillar #26839) and vetoes on a decline; the Data pillar it lands on holds nothing, so its apply asks nothing more. Pinned below, not changed.
  • Unchanged by design: how a held edit is judged and saved, Publish gating, and the question's wording (engine.edit.unsavedLeaveConfirm, no new i18n key). The Access pillar is not touched.

Why

Triage first grade on the card, verbatim: "p2, the remainder of #26705 (PR #26839): two more departures still drop a held pillar edit without asking: creating a new object, flow or Interfaces item, and opening another item from Pending changes. Route both through the same departure guard."

Tests

Three new files, one per pillar, each through the real StudioDesignSurface on the real route shape, with the held edit produced as #26839's files produce it (a Picklist with no option, a Notify step with no Title, a Metric widget with no dataset). The Data and Automations files replace the mocked Pending changes sheet with a stand-in that asks the host for an object through useSurfaceNavigator, as the sheet's security block does.

  • StudioDesignSurface.heldDataCreate-26845.test.tsx (3): New object asks, Cancel opens no dialog and keeps the held field, OK opens the dialog and the create (Ticket, stored as acme_ticket) asks nothing more; a Pending changes request for another object asks, Cancel keeps the held field, a request for the open object asks nothing, OK opens the other object; CONTROL with nothing held, New object and a request never ask.
  • StudioDesignSurface.heldFlowCreate-26845.test.tsx (3): New flow asks, Cancel opens no dialog and keeps the held step, OK opens the dialog; a cross-pillar request (an object, from Automations while a step is held) asks exactly once, Cancel stays on Automations with the held step, OK opens the Data pillar on that object with no second ask; CONTROL with nothing held.
  • StudioDesignSurface.heldWidgetCreate-26845.test.tsx (2): New dashboard asks, Cancel opens no dialog and keeps the held widget, OK opens the dialog; CONTROL with nothing held.
Runs (head 3a6924e)
  • New files: 3 files, 8/8 passed (lock VERDICT command-exit 0; tests 3.64 s of wall in total).
  • Every test file under ui/packages that imports StudioDesignSurface, DataPillar, AutomationsPillar or InterfacesPillar (161 files, DataPillar.surfaceRequest.test.tsx and the three fix(app-shell): an edit a Studio pillar holds unsent guards departure like a dirty pillar #26839 files among them): 161/161 files, 1179/1179 tests passed, lock VERDICT command-exit 0.
  • pnpm --filter @object-ui/app-shell run type-check (both projects, tests included): exit 0.
  • eslint on the four files: 0 errors; StudioDesignSurface.tsx carries 16 warnings, as on the base (the moved effect's set-state-in-effect warning moves with it; the count is unchanged), the three tests none.
Ablation (one-shot, through scripts/ablation-replace.mjs --delete; each leg restored to the HEAD blob with an empty git diff HEAD)
  • Leg 1, drop the Interfaces create guard (leafHeld): heldWidgetCreate 1 of 2 red, the create case only.
  • Leg 2, drop the Data create guard (openCreateObject): heldDataCreate 1 of 3 red, the New object case only.
  • Leg 3, drop the Data request guard (in the apply effect): heldDataCreate 1 of 3 red, the Pending changes case only.
  • Leg 4, drop the Automations create guard (flowHeld): heldFlowCreate 1 of 3 red, the New flow case only. Its cross-pillar case pins the existing surface guard and stays green, as intended.

Acceptance notes

  • The Interfaces "+" menu's other kinds (link, group, and the rest) add a nav entry and open nav editing without changing the open leaf, so they do not replace the leaf buffer and are not guarded here.
  • Only the Data pillar applies a same-pillar live request today; the Automations and Access pillars do not subscribe to the channel, so a same-pillar request there opens nothing and drops nothing.
  • origin/main gained 57820085 (fix(metadata-protocol): a draft's _diagnostics reports what its publish will refuse #26856, metadata-protocol only) after the base; it touches none of these files or the derived gate inputs, so it was not merged in.
  • @object-ui/app-shell is private: true, so no changeset.

Verification

Gates derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 3a6924e3: 24 families, all exit 0, reconciled with --ran (24 run, 0 NOT-MEASURED, 0 UNRUN). pnpm --dir ui exec vitest run scripts/ first queued out of the lock (exit 99, not run) while the 161-file run held it, then ran in full under the same slot: 178 files passed, 2 skipped, exit 0. pnpm check:new-line-citations (ui): 0 new citations.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint on the four files: 0 errors.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (Publishes nothing: @object-ui/app-shell is private.)
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English, apart from the maintainer brief above.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…asks before it drops a held Studio edit

A Data, Automations or Interfaces pillar now asks the shared departure
question before its New dialog opens while its autosave holds an edit, and
the Data pillar asks before a Pending changes request opens another object
in the same pillar. Declined, the held edit stays.

Claude-Session: https://claude.ai/code/session_01JQwxbZTZSxa584VWULEuMZ
Co-Authored-By: Claude <noreply@anthropic.com>
… held Studio edit now guards

Per pillar: create while held asks (declined, no dialog and the held edit
stays; accepted, the dialog opens and the create asks nothing more), the
Data pillar's same-pillar Pending changes request asks, a cross-pillar
request asks once, and with nothing held nothing asks.

Claude-Session: https://claude.ai/code/session_01JQwxbZTZSxa584VWULEuMZ
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#1 · session_01JQwxbZeBv2y91towxasSUT · head 3a6924e3 · 2026-10-27T22:01Z

席位意见:三个支柱的「新建」在打开对话框前先走共享守卫(取消即无代价);数据支柱的「待发布更改」跳转只在要打开另一个对象时才问。跨支柱跳转仍由原有的离开守卫只问一次。

  1. Shape: Fixes #26845; 4 files, +744/−15; source is StudioDesignSurface.tsx +56/−15, with three new per-pillar test files. No changeset (app-shell is private).
  2. Premise measured: the seat read the source diff on the head. openCreate (Interfaces, leafHeld), the new openCreateObject (Data, objHeld, wired to the rail "+" and the empty-state button) and openCreateFlow (Automations, flowHeld) each call confirmDropUnsavedWork before any dialog state changes. The Data pillar's live-request effect moved below objHeld unchanged except for the guard; the appliedRequestRef id check still applies each request once, so the widened dependency list does not re-ask. A request for the open object asks nothing. Dev: 3 new files 8/8; 161 affected files 1179/1179; 4 ablation legs each red on exactly its case, restored.
  3. Gates / check-runs: PR check-runs on 3a6924e3: 42 success, 14 skipped, 1 failure. The failure is the opened run's Check Changeset (21:46:10Z), which fired before skip-changeset landed (21:49:20Z); the labeled run's Check Changeset (run 40589937504, 21:49:31Z) is skipped as exempt, and the PR reads mergeable_state: clean. Dev's local run: 24/24 derived gates exit 0, --ran 0 NOT-MEASURED; app-shell type-check exit 0.
  4. Accepted deviations: skip-changeset was left out of pr_create and added afterwards through label-write.mjs (relay run 40589865776), as os-dev.md routes a late label; that makes api_writes 3, not the report's 2 (the dev's note to the seat). The Access region (seat 4's fix(app-shell): Studio Access refuses a new permission set over a taken identifier and can discard a set's draft #26916) is untouched.
  5. Out of scope: none filed. Skills-docs: none — UI guard only.
  6. Landing: not Tier H; the seat readies and arms auto-merge.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants