Skip to content

Commit e637678

Browse files
Copilothotlong
andcommitted
fix: make ViewConfigPanel draft changes reactive in ObjectView main view
- Fix objectViewSchema to read showSearch/showFilters from activeView instead of hardcoded true - Add handleViewUpdate callback for real-time field-by-field draft propagation - Wire onViewUpdate prop to ViewConfigPanel for live preview - Pass draft-aware views to PluginObjectView - Fix ViewConfigPanel useEffect to depend on activeView.id (prevents draft reset loop) - Add tests for draft persistence and real-time propagation Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 3150df6 commit e637678

3 files changed

Lines changed: 107 additions & 7 deletions

File tree

apps/console/src/__tests__/ViewConfigPanel.test.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,4 +551,87 @@ describe('ViewConfigPanel', () => {
551551
expect(screen.getByTestId('toggle-allowExport')).toHaveAttribute('aria-checked', 'false');
552552
expect(screen.getByTestId('toggle-addRecordViaForm')).toHaveAttribute('aria-checked', 'true');
553553
});
554+
555+
// ── Real-time draft propagation tests (issue fix) ──
556+
557+
it('keeps dirty state when re-rendered with same view ID but updated activeView', () => {
558+
const onViewUpdate = vi.fn();
559+
const { rerender } = render(
560+
<ViewConfigPanel
561+
open={true}
562+
onClose={vi.fn()}
563+
activeView={mockActiveView}
564+
objectDef={mockObjectDef}
565+
onViewUpdate={onViewUpdate}
566+
/>
567+
);
568+
569+
// Toggle showSearch — panel becomes dirty
570+
fireEvent.click(screen.getByTestId('toggle-showSearch'));
571+
expect(screen.getByTestId('view-config-footer')).toBeInTheDocument();
572+
573+
// Simulate parent re-rendering with the same view ID but merged draft
574+
// (this happens when onViewUpdate propagates to parent viewDraft → activeView)
575+
rerender(
576+
<ViewConfigPanel
577+
open={true}
578+
onClose={vi.fn()}
579+
activeView={{ ...mockActiveView, showSearch: false }}
580+
objectDef={mockObjectDef}
581+
onViewUpdate={onViewUpdate}
582+
/>
583+
);
584+
585+
// Draft footer should still be visible (isDirty should NOT reset for same view ID)
586+
expect(screen.getByTestId('view-config-footer')).toBeInTheDocument();
587+
});
588+
589+
it('resets dirty state when activeView changes to a different view ID', () => {
590+
const { rerender } = render(
591+
<ViewConfigPanel
592+
open={true}
593+
onClose={vi.fn()}
594+
activeView={mockActiveView}
595+
objectDef={mockObjectDef}
596+
/>
597+
);
598+
599+
// Make the panel dirty
600+
fireEvent.click(screen.getByTestId('toggle-showSearch'));
601+
expect(screen.getByTestId('view-config-footer')).toBeInTheDocument();
602+
603+
// Switch to a completely different view
604+
rerender(
605+
<ViewConfigPanel
606+
open={true}
607+
onClose={vi.fn()}
608+
activeView={{ id: 'pipeline', label: 'Pipeline', type: 'kanban', columns: ['name'] }}
609+
objectDef={mockObjectDef}
610+
/>
611+
);
612+
613+
// Draft should reset — footer should be gone
614+
expect(screen.queryByTestId('view-config-footer')).not.toBeInTheDocument();
615+
});
616+
617+
it('calls onViewUpdate for each real-time field change to enable live preview', () => {
618+
const onViewUpdate = vi.fn();
619+
render(
620+
<ViewConfigPanel
621+
open={true}
622+
onClose={vi.fn()}
623+
activeView={mockActiveView}
624+
objectDef={mockObjectDef}
625+
onViewUpdate={onViewUpdate}
626+
/>
627+
);
628+
629+
// Toggle multiple switches
630+
fireEvent.click(screen.getByTestId('toggle-showSearch'));
631+
fireEvent.click(screen.getByTestId('toggle-showFilters'));
632+
633+
expect(onViewUpdate).toHaveBeenCalledTimes(2);
634+
expect(onViewUpdate).toHaveBeenCalledWith('showSearch', false);
635+
expect(onViewUpdate).toHaveBeenCalledWith('showFilters', false);
636+
});
554637
});

