@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 +136 -0
- package/dist/components/DashboardRenderer.js +4 -2
- package/dist/components/FormRenderer.js +4 -2
- package/dist/components/PageRenderer.js +4 -2
- package/dist/console/AppContent.js +5 -3
- package/dist/hooks/useNavigationSync.js +38 -36
- package/dist/hooks/useObjectActions.d.ts +11 -1
- package/dist/hooks/useObjectActions.js +44 -8
- package/dist/layout/ActivityFeed.js +21 -10
- package/dist/layout/AppHeader.js +14 -8
- package/dist/layout/AppSidebar.js +3 -3
- package/dist/layout/AppSwitcher.js +1 -1
- package/dist/layout/PageHeader.d.ts +42 -0
- package/dist/layout/PageHeader.js +13 -0
- package/dist/layout/index.d.ts +2 -0
- package/dist/layout/index.js +1 -0
- package/dist/utils/getIcon.d.ts +4 -1
- package/dist/utils/getIcon.js +11 -2
- package/dist/utils/index.d.ts +8 -1
- package/dist/utils/index.js +13 -2
- package/dist/views/ActionParamDialog.js +3 -1
- package/dist/views/CreateViewDialog.js +162 -23
- package/dist/views/DashboardView.js +75 -28
- package/dist/views/MetadataInspector.js +6 -3
- package/dist/views/ObjectView.d.ts +1 -1
- package/dist/views/ObjectView.js +296 -42
- package/dist/views/PageView.js +3 -1
- package/dist/views/RecordDetailView.js +91 -9
- package/dist/views/RecordFormPage.js +21 -2
- package/dist/views/ReportView.js +71 -17
- package/dist/views/SearchResultsPage.js +10 -5
- package/dist/views/ViewConfigPanel.js +24 -6
- package/package.json +29 -29
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:
|
|
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 || '
|
|
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:
|
|
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' ? '
|
|
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:
|
|
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 || '
|
|
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:
|
|
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:
|
|
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(
|
|
138
|
+
toast.success(t('navigationSync.addedPage', { name: label || pageName }), {
|
|
137
139
|
action: {
|
|
138
|
-
label: '
|
|
140
|
+
label: t('navigationSync.undoLabel'),
|
|
139
141
|
onClick: async () => {
|
|
140
142
|
try {
|
|
141
143
|
await saveApp(appName, { ...app, navigation: prev });
|
|
142
|
-
toast.info('
|
|
144
|
+
toast.info(t('navigationSync.undone'));
|
|
143
145
|
}
|
|
144
146
|
catch {
|
|
145
|
-
toast.error('
|
|
147
|
+
toast.error(t('navigationSync.undoFailed'));
|
|
146
148
|
}
|
|
147
149
|
},
|
|
148
150
|
},
|
|
149
151
|
});
|
|
150
152
|
}
|
|
151
153
|
catch {
|
|
152
|
-
toast.error('
|
|
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(
|
|
173
|
+
toast.success(t('navigationSync.addedDashboard', { name: label || dashboardName }), {
|
|
172
174
|
action: {
|
|
173
|
-
label: '
|
|
175
|
+
label: t('navigationSync.undoLabel'),
|
|
174
176
|
onClick: async () => {
|
|
175
177
|
try {
|
|
176
178
|
await saveApp(appName, { ...app, navigation: prev });
|
|
177
|
-
toast.info('
|
|
179
|
+
toast.info(t('navigationSync.undone'));
|
|
178
180
|
}
|
|
179
181
|
catch {
|
|
180
|
-
toast.error('
|
|
182
|
+
toast.error(t('navigationSync.undoFailed'));
|
|
181
183
|
}
|
|
182
184
|
},
|
|
183
185
|
},
|
|
184
186
|
});
|
|
185
187
|
}
|
|
186
188
|
catch {
|
|
187
|
-
toast.error('
|
|
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(
|
|
206
|
+
toast.success(t('navigationSync.removedPage', { name: pageName }), {
|
|
205
207
|
action: {
|
|
206
|
-
label: '
|
|
208
|
+
label: t('navigationSync.undoLabel'),
|
|
207
209
|
onClick: async () => {
|
|
208
210
|
try {
|
|
209
211
|
await saveApp(appName, { ...app, navigation: prev });
|
|
210
|
-
toast.info('
|
|
212
|
+
toast.info(t('navigationSync.undone'));
|
|
211
213
|
}
|
|
212
214
|
catch {
|
|
213
|
-
toast.error('
|
|
215
|
+
toast.error(t('navigationSync.undoFailed'));
|
|
214
216
|
}
|
|
215
217
|
},
|
|
216
218
|
},
|
|
217
219
|
});
|
|
218
220
|
}
|
|
219
221
|
catch {
|
|
220
|
-
toast.error('
|
|
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(
|
|
236
|
+
toast.success(t('navigationSync.removedDashboard', { name: dashboardName }), {
|
|
235
237
|
action: {
|
|
236
|
-
label: '
|
|
238
|
+
label: t('navigationSync.undoLabel'),
|
|
237
239
|
onClick: async () => {
|
|
238
240
|
try {
|
|
239
241
|
await saveApp(appName, { ...app, navigation: prev });
|
|
240
|
-
toast.info('
|
|
242
|
+
toast.info(t('navigationSync.undone'));
|
|
241
243
|
}
|
|
242
244
|
catch {
|
|
243
|
-
toast.error('
|
|
245
|
+
toast.error(t('navigationSync.undoFailed'));
|
|
244
246
|
}
|
|
245
247
|
},
|
|
246
248
|
},
|
|
247
249
|
});
|
|
248
250
|
}
|
|
249
251
|
catch {
|
|
250
|
-
toast.error('
|
|
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(
|
|
269
|
+
toast.success(t('navigationSync.renamedPage', { oldName, newName }), {
|
|
268
270
|
action: {
|
|
269
|
-
label: '
|
|
271
|
+
label: t('navigationSync.undoLabel'),
|
|
270
272
|
onClick: async () => {
|
|
271
273
|
try {
|
|
272
274
|
await saveApp(appName, { ...app, navigation: prev });
|
|
273
|
-
toast.info('
|
|
275
|
+
toast.info(t('navigationSync.undone'));
|
|
274
276
|
}
|
|
275
277
|
catch {
|
|
276
|
-
toast.error('
|
|
278
|
+
toast.error(t('navigationSync.undoFailed'));
|
|
277
279
|
}
|
|
278
280
|
},
|
|
279
281
|
},
|
|
280
282
|
});
|
|
281
283
|
}
|
|
282
284
|
catch {
|
|
283
|
-
toast.error('
|
|
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(
|
|
299
|
+
toast.success(t('navigationSync.renamedDashboard', { oldName, newName }), {
|
|
298
300
|
action: {
|
|
299
|
-
label: '
|
|
301
|
+
label: t('navigationSync.undoLabel'),
|
|
300
302
|
onClick: async () => {
|
|
301
303
|
try {
|
|
302
304
|
await saveApp(appName, { ...app, navigation: prev });
|
|
303
|
-
toast.info('
|
|
305
|
+
toast.info(t('navigationSync.undone'));
|
|
304
306
|
}
|
|
305
307
|
catch {
|
|
306
|
-
toast.error('
|
|
308
|
+
toast.error(t('navigationSync.undoFailed'));
|
|
307
309
|
}
|
|
308
310
|
},
|
|
309
311
|
},
|
|
310
312
|
});
|
|
311
313
|
}
|
|
312
314
|
catch {
|
|
313
|
-
toast.error('
|
|
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
|
-
|
|
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: '
|
|
75
|
+
return { success: false, error: t('objectActions.noRecordId') };
|
|
40
76
|
try {
|
|
41
77
|
await dataSource.delete(objectName, recordId);
|
|
42
78
|
onRefresh?.();
|
|
43
|
-
toast.success(
|
|
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(
|
|
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:
|
|
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]);
|