@object-ui/app-shell 4.0.6 → 4.0.8

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,141 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 4.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 3d58eaa: fix(auth,app-shell): hide Log out menu item when auth is disabled (guest/preview mode)
8
+
9
+ When the console runs against a server with `discovery.services.auth.enabled === false`
10
+ (or in preview mode), `AuthProvider` hardcodes `isAuthenticated: true` and the mock
11
+ `signOut()` has no real backend. Previously, clicking "Log out" in the user menu had
12
+ no visible effect — the user/session were nulled but the UI stayed authenticated.
13
+
14
+ Changes:
15
+ - **`@object-ui/auth`** — added `isAuthEnabled: boolean` to `AuthContextValue`
16
+ (`true` only when real auth is in use, `false` for guest/preview modes).
17
+ - **`@object-ui/app-shell`** — `AppHeader` and `AppSidebar` now hide the "Log out"
18
+ menu item entirely when `!isAuthEnabled`, so users aren't presented with an action
19
+ that can't actually do anything. Also fixed two missed i18n strings in
20
+ `AppSidebar` ("Settings", "Log out").
21
+ - **`@object-ui/i18n`** — added `user.{profile,settings,logout}` namespace to all
22
+ 10 built-in locales (en/zh translated; ja/ko/de/fr/es/pt/ru/ar fall back to
23
+ English pending native translation).
24
+
25
+ - Updated dependencies [3d58eaa]
26
+ - @object-ui/auth@4.0.8
27
+ - @object-ui/i18n@4.0.8
28
+ - @object-ui/components@4.0.8
29
+ - @object-ui/fields@4.0.8
30
+ - @object-ui/plugin-calendar@4.0.8
31
+ - @object-ui/plugin-charts@4.0.8
32
+ - @object-ui/plugin-dashboard@4.0.8
33
+ - @object-ui/plugin-designer@4.0.8
34
+ - @object-ui/plugin-list@4.0.8
35
+ - @object-ui/react@4.0.8
36
+ - @object-ui/layout@4.0.8
37
+ - @object-ui/plugin-chatbot@4.0.8
38
+ - @object-ui/plugin-detail@4.0.8
39
+ - @object-ui/plugin-form@4.0.8
40
+ - @object-ui/plugin-grid@4.0.8
41
+ - @object-ui/plugin-kanban@4.0.8
42
+ - @object-ui/plugin-report@4.0.8
43
+ - @object-ui/plugin-view@4.0.8
44
+ - @object-ui/types@4.0.8
45
+ - @object-ui/core@4.0.8
46
+ - @object-ui/data-objectstack@4.0.8
47
+ - @object-ui/permissions@4.0.8
48
+ - @object-ui/collaboration@4.0.8
49
+
50
+ ## 4.0.7
51
+
52
+ ### Patch Changes
53
+
54
+ - 7c9b85c: Fix compatibility with the framework's normalized Expression envelope format.
55
+
56
+ `@objectstack/spec` now emits predicate (`visible` / `enabled`) and template
57
+ (`titleFormat`) fields as `{ dialect, source }` envelopes instead of bare
58
+ strings. The previous implementation assumed strings and crashed the record
59
+ detail view (`TypeError: titleFormat.replace is not a function`) and printed
60
+ `Failed to evaluate expression: ${[object Object]}` for every action visibility
61
+ predicate.
62
+ - `@object-ui/core`: `ExpressionEvaluator.evaluate` / `evaluateCondition` now
63
+ unwrap Expression envelopes transparently.
64
+ - `@object-ui/react`: new `toPredicateInput()` helper to safely normalize
65
+ `boolean | string | Expression` predicate inputs into the `${expr}` form
66
+ expected by `useCondition`.
67
+ - `@object-ui/components`: `action-bar`, `action-button`, `action-group`,
68
+ `action-icon`, `action-menu` renderers use `toPredicateInput()` instead of
69
+ template-literal interpolation that produced `${[object Object]}`.
70
+ - `@object-ui/plugin-detail`, `@object-ui/plugin-kanban`,
71
+ `@object-ui/plugin-calendar`, `@object-ui/app-shell`,
72
+ `@object-ui/console`: title-format helpers accept both legacy strings and
73
+ the new `{ source }` envelope.
74
+
75
+ All changes are backward-compatible — legacy bare strings continue to work.
76
+
77
+ - fd15918: Comprehensive i18n refactor + CI test fix.
78
+
79
+ **i18n (`@object-ui/i18n`)**
80
+ - Added ~130 new keys under 12 new top-level namespaces: `layout`, `search`,
81
+ `empty`, `renderer`, `actionDialog`, `rowAction`, `navigationSync`,
82
+ `objectActions`, `objectViewActions`, `dashboardActions`, `recordDetail`,
83
+ `cellRender`, plus `grid.{empty,yes,no,systemFields,openMenu}`.
84
+ - Mirrored all new top-level namespaces to all 10 built-in locales
85
+ (en, zh, ja, ko, de, fr, es, pt, ru, ar) to maintain key parity required
86
+ by the locale-structure test. Non-en/zh locales seed with English values
87
+ and rely on `fallbackLng: 'en'` until human translation lands.
88
+
89
+ **App shell (`@object-ui/app-shell`)** — replaced hardcoded English in 14
90
+ files with `useObjectTranslation`:
91
+ - Layout: `AppSidebar`, `ActivityFeed` (locale-aware relative time),
92
+ `MetadataInspector`.
93
+ - Views: `SearchResultsPage`, `ActionParamDialog`, `RecordFormPage`,
94
+ `RecordDetailView`, `PageView`, `DashboardView` (PDF / forecast toasts),
95
+ `ReportView`, `ObjectView` (rename / delete view toasts).
96
+ - Console: `AppContent` (no-apps empty state).
97
+ - Components: `PageRenderer`, `FormRenderer`, `DashboardRenderer`.
98
+ - Hooks: `useNavigationSync` (16 toasts incl. Undo label),
99
+ `useObjectActions` (delete confirm + success / failure toasts).
100
+
101
+ **Plugin grid (`@object-ui/plugin-grid`)**
102
+ - `ObjectGrid` record-detail panel now translates Empty / Yes / No / System
103
+ via the existing `useGridTranslation` safe-fallback wrapper.
104
+ - `RowActionMenu` adopts a local safe-fallback i18n wrapper for
105
+ `Open menu` / `Edit` / `Delete`, preserving standalone-usage guarantees.
106
+
107
+ **CLI test fix (`@object-ui/cli`)**
108
+ - `cli-bin.test.ts` auto-builds the package on first run when `dist/cli.js`
109
+ is missing, instead of throwing. This unbreaks `pnpm test:coverage` in CI
110
+ (root vitest run does not honor turbo's `^build` deps) and removes the
111
+ manual `pnpm --filter @object-ui/cli build` requirement for local dev.
112
+
113
+ - Updated dependencies [7c9b85c]
114
+ - Updated dependencies [fd15918]
115
+ - @object-ui/core@4.0.7
116
+ - @object-ui/react@4.0.7
117
+ - @object-ui/components@4.0.7
118
+ - @object-ui/plugin-detail@4.0.7
119
+ - @object-ui/plugin-kanban@4.0.7
120
+ - @object-ui/plugin-calendar@4.0.7
121
+ - @object-ui/i18n@4.0.7
122
+ - @object-ui/plugin-grid@4.0.7
123
+ - @object-ui/data-objectstack@4.0.7
124
+ - @object-ui/fields@4.0.7
125
+ - @object-ui/layout@4.0.7
126
+ - @object-ui/plugin-charts@4.0.7
127
+ - @object-ui/plugin-chatbot@4.0.7
128
+ - @object-ui/plugin-dashboard@4.0.7
129
+ - @object-ui/plugin-designer@4.0.7
130
+ - @object-ui/plugin-form@4.0.7
131
+ - @object-ui/plugin-list@4.0.7
132
+ - @object-ui/plugin-report@4.0.7
133
+ - @object-ui/plugin-view@4.0.7
134
+ - @object-ui/types@4.0.7
135
+ - @object-ui/auth@4.0.7
136
+ - @object-ui/permissions@4.0.7
137
+ - @object-ui/collaboration@4.0.7
138
+
3
139
  ## 4.0.6
4
140
 
5
141
  ### 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,14 +257,14 @@ 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
  }
