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', () => {