From d4acaf104e0fbc25700eb79e08670bf6ef00621f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 21 Feb 2026 08:51:36 +0000
Subject: [PATCH 1/3] Initial plan
From 570c1dd339f73eb9dd112134daf5d9543a08daf9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 21 Feb 2026 08:56:41 +0000
Subject: [PATCH 2/3] feat: Add Columns field selector with reorder support in
ViewConfigPanel
- Export EditorPanelType type for sub-panel state management
- Add handleColumnMove for column up/down reordering
- Enhanced column selector shows selected columns with move buttons
- Unselected fields shown separately for easy addition
- All changes propagate via onViewUpdate for real-time preview
- Added 9 new test cases covering reorder, save, and edge cases
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
---
.../src/__tests__/ViewConfigPanel.test.tsx | 184 ++++++++++++++++++
.../src/components/ViewConfigPanel.tsx | 72 ++++++-
2 files changed, 247 insertions(+), 9 deletions(-)
diff --git a/apps/console/src/__tests__/ViewConfigPanel.test.tsx b/apps/console/src/__tests__/ViewConfigPanel.test.tsx
index 30d0ccd29a..a9d468aa0d 100644
--- a/apps/console/src/__tests__/ViewConfigPanel.test.tsx
+++ b/apps/console/src/__tests__/ViewConfigPanel.test.tsx
@@ -1284,4 +1284,188 @@ describe('ViewConfigPanel', () => {
fireEvent.click(screen.getByText('console.objectView.sortBy'));
expect(screen.queryByTestId('inline-sort-builder')).not.toBeInTheDocument();
});
+
+ // ── EditorPanelType export test ──
+
+ it('exports EditorPanelType type', async () => {
+ const mod = await import('../components/ViewConfigPanel');
+ // EditorPanelType is a type-only export, but we verify the module exports correctly
+ expect(mod.ViewConfigPanel).toBeDefined();
+ });
+
+ // ── Column selector sub-panel tests ──
+
+ it('shows selected columns in order with move up/down buttons', () => {
+ render(
+
+ );
+
+ // Expand the Fields sub-section
+ fireEvent.click(screen.getByText('console.objectView.fields'));
+
+ // Selected columns section should appear
+ expect(screen.getByTestId('selected-columns')).toBeInTheDocument();
+
+ // Move buttons should exist for selected columns
+ expect(screen.getByTestId('col-move-up-name')).toBeInTheDocument();
+ expect(screen.getByTestId('col-move-down-name')).toBeInTheDocument();
+ expect(screen.getByTestId('col-move-up-stage')).toBeInTheDocument();
+ expect(screen.getByTestId('col-move-down-stage')).toBeInTheDocument();
+ expect(screen.getByTestId('col-move-up-amount')).toBeInTheDocument();
+ expect(screen.getByTestId('col-move-down-amount')).toBeInTheDocument();
+ });
+
+ it('disables move-up for first column and move-down for last column', () => {
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('console.objectView.fields'));
+
+ // First column (name) — move up disabled
+ expect(screen.getByTestId('col-move-up-name')).toBeDisabled();
+ expect(screen.getByTestId('col-move-down-name')).not.toBeDisabled();
+
+ // Last column (amount) — move down disabled
+ expect(screen.getByTestId('col-move-up-amount')).not.toBeDisabled();
+ expect(screen.getByTestId('col-move-down-amount')).toBeDisabled();
+ });
+
+ it('moves column down and updates draft.columns order', () => {
+ const onViewUpdate = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('console.objectView.fields'));
+
+ // Move "name" down — should swap with "stage"
+ fireEvent.click(screen.getByTestId('col-move-down-name'));
+ expect(onViewUpdate).toHaveBeenCalledWith('columns', ['stage', 'name', 'amount']);
+ });
+
+ it('moves column up and updates draft.columns order', () => {
+ const onViewUpdate = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('console.objectView.fields'));
+
+ // Move "amount" up — should swap with "stage"
+ fireEvent.click(screen.getByTestId('col-move-up-amount'));
+ expect(onViewUpdate).toHaveBeenCalledWith('columns', ['name', 'amount', 'stage']);
+ });
+
+ it('saves reordered columns via onSave', () => {
+ const onSave = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('console.objectView.fields'));
+
+ // Move "name" down
+ fireEvent.click(screen.getByTestId('col-move-down-name'));
+
+ // Footer should appear
+ expect(screen.getByTestId('view-config-footer')).toBeInTheDocument();
+
+ // Save
+ fireEvent.click(screen.getByTestId('view-config-save'));
+ expect(onSave).toHaveBeenCalledOnce();
+ expect(onSave.mock.calls[0][0].columns).toEqual(['stage', 'name', 'amount']);
+ });
+
+ it('shows unselected fields below selected columns', () => {
+ // Only 'name' and 'stage' are selected, 'amount' is not
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('console.objectView.fields'));
+
+ // 'amount' should have a checkbox but no move buttons
+ expect(screen.getByTestId('col-checkbox-amount')).toBeInTheDocument();
+ expect(screen.getByTestId('col-checkbox-amount')).not.toBeChecked();
+ expect(screen.queryByTestId('col-move-up-amount')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('col-move-down-amount')).not.toBeInTheDocument();
+ });
+
+ it('adding unselected field appends to columns and shows move buttons', () => {
+ const onViewUpdate = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('console.objectView.fields'));
+
+ // Click checkbox for 'amount' to add it
+ fireEvent.click(screen.getByTestId('col-checkbox-amount'));
+ expect(onViewUpdate).toHaveBeenCalledWith('columns', ['name', 'stage', 'amount']);
+ });
+
+ it('reorder triggers onViewUpdate for real-time preview', () => {
+ const onViewUpdate = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('console.objectView.fields'));
+
+ // Move "stage" up
+ fireEvent.click(screen.getByTestId('col-move-up-stage'));
+ expect(onViewUpdate).toHaveBeenCalledWith('columns', ['stage', 'name', 'amount']);
+
+ // Move "stage" down (now at index 0)
+ fireEvent.click(screen.getByTestId('col-move-down-stage'));
+ // After the first move, state has stage at index 0
+ // The second move should operate on the updated state
+ expect(onViewUpdate).toHaveBeenCalledTimes(2);
+ });
});
diff --git a/apps/console/src/components/ViewConfigPanel.tsx b/apps/console/src/components/ViewConfigPanel.tsx
index 283cac4088..f778ffc8ff 100644
--- a/apps/console/src/components/ViewConfigPanel.tsx
+++ b/apps/console/src/components/ViewConfigPanel.tsx
@@ -15,7 +15,7 @@
import { useMemo, useEffect, useRef, useState, useCallback } from 'react';
import { Button, Switch, Input, Checkbox, FilterBuilder, SortBuilder } from '@object-ui/components';
import type { FilterGroup, SortItem } from '@object-ui/components';
-import { X, Save, RotateCcw, ChevronDown, ChevronRight } from 'lucide-react';
+import { X, Save, RotateCcw, ChevronDown, ChevronRight, ArrowUp, ArrowDown } from 'lucide-react';
import { useObjectTranslation } from '@object-ui/i18n';
// ---------------------------------------------------------------------------
@@ -190,6 +190,8 @@ const ROW_HEIGHT_OPTIONS = [
];
/** Editor panel types that can be opened from clickable rows */
+export type EditorPanelType = 'columns' | 'filter' | 'sort';
+
export interface ViewConfigPanelProps {
/** Whether the panel is open */
open: boolean;
@@ -427,6 +429,18 @@ export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, obje
updateDraft('columns', currentCols);
}, [draft.columns, updateDraft]);
+ /** Move a column up or down in the columns array */
+ const handleColumnMove = useCallback((fieldName: string, direction: 'up' | 'down') => {
+ const currentCols: string[] = Array.isArray(draft.columns) ? [...draft.columns] : [];
+ const idx = currentCols.indexOf(fieldName);
+ if (idx < 0) return;
+ const targetIdx = direction === 'up' ? idx - 1 : idx + 1;
+ if (targetIdx < 0 || targetIdx >= currentCols.length) return;
+ // Swap
+ [currentCols[idx], currentCols[targetIdx]] = [currentCols[targetIdx], currentCols[idx]];
+ 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] || {};
@@ -584,21 +598,61 @@ export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, obje
onClick={() => toggleDataSub('fields')}
/>
{expandedDataSubs.fields && (
-
- {fieldOptions.map((f) => {
- const checked = Array.isArray(draft.columns) ? draft.columns.includes(f.value) : false;
- return (
+
+ {/* Selected columns — shown in draft order with reorder buttons */}
+ {Array.isArray(draft.columns) && draft.columns.length > 0 && (
+
+ {draft.columns.map((colName: string, idx: number) => {
+ const field = fieldOptions.find(f => f.value === colName);
+ return (
+
+
handleColumnToggle(colName, false)}
+ className="h-3.5 w-3.5 shrink-0"
+ />
+ {field?.label || colName}
+
+
+
+ );
+ })}
+
+ )}
+ {/* Unselected fields — available to add */}
+ {fieldOptions
+ .filter(f => !Array.isArray(draft.columns) || !draft.columns.includes(f.value))
+ .map((f) => (
- );
- })}
+ ))
+ }
)}
From 46856a76c0e7b653918b05ba2d978f32c398537d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 21 Feb 2026 08:59:33 +0000
Subject: [PATCH 3/3] chore: address code review feedback, update ROADMAP with
column reorder
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
---
ROADMAP.md | 1 +
apps/console/src/__tests__/ViewConfigPanel.test.tsx | 8 --------
2 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/ROADMAP.md b/ROADMAP.md
index f5a63302d6..495b6c4078 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -118,6 +118,7 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
- [x] Inline ViewConfigPanel for all view types (Airtable-style right sidebar)
- [x] Column visibility toggle from config panel
+- [x] Column reorder (move up/down) from config panel with real-time preview
- [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
diff --git a/apps/console/src/__tests__/ViewConfigPanel.test.tsx b/apps/console/src/__tests__/ViewConfigPanel.test.tsx
index a9d468aa0d..c2f223aed4 100644
--- a/apps/console/src/__tests__/ViewConfigPanel.test.tsx
+++ b/apps/console/src/__tests__/ViewConfigPanel.test.tsx
@@ -1285,14 +1285,6 @@ describe('ViewConfigPanel', () => {
expect(screen.queryByTestId('inline-sort-builder')).not.toBeInTheDocument();
});
- // ── EditorPanelType export test ──
-
- it('exports EditorPanelType type', async () => {
- const mod = await import('../components/ViewConfigPanel');
- // EditorPanelType is a type-only export, but we verify the module exports correctly
- expect(mod.ViewConfigPanel).toBeDefined();
- });
-
// ── Column selector sub-panel tests ──
it('shows selected columns in order with move up/down buttons', () => {