Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

fix(app-shell): refuse a toolbar flow's row count before its dialog, and translate the newTabUrl refusal (objectui#13397) - #13428

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-13397-flow-row-guard
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-13397-flow-row-guard

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Closes #13397
Clause-②: no

What changed

A record-scoped type: 'flow' action launched from a list toolbar with 0 or 2+ rows selected is now refused before its param dialog opens. The toast is in the session's language and nothing is sent. With exactly one row the flow behaves as before. This is the flow-action half that objectui#13384 (PR #13395) left out.

  • useConsoleActionRuntime flowHandler: the registered handler now carries refuseBeforeInput, the optional ActionRunnerHandler member from PR fix(core): refuse a single-record action's row count before its dialog, and translate the refusal (objectui#13384) #13395. It is serverActionHandler's own member: core's createServerActionHandler builds it and the console wrapper forwards it. That handler is built in this hook with no resolveRecordId, so the member runs resolveServerActionRecordId, the resolver the flow dispatch already runs. It also skips an action whose dialog itself collects recordId. No second copy of the rule is written. The dispatch itself is renamed flowDispatch. flowHandler wraps it in a useMemo with the member attached.
  • Dispatch-time refusal: a direct flowHandler call refused by the resolver now shows the refusal through its errorKey (t(errorKey, { defaultValue: error })), with the English as fallback for an empty answer. These are the same pack keys the runner's translator reads for the early refusal.
  • createConsoleServerActionHandler newTabUrl fast path:
    • The "no record id available" refusal moves to a pack key, console.serverAction.noRecordId, through the wrapper's existing t.
    • The resolver's own refusal on that path is shown through its errorKey too.
    • With no t, both stay English, as before.
  • @object-ui/i18n: console.serverAction.noRecordId is added to all ten packs, each with its own translation.
  • The ConsoleActionRuntime.flowHandler type now also declares refuseBeforeInput, as Pick of ServerActionHandler.
  • One changeset: app-shell and i18n, patch.

Tests

Run on head 90f61b9e7c:

  • pnpm exec vitest run --maxWorkers=2 over every test file that names flowHandler, refuseBeforeInput, resolveServerActionRecordId, newTabUrl or consoleServerAction (git grep: 28 files outside i18n), plus all of packages/i18n/ (the key-parity suites included). Result: 114/114 files, 1713 passed, 13 skipped.
  • New useConsoleActionRuntime.flowRowGuard-13397.test.tsx. The session reads zh-CN: the translator answers from the shipped zh pack.
    • flowHandler.refuseBeforeInput, 0 and 2 rows: it returns the resolver's error with actions.selectRowFirst / actions.selectExactlyOneRow. With 1 row it returns nothing.
    • Controls: an object-level toolbar flow with 0 rows is not refused. A dialog that collects recordId is not judged early.
    • Through ConsoleActionRuntimeProvider and a real runner, 0 and 2 rows: no param dialog opens, and no request is sent. The toast reads 此操作针对单条记录,请先选择一行。 / 此操作针对单条记录,请只选择一行。.
    • 1 row: the dialog opens, as today. Control: an object-level toolbar flow with 0 rows still opens its dialog.
    • A direct call with 0 and 2 rows returns the zh-CN refusal, and nothing is sent.
  • consoleServerAction.test.tsx, a new newTabUrl block:
    • No record id: the refusal reads 此操作针对单条记录,但没有可用的记录 ID。 and no tab opens. Control: with no t it stays English.
    • 2 rows: the resolver's refusal reads zh-CN. Control: 1 row still opens the tab.
  • New consoleNoRecordId-13397.test.ts (i18n): pins the zh-CN and en text. Every other pack defines the key with its own text.
  • Type-check, NODE_OPTIONS=--max-old-space-size=8192 pnpm exec turbo run type-check --concurrency=2 --filter=@object-ui/app-shell --filter=@object-ui/i18n: 31/31 tasks. That includes tsc -p tsconfig.test.json for both packages. The test-file tsconfig was re-run on the final head.
  • pnpm check:i18n-keys, check:i18n-drift, check:i18n-dead-keys: exit 0.
  • eslint on the touched files: 0 errors. No warning is added: the counts match origin/main for the two edited files that had warnings, and the new test files have none.
  • check:new-line-citations: 0 new. check-changeset-presence: pass.

