Skip to content

Commit 2ea17e5

Browse files
os-zhuangclaude
andauthored
feat(list): make inline editing a configurable view property with a runtime toggle (#2102)
Inline cell editing was effectively hardcoded per render path. Make it a first-class, per-view property with UI to flip it: - ViewSettingsPopover: a 'Record editing' toggle (mobile/compact settings). - ListView: a desktop toolbar toggle button (Pencil) for grid views, plus an onInlineEditChange callback and the popover wiring; reads schema.inlineEdit -> editable (already existed). - app-shell ObjectView (object saved views): persist the toggle to viewDef.inlineEdit via persistViewPatch, alongside sort/filter/hiddenFields. - InterfaceListPage (page lists): read inlineEdit from the page block's userActions.editInline instead of hardcoding false — inline editing becomes a page-authored property. When on, cells edit with the dedicated @object-ui/fields widgets (#2100). type-check clean; the one ListView rules-of-hooks lint warning is pre-existing (useObjectLabel try/catch). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8d351f1 commit 2ea17e5

4 files changed

Lines changed: 63 additions & 2 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,10 @@ export function InterfaceListPage({ page, className, onConfigChange, reserveEdit
383383
showGroup: false,
384384
showColor: false,
385385
allowExport: false,
386-
inlineEdit: false,
386+
// Inline record editing is a page-authored property: a list block opts in
387+
// via `userActions.editInline` (default off). When on, clicking a cell
388+
// edits it with the dedicated field widgets, same as the object views.
389+
inlineEdit: userActions.editInline === true,
387390
};
388391
// eslint-disable-next-line react-hooks/exhaustive-deps
389392
}, [objectDefName, viewDefJson, cfg]);

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,6 +1416,9 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an
14161416
onHiddenFieldsChange={(hidden: string[]) => {
14171417
persistViewPatch(viewDef.id, viewDef, { hiddenFields: hidden });
14181418
}}
1419+
onInlineEditChange={(next: boolean) => {
1420+
persistViewPatch(viewDef.id, viewDef, { inlineEdit: next });
1421+
}}
14191422
onColumnStateChange={(state: { order?: string[]; widths?: Record<string, number> }) => {
14201423
persistViewPatch(viewDef.id, viewDef, { columnState: state });
14211424
}}

packages/plugin-list/src/ListView.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import * as React from 'react';
1010
import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder, SortBuilder, NavigationOverlay, GroupingEditor, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, RefreshIndicator, DataEmptyState } from '@object-ui/components';
1111
import type { SortItem } from '@object-ui/components';
12-
import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Group, Paintbrush, Ruler, Inbox, Download, AlignJustify, Rows4, Rows3, Rows2, Share2, Printer, Plus, Trash2, CheckSquare, AlertTriangle, RotateCw, icons, type LucideIcon } from 'lucide-react';
12+
import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Pencil, Group, Paintbrush, Ruler, Inbox, Download, AlignJustify, Rows4, Rows3, Rows2, Share2, Printer, Plus, Trash2, CheckSquare, AlertTriangle, RotateCw, icons, type LucideIcon } from 'lucide-react';
1313
import type { FilterGroup } from '@object-ui/components';
1414
import { ViewSwitcherDropdown, ViewType } from './ViewSwitcher';
1515
import { ViewSettingsPopover } from './components/ViewSettingsPopover';
@@ -31,6 +31,8 @@ export interface ListViewProps {
3131
onSearchChange?: (search: string) => void;
3232
/** Called when the user toggles fields via the Hide Fields popover. */
3333
onHiddenFieldsChange?: (hidden: string[]) => void;
34+
/** Called when the user toggles inline record editing in View settings. */
35+
onInlineEditChange?: (next: boolean) => void;
3436
/** Called when the user resizes/reorders columns in the underlying grid. */
3537
onColumnStateChange?: (state: { order?: string[]; widths?: Record<string, number> }) => void;
3638
/** Callback when a row/item is clicked (overrides NavigationConfig) */
@@ -322,6 +324,7 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
322324
onSortChange,
323325
onSearchChange,
324326
onHiddenFieldsChange,
327+
onInlineEditChange,
325328
onColumnStateChange,
326329
onRowClick,
327330
showViewSwitcher: showViewSwitcherProp,
@@ -1639,6 +1642,24 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
16391642
<div className="h-4 w-px bg-border/60 mx-0.5" />
16401643
</>
16411644
)}
1645+
{/* Inline edit — toggle record editing for this (grid) view. Persists
1646+
`inlineEdit` on the view via onInlineEditChange. */}
1647+
{currentView === 'grid' && onInlineEditChange && !toolbarFlags.compactToolbar && (
1648+
<Button
1649+
variant="ghost"
1650+
size="sm"
1651+
onClick={() => onInlineEditChange(!schema.inlineEdit)}
1652+
className={cn(
1653+
"hidden sm:inline-flex h-7 px-2 text-muted-foreground hover:text-primary text-xs transition-colors duration-150",
1654+
schema.inlineEdit && "text-primary"
1655+
)}
1656+
title={t('list.inlineEditLabel', { defaultValue: 'Edit records inline (click a cell to edit)' })}
1657+
data-testid="toolbar-inline-edit-toggle"
1658+
>
1659+
<Pencil className="h-3.5 w-3.5 mr-1.5" />
1660+
<span className="hidden sm:inline">{t('list.inlineEditShort', { defaultValue: 'Edit inline' })}</span>
1661+
</Button>
1662+
)}
16421663
{/* Hide Fields — hidden on mobile (collapsed into ViewSettingsPopover) */}
16431664
{toolbarFlags.showHideFields && !toolbarFlags.compactToolbar && (
16441665
<Popover open={showHideFields} onOpenChange={setShowHideFields}>
@@ -1982,6 +2003,9 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
19822003
showHideFields={toolbarFlags.showHideFields}
19832004
hiddenFields={hiddenFields}
19842005
updateHiddenFields={updateHiddenFields}
2006+
showInlineEdit={currentView === 'grid'}
2007+
inlineEdit={!!schema.inlineEdit}
2008+
setInlineEdit={(v) => onInlineEditChange?.(v)}
19852009
/>
19862010
)}
19872011

