@object-ui/app-shell 4.0.6 → 4.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,94 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 4.0.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 7c9b85c: Fix compatibility with the framework's normalized Expression envelope format.
8
+
9
+ `@objectstack/spec` now emits predicate (`visible` / `enabled`) and template
10
+ (`titleFormat`) fields as `{ dialect, source }` envelopes instead of bare
11
+ strings. The previous implementation assumed strings and crashed the record
12
+ detail view (`TypeError: titleFormat.replace is not a function`) and printed
13
+ `Failed to evaluate expression: ${[object Object]}` for every action visibility
14
+ predicate.
15
+ - `@object-ui/core`: `ExpressionEvaluator.evaluate` / `evaluateCondition` now
16
+ unwrap Expression envelopes transparently.
17
+ - `@object-ui/react`: new `toPredicateInput()` helper to safely normalize
18
+ `boolean | string | Expression` predicate inputs into the `${expr}` form
19
+ expected by `useCondition`.
20
+ - `@object-ui/components`: `action-bar`, `action-button`, `action-group`,
21
+ `action-icon`, `action-menu` renderers use `toPredicateInput()` instead of
22
+ template-literal interpolation that produced `${[object Object]}`.
23
+ - `@object-ui/plugin-detail`, `@object-ui/plugin-kanban`,
24
+ `@object-ui/plugin-calendar`, `@object-ui/app-shell`,
25
+ `@object-ui/console`: title-format helpers accept both legacy strings and
26
+ the new `{ source }` envelope.
27
+
28
+ All changes are backward-compatible — legacy bare strings continue to work.
29
+
30
+ - fd15918: Comprehensive i18n refactor + CI test fix.
31
+
32
+ **i18n (`@object-ui/i18n`)**
33
+ - Added ~130 new keys under 12 new top-level namespaces: `layout`, `search`,
34
+ `empty`, `renderer`, `actionDialog`, `rowAction`, `navigationSync`,
35
+ `objectActions`, `objectViewActions`, `dashboardActions`, `recordDetail`,
36
+ `cellRender`, plus `grid.{empty,yes,no,systemFields,openMenu}`.
37
+ - Mirrored all new top-level namespaces to all 10 built-in locales
38
+ (en, zh, ja, ko, de, fr, es, pt, ru, ar) to maintain key parity required
39
+ by the locale-structure test. Non-en/zh locales seed with English values
40
+ and rely on `fallbackLng: 'en'` until human translation lands.
41
+
42
+ **App shell (`@object-ui/app-shell`)** — replaced hardcoded English in 14
43
+ files with `useObjectTranslation`:
44
+ - Layout: `AppSidebar`, `ActivityFeed` (locale-aware relative time),
45
+ `MetadataInspector`.
46
+ - Views: `SearchResultsPage`, `ActionParamDialog`, `RecordFormPage`,
47
+ `RecordDetailView`, `PageView`, `DashboardView` (PDF / forecast toasts),
48
+ `ReportView`, `ObjectView` (rename / delete view toasts).
49
+ - Console: `AppContent` (no-apps empty state).
50
+ - Components: `PageRenderer`, `FormRenderer`, `DashboardRenderer`.
51
+ - Hooks: `useNavigationSync` (16 toasts incl. Undo label),
52
+ `useObjectActions` (delete confirm + success / failure toasts).
53
+
54
+ **Plugin grid (`@object-ui/plugin-grid`)**
55
+ - `ObjectGrid` record-detail panel now translates Empty / Yes / No / System
56
+ via the existing `useGridTranslation` safe-fallback wrapper.
57
+ - `RowActionMenu` adopts a local safe-fallback i18n wrapper for
58
+ `Open menu` / `Edit` / `Delete`, preserving standalone-usage guarantees.
59
+
60
+ **CLI test fix (`@object-ui/cli`)**
61
+ - `cli-bin.test.ts` auto-builds the package on first run when `dist/cli.js`
62
+ is missing, instead of throwing. This unbreaks `pnpm test:coverage` in CI
63
+ (root vitest run does not honor turbo's `^build` deps) and removes the
64
+ manual `pnpm --filter @object-ui/cli build` requirement for local dev.
65
+
66
+ - Updated dependencies [7c9b85c]
67
+ - Updated dependencies [fd15918]
68
+ - @object-ui/core@4.0.7
69
+ - @object-ui/react@4.0.7
70
+ - @object-ui/components@4.0.7
71
+ - @object-ui/plugin-detail@4.0.7
72
+ - @object-ui/plugin-kanban@4.0.7
73
+ - @object-ui/plugin-calendar@4.0.7
74
+ - @object-ui/i18n@4.0.7
75
+ - @object-ui/plugin-grid@4.0.7
76
+ - @object-ui/data-objectstack@4.0.7
77
+ - @object-ui/fields@4.0.7
78
+ - @object-ui/layout@4.0.7
79
+ - @object-ui/plugin-charts@4.0.7
80
+ - @object-ui/plugin-chatbot@4.0.7
81
+ - @object-ui/plugin-dashboard@4.0.7
82
+ - @object-ui/plugin-designer@4.0.7
83
+ - @object-ui/plugin-form@4.0.7
84
+ - @object-ui/plugin-list@4.0.7
85
+ - @object-ui/plugin-report@4.0.7
86
+ - @object-ui/plugin-view@4.0.7
87
+ - @object-ui/types@4.0.7
88
+ - @object-ui/auth@4.0.7
89
+ - @object-ui/permissions@4.0.7
90
+ - @object-ui/collaboration@4.0.7
91
+
3
92
  ## 4.0.6
4
93
 
5
94
  ### Patch Changes
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { SchemaRendererProvider } from '@object-ui/react';
3
+ import { useObjectTranslation } from '@object-ui/i18n';
3
4
  /**
4
5
  * DashboardRenderer - Renders dashboard layouts from schema
5
6
  *
@@ -7,8 +8,9 @@ import { SchemaRendererProvider } from '@object-ui/react';
7
8
  * Delegates to registered dashboard plugins.
8
9
  */
9
10
  export function DashboardRenderer({ schema, dataSource, dashboardName, }) {
11
+ const { t } = useObjectTranslation();
10
12
  if (!schema) {
11
- return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: "No dashboard schema provided" }) }));
13
+ return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: t('renderer.noDashboardSchema') }) }));
12
14
  }
13
- return (_jsx(SchemaRendererProvider, { dataSource: dataSource, children: _jsxs("div", { className: "dashboard-renderer h-full p-4", children: [_jsx("h1", { className: "mb-4 text-2xl font-bold", children: schema.title || dashboardName || 'Dashboard' }), _jsxs("div", { className: "text-muted-foreground", children: ["Dashboard rendering: ", schema.title || dashboardName] })] }) }));
15
+ return (_jsx(SchemaRendererProvider, { dataSource: dataSource, children: _jsxs("div", { className: "dashboard-renderer h-full p-4", children: [_jsx("h1", { className: "mb-4 text-2xl font-bold", children: schema.title || dashboardName || t('renderer.dashboard') }), _jsx("div", { className: "text-muted-foreground", children: t('renderer.dashboardRendering', { name: schema.title || dashboardName }) })] }) }));
14
16
  }
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { SchemaRendererProvider } from '@object-ui/react';
3
+ import { useObjectTranslation } from '@object-ui/i18n';
3
4
  /**
4
5
  * FormRenderer - Renders forms (modal or inline)
5
6
  *
@@ -7,8 +8,9 @@ import { SchemaRendererProvider } from '@object-ui/react';
7
8
  * Handles both create and edit modes.
8
9
  */
9
10
  export function FormRenderer({ schema, dataSource, mode = 'create', recordId, onSuccess, onCancel, objectDef, }) {
11
+ const { t } = useObjectTranslation();
10
12
  if (!schema) {
11
- return (_jsx("div", { className: "flex items-center justify-center p-4", children: _jsx("div", { className: "text-muted-foreground", children: "No form schema provided" }) }));
13
+ return (_jsx("div", { className: "flex items-center justify-center p-4", children: _jsx("div", { className: "text-muted-foreground", children: t('renderer.noFormSchema') }) }));
12
14
  }
13
15
  const handleSubmit = async (data) => {
14
16
  try {
@@ -25,5 +27,5 @@ export function FormRenderer({ schema, dataSource, mode = 'create', recordId, on
25
27
  console.error('Form submission error:', error);
26
28
  }
27
29
  };
28
- return (_jsx(SchemaRendererProvider, { dataSource: dataSource, children: _jsxs("div", { className: "form-renderer p-4", children: [_jsx("h2", { className: "mb-4 text-xl font-semibold", children: schema.title || (mode === 'create' ? 'Create Record' : 'Edit Record') }), _jsxs("div", { className: "text-muted-foreground", children: ["Form rendering in ", mode, " mode", recordId && ` for record ${recordId}`] }), _jsxs("div", { className: "mt-4 flex gap-2", children: [_jsx("button", { onClick: () => handleSubmit({}), className: "rounded bg-primary px-4 py-2 text-primary-foreground", children: "Save" }), onCancel && (_jsx("button", { onClick: onCancel, className: "rounded border px-4 py-2", children: "Cancel" }))] })] }) }));
30
+ return (_jsx(SchemaRendererProvider, { dataSource: dataSource, children: _jsxs("div", { className: "form-renderer p-4", children: [_jsx("h2", { className: "mb-4 text-xl font-semibold", children: schema.title || (mode === 'create' ? t('renderer.createRecord') : t('renderer.editRecord')) }), _jsxs("div", { className: "text-muted-foreground", children: [t('renderer.formRenderingMode', { mode }), recordId && ` ${t('renderer.formRenderingFor', { id: recordId })}`] }), _jsxs("div", { className: "mt-4 flex gap-2", children: [_jsx("button", { onClick: () => handleSubmit({}), className: "rounded bg-primary px-4 py-2 text-primary-foreground", children: t('renderer.save') }), onCancel && (_jsx("button", { onClick: onCancel, className: "rounded border px-4 py-2", children: t('renderer.cancel') }))] })] }) }));
29
31
  }
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useObjectTranslation } from '@object-ui/i18n';
2
3
  /**
3
4
  * PageRenderer - Renders custom page schemas
4
5
  *
5
6
  * Framework-agnostic component that renders a page based on JSON schema.
6
7
  */
7
8
  export function PageRenderer({ schema, pageName }) {
9
+ const { t } = useObjectTranslation();
8
10
  if (!schema) {
9
- return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: "No page schema provided" }) }));
11
+ return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: t('renderer.noPageSchema') }) }));
10
12
  }
