From feb95319922b82dbddabab01d166ff16386c8f3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:28:28 +0000 Subject: [PATCH 1/4] Initial plan From b01c09140ea45da5b424ad32887b9dd9cdecb1fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:39:42 +0000 Subject: [PATCH 2/4] feat: restructure ViewConfigPanel with Airtable-style layout, collapsible sections, new appearance/user-action fields, breadcrumb header, and i18n for all 11 locales Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../src/components/ViewConfigPanel.tsx | 710 ++++++++++++------ packages/i18n/src/locales/ar.ts | 29 + packages/i18n/src/locales/de.ts | 29 + packages/i18n/src/locales/en.ts | 16 + packages/i18n/src/locales/es.ts | 29 + packages/i18n/src/locales/fr.ts | 29 + packages/i18n/src/locales/ja.ts | 29 + packages/i18n/src/locales/ko.ts | 29 + packages/i18n/src/locales/pt.ts | 29 + packages/i18n/src/locales/ru.ts | 29 + packages/i18n/src/locales/zh.ts | 29 + 11 files changed, 741 insertions(+), 246 deletions(-) diff --git a/apps/console/src/components/ViewConfigPanel.tsx b/apps/console/src/components/ViewConfigPanel.tsx index 8ec133e5..021301d0 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 } from 'lucide-react'; +import { X, Save, RotateCcw, ChevronDown, ChevronRight } from 'lucide-react'; import { useObjectTranslation } from '@object-ui/i18n'; // --------------------------------------------------------------------------- @@ -181,6 +181,14 @@ const VIEW_TYPE_LABELS: Record = { /** All available view type keys */ const VIEW_TYPE_OPTIONS = Object.keys(VIEW_TYPE_LABELS); +/** Row height options with Tailwind gap classes for visual icons */ +const ROW_HEIGHT_OPTIONS = [ + { value: 'short', gapClass: 'gap-0' }, + { value: 'medium', gapClass: 'gap-0.5' }, + { value: 'tall', gapClass: 'gap-1' }, + { value: 'extraTall', gapClass: 'gap-1.5' }, +]; + /** Editor panel types that can be opened from clickable rows */ export interface ViewConfigPanelProps { /** Whether the panel is open */ @@ -242,10 +250,27 @@ function ConfigRow({ label, value, onClick, children }: { label: string; value?: ); } -/** Section heading */ -function SectionHeader({ title }: { title: string }) { +/** Section heading with optional collapse/expand support */ +function SectionHeader({ title, collapsible, collapsed, onToggle, testId }: { title: string; collapsible?: boolean; collapsed?: boolean; onToggle?: () => void; testId?: string }) { + if (collapsible) { + return ( + + ); + } return ( -
+

{title}

); @@ -273,6 +298,16 @@ export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, obje // Local draft state — clone of activeView, mutated by UI interactions const [draft, setDraft] = useState>({}); const [isDirty, setIsDirty] = useState(false); + // Collapsible section state + const [collapsedSections, setCollapsedSections] = useState>({}); + const toggleSection = useCallback((section: string) => { + setCollapsedSections(prev => ({ ...prev, [section]: !prev[section] })); + }, []); + // Expandable sub-rows within Data section (sort, filter, fields) + const [expandedDataSubs, setExpandedDataSubs] = useState>({}); + const toggleDataSub = useCallback((key: string) => { + setExpandedDataSubs(prev => ({ ...prev, [key]: !prev[key] })); + }, []); // Reset draft when switching to a different view (by ID change only). // We intentionally depend on activeView.id rather than the full activeView @@ -399,6 +434,21 @@ export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, obje if (!open) return null; + // Summary values for compact Data section ConfigRows + const sortCount = Array.isArray(draft.sort) ? draft.sort.filter((s: any) => s.field).length : 0; + const filterCount = filterGroupValue.conditions.length; + const visibleColumnsCount = Array.isArray(draft.columns) ? draft.columns.length : 0; + const sortSummary = sortCount > 0 + ? t('console.objectView.sortsCount', { count: sortCount }) + : t('console.objectView.none'); + const filterSummary = filterCount > 0 + ? t('console.objectView.filtersCount', { count: filterCount }) + : t('console.objectView.none'); + const fieldsSummary = visibleColumnsCount > 0 + ? t('console.objectView.fieldsVisible', { count: visibleColumnsCount }) + : t('console.objectView.none'); + const groupByValue = draft.kanban?.groupByField || draft.kanban?.groupField || draft.groupBy || ''; + return (
- {/* Panel Header */} + {/* Panel Header — breadcrumb hierarchy: Page > ViewType */}
- - {panelTitle} - +
+ {t('console.objectView.page')} + + {VIEW_TYPE_LABELS[viewType] || viewType} +
+ ))} +
+ + + updateDraft('wrapHeaders', checked)} + className="scale-75" + /> + + + updateDraft('showDescription', checked)} + className="scale-75" + /> + + + updateDraft('collapseAllByDefault', checked)} + className="scale-75" + /> + +
+ )} - {/* Advanced Section */} - -
- - updateDraft('allowExport', checked)} - className="scale-75" - /> - -
+ {/* User Actions Section — collapsible */} + toggleSection('userActions')} + testId="section-userActions" + /> + {!collapsedSections.userActions && ( +
+ + updateDraft('editRecordsInline', checked)} + className="scale-75" + /> + + + updateDraft('addDeleteRecordsInline', checked)} + className="scale-75" + /> + + + updateDraft('clickIntoRecordDetails', checked)} + className="scale-75" + /> + + + updateDraft('addRecordViaForm', checked)} + className="scale-75" + /> + + + updateDraft('allowExport', checked)} + className="scale-75" + /> + +
+ )}
{/* Footer — Save / Discard buttons */} diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index f4711b60..023150da 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -210,6 +210,35 @@ const ar = { columnsConfigured: '{{count}} أعمدة', save: 'حفظ', discard: 'تجاهل', + createView: 'إنشاء عرض', + newView: 'عرض جديد', + advancedEditor: 'محرر متقدم', + typeOptions: 'خيارات النوع', + groupByField: 'حقل التجميع', + startDateField: 'حقل تاريخ البدء', + titleField: 'حقل العنوان', + latitudeField: 'حقل خط العرض', + longitudeField: 'حقل خط الطول', + imageField: 'حقل الصورة', + dateField: 'حقل التاريخ', + selectField: 'اختر حقلاً...', + gridOptionsHint: 'يستخدم عرض الشبكة الأعمدة المكونة أعلاه.', + groupBy: 'تجميع حسب', + prefixField: 'حقل البادئة', + fields: 'الحقول', + fieldsVisible: '{{count}} مرئي', + sortsCount: '{{count}} ترتيبات', + filtersCount: '{{count}} مرشحات', + endDateField: 'حقل تاريخ الانتهاء', + color: 'اللون', + fieldTextColor: 'لون نص الحقل', + rowHeight: 'ارتفاع الصف', + wrapHeaders: 'التفاف العناوين', + showFieldDescriptions: 'إظهار أوصاف الحقول', + collapseAllByDefault: 'طي الكل افتراضياً', + editRecordsInline: 'تحرير السجلات مباشرة', + addDeleteRecordsInline: 'إضافة/حذف السجلات مباشرة', + clickIntoRecordDetails: 'انقر لعرض تفاصيل السجل', }, localeSwitcher: { label: 'اللغة', diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 9cd28e71..fbc042ef 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -214,6 +214,35 @@ const de = { columnsConfigured: '{{count}} Spalten', save: 'Speichern', discard: 'Verwerfen', + createView: 'Ansicht erstellen', + newView: 'Neue Ansicht', + advancedEditor: 'Erweiterter Editor', + typeOptions: 'Typoptionen', + groupByField: 'Gruppierungsfeld', + startDateField: 'Startdatumsfeld', + titleField: 'Titelfeld', + latitudeField: 'Breitengradfeld', + longitudeField: 'Längengradfeld', + imageField: 'Bildfeld', + dateField: 'Datumsfeld', + selectField: 'Feld auswählen...', + gridOptionsHint: 'Die Tabellenansicht verwendet die oben konfigurierten Spalten.', + groupBy: 'Gruppieren nach', + prefixField: 'Präfixfeld', + fields: 'Felder', + fieldsVisible: '{{count}} sichtbar', + sortsCount: '{{count}} Sortierungen', + filtersCount: '{{count}} Filter', + endDateField: 'Enddatumsfeld', + color: 'Farbe', + fieldTextColor: 'Feldtextfarbe', + rowHeight: 'Zeilenhöhe', + wrapHeaders: 'Kopfzeilen umbrechen', + showFieldDescriptions: 'Feldbeschreibungen anzeigen', + collapseAllByDefault: 'Standardmäßig alle einklappen', + editRecordsInline: 'Datensätze inline bearbeiten', + addDeleteRecordsInline: 'Datensätze inline hinzufügen/löschen', + clickIntoRecordDetails: 'Klicken für Datensatzdetails', }, localeSwitcher: { label: 'Sprache', diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index ef093b23..d8a8e6bb 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -227,6 +227,22 @@ const en = { dateField: 'Date field', selectField: 'Select field...', gridOptionsHint: 'Grid view uses the columns configured above.', + groupBy: 'Group by', + prefixField: 'Prefix field', + fields: 'Fields', + fieldsVisible: '{{count}} visible', + sortsCount: '{{count}} sorts', + filtersCount: '{{count}} filters', + endDateField: 'End date field', + color: 'Color', + fieldTextColor: 'Field text color', + rowHeight: 'Row height', + wrapHeaders: 'Wrap headers', + showFieldDescriptions: 'Show field descriptions', + collapseAllByDefault: 'Collapse all by default', + editRecordsInline: 'Edit records inline', + addDeleteRecordsInline: 'Add/delete records inline', + clickIntoRecordDetails: 'Click into record details', }, localeSwitcher: { label: 'Language', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 9757d1ce..12ad941b 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -209,6 +209,35 @@ const es = { columnsConfigured: '{{count}} columnas', save: 'Guardar', discard: 'Descartar', + createView: 'Crear vista', + newView: 'Nueva vista', + advancedEditor: 'Editor avanzado', + typeOptions: 'Opciones de tipo', + groupByField: 'Campo de agrupación', + startDateField: 'Campo de fecha de inicio', + titleField: 'Campo de título', + latitudeField: 'Campo de latitud', + longitudeField: 'Campo de longitud', + imageField: 'Campo de imagen', + dateField: 'Campo de fecha', + selectField: 'Seleccionar campo...', + gridOptionsHint: 'La vista de cuadrícula usa las columnas configuradas arriba.', + groupBy: 'Agrupar por', + prefixField: 'Campo de prefijo', + fields: 'Campos', + fieldsVisible: '{{count}} visibles', + sortsCount: '{{count}} ordenamientos', + filtersCount: '{{count}} filtros', + endDateField: 'Campo de fecha de fin', + color: 'Color', + fieldTextColor: 'Color del texto del campo', + rowHeight: 'Altura de fila', + wrapHeaders: 'Ajustar encabezados', + showFieldDescriptions: 'Mostrar descripciones de campos', + collapseAllByDefault: 'Contraer todo por defecto', + editRecordsInline: 'Editar registros en línea', + addDeleteRecordsInline: 'Agregar/eliminar registros en línea', + clickIntoRecordDetails: 'Clic para detalles del registro', }, localeSwitcher: { label: 'Idioma', diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index 30d20b6c..dcca5e28 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -214,6 +214,35 @@ const fr = { columnsConfigured: '{{count}} colonnes', save: 'Enregistrer', discard: 'Annuler', + createView: 'Créer une vue', + newView: 'Nouvelle vue', + advancedEditor: 'Éditeur avancé', + typeOptions: 'Options de type', + groupByField: 'Champ de regroupement', + startDateField: 'Champ de date de début', + titleField: 'Champ de titre', + latitudeField: 'Champ de latitude', + longitudeField: 'Champ de longitude', + imageField: "Champ d'image", + dateField: 'Champ de date', + selectField: 'Sélectionner un champ...', + gridOptionsHint: 'La vue grille utilise les colonnes configurées ci-dessus.', + groupBy: 'Regrouper par', + prefixField: 'Champ de préfixe', + fields: 'Champs', + fieldsVisible: '{{count}} visibles', + sortsCount: '{{count}} tris', + filtersCount: '{{count}} filtres', + endDateField: 'Champ de date de fin', + color: 'Couleur', + fieldTextColor: 'Couleur du texte du champ', + rowHeight: 'Hauteur de ligne', + wrapHeaders: 'Retour à la ligne des en-têtes', + showFieldDescriptions: 'Afficher les descriptions des champs', + collapseAllByDefault: 'Tout réduire par défaut', + editRecordsInline: 'Modifier les enregistrements en ligne', + addDeleteRecordsInline: 'Ajouter/supprimer des enregistrements en ligne', + clickIntoRecordDetails: "Cliquer pour les détails de l'enregistrement", }, localeSwitcher: { label: 'Langue', diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index a3a6c563..23f09cf0 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -209,6 +209,35 @@ const ja = { columnsConfigured: '{{count}} 列', save: '保存', discard: '破棄', + createView: 'ビューを作成', + newView: '新しいビュー', + advancedEditor: '詳細エディター', + typeOptions: 'タイプオプション', + groupByField: 'グループ化フィールド', + startDateField: '開始日フィールド', + titleField: 'タイトルフィールド', + latitudeField: '緯度フィールド', + longitudeField: '経度フィールド', + imageField: '画像フィールド', + dateField: '日付フィールド', + selectField: 'フィールドを選択...', + gridOptionsHint: 'グリッドビューは上記で設定された列を使用します。', + groupBy: 'グループ化', + prefixField: 'プレフィックスフィールド', + fields: 'フィールド', + fieldsVisible: '{{count}} 件表示', + sortsCount: '{{count}} 件の並べ替え', + filtersCount: '{{count}} 件のフィルター', + endDateField: '終了日フィールド', + color: 'カラー', + fieldTextColor: 'フィールドテキストカラー', + rowHeight: '行の高さ', + wrapHeaders: 'ヘッダーの折り返し', + showFieldDescriptions: 'フィールド説明を表示', + collapseAllByDefault: 'デフォルトですべて折りたたむ', + editRecordsInline: 'インラインでレコードを編集', + addDeleteRecordsInline: 'インラインでレコードを追加/削除', + clickIntoRecordDetails: 'レコード詳細をクリックで表示', }, localeSwitcher: { label: '言語', diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 4c70883d..a1b2debb 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -209,6 +209,35 @@ const ko = { columnsConfigured: '{{count}}개 열', save: '저장', discard: '취소', + createView: '뷰 생성', + newView: '새 뷰', + advancedEditor: '고급 편집기', + typeOptions: '유형 옵션', + groupByField: '그룹화 필드', + startDateField: '시작 날짜 필드', + titleField: '제목 필드', + latitudeField: '위도 필드', + longitudeField: '경도 필드', + imageField: '이미지 필드', + dateField: '날짜 필드', + selectField: '필드 선택...', + gridOptionsHint: '그리드 뷰는 위에서 구성된 열을 사용합니다.', + groupBy: '그룹화 기준', + prefixField: '접두사 필드', + fields: '필드', + fieldsVisible: '{{count}}개 표시', + sortsCount: '{{count}}개 정렬', + filtersCount: '{{count}}개 필터', + endDateField: '종료 날짜 필드', + color: '색상', + fieldTextColor: '필드 텍스트 색상', + rowHeight: '행 높이', + wrapHeaders: '헤더 줄바꿈', + showFieldDescriptions: '필드 설명 표시', + collapseAllByDefault: '기본적으로 모두 접기', + editRecordsInline: '인라인으로 레코드 편집', + addDeleteRecordsInline: '인라인으로 레코드 추가/삭제', + clickIntoRecordDetails: '레코드 상세 보기 클릭', }, localeSwitcher: { label: '언어', diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 5183c758..462e3ff6 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -209,6 +209,35 @@ const pt = { columnsConfigured: '{{count}} colunas', save: 'Salvar', discard: 'Descartar', + createView: 'Criar visualização', + newView: 'Nova visualização', + advancedEditor: 'Editor avançado', + typeOptions: 'Opções de tipo', + groupByField: 'Campo de agrupamento', + startDateField: 'Campo de data de início', + titleField: 'Campo de título', + latitudeField: 'Campo de latitude', + longitudeField: 'Campo de longitude', + imageField: 'Campo de imagem', + dateField: 'Campo de data', + selectField: 'Selecionar campo...', + gridOptionsHint: 'A visualização de grade usa as colunas configuradas acima.', + groupBy: 'Agrupar por', + prefixField: 'Campo de prefixo', + fields: 'Campos', + fieldsVisible: '{{count}} visíveis', + sortsCount: '{{count}} ordenações', + filtersCount: '{{count}} filtros', + endDateField: 'Campo de data de término', + color: 'Cor', + fieldTextColor: 'Cor do texto do campo', + rowHeight: 'Altura da linha', + wrapHeaders: 'Quebrar cabeçalhos', + showFieldDescriptions: 'Mostrar descrições dos campos', + collapseAllByDefault: 'Recolher tudo por padrão', + editRecordsInline: 'Editar registros em linha', + addDeleteRecordsInline: 'Adicionar/excluir registros em linha', + clickIntoRecordDetails: 'Clique para detalhes do registro', }, localeSwitcher: { label: 'Idioma', diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index da7808da..fad4809d 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -209,6 +209,35 @@ const ru = { columnsConfigured: '{{count}} столбцов', save: 'Сохранить', discard: 'Отменить', + createView: 'Создать представление', + newView: 'Новое представление', + advancedEditor: 'Расширенный редактор', + typeOptions: 'Параметры типа', + groupByField: 'Поле группировки', + startDateField: 'Поле даты начала', + titleField: 'Поле заголовка', + latitudeField: 'Поле широты', + longitudeField: 'Поле долготы', + imageField: 'Поле изображения', + dateField: 'Поле даты', + selectField: 'Выберите поле...', + gridOptionsHint: 'Представление таблицы использует столбцы, настроенные выше.', + groupBy: 'Группировать по', + prefixField: 'Поле префикса', + fields: 'Поля', + fieldsVisible: '{{count}} видимых', + sortsCount: '{{count}} сортировок', + filtersCount: '{{count}} фильтров', + endDateField: 'Поле даты окончания', + color: 'Цвет', + fieldTextColor: 'Цвет текста поля', + rowHeight: 'Высота строки', + wrapHeaders: 'Перенос заголовков', + showFieldDescriptions: 'Показать описания полей', + collapseAllByDefault: 'Свернуть всё по умолчанию', + editRecordsInline: 'Редактировать записи встроенно', + addDeleteRecordsInline: 'Добавлять/удалять записи встроенно', + clickIntoRecordDetails: 'Нажмите для просмотра деталей записи', }, localeSwitcher: { label: 'Язык', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 6db20be5..4a121461 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -214,6 +214,35 @@ const zh = { columnsConfigured: '{{count}} 列', save: '保存', discard: '丢弃', + createView: '创建视图', + newView: '新视图', + advancedEditor: '高级编辑器', + typeOptions: '类型选项', + groupByField: '分组字段', + startDateField: '开始日期字段', + titleField: '标题字段', + latitudeField: '纬度字段', + longitudeField: '经度字段', + imageField: '图片字段', + dateField: '日期字段', + selectField: '选择字段...', + gridOptionsHint: '网格视图使用上方配置的列。', + groupBy: '分组依据', + prefixField: '前缀字段', + fields: '字段', + fieldsVisible: '{{count}} 个可见', + sortsCount: '{{count}} 个排序', + filtersCount: '{{count}} 个筛选', + endDateField: '结束日期字段', + color: '颜色', + fieldTextColor: '字段文字颜色', + rowHeight: '行高', + wrapHeaders: '自动换行标题', + showFieldDescriptions: '显示字段描述', + collapseAllByDefault: '默认全部折叠', + editRecordsInline: '内联编辑记录', + addDeleteRecordsInline: '内联添加/删除记录', + clickIntoRecordDetails: '点击进入记录详情', }, localeSwitcher: { label: '语言', From e2acfe0f9069e75bf2855dec354d1c6f0686966d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:43:31 +0000 Subject: [PATCH 3/4] test: update ViewConfigPanel tests for new layout, add tests for breadcrumb, collapsible sections, appearance and user actions fields; update ROADMAP.md Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- ROADMAP.md | 9 +- .../src/__tests__/ViewConfigPanel.test.tsx | 279 +++++++++++++++++- 2 files changed, 283 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index cf3391ee..40f70917 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -123,7 +123,14 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind - [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) +- [x] Panel header breadcrumb navigation (Page > List/Kanban/Gallery) +- [x] Collapsible/expandable sections with chevron toggle +- [x] Data section: Sort by (summary), Group by, Prefix field, Fields (count visible) +- [x] Appearance section: Color, Field text color, Row height (icon toggle), Wrap headers, Show field descriptions, Collapse all by default +- [x] User actions section: Edit records inline, Add/delete records inline, Click into record details +- [x] Calendar endDateField support +- [x] i18n for all 11 locales (en, zh, ja, de, fr, es, ar, ru, pt, ko) +- [ ] Conditional formatting rules --- diff --git a/apps/console/src/__tests__/ViewConfigPanel.test.tsx b/apps/console/src/__tests__/ViewConfigPanel.test.tsx index 9e0d2164..e2135065 100644 --- a/apps/console/src/__tests__/ViewConfigPanel.test.tsx +++ b/apps/console/src/__tests__/ViewConfigPanel.test.tsx @@ -127,13 +127,13 @@ describe('ViewConfigPanel', () => { expect(screen.getByTestId('view-config-panel')).toBeInTheDocument(); - // Check section headers - expect(screen.getByText('console.objectView.page')).toBeInTheDocument(); + // Check section headers (page appears in both breadcrumb and section) + expect(screen.getAllByText('console.objectView.page').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('console.objectView.data')).toBeInTheDocument(); expect(screen.getByText('console.objectView.appearance')).toBeInTheDocument(); - expect(screen.getByText('console.objectView.userFilters')).toBeInTheDocument(); expect(screen.getByText('console.objectView.userActions')).toBeInTheDocument(); - expect(screen.getByText('console.objectView.advanced')).toBeInTheDocument(); + // Breadcrumb shows view type label + expect(screen.getByTestId('panel-breadcrumb')).toBeInTheDocument(); }); it('displays view title in editable input', () => { @@ -186,6 +186,10 @@ describe('ViewConfigPanel', () => { /> ); + // Expand the Fields sub-section by clicking the summary row + const fieldsRow = screen.getByText('console.objectView.fields'); + fireEvent.click(fieldsRow); + // 3 fields → 3 checkboxes expect(screen.getByTestId('column-selector')).toBeInTheDocument(); expect(screen.getByTestId('col-checkbox-name')).toBeInTheDocument(); @@ -278,6 +282,10 @@ describe('ViewConfigPanel', () => { /> ); + // Expand sort and filter sub-sections + fireEvent.click(screen.getByText('console.objectView.sortBy')); + fireEvent.click(screen.getByText('console.objectView.filterBy')); + // FilterBuilder should have 0 conditions expect(screen.getByTestId('mock-filter-builder')).toHaveAttribute('data-condition-count', '0'); // SortBuilder should have 0 items @@ -457,6 +465,9 @@ describe('ViewConfigPanel', () => { /> ); + // Expand filter sub-section + fireEvent.click(screen.getByText('console.objectView.filterBy')); + const fb = screen.getByTestId('mock-filter-builder'); expect(fb).toHaveAttribute('data-condition-count', '1'); expect(fb).toHaveAttribute('data-field-count', '3'); @@ -473,6 +484,9 @@ describe('ViewConfigPanel', () => { /> ); + // Expand sort sub-section + fireEvent.click(screen.getByText('console.objectView.sortBy')); + const sb = screen.getByTestId('mock-sort-builder'); expect(sb).toHaveAttribute('data-sort-count', '1'); expect(sb).toHaveAttribute('data-field-count', '3'); @@ -491,6 +505,9 @@ describe('ViewConfigPanel', () => { /> ); + // Expand filter sub-section + fireEvent.click(screen.getByText('console.objectView.filterBy')); + fireEvent.click(screen.getByTestId('filter-builder-add')); expect(onViewUpdate).toHaveBeenCalledWith('filter', expect.any(Array)); }); @@ -507,6 +524,9 @@ describe('ViewConfigPanel', () => { /> ); + // Expand sort sub-section + fireEvent.click(screen.getByText('console.objectView.sortBy')); + fireEvent.click(screen.getByTestId('sort-builder-add')); expect(onViewUpdate).toHaveBeenCalledWith('sort', expect.any(Array)); }); @@ -523,6 +543,9 @@ describe('ViewConfigPanel', () => { /> ); + // Expand the Fields sub-section + fireEvent.click(screen.getByText('console.objectView.fields')); + // Uncheck the 'stage' column fireEvent.click(screen.getByTestId('col-checkbox-stage')); expect(onViewUpdate).toHaveBeenCalledWith('columns', ['name', 'amount']); @@ -717,6 +740,9 @@ describe('ViewConfigPanel', () => { /> ); + // Expand filter sub-section + fireEvent.click(screen.getByText('console.objectView.filterBy')); + const fb = screen.getByTestId('mock-filter-builder'); expect(fb).toHaveAttribute('data-condition-count', '2'); expect(screen.getByTestId('filter-condition-0')).toHaveTextContent('stage equals active'); @@ -736,6 +762,9 @@ describe('ViewConfigPanel', () => { /> ); + // Expand filter sub-section + fireEvent.click(screen.getByText('console.objectView.filterBy')); + const fb = screen.getByTestId('mock-filter-builder'); expect(fb).toHaveAttribute('data-condition-count', '2'); }); @@ -759,6 +788,9 @@ describe('ViewConfigPanel', () => { /> ); + // Expand filter sub-section + fireEvent.click(screen.getByText('console.objectView.filterBy')); + // The mock FilterBuilder receives normalized fields via data-field-count const fb = screen.getByTestId('mock-filter-builder'); expect(fb).toHaveAttribute('data-field-count', '5'); @@ -992,4 +1024,243 @@ describe('ViewConfigPanel', () => { // Now kanban groupBy should appear expect(screen.getByTestId('type-opt-kanban-groupByField')).toBeInTheDocument(); }); + + // ── Breadcrumb header tests ── + + it('renders breadcrumb header with Page > ViewType', () => { + render( + + ); + + const breadcrumb = screen.getByTestId('panel-breadcrumb'); + expect(breadcrumb).toBeInTheDocument(); + expect(breadcrumb).toHaveTextContent('Grid'); + }); + + it('breadcrumb updates when view type changes', () => { + render( + + ); + + const breadcrumb = screen.getByTestId('panel-breadcrumb'); + expect(breadcrumb).toHaveTextContent('Kanban'); + }); + + // ── Collapsible section tests ── + + it('collapses and expands Data section', () => { + render( + + ); + + // Data section is expanded by default — source row visible + expect(screen.getByText('Opportunity')).toBeInTheDocument(); + + // Click section header to collapse + const sectionBtn = screen.getByTestId('section-data'); + fireEvent.click(sectionBtn); + + // Source row should be hidden + expect(screen.queryByText('Opportunity')).not.toBeInTheDocument(); + + // Click again to expand + fireEvent.click(sectionBtn); + expect(screen.getByText('Opportunity')).toBeInTheDocument(); + }); + + it('collapses and expands Appearance section', () => { + render( + + ); + + // Appearance section is expanded by default + expect(screen.getByTestId('toggle-showDescription')).toBeInTheDocument(); + + // Click section header to collapse + fireEvent.click(screen.getByTestId('section-appearance')); + + // Toggle should be hidden + expect(screen.queryByTestId('toggle-showDescription')).not.toBeInTheDocument(); + }); + + // ── Appearance fields tests ── + + it('renders new appearance fields: color, fieldTextColor, rowHeight, wrapHeaders, collapseAllByDefault', () => { + render( + + ); + + expect(screen.getByTestId('appearance-color')).toBeInTheDocument(); + expect(screen.getByTestId('appearance-fieldTextColor')).toBeInTheDocument(); + expect(screen.getByTestId('appearance-rowHeight')).toBeInTheDocument(); + expect(screen.getByTestId('toggle-wrapHeaders')).toBeInTheDocument(); + expect(screen.getByTestId('toggle-collapseAllByDefault')).toBeInTheDocument(); + }); + + it('changes row height via icon buttons', () => { + const onViewUpdate = vi.fn(); + render( + + ); + + const mediumBtn = screen.getByTestId('row-height-medium'); + fireEvent.click(mediumBtn); + expect(onViewUpdate).toHaveBeenCalledWith('rowHeight', 'medium'); + }); + + it('toggles wrapHeaders via Switch', () => { + const onViewUpdate = vi.fn(); + render( + + ); + + fireEvent.click(screen.getByTestId('toggle-wrapHeaders')); + expect(onViewUpdate).toHaveBeenCalledWith('wrapHeaders', true); + }); + + // ── User actions fields tests ── + + it('renders new user action fields: editRecordsInline, addDeleteRecordsInline, clickIntoRecordDetails', () => { + render( + + ); + + expect(screen.getByTestId('toggle-editRecordsInline')).toBeInTheDocument(); + expect(screen.getByTestId('toggle-addDeleteRecordsInline')).toBeInTheDocument(); + expect(screen.getByTestId('toggle-clickIntoRecordDetails')).toBeInTheDocument(); + }); + + it('toggles editRecordsInline via Switch', () => { + const onViewUpdate = vi.fn(); + render( + + ); + + fireEvent.click(screen.getByTestId('toggle-editRecordsInline')); + expect(onViewUpdate).toHaveBeenCalledWith('editRecordsInline', false); + }); + + // ── Data section: Group by and Prefix field tests ── + + it('renders Group by and Prefix field selectors in Data section', () => { + render( + + ); + + expect(screen.getByTestId('data-groupBy')).toBeInTheDocument(); + expect(screen.getByTestId('data-prefixField')).toBeInTheDocument(); + }); + + it('changes groupBy and propagates to kanban type option', () => { + const onViewUpdate = vi.fn(); + render( + + ); + + const groupBySelect = screen.getByTestId('data-groupBy'); + fireEvent.change(groupBySelect, { target: { value: 'stage' } }); + + expect(onViewUpdate).toHaveBeenCalledWith('groupBy', 'stage'); + expect(onViewUpdate).toHaveBeenCalledWith('kanban', expect.objectContaining({ groupByField: 'stage' })); + }); + + // ── Calendar endDateField test ── + + it('shows calendar endDateField when view type is calendar', () => { + render( + + ); + + expect(screen.getByTestId('type-opt-calendar-startDateField')).toBeInTheDocument(); + expect(screen.getByTestId('type-opt-calendar-endDateField')).toBeInTheDocument(); + expect(screen.getByTestId('type-opt-calendar-titleField')).toBeInTheDocument(); + }); + + // ── Data sub-section expand/collapse tests ── + + it('expands and collapses sort/filter/fields sub-sections in Data', () => { + render( + + ); + + // Sort sub-section starts collapsed + expect(screen.queryByTestId('inline-sort-builder')).not.toBeInTheDocument(); + + // Click to expand + fireEvent.click(screen.getByText('console.objectView.sortBy')); + expect(screen.getByTestId('inline-sort-builder')).toBeInTheDocument(); + + // Click again to collapse + fireEvent.click(screen.getByText('console.objectView.sortBy')); + expect(screen.queryByTestId('inline-sort-builder')).not.toBeInTheDocument(); + }); }); From a4bec6937a83d60c48298ec696a9781907874a32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:44:48 +0000 Subject: [PATCH 4/4] fix: add aria-expanded to collapsible sections and role=radio to row height buttons for accessibility Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- apps/console/src/components/ViewConfigPanel.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/console/src/components/ViewConfigPanel.tsx b/apps/console/src/components/ViewConfigPanel.tsx index 021301d0..6f77c336 100644 --- a/apps/console/src/components/ViewConfigPanel.tsx +++ b/apps/console/src/components/ViewConfigPanel.tsx @@ -259,6 +259,7 @@ function SectionHeader({ title, collapsible, collapsed, onToggle, testId }: { ti className="flex items-center justify-between pt-4 pb-1.5 first:pt-0 w-full text-left" onClick={onToggle} type="button" + aria-expanded={!collapsed} >

{title}

{collapsed ? ( @@ -833,11 +834,14 @@ export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, obje -
+
{ROW_HEIGHT_OPTIONS.map(opt => (