packages/plugin-list/src/components/ViewSettingsPopover.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ export interface ViewSettingsPopoverProps {
7070
showHideFields?: boolean;
7171
hiddenFields?: Set<string>;
7272
updateHiddenFields?: (next: Set<string>) => void;
73+
74+
/** Record editing — toggle inline cell editing (persists `inlineEdit` on the view). */
75+
showInlineEdit?: boolean;
76+
inlineEdit?: boolean;
77+
setInlineEdit?: (next: boolean) => void;
7378
}
7479

7580
interface SectionProps {
@@ -131,6 +136,9 @@ export function ViewSettingsPopover(props: ViewSettingsPopoverProps) {
131136
showHideFields,
132137
hiddenFields,
133138
updateHiddenFields,
139+
showInlineEdit,
140+
inlineEdit,
141+
setInlineEdit,
134142
} = props;
135143

136144
const [open, setOpen] = React.useState(false);
@@ -141,6 +149,7 @@ export function ViewSettingsPopover(props: ViewSettingsPopoverProps) {
141149
!!rowColorConfig?.field,
142150
density && density.mode !== 'compact',
143151
(hiddenFields?.size ?? 0) > 0,
152+
!!inlineEdit,
144153
].filter(Boolean).length;
145154

146155
const DensityIcon =
@@ -266,6 +275,28 @@ export function ViewSettingsPopover(props: ViewSettingsPopoverProps) {
266275
</Section>
267276
)}
268277

278+
{showInlineEdit && setInlineEdit && (
279+
<Section
280+
title={t('list.recordEditingTitle', { defaultValue: 'Record editing' })}
281+
defaultOpen={!!inlineEdit}
282+
>
283+
<label className="flex items-center gap-2 text-xs py-1 px-1 rounded hover:bg-muted cursor-pointer">
284+
<input
285+
type="checkbox"
286+
checked={!!inlineEdit}
287+
onChange={() => setInlineEdit(!inlineEdit)}
288+
className="rounded border-input"
289+
data-testid="view-settings-inline-edit"
290+
/>
291+
<span>
292+
{t('list.inlineEditLabel', {
293+
defaultValue: 'Edit records inline (click a cell to edit)',
294+
})}
295+
</span>
296+
</label>
297+
</Section>
298+
)}
299+
269300
{showHideFields && hiddenFields && updateHiddenFields && (
270301
<Section
271302
title={t('list.hideFieldsTitle', { defaultValue: 'Hide fields' })}

0 commit comments

Comments
 (0)