diff --git a/packages/components/src/renderers/complex/data-table.tsx b/packages/components/src/renderers/complex/data-table.tsx index 3029dbf586..eac281db7e 100644 --- a/packages/components/src/renderers/complex/data-table.tsx +++ b/packages/components/src/renderers/complex/data-table.tsx @@ -11,6 +11,7 @@ import React, { useState, useMemo, useRef, useEffect } from 'react'; import { cn } from '../../lib/utils'; import { ComponentRegistry } from '@object-ui/core'; import type { DataTableSchema } from '@object-ui/types'; +import { useObjectTranslation } from '@object-ui/react'; import { Table, TableHeader, @@ -52,6 +53,50 @@ import { type SortDirection = 'asc' | 'desc' | null; +// Default English fallback translations for the data table +const TABLE_DEFAULT_TRANSLATIONS: Record = { + 'table.rowsPerPage': 'Rows per page', + 'table.pageInfo': 'Page {{current}} of {{total}}', + 'table.totalRecords': '{{count}} total', +}; + +/** + * Safe wrapper for useObjectTranslation that falls back to English defaults + * when I18nProvider is not available (e.g., standalone usage). + */ +function useTableTranslation() { + try { + const result = useObjectTranslation(); + const testValue = result.t('table.rowsPerPage'); + if (testValue === 'table.rowsPerPage') { + return { + t: (key: string, options?: Record) => { + let value = TABLE_DEFAULT_TRANSLATIONS[key] || key; + if (options) { + for (const [k, v] of Object.entries(options)) { + value = value.replace(`{{${k}}}`, String(v)); + } + } + return value; + }, + }; + } + return { t: result.t }; + } catch { + return { + t: (key: string, options?: Record) => { + let value = TABLE_DEFAULT_TRANSLATIONS[key] || key; + if (options) { + for (const [k, v] of Object.entries(options)) { + value = value.replace(`{{${k}}}`, String(v)); + } + } + return value; + }, + }; + } +} + /** * Enterprise-level data table component with Airtable-like features. * @@ -110,6 +155,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { showAddRow = false, } = schema; + // i18n support for pagination labels + const { t } = useTableTranslation(); + // Ensure data is always an array – provider config objects or null/undefined // must not reach array operations like .filter() / .some() const data = Array.isArray(rawData) ? rawData : []; @@ -936,7 +984,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { {pagination && sortedData.length > 0 && (
- Rows per page: + {t('table.rowsPerPage')}: + {schema.search.shortcut && ( + + {schema.search.shortcut} + + )} +
+ )} + {schema.actions?.map((action, idx) => ( + + ))} + {schema.rightContent && } +
), { namespace: 'ui', label: 'Header Bar', inputs: [ - { name: 'crumbs', type: 'array', label: 'Breadcrumbs' } + { name: 'crumbs', type: 'array', label: 'Breadcrumbs' }, + { name: 'search', type: 'object', label: 'Search Configuration' }, + { name: 'actions', type: 'array', label: 'Action Slots' }, + { name: 'rightContent', type: 'object', label: 'Right Content' }, ], defaultProps: { crumbs: [ diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 475fb631d9..b099457845 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -73,6 +73,8 @@ const ar = { hideColumn: 'إخفاء العمود', freezeColumn: 'تجميد العمود', unfreezeColumn: 'إلغاء تجميد العمود', + pageInfo: 'صفحة {{current}} من {{total}}', + totalRecords: '{{count}} إجمالي', }, grid: { actions: 'إجراءات', @@ -104,6 +106,23 @@ const ar = { addRecord: 'إضافة سجل', tabs: 'علامات التبويب', allRecords: 'جميع السجلات', + search: 'بحث', + filter: 'تصفية', + filterRecords: 'تصفية السجلات', + sort: 'ترتيب', + sortRecords: 'ترتيب السجلات', + group: 'تجميع', + groupBy: 'تجميع حسب', + export: 'تصدير', + exportAs: 'تصدير كـ {{format}}', + color: 'لون', + rowColor: 'لون الصف', + colorByField: 'تلوين حسب الحقل', + clear: 'مسح', + none: 'لا شيء', + hideFields: 'إخفاء الحقول', + noItems: 'لم يتم العثور على عناصر', + noItemsMessage: 'لا توجد سجلات للعرض. حاول تعديل الفلاتر أو إضافة بيانات جديدة.', }, kanban: { addCard: 'إضافة بطاقة', diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index a73b29c51f..ebb6c8b208 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -72,6 +72,8 @@ const de = { hideColumn: 'Spalte ausblenden', freezeColumn: 'Spalte fixieren', unfreezeColumn: 'Spalte lösen', + pageInfo: 'Seite {{current}} von {{total}}', + totalRecords: '{{count}} gesamt', }, grid: { actions: 'Aktionen', @@ -103,6 +105,23 @@ const de = { addRecord: 'Datensatz hinzufügen', tabs: 'Tabs', allRecords: 'Alle Datensätze', + search: 'Suche', + filter: 'Filtern', + filterRecords: 'Datensätze filtern', + sort: 'Sortieren', + sortRecords: 'Datensätze sortieren', + group: 'Gruppieren', + groupBy: 'Gruppieren nach', + export: 'Exportieren', + exportAs: 'Exportieren als {{format}}', + color: 'Farbe', + rowColor: 'Zeilenfarbe', + colorByField: 'Nach Feld einfärben', + clear: 'Löschen', + none: 'Keine', + hideFields: 'Felder ausblenden', + noItems: 'Keine Einträge gefunden', + noItemsMessage: 'Es gibt keine Datensätze. Versuchen Sie, die Filter anzupassen oder neue Daten hinzuzufügen.', }, kanban: { addCard: 'Karte hinzufügen', diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 0ee5b89491..3aea702d4e 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -72,6 +72,8 @@ const en = { hideColumn: 'Hide column', freezeColumn: 'Freeze column', unfreezeColumn: 'Unfreeze column', + pageInfo: 'Page {{current}} of {{total}}', + totalRecords: '{{count}} total', }, grid: { actions: 'Actions', @@ -103,6 +105,23 @@ const en = { addRecord: 'Add record', tabs: 'Tabs', allRecords: 'All Records', + search: 'Search', + filter: 'Filter', + filterRecords: 'Filter Records', + sort: 'Sort', + sortRecords: 'Sort Records', + group: 'Group', + groupBy: 'Group By', + export: 'Export', + exportAs: 'Export as {{format}}', + color: 'Color', + rowColor: 'Row Color', + colorByField: 'Color by field', + clear: 'Clear', + none: 'None', + hideFields: 'Hide fields', + noItems: 'No items found', + noItemsMessage: 'There are no records to display. Try adjusting your filters or adding new data.', }, kanban: { addCard: 'Add card', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 76c9d06b2d..6201fdb567 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -72,6 +72,8 @@ const es = { hideColumn: 'Ocultar columna', freezeColumn: 'Fijar columna', unfreezeColumn: 'Desfijar columna', + pageInfo: 'Página {{current}} de {{total}}', + totalRecords: '{{count}} en total', }, grid: { actions: 'Acciones', @@ -103,6 +105,23 @@ const es = { addRecord: 'Agregar registro', tabs: 'Pestañas', allRecords: 'Todos los registros', + search: 'Buscar', + filter: 'Filtrar', + filterRecords: 'Filtrar registros', + sort: 'Ordenar', + sortRecords: 'Ordenar registros', + group: 'Agrupar', + groupBy: 'Agrupar por', + export: 'Exportar', + exportAs: 'Exportar como {{format}}', + color: 'Color', + rowColor: 'Color de fila', + colorByField: 'Colorear por campo', + clear: 'Borrar', + none: 'Ninguno', + hideFields: 'Ocultar campos', + noItems: 'No se encontraron elementos', + noItemsMessage: 'No hay registros para mostrar. Intente ajustar los filtros o agregar nuevos datos.', }, kanban: { addCard: 'Añadir tarjeta', diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index 9fab28b634..a2995db14e 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -72,6 +72,8 @@ const fr = { hideColumn: 'Masquer la colonne', freezeColumn: 'Figer la colonne', unfreezeColumn: 'Libérer la colonne', + pageInfo: 'Page {{current}} sur {{total}}', + totalRecords: '{{count}} au total', }, grid: { actions: 'Actions', @@ -103,6 +105,23 @@ const fr = { addRecord: 'Ajouter un enregistrement', tabs: 'Onglets', allRecords: 'Tous les enregistrements', + search: 'Rechercher', + filter: 'Filtrer', + filterRecords: 'Filtrer les enregistrements', + sort: 'Trier', + sortRecords: 'Trier les enregistrements', + group: 'Grouper', + groupBy: 'Grouper par', + export: 'Exporter', + exportAs: 'Exporter en {{format}}', + color: 'Couleur', + rowColor: 'Couleur de ligne', + colorByField: 'Colorer par champ', + clear: 'Effacer', + none: 'Aucun', + hideFields: 'Masquer les champs', + noItems: 'Aucun élément trouvé', + noItemsMessage: "Il n'y a aucun enregistrement à afficher. Essayez d'ajuster les filtres ou d'ajouter de nouvelles données.", }, kanban: { addCard: 'Ajouter une carte', diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 4cce1d872c..1247aa9c95 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -72,6 +72,8 @@ const ja = { hideColumn: '列を非表示', freezeColumn: '列を固定', unfreezeColumn: '列の固定を解除', + pageInfo: '{{total}}ページ中{{current}}ページ', + totalRecords: '合計{{count}}件', }, grid: { actions: 'アクション', @@ -103,6 +105,23 @@ const ja = { addRecord: 'レコードを追加', tabs: 'タブ', allRecords: 'すべてのレコード', + search: '検索', + filter: 'フィルター', + filterRecords: 'レコードをフィルター', + sort: '並べ替え', + sortRecords: 'レコードを並べ替え', + group: 'グループ', + groupBy: 'グループ化', + export: 'エクスポート', + exportAs: '{{format}}としてエクスポート', + color: '色', + rowColor: '行の色', + colorByField: 'フィールドで色分け', + clear: 'クリア', + none: 'なし', + hideFields: 'フィールドを非表示', + noItems: '項目が見つかりません', + noItemsMessage: '表示するレコードがありません。フィルターを調整するか、新しいデータを追加してください。', }, kanban: { addCard: 'カードを追加', diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 95aa22db13..cd79e2a02a 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -72,6 +72,8 @@ const ko = { hideColumn: '열 숨기기', freezeColumn: '열 고정', unfreezeColumn: '열 고정 해제', + pageInfo: '{{total}} 페이지 중 {{current}} 페이지', + totalRecords: '총 {{count}}개', }, grid: { actions: '작업', @@ -103,6 +105,23 @@ const ko = { addRecord: '레코드 추가', tabs: '탭', allRecords: '전체 레코드', + search: '검색', + filter: '필터', + filterRecords: '레코드 필터', + sort: '정렬', + sortRecords: '레코드 정렬', + group: '그룹', + groupBy: '그룹 기준', + export: '내보내기', + exportAs: '{{format}}으로 내보내기', + color: '색상', + rowColor: '행 색상', + colorByField: '필드별 색상', + clear: '지우기', + none: '없음', + hideFields: '필드 숨기기', + noItems: '항목을 찾을 수 없습니다', + noItemsMessage: '표시할 레코드가 없습니다. 필터를 조정하거나 새 데이터를 추가해 보세요.', }, kanban: { addCard: '카드 추가', diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index e6ade96e9e..1a3790bfd1 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -72,6 +72,8 @@ const pt = { hideColumn: 'Ocultar coluna', freezeColumn: 'Fixar coluna', unfreezeColumn: 'Soltar coluna', + pageInfo: 'Página {{current}} de {{total}}', + totalRecords: '{{count}} no total', }, grid: { actions: 'Ações', @@ -103,6 +105,23 @@ const pt = { addRecord: 'Adicionar registro', tabs: 'Abas', allRecords: 'Todos os registros', + search: 'Pesquisar', + filter: 'Filtrar', + filterRecords: 'Filtrar registros', + sort: 'Ordenar', + sortRecords: 'Ordenar registros', + group: 'Agrupar', + groupBy: 'Agrupar por', + export: 'Exportar', + exportAs: 'Exportar como {{format}}', + color: 'Cor', + rowColor: 'Cor da linha', + colorByField: 'Colorir por campo', + clear: 'Limpar', + none: 'Nenhum', + hideFields: 'Ocultar campos', + noItems: 'Nenhum item encontrado', + noItemsMessage: 'Não há registros para exibir. Tente ajustar os filtros ou adicionar novos dados.', }, kanban: { addCard: 'Adicionar cartão', diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 2f0eb7ec04..13731ce881 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -72,6 +72,8 @@ const ru = { hideColumn: 'Скрыть столбец', freezeColumn: 'Закрепить столбец', unfreezeColumn: 'Открепить столбец', + pageInfo: 'Страница {{current}} из {{total}}', + totalRecords: 'Всего {{count}}', }, grid: { actions: 'Действия', @@ -103,6 +105,23 @@ const ru = { addRecord: 'Добавить запись', tabs: 'Вкладки', allRecords: 'Все записи', + search: 'Поиск', + filter: 'Фильтр', + filterRecords: 'Фильтр записей', + sort: 'Сортировка', + sortRecords: 'Сортировка записей', + group: 'Группировка', + groupBy: 'Группировать по', + export: 'Экспорт', + exportAs: 'Экспорт в {{format}}', + color: 'Цвет', + rowColor: 'Цвет строки', + colorByField: 'Окраска по полю', + clear: 'Очистить', + none: 'Нет', + hideFields: 'Скрыть поля', + noItems: 'Элементы не найдены', + noItemsMessage: 'Нет записей для отображения. Попробуйте изменить фильтры или добавить новые данные.', }, kanban: { addCard: 'Добавить карточку', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index e36bf6f001..0dbf94e3da 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -72,6 +72,8 @@ const zh = { hideColumn: '隐藏列', freezeColumn: '冻结列', unfreezeColumn: '取消冻结列', + pageInfo: '第 {{current}} 页,共 {{total}} 页', + totalRecords: '共 {{count}} 条', }, grid: { actions: '操作', @@ -103,6 +105,23 @@ const zh = { addRecord: '添加记录', tabs: '标签页', allRecords: '全部记录', + search: '搜索', + filter: '筛选', + filterRecords: '筛选记录', + sort: '排序', + sortRecords: '排序记录', + group: '分组', + groupBy: '分组依据', + export: '导出', + exportAs: '导出为 {{format}}', + color: '颜色', + rowColor: '行颜色', + colorByField: '按字段着色', + clear: '清除', + none: '无', + hideFields: '隐藏字段', + noItems: '未找到项目', + noItemsMessage: '没有可显示的记录。请尝试调整筛选条件或添加新数据。', }, kanban: { addCard: '添加卡片', diff --git a/packages/layout/src/SidebarNav.tsx b/packages/layout/src/SidebarNav.tsx index 70b0051b4d..8c7e2d4072 100644 --- a/packages/layout/src/SidebarNav.tsx +++ b/packages/layout/src/SidebarNav.tsx @@ -9,44 +9,155 @@ import { SidebarMenu, SidebarMenuItem, SidebarMenuButton, + SidebarMenuSub, + SidebarMenuSubItem, + SidebarMenuSubButton, + Badge, + Input, + Collapsible, + CollapsibleTrigger, + CollapsibleContent, } from '@object-ui/components'; +import { ChevronRight, Search } from 'lucide-react'; export interface NavItem { title: string; href: string; icon?: React.ComponentType<{ className?: string }>; + badge?: string | number; + badgeVariant?: 'default' | 'destructive' | 'outline'; + children?: NavItem[]; +} + +export interface NavGroup { + label: string; + items: NavItem[]; } export interface SidebarNavProps { - items: NavItem[]; + items: NavItem[] | NavGroup[]; title?: string; className?: string; collapsible?: "offcanvas" | "icon" | "none"; + searchEnabled?: boolean; + searchPlaceholder?: string; +} + +function isNavGroup(item: NavItem | NavGroup): item is NavGroup { + return 'items' in item && !('href' in item); } -export function SidebarNav({ items, title = "Application", className, collapsible = "icon" }: SidebarNavProps) { +function NavItemRenderer({ item, pathname }: { item: NavItem; pathname: string }) { + if (item.children && item.children.length > 0) { + return ( + + + + + {item.icon && } + {item.title} + {item.badge != null && ( + + {item.badge} + + )} + + + + + + {item.children.map((child) => ( + + + + {child.icon && } + {child.title} + {child.badge != null && ( + + {child.badge} + + )} + + + + ))} + + + + + ); + } + + return ( + + + + {item.icon && } + {item.title} + {item.badge != null && ( + + {item.badge} + + )} + + + + ); +} + +export function SidebarNav({ items, title = "Application", className, collapsible = "icon", searchEnabled = false, searchPlaceholder = "Search..." }: SidebarNavProps) { const location = useLocation(); + const [search, setSearch] = React.useState(''); + + const flatItems: Array<{ groupLabel?: string; items: NavItem[] }> = React.useMemo(() => { + if (items.length === 0) return []; + if (isNavGroup(items[0])) { + return (items as NavGroup[]).map(g => ({ groupLabel: g.label, items: g.items })); + } + return [{ items: items as NavItem[] }]; + }, [items]); + + const filteredGroups = React.useMemo(() => { + if (!search) return flatItems; + const lowerSearch = search.toLowerCase(); + return flatItems.map(group => ({ + ...group, + items: group.items.filter(item => + item.title.toLowerCase().includes(lowerSearch) || + item.children?.some(child => child.title.toLowerCase().includes(lowerSearch)) + ), + })).filter(group => group.items.length > 0); + }, [flatItems, search]); return ( - - {title} - - - {items.map((item) => ( - - - - {item.icon && } - {item.title} - - - - ))} - - - + {searchEnabled && ( +
+
+ + setSearch(e.target.value)} + className="pl-8 h-9" + /> +
+
+ )} + {filteredGroups.map((group, gIdx) => ( + + {group.groupLabel || title} + + + {group.items.map((item) => ( + + ))} + + + + ))}
); diff --git a/packages/plugin-grid/src/ObjectGrid.tsx b/packages/plugin-grid/src/ObjectGrid.tsx index 3033513abd..92a3522bb5 100644 --- a/packages/plugin-grid/src/ObjectGrid.tsx +++ b/packages/plugin-grid/src/ObjectGrid.tsx @@ -23,7 +23,7 @@ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import type { ObjectGridSchema, DataSource, ListColumn, ViewData } from '@object-ui/types'; -import { SchemaRenderer, useDataScope, useNavigationOverlay, useAction } from '@object-ui/react'; +import { SchemaRenderer, useDataScope, useNavigationOverlay, useAction, useObjectTranslation } from '@object-ui/react'; import { getCellRenderer, formatCurrency, formatCompactCurrency, formatDate, formatPercent, humanizeLabel } from '@object-ui/fields'; import { Badge, Button, NavigationOverlay, @@ -39,6 +39,57 @@ import { useColumnSummary } from './useColumnSummary'; import { RowActionMenu, formatActionLabel } from './components/RowActionMenu'; import { BulkActionBar } from './components/BulkActionBar'; +// Default English fallback translations for the grid +const GRID_DEFAULT_TRANSLATIONS: Record = { + 'grid.actions': 'Actions', + 'grid.edit': 'Edit', + 'grid.delete': 'Delete', + 'grid.export': 'Export', + 'grid.exportAs': 'Export as {{format}}', + 'grid.loading': 'Loading grid...', + 'grid.errorLoading': 'Error loading grid', + 'grid.pullToRefresh': 'Pull to refresh', + 'grid.refreshing': 'Refreshing…', + 'grid.openRecord': 'Open record', +}; + +/** + * Safe wrapper for useObjectTranslation that falls back to English defaults + * when I18nProvider is not available (e.g., standalone usage). + */ +function useGridTranslation() { + try { + const result = useObjectTranslation(); + const testValue = result.t('grid.actions'); + if (testValue === 'grid.actions') { + return { + t: (key: string, options?: Record) => { + let value = GRID_DEFAULT_TRANSLATIONS[key] || key; + if (options) { + for (const [k, v] of Object.entries(options)) { + value = value.replace(`{{${k}}}`, String(v)); + } + } + return value; + }, + }; + } + return { t: result.t }; + } catch { + return { + t: (key: string, options?: Record) => { + let value = GRID_DEFAULT_TRANSLATIONS[key] || key; + if (options) { + for (const [k, v] of Object.entries(options)) { + value = value.replace(`{{${k}}}`, String(v)); + } + } + return value; + }, + }; + } +} + export interface ObjectGridProps { schema: ObjectGridSchema; dataSource?: DataSource; @@ -126,6 +177,7 @@ export const ObjectGrid: React.FC = ({ const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const { t } = useGridTranslation(); const [objectSchema, setObjectSchema] = useState(null); const [useCardView, setUseCardView] = useState(false); const [refreshKey, setRefreshKey] = useState(0); @@ -776,7 +828,7 @@ export const ObjectGrid: React.FC = ({ if (error) { return (
-

Error loading grid

+

{t('grid.errorLoading')}

{error.message}

); @@ -802,7 +854,7 @@ export const ObjectGrid: React.FC = ({ return (
-

Loading grid...

+

{t('grid.loading')}

); } @@ -840,7 +892,7 @@ export const ObjectGrid: React.FC = ({ const columnsWithActions = (hasActions || hasRowActions) ? [ ...persistedColumns, { - header: 'Actions', + header: t('grid.actions'), accessorKey: '_actions', cell: (_value: any, row: any) => ( = ({ className="h-7 px-2 text-muted-foreground hover:text-primary text-xs" > - Export + {t('grid.export')} @@ -1211,7 +1263,7 @@ export const ObjectGrid: React.FC = ({ onClick={() => handleExport(format)} > - Export as {format.toUpperCase()} + {t('grid.exportAs', { format: format.toUpperCase() })} ))} @@ -1377,7 +1429,7 @@ export const ObjectGrid: React.FC = ({ className="flex items-center justify-center text-xs text-muted-foreground" style={{ height: pullDistance }} > - {isRefreshing ? 'Refreshing…' : 'Pull to refresh'} + {isRefreshing ? t('grid.refreshing') : t('grid.pullToRefresh')} )} {gridToolbar} diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index 0b492b7352..6d972969c6 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -196,8 +196,18 @@ const LIST_DEFAULT_TRANSLATIONS: Record = { 'list.noItemsMessage': 'There are no records to display. Try adjusting your filters or adding new data.', 'list.search': 'Search', 'list.filter': 'Filter', + 'list.filterRecords': 'Filter Records', 'list.sort': 'Sort', + 'list.sortRecords': 'Sort Records', + 'list.group': 'Group', + 'list.groupBy': 'Group By', 'list.export': 'Export', + 'list.exportAs': 'Export as {{format}}', + 'list.color': 'Color', + 'list.rowColor': 'Row Color', + 'list.colorByField': 'Color by field', + 'list.clear': 'Clear', + 'list.none': 'None', 'list.hideFields': 'Hide fields', 'list.showAll': 'Show all', 'list.pullToRefresh': 'Pull to refresh', @@ -1113,7 +1123,7 @@ export const ListView: React.FC = ({ )} > - Filter + {t('list.filter')} {hasFilters && ( {currentFilters.conditions?.length || 0} @@ -1124,7 +1134,7 @@ export const ListView: React.FC = ({
-

Filter Records

+

{t('list.filterRecords')}

= ({ )} > - Group + {t('list.group')} {groupingConfig && groupingConfig.fields?.length > 0 && ( {groupingConfig.fields.length} @@ -1163,10 +1173,10 @@ export const ListView: React.FC = ({
-

Group By

+

{t('list.groupBy')}

{groupingConfig && ( )}
@@ -1212,7 +1222,7 @@ export const ListView: React.FC = ({ )} > - Sort + {t('list.sort')} {currentSort.length > 0 && ( {currentSort.length} @@ -1223,7 +1233,7 @@ export const ListView: React.FC = ({
-

Sort Records

+

{t('list.sortRecords')}

= ({ )} > - Color + {t('list.color')}
-

Row Color

+

{t('list.rowColor')}

{rowColorConfig && ( )}
- + handleSearchChange(e.target.value)} className="pl-7 h-8 text-xs" diff --git a/packages/plugin-view/src/ViewSwitcher.tsx b/packages/plugin-view/src/ViewSwitcher.tsx index 24ebb8c732..8128b0f4a1 100644 --- a/packages/plugin-view/src/ViewSwitcher.tsx +++ b/packages/plugin-view/src/ViewSwitcher.tsx @@ -32,6 +32,11 @@ import { LayoutGrid, List, Map, + Plus, + Share2, + Settings, + Copy, + Trash2, icons, type LucideIcon, } from 'lucide-react'; @@ -42,6 +47,9 @@ export type ViewSwitcherProps = { schema: ViewSwitcherSchema; className?: string; onViewChange?: (view: ViewType) => void; + onCreateView?: () => void; + onViewAction?: (action: string, view: ViewType) => void; + createViewLabel?: string; [key: string]: any; }; @@ -155,10 +163,27 @@ function getInitialView(schema: ViewSwitcherSchema): ViewType | undefined { return schema.views?.[0]?.type; } +const DEFAULT_VIEW_ACTION_ICONS: Record = { + share: Share2, + settings: Settings, + duplicate: Copy, + delete: Trash2, +}; + +const DEFAULT_VIEW_ACTION_LABELS: Record = { + share: 'Share', + settings: 'Settings', + duplicate: 'Duplicate', + delete: 'Delete', +}; + export const ViewSwitcher: React.FC = ({ schema, className, onViewChange, + onCreateView, + onViewAction, + createViewLabel = 'Create view', ...props }) => { const storageKey = React.useMemo(() => { @@ -225,8 +250,42 @@ export const ViewSwitcher: React.FC = ({ const isVertical = position === 'left' || position === 'right'; const orientation = isVertical ? 'vertical' : 'horizontal'; + const viewActionButtons = schema.viewActions && schema.viewActions.length > 0 ? ( +
+ {schema.viewActions.map((action, idx) => { + const ActionIcon = action.icon + ? resolveIcon(action.icon) || DEFAULT_VIEW_ACTION_ICONS[action.type] + : DEFAULT_VIEW_ACTION_ICONS[action.type]; + return ( + + ); + })} +
+ ) : null; + + const createViewButton = schema.allowCreateView ? ( + + ) : null; + const switcher = ( -
+
{variant === 'dropdown' && (