Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions packages/components/src/renderers/complex/data-table.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand DownExpand Up@@ -52,6 +53,50 @@ import {

type SortDirection = 'asc' | 'desc' | null;

// Default English fallback translations for the data table
const TABLE_DEFAULT_TRANSLATIONS: Record<string, string> = {
'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<string, unknown>) => {
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<string, unknown>) => {
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.
*
Expand DownExpand Up@@ -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 : [];
Expand DownExpand Up@@ -936,7 +984,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
{pagination && sortedData.length > 0 && (
<div className="flex flex-col sm:flex-row items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span className="text-xs sm:text-sm text-muted-foreground">Rows per page:</span>
<span className="text-xs sm:text-sm text-muted-foreground">{t('table.rowsPerPage')}:</span>
<Select
value={pageSize.toString()}
onValueChange={(value) => {
Expand All@@ -959,7 +1007,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {

<div className="flex items-center gap-2">
<span className="text-xs sm:text-sm text-muted-foreground">
Page {currentPage} of {totalPages} <span className="hidden sm:inline">({sortedData.length} total)</span>
{t('table.pageInfo', { current: currentPage, total: totalPages })} <span className="hidden sm:inline">({t('table.totalRecords', { count: sortedData.length })})</span>
</span>
<div className="flex items-center gap-1">
<Button
Expand Down
80 changes: 69 additions & 11 deletions packages/components/src/renderers/navigation/header-bar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,8 @@

import React from 'react';
import { ComponentRegistry } from '@object-ui/core';
import type { HeaderBarSchema } from '@object-ui/types';
import { resolveI18nLabel } from '@object-ui/react';
import type { HeaderBarSchema, BreadcrumbItem as BreadcrumbItemType } from '@object-ui/types';
import { resolveI18nLabel, SchemaRenderer } from '@object-ui/react';
import {
SidebarTrigger,
Separator,
Expand All@@ -18,8 +18,45 @@ import {
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
BreadcrumbPage
BreadcrumbPage,
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
Input,
} from '../../ui';
import { ChevronDown, Search } from 'lucide-react';

function BreadcrumbLabel({ crumb, isLast }: { crumb: BreadcrumbItemType; isLast: boolean }) {
const label = resolveI18nLabel(crumb.label) ?? '';

if (crumb.siblings && crumb.siblings.length > 0) {
return (
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-1">
{isLast ? (
<span className="font-semibold">{label}</span>
) : (
<span>{label}</span>
)}
<ChevronDown className="h-3 w-3" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{crumb.siblings.map((sibling, i) => (
<DropdownMenuItem key={i} asChild>
<a href={sibling.href}>{sibling.label}</a>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}

if (isLast) {
return <BreadcrumbPage>{label}</BreadcrumbPage>;
}
return <BreadcrumbLink href={crumb.href || '#'}>{label}</BreadcrumbLink>;
}

ComponentRegistry.register('header-bar',
({ schema }: { schema: HeaderBarSchema }) => (
Expand All@@ -28,27 +65,48 @@ ComponentRegistry.register('header-bar',
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
{schema.crumbs?.map((crumb: any, idx: number) => (
{schema.crumbs?.map((crumb: BreadcrumbItemType, idx: number) => (
<React.Fragment key={idx}>
<BreadcrumbItem>
{idx === schema.crumbs.length - 1 ? (
<BreadcrumbPage>{resolveI18nLabel(crumb.label) ?? ''}</BreadcrumbPage>
) : (
<BreadcrumbLink href={crumb.href || '#'}>{resolveI18nLabel(crumb.label) ?? ''}</BreadcrumbLink>
)}
<BreadcrumbLabel crumb={crumb} isLast={idx === schema.crumbs!.length - 1} />
</BreadcrumbItem>
{idx < schema.crumbs.length - 1 && <BreadcrumbSeparator />}
{idx < schema.crumbs!.length - 1 && <BreadcrumbSeparator />}
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>

<div className="ml-auto flex items-center gap-2">
{schema.search?.enabled && (
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder={schema.search.placeholder}
className="pl-8 w-[200px] lg:w-[300px]"
/>
Comment on lines +79 to +87

CopilotAIFeb 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new search slot renders an <Input> but doesn’t expose any way for schema/consumers to receive the query (no value, onChange, binding, or action dispatch). As-is, it’s a “dead” input that won’t affect application state. Consider adding a schema-level callback/event (e.g., onSearchChange action name) or a bind path so the input is actually usable.

Copilot uses AI. Check for mistakes.
{schema.search.shortcut && (
<kbd className="pointer-events-none absolute right-2 top-2 hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
{schema.search.shortcut}
</kbd>
)}
</div>
)}
{schema.actions?.map((action, idx) => (
<SchemaRenderer key={idx} schema={action} />
))}
{schema.rightContent && <SchemaRenderer schema={schema.rightContent} />}
</div>
</header>
),
{
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: [
Expand Down
19 changes: 19 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,6 +73,8 @@ const ar = {
hideColumn: 'إخفاء العمود',
freezeColumn: 'تجميد العمود',
unfreezeColumn: 'إلغاء تجميد العمود',
pageInfo: 'صفحة {{current}} من {{total}}',
totalRecords: '{{count}} إجمالي',
},
grid: {
actions: 'إجراءات',
Expand DownExpand Up@@ -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: 'إضافة بطاقة',
Expand Down
19 changes: 19 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand DownExpand Up@@ -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',
Expand Down
19 changes: 19 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand DownExpand Up@@ -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',
Expand Down
19 changes: 19 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand DownExpand Up@@ -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',
Expand Down
19 changes: 19 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand DownExpand Up@@ -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',
Expand Down
19 changes: 19 additions & 0 deletions packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,6 +72,8 @@ const ja = {
hideColumn: '列を非表示',
freezeColumn: '列を固定',
unfreezeColumn: '列の固定を解除',
pageInfo: '{{total}}ページ中{{current}}ページ',
totalRecords: '合計{{count}}件',
},
grid: {
actions: 'アクション',
Expand DownExpand Up@@ -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: 'カードを追加',
Expand Down
Loading
Loading