11
- return (_jsxs("div", { className: "page-renderer h-full p-4", children: [_jsx("h1", { className: "mb-4 text-2xl font-bold", children: schema.title || pageName || 'Page' }), _jsxs("div", { className: "text-muted-foreground", children: ["Page rendering: ", schema.title || pageName] })] }));
13
+ return (_jsxs("div", { className: "page-renderer h-full p-4", children: [_jsx("h1", { className: "mb-4 text-2xl font-bold", children: schema.title || pageName || t('renderer.page') }), _jsx("div", { className: "text-muted-foreground", children: t('renderer.pageRendering', { name: schema.title || pageName }) })] }));
12
14
  }
@@ -257,7 +257,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
257
257
  const isCreateAppRoute = location.pathname.endsWith('/create-app');
258
258
  const isSystemRoute = location.pathname.includes('/system');
259
259
  if (!activeApp && !isCreateAppRoute && !isSystemRoute)
260
- return (_jsx("div", { className: "h-screen flex items-center justify-center", children: _jsxs(Empty, { children: [_jsx(EmptyTitle, { children: "No Apps Configured" }), _jsx(EmptyDescription, { children: "No applications have been registered. Create your first app or visit System Settings to configure your environment." }), _jsxs("div", { className: "mt-4 flex flex-col sm:flex-row items-center gap-3", children: [_jsx(Button, { onClick: () => navigate('/create-app'), "data-testid": "create-first-app-btn", children: "Create Your First App" }), _jsx(Button, { variant: "outline", onClick: () => navigate('/apps/setup'), "data-testid": "go-to-settings-btn", children: "System Settings" })] })] }) }));
260
+ return (_jsx("div", { className: "h-screen flex items-center justify-center", children: _jsxs(Empty, { children: [_jsx(EmptyTitle, { children: t('empty.noAppsConfigured') }), _jsx(EmptyDescription, { children: t('empty.noAppsConfiguredDescription') }), _jsxs("div", { className: "mt-4 flex flex-col sm:flex-row items-center gap-3", children: [_jsx(Button, { onClick: () => navigate('/create-app'), "data-testid": "create-first-app-btn", children: t('empty.createFirstApp') }), _jsx(Button, { variant: "outline", onClick: () => navigate('/apps/setup'), "data-testid": "go-to-settings-btn", children: t('empty.systemSettings') })] })] }) }));
261
261
  if (!activeApp && (isCreateAppRoute || isSystemRoute)) {
262
262
  return (_jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsxs(Routes, { children: [_jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), extraRoutesNoApp] }) }));
263
263
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { useCallback, useEffect, useRef } from 'react';
12
12
  import { toast } from 'sonner';
13
+ import { useObjectTranslation } from '@object-ui/i18n';
13
14
  import { useAdapter } from '../providers/AdapterProvider';
14
15
  import { useMetadata } from '../providers/MetadataProvider';
15
16
  // ============================================================================
