Skip to content

Commit e1bdc9f

Browse files
authored
Merge pull request #905 from objectstack-ai/copilot/optimize-platform-ui-schema
Extend schema expressiveness for HeaderBar, SidebarNav, BreadcrumbItem, ViewSwitcher; fix i18n across data-table, ListView toolbar, and ObjectGrid
2 parents f8450a1 + 2034218 commit e1bdc9f

File tree

20 files changed

+645
-66
lines changed

20 files changed

+645
-66
lines changed

packages/components/src/renderers/complex/data-table.tsx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import React, { useState, useMemo, useRef, useEffect } from 'react';
1111
import { cn } from '../../lib/utils';
1212
import { ComponentRegistry } from '@object-ui/core';
1313
import type { DataTableSchema } from '@object-ui/types';
14+
import { useObjectTranslation } from '@object-ui/react';
1415
import {
1516
Table,
1617
TableHeader,
@@ -52,6 +53,50 @@ import {
5253

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

56+
// Default English fallback translations for the data table
57+
const TABLE_DEFAULT_TRANSLATIONS: Record<string, string> = {
58+
'table.rowsPerPage': 'Rows per page',
59+
'table.pageInfo': 'Page {{current}} of {{total}}',
60+
'table.totalRecords': '{{count}} total',
61+
};
62+
63+
/**
64+
* Safe wrapper for useObjectTranslation that falls back to English defaults
65+
* when I18nProvider is not available (e.g., standalone usage).
66+
*/
67+
function useTableTranslation() {
68+
try {
69+
const result = useObjectTranslation();
70+
const testValue = result.t('table.rowsPerPage');
71+
if (testValue === 'table.rowsPerPage') {
72+
return {
73+
t: (key: string, options?: Record<string, unknown>) => {
74+
let value = TABLE_DEFAULT_TRANSLATIONS[key] || key;
75+
if (options) {
76+
for (const [k, v] of Object.entries(options)) {
77+
value = value.replace(`{{${k}}}`, String(v));
78+
}
79+
}
80+
return value;
81+
},
82+
};
83+
}
84+
return { t: result.t };
85+
} catch {
86+
return {
87+
t: (key: string, options?: Record<string, unknown>) => {
88+
let value = TABLE_DEFAULT_TRANSLATIONS[key] || key;
89+
if (options) {
90+
for (const [k, v] of Object.entries(options)) {
91+
value = value.replace(`{{${k}}}`, String(v));
92+
}
93+
}
94+
return value;
95+
},
96+
};
97+
}
98+
}
99+
55100
/**
56101
* Enterprise-level data table component with Airtable-like features.
57102
*
@@ -110,6 +155,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
110155
showAddRow = false,
111156
} = schema;
112157

158+
// i18n support for pagination labels
159+
const { t } = useTableTranslation();
160+
113161
// Ensure data is always an array – provider config objects or null/undefined
114162
// must not reach array operations like .filter() / .some()
115163
const data = Array.isArray(rawData) ? rawData : [];
@@ -936,7 +984,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
936984
{pagination && sortedData.length > 0 && (
937985
<div className="flex flex-col sm:flex-row items-center justify-between gap-2">
938986
<div className="flex items-center gap-2">
939-
<span className="text-xs sm:text-sm text-muted-foreground">Rows per page:</span>
987+
<span className="text-xs sm:text-sm text-muted-foreground">{t('table.rowsPerPage')}:</span>
940988
<Select
941989
value={pageSize.toString()}
942990
onValueChange={(value) => {
@@ -959,7 +1007,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
9591007

9601008
<div className="flex items-center gap-2">
9611009
<span className="text-xs sm:text-sm text-muted-foreground">
962-
Page {currentPage} of {totalPages} <span className="hidden sm:inline">({sortedData.length} total)</span>
1010+
{t('table.pageInfo', { current: currentPage, total: totalPages })} <span className="hidden sm:inline">({t('table.totalRecords', { count: sortedData.length })})</span>
9631011
</span>
9641012
<div className="flex items-center gap-1">
9651013
<Button

packages/components/src/renderers/navigation/header-bar.tsx

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
import React from 'react';
1010
import { ComponentRegistry } from '@object-ui/core';
11-
import type { HeaderBarSchema } from '@object-ui/types';
12-
import { resolveI18nLabel } from '@object-ui/react';
11+
import type { HeaderBarSchema, BreadcrumbItem as BreadcrumbItemType } from '@object-ui/types';
12+
import { resolveI18nLabel, SchemaRenderer } from '@object-ui/react';
1313
import {
1414
SidebarTrigger,
1515
Separator,
@@ -18,8 +18,45 @@ import {
1818
BreadcrumbItem,
1919
BreadcrumbLink,
2020
BreadcrumbSeparator,
21-
BreadcrumbPage
21+
BreadcrumbPage,
22+
DropdownMenu,
23+
DropdownMenuTrigger,
24+
DropdownMenuContent,
25+
DropdownMenuItem,
26+
Input,
2227
} from '../../ui';
28+
import { ChevronDown, Search } from 'lucide-react';
29+
30+
function BreadcrumbLabel({ crumb, isLast }: { crumb: BreadcrumbItemType; isLast: boolean }) {
31+
const label = resolveI18nLabel(crumb.label) ?? '';
32+
33+
if (crumb.siblings && crumb.siblings.length > 0) {
34+
return (
35+
<DropdownMenu>
36+
<DropdownMenuTrigger className="flex items-center gap-1">
37+
{isLast ? (
38+
<span className="font-semibold">{label}</span>
39+
) : (
40+
<span>{label}</span>
41+
)}
42+
<ChevronDown className="h-3 w-3" />
43+
</DropdownMenuTrigger>
44+
<DropdownMenuContent align="start">
45+
{crumb.siblings.map((sibling, i) => (
46+
<DropdownMenuItem key={i} asChild>
47+
<a href={sibling.href}>{sibling.label}</a>
48+
</DropdownMenuItem>
49+
))}
50+
</DropdownMenuContent>
51+
</DropdownMenu>
52+
);
53+
}
54+
55+
if (isLast) {
56+
return <BreadcrumbPage>{label}</BreadcrumbPage>;
57+
}
58+
return <BreadcrumbLink href={crumb.href || '#'}>{label}</BreadcrumbLink>;
59+
}
2360

2461
ComponentRegistry.register('header-bar',
2562
({ schema }: { schema: HeaderBarSchema }) => (
@@ -28,27 +65,48 @@ ComponentRegistry.register('header-bar',
2865
<Separator orientation="vertical" className="mr-2 h-4" />
2966
<Breadcrumb>
3067
<BreadcrumbList>
31-
{schema.crumbs?.map((crumb: any, idx: number) => (
68+
{schema.crumbs?.map((crumb: BreadcrumbItemType, idx: number) => (
3269
<React.Fragment key={idx}>
3370
<BreadcrumbItem>
34-
{idx === schema.crumbs.length - 1 ? (
35-
<BreadcrumbPage>{resolveI18nLabel(crumb.label) ?? ''}</BreadcrumbPage>
36-
) : (
37-
<BreadcrumbLink href={crumb.href || '#'}>{resolveI18nLabel(crumb.label) ?? ''}</BreadcrumbLink>
38-
)}
71+
<BreadcrumbLabel crumb={crumb} isLast={idx === schema.crumbs!.length - 1} />
3972
</BreadcrumbItem>
40-
{idx < schema.crumbs.length - 1 && <BreadcrumbSeparator />}
73+
{idx < schema.crumbs!.length - 1 && <BreadcrumbSeparator />}
4174
</React.Fragment>
4275
))}
4376
</BreadcrumbList>
4477
</Breadcrumb>
78+
79+
<div className="ml-auto flex items-center gap-2">
80+
{schema.search?.enabled && (
81+
<div className="relative">
82+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
83+
<Input
84+
type="search"
85+
placeholder={schema.search.placeholder}
86+
className="pl-8 w-[200px] lg:w-[300px]"
87+
/>
88+
{schema.search.shortcut && (
89+
<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">
90+
{schema.search.shortcut}
91+
</kbd>
92+
)}
93+
</div>
94+
)}
95+
{schema.actions?.map((action, idx) => (
96+
<SchemaRenderer key={idx} schema={action} />
97+
))}
98+
{schema.rightContent && <SchemaRenderer schema={schema.rightContent} />}
99+
</div>
45100
</header>
46101
),
47102
{
48103
namespace: 'ui',
49104
label: 'Header Bar',
50105
inputs: [
51-
{ name: 'crumbs', type: 'array', label: 'Breadcrumbs' }
106+
{ name: 'crumbs', type: 'array', label: 'Breadcrumbs' },
107+
{ name: 'search', type: 'object', label: 'Search Configuration' },
108+
{ name: 'actions', type: 'array', label: 'Action Slots' },
109+
{ name: 'rightContent', type: 'object', label: 'Right Content' },
52110
],
53111
defaultProps: {
54112
crumbs: [

packages/i18n/src/locales/ar.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ const ar = {
7373
hideColumn: 'إخفاء العمود',
7474
freezeColumn: 'تجميد العمود',
7575
unfreezeColumn: 'إلغاء تجميد العمود',
76+
pageInfo: 'صفحة {{current}} من {{total}}',
77+
totalRecords: '{{count}} إجمالي',
7678
},
7779
grid: {
7880
actions: 'إجراءات',
@@ -104,6 +106,23 @@ const ar = {
104106
addRecord: 'إضافة سجل',
105107
tabs: 'علامات التبويب',
106108
allRecords: 'جميع السجلات',
109+
search: 'بحث',
110+
filter: 'تصفية',
111+
filterRecords: 'تصفية السجلات',
112+
sort: 'ترتيب',
113+
sortRecords: 'ترتيب السجلات',
114+
group: 'تجميع',
115+
groupBy: 'تجميع حسب',
116+
export: 'تصدير',
117+
exportAs: 'تصدير كـ {{format}}',
118+
color: 'لون',
119+
rowColor: 'لون الصف',
120+
colorByField: 'تلوين حسب الحقل',
121+
clear: 'مسح',
122+
none: 'لا شيء',
123+
hideFields: 'إخفاء الحقول',
124+
noItems: 'لم يتم العثور على عناصر',
125+
noItemsMessage: 'لا توجد سجلات للعرض. حاول تعديل الفلاتر أو إضافة بيانات جديدة.',
107126
},
108127
kanban: {
109128
addCard: 'إضافة بطاقة',

packages/i18n/src/locales/de.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const de = {
7272
hideColumn: 'Spalte ausblenden',
7373
freezeColumn: 'Spalte fixieren',
7474
unfreezeColumn: 'Spalte lösen',
75+
pageInfo: 'Seite {{current}} von {{total}}',
76+
totalRecords: '{{count}} gesamt',
7577
},
7678
grid: {
7779
actions: 'Aktionen',
@@ -103,6 +105,23 @@ const de = {
103105
addRecord: 'Datensatz hinzufügen',
104106
tabs: 'Tabs',
105107
allRecords: 'Alle Datensätze',
108+
search: 'Suche',
109+
filter: 'Filtern',
110+
filterRecords: 'Datensätze filtern',
111+
sort: 'Sortieren',
112+
sortRecords: 'Datensätze sortieren',
113+
group: 'Gruppieren',
114+
groupBy: 'Gruppieren nach',
115+
export: 'Exportieren',
116+
exportAs: 'Exportieren als {{format}}',
117+
color: 'Farbe',
118+
rowColor: 'Zeilenfarbe',
119+
colorByField: 'Nach Feld einfärben',
120+
clear: 'Löschen',
121+
none: 'Keine',
122+
hideFields: 'Felder ausblenden',
123+
noItems: 'Keine Einträge gefunden',
124+
noItemsMessage: 'Es gibt keine Datensätze. Versuchen Sie, die Filter anzupassen oder neue Daten hinzuzufügen.',
106125
},
107126
kanban: {
108127
addCard: 'Karte hinzufügen',

packages/i18n/src/locales/en.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const en = {
7272
hideColumn: 'Hide column',
7373
freezeColumn: 'Freeze column',
7474
unfreezeColumn: 'Unfreeze column',
75+
pageInfo: 'Page {{current}} of {{total}}',
76+
totalRecords: '{{count}} total',
7577
},
7678
grid: {
7779
actions: 'Actions',
@@ -103,6 +105,23 @@ const en = {
103105
addRecord: 'Add record',
104106
tabs: 'Tabs',
105107
allRecords: 'All Records',
108+
search: 'Search',
109+
filter: 'Filter',
110+
filterRecords: 'Filter Records',
111+
sort: 'Sort',
112+
sortRecords: 'Sort Records',
113+
group: 'Group',
114+
groupBy: 'Group By',
115+
export: 'Export',
116+
exportAs: 'Export as {{format}}',
117+
color: 'Color',
118+
rowColor: 'Row Color',
119+
colorByField: 'Color by field',
120+
clear: 'Clear',
121+
none: 'None',
122+
hideFields: 'Hide fields',
123+
noItems: 'No items found',
124+
noItemsMessage: 'There are no records to display. Try adjusting your filters or adding new data.',
106125
},
107126
kanban: {
108127
addCard: 'Add card',

packages/i18n/src/locales/es.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const es = {
7272
hideColumn: 'Ocultar columna',
7373
freezeColumn: 'Fijar columna',
7474
unfreezeColumn: 'Desfijar columna',
75+
pageInfo: 'Página {{current}} de {{total}}',
76+
totalRecords: '{{count}} en total',
7577
},
7678
grid: {
7779
actions: 'Acciones',
@@ -103,6 +105,23 @@ const es = {
103105
addRecord: 'Agregar registro',
104106
tabs: 'Pestañas',
105107
allRecords: 'Todos los registros',
108+
search: 'Buscar',
109+
filter: 'Filtrar',
110+
filterRecords: 'Filtrar registros',
111+
sort: 'Ordenar',
112+
sortRecords: 'Ordenar registros',
113+
group: 'Agrupar',
114+
groupBy: 'Agrupar por',
115+
export: 'Exportar',
116+
exportAs: 'Exportar como {{format}}',
117+
color: 'Color',
118+
rowColor: 'Color de fila',
119+
colorByField: 'Colorear por campo',
120+
clear: 'Borrar',
121+
none: 'Ninguno',
122+
hideFields: 'Ocultar campos',
123+
noItems: 'No se encontraron elementos',
124+
noItemsMessage: 'No hay registros para mostrar. Intente ajustar los filtros o agregar nuevos datos.',
106125
},
107126
kanban: {
108127
addCard: 'Añadir tarjeta',

packages/i18n/src/locales/fr.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const fr = {
7272
hideColumn: 'Masquer la colonne',
7373
freezeColumn: 'Figer la colonne',
7474
unfreezeColumn: 'Libérer la colonne',
75+
pageInfo: 'Page {{current}} sur {{total}}',
76+
totalRecords: '{{count}} au total',
7577
},
7678
grid: {
7779
actions: 'Actions',
@@ -103,6 +105,23 @@ const fr = {
103105
addRecord: 'Ajouter un enregistrement',
104106
tabs: 'Onglets',
105107
allRecords: 'Tous les enregistrements',
108+
search: 'Rechercher',
109+
filter: 'Filtrer',
110+
filterRecords: 'Filtrer les enregistrements',
111+
sort: 'Trier',
112+
sortRecords: 'Trier les enregistrements',
113+
group: 'Grouper',
114+
groupBy: 'Grouper par',
115+
export: 'Exporter',
116+
exportAs: 'Exporter en {{format}}',
117+
color: 'Couleur',
118+
rowColor: 'Couleur de ligne',
119+
colorByField: 'Colorer par champ',
120+
clear: 'Effacer',
121+
none: 'Aucun',
122+
hideFields: 'Masquer les champs',
123+
noItems: 'Aucun élément trouvé',
124+
noItemsMessage: "Il n'y a aucun enregistrement à afficher. Essayez d'ajuster les filtres ou d'ajouter de nouvelles données.",
106125
},
107126
kanban: {
108127
addCard: 'Ajouter une carte',

packages/i18n/src/locales/ja.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const ja = {
7272
hideColumn: '列を非表示',
7373
freezeColumn: '列を固定',
7474
unfreezeColumn: '列の固定を解除',
75+
pageInfo: '{{total}}ページ中{{current}}ページ',
76+
totalRecords: '合計{{count}}件',
7577
},
7678
grid: {
7779
actions: 'アクション',
@@ -103,6 +105,23 @@ const ja = {
103105
addRecord: 'レコードを追加',
104106
tabs: 'タブ',
105107
allRecords: 'すべてのレコード',
108+
search: '検索',
109+
filter: 'フィルター',
110+
filterRecords: 'レコードをフィルター',
111+
sort: '並べ替え',
112+
sortRecords: 'レコードを並べ替え',
113+
group: 'グループ',
114+
groupBy: 'グループ化',
115+
export: 'エクスポート',
116+
exportAs: '{{format}}としてエクスポート',
117+
color: '色',
118+
rowColor: '行の色',
119+
colorByField: 'フィールドで色分け',
120+
clear: 'クリア',
121+
none: 'なし',
122+
hideFields: 'フィールドを非表示',
123+
noItems: '項目が見つかりません',
124+
noItemsMessage: '表示するレコードがありません。フィルターを調整するか、新しいデータを追加してください。',
106125
},
107126
kanban: {
108127
addCard: 'カードを追加',

0 commit comments

Comments
 (0)