264
264
  const expressionUser = user
265
265
  ? { name: user.name, email: user.email, role: user.role ?? 'user' }
266
266
  : { name: 'Anonymous', email: '', role: 'guest' };
267
- return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}), _jsx(ErrorBoundary, { children: _jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: findFirstRoute(activeApp.navigation || []), replace: true }) }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }, refreshKey) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "design/page/:pageName", element: _jsx(PageDesignPage, {}) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), extraRoutes] }) }) }), currentObjectDef && (_jsx(ModalForm, { schema: {
267
+ return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}), _jsx(ErrorBoundary, { children: _jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: findFirstRoute(activeApp.navigation || []), replace: true }) }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }, refreshKey) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "design/page/:pageName", element: _jsx(PageDesignPage, {}) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), extraRoutes] }) }) }), currentObjectDef && (_jsx(ModalForm, { schema: {
268
268
  type: 'object-form',
269
269
  formType: 'modal',
270
270
  objectName: currentObjectDef.name,
@@ -296,7 +296,9 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
296
296
  onCancel: handleDialogCancel,
297
297
  showSubmit: true,
298
298
  showCancel: true,
299
- submitText: t('form.saveRecord'),
299
+ submitText: editingRecord
300
+ ? t('form.update', { defaultValue: t('common.save') || '保存' })
301
+ : t('form.create', { defaultValue: t('common.create') || '创建' }),
300
302
  cancelText: t('common.cancel'),
301
303
  }, dataSource: dataSource }, editingRecord?.id || 'new'))] })] }));