@@ -99,6 +100,7 @@ export function navigationEqual(a, b) {
99
100
  export function useNavigationSync() {
100
101
  const adapter = useAdapter();
101
102
  const { apps, refresh } = useMetadata();
103
+ const { t } = useObjectTranslation();
102
104
  // Keep a ref so the undo closure always reads the latest adapter
103
105
  const adapterRef = useRef(adapter);
104
106
  adapterRef.current = adapter;
@@ -133,25 +135,25 @@ export function useNavigationSync() {
133
135
  const updatedApp = { ...app, navigation: updated };
134
136
  try {
135
137
  await saveApp(appName, updatedApp);
136
- toast.success(`Navigation updated: added page "${label || pageName}"`, {
138
+ toast.success(t('navigationSync.addedPage', { name: label || pageName }), {
137
139
  action: {
138
- label: 'Undo',
140
+ label: t('navigationSync.undoLabel'),
139
141
  onClick: async () => {
140
142
  try {
141
143
  await saveApp(appName, { ...app, navigation: prev });
142
- toast.info('Navigation change undone');
144
+ toast.info(t('navigationSync.undone'));
143
145
  }
144
146
  catch {
145
- toast.error('Failed to undo navigation change');
147
+ toast.error(t('navigationSync.undoFailed'));
146
148
  }
147
149
  },
148
150
  },
149
151
  });
150
152
  }
151
153
  catch {
152
- toast.error('Failed to update navigation');
154
+ toast.error(t('navigationSync.updateFailed'));
153
155
  }
154
- }, [findApp, saveApp]);
156
+ }, [findApp, saveApp, t]);
155
157
  const syncDashboardCreated = useCallback(async (appName, dashboardName, label) => {
156
158
  const app = findApp(appName);
157
159
  if (!app)
@@ -168,25 +170,25 @@ export function useNavigationSync() {
168
170
  const updatedApp = { ...app, navigation: updated };
169
171
  try {
170
172
  await saveApp(appName, updatedApp);
171
- toast.success(`Navigation updated: added dashboard "${label || dashboardName}"`, {
173
+ toast.success(t('navigationSync.addedDashboard', { name: label || dashboardName }), {
172
174
  action: {
173
- label: 'Undo',
175
+ label: t('navigationSync.undoLabel'),
174
176
  onClick: async () => {
175
177
  try {
176
178
  await saveApp(appName, { ...app, navigation: prev });
177
- toast.info('Navigation change undone');
179
+ toast.info(t('navigationSync.undone'));
178
180
  }
179
181
  catch {
180
- toast.error('Failed to undo navigation change');
182
+ toast.error(t('navigationSync.undoFailed'));
181
183
  }
182
184
  },
183
185
  },
184
186
  });
185
187
  }
186
188
  catch {
187
- toast.error('Failed to update navigation');
189
+ toast.error(t('navigationSync.updateFailed'));
188
190
  }
189
- }, [findApp, saveApp]);
191
+ }, [findApp, saveApp, t]);
190
192
  // ------------------------------------------------------------------
191
193
  // Deleted
192
194
  // ------------------------------------------------------------------
@@ -201,25 +203,25 @@ export function useNavigationSync() {
201
203
  const updatedApp = { ...app, navigation: updated };
202
204
  try {
203
205
  await saveApp(appName, updatedApp);
204
- toast.success(`Navigation updated: removed page "${pageName}"`, {
206
+ toast.success(t('navigationSync.removedPage', { name: pageName }), {
205
207
  action: {
206
- label: 'Undo',
208
+ label: t('navigationSync.undoLabel'),
207
209
  onClick: async () => {
208
210
  try {
209
211
  await saveApp(appName, { ...app, navigation: prev });
210
- toast.info('Navigation change undone');
212
+ toast.info(t('navigationSync.undone'));
211
213
  }
212
214
  catch {
213
- toast.error('Failed to undo navigation change');
215
+ toast.error(t('navigationSync.undoFailed'));
214
216
  }
215
217
  },
216
218
  },
217
219
  });
218
220
  }
219
221
  catch {
220
- toast.error('Failed to update navigation');
222
+ toast.error(t('navigationSync.updateFailed'));
221
223
  }
222
- }, [findApp, saveApp]);
224
+ }, [findApp, saveApp, t]);
223
225
  const syncDashboardDeleted = useCallback(async (appName, dashboardName) => {
224
226
  const app = findApp(appName);
225
227
  if (!app)
@@ -231,25 +233,25 @@ export function useNavigationSync() {
231
233
  const updatedApp = { ...app, navigation: updated };
232
234
  try {
233
235
  await saveApp(appName, updatedApp);
234
- toast.success(`Navigation updated: removed dashboard "${dashboardName}"`, {
236
+ toast.success(t('navigationSync.removedDashboard', { name: dashboardName }), {
235
237
  action: {
236
- label: 'Undo',
238
+ label: t('navigationSync.undoLabel'),
237
239
  onClick: async () => {
238
240
  try {
239
241
  await saveApp(appName, { ...app, navigation: prev });
240
- toast.info('Navigation change undone');
242
+ toast.info(t('navigationSync.undone'));
241
243
  }
242
244
  catch {
243
- toast.error('Failed to undo navigation change');
245
+ toast.error(t('navigationSync.undoFailed'));
244
246
  }
245
247
  },
246
248
  },
247
249
  });
248
250
  }
249
251
  catch {
250
- toast.error('Failed to update navigation');
252
+ toast.error(t('navigationSync.updateFailed'));
251
253
  }
252
- }, [findApp, saveApp]);
254
+ }, [findApp, saveApp, t]);
253
255
  // ------------------------------------------------------------------
254
256
  // Renamed
255
257
  // ------------------------------------------------------------------
@@ -264,25 +266,25 @@ export function useNavigationSync() {
264
266
  const updatedApp = { ...app, navigation: updated };
265
267
  try {
266
268
  await saveApp(appName, updatedApp);
267
- toast.success(`Navigation updated: renamed page "${oldName}" "${newName}"`, {
269
+ toast.success(t('navigationSync.renamedPage', { oldName, newName }), {
268
270
  action: {
269
- label: 'Undo',
271
+ label: t('navigationSync.undoLabel'),
270
272
  onClick: async () => {
271
273
  try {
272
274
  await saveApp(appName, { ...app, navigation: prev });
273
- toast.info('Navigation change undone');
275
+ toast.info(t('navigationSync.undone'));
274
276
  }
275
277
  catch {
276
- toast.error('Failed to undo navigation change');
278
+ toast.error(t('navigationSync.undoFailed'));
277
279
  }
278
280
  },
279
281
  },
280
282
  });
281
283
  }
282
284
  catch {
283
- toast.error('Failed to update navigation');
285
+ toast.error(t('navigationSync.updateFailed'));
284
286
  }
285
- }, [findApp, saveApp]);
287
+ }, [findApp, saveApp, t]);
286
288
  const syncDashboardRenamed = useCallback(async (appName, oldName, newName) => {
287
289
  const app = findApp(appName);
288
290
  if (!app)
@@ -294,25 +296,25 @@ export function useNavigationSync() {
294
296
  const updatedApp = { ...app, navigation: updated };
295
297
  try {
296
298
  await saveApp(appName, updatedApp);
297
- toast.success(`Navigation updated: renamed dashboard "${oldName}" "${newName}"`, {
299
+ toast.success(t('navigationSync.renamedDashboard', { oldName, newName }), {
298
300
  action: {
299
- label: 'Undo',
301
+ label: t('navigationSync.undoLabel'),
300
302
  onClick: async () => {
301
303
  try {
302
304
  await saveApp(appName, { ...app, navigation: prev });
303
- toast.info('Navigation change undone');
305
+ toast.info(t('navigationSync.undone'));
304
306
  }
305
307
  catch {
306
- toast.error('Failed to undo navigation change');
308
+ toast.error(t('navigationSync.undoFailed'));
307
309
  }
308
310
  },
309
311
  },
310
312
  });
311
313
  }
312
314
  catch {
313
- toast.error('Failed to update navigation');
315
+ toast.error(t('navigationSync.updateFailed'));
314
316
  }
315
- }, [findApp, saveApp]);
317
+ }, [findApp, saveApp, t]);
316
318
  // ------------------------------------------------------------------
317
319
  // All-Apps convenience methods
318
320
  // ------------------------------------------------------------------
@@ -17,6 +17,16 @@ interface ObjectActionConfig {
17
17
  dataSource: any;
18
18
  onEdit?: (record: any) => void;
19
19
  onRefresh?: () => void;
20
+ /** Optional shadcn-style confirm handler — falls back to window.confirm */
21
+ onConfirm?: (message: string, options?: {
22
+ title?: string;
23
+ confirmText?: string;
24
+ cancelText?: string;
25
+ }) => Promise<boolean>;
26
+ /** Optional toast handler — falls back to sonner */
27
+ onToast?: (message: string, options?: {
28
+ type?: string;
29
+ }) => void;
20
30
  }
21
31
  interface ObjectActions {
22
32
  /** Run an action by schema or type string */
@@ -34,5 +44,5 @@ interface ObjectActions {
34
44
  /** Last error message */
35
45
  error: string | null;
36
46
  }
37
- export declare function useObjectActions({ objectName, objectLabel, dataSource, onEdit, onRefresh, }: ObjectActionConfig): ObjectActions;
47
+ export declare function useObjectActions({ objectName, objectLabel, dataSource, onEdit, onRefresh, onConfirm, onToast, }: ObjectActionConfig): ObjectActions;
38
48
  export {};
@@ -13,10 +13,12 @@
13
13
  import { useCallback, useEffect } from 'react';
14
14
  import { useNavigate, useParams } from 'react-router-dom';
15
15
  import { useActionRunner } from '@object-ui/react';
16
+ import { useObjectTranslation } from '@object-ui/i18n';
16
17
  import { toast } from 'sonner';
17
- export function useObjectActions({ objectName, objectLabel, dataSource, onEdit, onRefresh, }) {
18
+ export function useObjectActions({ objectName, objectLabel, dataSource, onEdit, onRefresh, onConfirm, onToast, }) {
18
19
  const navigate = useNavigate();
19
20
  const { appName } = useParams();
21
+ const { t } = useObjectTranslation();
20
22
  const baseUrl = `/apps/${appName}`;
21
23
  const { execute, loading, error, runner } = useActionRunner({
22
24
  context: {
@@ -24,6 +26,8 @@ export function useObjectActions({ objectName, objectLabel, dataSource, onEdit,
24
26
  objectLabel: objectLabel || objectName,
25
27
  baseUrl,
26
28
  },
29
+ onConfirm,
30
+ onToast,
27
31
  });
28
32
  // Register custom handlers
29
33
  useEffect(() => {
@@ -34,17 +38,25 @@ export function useObjectActions({ objectName, objectLabel, dataSource, onEdit,
34
38
  });
35
39
  // Handler: delete
36
40
  runner.registerHandler('delete', async (action) => {
37
- const recordId = action.params?.recordId || action.recordId;
41
+ // Accept several param shapes used across call sites:
42
+ // { params: { recordId } } — toolbar / programmatic deletes
43
+ // { params: { record } } — ObjectGrid row dropdown
44
+ // { params: { records: [...] } } — bulk delete
45
+ // { recordId } (legacy) — pre-params shape
46
+ const recordId = action.params?.recordId ??
47
+ action.params?.record?.id ??
48
+ action.params?.records?.[0]?.id ??
49
+ action.recordId;
38
50
  if (!recordId)
39
- return { success: false, error: 'No record ID provided' };
51
+ return { success: false, error: t('objectActions.noRecordId') };
40
52
  try {
41
53
  await dataSource.delete(objectName, recordId);
42
54
  onRefresh?.();
43
- toast.success(`${objectLabel || objectName} deleted successfully`);
55
+ toast.success(t('objectActions.deleteSuccess', { label: objectLabel || objectName }));
44
56
  return { success: true, reload: true };
45
57
  }
46
58
  catch (err) {
47
- toast.error(`Failed to delete ${objectLabel || objectName}`, {
59
+ toast.error(t('objectActions.deleteFailed', { label: objectLabel || objectName }), {
48
60
  description: err.message,
49
61
  });
50
62
  return { success: false, error: err.message };
@@ -63,17 +75,17 @@ export function useObjectActions({ objectName, objectLabel, dataSource, onEdit,
63
75
  onRefresh?.();
64
76
  return { success: true, reload: true };
65
77
  });
66
- }, [runner, objectName, dataSource, onEdit, onRefresh, navigate, baseUrl]);
78
+ }, [runner, objectName, dataSource, onEdit, onRefresh, navigate, baseUrl, t, objectLabel]);
67
79
  const create = useCallback(() => {
68
80
  onEdit?.(null);
69
81
  }, [onEdit]);
70
82
  const deleteRecord = useCallback(async (recordId) => {
71
83
  return execute({
72
84
  type: 'delete',
73
- confirmText: `Are you sure you want to delete this record?`,
85
+ confirmText: t('objectActions.deleteConfirm'),
74
86
  params: { recordId },
75
87
  });
76
- }, [execute]);
88
+ }, [execute, t]);
77
89
  const navigateToView = useCallback((viewId) => {
78
90
  navigate(`${baseUrl}/${objectName}/view/${viewId}`);
79
91
  }, [navigate, baseUrl, objectName]);
@@ -10,30 +10,34 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  import { useState } from 'react';
11
11
  import { Button, Badge, Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, } from '@object-ui/components';
12
12
  import { Bell, Plus, Pencil, Trash2, MessageSquare, Filter } from 'lucide-react';
13
+ import { useObjectTranslation } from '@object-ui/i18n';
13
14
  const typeConfig = {
14
15
  create: { icon: Plus, color: 'text-green-500' },
15
16
  update: { icon: Pencil, color: 'text-blue-500' },
16
17
  delete: { icon: Trash2, color: 'text-red-500' },
17
18
  comment: { icon: MessageSquare, color: 'text-amber-500' },
18
19
  };
19
- /** Format an ISO timestamp as a relative string (e.g. "2m ago"). */
20
- function formatRelativeTime(iso) {
20
+ /** Format an ISO timestamp as a localized relative string (e.g. "2m ago"). */
21
+ function formatRelativeTime(iso, t) {
21
22
  const ms = new Date(iso).getTime();
22
23
  if (Number.isNaN(ms))
23
24
  return '';
24
25
  const seconds = Math.floor((Date.now() - ms) / 1000);
26
+ if (seconds < 5)
27
+ return t('layout.activityFeed.relativeJustNow');
25
28
  if (seconds < 60)
26
- return `${Math.max(seconds, 0)}s ago`;
29
+ return t('layout.activityFeed.relativeSecondsAgo', { count: Math.max(seconds, 0) });
27
30
  const minutes = Math.floor(seconds / 60);
28
31
  if (minutes < 60)
29
- return `${minutes}m ago`;
32
+ return t('layout.activityFeed.relativeMinutesAgo', { count: minutes });
30
33
  const hours = Math.floor(minutes / 60);
31
34
  if (hours < 24)
32
- return `${hours}h ago`;
35
+ return t('layout.activityFeed.relativeHoursAgo', { count: hours });
33
36
  const days = Math.floor(hours / 24);
34
- return `${days}d ago`;
37
+ return t('layout.activityFeed.relativeDaysAgo', { count: days });
35
38
  }
36
39
  export function ActivityFeed({ activities = [], className }) {
40
+ const { t } = useObjectTranslation();
37
41
  const [open, setOpen] = useState(false);
38
42
  const [showFilters, setShowFilters] = useState(false);
39
43
  const [notificationPreferences, setNotificationPreferences] = useState({
@@ -46,12 +50,19 @@ export function ActivityFeed({ activities = [], className }) {
46
50
  setNotificationPreferences(prev => ({ ...prev, [type]: !prev[type] }));
47
51
  };
48
52
  const filteredActivities = activities.filter(a => notificationPreferences[a.type]);
49
- return (_jsxs(Sheet, { open: open, onOpenChange: setOpen, children: [_jsx(SheetTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "icon", className: className ?? 'h-8 w-8', "aria-label": "Activity feed", children: [_jsx(Bell, { className: "h-4 w-4" }), activities.length > 0 && (_jsx("span", { className: "absolute -top-0.5 -right-0.5 h-3.5 w-3.5 rounded-full bg-primary text-[9px] font-bold text-primary-foreground flex items-center justify-center", children: activities.length > 9 ? '9+' : activities.length }))] }) }), _jsxs(SheetContent, { side: "right", className: "w-80 sm:w-96", children: [_jsx(SheetHeader, { children: _jsxs(SheetTitle, { className: "flex items-center justify-between", children: ["Recent Activity", _jsxs(Button, { variant: showFilters ? 'secondary' : 'ghost', size: "sm", className: "h-7 px-2", onClick: () => setShowFilters(!showFilters), children: [_jsx(Filter, { className: "h-3.5 w-3.5 mr-1" }), "Filter"] })] }) }), showFilters && (_jsx("div", { className: "flex flex-wrap gap-1.5 mt-3 px-1", children: Object.keys(typeConfig).map(type => {
53
+ /** Localized labels for activity type badges. */
54
+ const typeLabels = {
55
+ create: t('layout.activityFeed.typeCreate'),
56
+ update: t('layout.activityFeed.typeUpdate'),
57
+ delete: t('layout.activityFeed.typeDelete'),
58
+ comment: t('layout.activityFeed.typeComment'),
59
+ };
60
+ return (_jsxs(Sheet, { open: open, onOpenChange: setOpen, children: [_jsx(SheetTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "icon", className: className ?? 'h-8 w-8', "aria-label": t('layout.activityFeed.ariaLabel'), children: [_jsx(Bell, { className: "h-4 w-4" }), activities.length > 0 && (_jsx("span", { className: "absolute -top-0.5 -right-0.5 h-3.5 w-3.5 rounded-full bg-primary text-[9px] font-bold text-primary-foreground flex items-center justify-center", children: activities.length > 9 ? '9+' : activities.length }))] }) }), _jsxs(SheetContent, { side: "right", className: "w-80 sm:w-96", children: [_jsx(SheetHeader, { children: _jsxs(SheetTitle, { className: "flex items-center justify-between", children: [t('layout.activityFeed.title'), _jsxs(Button, { variant: showFilters ? 'secondary' : 'ghost', size: "sm", className: "h-7 px-2", onClick: () => setShowFilters(!showFilters), children: [_jsx(Filter, { className: "h-3.5 w-3.5 mr-1" }), t('layout.activityFeed.filter')] })] }) }), showFilters && (_jsx("div", { className: "flex flex-wrap gap-1.5 mt-3 px-1", children: Object.keys(typeConfig).map(type => {
50
61
  const { icon: Icon, color } = typeConfig[type];
51
62
  const active = notificationPreferences[type];
52
- return (_jsxs(Badge, { variant: active ? 'default' : 'outline', className: "cursor-pointer select-none gap-1 capitalize", onClick: () => togglePreference(type), children: [_jsx(Icon, { className: `h-3 w-3 ${active ? '' : color}` }), type] }, type));
53
- }) })), filteredActivities.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-16 text-muted-foreground", children: [_jsx(Bell, { className: "h-8 w-8 opacity-40" }), _jsx("p", { className: "text-sm", children: "No recent activity" })] })) : (_jsx("ul", { className: "mt-4 space-y-1 overflow-y-auto max-h-[calc(100vh-8rem)]", children: filteredActivities.map((item) => {
63
+ return (_jsxs(Badge, { variant: active ? 'default' : 'outline', className: "cursor-pointer select-none gap-1", onClick: () => togglePreference(type), children: [_jsx(Icon, { className: `h-3 w-3 ${active ? '' : color}` }), typeLabels[type]] }, type));
64
+ }) })), filteredActivities.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-16 text-muted-foreground", children: [_jsx(Bell, { className: "h-8 w-8 opacity-40" }), _jsx("p", { className: "text-sm", children: t('layout.activityFeed.empty') })] })) : (_jsx("ul", { className: "mt-4 space-y-1 overflow-y-auto max-h-[calc(100vh-8rem)]", children: filteredActivities.map((item) => {
54
65
  const { icon: Icon, color } = typeConfig[item.type];
55
- return (_jsxs("li", { className: "flex items-start gap-3 rounded-md px-2 py-2 hover:bg-muted/50 transition-colors", children: [_jsx("span", { className: `mt-0.5 shrink-0 ${color}`, children: _jsx(Icon, { className: "h-4 w-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-sm leading-snug", children: item.description }), _jsxs("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [item.user, " \u00B7 ", formatRelativeTime(item.timestamp)] })] })] }, item.id));
66
+ return (_jsxs("li", { className: "flex items-start gap-3 rounded-md px-2 py-2 hover:bg-muted/50 transition-colors", children: [_jsx("span", { className: `mt-0.5 shrink-0 ${color}`, children: _jsx(Icon, { className: "h-4 w-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-sm leading-snug", children: item.description }), _jsxs("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [item.user, " \u00B7 ", formatRelativeTime(item.timestamp, t)] })] })] }, item.id));
56
67
  }) }))] })] }));
57
68
  }
@@ -177,7 +177,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
177
177
  { id: 'sys-roles', label: 'Roles', type: 'url', url: '/apps/setup/system/roles', icon: 'shield' },
178
178
  { id: 'sys-create-app', label: 'Create App', type: 'url', url: '/create-app', icon: 'plus' },
179
179
  ], []);
180
- return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: activeApp ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", style: primaryColor ? { backgroundColor: primaryColor } : undefined, children: logo ? (_jsx("img", { src: logo, alt: resolveI18nLabel(activeApp.label, t), className: "size-6 object-contain" })) : (React.createElement(getIcon(activeApp.icon), { className: "size-4" })) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: resolveI18nLabel(activeApp.label, t) }), _jsx("span", { className: "truncate text-xs", children: resolveI18nLabel(activeApp.description, t) || `${activeApps.length} Apps Available` })] }), _jsx(ChevronsUpDown, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "Switch Application" }), activeApps.map((app) => (_jsxs(DropdownMenuItem, { onClick: () => onAppChange(app.name), className: "gap-2 p-2", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-sm border", children: app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : _jsx(Database, { className: "size-3" }) }), resolveI18nLabel(app.label, t), activeApp.name === app.name && _jsx("span", { className: "ml-auto text-xs", children: "\u2713" })] }, app.name))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/home'), "data-testid": "home-link-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Home, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Home" })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/create-app`), "data-testid": "add-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Plus, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Add App" })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/edit-app/${activeAppName}`), "data-testid": "edit-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Pencil, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Edit App" })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/apps/setup/system/apps'), "data-testid": "manage-all-apps-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Settings, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Manage All Apps" })] })] })] })) : (_jsxs(SidebarMenuButton, { size: "lg", onClick: () => navigate('/apps/setup'), "data-testid": "system-sidebar-header", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: _jsx(Settings, { className: "size-4" }) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: "System Console" }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: "No apps configured" })] })] })) }) }) }), _jsx(SidebarContent, { children: activeApp ? (_jsxs(_Fragment, { children: [areas.length > 1 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Area"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: areas.map((area) => {
180
+ return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: activeApp ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", style: primaryColor ? { backgroundColor: primaryColor } : undefined, children: logo ? (_jsx("img", { src: logo, alt: resolveI18nLabel(activeApp.label, t), className: "size-6 object-contain" })) : (React.createElement(getIcon(activeApp.icon), { className: "size-4" })) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: resolveI18nLabel(activeApp.label, t) }), _jsx("span", { className: "truncate text-xs", children: resolveI18nLabel(activeApp.description, t) || `${activeApps.length} Apps Available` })] }), _jsx(ChevronsUpDown, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "Switch Application" }), activeApps.map((app) => (_jsxs(DropdownMenuItem, { onClick: () => onAppChange(app.name), className: "gap-2 p-2", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-sm border", children: app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : _jsx(Database, { className: "size-3" }) }), resolveI18nLabel(app.label, t), activeApp.name === app.name && _jsx("span", { className: "ml-auto text-xs", children: "\u2713" })] }, app.name))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/home'), "data-testid": "home-link-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Home, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.home') })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/create-app`), "data-testid": "add-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Plus, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.addApp') })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/edit-app/${activeAppName}`), "data-testid": "edit-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Pencil, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.editApp') })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/apps/setup/system/apps'), "data-testid": "manage-all-apps-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Settings, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.manageAllApps') })] })] })] })) : (_jsxs(SidebarMenuButton, { size: "lg", onClick: () => navigate('/apps/setup'), "data-testid": "system-sidebar-header", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: _jsx(Settings, { className: "size-4" }) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: t('layout.appSwitcher.systemConsole') }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: t('layout.appSwitcher.noAppsConfigured') })] })] })) }) }) }), _jsx(SidebarContent, { children: activeApp ? (_jsxs(_Fragment, { children: [areas.length > 1 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Area"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: areas.map((area) => {
181
181
  const AreaIcon = getIcon(area.icon);
