Skip to content

Studio: add a field to a read-only base object — written as an extension in <base>.custom named xxx__c, base fields editable only within the display-property whitelist #28575

Closed

Description

@objectstack-fleet

epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848

Filing gate: ④ child of epic #28236 (P2, cut 2026-11-01 on the maintainer's 「四项提速措施全部按推荐执行」, schedule reviewed) · plan of record: ADR-0146 Rollout P2 "N-23a/b/c object extension … Studio add-field (D10)". ⛔ Not a claim.

Blocked-by: #28560, #28564, #28565, #28574, #28407

Part of #28236 (P2, child N-23c) · governs: ADR-0146 D10 (last paragraph: Studio's add field writes an extension in .custom, suffixed, the base untouched; the display-property patch), D3 (auto-create, no prompt) · lane: domain:ui · size: L.

Why it waits. This card writes the extension body N-23a declares, including its per-field patch slot. Four other cards come first:

Business reason. An administrator wants a "Region" field on HotCRM's Account. Today Studio refuses because HotCRM is read-only. They should add it in Studio and get region__c in hotcrm.custom. HotCRM stays as shipped and the next HotCRM upgrade cannot collide with the field. Renaming a HotCRM field's label works the same way, within the whitelist.

Measured (35b178bd0a).

  • Data pillar: canAddFields = !readOnly && !objIsExternal (ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx:7485). addField pushes the field into the open object's own buffer (objDraft), and autosave writes that object whole (:7473–7484). On a read-only base, nothing is offered.
  • AddFieldPopover.tsx derives the api name from the label (toFieldNameLoose) and refuses a taken name (fieldNameTakenRefusal).
  • Spec: EXTENSION_FIELD_SUFFIX = '__c' and isExtensionFieldName (packages/spec/src/data/field-customization.ts:31, :40). BASE_FIELD_DISPLAY_WHITELIST is label, description, inlineHelpText, placeholder and option label (:74). The patch slot is not declared yet (:21–23, N-23a).
  • ObjectExtensionSchema (packages/spec/src/data/object.zod.ts:4709) carries extend, fields, label, pluralLabel, description, validations, indexes and priority.
  • No runtime write path for an extension exists. object_extension has no slot of its own (packages/objectql/src/plugin.ts:178–182), and only the manifest load registers one (objectql/src/engine.ts, the 'extend' registration). N-23e builds the door.

Wanted.

  1. On a base object of a read-only base, Studio offers Add field (grid "+" and Form toolbar). Create ensures <base>.custom without a prompt and writes an extension of that object into the pair, not into the base object's buffer.
  2. The popover shows the api name with __c before Create (region → region__c). A typed name is suffixed the same way. The taken-name refusal covers the folded object's fields.
  3. The new field appears in the grid and form at once, and its inspector edits the extension. The base object's other fields stay read-only.
  4. On a base field, the inspector edits only the whitelisted properties, read from the spec constant (no client copy). Edits are written as the per-field patch in the pair's extension. Every other property is shown read-only, with one sentence naming the whitelist.
  5. Server refusals are shown verbatim: suffix, structural key, walled 403 from the ensure door. Under a walled posture, for a principal the served facts refuse, the actions draw disabled in the console's own words.
  6. Unchanged: add-field on a writable package's own object (plain names), and the field inspector there.

Tests. Fixture read-only base + object:

  • Add field "Region" ensures the pair once, writes an extension with region__c into it, and leaves the base object's body unsent;
  • a second add does not ensure again;
  • a label patch on a base field writes only label into the patch;
  • required on a base field is not editable;
  • a refusal is shown verbatim;
  • the walled tenant case is disabled;
  • a writable package's add-field is unchanged.

