Repository navigation
refactor(components): the list Group panel picks its field with the shared Select (objectui#11865) - #11934
Conversation
…hared Select `GroupingEditor`'s field selector was a browser-native select beside the Filter and Sort panels, which pick a field from the same list with the shared Radix `Select`. It now uses that primitive. A level grouped by a field the options do not carry is mounted as its own item, labelled with the field name, so the trigger shows what the level holds. `GroupingEditorProps` and the value written to `onChange` are unchanged. Part of objectui#11865. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
… and empty state The field control is the Radix combobox trigger; picking a field writes the same `GroupingConfigValue` the native control wrote; the empty state and the add / remove buttons behave as before; a level grouped by a field the options do not carry shows that field; selection works from the keyboard. Part of objectui#11865. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
…ord the observed test direction Patch changeset for `@object-ui/components`. The suite header now states the direction observed against the native control, and the comments no longer say the Filter panel picks from the same list (it lists the object's fields, a superset of the view's columns) or claim a disabled styling this panel has no channel to reach. Part of objectui#11865. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
❌ 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. Which half objected:
📦 Bundle Size Report
Size Limits
|
…elect Sync with main after objectui#11956 landed; no other change. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
|
| Step | Outcome |
|---|---|
| Build packages | success |
| Check console performance budget | failure |
Which half objected:
| Eager-closure half | Verdict |
|---|---|
| Aggregate closure ceiling | ✅ pass |
| Per-chunk ceilings | ✅ pass |
| Per-chunk membership (declared packages) | ✅ pass |
| Ceiling sensitivity (headroom) | ✅ pass |
| Ceiling freshness (checkout vs. base branch) |
⚠️ A superseded ceiling is neither a size regression nor a drifted gauge. The bundle was measured correctly and the ceilings on the base branch are correct — but this checkout predates a change to them, so the verdicts above were weighed against numbers that are no longer in force. Nothing grew. Update this branch onto the base branch and letBundle Analysisrun again; do not widen a ceiling to clear it. TheCheck console performance budgetstep log names each superseded constant with both values (objectui#6245).
Reason: The entry chunk measured 71.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.
See the workflow run for details.
📦 Bundle Size Report
| Package | Size | Gzipped |
|---|---|---|
| app-shell (consoleActionDispatch.js) | 0.20KB | 0.19KB |
| app-shell (index.js) | 17.82KB | 6.58KB |
| app-shell (runtime-config.js) | 22.59KB | 7.89KB |
| 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) | 40.22KB | 10.61KB |
| auth (AuthShell.js) | 3.49KB | 1.40KB |
| auth (ForgotPasswordForm.js) | 12.21KB | 3.45KB |
| auth (LoginForm.js) | 18.15KB | 5.40KB |
| auth (PreviewBanner.js) | 0.90KB | 0.50KB |
| auth (RegisterForm.js) | 6.72KB | 2.24KB |
| auth (SocialSignInButtons.js) | 9.70KB | 3.93KB |
| auth (UserMenu.js) | 3.39KB | 1.21KB |
| auth (auth-gate-events.js) | 1.29KB | 0.66KB |
| auth (authStyles.js) | 5.04KB | 1.72KB |
| auth (createAuthClient.js) | 40.70KB | 10.94KB |
| 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.11KB | 7.97KB |
| collaboration (LiveCursors.js) | 3.17KB | 1.27KB |
| collaboration (PresenceAvatars.js) | 6.28KB | 2.60KB |
| collaboration (PresenceProvider.js) | 2.79KB | 1.13KB |
| collaboration (index.js) | 1.68KB | 0.73KB |
| 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) | 586.61KB | 141.21KB |
| core (index.js) | 10.00KB | 3.96KB |
| create-plugin (index.js) | 27.94KB | 9.51KB |
| data-objectstack (index.js) | 240.39KB | 67.09KB |
| fields (index.js) | 266.92KB | 67.47KB |
| 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) | 38.37KB | 10.31KB |
| i18n (useSafeTranslation.js) | 7.14KB | 2.92KB |
| layout (index.js) | 41.50KB | 11.82KB |
| 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) | 13.86KB | 5.00KB |
| permissions (PermissionContext.js) | 0.31KB | 0.25KB |
| permissions (PermissionGuard.js) | 0.89KB | 0.45KB |
| permissions (PermissionProvider.js) | 6.52KB | 2.26KB |
| permissions (discardProofCache.js) | 1.04KB | 0.55KB |
| permissions (evaluator.js) | 8.33KB | 3.07KB |
| permissions (index.js) | 0.93KB | 0.41KB |
| 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.04KB | 3.92KB |
| plugin-calendar (index.js) | 53.39KB | 15.52KB |
| plugin-charts (index.js) | 84.71KB | 23.25KB |
| plugin-chatbot (index.js) | 199.63KB | 47.46KB |
| plugin-dashboard (index.js) | 144.82KB | 39.17KB |
| plugin-designer (index.js) | 231.46KB | 48.87KB |
| plugin-detail (index.js) | 248.57KB | 65.47KB |
| plugin-editor (index.js) | 2.23KB | 1.05KB |
| plugin-form (index.js) | 176.62KB | 45.75KB |
| plugin-gantt (index.js) | 179.17KB | 45.07KB |
| plugin-grid (index.js) | 248.06KB | 68.77KB |
| plugin-kanban (index.js) | 52.17KB | 16.37KB |
| plugin-list (index.js) | 117.45KB | 29.33KB |
| plugin-map (index.js) | 27.24KB | 9.03KB |
| plugin-markdown (index.js) | 13.88KB | 4.80KB |
| plugin-report (index.js) | 44.12KB | 12.29KB |
| plugin-timeline (index.js) | 39.10KB | 11.81KB |
| plugin-tree (index.js) | 15.07KB | 5.33KB |
| plugin-view (index.js) | 91.07KB | 22.93KB |
| 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.81KB | 3.58KB |
| 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) | 120.63KB | 39.56KB |
| react (data-invalidation.js) | 5.05KB | 2.08KB |
| react (index.js) | 4.50KB | 2.06KB |
| 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.08KB | 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.80KB |
| sdui-parser (provenance.js) | 3.84KB | 1.90KB |
| sdui-parser (types.js) | 0.28KB | 0.23KB |
| 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 (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 (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.25KB |
| types (system-fields.js) | 3.33KB | 1.54KB |
| types (theme.js) | 6.28KB | 2.87KB |
| types (ui-action.js) | 8.11KB | 3.32KB |
| 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
…elect Sync with main after objectui#11949 replaced the eager-closure ceiling; no other change. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
✅ 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
|
Part of #11865
Clause-②: no
The list Group panel's field picker (
GroupingEditorin@object-ui/components) is now the shared RadixSelect, the control the Filter and Sort panels beside it pick a field with. This is the Group panel's part of objectui#11865 only. The validation rule editor and Explain access stay with the card (held by objectui#11861 and objectui#11862 per the claim), so the card stays open.Run by the os-dev agent for seat
domain:ui#2, claim comment 6055510431, sessionhttps://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU.What changed
packages/components/src/custom/grouping-editor.tsx: each grouping level's native select element is replaced bySelect,SelectTrigger,SelectValue,SelectContentandSelectItemfrom../ui/select. The trigger keepsdata-testid="grouping-field-N"(N is the level index) and the row'sh-7/text-xssizing.GroupingEditorPropsand the package exports are unchanged, andpackages/components/src/ui/**is untouched. Both mount points get it, because both renderGroupingEditor: the toolbar Group popover inListViewand the Group section ofViewSettingsPopover.fieldis not infieldOptionsgets its own item, labelled with the field name, so the trigger shows what the level holds. This is reachable:ListViewbuilds the options fromschema.columnsand the grouping fromschema.grouping, so a view grouped by a column it does not show hits it. It is the same invariant the Filter panel's value picker keeps (objectui#4874).packages/components/src/__tests__/grouping-editor-shared-select-11865.test.tsx, and the patch changeset.changeset/11865-grouping-editor-shared-select.md.Select, not Combobox (dispatch H1)
Measured on the base: the Filter panel's field picker (
FilterBuilder) and the Sort panel's (SortBuilder) are both the shared RadixSelect. Neither usesCombobox. The Filter panel lists the object's fields, a superset of the Group panel's column list, so the longer list already usesSelect. Matching it is what makes the three panels look and behave alike. Triage named no numeric threshold forCombobox.NativeSelectis not used.Behaviour (dispatch H2)
{ ...level, field }, the same expression as before. The suite pins it against a JSON literal, and the same literal was read from achangeevent on the pre-conversion component (see Reverse verification).fieldOptionsorder. The native control listed the same.undefined.name, shownName, while the level heldregion. The trigger now showsregion.Read-only (dispatch H3): no channel on this surface
GroupingEditorPropshas nodisabledorreadOnly, neither consumer gates the editor, and no ancestor fieldset disables it. The props are frozen by the claim, so this surface has no read-only state to pin. The primitive's disabled styling comes with the control once a caller can disable it, which needs a props change that is not on this claim.Accessible name (dispatch H4): there was none to keep
A probe in happy-dom (testing-library role and name queries, scratch file not committed) read both controls: the base native
SELECTand the head triggerBUTTONeach compute an empty accessible name, and no combobox in the editor has a non-empty one. Giving it a name needs a label string that the frozenlabelsprop does not carry, andpackages/i18nis off this claim. Keyboard selection through the primitive is pinned: Enter on the trigger opens the list, Enter on an option selects it.Reverse verification
Both legs ran with the implementation committed. Each restored
grouping-editor.tsxwithgit checkout HEAD --, under a trap, and proved the restore by blob hash:git hash-objectequal to the HEAD blobfe7e8e2c,git diff HEAD0 bytes,git statusclean.e81c846content written into the worktree):Tests 9 failed | 3 passed (12). The three green ones are the empty-state and add / remove pins. The round-trip pins are red there because they open the primitive's listbox, which a native select does not have.{ tag: "SELECT", value: "name", selectedIndex: 0, shown: "Name" }for a level holdingregion;changetostatuswrote exactly{"fields":[{"field":"status","order":"desc","collapsed":true}]}, the literal the suite pins (green);statusandname:["Status", "Owner"], as the suite pins (green).Gates (HEAD
b5272f93d)pnpm exec vitest runover the new suite,packages/plugin-list/src/__tests__/ListView.test.tsx(its grouping popover tests),packages/plugin-list/src/__tests__/ViewSettingsPopover.rowColorTitle.test.tsxandpackages/core/src/utils/__tests__/column-identity.ratchet.test.ts:Test Files 4 passed (4),Tests 170 passed (170).@object-ui/componentssuite, repo-rootpnpm exec vitest run packages/components/in two shards. Shard 1/2:Test Files 185 passed (185),Tests 1813 passed | 17 skipped (1830). Shard 2/2:Test Files 184 passed | 1 skipped (185),Tests 1970 passed | 7 skipped (1977).pnpm --filter @object-ui/components type-check, after building the@object-ui/components^...closure: both legs echoed (tsc --noEmit && tsc -p tsconfig.test.json), exit 0.--listFilesOnlyshows both touched files in the programs.pnpm exec eslinton the two touched TypeScript files: exit 0. With--format json: 2 files, 0 errors, 0 warnings.node scripts/check-changeset-presence.mjs: "2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)".node scripts/check-changeset-no-major.mjs: nomajor.pnpm check:new-line-citations:VERDICT new-cross-file-line-citations: 0 new citation(s).pnpm check:control-bytes: OK.pnpm check:component-surface-parity: REPORT-ONLY, exit 0, no row names the grouping editor.check-changeset-fixed,check-changeset-overwrite,check-changeset-claims("No pending changeset names a file this change touches"),check:pending-changeset-literals,check-test-path-roots,check:phantom-deps,check:unreferenced-sources,check-vi-mock-specifiers.Eager closure (console first load)
pnpm exec vite buildinapps/console, thenpnpm check:eager-closure, on head and on the base. The base leg is the same tree withgrouping-editor.tsxat itse81c846blob, which is the only bundle input this branch changes.b5272f93d: "Console eager closure is 3306.2 KB gzipped across 336 of 2458 chunks (budget: 3307.0 KB, headroom: 0.8 KB)".eagerGzipBytes3385540.eagerGzipBytes3385542.ui-componentschunk: +157 raw, +22 gzip. The other eager chunks move by a few gzip bytes with 0 raw bytes, from the newui-componentscontent hash in their import specifiers.ui/selectand@radix-ui/react-selectwere already in the eager closure:packages/componentslands whole inui-components, andFilterBuilder/SortBuilderalready import the primitive.Docs
No sentence in
content/docsor a package README describes the Group panel's control, so there is nothing to report under the docs-driven rule.Acceptance notes
aria-labeleither, read from source). Not filed: there is no measurement at a public entry. Carrier: none named.SelectItemrefusesvalue="".ListViewkeeps a string column as written, so a column authored as an empty string would reach these options, as it already reaches the Filter and Sort panels' items. Whether the spec refuses an empty column name was not measured. Not filed.Generated by Claude Code