Skip to content

fix(gantt,map,calendar): 三份私有 sort→$orderby 副本收敛到 core 共享 sink - #4868

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4022-sort-query-sink
Aug 16, 2026
Merged

fix(gantt,map,calendar): 三份私有 sort→$orderby 副本收敛到 core 共享 sink#4868
yinlianghui merged 1 commit into
mainfrom
claude/issue-4022-sort-query-sink

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4022

前提复核(按 origin/main 65e88e6c2)

卡的前提成立,一处行号漂移:三份逐字副本仍在,但 calendar 那份已从 ObjectCalendar.tsx:111 漂到 :127(gantt :315、map :113 与卡内读数一致)。三份互为逐字副本这一点复核无误。

伴随卡 #4038 未并入:认领时点它已 closed(completed,PR #4267 于 08-11 合入),晋级评论给的前提「仍 open 且无人认领」不满足,故 ListViewBlock 一字未碰。

改了什么

三处删除本地 convertSortToQueryParams,改为从 @object-ui/core 导入共享 sink(三个包本就已依赖 @object-ui/core,且都已从它导入 extractRecords / buildExpandFields,故无新增依赖)。

退役核验(声明式 grep):grep -rn "function convertSortToQueryParams" packages/ 全仓仅剩 packages/core/src/utils/sort-query.ts:60 一条 —— sink 是唯一定义。

副本 vs sink 的语义差(这是 behavior fix,不是纯重构)

两处差异都是更贴合已声明的契约,不是新增容忍:

  1. 数组项缺 order → 升序,不再丢弃。 副本要求 item.field && item.order 两者皆真,否则 continue,于是 sort: [{ field: 'amount' }] 静默丢掉整条排序。同一份副本对字符串写法("amount")却早已按升序处理 —— 这是两种写法之间的不一致,不是刻意的严格。QueryParams.$orderby 自己声明的成员形状即 { field: string; order?: 'asc' | 'desc' }(order 可选),@object-ui/data-objectstackserializeOrderBy 对缺方向同样按升序。
  2. 无可排序内容 → undefined,不再是 {} 空对象是 truthy 值,只是恰好被 adapter 序列化器当成「无排序」。

另有两处副本的边角行为一并被 sink 收掉,均在已声明契约之外、无消费者依赖,记录以免读者以为是遗漏:字符串路径副本用 split(' ') 且不 trim,"name desc"(双空格)会被读成升序、" " 会产出 { '': 'asc' } 这种空字段名条目;sink 用 trim().split(/\s+/) 并在字段名为空时返回 undefined。数组路径副本把 item.order 原样写出(order: 'DESC' 会照抄),sink 归一到 'desc' / 'asc' 两值。

钉子(每个消费点一条,可达面是存视图元数据)

可达性说明,免得把严重度判高:SortConfig.order / ElementDataSourceSort.order 在 objectui 自己的类型里都是必填,类型化调用方写不出缺 order 的项;能走到这里的只有未类型化的已存视图元数据(ElementSavedView 按设计是松记录)。三条钉子因此都走 saved-view 路径,而不是直接喂 schema.sort —— 钉在真正可达的authoring surface 上。

  • ObjectGantt.elementDataSource.test.tsx — 新增 2 条:缺 order 项不丢({ end_date: 'asc', name: 'desc' });无可排序内容时不下发 $orderby
  • ObjectMap.elementDataSource.test.tsx — 新增 1 条:缺 order 项不丢({ rating: 'asc', name: 'desc' })。
  • ObjectCalendar.elementDataSource.test.tsx — 新增 1 条同上({ starts_at: 'asc', name: 'desc' });并把原有的 expect(params.$orderby).toBeTruthy() 收紧为 toEqual({ name: 'desc' }) —— 空对象同样 truthy,而 {} 正是旧副本把所有项丢光时的产物,该断言原本无法为正确的理由失败(fixture 三分法里的「整例替换」同因)。

反向验证(方向为先预判后执行)

预判:把 gantt 一处 import 换回本地副本后,该消费点的两条新钉子转红并指名缺陷,而原有 5 条保持绿 —— 原有 5 条喂的都是 { field, order } 齐全的项,两份实现对它们逐字节同解,它们不具备判别力,这点如实记录而不是假装它们也守住了。

实测与预判逐条吻合:

 ✓ queries the bound object with the saved view's filter and sort
 ✓ narrows, never widens: the binding's own filter ANDs with the view's
 ✓ reports an unresolvable `view` instead of fetching the whole object
 ✓ writes NO row cap: the gantt reload issues no $top for one to land on
 ✓ leaves a gantt with NO dataSource exactly as it was
 × orders by a sort entry that omits `order` instead of dropping it
   → expected { name: 'desc' } to deeply equal { end_date: 'asc', name: 'desc' }
 × sends NO $orderby when nothing orderable was authored
   → expected {} to be undefined
 Test Files  1 failed (1)
      Tests  2 failed | 5 passed (7)

两条红都精确复现了卡里描述的缺陷本体:end_date 被丢掉、空对象取代 undefined。随后 git checkout -- 还原(未用 stash)。

core 里被本 PR 证伪的注释