182
182
  const isActiveArea = area.id === activeAreaId;
183
183
  return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { isActive: isActiveArea, tooltip: area.label, onClick: () => setActiveAreaId(area.id), children: [_jsx(AreaIcon, { className: "h-4 w-4" }), _jsx("span", { children: area.label })] }) }, area.id));
@@ -22,13 +22,20 @@ export declare function capitalizeFirst(str: string): string;
22
22
  /**
23
23
  * Format a record title using the titleFormat pattern.
24
24
  *
25
+ * Accepts either a legacy string template or an Expression envelope
26
+ * (`{ dialect: 'template', source: string }`) emitted by `@objectstack/spec`'s
27
+ * normalized templates. The placeholder syntax (`{field}`) is identical in both
28
+ * shapes; only the wrapping object is new.
29
+ *
25
30
  * Empty placeholders (missing or null/empty fields) are stripped along with
26
31
  * any orphan separator they leave behind, so a template like
27
32
  * "{full_name} - {company}"
28
33
  * evaluated against `{ company: "Acme" }` resolves to `"Acme"` rather than
29
34
  * `" - Acme"`. Returns an empty string when no placeholder resolved.
30
35
  */
31
- export declare function formatRecordTitle(titleFormat: string | undefined, record: any): string;
36
+ export declare function formatRecordTitle(titleFormat: string | {
37
+ source?: string;
38
+ } | undefined, record: any): string;
32
39
  /**
33
40
  * Get display name for a record using titleFormat or fallback
34
41
  * @param objectDef Object definition with optional titleFormat
@@ -38,6 +38,11 @@ const SEPARATOR_CLASS = '[-\\u2013\\u2014|/·,:]';
38
38
  /**
39
39
  * Format a record title using the titleFormat pattern.
40
40
  *
41
+ * Accepts either a legacy string template or an Expression envelope
42
+ * (`{ dialect: 'template', source: string }`) emitted by `@objectstack/spec`'s
43
+ * normalized templates. The placeholder syntax (`{field}`) is identical in both
44
+ * shapes; only the wrapping object is new.
45
+ *
41
46
  * Empty placeholders (missing or null/empty fields) are stripped along with
42
47
  * any orphan separator they leave behind, so a template like
43
48
  * "{full_name} - {company}"
@@ -45,11 +50,17 @@ const SEPARATOR_CLASS = '[-\\u2013\\u2014|/·,:]';
45
50
  * `" - Acme"`. Returns an empty string when no placeholder resolved.
46
51
  */
