Repository navigation
Record-level inline edit: editable highlights + one atomic Save (follow-up to #2402) #2407
Description
Follow-up to #2402 (Edit-as-primary CTA + double-click inline edit, merged). Delivered as one issue, two phases.
Problem
- Fields are interrelated → per-field save-on-blur is unsafe (bypasses cross-field validation, can persist inconsistent intermediate state). The whole-record batch model from Detail view: Edit as primary CTA; enter inline edit by double-clicking a field (#2401) #2402 is correct; keep one atomic save.
- Highlight fields (Status / Health / Budget / End Date / Account) can't be inline-edited. Root cause (confirmed in code): `record:highlights` (→ `HeaderHighlight`) is a separate, read-only page block that registers its field names into `HighlightFieldsContext`; `record:details` dedupes those fields out of the editable body. Inline-edit state (`isInlineEditing` / `editedValues`) lives inside `DetailView` (a child of `record:details`), so the sibling highlights block can't reach it. Net: entering inline edit only makes body fields editable; the most edit-worthy fields (highlights) stay read-only.
Goal
Lift inline edit into a record-level shared mode so the highlights strip and the details body share ONE edit session + ONE atomic, cross-field-validated Save. Fixes (2), honors (1). Long-term: inline edit becomes a reusable record-level primitive (any `record:*` block can opt in), not `DetailView`-private state.
Design
New primitives
- `InlineEditContext` (@object-ui/react) — a separate context (mirrors `HighlightFieldsContext` so edit-draft churn doesn't re-render every `record:*` consumer). Pure UI state: `editing, canEdit, draft, autoFocusField, saving, error, enter(field?), setField, cancel, reset`. Export `InlineEditProvider` + `useInlineEdit()`.
- `` (plugin-detail) — extract the edit-input branch from `DetailSection` so `HeaderHighlight` renders identical inputs. Covers every widget it handles today: `SelectField`, `BooleanField`, `LookupField`, `UserField`, `CapabilityMultiSelectField` (feat(app-shell,plugin-detail): permission sets — Studio designs, Setup assigns (ADR-0056) #2403), plain input (number/date/text, ISO coercion), `extractLookupId`, `PermissionFacetLink` (feat(app-shell,plugin-detail): permission sets — Studio designs, Setup assigns (ADR-0056) #2403). Keeps the computed-type gate.
- `` (plugin-detail) — record-level sticky Save/Cancel bar + atomic save + OCC. On Save: one `dataSource.update(obj, id, draft, { ifMatch: data.updated_at })` → `refresh()` → reset. On 409 → reuse `ConcurrentUpdateDialog`. `draft` holds only user-edited keys → computed/read-only never sent.
Wiring
- `RecordDetailView` (app-shell): wrap page content in `` inside `RecordContextProvider` (around the highlights+details SchemaRenderer) + render ``.
- `record-highlights` / `HeaderHighlight`: when `editing`, render `` (value = `draft[name] ?? data[name]`, write via `setField`), pencil/double-click `enter(field)`, computed/readonly + `canEdit` gate, don't hide empty while editing.
- `record-details` / `DetailView` / `DetailSection`: DetailView consumes `InlineEditContext` instead of local state; drop its Block B bar + batch save. Keep the highlight↔body dedup.
Standalone `DetailView` (drawer / direct usage, e.g. `RecordDetailDrawer`): DetailView always consumes `InlineEditContext`; standalone hosts get a local `` + `` wrapper (no dual-mode branching inside DetailView). This is the main regression surface.
Phasing
- P1 — lift + atomic save (plumbing). Add `InlineEditContext`; move DetailView (page + standalone) onto it; add `` with the single atomic OCC save; delete DetailView's local batch save. Highlights stay read-only. Isolates the riskiest change (DetailView rewire + save semantics).
- P2 — editable highlights. Extract ``; make `HeaderHighlight` editable against the shared draft; pencil/double-click on highlight fields. Includes compact-layout UX (a select/lookup/date editor in a narrow highlight column — design expand-on-edit / popover, Salesforce-style).
Each phase is its own PR.
Guardrails preserved
Computed (`formula`/`summary`/`rollup`/`auto_number`) + `readonly` non-editable; `affordances.edit` object-lifecycle gate (via `canEdit`); approval lock; OCC (`ifMatch` + `ConcurrentUpdateDialog`); atomic partial update never writes computed/read-only fields; FLS/permission field filtering unchanged.
Cost & risk
- Cost: ~7–9 files incl. a foundational shared context + extracting the most complex part of `DetailSection` + a new save bar.
- Risk: (1) `DetailView` is multi-use — rewiring its state ownership is the Implement visual designer for Object UI schemas #1 regression risk; (2) save semantics change (per-field loop → one atomic write) interacts with backend validation / computed recompute / OCC-conflict UX; (3) editable highlights in a compact strip = layout/UX polish risk.
- Mitigation: the P1/P2 split isolates the risky plumbing from the visible feature; each phase independently verified.
Acceptance criteria
P1
- `InlineEditContext` / `InlineEditProvider` / `useInlineEdit` exported from @object-ui/react.
- Double-click / pencil on a body field enters edit via the shared context; one record-level Save/Cancel bar.
- Save issues exactly ONE `update` with only edited fields; 409 → ConcurrentUpdateDialog.
- Standalone DetailView (drawer) still edits + saves.
- Existing inline-edit tests pass; new tests for atomic save + standalone.
P2
- Hover/double-click a highlight field (Status/Budget/…) enters the same record edit session, editable in place.
- Highlight + body edits commit together in ONE Save.
- Computed/readonly highlight fields expose no editor.
- Compact-layout editor is legible.
Verification
- Unit: InlineFieldInput parity; editable highlights; computed/readonly gate; atomic-save assembles one update from `draft`; standalone DetailView.
- Builds: `plugin-detail` (vite), `app-shell` (tsc), `react`.
- Browser (:5181, showcase Project): double-click a highlight (Status/Budget) AND a body field → single Save → both persist; Cancel restores; confirm one PATCH carrying only edited fields.
Reuse (don't reinvent)
`HighlightFieldsContext` pattern (RecordContext.tsx); `ConcurrentUpdateDialog` + `isConcurrentUpdateError`; sticky-bar chrome from `DraftPreviewBar.tsx` / `RuntimeDraftBar.tsx`; the update/updateOne/patch + `ifMatch` fallback in `record-details.tsx`.
Activity
os-zhuang commentedon Jul 13, 2026
Execution note (sequencing): land the
<InlineFieldInput>extraction as a standalone, behavior-preserving refactor PR first. It's already covered by the #2402DetailSectioninline-edit tests, so it can go in green with zero UX change, and it shrinks P2's risk surface — P2 then only adds the editable-highlights wiring on top of an already-proven shared input component.Suggested order:
<InlineFieldInput>extraction — no-op refactor.InlineEditContext+ one atomic OCC save (DetailView delegates; standalone wrapped). Highlights still read-only.feat(detail): editable record highlights on the shared inline-edit dr…
fix(detail): highlight strip lookup editor honors ObjectStack `refere…
os-zhuang commentedon Jul 16, 2026
✅ Verified end-to-end — closing
All phases merged: step 0 (#2529), P1 (#2542), P2 (#2549), plus the
reference-key follow-up fix (#2587).Live browser verification (isolated stack: fresh showcase backend
objectstack dev --seed-admin --fresh+ console dev from objectui main, showcase Project "Website Relaunch"):P2 + #2587 fix
referencekey (#2407) #2587 this showed the "选择..." placeholder and "未找到选项" because the backend serves"reference": "showcase_account"(notreference_to) and HeaderHighlight didn't normalize the key.PATCH /api/v1/data/showcase_project/:id(200) carrying both fields; both values persisted across a full page reload (highlight strip resolves the new label correctly); activity feed logged a single update.P1 (verified by merged unit/e2e suites in #2542 + exercised above): shared
InlineEditContextsession, one atomic record-level save bar, draft holds only edited keys.Guardrails observed live: computed/system fields exposed no editor;
affordances.editgate honored.Unit coverage:
packages/plugin-detailsuite (23 files / 211 tests) green, including the new reference-key normalization regression tests from #2587.fix(core+data-objectstack+app-shell): canonicalize reference/referenc…
feat(detail+fields+components+app-shell): record inline-edit polish (#…
Resolve merge conflicts for #2575 (merge main into claude/github-issu…