packages/core/src/utils/sort-query.ts 的头注释原文写着「三份副本仍在,收敛它们不属于 #7137 的范围」—— 本 PR 一落地这句话就成了假话。仅更正该注释(并把描述副本行为的时态改为过去时),sink 的逻辑一行未动

测试

pnpm exec vitest run packages/plugin-gantt packages/plugin-map packages/plugin-calendar --maxWorkers=2
 Test Files  59 passed (59)
      Tests  490 passed (490)
turbo run type-check --concurrency=2
 Tasks:    81 successful, 81 total

node scripts/check-control-bytes.mjs → OK(4354 个跟踪文本文件)。

范围外(已另立单,不在本 PR 修)

卡的「建议范围」第三条提到 packages/plugin-view/src/ObjectView.tsx 那条 $orderby: sort 直接透传要不要也走这个 sink。本 PR 不动它:它属于另一个包与另一条容器路径,且今天没有用户可见缺陷(serializeOrderBy 对缺方向本就按升序)。因为 #4022 一合就会关闭、这条观察会随之丢失,已另立 finding 单承接。


Generated by Claude Code

gantt / map / calendar 各自内联了一份逐字相同的 convertSortToQueryParams。
该副本要求数组项同时带 field 与 order,缺 order 的项被静默丢弃 —— 存视图里
sort: [{ field: 'amount' }] 于是整条排序都到不了 wire。同一份副本对字符串
写法("amount")却已按升序处理,所以这是两种写法之间的不一致。

三处改为从 @object-ui/core 导入共享 sink(objectstack#7137 引入,timeline
与 record:line_items 已在用),私有副本删除 —— 全仓仅剩 core 一处声明。

随迁移落地两处行为变化(均是更贴合已声明契约,不是新增容忍):
- 缺 order 的数组项按升序排,不再消失($orderby 自己的成员形状即
  { field: string; order?: 'asc' | 'desc' })。
- 无可排序内容时不再下发 {},而是不带 $orderby。

每个消费点补「不带 order 的项不丢」钉子;calendar 原先的
expect($orderby).toBeTruthy() 收紧为逐值断言 —— 空对象同样 truthy,正是旧
副本丢光所有项时的产物。core 里被本 PR 证伪的注释(「三份副本仍在」)一并
更正,sink 逻辑未动。

Fixes #4022

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-DkNlwUhU.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
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.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 496.22KB 110.42KB
core (index.js) 3.79KB 1.52KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 157.05KB 43.28KB
fields (index.js) 231.73KB 57.60KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.86KB 10.83KB
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.74KB
mobile (index.js) 1.50KB 0.62KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.44KB 18.28KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 128.14KB 32.58KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 239.81KB 59.97KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 115.34KB 27.74KB
plugin-gantt (index.js) 164.06KB 39.94KB
plugin-grid (index.js) 192.23KB 51.30KB
plugin-kanban (index.js) 52.72KB 14.54KB
plugin-list (index.js) 111.23KB 26.97KB
plugin-map (index.js) 17.91KB 5.72KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.38KB 11.09KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 83.81KB 20.49KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.53KB 9.41KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.28KB 0.68KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.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 (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
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

Copy link
Copy Markdown
Collaborator Author

【PM 验收 · ACCEPT】objectui 分片 PM(session_01GTRjn8xBqp75dk7kFupVRt)对 #4022(三份 convertSortToQueryParams 副本迁 core 共享 sink)的实施验收:

实物核验:8 文件对账相符 —— 三消费点删副本改 import(净 -95 行)、四条新钉(全走 saved-view 可达路径,可达性论证成立:类型化调用方写不出缺 order 项)、core sink 非注释行改动 0(PM 逐行核)。模型标识 0、releases 0。#4038 跳过正确:认领时点已被 PR4267 关闭,晋级评论前提不满足,ListViewBlock 零触碰 —— 前提核查纪律的教科书执行。core 只读偏离核准:sink 头注释「三份副本仍在」被本 PR 直接证伪,留着即本 PR 自己制造的假话 —— 改注释是必要收敛非扩界。calendar 的 toBeTruthy 收紧同理({} 正是旧副本丢光项的产物,原断言无法为正确理由失败)。

semantic diff 定性:四条差异(丢项→保留、{}→undefined、字符串边角、order 归一)以 QueryParams 自声明形状 + serializeOrderBy 同侧行为为准绳,behavior fix 而非纯重构,changeset/PR 正文已声明;truthy 性差异经 serializeOrderBy 实测无实际下游可见面。反向验证:2 红 5 绿逐条命中预判,红文逐字对应两处缺陷本体;诚实记录原有 5 条不具判别力。退役核验:声明式 grep 全仓仅 core 一条。CI 亲读:19 项全 completed(17 success + 2 skipped),零失败 —— dev 交接时未收敛的 5 项已由 PM 亲读补上终态。

新 finding #4869(ObjectView 最后一处不走 sink,含 defaultSort 拼法先裁定后迁移的正确顺序)查重合格,留分诊。

处置:undraft + auto-merge(SQUASH)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 16, 2026 19:25
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 16, 2026
Merged via the queue into main with commit 5edc0c5 Aug 16, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4022-sort-query-sink branch August 16, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants