Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,21 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th

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

### ListView Multi-Navigation Mode Support — split/popover/page/new_window (February 2026)

**Root Cause:** While `drawer`/`modal` modes worked in the Console ObjectView, the remaining 4 navigation modes had gaps:
1. Console's `onNavigate` callback relied on implicit fallthrough for `view` action (page mode) — not explicit.
2. `PluginObjectView`'s `formLayout` only mapped `drawer`/`modal` modes; `split`/`popover` fell through to the default layout (`drawer`), rendering the wrong overlay type.
3. `PluginObjectView` lacked `NavigationOverlay` integration for `split` (resizable side-by-side panels) and `popover` (compact dialog preview).

**Fix:**
- Console `onNavigate` now explicitly checks for `action === 'view'` (page mode) alongside the existing `'new_window'` check.
- `PluginObjectView` `formLayout` now includes `split` and `popover` branches.
- `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`).
- Split mode close button properly resets form state via `handleFormCancel`.

**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.

### ListView Grouping Mode Empty Rows (February 2026)

**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.
Expand Down
12 changes: 8 additions & 4 deletions apps/console/src/components/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -433,10 +433,14 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
return;
}
// page / view mode — navigate to record detail page
if (viewId) {
navigate(`../../record/${String(recordId)}`, { relative: 'path' });
} else {
navigate(`record/${String(recordId)}`);
// Handles action === 'view' (from useNavigationOverlay page mode) and
// default fallthrough for any unrecognised action
if (action === 'view' || !action || action === 'page') {
if (viewId) {
navigate(`../../record/${String(recordId)}`, { relative: 'path' });
} else {
navigate(`record/${String(recordId)}`);
}
Comment on lines +436 to +443

Copilot AI Feb 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useNavigationOverlay calls onNavigate(recordId, view ?? 'view') for page mode, where view comes from navigation.view (a user-configurable target view name). With the new guard, any configured navigation.view value other than 'view' will prevent navigation entirely. Consider treating any action other than 'new_window' as page navigation (or explicitly handling navigation.view by incorporating it into the route/query) so page mode keeps working when a view name is set.

Copilot uses AI. Check for mistakes.
}
},
});
Expand Down
65 changes: 65 additions & 0 deletions packages/plugin-view/src/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
DrawerHeader,
DrawerTitle,
DrawerDescription,
NavigationOverlay,
Button,
Tabs,
TabsList,
Expand Down Expand Up @@ -974,8 +975,57 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
// Determine which form container to render
const formLayout = navigationConfig?.mode === 'modal' ? 'modal'
: navigationConfig?.mode === 'drawer' ? 'drawer'
: navigationConfig?.mode === 'split' ? 'split'
: navigationConfig?.mode === 'popover' ? 'popover'
: layout;

// Build the record detail content for NavigationOverlay (split/popover modes)
const renderOverlayDetail = (record: Record<string, unknown>) => (

Copilot AI Feb 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderOverlayDetail receives a record argument from NavigationOverlay, but it is unused (the form schema is instead derived from selectedRecord in closure). Rename the parameter to _record to avoid unused-var noise, or refactor buildFormSchema to accept the record so the overlay content is purely driven by the provided record.

Suggested change
const renderOverlayDetail = (record: Record<string, unknown>) => (
const renderOverlayDetail = (_record: Record<string, unknown>) => (

Copilot uses AI. Check for mistakes.
<div className="space-y-3">
<ObjectForm schema={buildFormSchema()} dataSource={dataSource} />
</div>
);

// Shared handler for NavigationOverlay onOpenChange — close form when overlay is dismissed
const handleOverlayOpenChange = useCallback((open: boolean) => {
if (!open) handleFormCancel();
}, [handleFormCancel]);

// For split mode, wrap content inside NavigationOverlay with mainContent
if (formLayout === 'split') {
const objectLabel = (objectSchema?.label as string) || schema.objectName;
return (
<div className={cn('flex flex-col h-full min-w-0 overflow-hidden', className)}>
{(schema.title || schema.description) && (
<div className="mb-4 shrink-0">
{schema.title && <h2 className="text-2xl font-bold tracking-tight">{schema.title}</h2>}
{schema.description && <p className="text-muted-foreground mt-1">{schema.description}</p>}
Comment on lines +998 to +1002

Copilot AI Feb 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The split-mode branch duplicates the outer layout structure (title/description/toolbar wrappers) from the main return path. This increases the risk of future UI divergence between split and non-split modes. Consider extracting a shared wrapper/layout component/function so split mode only changes the content region wiring.

Copilot uses AI. Check for mistakes.
</div>
)}
<div className="mb-4 shrink-0">{renderToolbar()}</div>
<div className="flex-1 min-h-0 min-w-0 overflow-hidden">
{isFormOpen && selectedRecord ? (
<NavigationOverlay
isOpen={isFormOpen}
selectedRecord={selectedRecord}
mode="split"
Comment on lines +1007 to +1011

Copilot AI Feb 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In split navigation mode, the overlay is only rendered when isFormOpen && selectedRecord. This means create-mode (where selectedRecord is intentionally null) will not render any form at all, and edit/view behavior becomes dependent on selectedRecord being set. Consider using split/popup overlays only for record-detail view (e.g., formMode === 'view'), or falling back to the existing drawer/modal form container for create/edit so those flows keep working when navigation.mode is split.

Copilot uses AI. Check for mistakes.
close={handleFormCancel}
setIsOpen={handleOverlayOpenChange}
width={navigationConfig?.width}
isOverlay={true}
title={`${objectLabel} Detail`}
mainContent={<div className="h-full overflow-auto">{renderContent()}</div>}
>
{renderOverlayDetail}
</NavigationOverlay>
) : (
renderContent()
)}
</div>
</div>
);
}

return (
<div className={cn('flex flex-col h-full min-w-0 overflow-hidden', className)}>
{/* Title and description */}
Expand Down Expand Up @@ -1003,6 +1053,21 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
{/* Form (drawer or modal) */}
{formLayout === 'drawer' && renderDrawerForm()}
{formLayout === 'modal' && renderModalForm()}
{/* Popover mode — uses NavigationOverlay Dialog fallback (no popoverTrigger) */}
{formLayout === 'popover' && isFormOpen && selectedRecord && (
<NavigationOverlay
isOpen={isFormOpen}
selectedRecord={selectedRecord}
mode="popover"
Comment on lines +1056 to +1061

Copilot AI Feb 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Popover mode has the same isFormOpen && selectedRecord gate. As a result, create-mode (no selectedRecord) cannot open a form when navigation.mode is popover because neither drawer/modal nor NavigationOverlay will render. Recommend ensuring create/edit continue to render via the normal form container, and reserving the popover NavigationOverlay for record-detail view only.

Copilot uses AI. Check for mistakes.
close={handleFormCancel}
setIsOpen={handleOverlayOpenChange}
width={navigationConfig?.width}
isOverlay={true}
title={getFormTitle()}
>
{renderOverlayDetail}
</NavigationOverlay>
)}
</div>
);
};
27 changes: 25 additions & 2 deletions packages/plugin-view/src/__tests__/ObjectView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,9 @@ describe('ObjectView', () => {

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

// Split mode should trigger form display in view mode
// Split mode renders NavigationOverlay with split panels including a close button
expect(screen.getByTestId('object-form')).toBeDefined();
expect(screen.getByLabelText('Close panel')).toBeDefined();
});
Comment on lines +303 to 306

Copilot AI Feb 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new assertions use toBeDefined(), which is redundant with getBy* queries (they already throw if not found) and is inconsistent with other tests in this file that use toBeInTheDocument(). Prefer expect(...).toBeInTheDocument() (or drop the matcher entirely) for clearer intent.

Copilot uses AI. Check for mistakes.

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

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

// Popover mode should trigger form display in view mode
// Popover mode renders NavigationOverlay Dialog fallback (no popoverTrigger)
expect(screen.getByTestId('object-form')).toBeDefined();
expect(screen.getByRole('dialog')).toBeDefined();
});

it('should close split panel and return to normal view', () => {
const schema: ObjectViewSchema = {
type: 'object-view',
objectName: 'contacts',
navigation: { mode: 'split' },
};

render(<ObjectView schema={schema} dataSource={mockDataSource} />);

// Open split panel
fireEvent.click(screen.getByTestId('grid-row'));
expect(screen.getByLabelText('Close panel')).toBeDefined();

// Close split panel
fireEvent.click(screen.getByLabelText('Close panel'));

// Form should be gone, grid should remain
expect(screen.queryByLabelText('Close panel')).toBeNull();
expect(screen.getByTestId('object-grid')).toBeDefined();
});
});

Expand Down