diff --git a/ROADMAP.md b/ROADMAP.md index bc44edfab..fe75647f4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -251,7 +251,7 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind 2. ~~**Console `renderListView`:** Omits toolbar/display flags from `fullSchema`~~ → Now passes all config properties 3. ~~**`NamedListView` type:** Missing toolbar/display properties~~ → Added as first-class properties 4. ~~**Plugin `renderListView` schema missing toolbar flags:** `renderContent` → `renderListView` schema did not include `showSearch`/`showFilters`/`showSort`~~ → Now propagated (PR #771) -5. ~~**ListView toolbar unconditionally rendered:** Search/Filter/Sort buttons always visible regardless of schema flags~~ → Now conditionally rendered based on `schema.showSearch`/`showFilters`/`showSort` (PR #771) +5. ~~**ListView toolbar unconditionally rendered:** Search/Filter/Sort buttons always visible regardless of schema flags~~ → Now conditionally rendered based on `schema.showSearch`/`showFilters`/`showSort` (PR #771); `userActions.sort`/`search`/`filter`/`rowHeight`/`addRecordForm` now override toolbar flags (Issue #737) 6. ~~**Hide Fields/Group/Color/Density buttons always visible:** No schema property to control visibility~~ → Added `showHideFields`/`showGroup`/`showColor`/`showDensity` with conditional rendering (Issue #719) 7. ~~**Export toggle broken:** ViewConfigPanel writes `allowExport: boolean` but ListView checks `exportOptions` object~~ → Export now checks both `exportOptions && allowExport !== false`; Console clears `exportOptions` when `allowExport === false` (Issue #719) 8. ~~**`hasExport` logic bug:** `draft.allowExport !== false` was always true when undefined~~ → Fixed to `draft.allowExport === true || draft.exportOptions != null` (Issue #719) diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 27a6175c9..78c574f0f 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -101,6 +101,7 @@ const ar = { list: { recordCount: '{{count}} سجلات', recordCountOne: '{{count}} سجل', + addRecord: 'إضافة سجل', }, kanban: { addCard: 'إضافة بطاقة', diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index c74d2508e..d79de08a2 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -100,6 +100,7 @@ const de = { list: { recordCount: '{{count}} Datensätze', recordCountOne: '{{count}} Datensatz', + addRecord: 'Datensatz hinzufügen', }, kanban: { addCard: 'Karte hinzufügen', diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 6850dca62..8522c898a 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -100,6 +100,7 @@ const en = { list: { recordCount: '{{count}} records', recordCountOne: '{{count}} record', + addRecord: 'Add record', }, kanban: { addCard: 'Add card', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index a79d879c1..3cd1ed9bf 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -100,6 +100,7 @@ const es = { list: { recordCount: '{{count}} registros', recordCountOne: '{{count}} registro', + addRecord: 'Agregar registro', }, kanban: { addCard: 'Añadir tarjeta', diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index 6d107ef3f..2cd91d6dc 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -100,6 +100,7 @@ const fr = { list: { recordCount: '{{count}} enregistrements', recordCountOne: '{{count}} enregistrement', + addRecord: 'Ajouter un enregistrement', }, kanban: { addCard: 'Ajouter une carte', diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index cecf26ed5..7b5cdc943 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -100,6 +100,7 @@ const ja = { list: { recordCount: '{{count}} 件のレコード', recordCountOne: '{{count}} 件のレコード', + addRecord: 'レコードを追加', }, kanban: { addCard: 'カードを追加', diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 73f278252..4ef44aec3 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -100,6 +100,7 @@ const ko = { list: { recordCount: '{{count}}개 레코드', recordCountOne: '{{count}}개 레코드', + addRecord: '레코드 추가', }, kanban: { addCard: '카드 추가', diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 3b245550e..347350d53 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -100,6 +100,7 @@ const pt = { list: { recordCount: '{{count}} registros', recordCountOne: '{{count}} registro', + addRecord: 'Adicionar registro', }, kanban: { addCard: 'Adicionar cartão', diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 3bb174dbc..48502c540 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -100,6 +100,7 @@ const ru = { list: { recordCount: '{{count}} записей', recordCountOne: '{{count}} запись', + addRecord: 'Добавить запись', }, kanban: { addCard: 'Добавить карточку', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 0244e6242..5382241ca 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -100,6 +100,7 @@ const zh = { list: { recordCount: '{{count}} 条记录', recordCountOne: '{{count}} 条记录', + addRecord: '添加记录', }, kanban: { addCard: '添加卡片', diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index fc93be0a1..980b4acce 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -202,6 +202,7 @@ const LIST_DEFAULT_TRANSLATIONS: Record = { 'list.pullToRefresh': 'Pull to refresh', 'list.refreshing': 'Refreshing…', 'list.dataLimitReached': 'Showing first {{limit}} records. More data may be available.', + 'list.addRecord': 'Add record', }; /** @@ -265,6 +266,7 @@ export const ListView: React.FC = ({ // Resolve toolbar visibility flags: userActions overrides showX flags const toolbarFlags = React.useMemo(() => { const ua = schema.userActions; + const addRecordEnabled = schema.addRecord?.enabled === true && ua?.addRecordForm !== false; return { showSearch: ua?.search !== undefined ? ua.search : schema.showSearch !== false, showSort: ua?.sort !== undefined ? ua.sort : schema.showSort !== false, @@ -273,8 +275,10 @@ export const ListView: React.FC = ({ showHideFields: schema.showHideFields !== false, showGroup: schema.showGroup !== false, showColor: schema.showColor !== false, + showAddRecord: addRecordEnabled, + addRecordPosition: (schema.addRecord?.position === 'bottom' ? 'bottom' : 'top') as 'top' | 'bottom', }; - }, [schema.userActions, schema.showSearch, schema.showSort, schema.showFilters, schema.showDensity, schema.showHideFields, schema.showGroup, schema.showColor]); + }, [schema.userActions, schema.showSearch, schema.showSort, schema.showFilters, schema.showDensity, schema.showHideFields, schema.showGroup, schema.showColor, schema.addRecord, schema.userActions?.addRecordForm]); const [currentView, setCurrentView] = React.useState( (schema.viewType as ViewType) @@ -1324,8 +1328,8 @@ export const ListView: React.FC = ({ {/* Right: Add Record + Search */}
- {/* Add Record */} - {schema.addRecord?.enabled && ( + {/* Add Record (top position) */} + {toolbarFlags.showAddRecord && toolbarFlags.addRecordPosition === 'top' && ( )} @@ -1453,6 +1457,22 @@ export const ListView: React.FC = ({ )}
+ {/* Add Record (bottom position) */} + {toolbarFlags.showAddRecord && toolbarFlags.addRecordPosition === 'bottom' && ( +
+ +
+ )} + {/* Bulk Actions Bar — skip for grid view since ObjectGrid renders its own BulkActionBar */} {schema.bulkActions && schema.bulkActions.length > 0 && selectedRows.length > 0 && currentView !== 'grid' && (
{ renderWithProvider(); expect(screen.queryByTestId('add-record-button')).not.toBeInTheDocument(); }); + + it('should hide add record button when userActions.addRecordForm is false', () => { + const schema: ListViewSchema = { + type: 'list-view', + objectName: 'contacts', + viewType: 'grid', + fields: ['name', 'email'], + addRecord: { enabled: true }, + userActions: { addRecordForm: false }, + }; + + renderWithProvider(); + expect(screen.queryByTestId('add-record-button')).not.toBeInTheDocument(); + }); + + it('should render add record button at bottom when position is bottom', () => { + const schema: ListViewSchema = { + type: 'list-view', + objectName: 'contacts', + viewType: 'grid', + fields: ['name', 'email'], + addRecord: { enabled: true, position: 'bottom' }, + }; + + renderWithProvider(); + const btn = screen.getByTestId('add-record-button'); + expect(btn).toBeInTheDocument(); + // The bottom button is wrapped in a border-t div outside the toolbar + expect(btn.closest('div.border-t')).toBeTruthy(); + }); + + it('should render add record button in toolbar when position is top', () => { + const schema: ListViewSchema = { + type: 'list-view', + objectName: 'contacts', + viewType: 'grid', + fields: ['name', 'email'], + addRecord: { enabled: true, position: 'top' }, + }; + + renderWithProvider(); + const btn = screen.getByTestId('add-record-button'); + expect(btn).toBeInTheDocument(); + // The top button is inside the toolbar border-b div + expect(btn.closest('div.border-b')).toBeTruthy(); + }); }); // ============================