Ablation

Each run used ablation-replace with on-disk proof, and was restored to git diff HEAD empty. Each ran the new flow suite, consoleServerAction.test.tsx and useConsoleActionRuntime.test.tsx (92 tests).

Mutation Result
flowHandler's refuseBeforeInput set to undefined 4 red: both member cases (0 and 2 rows) and both end-to-end cases (0 and 2 rows). The 1-row and control cases stay green.
Dispatch-time errorKey translation dropped in flowDispatch 2 red (the direct-call cases, 0 and 2 rows)
newTabUrl "no record id" back to the hard-coded English 1 red (the zh-CN case). The English control stays green.
newTabUrl resolver refusal shown without its errorKey 1 red

Acceptance notes

  • Judgment call: the flow member reuses serverActionHandler.refuseBeforeInput rather than calling core directly. Core's collectsRecordIdParam is not exported, and core is outside this card's surface. Reusing the member keeps one copy of the rule. The coupling holds while this hook builds serverActionHandler with no resolveRecordId. The member pins in the new suite compare the flow's answer with resolveServerActionRecordId's, so a change there goes red.

  • Out of scope, still open: core's own dispatch-time refusal (createServerActionHandler, for a caller that invokes the handler directly, outside the runner) still returns the English error, because its result carries no errorKey. Through the runner it is never reached for a row-count refusal, because the early refusal comes first. Carrier: none.

  • RecordDetailView's copy of the flow handler is not touched. It resolves against its own record, so a row count never applies there.

  • Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer)

🤖 Generated with Claude Code

https://claude.ai/code/session_01WJozybDmRH8z2c5SD7vJkj

…and translate the newTabUrl refusal (objectui#13397)

The console flowHandler gains refuseBeforeInput, the early row-count
refusal objectui#13384 gave the script handler, answered by the same
resolver its dispatch runs. A dispatch-time refusal is shown through its
errorKey. The console server-action wrapper's newTabUrl "no record id"
refusal moves to console.serverAction.noRecordId in all ten packs.

Claude-Session: https://claude.ai/code/session_01WJozybDmRH8z2c5SD7vJkj
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 78 chunks) 3249.1 KB 3744.1 KB
Main entry chunk (gzip) 82.1 KB 350 KB
Entry file index-CgyXvbTP.js —
Status PASS —

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

Package Size Gzipped
app-shell (buildIdentity.js) 0.80KB 0.49KB
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 21.41KB 7.90KB
app-shell (runtime-config.js) 24.52KB 8.48KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 45.59KB 12.03KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.67KB 6.12KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.74KB 3.94KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 42.84KB 11.66KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 614.88KB 150.53KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 253.04KB 70.68KB
fields (index.js) 294.86KB 75.54KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.17KB 11.96KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (ServerReadScope.js) 4.03KB 1.82KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.99KB 0.43KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.91KB
plugin-calendar (index.js) 55.41KB 16.19KB
plugin-charts (index.js) 88.30KB 24.33KB
plugin-chatbot (index.js) 237.89KB 56.23KB
plugin-dashboard (index.js) 147.15KB 39.95KB
plugin-designer (index.js) 232.83KB 50.01KB
plugin-detail (index.js) 267.21KB 71.14KB
plugin-editor (index.js) 2.21KB 1.04KB
plugin-form (index.js) 185.46KB 48.48KB
plugin-gantt (index.js) 178.19KB 45.06KB
plugin-grid (index.js) 278.30KB 77.57KB
plugin-kanban (index.js) 64.27KB 19.81KB
plugin-list (index.js) 149.60KB 39.26KB
plugin-map (index.js) 27.11KB 8.99KB
plugin-markdown (index.js) 13.79KB 4.79KB
plugin-report (index.js) 46.42KB 13.06KB
plugin-timeline (index.js) 39.30KB 11.87KB
plugin-tree (index.js) 15.25KB 5.38KB
plugin-view (index.js) 94.26KB 24.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 129.13KB 42.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.71KB 2.11KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (product-blocks.js) 0.20KB 0.19KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT — objectui#13397 (p3, S), head 90f61b9e7c2e42535446b8e82a50bf891fad4cf8, reviewed 2026-10-23T21:43Z.