47
52
  export function formatRecordTitle(titleFormat, record) {
48
- if (!titleFormat || !record) {
53
+ // Normalize Expression envelope ({ dialect, source }) → raw template string.
54
+ const template = typeof titleFormat === 'string'
55
+ ? titleFormat
56
+ : (titleFormat && typeof titleFormat === 'object' && typeof titleFormat.source === 'string')
57
+ ? titleFormat.source
58
+ : undefined;
59
+ if (!template || !record) {
49
60
  return record?.id || record?._id || 'Record';
50
61
  }
51
62
  let anyResolved = false;
52
- let out = titleFormat.replace(/\{([^{}]+)\}/g, (_match, fieldName) => {
63
+ let out = template.replace(/\{([^{}]+)\}/g, (_match, fieldName) => {
53
64
  const value = record[fieldName.trim()];
54
65
  if (value === null || value === undefined || value === '') {
55
66
  return EMPTY_TOKEN;
@@ -12,7 +12,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  */
13
13
  import { useState, useEffect } from 'react';
14
14
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Button, Input, Label, Textarea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
15
+ import { useObjectTranslation } from '@object-ui/i18n';
15
16
  export function ActionParamDialog({ state, onOpenChange }) {
17
+ const { t } = useObjectTranslation();
16
18
  const [values, setValues] = useState({});
17
19
  const [errors, setErrors] = useState({});
18
20
  // Reset values when params change
@@ -63,5 +65,5 @@ export function ActionParamDialog({ state, onOpenChange }) {
63
65
  return (_jsx(Dialog, { open: state.open, onOpenChange: (open) => {
64
66
  if (!open)
65
67
  handleCancel();
66
- }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Action Parameters" }), _jsx(DialogDescription, { children: "Please provide the required information to continue." })] }), _jsx("div", { className: "grid gap-4 py-4", children: state.params.map((param) => (_jsxs("div", { className: "grid gap-2", children: [_jsxs(Label, { htmlFor: param.name, children: [param.label, param.required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] }), param.type === 'select' && param.options ? (_jsxs(Select, { value: values[param.name] ?? '', onValueChange: (val) => updateValue(param.name, val), children: [_jsx(SelectTrigger, { id: param.name, className: errors[param.name] ? 'border-destructive' : '', children: _jsx(SelectValue, { placeholder: param.placeholder || `Select ${param.label}` }) }), _jsx(SelectContent, { children: param.options.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] })) : param.type === 'textarea' ? (_jsx(Textarea, { id: param.name, value: values[param.name] ?? '', onChange: (e) => updateValue(param.name, e.target.value), placeholder: param.placeholder, className: errors[param.name] ? 'border-destructive' : '' })) : param.type === 'number' ? (_jsx(Input, { id: param.name, type: "number", value: values[param.name] ?? '', onChange: (e) => updateValue(param.name, e.target.value === '' ? undefined : e.target.valueAsNumber), placeholder: param.placeholder, className: errors[param.name] ? 'border-destructive' : '' })) : (_jsx(Input, { id: param.name, type: ['email', 'url', 'date', 'datetime-local', 'time', 'password'].includes(param.type) ? param.type : 'text', value: values[param.name] ?? '', onChange: (e) => updateValue(param.name, e.target.value), placeholder: param.placeholder, className: errors[param.name] ? 'border-destructive' : '' })), errors[param.name] && (_jsxs("p", { className: "text-xs text-destructive", children: [param.label, " is required"] })), param.helpText && (_jsx("p", { className: "text-xs text-muted-foreground", children: param.helpText }))] }, param.name))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: handleCancel, children: "Cancel" }), _jsx(Button, { onClick: handleSubmit, children: "Confirm" })] })] }) }));
68
+ }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('actionDialog.title') }), _jsx(DialogDescription, { children: t('actionDialog.description') })] }), _jsx("div", { className: "grid gap-4 py-4", children: state.params.map((param) => (_jsxs("div", { className: "grid gap-2", children: [_jsxs(Label, { htmlFor: param.name, children: [param.label, param.required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] }), param.type === 'select' && param.options ? (_jsxs(Select, { value: values[param.name] ?? '', onValueChange: (val) => updateValue(param.name, val), children: [_jsx(SelectTrigger, { id: param.name, className: errors[param.name] ? 'border-destructive' : '', children: _jsx(SelectValue, { placeholder: param.placeholder || t('actionDialog.selectPlaceholder', { label: param.label }) }) }), _jsx(SelectContent, { children: param.options.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] })) : param.type === 'textarea' ? (_jsx(Textarea, { id: param.name, value: values[param.name] ?? '', onChange: (e) => updateValue(param.name, e.target.value), placeholder: param.placeholder, className: errors[param.name] ? 'border-destructive' : '' })) : param.type === 'number' ? (_jsx(Input, { id: param.name, type: "number", value: values[param.name] ?? '', onChange: (e) => updateValue(param.name, e.target.value === '' ? undefined : e.target.valueAsNumber), placeholder: param.placeholder, className: errors[param.name] ? 'border-destructive' : '' })) : (_jsx(Input, { id: param.name, type: ['email', 'url', 'date', 'datetime-local', 'time', 'password'].includes(param.type) ? param.type : 'text', value: values[param.name] ?? '', onChange: (e) => updateValue(param.name, e.target.value), placeholder: param.placeholder, className: errors[param.name] ? 'border-destructive' : '' })), errors[param.name] && (_jsx("p", { className: "text-xs text-destructive", children: t('actionDialog.requiredError', { label: param.label }) })), param.helpText && (_jsx("p", { className: "text-xs text-muted-foreground", children: param.helpText }))] }, param.name))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: handleCancel, children: t('actionDialog.cancel') }), _jsx(Button, { onClick: handleSubmit, children: t('actionDialog.confirm') })] })] }) }));
67
69
  }
@@ -147,21 +147,21 @@ export function DashboardView({ dataSource }) {
147
147
  }), []);
148
148
  const scriptHandlers = useMemo(() => ({
149
149
  export_dashboard_pdf: async () => {
150
- toast.info('Preparing PDF export…');
150
+ toast.info(t('dashboardActions.pdfPreparing'));
151
151
  try {
152
152
  window.print();
153
153
  return { success: true };
154
154
  }
155
155
  catch (err) {
156
- toast.error(`Export failed: ${err?.message || String(err)}`);
156
+ toast.error(t('dashboardActions.exportFailed', { message: err?.message || String(err) }));
157
157
  return { success: false, error: err?.message || String(err) };
158
158
  }
159
159
  },
160
160
  forecast_dashboard: async () => {
161
- toast.info('Forecast view coming soon');
161
+ toast.info(t('dashboardActions.forecastSoon'));
162
162
  return { success: true };
163
163
  },
164
- }), []);
164
+ }), [t]);
165
165
  useEffect(() => {
166
166
  setIsLoading(true);
167
167
  setEditSchema(null);
@@ -349,7 +349,7 @@ export function DashboardView({ dataSource }) {
349
349
  return _jsx(SkeletonDashboard, {});
350
350
  }
351
351
  if (!dashboard) {
352
- return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(LayoutDashboard, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: "Dashboard Not Found" }), _jsxs(EmptyDescription, { children: ["The dashboard \"", dashboardName, "\" could not be found. It may have been removed or renamed."] })] }) }));
352
+ return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(LayoutDashboard, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.dashboardNotFound') }), _jsx(EmptyDescription, { children: t('empty.dashboardNotFoundDescription', { name: dashboardName }) })] }) }));
353
353
  }
354
354
  const previewSchema = editSchema || dashboard;
355
355
  return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [_jsxs("div", { className: "flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate", children: dashboardLabel({ name: dashboard.name, label: resolveI18nLabel(dashboard.label, t) }) || dashboard.name }), (() => {
@@ -375,5 +375,5 @@ export function DashboardView({ dataSource }) {
375
375
  showSubmit: true,
376
376
  showCancel: true,
377
377
  }, dataSource: adapter })) : modalState ? (_jsx(Dialog, { open: true, onOpenChange: (open) => { if (!open)
378
- closeModal({ success: false }); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: modalState.schema?.title || 'Action' }), modalState.schema?.description && (_jsx(DialogDescription, { children: modalState.schema.description }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => closeModal({ success: false }), children: "Cancel" }), _jsx(Button, { onClick: () => closeModal({ success: true }), children: "OK" })] })] }) })) : null] }));
378
+ closeModal({ success: false }); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: modalState.schema?.title || t('actionDialog.defaultActionTitle') }), modalState.schema?.description && (_jsx(DialogDescription, { children: modalState.schema.description }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => closeModal({ success: false }), children: t('actionDialog.cancel') }), _jsx(Button, { onClick: () => closeModal({ success: true }), children: t('actionDialog.ok') })] })] }) })) : null] }));
379
379
  }
@@ -9,12 +9,14 @@ import { useState, useMemo } from 'react';
9
9
  import { Button } from '@object-ui/components';
10
10
  import { Code2, Copy, Check, ChevronDown, ChevronRight } from 'lucide-react';
11
11
  import { parseDebugFlags } from '@object-ui/core';
12
+ import { useObjectTranslation } from '@object-ui/i18n';
12
13
  /**
13
14
  * Toggle button for the metadata inspector.
14
15
  * Place this in your header/toolbar area.
15
16
  */
16
17
  export function MetadataToggle({ open, onToggle, className }) {
17
- return (_jsxs(Button, { size: "sm", variant: open ? 'secondary' : 'outline', onClick: onToggle, className: `shadow-none gap-2 ${className || ''}`, title: "Toggle Metadata Inspector", children: [_jsx(Code2, { className: "h-4 w-4" }), _jsx("span", { className: "hidden lg:inline", children: "Metadata" })] }));
18
+ const { t } = useObjectTranslation();
19
+ return (_jsxs(Button, { size: "sm", variant: open ? 'secondary' : 'outline', onClick: onToggle, className: `shadow-none gap-2 ${className || ''}`, title: t('layout.metadata.toggleTitle'), children: [_jsx(Code2, { className: "h-4 w-4" }), _jsx("span", { className: "hidden lg:inline", children: t('layout.metadata.label') })] }));
18
20
  }
