>({});
const [isDirty, setIsDirty] = useState(false);
@@ -260,9 +279,9 @@ export function ViewConfigPanel({ open, onClose, activeView, objectDef, onViewUp
// object so that real-time draft propagation (via onViewUpdate → parent
// setViewDraft → merged activeView) does not reset isDirty to false.
useEffect(() => {
- setDraft({ ...activeView });
- setIsDirty(false);
- }, [activeView.id]);
+ setDraft({ ...effectiveActiveView });
+ setIsDirty(mode === 'create');
+ }, [mode === 'create' ? mode : activeView.id]);
// Focus the panel when it opens for keyboard accessibility
useEffect(() => {
@@ -280,16 +299,27 @@ export function ViewConfigPanel({ open, onClose, activeView, objectDef, onViewUp
/** Discard all draft changes */
const handleDiscard = useCallback(() => {
+ if (mode === 'create') {
+ onClose();
+ return;
+ }
setDraft({ ...activeView });
setIsDirty(false);
- }, [activeView]);
+ }, [activeView, mode, onClose]);
/** Save draft via parent callback */
const handleSave = useCallback(() => {
- onSave?.(draft);
+ if (mode === 'create') {
+ onCreate?.(draft);
+ } else {
+ onSave?.(draft);
+ }
setIsDirty(false);
- }, [draft, onSave]);
+ }, [draft, onSave, onCreate, mode]);
+ const panelTitle = mode === 'create'
+ ? t('console.objectView.createView')
+ : t('console.objectView.configureView');
const viewLabel = draft.label || draft.id || activeView.id;
const viewType = draft.type || 'grid';
@@ -361,6 +391,12 @@ export function ViewConfigPanel({ open, onClose, activeView, objectDef, onViewUp
updateDraft('columns', currentCols);
}, [draft.columns, updateDraft]);
+ /** Handle type-specific option change (e.g., kanban.groupByField, calendar.startDateField) */
+ const handleTypeOptionChange = useCallback((typeKey: string, optionKey: string, value: any) => {
+ const current = draft[typeKey] || {};
+ updateDraft(typeKey, { ...current, [optionKey]: value });
+ }, [draft, updateDraft]);
+
if (!open) return null;
return (
@@ -368,14 +404,14 @@ export function ViewConfigPanel({ open, onClose, activeView, objectDef, onViewUp
ref={panelRef}
data-testid="view-config-panel"
role="complementary"
- aria-label={t('console.objectView.configureView')}
+ aria-label={panelTitle}
tabIndex={-1}
className="absolute inset-y-0 right-0 w-full sm:w-72 lg:w-80 sm:relative sm:inset-auto border-l bg-background flex flex-col shrink-0 z-20 transition-all overflow-hidden"
>
{/* Panel Header */}
- {t('console.objectView.configureView')}
+ {panelTitle}
+ {/* Type-Specific Options Section */}
+ {viewType !== 'grid' && (
+ <>
+
+
+ {viewType === 'kanban' && (
+
+ {t('console.objectView.groupByField')}
+
+
+ )}
+ {viewType === 'calendar' && (
+ <>
+
+ {t('console.objectView.startDateField')}
+
+
+
+ {t('console.objectView.titleField')}
+
+
+ >
+ )}
+ {viewType === 'map' && (
+ <>
+
+ {t('console.objectView.latitudeField')}
+
+
+
+ {t('console.objectView.longitudeField')}
+
+
+ >
+ )}
+ {viewType === 'gallery' && (
+
+ {t('console.objectView.imageField')}
+
+
+ )}
+ {(viewType === 'timeline' || viewType === 'gantt') && (
+ <>
+
+ {t('console.objectView.dateField')}
+
+
+
+ {t('console.objectView.titleField')}
+
+
+ >
+ )}
+
+ >
+ )}
+ {viewType === 'grid' && (
+
+ )}
+
{/* User Filters Section */}
diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts
index f6fde1052f..ef093b2325 100644
--- a/packages/i18n/src/locales/en.ts
+++ b/packages/i18n/src/locales/en.ts
@@ -214,6 +214,19 @@ const en = {
columnsConfigured: '{{count}} columns',
save: 'Save',
discard: 'Discard',
+ createView: 'Create View',
+ newView: 'New View',
+ advancedEditor: 'Advanced Editor',
+ typeOptions: 'Type Options',
+ groupByField: 'Group by field',
+ startDateField: 'Start date field',
+ titleField: 'Title field',
+ latitudeField: 'Latitude field',
+ longitudeField: 'Longitude field',
+ imageField: 'Image field',
+ dateField: 'Date field',
+ selectField: 'Select field...',
+ gridOptionsHint: 'Grid view uses the columns configured above.',
},
localeSwitcher: {
label: 'Language',
diff --git a/packages/types/src/designer.ts b/packages/types/src/designer.ts
index e7a56513f5..4af51160a9 100644
--- a/packages/types/src/designer.ts
+++ b/packages/types/src/designer.ts
@@ -443,6 +443,113 @@ export interface ViewDesignerSchema extends BaseSchema {
onCancel?: string;
}
+// ============================================================================
+// Unified View Configuration
+// ============================================================================
+
+/** View type union */
+export type UnifiedViewType = 'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart';
+
+/**
+ * Unified data model for view configuration.
+ *
+ * Used by both ViewConfigPanel (create/edit) and ViewDesigner (advanced editor).
+ * Columns may be simple field-name strings or rich ViewDesignerColumn objects;
+ * consumers should handle both.
+ */
+export interface UnifiedViewConfig {
+ /** View identifier */
+ id?: string;
+ /** Display label */
+ label?: string;
+ /** View type */
+ type?: UnifiedViewType;
+ /** Column configuration — simple field names or rich ViewDesignerColumn objects */
+ columns?: Array;
+ /** Filter conditions in @objectstack/spec JSON-rules array format */
+ filter?: any[];
+ /** Sort configuration */
+ sort?: Array<{ field: string; order?: string; direction?: string; id?: string }>;
+ /** Description */
+ description?: string;
+ /** Enable search bar */
+ showSearch?: boolean;
+ /** Enable user filter controls */
+ showFilters?: boolean;
+ /** Enable user sort controls */
+ showSort?: boolean;
+ /** Allow data export */
+ allowExport?: boolean;
+ /** Show view description */
+ showDescription?: boolean;
+ /** Enable "add record via form" action */
+ addRecordViaForm?: boolean;
+ /** Export options */
+ exportOptions?: any;
+
+ // -- Type-specific options (nested per @objectstack/spec protocol) ----------
+
+ /** Kanban-specific options */
+ kanban?: {
+ groupByField?: string;
+ groupField?: string;
+ titleField?: string;
+ columns?: string[];
+ };
+ /** Calendar-specific options */
+ calendar?: {
+ startDateField?: string;
+ endDateField?: string;
+ titleField?: string;
+ colorField?: string;
+ allDayField?: string;
+ defaultView?: string;
+ };
+ /** Map-specific options */
+ map?: {
+ locationField?: string;
+ titleField?: string;
+ latitudeField?: string;
+ longitudeField?: string;
+ zoom?: number;
+ center?: { lat: number; lng: number };
+ };
+ /** Gallery-specific options */
+ gallery?: {
+ imageField?: string;
+ titleField?: string;
+ subtitleField?: string;
+ };
+ /** Timeline-specific options */
+ timeline?: {
+ dateField?: string;
+ titleField?: string;
+ descriptionField?: string;
+ };
+ /** Gantt-specific options */
+ gantt?: {
+ startDateField?: string;
+ endDateField?: string;
+ titleField?: string;
+ progressField?: string;
+ dependenciesField?: string;
+ colorField?: string;
+ };
+ /** Chart-specific options */
+ chart?: {
+ chartType?: string;
+ xAxisField?: string;
+ yAxisFields?: string[];
+ aggregation?: string;
+ series?: any[];
+ config?: any;
+ filter?: any;
+ };
+
+ /** Catch-all for additional properties */
+ [key: string]: any;
+}
+
// ============================================================================
// Multi-User Collaborative Editing
// ============================================================================
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index c54afed1ce..df434892af 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -542,6 +542,8 @@ export type {
CollaborationConfig,
ViewDesignerColumn,
ViewDesignerSchema,
+ UnifiedViewType,
+ UnifiedViewConfig,
} from './designer';
// ============================================================================
From 4d7a6114956e57c7260af0dbbfed17f5fcd35221 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 21 Feb 2026 06:48:30 +0000
Subject: [PATCH 3/4] fix: update ObjectView tests for new panel-based view
creation flow
- Update "Add View" tests to verify panel opens instead of navigation
- Add test for "Advanced Editor" button navigation
- Update ROADMAP.md with completed P1.8 items
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
---
ROADMAP.md | 10 +++++---
.../console/src/__tests__/ObjectView.test.tsx | 25 ++++++++++++++++---
2 files changed, 29 insertions(+), 6 deletions(-)
diff --git a/ROADMAP.md b/ROADMAP.md
index 555539bee3..cf3391ee24 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -116,9 +116,13 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
### P1.8 Console — View Config Panel (Phase 20)
-- [ ] Inline ViewConfigPanel for all view types (Airtable-style right sidebar)
-- [ ] Column visibility toggle from config panel
-- [ ] Sort/filter/group config from right sidebar
+- [x] Inline ViewConfigPanel for all view types (Airtable-style right sidebar)
+- [x] Column visibility toggle from config panel
+- [x] Sort/filter/group config from right sidebar
+- [x] Type-specific options in config panel (kanban/calendar/map/gallery/timeline/gantt)
+- [x] Unified create/edit mode (`mode="create"|"edit"`) — single panel entry point
+- [x] Unified data model (`UnifiedViewConfig`) for view configuration
+- [x] ViewDesigner retained as "Advanced Editor" with weaker entry point
- [ ] View appearance settings (density, row color, conditional formatting)
---
diff --git a/apps/console/src/__tests__/ObjectView.test.tsx b/apps/console/src/__tests__/ObjectView.test.tsx
index 1fbe334870..2d9c23083e 100644
--- a/apps/console/src/__tests__/ObjectView.test.tsx
+++ b/apps/console/src/__tests__/ObjectView.test.tsx
@@ -227,7 +227,7 @@ describe('ObjectView Component', () => {
expect(screen.queryByTitle('console.objectView.designTools')).not.toBeInTheDocument();
});
- it('navigates to view designer with relative path from nested view route', () => {
+ it('opens config panel in create mode when Add View is clicked from nested view route', () => {
mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
mockUseParams.mockReturnValue({ objectName: 'opportunity', viewId: 'pipeline' });
@@ -240,10 +240,12 @@ describe('ObjectView Component', () => {
const addViewBtn = screen.getByText('console.objectView.addView');
fireEvent.click(addViewBtn);
- expect(mockNavigate).toHaveBeenCalledWith('../../views/new', { relative: 'path' });
+ // Should open config panel instead of navigating
+ expect(mockNavigate).not.toHaveBeenCalledWith('../../views/new', { relative: 'path' });
+ expect(screen.getByTestId('view-config-panel')).toBeInTheDocument();
});
- it('navigates to view designer with relative path from root object route', () => {
+ it('opens config panel in create mode when Add View is clicked from root object route', () => {
mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
mockUseParams.mockReturnValue({ objectName: 'opportunity' });
@@ -255,6 +257,23 @@ describe('ObjectView Component', () => {
const addViewBtn = screen.getByText('console.objectView.addView');
fireEvent.click(addViewBtn);
+ // Should open config panel instead of navigating
+ expect(mockNavigate).not.toHaveBeenCalledWith('views/new', { relative: 'path' });
+ expect(screen.getByTestId('view-config-panel')).toBeInTheDocument();
+ });
+
+ it('navigates to view designer when Advanced Editor is clicked', () => {
+ mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
+ mockUseParams.mockReturnValue({ objectName: 'opportunity' });
+
+ render();
+
+ const designBtn = screen.getByTitle('console.objectView.designTools');
+ fireEvent.click(designBtn);
+
+ const advancedBtn = screen.getByText('console.objectView.advancedEditor');
+ fireEvent.click(advancedBtn);
+
expect(mockNavigate).toHaveBeenCalledWith('views/new', { relative: 'path' });
});
From e6fa5a55bf8de7463a7b687c6be5c0a0daf1352b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 21 Feb 2026 06:49:28 +0000
Subject: [PATCH 4/4] refactor: fix useEffect dependency array per code review
feedback
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
---
apps/console/src/components/ViewConfigPanel.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/console/src/components/ViewConfigPanel.tsx b/apps/console/src/components/ViewConfigPanel.tsx
index bb04ca3709..8ec133e5d6 100644
--- a/apps/console/src/components/ViewConfigPanel.tsx
+++ b/apps/console/src/components/ViewConfigPanel.tsx
@@ -281,7 +281,7 @@ export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, obje
useEffect(() => {
setDraft({ ...effectiveActiveView });
setIsDirty(mode === 'create');
- }, [mode === 'create' ? mode : activeView.id]);
+ }, [mode, activeView.id]); // eslint-disable-line react-hooks/exhaustive-deps
// Focus the panel when it opens for keyboard accessibility
useEffect(() => {