Skip to content

Commit 0ccb1df

Browse files
authored
Merge pull request #891 from objectstack-ai/copilot/fix-listview-navigation-issue
2 parents b0dfbda + 968eb4f commit 0ccb1df

File tree

4 files changed

+113
-6
lines changed

4 files changed

+113
-6
lines changed

ROADMAP.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,21 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
10501050

10511051
**Tests:** All 32 ObjectView tests and 29 useNavigationOverlay tests pass.
10521052

1053+
### ListView Multi-Navigation Mode Support — split/popover/page/new_window (February 2026)
1054+
1055+
**Root Cause:** While `drawer`/`modal` modes worked in the Console ObjectView, the remaining 4 navigation modes had gaps:
1056+
1. Console's `onNavigate` callback relied on implicit fallthrough for `view` action (page mode) — not explicit.
1057+
2. `PluginObjectView`'s `formLayout` only mapped `drawer`/`modal` modes; `split`/`popover` fell through to the default layout (`drawer`), rendering the wrong overlay type.
1058+
3. `PluginObjectView` lacked `NavigationOverlay` integration for `split` (resizable side-by-side panels) and `popover` (compact dialog preview).
1059+
1060+
**Fix:**
1061+
- Console `onNavigate` now explicitly checks for `action === 'view'` (page mode) alongside the existing `'new_window'` check.
1062+
- `PluginObjectView` `formLayout` now includes `split` and `popover` branches.
1063+
- `PluginObjectView` imports and renders `NavigationOverlay` from `@object-ui/components` for both `split` mode (with `mainContent` wrapping the grid) and `popover` mode (Dialog fallback when no `popoverTrigger`).
1064+
- Split mode close button properly resets form state via `handleFormCancel`.
1065+
1066+
**Tests:** Updated split/popover tests to verify `NavigationOverlay` rendering (close panel button for split, dialog role for popover). Added split close-and-return test. All 29 PluginObjectView tests and 37 Console ObjectView tests pass.
1067+
10531068
### ListView Grouping Mode Empty Rows (February 2026)
10541069

10551070
**Root Cause:** When grouping is enabled in list view, `buildGroupTableSchema` in `ObjectGrid.tsx` sets `pagination: false` but inherits `pageSize: 10` from the parent schema. The `DataTableRenderer` filler row logic (`Array.from({ length: Math.max(0, pageSize - paginatedData.length) })`) pads each group table with empty rows up to `pageSize`, creating many blank lines.

apps/console/src/components/ObjectView.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -433,10 +433,14 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
433433
return;
434434
}
435435
// page / view mode — navigate to record detail page
436-
if (viewId) {
437-
navigate(`../../record/${String(recordId)}`, { relative: 'path' });
438-
} else {
439-
navigate(`record/${String(recordId)}`);
436+
// Handles action === 'view' (from useNavigationOverlay page mode) and
437+
// default fallthrough for any unrecognised action
438+
if (action === 'view' || !action || action === 'page') {
439+
if (viewId) {
440+
navigate(`../../record/${String(recordId)}`, { relative: 'path' });
441+
} else {
442+
navigate(`record/${String(recordId)}`);
443+
}
440444
}
441445
},
442446
});

packages/plugin-view/src/ObjectView.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
DrawerHeader,
5050
DrawerTitle,
5151
DrawerDescription,
52+
NavigationOverlay,
5253
Button,
5354
Tabs,
5455
TabsList,
@@ -974,8 +975,57 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
974975
// Determine which form container to render
975976
const formLayout = navigationConfig?.mode === 'modal' ? 'modal'
976977
: navigationConfig?.mode === 'drawer' ? 'drawer'
978+
: navigationConfig?.mode === 'split' ? 'split'
979+
: navigationConfig?.mode === 'popover' ? 'popover'
977980
: layout;
978981