19
21
  /**
20
22
  * The side panel that renders JSON metadata sections.
@@ -22,6 +24,7 @@ export function MetadataToggle({ open, onToggle, className }) {
22
24
  export function MetadataPanel({ sections, open }) {
23
25
  if (!open)
24
26
  return null;
27
+ const { t } = useObjectTranslation();
25
28
  const [expandedSections, setExpandedSections] = useState(['0']);
26
29
  const [copiedIndex, setCopiedIndex] = useState(null);
27
30
  const handleCopy = async (data, index) => {
@@ -39,11 +42,11 @@ export function MetadataPanel({ sections, open }) {
39
42
  ? prev.filter(i => i !== index)
40
43
  : [...prev, index]);
41
44
  };
42
- return (_jsxs("div", { className: "w-80 border-l bg-background p-0 overflow-hidden flex flex-col shrink-0 z-20 transition-all", children: [_jsxs("div", { className: "px-4 py-3 border-b bg-muted/5 flex items-center justify-between", children: [_jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: "Metadata Inspector" }), _jsx("span", { className: "text-[10px] text-muted-foreground/60", children: "JSON" })] }), _jsx("div", { className: "flex-1 overflow-auto", children: sections.map((section, index) => {
45
+ return (_jsxs("div", { className: "w-80 border-l bg-background p-0 overflow-hidden flex flex-col shrink-0 z-20 transition-all", children: [_jsxs("div", { className: "px-4 py-3 border-b bg-muted/5 flex items-center justify-between", children: [_jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: t('layout.metadata.panelTitle') }), _jsx("span", { className: "text-[10px] text-muted-foreground/60", children: t('layout.metadata.jsonBadge') })] }), _jsx("div", { className: "flex-1 overflow-auto", children: sections.map((section, index) => {
43
46
  const sectionId = String(index);
44
47
  const isExpanded = expandedSections.includes(sectionId);
45
48
  const isCopied = copiedIndex === index;
46
- return (_jsxs("div", { className: "border-b last:border-b-0", children: [_jsxs("div", { className: "flex items-center justify-between px-4 py-3 hover:bg-muted/5 transition-colors", children: [_jsxs("button", { onClick: () => toggleSection(sectionId), className: "flex-1 flex items-center justify-between text-left", children: [_jsx("h4", { className: "text-xs font-semibold text-foreground", children: section.title }), isExpanded ? (_jsx(ChevronDown, { className: "h-3 w-3 text-muted-foreground ml-2" })) : (_jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground ml-2" }))] }), _jsx("button", { onClick: () => handleCopy(section.data, index), className: "p-1 hover:bg-muted rounded transition-colors ml-2", title: "Copy JSON", children: isCopied ? (_jsx(Check, { className: "h-3 w-3 text-green-600" })) : (_jsx(Copy, { className: "h-3 w-3 text-muted-foreground" })) })] }), isExpanded && (_jsx("div", { className: "px-4 pb-4", children: _jsx("div", { className: "relative rounded-md border border-border/50 bg-muted/5 overflow-hidden", children: _jsx("pre", { className: "text-[11px] leading-relaxed p-3 overflow-auto max-h-96 font-mono text-foreground/90", children: JSON.stringify(section.data, null, 2) }) }) }))] }, index));
49
+ return (_jsxs("div", { className: "border-b last:border-b-0", children: [_jsxs("div", { className: "flex items-center justify-between px-4 py-3 hover:bg-muted/5 transition-colors", children: [_jsxs("button", { onClick: () => toggleSection(sectionId), className: "flex-1 flex items-center justify-between text-left", children: [_jsx("h4", { className: "text-xs font-semibold text-foreground", children: section.title }), isExpanded ? (_jsx(ChevronDown, { className: "h-3 w-3 text-muted-foreground ml-2" })) : (_jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground ml-2" }))] }), _jsx("button", { onClick: () => handleCopy(section.data, index), className: "p-1 hover:bg-muted rounded transition-colors ml-2", title: t('layout.metadata.copyJson'), children: isCopied ? (_jsx(Check, { className: "h-3 w-3 text-green-600" })) : (_jsx(Copy, { className: "h-3 w-3 text-muted-foreground" })) })] }), isExpanded && (_jsx("div", { className: "px-4 pb-4", children: _jsx("div", { className: "relative rounded-md border border-border/50 bg-muted/5 overflow-hidden", children: _jsx("pre", { className: "text-[11px] leading-relaxed p-3 overflow-auto max-h-96 font-mono text-foreground/90", children: JSON.stringify(section.data, null, 2) }) }) }))] }, index));
47
50
  }) })] }));
48
51
  }
49
52
  /**
@@ -12,6 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  import { useMemo, useState, useCallback, useEffect, lazy, Suspense } from 'react';
13
13
  import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
14
14
  const ObjectChart = lazy(() => import('@object-ui/plugin-charts').then((m) => ({ default: m.ObjectChart })));
15
+ const ImportWizard = lazy(() => import('@object-ui/plugin-grid').then((m) => ({ default: m.ImportWizard })));
15
16
  import { ListView } from '@object-ui/plugin-list';
16
17
  import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
17
18
  import { ObjectView as PluginObjectView, ViewTabBar, ManageViewsDialog } from '@object-ui/plugin-view';
@@ -19,7 +20,7 @@ import { ObjectView as PluginObjectView, ViewTabBar, ManageViewsDialog } from '@
19
20
  // uses ComponentRegistry.registerLazy so heavy plugins stay code-split).
20
21
  // Do NOT add eager `import '@object-ui/plugin-*'` side-effect imports here.
21
22
  import { Button, Empty, EmptyTitle, EmptyDescription, NavigationOverlay } from '@object-ui/components';
22
- import { Plus, Table as TableIcon, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
23
+ import { Plus, Upload, Table as TableIcon, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
23
24
  import { getIcon } from '../utils/getIcon';
24
25
  import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
25
26
  import { ViewConfigPanel } from './ViewConfigPanel';
@@ -298,6 +299,8 @@ export function ObjectView({ dataSource, objects, onEdit }) {
298
299
  }
299
300
  // Refresh trigger — bumped after view CRUD or external data mutations.
300
301
  const [refreshKey, setRefreshKey] = useState(0);
302
+ // Import wizard open/close state — toolbar entry triggers it.
303
+ const [showImport, setShowImport] = useState(false);
301
304
  // ─── User-defined views (sys_view) ──────────────────────────────────
302
305
  // Saved views created via the ViewConfigPanel ("Add View") are persisted
303
306
  // to the `sys_view` object. We fetch them here and merge into `views` so
@@ -534,7 +537,7 @@ export function ObjectView({ dataSource, objects, onEdit }) {
534
537
  }
535
538
  catch (err) {
536
539
  console.error('[ViewTabBar] Failed to rename view:', err);
537
- toast.error('Failed to rename view');
540
+ toast.error(t('objectViewActions.renameFailed'));
538
541
  }
539
542
  }, [dataSource, isSavedView, t]);
540
543
  // Promise-based confirm/param dialogs — declared early so destructive
@@ -581,7 +584,7 @@ export function ObjectView({ dataSource, objects, onEdit }) {
581
584
  }
582
585
  catch (err) {
583
586
  console.error('[ViewTabBar] Failed to delete view:', err);
584
- toast.error('Failed to delete view');
587
+ toast.error(t('objectViewActions.deleteFailed'));
585
588
  }
586
589
  }, [dataSource, isSavedView, activeViewId, views, viewId, navigate, t, confirmHandler]);
587
590
  const handleDuplicateView = useCallback(async (vid) => {
@@ -696,14 +699,6 @@ export function ObjectView({ dataSource, objects, onEdit }) {
696
699
  const handleAddView = useCallback(() => {
697
700
  setShowCreateViewDialog(true);
698
701
  }, []);
699
- // Action system for toolbar operations — refreshKey moved up (declared earlier).
700
- const actions = useObjectActions({
701
- objectName: objectDef.name,
702
- objectLabel: objectDef.label,
703
- dataSource,
704
- onEdit,
705
- onRefresh: () => setRefreshKey(k => k + 1),
706
- });
707
702
  // ─── ActionProvider handlers for schema-driven toolbar actions ──────
708
703
  const currentUser = user
709
704
  ? { id: user.id, name: user.name, avatar: user.image }
@@ -714,6 +709,18 @@ export function ObjectView({ dataSource, objects, onEdit }) {
714
709
  else
715
710
  toast.success(message);
716
711
  }, []);
712
+ // Action system for toolbar operations — refreshKey moved up (declared earlier).
713
+ // Wired to confirmHandler/toastHandler so deletes use the Shadcn AlertDialog
714
+ // and Sonner toast instead of native window.confirm.
715
+ const actions = useObjectActions({
716
+ objectName: objectDef.name,
717
+ objectLabel: objectDef.label,
718
+ dataSource,
719
+ onEdit,
720
+ onRefresh: () => setRefreshKey(k => k + 1),
721
+ onConfirm: confirmHandler,
722
+ onToast: toastHandler,
723
+ });
717
724
  const navigateHandler = useCallback((url, options) => {
718
725
  if (options?.external || options?.newTab) {
719
726
  window.open(url, '_blank', 'noopener,noreferrer');
@@ -1040,10 +1047,17 @@ export function ObjectView({ dataSource, objects, onEdit }) {
1040
1047
  },
1041
1048
  },
1042
1049
  };
1043
- return (_jsx(ListView, { schema: fullSchema, className: className, onEdit: editHandler, onRowClick: (record) => {
1050
+ return (_jsx(ListView, { schema: fullSchema, className: className, onEdit: editHandler, onDelete: (record) => {
1051
+ if (record?.id != null) {
1052
+ // useObjectActions.deleteRecord wraps execute() which
1053
+ // already shows a confirmation dialog + success toast
1054
+ // and triggers onRefresh on success.
1055
+ actions.deleteRecord(String(record.id));
1056
+ }
1057
+ }, onRowClick: (record) => {
1044
1058
  navOverlay.handleClick(record);
1045
1059
  }, dataSource: ds }, key));
1046
- }, [activeView, objectDef, objectName, refreshKey, navOverlay]);
1060
+ }, [activeView, objectDef, objectName, refreshKey, navOverlay, actions]);
1047
1061
  // Memoize the merged views array so PluginObjectView doesn't get a new
1048
1062
  // reference on every render (which would trigger unnecessary data refetches).
1049
1063
  const mergedViews = useMemo(() => views.map((v) => v.id === activeViewId && viewDraft && viewDraft.id === v.id
@@ -1080,13 +1094,28 @@ export function ObjectView({ dataSource, objects, onEdit }) {
1080
1094
  }
1081
1095
  },
1082
1096
  }), [objectDef.name, onEdit, activeView?.showSearch, activeView?.showFilters, activeView?.showSort, navigate, viewId, isAdmin]);
1083
- return (_jsxs(ActionProvider, { context: { objectName: objectDef.name, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [_jsxs("div", { className: "flex justify-between items-center py-2.5 sm:py-3 px-3 sm:px-4 border-b shrink-0 bg-background z-10", children: [_jsxs("div", { className: "flex items-center gap-2 sm:gap-3 min-w-0 flex-1", children: [_jsx("div", { className: "bg-primary/10 p-1.5 sm:p-2 rounded-md shrink-0", children: (() => { const I = getIcon(objectDef?.icon); return _jsx(I, { className: "h-4 w-4 text-primary" }); })() }), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-base sm:text-lg font-semibold tracking-tight text-foreground truncate", children: objectLabel(objectDef) }), objectDef.description && (_jsx("p", { className: "text-xs text-muted-foreground truncate hidden sm:block max-w-md", children: objectDesc(objectDef) }))] })] }), _jsxs("div", { className: "flex items-center gap-1.5 sm:gap-2 shrink-0", children: [can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", onClick: actions.create, className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.new') })] })), objectDef.actions?.some((a) => a.locations?.includes('list_toolbar')) && (_jsx(SchemaRenderer, { schema: {
1097
+ return (_jsxs(ActionProvider, { context: { objectName: objectDef.name, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [_jsxs("div", { className: "flex justify-between items-center py-2.5 sm:py-3 px-3 sm:px-4 border-b shrink-0 bg-background z-10", children: [_jsxs("div", { className: "flex items-center gap-2 sm:gap-3 min-w-0 flex-1", children: [_jsx("div", { className: "bg-primary/10 p-1.5 sm:p-2 rounded-md shrink-0", children: (() => { const I = getIcon(objectDef?.icon); return _jsx(I, { className: "h-4 w-4 text-primary" }); })() }), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-base sm:text-lg font-semibold tracking-tight text-foreground truncate", children: objectLabel(objectDef) }), objectDef.description && (_jsx("p", { className: "text-xs text-muted-foreground truncate hidden sm:block max-w-md", children: objectDesc(objectDef) }))] })] }), _jsxs("div", { className: "flex items-center gap-1.5 sm:gap-2 shrink-0", children: [can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", onClick: actions.create, className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.new') })] })), can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setShowImport(true), className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", title: t('console.objectView.importTitle'), "data-testid": "object-view-import-button", children: [_jsx(Upload, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.import') })] })), objectDef.actions?.some((a) => a.locations?.includes('list_toolbar')) && (_jsx(SchemaRenderer, { schema: {
1084
1098
  type: 'action:bar',
1085
1099
  location: 'list_toolbar',
1086
1100
  actions: objectDef.actions,
1087
1101
  size: 'sm',
1088
1102
  variant: 'outline',
1089
- } }))] })] }), views.length >= 1 && (() => {
1103
+ } }))] })] }), showImport && (_jsx(Suspense, { fallback: null, children: _jsx(ImportWizard, { open: showImport, onOpenChange: setShowImport, objectName: objectDef.name, objectLabel: objectLabel(objectDef), fields: Object.entries(objectDef.fields || {}).map(([name, def]) => ({
1104
+ name,
1105
+ label: def?.label || name,
1106
+ type: def?.type || 'text',
1107
+ required: !!def?.required,
1108
+ })), dataSource: dataSource, onComplete: (result) => {
1109
+ setRefreshKey(k => k + 1);
1110
+ const ok = result.importedRows;
1111
+ const skip = result.skippedRows;
1112
+ if (skip > 0) {
1113
+ toast.warning(t('console.objectView.importedWithSkipped', { ok, skipped: skip }));
1114
+ }
1115
+ else if (ok > 0) {
1116
+ toast.success(t('console.objectView.importedToast', { count: ok }));
1117
+ }
1118
+ } }) })), views.length >= 1 && (() => {
1090
1119
  const viewTabItems = views.map((view) => {
1091
1120
  const saved = savedViews.find((sv) => (sv.id || sv._id) === view.id);
1092
1121
  // System views (loaded from objectDef.listViews / metadata) are
@@ -9,11 +9,13 @@ import { useParams, useSearchParams } from 'react-router-dom';
9
9
  import { SchemaRenderer } from '@object-ui/react';
10
10
  import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
11
11
  import { FileText, Pencil } from 'lucide-react';
12
+ import { useObjectTranslation } from '@object-ui/i18n';
12
13
  import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
13
14
  import { useMetadata } from '../providers/MetadataProvider';
14
15
  import { DesignDrawer } from './DesignDrawer';
15
16
  const PageCanvasEditor = lazy(() => import('@object-ui/plugin-designer').then((m) => ({ default: m.PageCanvasEditor })));
16
17
  export function PageView() {
18
+ const { t } = useObjectTranslation();
17
19
  const { pageName } = useParams();
18
20
  const [searchParams] = useSearchParams();
19
21
  const { showDebug } = useMetadataInspector();
@@ -35,7 +37,7 @@ export function PageView() {
35
37
  }
36
38
  }, []);
37
39
  if (!page) {
38
- return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(FileText, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: "Page Not Found" }), _jsxs(EmptyDescription, { children: ["The page \"", pageName, "\" could not be found. It may have been removed or renamed."] })] }) }));
40
+ return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(FileText, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.pageNotFound') }), _jsx(EmptyDescription, { children: t('empty.pageNotFoundDescription', { name: pageName }) })] }) }));
39
41
  }
40
42
  // Convert URL search params to an object for the page context
41
43
  const params = Object.fromEntries(searchParams.entries());
@@ -473,9 +473,9 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
473
473
  return _jsx(SkeletonDetail, {});
474
474
  }
475
475
  if (!objectDef) {
476
- return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: "Object Not Found" }), _jsxs(EmptyDescription, { children: ["Object \"", objectName, "\" definition missing. Check your configuration or navigate back to select a valid object."] })] }) }));
476
+ return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.objectNotFound') }), _jsx(EmptyDescription, { children: t('empty.objectNotFoundDescription', { name: objectName }) })] }) }));
477
477
  }
478
- return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsx("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: "Users viewing this record", children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] })) }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsx("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: _jsx(ActionProvider, { context: { record: {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectDef.label, onDataLoaded: (record) => {
478
+ return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsx("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] })) }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsx("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: _jsx(ActionProvider, { context: { record: {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectDef.label, onDataLoaded: (record) => {
479
479
  if (!record || typeof record !== 'object')
480
480
  return;
481
481
  // Resolve the same way DetailView's header does, so the
@@ -141,7 +141,7 @@ export function RecordFormPage({ mode }) {
141
141
  return _jsx(SkeletonDetail, {});
142
142
  }
143
143
  if (!objectDef) {
144
- return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: "Object Not Found" }), _jsxs(EmptyDescription, { children: ["Object \"", objectName, "\" definition missing. Check your configuration or navigate back to select a valid object."] }), _jsx("div", { className: "mt-4", children: _jsxs(Button, { variant: "outline", onClick: () => navigate(baseUrl), children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), "Back"] }) })] }) }));
144
+ return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.objectNotFound') }), _jsx(EmptyDescription, { children: t('empty.objectNotFoundDescription', { name: objectName }) }), _jsx("div", { className: "mt-4", children: _jsxs(Button, { variant: "outline", onClick: () => navigate(baseUrl), children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), t('empty.back')] }) })] }) }));
145
145
  }
146
146
  return (_jsx(ExpressionProvider, { user: expressionUser, app: { name: appName }, data: {}, children: _jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", "data-testid": "record-form-page", "data-mode": mode, children: [_jsxs("header", { className: "sticky top-0 z-10 flex items-center gap-3 border-b bg-background px-4 py-3 sm:px-6", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: goBack, "data-testid": "record-form-page-back", "aria-label": t('common.back', { defaultValue: 'Back' }), children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Link, { to: objectListUrl, className: "hover:text-foreground transition-colors", children: label }), _jsx("span", { "aria-hidden": "true", children: "/" }), _jsx("span", { className: "text-foreground font-medium", "data-testid": "record-form-page-title", children: pageTitle })] })] }), _jsx("div", { className: "flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto max-w-4xl", children: _jsx(ObjectForm, { schema: {
147
147
  type: 'object-form',
@@ -5,6 +5,7 @@ const ReportViewer = lazy(() => import('@object-ui/plugin-report').then((m) => (
5
5
  const ReportConfigPanel = lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportConfigPanel })));
6
6
  import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
7
7
  import { Pencil, BarChart3, Loader2 } from 'lucide-react';
8
+ import { useObjectTranslation } from '@object-ui/i18n';
8
9
  import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
9
10
  import { useMetadata } from '../providers/MetadataProvider';
10
11
  import { useAdapter } from '../providers/AdapterProvider';
@@ -20,6 +21,7 @@ const FALLBACK_FIELDS = [
20
21
  { value: 'amount', label: 'Amount', type: 'number' },
21
22
  ];
22
23
  export function ReportView({ dataSource }) {
24
+ const { t } = useObjectTranslation();
23
25
  const { reportName } = useParams();
24
26
  const { showDebug } = useMetadataInspector();
25
27
  const adapter = useAdapter();
@@ -194,7 +196,7 @@ export function ReportView({ dataSource }) {
194
196
  }
195
197
  if (!initialReport || !reportData) {
196
198
  if (!loading && !initialReport) {
197
- return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(BarChart3, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: "Report Not Found" }), _jsxs(EmptyDescription, { children: ["The report \"", reportName, "\" could not be found. It may have been removed or renamed."] })] }) }));
199
+ return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(BarChart3, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.reportNotFound') }), _jsx(EmptyDescription, { children: t('empty.reportNotFoundDescription', { name: reportName }) })] }) }));
198
200
  }
199
201
  return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-8 w-8 animate-spin text-muted-foreground" }) }));
200
202
  }
@@ -11,6 +11,7 @@ import { useState, useMemo } from 'react';
11
11
  import { useSearchParams, Link, useParams } from 'react-router-dom';
12
12
  import { Input, Card, CardContent, Badge, } from '@object-ui/components';
13
13
  import { Search, Database, LayoutDashboard, FileText, BarChart3, ArrowLeft, } from 'lucide-react';
14
+ import { useObjectTranslation } from '@object-ui/i18n';
14
15
  import { useMetadata } from '../providers/MetadataProvider';
15
16
  /** Flatten nested navigation groups into a flat list of leaf items */