apps/console/src/components/ObjectView.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,15 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
134134
? { ...baseView, ...viewDraft }
135135
: baseView;
136136

137+
/** Real-time draft field update — propagates each toggle/input change immediately */
138+
const handleViewUpdate = useCallback((field: string, value: any) => {
139+
setViewDraft(prev => ({
140+
...(prev || {}),
141+
id: baseView?.id,
142+
[field]: value,
143+
}));
144+
}, [baseView?.id]);
145+
137146
const handleViewChange = (newViewId: string) => {
138147
// The plugin ObjectView returns the view ID directly via onViewChange
139148
const matchedView = views.find((v: any) => v.id === newViewId);
@@ -324,21 +333,21 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
324333
);
325334
}, [activeView, objectDef, objectName, refreshKey]);
326335

327-
// Build the ObjectViewSchema for the plugin
336+
// Build the ObjectViewSchema for the plugin — reads from activeView (which merges draft)
328337
const objectViewSchema = useMemo(() => ({
329338
type: 'object-view' as const,
330339
objectName: objectDef.name,
331340
layout: 'page' as const,
332-
showSearch: true,
333-
showFilters: true,
341+
showSearch: activeView?.showSearch !== false,
342+
showFilters: activeView?.showFilters !== false,
334343
showCreate: false, // We render our own create button in the header
335344
showRefresh: true,
336345
onNavigate: (recordId: string | number, mode: 'view' | 'edit') => {
337346
if (mode === 'edit') {
338347
onEdit?.({ _id: recordId, id: recordId });
339348
}
340349
},
341-
}), [objectDef.name, onEdit]);
350+
}), [objectDef.name, onEdit, activeView?.showSearch, activeView?.showFilters]);
342351

343352
return (
344353
<div className="h-full flex flex-col bg-background">
@@ -471,7 +480,11 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
471480
key={refreshKey}
472481
schema={objectViewSchema}
473482
dataSource={dataSource}
474-
views={views}
483+
views={views.map((v: any) =>
484+
v.id === activeViewId && viewDraft && viewDraft.id === v.id
485+
? { ...v, ...viewDraft }
486+
: v
487+
)}
475488
activeViewId={activeViewId}
476489
onViewChange={handleViewChange}
477490
onEdit={(record: any) => onEdit?.(record)}
@@ -503,6 +516,7 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
503516
recordCount={recordCount}
504517
onOpenEditor={handleOpenEditor}
505518
onSave={handleViewConfigSave}
519+
onViewUpdate={handleViewUpdate}
506520
/>
507521
</div>
508522

apps/console/src/components/ViewConfigPanel.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,14 @@ export function ViewConfigPanel({ open, onClose, activeView, objectDef, onViewUp
110110
const [draft, setDraft] = useState<Record<string, any>>({});
111111
const [isDirty, setIsDirty] = useState(false);
112112

113-
// Reset draft when activeView changes (e.g. switching views)
113+
// Reset draft when switching to a different view (by ID change only).
114+
// We intentionally depend on activeView.id rather than the full activeView
115+
// object so that real-time draft propagation (via onViewUpdate → parent
116+
// setViewDraft → merged activeView) does not reset isDirty to false.
114117
useEffect(() => {
115118
setDraft({ ...activeView });
116119
setIsDirty(false);
117-
}, [activeView]);
120+
}, [activeView.id]);
118121

119122
// Focus the panel when it opens for keyboard accessibility
120123
useEffect(() => {

0 commit comments

Comments
 (0)