Skip to content

Commit 471c5d3

Browse files
authored
feat(detail): editable record highlights on the shared inline-edit draft (#2549)
objectui#2407 P2 (final phase). Makes the highlights strip editable in place, sharing ONE draft + ONE atomic Save with the details body. HeaderHighlight consumes useInlineEdit() (hover-pencil/double-click enters the shared session; renders the same InlineFieldInput as the body; computed/readonly/system highlights expose no editor; compact-layout expand-on-edit). RecordDetailView (app-shell) hosts ONE InlineEditProvider spanning record:highlights + record:details plus the single record-level InlineEditSaveBar; record:details drops its P1-local provider/bar and consumes the shared context; record:highlights threads the DataSource through. Guardrails preserved: computed/readonly/system non-editable, canEdit gate, OCC ifMatch + ConcurrentUpdateDialog, only-edited-keys. Verified: 205 tests (22 files) incl. a new HeaderHighlight editable suite; plugin-detail type-check + vite build clean; app-shell type-check clean w.r.t. this change. Browser-verified in real Chromium (harness mirroring the app-shell provider structure): double-click a highlight → whole record enters edit → a highlight edit + a body edit commit together in exactly ONE update({budget,health},{ifMatch:v1}); computed highlight exposes no editor. Completes #2407 across step 0 (#2529) + P1 (#2542) + P2.
1 parent ddcc7d6 commit 471c5d3

6 files changed

Lines changed: 348 additions & 161 deletions

File tree

.changeset/editable-highlights.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@object-ui/plugin-detail": minor
3+
"@object-ui/app-shell": minor
4+
---
5+
6+
feat(detail): editable record highlights on the shared inline-edit draft (objectui#2407 P2)
7+
8+
The highlights strip is now editable in place and shares ONE draft + ONE atomic
9+
Save with the details body (building on the P1 `InlineEditContext` / `#2529`
10+
`InlineFieldInput`).
11+
12+
- **`HeaderHighlight`** consumes `useInlineEdit()`: hovering a highlight shows a
13+
pencil and double-click enters the shared record edit session; each editable
14+
highlight renders the same `<InlineFieldInput>` the body uses (value =
15+
`draft[name] ?? data[name]`, write via `setField`). Computed
16+
(`formula`/`summary`/`rollup`/`auto_number`), `readonly`, and system fields
17+
expose no editor. Empty highlights are kept while editing so they can be
18+
filled. Compact-layout UX: an actively-edited column widens and renders the
19+
editor full-width (Salesforce-style expand-on-edit).
20+
- **`RecordDetailView`** (app-shell) hosts ONE `<InlineEditProvider>` (with the
21+
object-lifecycle `canEdit` gate) spanning both `record:highlights` and
22+
`record:details`, plus the single record-level `<InlineEditSaveBar>` — so a
23+
highlight edit and a body edit commit together in ONE
24+
`update(obj, id, draft, { ifMatch })`.
25+
- **`record:details`** drops its P1-local provider/save bar (it would otherwise
26+
split the draft from the highlights) and just consumes the shared context;
27+
**`record:highlights`** threads the DataSource through for lookup/user editors.
28+
29+
Guardrails preserved: computed/readonly/system highlights non-editable; `canEdit`
30+
gate; OCC (`ifMatch` + `ConcurrentUpdateDialog`); only user-edited keys are sent.

packages/app-shell/src/views/RecordDetailView.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010

1111
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
1212
import { useParams, useNavigate, useLocation, useSearchParams, Link } from 'react-router-dom';
13-
import { RecordChatterPanel, buildDefaultPageSchema, deriveFieldGroupDetailSections, extractMentions } from '@object-ui/plugin-detail';
13+
import { RecordChatterPanel, InlineEditSaveBar, buildDefaultPageSchema, deriveFieldGroupDetailSections, extractMentions } from '@object-ui/plugin-detail';
1414
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
1515
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
16-
import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider, useGlobalUndo, useDataInvalidation, notifyDataChanged } from '@object-ui/react';
16+
import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider, InlineEditProvider, useGlobalUndo, useDataInvalidation, notifyDataChanged } from '@object-ui/react';
1717
import { buildExpandFields } from '@object-ui/core';
1818
import { toast } from 'sonner';
1919
import { useRecordPresence, PresenceAvatars } from '@object-ui/collaboration';
@@ -1737,6 +1737,11 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
17371737
isFavorite={isRecordFavorite}
17381738
onToggleFavorite={favoriteRecord ? handleToggleRecordFavorite : undefined}
17391739
>
1740+
{/* objectui#2407 P2 — ONE record-level inline-edit session spanning
1741+
the highlights strip AND the details body (both descend from this
1742+
provider), committed together by the single <InlineEditSaveBar>
1743+
below. `canEdit` carries the object-lifecycle / permission gate. */}
1744+
<InlineEditProvider canEdit={resolveCrudAffordances(objectDef as any).edit}>
17401745
<HighlightFieldsProvider>
17411746
<DiscussionContextProvider
17421747
items={feedItems as any}
@@ -1824,6 +1829,18 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
18241829
/>
18251830
</div>
18261831
)}
1832+
{/* Record-level inline-edit Save/Cancel bar (objectui#2407 P2) —
1833+
commits the whole draft (highlights + body) in ONE atomic OCC
1834+
update; renders (sticky) only while editing. */}
1835+
<InlineEditSaveBar
1836+
dataSource={dataSource}
1837+
objectName={objectName}
1838+
recordId={pureRecordId}
1839+
data={pageRecord}
1840+
refresh={notifyRecordChanged}
1841+
fieldLabelFor={(name: string) => (objectDef as any)?.fields?.[name]?.label || fieldLabel(objectName || '', name, name)}
1842+
locked={(pageRecord as any)?.approval_status === 'pending' || (pageRecord as any)?.approval_status === 'in_approval'}
1843+
/>
18271844
</div>
18281845
<MetadataPanel
18291846
open={showDebug}
@@ -1834,6 +1851,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
18341851
</ActionProvider>
18351852
</DiscussionContextProvider>
18361853
</HighlightFieldsProvider>
1854+
</InlineEditProvider>
18371855
</RecordContextProvider>
18381856

18391857
{/* Action Confirm Dialog */}

0 commit comments

Comments
 (0)