@object-ui/app-shell 4.0.5 → 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 +134 -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 +1 -1
- package/dist/hooks/useNavigationSync.js +38 -36
- package/dist/hooks/useObjectActions.d.ts +11 -1
- package/dist/hooks/useObjectActions.js +20 -8
- package/dist/layout/ActivityFeed.js +21 -10
- package/dist/layout/AppSidebar.js +2 -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/DashboardView.js +6 -6
- package/dist/views/MetadataInspector.js +6 -3
- package/dist/views/ObjectView.js +44 -15
- package/dist/views/PageView.js +3 -1
- package/dist/views/RecordDetailView.js +2 -2
- package/dist/views/RecordFormPage.js +1 -1
- package/dist/views/ReportView.js +3 -1
- package/dist/views/SearchResultsPage.js +10 -5
- package/package.json +24 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,139 @@
|
|
|
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
|
+
|
|
92
|
+
## 4.0.6
|
|
93
|
+
|
|
94
|
+
### Patch Changes
|
|
95
|
+
|
|
96
|
+
- 925051d: fix: convert Tailwind v3 `[--var]` arbitrary value syntax to v4 `(--var)`
|
|
97
|
+
|
|
98
|
+
Shadcn `Sidebar`, `Calendar`, `Chart`, `Popover`, `Tooltip`, `HoverCard`,
|
|
99
|
+
`Menubar`, `Select`, `Dropdown`, `Context-Menu`, and `AppSidebar` used the
|
|
100
|
+
Tailwind v3 syntax `w-[--sidebar-width]`, `origin-[--radix-...]`, etc.
|
|
101
|
+
Tailwind v4 no longer interprets the bare `--xxx` inside arbitrary values
|
|
102
|
+
as `var(--xxx)`, so the rule emits empty CSS — the sidebar collapses to
|
|
103
|
+
0 width and overlays the main content, dropdown/popover positions fall
|
|
104
|
+
back to the wrong origin, and the calendar cells lose their fixed size.
|
|
105
|
+
|
|
106
|
+
Replaced all such occurrences with the v4 CSS-variable shorthand
|
|
107
|
+
`w-(--sidebar-width)`, `origin-(--radix-...)`, etc. Existing
|
|
108
|
+
`[calc(var(--xxx)*-1)]` arbitrary expressions are unaffected.
|
|
109
|
+
|
|
110
|
+
- Updated dependencies [89ae109]
|
|
111
|
+
- Updated dependencies [925051d]
|
|
112
|
+
- Updated dependencies [1b6dc64]
|
|
113
|
+
- @object-ui/plugin-grid@4.0.6
|
|
114
|
+
- @object-ui/plugin-form@4.0.6
|
|
115
|
+
- @object-ui/fields@4.0.6
|
|
116
|
+
- @object-ui/components@4.0.6
|
|
117
|
+
- @object-ui/plugin-chatbot@4.0.6
|
|
118
|
+
- @object-ui/plugin-designer@4.0.6
|
|
119
|
+
- @object-ui/plugin-kanban@4.0.6
|
|
120
|
+
- @object-ui/plugin-view@4.0.6
|
|
121
|
+
- @object-ui/plugin-calendar@4.0.6
|
|
122
|
+
- @object-ui/plugin-detail@4.0.6
|
|
123
|
+
- @object-ui/plugin-report@4.0.6
|
|
124
|
+
- @object-ui/layout@4.0.6
|
|
125
|
+
- @object-ui/plugin-charts@4.0.6
|
|
126
|
+
- @object-ui/plugin-dashboard@4.0.6
|
|
127
|
+
- @object-ui/plugin-list@4.0.6
|
|
128
|
+
- @object-ui/types@4.0.6
|
|
129
|
+
- @object-ui/core@4.0.6
|
|
130
|
+
- @object-ui/i18n@4.0.6
|
|
131
|
+
- @object-ui/react@4.0.6
|
|
132
|
+
- @object-ui/data-objectstack@4.0.6
|
|
133
|
+
- @object-ui/auth@4.0.6
|
|
134
|
+
- @object-ui/permissions@4.0.6
|
|
135
|
+
- @object-ui/collaboration@4.0.6
|
|
136
|
+
|
|
3
137
|
## 4.0.5
|
|
4
138
|
|
|
5
139
|
### 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,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:
|
|
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(
|
|
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,25 @@ 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
|
|
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: '
|
|
51
|
+
return { success: false, error: t('objectActions.noRecordId') };
|
|
40
52
|
try {
|
|
41
53
|
await dataSource.delete(objectName, recordId);
|
|
42
54
|
onRefresh?.();
|
|
43
|
-
toast.success(
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
32
|
+
return t('layout.activityFeed.relativeMinutesAgo', { count: minutes });
|
|
30
33
|
const hours = Math.floor(minutes / 60);
|
|
31
34
|
if (hours < 24)
|
|
32
|
-
return
|
|
35
|
+
return t('layout.activityFeed.relativeHoursAgo', { count: hours });
|
|
33
36
|
const days = Math.floor(hours / 24);
|
|
34
|
-
return
|
|
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
|
-
|
|
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
|
|
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:
|
|
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,14 +177,14 @@ 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-
|
|
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));
|
|
184
184
|
}) }) })] })), _jsx(SidebarGroup, { className: "py-0", children: _jsxs(SidebarGroupContent, { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-70" }), _jsx(SidebarInput, { placeholder: "Search navigation...", value: navSearchQuery, onChange: (e) => setNavSearchQuery(e.target.value), className: "pl-8" })] }) }), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, searchQuery: navSearchQuery, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), "Recent"] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), "Favorites"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": `Remove ${item.label} from favorites`, children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(SidebarGroup, { "data-testid": "system-fallback-nav", children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Settings, { className: "h-3.5 w-3.5" }), "System"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: systemFallbackNavigation.map((item) => {
|
|
185
185
|
const NavIcon = getIcon(item.icon);
|
|
186
186
|
return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.url || '/system', children: [_jsx(NavIcon, { className: "h-4 w-4" }), _jsx("span", { children: item.label })] }) }) }, item.id));
|
|
187
|
-
}) }) })] })) }), _jsx(SidebarFooter, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _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: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] }), _jsx(ChevronsUpDown, { className: "ml-auto size-4" })] }) }), _jsxs(DropdownMenuContent, { className: "w-
|
|
187
|
+
}) }) })] })) }), _jsx(SidebarFooter, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _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: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] }), _jsx(ChevronsUpDown, { className: "ml-auto size-4" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", side: isMobile ? "bottom" : "right", align: "end", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-1 py-1.5 text-left text-sm", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), "Settings"] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), "Log out"] })] })] }) }) }) })] }), isMobile && (_jsx("div", { className: "fixed bottom-0 left-0 right-0 z-50 flex items-center justify-around border-t bg-background/95 backdrop-blur-sm px-2 py-1 sm:hidden safe-area-bottom", children: (activeApp ? resolvedNavigation : systemFallbackNavigation).filter((n) => n.type !== 'group').slice(0, 5).map((item) => {
|
|
188
188
|
const NavIcon = getIcon(item.icon);
|
|
189
189
|
const baseUrl = activeApp ? `/apps/${activeAppName}` : '';
|
|
190
190
|
let href = item.url || '#';
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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 |
|
|
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
|
package/dist/utils/index.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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:
|
|
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('
|
|
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(
|
|
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('
|
|
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:
|
|
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 || '
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
/**
|
package/dist/views/ObjectView.js
CHANGED
|
@@ -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('
|
|
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('
|
|
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,
|
|
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
|
-
} }))] })] }),
|
|
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
|
package/dist/views/PageView.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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',
|
package/dist/views/ReportView.js
CHANGED
|
@@ -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:
|
|
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" }),
|
|
99
|
-
?
|
|
100
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
31
|
-
"@object-ui/collaboration": "4.0.
|
|
32
|
-
"@object-ui/components": "4.0.
|
|
33
|
-
"@object-ui/core": "4.0.
|
|
34
|
-
"@object-ui/data-objectstack": "4.0.
|
|
35
|
-
"@object-ui/fields": "4.0.
|
|
36
|
-
"@object-ui/i18n": "4.0.
|
|
37
|
-
"@object-ui/layout": "4.0.
|
|
38
|
-
"@object-ui/permissions": "4.0.
|
|
39
|
-
"@object-ui/react": "4.0.
|
|
40
|
-
"@object-ui/types": "4.0.
|
|
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.
|
|
47
|
-
"@object-ui/plugin-charts": "4.0.
|
|
48
|
-
"@object-ui/plugin-chatbot": "4.0.
|
|
49
|
-
"@object-ui/plugin-dashboard": "4.0.
|
|
50
|
-
"@object-ui/plugin-designer": "4.0.
|
|
51
|
-
"@object-ui/plugin-detail": "4.0.
|
|
52
|
-
"@object-ui/plugin-form": "4.0.
|
|
53
|
-
"@object-ui/plugin-grid": "4.0.
|
|
54
|
-
"@object-ui/plugin-kanban": "4.0.
|
|
55
|
-
"@object-ui/plugin-list": "4.0.
|
|
56
|
-
"@object-ui/plugin-report": "4.0.
|
|
57
|
-
"@object-ui/plugin-view": "4.0.
|
|
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",
|