982+
// Build the record detail content for NavigationOverlay (split/popover modes)
983+
const renderOverlayDetail = (record: Record<string, unknown>) => (
984+
<div className="space-y-3">
985+
<ObjectForm schema={buildFormSchema()} dataSource={dataSource} />
986+
</div>
987+
);
988+
989+
// Shared handler for NavigationOverlay onOpenChange — close form when overlay is dismissed
990+
const handleOverlayOpenChange = useCallback((open: boolean) => {
991+
if (!open) handleFormCancel();
992+
}, [handleFormCancel]);
993+
994+
// For split mode, wrap content inside NavigationOverlay with mainContent
995+
if (formLayout === 'split') {
996+
const objectLabel = (objectSchema?.label as string) || schema.objectName;
997+
return (
998+
<div className={cn('flex flex-col h-full min-w-0 overflow-hidden', className)}>
999+
{(schema.title || schema.description) && (
1000+
<div className="mb-4 shrink-0">
1001+
{schema.title && <h2 className="text-2xl font-bold tracking-tight">{schema.title}</h2>}
1002+
{schema.description && <p className="text-muted-foreground mt-1">{schema.description}</p>}
1003+
</div>
1004+
)}
1005+
<div className="mb-4 shrink-0">{renderToolbar()}</div>
1006+
<div className="flex-1 min-h-0 min-w-0 overflow-hidden">
1007+
{isFormOpen && selectedRecord ? (
1008+
<NavigationOverlay
1009+
isOpen={isFormOpen}
1010+
selectedRecord={selectedRecord}
1011+
mode="split"
1012+
close={handleFormCancel}
1013+
setIsOpen={handleOverlayOpenChange}
1014+
width={navigationConfig?.width}
1015+
isOverlay={true}
1016+
title={`${objectLabel} Detail`}
1017+
mainContent={<div className="h-full overflow-auto">{renderContent()}</div>}
1018+
>
1019+
{renderOverlayDetail}
1020+
</NavigationOverlay>
1021+
) : (
1022+
renderContent()
1023+
)}
1024+
</div>
1025+
</div>
1026+
);
1027+
}
1028+
9791029
return (
9801030
<div className={cn('flex flex-col h-full min-w-0 overflow-hidden', className)}>
9811031
{/* Title and description */}
@@ -1003,6 +1053,21 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
10031053
{/* Form (drawer or modal) */}
10041054
{formLayout === 'drawer' && renderDrawerForm()}
10051055
{formLayout === 'modal' && renderModalForm()}
1056+
{/* Popover mode — uses NavigationOverlay Dialog fallback (no popoverTrigger) */}
1057+
{formLayout === 'popover' && isFormOpen && selectedRecord && (
1058+
<NavigationOverlay
1059+
isOpen={isFormOpen}
1060+
selectedRecord={selectedRecord}
1061+
mode="popover"
1062+
close={handleFormCancel}
1063+
setIsOpen={handleOverlayOpenChange}
1064+
width={navigationConfig?.width}
1065+
isOverlay={true}
1066+
title={getFormTitle()}
1067+
>
1068+
{renderOverlayDetail}
1069+
</NavigationOverlay>
1070+
)}
10061071
</div>
10071072
);
10081073
};

packages/plugin-view/src/__tests__/ObjectView.test.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,9 @@ describe('ObjectView', () => {
300300

301301
fireEvent.click(screen.getByTestId('grid-row'));
302302

303-
// Split mode should trigger form display in view mode
303+
// Split mode renders NavigationOverlay with split panels including a close button
304304
expect(screen.getByTestId('object-form')).toBeDefined();
305+
expect(screen.getByLabelText('Close panel')).toBeDefined();
305306
});
306307

307308
it('should open form in view mode when popover navigation mode is clicked', () => {
@@ -315,8 +316,30 @@ describe('ObjectView', () => {
315316

316317
fireEvent.click(screen.getByTestId('grid-row'));
317318

318-
// Popover mode should trigger form display in view mode
319+
// Popover mode renders NavigationOverlay Dialog fallback (no popoverTrigger)
319320
expect(screen.getByTestId('object-form')).toBeDefined();
321+
expect(screen.getByRole('dialog')).toBeDefined();
322+
});
323+
324+
it('should close split panel and return to normal view', () => {
325+
const schema: ObjectViewSchema = {
326+
type: 'object-view',
327+
objectName: 'contacts',
328+
navigation: { mode: 'split' },
329+
};
330+
331+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
332+
333+
// Open split panel
334+
fireEvent.click(screen.getByTestId('grid-row'));
335+
expect(screen.getByLabelText('Close panel')).toBeDefined();
336+
337+
// Close split panel
338+
fireEvent.click(screen.getByLabelText('Close panel'));
339+
340+
// Form should be gone, grid should remain
341+
expect(screen.queryByLabelText('Close panel')).toBeNull();
342+
expect(screen.getByTestId('object-grid')).toBeDefined();
320343
});
321344
});
322345

0 commit comments

Comments
 (0)