Release safety. Safe on its own: the write door (N-23e), its gates (N-23b, N-23d) and the ensure door (#28407) are all in Blocked-by:. Without this card, base objects stay read-only in Studio, as today.

Not here. The patch slot (N-23a). __c at every door (N-23b). Patch merge and target check (N-23d). The extension write door (N-23e). The customized/added marks on rails (N-19). Directory marks (N-26b). No fields table (D6).

Dedupe. Searched add field __c and extension field studio base object: only #28236, #28411 (the read) and closed N-08 history (#28404, #28467).

Links back: #28236, #28407, #28404, ADR-0146 D10, D3, D6, ADR-0029 D9.2.


Generated by Claude Code

Activity

  1. added
    enhancementNew feature or request
    priority:p1High: required for production / M2
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    on Nov 1, 2026
  2. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Unblocked (R828). Every card in Blocked-by: is closed as completed: #28560, #28564, #28565, #28574 and #28407. Triage read "Why it waits" before releasing. The card writes the extension body N-23a declares, and the cards it said must land first have landed. Its write door (N-23e), its gates (N-23b, N-23d) and its ensure door are all among those closed blockers, so nothing release-relevant still waits.

    pm:blocked → pm:queue, priority stays p1 (domain:ui).


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

    Claim: PM loop round 7 · 2026-11-02T00:14Z
    Session: session_01QsR2jVPKQuoz3QHyQ76RnU (card thread, os-dev); dispatched by seat session session_01QsR2jUJ19MstFvFhjgiYTu
    Account: zhuangjianguo
    Branch: claude/project-thread-y2hv3c (platform-assigned to the card thread; it can push only this branch)
    Worktree: objectstack-issue-28575
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (the Data pillar's add-field path), studio-design/AddFieldPopover.tsx, views/metadata-admin/inspectors/ObjectFieldInspector.tsx, the N-17 ensure path it reuses (studio-design/packages-io.ts, studio-design/StudioCustomizeControls.tsx), ui/packages/i18n/src/namespaces/studio/{en,zh}.ts, their tests, and a root .changeset; server and packages/spec read only (stop on breach; explain in the report)
    Container & model: L, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-read: 6565810476
    Premise: holds (at origin/main 6ea8342a: canAddFields = !readOnly && !objIsExternal is StudioDesignSurface.tsx:8287; EXTENSION_FIELD_SUFFIX / BASE_FIELD_DISPLAY_WHITELIST are packages/spec/src/data/field-customization.ts:33 / :75; object_extension is an authorable item through the /api/v1/meta doors since #28574 (PR #28811, 0c1829a28a); the fieldPatches slot is on ObjectExtensionSchema (#28560))
    Serial constraints cleared: this seat's PR #28818 (#28578) also edits StudioDesignSurface.tsx and the studio i18n namespace; whichever lands later merges main.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

    Landed · 2026-11-02T03:02Z · PR #28860 merged through the merge queue as 7fa9873f at 2026-11-02T03:02Z (GitHub's clock). ACCEPT 6567788691 at head b48d97ff.

    probe before (7fa9873f~1) now (origin/main)
    export function foldObjectExtension in studio-design/StudioCustomizeControls.tsx 0 1
    studio-design/DataPillar.baseObjectExtension-28575.test.tsx exists no yes

    Fixes #28575 closed this card. pm:dispatched comes off in the same stroke as this note.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jVPKQuoz3QHyQ76RnU
    os-dev-report

    {
      "issue": 28575,
      "status": "done",
      "branch": "claude/project-thread-y2hv3c",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/28860",
      "session": "session_01QsR2jVPKQuoz3QHyQ76RnU",
      "premise_still_valid": true,
      "summary": "Studio's Data pillar offers Add field on a base object of a read-only base (Form toolbar leading slot and grid +). Create ensures <base>.custom once and writes an object_extension draft with a __c field into the pair; the base object's body is never sent. On a base field only BASE_FIELD_DISPLAY_WHITELIST keys are editable (read from the spec), written as fieldPatches; refusals verbatim; walled posture draws the action disabled. Merged as 7fa9873f.",
      "tests": "New DataPillar.baseObjectExtension-28575 (10 cases incl. the seat's dedupe case) + StudioCustomizeControls.objectExtension-28575 (9 unit pins), green. Sweep: studio-design + inspectors + zh census + studio-locale + packages/i18n, 615 files / 6493 tests, one red (gridColumns identity-churn liveness, caused by my names-key memo), fixed and re-run (36 files / 206 tests). After the merge of main: 113 files / 830 tests green. Ablations via ablation-replace.mjs, restored to HEAD: suffix removed -> 3 of 10 red; base fields routed whole -> 7 red across both files.",
      "gates": "dispatch-gates --commands: 34 commands, all exit 0 (check:type-check-debt first NOT MEASURED on a lock timeout, re-run OK). app-shell type-check green at each head; ui scripts/ 169 files green; check:i18n green after the merge; cross-file line citations 0. CI: Type Check UI red at fe34b830 on check:spec-symbols (local isPlainRecord), fixed at 10f4f254; merged main at b48d97ff; landed 7fa9873f.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 GitHub MCP calls",
      "api_writes": "1 relay write — pr_create (POST /repos/objectstack-ai/objectstack/pulls); plus 3 git pushes",
      "open_questions": [],
      "out_of_scope_findings": [],
      "deviations": [
        "Wanted 1 grid half: a draft field joins the Records grid columns only after publish (gridColumns projects published names, cloud#1652); accepted by the seat and recorded in Acceptance notes.",
        "Form toolbar Add field sits in FormToolbar's leading slot and the walled disabled action in the object header, because ObjectFormDesigner.tsx and GridFieldAuthoringProvider are outside the surface.",
        "Base-field mode reaches the inspector by React context (inspector-registry.ts outside the surface).",
        "Added beyond the card: a cleared patch value is held as '' in the buffer and dropped on save."
      ]
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:uienhancementNew feature or requestpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions