From d5e67105bd6499d549d9b0741530b40651d7dcaf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 04:02:00 +0000 Subject: [PATCH 1/4] Initial plan From 98253b8a87f40648a35a3270ba1b47ffd098d4d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 04:10:00 +0000 Subject: [PATCH 2/4] feat: add updateViewConfig to DataSource interface and persist view config on save - Add optional updateViewConfig method to DataSource interface in @object-ui/types - Update ObjectView.handleViewConfigSave to call dataSource.updateViewConfig when available - Log warning when updateViewConfig is not available (backward compatible) - Log error when updateViewConfig call fails - Add 3 new tests covering persistence, fallback warning, and error handling - Fix Button/Input/Switch mocks in ObjectView tests to pass through data-testid Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../console/src/__tests__/ObjectView.test.tsx | 98 ++++++++++++++++++- apps/console/src/components/ObjectView.tsx | 11 ++- packages/types/src/data.ts | 13 +++ 3 files changed, 119 insertions(+), 3 deletions(-) diff --git a/apps/console/src/__tests__/ObjectView.test.tsx b/apps/console/src/__tests__/ObjectView.test.tsx index 15d93ce074..48bd71eb8f 100644 --- a/apps/console/src/__tests__/ObjectView.test.tsx +++ b/apps/console/src/__tests__/ObjectView.test.tsx @@ -24,8 +24,16 @@ vi.mock('@object-ui/components', async (importOriginal) => { return { ...actual, cn: (...inputs: any[]) => inputs.filter(Boolean).join(' '), - Button: ({ children, onClick, title }: any) => , - Input: (props: any) => , + Button: ({ children, onClick, title, ...rest }: any) => , + Input: ({ ...props }: any) => , + Switch: ({ checked, onCheckedChange, ...props }: any) => ( + , Tabs: ({ value, onValueChange, children }: any) => ( @@ -338,4 +346,90 @@ describe('ObjectView Component', () => { const footer = await screen.findByTestId('record-count-footer'); expect(footer).toBeInTheDocument(); }); + + it('calls dataSource.updateViewConfig when saving view config', async () => { + const mockUpdateViewConfig = vi.fn().mockResolvedValue({}); + const dsWithUpdate = { + ...mockDataSource, + updateViewConfig: mockUpdateViewConfig, + }; + mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' }; + mockUseParams.mockReturnValue({ objectName: 'opportunity' }); + + render(); + + // Open config panel + fireEvent.click(screen.getByTitle('console.objectView.designTools')); + fireEvent.click(screen.getByText('console.objectView.editView')); + expect(screen.getByTestId('view-config-panel')).toBeInTheDocument(); + + // Wait for draft to be initialized from activeView, then modify + const titleInput = await screen.findByDisplayValue('All Opportunities'); + fireEvent.change(titleInput, { target: { value: 'My Custom View' } }); + + // Save button should appear after dirty state + const saveBtn = await screen.findByTestId('view-config-save'); + fireEvent.click(saveBtn); + + expect(mockUpdateViewConfig).toHaveBeenCalledOnce(); + expect(mockUpdateViewConfig).toHaveBeenCalledWith( + 'opportunity', + 'all', + expect.objectContaining({ label: 'My Custom View' }), + ); + }); + + it('logs warning when dataSource.updateViewConfig is not available', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' }; + mockUseParams.mockReturnValue({ objectName: 'opportunity' }); + + render(); + + // Open config panel + fireEvent.click(screen.getByTitle('console.objectView.designTools')); + fireEvent.click(screen.getByText('console.objectView.editView')); + + // Make a change and save + const titleInput = await screen.findByDisplayValue('All Opportunities'); + fireEvent.change(titleInput, { target: { value: 'Changed' } }); + const saveBtn = await screen.findByTestId('view-config-save'); + fireEvent.click(saveBtn); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('updateViewConfig is not available'), + ); + warnSpy.mockRestore(); + }); + + it('logs error when dataSource.updateViewConfig rejects', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const dsWithFailingUpdate = { + ...mockDataSource, + updateViewConfig: vi.fn().mockRejectedValue(new Error('Network error')), + }; + mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' }; + mockUseParams.mockReturnValue({ objectName: 'opportunity' }); + + render(); + + // Open config panel + fireEvent.click(screen.getByTitle('console.objectView.designTools')); + fireEvent.click(screen.getByText('console.objectView.editView')); + + // Make a change and save + const titleInput = await screen.findByDisplayValue('All Opportunities'); + fireEvent.change(titleInput, { target: { value: 'Failed' } }); + const saveBtn = await screen.findByTestId('view-config-save'); + fireEvent.click(saveBtn); + + // Wait for the promise rejection to be caught + await vi.waitFor(() => { + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('Failed to persist view config'), + expect.any(Error), + ); + }); + errorSpy.mockRestore(); + }); }); diff --git a/apps/console/src/components/ObjectView.tsx b/apps/console/src/components/ObjectView.tsx index 441455e2b4..19c2884021 100644 --- a/apps/console/src/components/ObjectView.tsx +++ b/apps/console/src/components/ObjectView.tsx @@ -72,7 +72,16 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) { const handleViewConfigSave = useCallback((draft: Record) => { setViewDraft(draft); setRefreshKey(k => k + 1); - }, []); + + // Persist to backend if dataSource supports it + if (dataSource?.updateViewConfig) { + dataSource.updateViewConfig(objectName || '', draft.id || '', draft).catch((err: any) => { + console.error('[ViewConfigPanel] Failed to persist view config:', err); + }); + } else { + console.warn('[ViewConfigPanel] dataSource.updateViewConfig is not available. View config saved locally only.'); + } + }, [dataSource, objectName]); const handleOpenEditor = useCallback((editor: EditorPanelType) => { console.info('[ViewConfigPanel] Open editor:', editor); diff --git a/packages/types/src/data.ts b/packages/types/src/data.ts index b775708c03..4aa18b825b 100644 --- a/packages/types/src/data.ts +++ b/packages/types/src/data.ts @@ -237,6 +237,19 @@ export interface DataSource { */ getView?(objectName: string, viewId: string): Promise; + /** + * Persist a view configuration to the backend. + * Called when a user saves view settings (columns, filters, sort, toggles, etc.) + * from the inline ViewConfigPanel. + * Optional — implementations that do not support view persistence may omit this. + * + * @param objectName - Object name + * @param viewId - View identifier (e.g., 'all', 'pipeline') + * @param config - The full view configuration to persist + * @returns Promise resolving to the persisted config (or void) + */ + updateViewConfig?(objectName: string, viewId: string, config: Record): Promise | void>; + /** * Get an application definition by name or ID. * Used by app shells to render server-defined navigation, branding, and layout. From 2763aa23ad599855abb55064eeb8313ec9eb18f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 04:12:23 +0000 Subject: [PATCH 3/4] fix: validate objectName/viewId before calling updateViewConfig, clean up Input mock Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- apps/console/src/__tests__/ObjectView.test.tsx | 2 +- apps/console/src/components/ObjectView.tsx | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/console/src/__tests__/ObjectView.test.tsx b/apps/console/src/__tests__/ObjectView.test.tsx index 48bd71eb8f..1fbe334870 100644 --- a/apps/console/src/__tests__/ObjectView.test.tsx +++ b/apps/console/src/__tests__/ObjectView.test.tsx @@ -25,7 +25,7 @@ vi.mock('@object-ui/components', async (importOriginal) => { ...actual, cn: (...inputs: any[]) => inputs.filter(Boolean).join(' '), Button: ({ children, onClick, title, ...rest }: any) => , - Input: ({ ...props }: any) => , + Input: (props: any) => , Switch: ({ checked, onCheckedChange, ...props }: any) => (