Seat: domain:ui#1 · Clause-②: no

项 读数
形态 16 个文件,+382/−8:useConsoleActionRuntime.tsx、consoleServerAction.ts、10 个语言包各加一个键 console.serverAction.noRecordId、三个测试、一个 changeset(app-shell、i18n patch)。NOT GOVERNED。均在认领申报的文件面内。与当前 main 无冲突(本次读取时 git merge-tree 无冲突)。ConsoleActionRuntime 不在包的公开导出里,flowHandler 类型加上 refuseBeforeInput 不算公开变形。
改动核对 读了 diff。原来的派发改名 flowDispatch;注册的 flowHandler 用 useMemo 包它,并带上 serverActionHandler.refuseBeforeInput,即 objectui#13384 由 core 构造、控制台转发的同一个成员。这里构造该 handler 时不传 resolveRecordId,所以它跑的正是 flowDispatch 用的 resolveServerActionRecordId,参数框自己收 recordId 时也照样跳过;规则只有一份。直接调用时的派发期拒绝改走 errorKey 翻译,英文作缺省。newTabUrl 的「no record id available」改为新键,解析器在这条路径上的拒绝也走它的 errorKey。
钉测与门禁 真实 runner 经 ConsoleActionRuntimeProvider:0 行或 2 行不开参数框、不发请求、提示为 zh-CN;1 行照旧开框。成员本身返回解析器的错误和键;直接调用返回 zh-CN 拒绝;newTabUrl 两种拒绝都读 zh-CN;i18n 测试钉住新键的中英文,并检查其他语言包各有译文。对照:对象级工具栏流程 0 行仍开框;自带 recordId 的参数框不提前拒绝;没有 t 时保持英文;1 行仍打开标签页。反向验证四处各自变红(4/2/1/1 项)。dev 本地:27 个非 i18n 文件加整个 packages/i18n/,共 114 个文件 1713 项通过;type-check 31/31;eslint 0 错误;check:i18n-*、changeset 和 check:new-line-citations 通过。CI 全绿后由席位转 ready、进合并队列。
接受的偏差 1)复用 serverActionHandler 的成员而不是直接调 core:core 的 collectsRecordIdParam 没导出且不在文件面内;这层耦合只在本 hook 构造该 handler 时不传解析器的前提下成立,新钉测拿流程的答案对照 resolveServerActionRecordId,前提变了会变红,接受。2)PR 正文「Tests」一节写的「28 files outside i18n」应为 27 个,总数 114/114 正确;以本核对为准,不改正文。
不在本卡内 dev 记下(PR 已写):core createServerActionHandler 给跳过 runner 的调用方的派发期拒绝仍是英文(其结果不带 errorKey),经 runner 的行数拒绝不会走到它;RecordDetailView 的流程 handler 对自己的记录解析,不涉及行数。
落地条款 Closes #13397:合并后关卡。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 21:44
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 23, 2026 21:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 4eeb174 Oct 23, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-13397-flow-row-guard branch October 23, 2026 21:57
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flow actions launched from a list toolbar still open their param dialog before the row-count refusal, and refuse in English

2 participants