Skip to content

fix(app-shell): Studio's Data pillar keeps platform-injected hidden system fields out of the grid, Form preview and designer (objectui#11780) - #11828

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11780-studio-hidden-system-fields
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11780-studio-hidden-system-fields

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11780

Clause-②: no

What changed

Studio's Data pillar kept framework fields out of the records grid (gridColumns), the Form preview (formFields) and the form designer by one fixed name list, STUDIO_SYSTEM_FIELD_NAMES. The platform's search companion __search and owning_business_unit_id are not on that list, so every author saw "Search Index" and "Owning Business Unit" in all three views, while the runtime list hides both.

  • New module-private predicate isStudioHiddenSystemField(def) in studio-design/studioHiddenSystemField.ts (not on the package entry): system === true && hidden === true on the served field definition, per the seat's ruling. An author field with hidden: true and no system stays visible; so does owner_id (system, not hidden).
  • gridColumns and formFields drop entries that match it, beside the unchanged name-list check. Memo keys are unchanged: objDraft.fields, plus publishedFieldNames for the grid.
  • ObjectFormDesigner: one module-level test, isKeptOffLayout(entry, systemFieldNames) (the name list OR the predicate), now serves all three of its readers: the density count, the containers, and the commit's write-back. A field kept off the canvas is therefore always written back. No prop change; the mount still passes STUDIO_SYSTEM_FIELD_NAMES.
  • STUDIO_SYSTEM_FIELD_NAMES keeps its job, measured below: the platform's audit columns are system without hidden, so the marks alone would put them back. Its doc block now says so.
  • Patch changeset for @object-ui/app-shell.

Fence held: no MetadataClient.save call site, no version-token read, no change to the object-draft load effect, no edit to the metadata-admin i18n.ts, no package-entry export, no type member, no locale key. ObjectFormDesigner is not re-exported from the app-shell index.ts (grep: zero hits; the positive control registerMetadataPreview hits).

Measurements against the PM hypotheses

H1, measured live. objectstack a543e244, examples/app-showcase booted with objectstack dev --seed-admin --fresh on an isolated port, read through the same endpoints MetadataClient.layered and getDraft call.

  • No pending draft, showcase_account, GET /api/v1/meta/object/showcase_account/layers. effective.fields is the record shape. __search: hidden true, system true, readonly true. owning_business_unit_id: hidden true, system true, readonly true. organization_id: the same. owner_id: system true, readonly false, no hidden. created_at: system true, readonly true, no hidden. Author field name: hidden false, readonly false, no system. (code.fields has no __search; it is provisioned into effective.)
  • Served draft: an object created the way doCreateObject creates one (the skeleton with one name field, saved with mode=draft into a Studio-created package), then GET ...?state=draft. item.fields is organization_id, created_at, created_by, updated_at, updated_by, owner_id, owning_business_unit_id, name, __search. __search and owning_business_unit_id: hidden true, system true, readonly true. Author field name: label only. An object of a code-provided package takes no draft (the save answers 403, read-only package), so the served-draft case is a Studio-authored object.
  • So the marks sit on the entries readFields(objDraft.fields) returns in both cases. No second source was needed.

H2, confirmed. The three readers above are the leak. No separate seed carries the injected fields: buildObjectSkeleton seeds one name text field, the server adds the injected columns to the served draft, and the designer's ungrouped bucket rendered them. That bucket is the filer's "default layout seed".

H3, confirmed and kept. The designer's commit writes the kept-off entries followed by the canvas entries. With the shared test, the injected hidden fields ride the same path as the audit columns. Pinned through the real pillar: a designer drop is auto-saved with both fields present and their definitions deep-equal to the served ones.

H4, not touched. ObjectFormCanvas (the object metadata preview, mounted through ObjectPreview in the metadata-admin editor) renders every entry through groupEntries(view, ...) with no system filter of any kind; it shows the audit columns too. It is a full field-inventory editor, and the Data pillar does not mount it (the pillar mounts ObjectFormDesigner and the grid). Hiding fields there would be a new design decision, not this leak.

H5, kept. Both memo keys are unchanged. A new pin holds the grid columns array at the same identity across an object-label edit made through the real onPatch.

Live check in the browser

The console dev server from this branch, proxied to that backend, headless Chromium at 1440x900, signed in as the seeded admin.

  • This branch: the showcase_account Records grid headers run Account Name, Industry, ..., Owner, Loyalty Tier, LinkedIn URL, CSAT Score, Actions. Neither "Search Index" nor "Owning Business Unit" appears. Both designers (showcase_account, and the Studio-created object) show neither label. The new object's designer shows Owner and Name.
  • The same session with both source files set to the base commit (restored afterwards; blob hashes equal HEAD, git diff HEAD empty): the headers include "Owning Business Unit" and "Search Index", and both designers show both labels.

Pins

DataPillar.hiddenSystemFields-11780.test.tsx drives the real DataPillar. The fixture copies the platform's own literals (provisionSearchCompanion, OWNING_BUSINESS_UNIT_FIELD_DEF, TENANT_SCOPE_FIELD_DEF, the created_at row of AUDIT_FIELD_DEFS). Each case runs twice: once on an object with no pending draft, and once with a served draft.

  • Records grid, Form preview, designer: __search, owning_business_unit_id and organization_id are absent. The author fields are present.
  • Controls: internal_note (author hidden: true, no system) and owner_id (system, not hidden) stay. created_at stays out, by the name list.
  • Write-back: a designer drop (the captured onDragEnd, with the real DndContext rendered) auto-saves fields. In those fields industry now leads name, every hidden system field's definition is deep-equal to the served one, and the key set is unchanged.
  • Identity: an unrelated draft edit (the object label) leaves the grid columns array at the same identity, and the save carries the new label.
  • The predicate's truth table: both marks, booleans only.