16
17
  function flattenNavigation(items) {
@@ -38,6 +39,7 @@ const TYPE_COLORS = {
38
39
  report: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
39
40
  };
40
41
  export function SearchResultsPage() {
42
+ const { t } = useObjectTranslation();
41
43
  const { appName } = useParams();
42
44
  const [searchParams, setSearchParams] = useSearchParams();
43
45
  const queryParam = searchParams.get('q') || '';
@@ -95,13 +97,16 @@ export function SearchResultsPage() {
95
97
  }
96
98
  return groups;
97
99
  }, [results]);
98
- return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6 max-w-4xl mx-auto", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Link, { to: baseUrl, className: "flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back"] }), _jsx("h1", { className: "text-xl font-semibold", children: "Search" })] }), _jsxs("div", { className: "relative", children: [_jsx("label", { htmlFor: "search-results-input", className: "sr-only", children: "Search objects, dashboards, pages, reports" }), _jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { id: "search-results-input", value: query, onChange: (e) => handleSearch(e.target.value), placeholder: "Search objects, dashboards, pages, reports...", className: "pl-10 h-11 text-base", autoFocus: true })] }), _jsx("div", { className: "text-sm text-muted-foreground", children: query.trim()
99
- ? `${results.length} result${results.length !== 1 ? 's' : ''} for "${query}"`
100
- : `${allItems.length} items available` }), results.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: [_jsx(Search, { className: "h-12 w-12 text-muted-foreground/30 mb-4" }), _jsx("p", { className: "text-lg font-medium text-muted-foreground", children: "No results found" }), _jsx("p", { className: "text-sm text-muted-foreground/80 mt-1", children: "Try adjusting your search terms" })] })) : (_jsx("div", { className: "space-y-6", children: Object.entries(grouped).map(([type, items]) => {
100
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6 max-w-4xl mx-auto", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Link, { to: baseUrl, className: "flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), t('search.back')] }), _jsx("h1", { className: "text-xl font-semibold", children: t('search.title') })] }), _jsxs("div", { className: "relative", children: [_jsx("label", { htmlFor: "search-results-input", className: "sr-only", children: t('search.placeholder') }), _jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { id: "search-results-input", value: query, onChange: (e) => handleSearch(e.target.value), placeholder: t('search.placeholder'), className: "pl-10 h-11 text-base", autoFocus: true })] }), _jsx("div", { className: "text-sm text-muted-foreground", children: query.trim()
101
+ ? t(results.length === 1 ? 'search.resultsCount' : 'search.resultsCountPlural', { count: results.length, query })
102
+ : t('search.itemsAvailable', { count: allItems.length }) }), results.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: [_jsx(Search, { className: "h-12 w-12 text-muted-foreground/30 mb-4" }), _jsx("p", { className: "text-lg font-medium text-muted-foreground", children: t('search.noResults') }), _jsx("p", { className: "text-sm text-muted-foreground/80 mt-1", children: t('search.noResultsHint') })] })) : (_jsx("div", { className: "space-y-6", children: Object.entries(grouped).map(([type, items]) => {
101
103
  const TypeIcon = TYPE_ICONS[type] || Database;
102
- return (_jsxs("div", { children: [_jsxs("h2", { className: "text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1.5", children: [_jsx(TypeIcon, { className: "h-4 w-4" }), type.charAt(0).toUpperCase() + type.slice(1), "s", _jsx(Badge, { variant: "secondary", className: "ml-1 text-xs", children: items.length })] }), _jsx("div", { className: "grid gap-2", children: items.map(item => {
104
+ const typeLabelKey = `search.type${type.charAt(0).toUpperCase()}${type.slice(1)}s`;
105
+ const badgeKey = `search.badge${type.charAt(0).toUpperCase()}${type.slice(1)}`;
106
+ return (_jsxs("div", { children: [_jsxs("h2", { className: "text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1.5", children: [_jsx(TypeIcon, { className: "h-4 w-4" }), t(typeLabelKey), _jsx(Badge, { variant: "secondary", className: "ml-1 text-xs", children: items.length })] }), _jsx("div", { className: "grid gap-2", children: items.map(item => {
103
107
  const ItemIcon = TYPE_ICONS[item.type] || Database;
104
- return (_jsx(Link, { to: item.href, className: "rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: _jsx(Card, { className: "hover:bg-accent/50 transition-colors cursor-pointer", children: _jsxs(CardContent, { className: "flex items-center gap-3 p-3", children: [_jsx("div", { className: `flex h-8 w-8 items-center justify-center rounded ${TYPE_COLORS[item.type] || ''}`, children: _jsx(ItemIcon, { className: "h-4 w-4" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-medium truncate", children: item.label }), item.description && (_jsx("p", { className: "text-xs text-muted-foreground truncate", children: item.description }))] }), _jsx(Badge, { variant: "outline", className: "text-xs shrink-0", children: item.type })] }) }) }, item.id));
108
+ const itemBadgeKey = `search.badge${item.type.charAt(0).toUpperCase()}${item.type.slice(1)}`;
109
+ return (_jsx(Link, { to: item.href, className: "rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: _jsx(Card, { className: "hover:bg-accent/50 transition-colors cursor-pointer", children: _jsxs(CardContent, { className: "flex items-center gap-3 p-3", children: [_jsx("div", { className: `flex h-8 w-8 items-center justify-center rounded ${TYPE_COLORS[item.type] || ''}`, children: _jsx(ItemIcon, { className: "h-4 w-4" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-medium truncate", children: item.label }), item.description && (_jsx("p", { className: "text-xs text-muted-foreground truncate", children: item.description }))] }), _jsx(Badge, { variant: "outline", className: "text-xs shrink-0", children: t(itemBadgeKey) })] }) }) }, item.id));
105
110
  }) })] }, type));
106
111
  }) }))] }));
107
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "4.0.6",
3
+ "version": "4.0.7",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
@@ -27,34 +27,34 @@
27
27
  "dependencies": {
28
28
  "lucide-react": "^1.14.0",
29
29
  "sonner": "^2.0.7",
30
- "@object-ui/auth": "4.0.6",
31
- "@object-ui/collaboration": "4.0.6",
32
- "@object-ui/components": "4.0.6",
33
- "@object-ui/core": "4.0.6",
34
- "@object-ui/data-objectstack": "4.0.6",
35
- "@object-ui/fields": "4.0.6",
36
- "@object-ui/i18n": "4.0.6",
37
- "@object-ui/layout": "4.0.6",
38
- "@object-ui/permissions": "4.0.6",
39
- "@object-ui/react": "4.0.6",
40
- "@object-ui/types": "4.0.6"
30
+ "@object-ui/auth": "4.0.7",
31
+ "@object-ui/collaboration": "4.0.7",
32
+ "@object-ui/components": "4.0.7",
33
+ "@object-ui/core": "4.0.7",
34
+ "@object-ui/data-objectstack": "4.0.7",
35
+ "@object-ui/fields": "4.0.7",
36
+ "@object-ui/i18n": "4.0.7",
37
+ "@object-ui/layout": "4.0.7",
38
+ "@object-ui/permissions": "4.0.7",
39
+ "@object-ui/react": "4.0.7",
40
+ "@object-ui/types": "4.0.7"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "react": "^18.0.0 || ^19.0.0",
44
44
  "react-dom": "^18.0.0 || ^19.0.0",
45
45
  "react-router-dom": "^6.0.0 || ^7.0.0",
46
- "@object-ui/plugin-calendar": "4.0.6",
47
- "@object-ui/plugin-charts": "4.0.6",
48
- "@object-ui/plugin-chatbot": "4.0.6",
49
- "@object-ui/plugin-dashboard": "4.0.6",
50
- "@object-ui/plugin-designer": "4.0.6",
51
- "@object-ui/plugin-detail": "4.0.6",
52
- "@object-ui/plugin-form": "4.0.6",
53
- "@object-ui/plugin-grid": "4.0.6",
54
- "@object-ui/plugin-kanban": "4.0.6",
55
- "@object-ui/plugin-list": "4.0.6",
56
- "@object-ui/plugin-report": "4.0.6",
57
- "@object-ui/plugin-view": "4.0.6"
46
+ "@object-ui/plugin-calendar": "4.0.7",
47
+ "@object-ui/plugin-charts": "4.0.7",
48
+ "@object-ui/plugin-chatbot": "4.0.7",
49
+ "@object-ui/plugin-dashboard": "4.0.7",
50
+ "@object-ui/plugin-designer": "4.0.7",
51
+ "@object-ui/plugin-detail": "4.0.7",
52
+ "@object-ui/plugin-form": "4.0.7",
53
+ "@object-ui/plugin-grid": "4.0.7",
54
+ "@object-ui/plugin-kanban": "4.0.7",
55
+ "@object-ui/plugin-list": "4.0.7",
56
+ "@object-ui/plugin-report": "4.0.7",
57
+ "@object-ui/plugin-view": "4.0.7"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^25.6.0",