302
304
  }
@@ -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,49 @@ 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 (multi-row)
45
+ // { recordId } (legacy) — pre-params shape
46
+ const records = Array.isArray(action.params?.records)
47
+ ? action.params.records.filter((r) => r?.id != null)
48
+ : null;
49
+ // Bulk path — delete every record in parallel and report a summary.
50
+ if (records && records.length > 1) {
51
+ const results = await Promise.allSettled(records.map((r) => dataSource.delete(objectName, r.id)));
52
+ const failed = results.filter(r => r.status === 'rejected').length;
53
+ const succeeded = results.length - failed;
54
+ onRefresh?.();
55
+ if (failed === 0) {
56
+ toast.success(t('objectActions.bulkDeleteSuccess', {
57
+ count: succeeded,
58
+ label: objectLabel || objectName,
59
+ defaultValue: `Deleted ${succeeded} ${objectLabel || objectName} records`,
60
+ }));
61
+ return { success: true, reload: true };
62
+ }
63
+ toast.error(t('objectActions.bulkDeletePartial', {
64
+ succeeded,
65
+ failed,
66
+ defaultValue: `${succeeded} deleted, ${failed} failed`,
67
+ }));
68
+ return { success: false, error: `${failed} failed` };
69
+ }
70
+ const recordId = action.params?.recordId ??
71
+ action.params?.record?.id ??
72
+ records?.[0]?.id ??
73
+ action.recordId;
38
74
  if (!recordId)
39
- return { success: false, error: 'No record ID provided' };
75
+ return { success: false, error: t('objectActions.noRecordId') };
40
76
  try {
41
77
  await dataSource.delete(objectName, recordId);
42
78
  onRefresh?.();
43
- toast.success(`${objectLabel || objectName} deleted successfully`);
79
+ toast.success(t('objectActions.deleteSuccess', { label: objectLabel || objectName }));
44
80
  return { success: true, reload: true };
45
81
  }
46
82
  catch (err) {
47
- toast.error(`Failed to delete ${objectLabel || objectName}`, {
83
+ toast.error(t('objectActions.deleteFailed', { label: objectLabel || objectName }), {
48
84
  description: err.message,
49
85
  });
50
86
  return { success: false, error: err.message };
@@ -63,17 +99,17 @@ export function useObjectActions({ objectName, objectLabel, dataSource, onEdit,
63
99
  onRefresh?.();
64
100
  return { success: true, reload: true };
65
101
  });
66
- }, [runner, objectName, dataSource, onEdit, onRefresh, navigate, baseUrl]);
102
+ }, [runner, objectName, dataSource, onEdit, onRefresh, navigate, baseUrl, t, objectLabel]);
67
103
  const create = useCallback(() => {
68
104
  onEdit?.(null);
69
105
  }, [onEdit]);
70
106
  const deleteRecord = useCallback(async (recordId) => {
71
107
  return execute({
72
108
  type: 'delete',
73
- confirmText: `Are you sure you want to delete this record?`,
109
+ confirmText: t('objectActions.deleteConfirm'),
74
110
  params: { recordId },
75
111
  });
76
- }, [execute]);
112
+ }, [execute, t]);
77
113
  const navigateToView = useCallback((viewId) => {
78
114
  navigate(`${baseUrl}/${objectName}/view/${viewId}`);
79
115
  }, [navigate, baseUrl, objectName]);