Reverse checks. The fix was committed first. Each leg is restored with git checkout HEAD --, then proven by a blob hash equal to HEAD and an empty git diff HEAD. Predicted direction first:

  1. Both source files at the base commit (marker counts 4 and 4 fell to 0 and 0): 6 failed, 5 passed. The failures are grid, Form preview and designer, each twice; the first reads "expected [ 'name', 'industry', ... ] to not include '__search'". Write-back, identity and the predicate stay green, as predicted.
  2. The designer commit's write-back test changed back to the name list alone: 2 failed (write-back, twice), "expected undefined to deeply equal { type: 'text', ... }". This leg proves the hidden fields would be dropped.
  3. The gridColumns key loosened to objDraft: 2 failed (identity, twice).

The first attempt at leg 3 was a no-op: the script hit a syntax error before the mutation landed (anchor count 1/0 unchanged, nothing written). It was rerun with a corrected script, and the counts after the mutation read 0/1.

Gates (local HEAD 7343b3a)

  • pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build: exit 0
  • pnpm exec vitest run on StudioDesignSurface.gridColumns, StudioDesignSurface.formFields, ObjectFormDesigner, every DataPillar.* test and the new pins: exit 0, 13 files, 56 tests passed
  • pnpm exec vitest run packages/app-shell/src/views/studio-design/ (at 9feaf8b; 7343b3a changes only the new test file's mock typing): exit 0, 93 files, 548 tests passed
  • pnpm --filter @object-ui/app-shell type-check (echoes tsc --noEmit && tsc -p tsconfig.test.json): exit 0
  • pnpm check:control-bytes OK · check:new-line-citations VERDICT 0 new citation(s) · check:changeset-claims OK · check:pending-changeset-literals OK: all exit 0
  • Added for this diff: check:vi-mock-specifiers · check:vi-mock-inherit · check:vi-mock-override-shape (new vi.mock doubles) · check:test-path-roots · check:unreferenced-sources (new source file) · check:metadata-write-doors: all exit 0. Also node scripts/check-changeset-presence.mjs, node scripts/check-changeset-no-major.mjs and node scripts/check-governed-queue-guard.mjs --test on the changed paths (NOT GOVERNED): all exit 0.
  • Targeted eslint, as the package's lint runs it, on the four touched source files: 0 errors. On the two modified files the warnings are the same set as at the base commit (ObjectFormDesigner 1, StudioDesignSurface 17). The new files have none. Type-aware linting is not enabled in eslint.config.js, so this diff cannot move a verdict on an untouched file. The repo-wide pnpm lint belongs to CI.

Acceptance notes

  • The designer's write-back puts the kept-off fields first in fields. That was already true for the audit columns, and is now true for the injected hidden ones. Their definitions are unchanged. Before this change these fields sat in the ungrouped bucket and were rewritten at that bucket's position, so a designer edit moved them before too.
  • The object header's "N fields" count still counts every served field, the system ones included. It did so before this change, and it is not on this card's surface.
  • One line outside the claim's listed regions of StudioDesignSurface.tsx: the import of the new helper, in the module's import block. In-flight draft PR fix(app-shell): Studio draft saves send the version they were built on, and a stale save opens a reload / overwrite dialog (objectui#11773) #11826 also adds an import there, in an earlier part of the block near useMetadataClient. The two hunks do not overlap, and neither do its DataPillar hunks with this branch's.

Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z (dispatching seat domain:ui#1).


Generated by Claude Code

claude added 2 commits October 7, 2026 18:45
…ystem fields out of the grid, Form preview and designer (objectui#11780)

The records grid (`gridColumns`), the Form preview (`formFields`) and the
form designer dropped framework fields by one fixed name list, which does not
name the search companion `__search` or `owning_business_unit_id`, so every
author saw both. The platform marks the columns it injects and hides with
`system: true` + `hidden: true`; `isStudioHiddenSystemField` reads that pair
off each field definition, beside the name list (which keeps the audit
columns: `system` without `hidden`).

- Author-hidden fields without `system` stay visible (the way to un-hide them).
- `owner_id` (`system`, not hidden) stays visible.
- The designer hides such fields from the canvas and writes them back with
  their definitions unchanged; one test serves the density count, the
  containers and the commit.
- Memo keys are unchanged (`objDraft.fields`, plus `publishedFieldNames` for
  the grid).

Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
…onfig.test.json accepts them

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

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 336 chunks) 3516.4 KB 3551.8 KB
Main entry chunk (gzip) 156.4 KB 350 KB
Entry file index-D0AMiaIR.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 (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) 579.68KB 139.46KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 235.41KB 65.46KB
fields (index.js) 266.88KB 67.46KB
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.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.22KB 38.99KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.86KB 65.27KB
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) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 7, 2026 19:31
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit d53fd02 Oct 7, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11780-studio-hidden-system-fields branch October 7, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants