Align ViewConfigPanel with full ListViewSchema spec#725
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…stViewSchema Add 24 new optional properties to the NamedListView interface including: - navigation, selection, pagination configuration - searchableFields, filterableFields, hiddenFields - resizable, densityMode, rowHeight display options - exportOptions, rowActions, bulkActions - sharing, addRecord, conditionalFormatting - quickFilters, showRecordCount, allowPrinting - virtualScroll, emptyState, aria accessibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n, and propagation layers Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…D, enhance test Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR expands ObjectUI’s list view configuration support to fully cover the ListViewSchema spec by extending types/validation, adding missing ViewConfigPanel controls, and propagating the new configuration through the Console → plugin-view → ListView pipeline.
Changes:
- Extended
NamedListViewandListViewSchemaZod validation with the remaining spec attributes (navigation/selection/pagination/exportOptions/etc.). - Added UI controls in
ViewConfigPanelfor the missing spec properties (including semantic alignment likeinlineEditand spec-alignedrowHeightvalues). - Propagated new spec fields through Console
ObjectViewand plugin-viewObjectViewfor live preview / rendering.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/types/src/zod/objectql.zod.ts | Adds Zod validation for the newly supported ListViewSchema properties. |
| packages/types/src/objectql.ts | Extends NamedListView with new spec-aligned configuration fields. |
| packages/plugin-view/src/ObjectView.tsx | Forwards newly added view config properties into the schema passed to renderListView. |
| packages/i18n/src/locales/en.ts | Adds i18n keys for the new ViewConfigPanel controls. |
| packages/i18n/src/locales/zh.ts | Adds i18n keys for the new ViewConfigPanel controls (Chinese locale). |
| apps/console/src/components/ViewConfigPanel.tsx | Implements new config controls (navigation, selection, pagination, export sub-config, etc.) and semantic fixes. |
| apps/console/src/components/ObjectView.tsx | Propagates additional spec fields (and aligns inline edit semantics) into live preview schema. |
| apps/console/src/tests/ViewConfigPanel.test.tsx | Updates/extends tests to cover the new controls and renamed semantics. |
| ROADMAP.md | Marks the expanded ViewConfigPanel/spec-alignment work as complete in the roadmap. |
| @@ -325,8 +334,9 @@ export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, obje | |||
| const hasColor = draft.showColor !== false; | |||
| const hasDensity = draft.showDensity !== false; | |||
| const hasExport = draft.exportOptions != null || draft.allowExport === true; | |||
There was a problem hiding this comment.
hasExport is derived from draft.exportOptions != null || draft.allowExport === true, which makes it impossible to disable export once exportOptions has been set (toggling allowExport off won’t change hasExport). Consider aligning this with the other toolbar flags (treat export as enabled unless allowExport === false) and/or clear exportOptions when export is disabled so the switch and sub-config behave consistently.
| const hasExport = draft.exportOptions != null || draft.allowExport === true; | |
| const hasExport = draft.allowExport !== false; |
| const hasAddForm = draft.addRecordViaForm === true; | ||
| const hasAddForm = draft.addRecordViaForm === true || draft.addRecord?.enabled === true; | ||
| const hasShowDescription = draft.showDescription !== false; | ||
| const navigationMode = draft.navigation?.mode || 'page'; |
There was a problem hiding this comment.
navigationMode defaults to 'page' whenever draft.navigation?.mode is missing, but legacy views can still be configured via clickIntoRecordDetails: false (with no navigation object). In that case, the UI will incorrectly show "page" and may re-enable navigation on save. Derive the initial mode from clickIntoRecordDetails when navigation.mode is undefined (e.g., map false → 'none').
| const navigationMode = draft.navigation?.mode || 'page'; | |
| const navigationMode = | |
| draft.navigation?.mode ?? (draft.clickIntoRecordDetails === false ? 'none' : 'page'); |
| const val = Number(e.target.value) || undefined; | ||
| updateDraft('pagination', { ...(draft.pagination || {}), pageSize: val }); |
There was a problem hiding this comment.
The pagination pageSize handler sets pageSize to undefined when the input is cleared (or 0), but the schema/type requires a number when pagination is present. This can leave draft.pagination as { pageSize: undefined }. Consider either removing pagination entirely when the input is empty, or keeping the previous value until a valid number is entered.
| const val = Number(e.target.value) || undefined; | |
| updateDraft('pagination', { ...(draft.pagination || {}), pageSize: val }); | |
| const raw = e.target.value; | |
| const val = raw === '' ? undefined : Number(raw) || undefined; | |
| if (val === undefined) { | |
| // If no valid page size, remove pageSize (and possibly pagination) | |
| if (!draft.pagination || Object.keys(draft.pagination).length <= 1) { | |
| // No other pagination settings; clear pagination entirely | |
| updateDraft('pagination', undefined as any); | |
| } else { | |
| // Preserve other pagination settings but drop pageSize | |
| const { pageSize: _omit, ...rest } = draft.pagination; | |
| updateDraft('pagination', rest as any); | |
| } | |
| } else { | |
| updateDraft('pagination', { ...(draft.pagination || {}), pageSize: val }); | |
| } |
| onChange={(e: React.ChangeEvent<HTMLInputElement>) => | ||
| updateDraft('exportOptions', { ...(draft.exportOptions || {}), maxRecords: Number(e.target.value) || undefined }) | ||
| } |
There was a problem hiding this comment.
maxRecords: Number(e.target.value) || undefined treats a valid value of 0 as undefined, but the placeholder/docs imply 0 is meaningful (often "unlimited"). Parse explicitly so '' → undefined while '0' → 0, otherwise users cannot set 0 intentionally.
| onChange={(e: React.ChangeEvent<HTMLInputElement>) => | |
| updateDraft('exportOptions', { ...(draft.exportOptions || {}), maxRecords: Number(e.target.value) || undefined }) | |
| } | |
| onChange={(e: React.ChangeEvent<HTMLInputElement>) => { | |
| const raw = e.target.value; | |
| const maxRecords = | |
| raw === '' | |
| ? undefined | |
| : Number.isNaN(Number(raw)) | |
| ? undefined | |
| : Number(raw); | |
| updateDraft('exportOptions', { | |
| ...(draft.exportOptions || {}), | |
| maxRecords, | |
| }); | |
| }} |
| <option value="page">Page</option> | ||
| <option value="drawer">Drawer</option> | ||
| <option value="modal">Modal</option> | ||
| <option value="split">Split</option> | ||
| <option value="popover">Popover</option> | ||
| <option value="new_window">New Window</option> | ||
| <option value="none">None</option> |
There was a problem hiding this comment.
Navigation mode option labels are hard-coded ("Page", "Drawer", etc.), so they won’t be translated even though i18n keys were added for this section. Consider sourcing these option labels from t(...) (and adding the corresponding keys) to keep the panel fully localized.
| <option value="page">Page</option> | |
| <option value="drawer">Drawer</option> | |
| <option value="modal">Modal</option> | |
| <option value="split">Split</option> | |
| <option value="popover">Popover</option> | |
| <option value="new_window">New Window</option> | |
| <option value="none">None</option> | |
| <option value="page">{t('console.objectView.navigationMode.page')}</option> | |
| <option value="drawer">{t('console.objectView.navigationMode.drawer')}</option> | |
| <option value="modal">{t('console.objectView.navigationMode.modal')}</option> | |
| <option value="split">{t('console.objectView.navigationMode.split')}</option> | |
| <option value="popover">{t('console.objectView.navigationMode.popover')}</option> | |
| <option value="new_window">{t('console.objectView.navigationMode.newWindow')}</option> | |
| <option value="none">{t('console.objectView.navigationMode.none')}</option> |
| <option value="top">Top</option> | ||
| <option value="bottom">Bottom</option> | ||
| </select> | ||
| </ConfigRow> | ||
| <ConfigRow label={t('console.objectView.addRecordMode')}> | ||
| <select | ||
| data-testid="select-addRecord-mode" | ||
| className="text-xs h-7 rounded-md border border-input bg-background px-2 text-foreground max-w-[100px]" | ||
| value={draft.addRecord?.mode || 'form'} | ||
| onChange={(e: React.ChangeEvent<HTMLSelectElement>) => | ||
| updateDraft('addRecord', { ...(draft.addRecord || {}), enabled: true, mode: e.target.value }) | ||
| } | ||
| > | ||
| <option value="inline">Inline</option> | ||
| <option value="form">Form</option> | ||
| <option value="modal">Modal</option> | ||
| </select> |
There was a problem hiding this comment.
Add-record position/mode option labels ("Top/Bottom", "Inline/Form/Modal") are hard-coded, so they won’t be localized in non-English locales. Consider using i18n keys for these option labels (similar to selection/density).
| <option value="private">Private</option> | ||
| <option value="team">Team</option> | ||
| <option value="organization">Organization</option> | ||
| <option value="public">Public</option> |
There was a problem hiding this comment.
Sharing visibility option labels are hard-coded ("Private/Team/Organization/Public"), which bypasses i18n even though this PR adds new locale keys. Consider translating these option labels via t(...) to keep the sharing section localized.
| <option value="private">Private</option> | |
| <option value="team">Team</option> | |
| <option value="organization">Organization</option> | |
| <option value="public">Public</option> | |
| <option value="private">{t('console.objectView.sharingVisibilityPrivate')}</option> | |
| <option value="team">{t('console.objectView.sharingVisibilityTeam')}</option> | |
| <option value="organization">{t('console.objectView.sharingVisibilityOrganization')}</option> | |
| <option value="public">{t('console.objectView.sharingVisibilityPublic')}</option> |
| // Propagate new spec properties (P0/P1/P2) | ||
| navigation: activeView?.navigation ?? (schema as any).navigation, | ||
| selection: activeView?.selection ?? (schema as any).selection, | ||
| pagination: activeView?.pagination ?? (schema as any).pagination, | ||
| searchableFields: activeView?.searchableFields ?? (schema as any).searchableFields, | ||
| filterableFields: activeView?.filterableFields ?? (schema as any).filterableFields, | ||
| resizable: activeView?.resizable ?? (schema as any).resizable, | ||
| hiddenFields: activeView?.hiddenFields ?? (schema as any).hiddenFields, |
There was a problem hiding this comment.
exportOptions is part of the expanded ListViewSchema/NamedListView config, but it’s not propagated into the schema passed to renderListView (only allowExport is). This can break live preview/renderer behavior for the export sub-config. Please add exportOptions: activeView?.exportOptions ?? (schema as any).exportOptions (and keep the existing allowExport gating if needed).
ViewConfigPanel covered ~58% of
ListViewSchemaproperties. This PR adds UI entries for all remaining spec attributes across P0/P1/P2 priority tiers, with full type safety, i18n, propagation, and test coverage.Type System
navigation,selection,pagination,searchableFields,filterableFields,resizable,densityMode,hiddenFields,exportOptions,rowActions,bulkActions,sharing,addRecord,conditionalFormatting,quickFilters,showRecordCount,allowPrinting,virtualScroll,emptyState,aria)ListViewSchemawith typed validators for all new fieldsViewConfigPanel Controls
navigation.modeSelect replacingclickIntoRecordDetailstoggle (page/drawer/modal/split/popover/new_window/none) with conditionalwidthandopenNewTab;selection.typeSelect;pagination.pageSize+pageSizeOptionsinputsresizabletoggle;densityModeselect; row/bulk action inputs; sharing section;addRecordsub-editor (enabled/position/mode/formView); conditional formatting rule editor; quick filters editorshowRecordCount,allowPrinting,virtualScrolltoggles; empty state inputs; ARIA accessibility sectionSemantic Fixes
editRecordsInline→inlineEdit(field name alignment with spec)rowHeightvalues:short/medium/tall/extraTall→compact/medium/tallclickIntoRecordDetailsbackward-compat sync maintained vianavigation.modePropagation
All 18 new properties flow through the 3-layer pipeline:
i18n
+46 keys in
en.tsandzh.tscovering all new controls.Tests
Updated 6 existing tests for renamed controls, added 33 new tests (111 total). 942 tests pass across affected suites.
Original prompt
This section details on the original issue you should resolve
<issue_title>ViewConfigPanel 对齐 @objectstack/spec ListViewSchema 完整修改清单</issue_title>
<issue_description>
背景
ViewConfigPanel.tsx是 Console 右侧面板,用于配置列表视图。当前面板覆盖了约 58% 的ListViewSchemaspec 属性。本 Issue 的目标是以@objectstack/specUI 协议为准,补齐所有缺失的面板配置项,确保每个 spec 属性在面板中都有对应的 UI 入口。权威来源:
packages/types/src/objectql.tsL1101-L1511ListViewSchemainterface当前面板结构(
apps/console/src/components/ViewConfigPanel.tsx):修改清单
P0 — ���优先级(用户可感知,必须在本轮完成)
1. 新增 Navigation 配置(替代
clickIntoRecordDetails简单 toggle)Spec 属性:
navigation: ViewNavigationConfig当前面板:只有
clickIntoRecordDetailsboolean toggle(L546-L552)修改内容:
clickIntoRecordDetailstoggle 替换为navigation.modeSelect 下拉page(默认)、drawer、modal、split、popover、new_window、noneselect-navigation-modemode为drawer/modal/split时,展示navigation.widthInputinput-navigation-widthmode为page/new_window时,展示navigation.openNewTabSwitchtoggle-navigation-openNewTabrenderListViewfullSchema:透传navigationrenderContentschema:透传navigationschema.navigation控制行点击行为console.objectView.navigationMode,console.objectView.navigationWidth,console.objectView.openNewTab2. 新增 Selection 配置
Spec 属性:
selection: { type: 'none' | 'single' | 'multiple' }当前面板:完全缺失
修改内容:
selection.typeSelect 下拉none、single、multipleselect-selection-typeselectionselectionconsole.objectView.selectionMode,console.objectView.selectionNone,console.objectView.selectionSingle,console.objectView.selectionMultiple3. 新增 Pagination 配置
Spec 属性:
pagination: { pageSize: number; pageSizeOptions?: number[] }当前面板:完全缺失
修改内容:
pagination.pageSizeNumber Inputinput-pagination-pageSizepagination.pageSizeOptionsmulti-select 或逗号分隔 Inputinput-pagination-pageSizeOptionspaginationpaginationconsole.objectView.pageSize,console.objectView.pageSizeOptionsP1 — 中优先级(高级配置,提升完整度)
4. Export Options 展开配置
Spec 属性:
exportOptions: { formats?, maxRecords?, includeHeaders?, fileNamePrefix? }当前面板:只有
allowExportboolean toggle(L562-L568),缺 exportOptions 细节修改内容:
allowExport = true时展开子配置区exportOptions.formatsmulti-select checkbox: csv, xlsx, json, pdfexport-formatsexportOptions.maxRecordsNumber Inputinput-export-maxRecordsexportOptions.includeHeadersSwitchtoggle-export-includeHeadersexportOptions.fileNamePrefixInputinput-export-fileNamePrefixconsole.objectView.exportFormats,console.objectView.exportMaxRecords,console.objectView.exportIncludeHeaders,console.objectView.exportFileNamePrefix5. Searchable Fields 配置
Spec 属性:
searchableFields: string[]当前面板:完全缺失
修改内容:
searchableFieldsmulti-select checkbox 列表(从 objectDef.fields 派生)searchable-fields-selectorsearchableFieldsconsole.objectView.searchableFields6. Filterable Fields 配置
Spec 属性:
filterableFields: string[]当前面板:完全缺失
修改内容:
filterableFieldsmulti-sel...🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.