@object-ui/app-shell 4.0.1 → 4.0.4
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 +99 -0
- package/README.md +54 -0
- package/dist/console/AppContent.js +93 -37
- package/dist/console/ConsoleShell.js +4 -1
- package/dist/console/home/HomeLayout.js +12 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/layout/ConsoleFloatingChatbot.d.ts +8 -1
- package/dist/layout/ConsoleFloatingChatbot.js +79 -10
- package/dist/layout/ConsoleLayout.js +5 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/recordFormNavigation.d.ts +130 -0
- package/dist/utils/recordFormNavigation.js +96 -0
- package/dist/views/RecordFormPage.d.ts +42 -0
- package/dist/views/RecordFormPage.js +171 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +1 -0
- package/package.json +24 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,104 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [d2b6ece]
|
|
8
|
+
- @object-ui/components@4.0.4
|
|
9
|
+
- @object-ui/fields@4.0.4
|
|
10
|
+
- @object-ui/layout@4.0.4
|
|
11
|
+
- @object-ui/plugin-calendar@4.0.4
|
|
12
|
+
- @object-ui/plugin-charts@4.0.4
|
|
13
|
+
- @object-ui/plugin-chatbot@4.0.4
|
|
14
|
+
- @object-ui/plugin-dashboard@4.0.4
|
|
15
|
+
- @object-ui/plugin-designer@4.0.4
|
|
16
|
+
- @object-ui/plugin-detail@4.0.4
|
|
17
|
+
- @object-ui/plugin-form@4.0.4
|
|
18
|
+
- @object-ui/plugin-grid@4.0.4
|
|
19
|
+
- @object-ui/plugin-kanban@4.0.4
|
|
20
|
+
- @object-ui/plugin-list@4.0.4
|
|
21
|
+
- @object-ui/plugin-report@4.0.4
|
|
22
|
+
- @object-ui/plugin-view@4.0.4
|
|
23
|
+
- @object-ui/types@4.0.4
|
|
24
|
+
- @object-ui/core@4.0.4
|
|
25
|
+
- @object-ui/i18n@4.0.4
|
|
26
|
+
- @object-ui/react@4.0.4
|
|
27
|
+
- @object-ui/data-objectstack@4.0.4
|
|
28
|
+
- @object-ui/auth@4.0.4
|
|
29
|
+
- @object-ui/permissions@4.0.4
|
|
30
|
+
- @object-ui/collaboration@4.0.4
|
|
31
|
+
|
|
32
|
+
## 4.0.3
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- 4be43e2: **Page-mode record forms (`editMode: 'page'`).** New per-object metadata flag that opts a record's create/edit form into a dedicated full-screen route (`/apps/:appName/:objectName/new`, `/apps/:appName/:objectName/record/:recordId/edit`). Two new declarative actions `navigate_create` and `navigate_edit` open these routes from JSON action buttons. Default modal behavior is preserved for objects that do not set `editMode`.
|
|
37
|
+
|
|
38
|
+
**`@object-ui/plugin-list` & `@object-ui/plugin-detail`: `ComponentRegistry` singleton fix.** Both plugins' Vite configs now mark all `@object-ui/*` packages as external so each plugin no longer bundles its own private copy of `@object-ui/core`. Cross-plugin component lookups now resolve correctly from the same singleton registry. `plugin-list` dist shrank from multi-MB to 67 kB (gzip 16 kB); `plugin-detail` to 124 kB (gzip 28 kB).
|
|
39
|
+
|
|
40
|
+
**`@object-ui/app-shell` `CreateViewDialog` churn fix.** `existingSet` is now memoised on the joined string key of `existingLabels` rather than the raw array reference, preventing the name-suggest `useEffect` from re-firing on every parent render.
|
|
41
|
+
|
|
42
|
+
**CI fixes.** `ReportViewer` conditional-formatting test now accepts both `rgb(...)` and hex color representations. `ObjectView` i18n mocks rewritten to mirror the real hook shapes (`useObjectTranslation`, `useObjectLabel`).
|
|
43
|
+
|
|
44
|
+
- Updated dependencies [4be43e2]
|
|
45
|
+
- @object-ui/types@4.0.3
|
|
46
|
+
- @object-ui/core@4.0.3
|
|
47
|
+
- @object-ui/i18n@4.0.3
|
|
48
|
+
- @object-ui/react@4.0.3
|
|
49
|
+
- @object-ui/components@4.0.3
|
|
50
|
+
- @object-ui/fields@4.0.3
|
|
51
|
+
- @object-ui/layout@4.0.3
|
|
52
|
+
- @object-ui/data-objectstack@4.0.3
|
|
53
|
+
- @object-ui/auth@4.0.3
|
|
54
|
+
- @object-ui/permissions@4.0.3
|
|
55
|
+
- @object-ui/plugin-calendar@4.0.3
|
|
56
|
+
- @object-ui/plugin-charts@4.0.3
|
|
57
|
+
- @object-ui/plugin-chatbot@4.0.3
|
|
58
|
+
- @object-ui/plugin-dashboard@4.0.3
|
|
59
|
+
- @object-ui/plugin-designer@4.0.3
|
|
60
|
+
- @object-ui/plugin-detail@4.0.3
|
|
61
|
+
- @object-ui/plugin-form@4.0.3
|
|
62
|
+
- @object-ui/plugin-grid@4.0.3
|
|
63
|
+
- @object-ui/plugin-kanban@4.0.3
|
|
64
|
+
- @object-ui/plugin-list@4.0.3
|
|
65
|
+
- @object-ui/plugin-report@4.0.3
|
|
66
|
+
- @object-ui/plugin-view@4.0.3
|
|
67
|
+
- @object-ui/collaboration@4.0.3
|
|
68
|
+
|
|
69
|
+
## Unreleased
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- **Page-mode record forms.** Objects can now opt into a route-driven
|
|
74
|
+
full-screen create/edit experience by setting `editMode: 'page'` on the
|
|
75
|
+
object metadata (default remains `'modal'`). When opted in, the
|
|
76
|
+
console mounts two new routes under `/apps/:appName/`:
|
|
77
|
+
- `:objectName/new` for create
|
|
78
|
+
- `:objectName/record/:recordId/edit` for edit
|
|
79
|
+
|
|
80
|
+
URLs are deep-linkable, refresh-safe, and respect the browser back
|
|
81
|
+
button. The new `RecordFormPage` view renders inside the existing
|
|
82
|
+
`ConsoleLayout` chrome and reuses the same `<ObjectForm>` pipeline as
|
|
83
|
+
the modal flow, so every existing form configuration (sections,
|
|
84
|
+
visibility expressions, validations, `formType: 'tabbed' | 'wizard'`,
|
|
85
|
+
…) works without changes.
|
|
86
|
+
|
|
87
|
+
Two declarative actions expose the routes for `<action:button>` JSON:
|
|
88
|
+
- `{ "action": "navigate_create", "params": { "objectName": "..." } }`
|
|
89
|
+
- `{ "action": "navigate_edit", "params": { "objectName": "...", "recordId": "..." } }`
|
|
90
|
+
|
|
91
|
+
When called from inside an `ObjectView` the `objectName` falls back to
|
|
92
|
+
the action context, so it can be omitted from the params.
|
|
93
|
+
|
|
94
|
+
See `content/docs/guide/record-edit-modes.md` for a walkthrough.
|
|
95
|
+
- New view: `packages/app-shell/src/views/RecordFormPage.tsx`
|
|
96
|
+
- New helpers: `resolveRecordFormTarget`, `resolveNavigateCreateUrl`,
|
|
97
|
+
`resolveNavigateEditUrl` in
|
|
98
|
+
`packages/app-shell/src/utils/recordFormNavigation.ts`
|
|
99
|
+
- Tests: `RecordFormPage.test.tsx` (6) and
|
|
100
|
+
`recordFormNavigation.test.ts` (22), all passing.
|
|
101
|
+
|
|
3
102
|
## 4.0.1
|
|
4
103
|
|
|
5
104
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -175,6 +175,60 @@ See `examples/byo-backend-console` for a complete working example that demonstra
|
|
|
175
175
|
- Cherry-picking only needed components
|
|
176
176
|
- Building a console in ~100 lines of code
|
|
177
177
|
|
|
178
|
+
## Record create/edit modes
|
|
179
|
+
|
|
180
|
+
The default `<DefaultAppContent>` shell mounts a global `<ModalForm>` for
|
|
181
|
+
record create/edit interactions. Each object can opt in to a route-driven
|
|
182
|
+
full-screen experience instead by setting `editMode` on its metadata:
|
|
183
|
+
|
|
184
|
+
```jsonc
|
|
185
|
+
// objects/account.json
|
|
186
|
+
{
|
|
187
|
+
"name": "account",
|
|
188
|
+
"label": "Account",
|
|
189
|
+
"editMode": "page", // ← opt-in. Default is "modal".
|
|
190
|
+
"fields": { /* ... */ }
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
When `editMode: 'page'` is set, clicking **Create** or **Edit** for an
|
|
195
|
+
`account` record navigates to a dedicated route instead of opening the
|
|
196
|
+
dialog:
|
|
197
|
+
|
|
198
|
+
| Action | URL |
|
|
199
|
+
|--------|-----|
|
|
200
|
+
| Create | `/apps/:appName/account/new` |
|
|
201
|
+
| Edit | `/apps/:appName/account/record/:recordId/edit` |
|
|
202
|
+
|
|
203
|
+
These routes are deep-linkable (refresh-safe), respect the browser back
|
|
204
|
+
button, and render the same `<ObjectForm>` pipeline as the modal — so
|
|
205
|
+
`tabbed`, `wizard`, and section configurations work in both modes.
|
|
206
|
+
|
|
207
|
+
JSON `<action:button>` schemas can also trigger the page routes directly
|
|
208
|
+
via the action runner, regardless of the object's `editMode`:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"type": "action:button",
|
|
213
|
+
"label": "New Account",
|
|
214
|
+
"action": { "action": "navigate_create", "params": { "objectName": "account" } }
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"type": "action:button",
|
|
221
|
+
"label": "Edit",
|
|
222
|
+
"action": {
|
|
223
|
+
"action": "navigate_edit",
|
|
224
|
+
"params": { "objectName": "account", "recordId": "${record.id}" }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
See [`content/docs/guide/record-edit-modes.md`](../../content/docs/guide/record-edit-modes.md)
|
|
230
|
+
for a longer walkthrough.
|
|
231
|
+
|
|
178
232
|
<!-- release-metadata:v3.3.0 -->
|
|
179
233
|
|
|
180
234
|
## Compatibility
|
|
@@ -13,13 +13,14 @@ import { useState, useEffect, useCallback, lazy, Suspense, useMemo } from 'react
|
|
|
13
13
|
import { ModalForm } from '@object-ui/plugin-form';
|
|
14
14
|
import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
|
|
15
15
|
import { toast } from 'sonner';
|
|
16
|
-
import {
|
|
16
|
+
import { useActionRunner, useGlobalUndo } from '@object-ui/react';
|
|
17
17
|
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
18
18
|
import { useAuth } from '@object-ui/auth';
|
|
19
19
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
20
20
|
import { useAdapter } from '../providers/AdapterProvider';
|
|
21
21
|
import { ExpressionProvider, evaluateVisibility } from '../providers/ExpressionProvider';
|
|
22
22
|
import { useRecentItems } from '../hooks/useRecentItems';
|
|
23
|
+
import { resolveRecordFormTarget, resolveNavigateCreateUrl, resolveNavigateEditUrl } from '../utils/recordFormNavigation';
|
|
23
24
|
import { ExpressionEvaluator } from '@object-ui/core';
|
|
24
25
|
// Components (eagerly loaded — always needed)
|
|
25
26
|
import { ConsoleLayout } from '../layout/ConsoleLayout';
|
|
@@ -36,6 +37,7 @@ const DashboardView = lazy(() => import('../views/DashboardView').then(m => ({ d
|
|
|
36
37
|
const PageView = lazy(() => import('../views/PageView').then(m => ({ default: m.PageView })));
|
|
37
38
|
const ReportView = lazy(() => import('../views/ReportView').then(m => ({ default: m.ReportView })));
|
|
38
39
|
const SearchResultsPage = lazy(() => import('../views/SearchResultsPage').then(m => ({ default: m.SearchResultsPage })));
|
|
40
|
+
const RecordFormPage = lazy(() => import('../views/RecordFormPage').then(m => ({ default: m.RecordFormPage })));
|
|
39
41
|
// Designer pages — sourced from @object-ui/plugin-designer so third-party hosts
|
|
40
42
|
// can opt out by not registering these routes.
|
|
41
43
|
const CreateAppPage = lazy(() => import('@object-ui/plugin-designer').then(m => ({ default: m.CreateAppPage })));
|
|
@@ -104,13 +106,54 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
104
106
|
setIsDialogOpen(false);
|
|
105
107
|
return { success: true };
|
|
106
108
|
});
|
|
109
|
+
// Page-mode navigation handlers — declarative counterparts to the
|
|
110
|
+
// imperative `handleEdit` callback. These let JSON schemas open the
|
|
111
|
+
// full-screen create/edit pages directly via `<action:button>` without
|
|
112
|
+
// any custom code:
|
|
113
|
+
// { "action": "navigate_create", "params": { "objectName": "..." } }
|
|
114
|
+
// { "action": "navigate_edit",
|
|
115
|
+
// "params": { "objectName": "...", "recordId": "..." } }
|
|
116
|
+
// The `objectName` param falls back to the action context's
|
|
117
|
+
// `objectName` (set per view) so action buttons mounted inside an
|
|
118
|
+
// ObjectView can omit it.
|
|
119
|
+
// NOTE on duplication below: each handler reads `runner.getContext()`
|
|
120
|
+
// INSIDE its closure (at action-invocation time) rather than once at
|
|
121
|
+
// registration. Hoisting the call outside the registrations would
|
|
122
|
+
// freeze the context to whatever it was when the effect last ran,
|
|
123
|
+
// breaking dynamic per-view `runner.updateContext({ objectName, ... })`
|
|
124
|
+
// calls (used by ObjectView / RecordDetailView). Keep the call where
|
|
125
|
+
// it is.
|
|
126
|
+
runner.registerHandler('navigate_create', async (action) => {
|
|
127
|
+
const ctx = runner.getContext?.() ?? {};
|
|
128
|
+
const result = resolveNavigateCreateUrl({
|
|
129
|
+
action,
|
|
130
|
+
context: ctx,
|
|
131
|
+
defaultBaseUrl: `/apps/${appName ?? ''}`,
|
|
132
|
+
});
|
|
133
|
+
if (!result.success)
|
|
134
|
+
return result;
|
|
135
|
+
navigate(result.url);
|
|
136
|
+
return { success: true };
|
|
137
|
+
});
|
|
138
|
+
runner.registerHandler('navigate_edit', async (action) => {
|
|
139
|
+
const ctx = runner.getContext?.() ?? {};
|
|
140
|
+
const result = resolveNavigateEditUrl({
|
|
141
|
+
action,
|
|
142
|
+
context: ctx,
|
|
143
|
+
defaultBaseUrl: `/apps/${appName ?? ''}`,
|
|
144
|
+
});
|
|
145
|
+
if (!result.success)
|
|
146
|
+
return result;
|
|
147
|
+
navigate(result.url);
|
|
148
|
+
return { success: true };
|
|
149
|
+
});
|
|
107
150
|
// NOTE: `flow` actions are handled at the per-view ActionProvider level
|
|
108
151
|
// (RecordDetailView / ObjectView) so they share the same ActionRunner that
|
|
109
152
|
// <action:button> renderers consume via useAction(). Do NOT register a
|
|
110
153
|
// `flow` handler on this top-level useActionRunner — it lives on a
|
|
111
154
|
// different ActionRunner instance and would never be invoked from the
|
|
112
155
|
// record/list action buttons.
|
|
113
|
-
}, [runner]);
|
|
156
|
+
}, [runner, navigate, appName]);
|
|
114
157
|
useEffect(() => {
|
|
115
158
|
if (!dataSource)
|
|
116
159
|
return;
|
|
@@ -185,6 +228,19 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
185
228
|
}
|
|
186
229
|
}, [location.pathname, addRecentItem]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
187
230
|
const handleEdit = (record) => {
|
|
231
|
+
// Page-mode opt-in: when the object metadata declares
|
|
232
|
+
// `editMode: 'page'`, route to the full-screen create/edit page instead
|
|
233
|
+
// of opening the global ModalForm. Default behavior (modal) is
|
|
234
|
+
// preserved for any object without the flag.
|
|
235
|
+
const target = resolveRecordFormTarget({
|
|
236
|
+
objectDef: currentObjectDef,
|
|
237
|
+
baseUrl: activeApp?.name ? `/apps/${activeApp.name}` : '',
|
|
238
|
+
record,
|
|
239
|
+
});
|
|
240
|
+
if (target.kind === 'page') {
|
|
241
|
+
navigate(target.url);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
188
244
|
setEditingRecord(record);
|
|
189
245
|
setIsDialogOpen(true);
|
|
190
246
|
};
|
|
@@ -208,41 +264,41 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
208
264
|
const expressionUser = user
|
|
209
265
|
? { name: user.name, email: user.email, role: user.role ?? 'user' }
|
|
210
266
|
: { name: 'Anonymous', email: '', role: 'guest' };
|
|
211
|
-
return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}),
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
267
|
+
return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}), _jsx(ErrorBoundary, { children: _jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: findFirstRoute(activeApp.navigation || []), replace: true }) }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }, refreshKey) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "design/page/:pageName", element: _jsx(PageDesignPage, {}) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), extraRoutes] }) }) }), currentObjectDef && (_jsx(ModalForm, { schema: {
|
|
268
|
+
type: 'object-form',
|
|
269
|
+
formType: 'modal',
|
|
270
|
+
objectName: currentObjectDef.name,
|
|
271
|
+
mode: editingRecord ? 'edit' : 'create',
|
|
272
|
+
recordId: editingRecord?.id,
|
|
273
|
+
title: editingRecord
|
|
274
|
+
? t('form.editTitle', { object: objectLabel(currentObjectDef) })
|
|
275
|
+
: t('form.createTitle', { object: objectLabel(currentObjectDef) }),
|
|
276
|
+
description: editingRecord
|
|
277
|
+
? t('form.editDescription', { object: objectLabel(currentObjectDef) })
|
|
278
|
+
: t('form.createDescription', { object: objectLabel(currentObjectDef) }),
|
|
279
|
+
open: isDialogOpen,
|
|
280
|
+
onOpenChange: setIsDialogOpen,
|
|
281
|
+
layout: 'vertical',
|
|
282
|
+
fields: currentObjectDef.fields
|
|
283
|
+
? (Array.isArray(currentObjectDef.fields)
|
|
284
|
+
? currentObjectDef.fields
|
|
285
|
+
.filter((f) => {
|
|
286
|
+
if (typeof f === 'string')
|
|
287
|
+
return true;
|
|
288
|
+
return evaluateVisibility(f.visible, expressionEvaluator);
|
|
289
|
+
})
|
|
290
|
+
.map((f) => typeof f === 'string' ? f : f.name)
|
|
291
|
+
: Object.entries(currentObjectDef.fields)
|
|
292
|
+
.filter(([_, f]) => evaluateVisibility(f.visible, expressionEvaluator))
|
|
293
|
+
.map(([key]) => key))
|
|
294
|
+
: [],
|
|
295
|
+
onSuccess: handleCrudSuccess,
|
|
296
|
+
onCancel: handleDialogCancel,
|
|
297
|
+
showSubmit: true,
|
|
298
|
+
showCancel: true,
|
|
299
|
+
submitText: t('form.saveRecord'),
|
|
300
|
+
cancelText: t('common.cancel'),
|
|
301
|
+
}, dataSource: dataSource }, editingRecord?.id || 'new'))] })] }));
|
|
246
302
|
}
|
|
247
303
|
function findFirstRoute(items) {
|
|
248
304
|
if (!items || items.length === 0)
|
|
@@ -13,6 +13,7 @@ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
|
13
13
|
import { Suspense } from 'react';
|
|
14
14
|
import { Navigate, useLocation } from 'react-router-dom';
|
|
15
15
|
import { AuthGuard, useAuth } from '@object-ui/auth';
|
|
16
|
+
import { SchemaRendererProvider } from '@object-ui/react';
|
|
16
17
|
import { AdapterProvider, useAdapter } from '../providers/AdapterProvider';
|
|
17
18
|
import { MetadataProvider, useMetadata } from '../providers/MetadataProvider';
|
|
18
19
|
import { NavigationProvider } from '../context/NavigationContext';
|
|
@@ -50,7 +51,9 @@ function ConnectedShellInner({ children }) {
|
|
|
50
51
|
const adapter = useAdapter();
|
|
51
52
|
if (!adapter)
|
|
52
53
|
return _jsx(LoadingFallback, {});
|
|
53
|
-
|
|
54
|
+
// Expose the adapter via SchemaRendererContext so descendant hooks like
|
|
55
|
+
// useDiscovery() (used to gate the global AI chatbot) can resolve it.
|
|
56
|
+
return (_jsx(SchemaRendererProvider, { dataSource: adapter, children: _jsx(MetadataProvider, { adapter: adapter, children: children }) }));
|
|
54
57
|
}
|
|
55
58
|
/**
|
|
56
59
|
* RequireOrganization — redirects to /organizations when the multi-tenant
|
|
@@ -8,13 +8,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
|
-
import { useEffect } from 'react';
|
|
11
|
+
import { useEffect, Suspense, lazy } from 'react';
|
|
12
12
|
import { useNavigationContext } from '../../context/NavigationContext';
|
|
13
13
|
import { AppHeader } from '../../layout/AppHeader';
|
|
14
|
+
import { useDiscovery } from '@object-ui/react';
|
|
15
|
+
// Lazy-load the chatbot so its heavy markdown deps stay out of the initial
|
|
16
|
+
// paint until the AI assistant is actually enabled.
|
|
17
|
+
const ConsoleFloatingChatbot = lazy(() => import('../../layout/ConsoleFloatingChatbot'));
|
|
14
18
|
export function HomeLayout({ children }) {
|
|
15
19
|
const { setContext } = useNavigationContext();
|
|
20
|
+
const { isAiEnabled } = useDiscovery();
|
|
21
|
+
// Render the chatbot whenever AI is reachable. If the developer has explicitly
|
|
22
|
+
// configured `VITE_AI_BASE_URL`, trust that opt-in even when discovery
|
|
23
|
+
// reports AI as disabled (e.g. framework started without `--preset full`).
|
|
24
|
+
const aiBaseUrlConfigured = Boolean(import.meta.env?.VITE_AI_BASE_URL);
|
|
25
|
+
const showChatbot = isAiEnabled || aiBaseUrlConfigured;
|
|
16
26
|
useEffect(() => {
|
|
17
27
|
setContext('home');
|
|
18
28
|
}, [setContext]);
|
|
19
|
-
return (_jsxs("div", { className: "flex min-h-svh w-full flex-col bg-background", "data-testid": "home-layout", children: [_jsx("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: _jsx(AppHeader, { variant: "home" }) }), _jsx("main", { className: "flex-1 min-w-0 overflow-auto", children: children })] }));
|
|
29
|
+
return (_jsxs("div", { className: "flex min-h-svh w-full flex-col bg-background", "data-testid": "home-layout", children: [_jsx("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: _jsx(AppHeader, { variant: "home" }) }), _jsx("main", { className: "flex-1 min-w-0 overflow-auto", children: children }), showChatbot && (_jsx(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { appLabel: "Workspace", objects: [] }) }))] }));
|
|
20
30
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,8 @@ export { ConsoleShell, ConnectedShell, RequireOrganization, AuthenticatedRoute,
|
|
|
22
22
|
export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, ConnectionStatus, ActivityFeed, LocaleSwitcher, ModeToggle, AuthPageLayout, } from './layout';
|
|
23
23
|
export type { ActivityItem } from './layout';
|
|
24
24
|
export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
|
|
25
|
-
export { ObjectView, RecordDetailView, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
|
|
25
|
+
export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
|
|
26
|
+
export type { RecordFormPageProps } from './views';
|
|
26
27
|
export { useFavorites, useMetadataService, useNavPins, useNavigationSync, NavigationSyncEffect, addNavigationItem, removeNavigationItems, renameNavigationItems, navigationEqual, generateNavId, useResponsiveSidebar, } from './hooks';
|
|
27
28
|
export type { FavoriteItem } from './hooks';
|
|
28
29
|
export { NavigationProvider, useNavigationContext, FavoritesProvider } from './context';
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, Conn
|
|
|
25
25
|
// Top-level chrome (dialogs, providers, error boundaries)
|
|
26
26
|
export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
|
|
27
27
|
// Standard inner-SPA views
|
|
28
|
-
export { ObjectView, RecordDetailView, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
|
|
28
|
+
export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
|
|
29
29
|
// Hooks
|
|
30
30
|
export { useFavorites, useMetadataService, useNavPins, useNavigationSync, NavigationSyncEffect, addNavigationItem, removeNavigationItems, renameNavigationItems, navigationEqual, generateNavId, useResponsiveSidebar, } from './hooks';
|
|
31
31
|
// Context providers
|
|
@@ -5,6 +5,13 @@ interface ConsoleObject {
|
|
|
5
5
|
export interface ConsoleFloatingChatbotProps {
|
|
6
6
|
appLabel: string;
|
|
7
7
|
objects: ConsoleObject[];
|
|
8
|
+
/**
|
|
9
|
+
* Base URL of the AI service. Defaults to `${VITE_SERVER_URL}/api/v1/ai`
|
|
10
|
+
* (or the relative `/api/v1/ai` when no server URL is configured).
|
|
11
|
+
*/
|
|
12
|
+
apiBase?: string;
|
|
13
|
+
/** Default agent name to select on first render. */
|
|
14
|
+
defaultAgent?: string;
|
|
8
15
|
}
|
|
9
|
-
export default function ConsoleFloatingChatbot({ appLabel, objects }: ConsoleFloatingChatbotProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, }: ConsoleFloatingChatbotProps): import("react/jsx-runtime").JSX.Element;
|
|
10
17
|
export {};
|
|
@@ -1,27 +1,96 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ConsoleFloatingChatbot
|
|
4
|
+
*
|
|
5
|
+
* Wires the global FAB chatbot to the framework's `@objectstack/service-ai`
|
|
6
|
+
* backend (the `/api/v1/ai/agents/:agentName/chat` Vercel Data Stream
|
|
7
|
+
* endpoint) and exposes an in-header agent picker.
|
|
8
|
+
*
|
|
9
|
+
* The chatbot pulls in `react-markdown` + `micromark` (~150 KB) which is
|
|
10
|
+
* unused on every page until the AI assistant is enabled, so deferring it
|
|
11
|
+
* keeps those bytes off the initial paint.
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { FloatingChatbot, useObjectChat, useAgents, } from '@object-ui/plugin-chatbot';
|
|
16
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
|
|
17
|
+
const DEFAULT_AI_PATH = '/api/v1/ai';
|
|
18
|
+
function resolveApiBase(explicit) {
|
|
19
|
+
if (explicit)
|
|
20
|
+
return explicit.replace(/\/$/, '');
|
|
21
|
+
const env = import.meta.env ?? {};
|
|
22
|
+
const fromEnv = env.VITE_AI_BASE_URL;
|
|
23
|
+
if (fromEnv)
|
|
24
|
+
return fromEnv.replace(/\/$/, '');
|
|
25
|
+
const serverUrl = env.VITE_SERVER_URL ?? '';
|
|
26
|
+
return `${serverUrl.replace(/\/$/, '')}${DEFAULT_AI_PATH}`;
|
|
27
|
+
}
|
|
28
|
+
function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, activeAgent, onAgentChange, chatApi, }) {
|
|
4
29
|
const objectNames = objects.map((o) => o.label || o.name).join(', ');
|
|
30
|
+
const activeAgentLabel = React.useMemo(() => {
|
|
31
|
+
const found = agents.find((a) => a.name === activeAgent);
|
|
32
|
+
return found?.label ?? activeAgent ?? appLabel;
|
|
33
|
+
}, [agents, activeAgent, appLabel]);
|
|
34
|
+
const welcomeContent = activeAgent
|
|
35
|
+
? `Hello! I'm **${activeAgentLabel}**, your ${appLabel} assistant. How can I help you today?`
|
|
36
|
+
: `Hello! I'm your **${appLabel}** assistant. ${agentsError ? '(Backend unreachable — running in offline demo mode.)' : ''}`;
|
|
5
37
|
const { messages, isLoading, error, sendMessage, stop, reload, clear, } = useObjectChat({
|
|
38
|
+
api: chatApi,
|
|
39
|
+
conversationId: activeAgent ? `${appLabel}:${activeAgent}` : undefined,
|
|
40
|
+
body: {
|
|
41
|
+
context: {
|
|
42
|
+
activeApp: appLabel,
|
|
43
|
+
objects: objects.map((o) => ({ name: o.name, label: o.label })),
|
|
44
|
+
agentName: activeAgent,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
6
47
|
initialMessages: [
|
|
7
48
|
{
|
|
8
49
|
id: 'welcome',
|
|
9
50
|
role: 'assistant',
|
|
10
|
-
content:
|
|
51
|
+
content: welcomeContent,
|
|
11
52
|
},
|
|
12
53
|
],
|
|
13
|
-
|
|
54
|
+
// Local-mode fallback: only used when `chatApi` is undefined (no agent
|
|
55
|
+
// resolved yet, or no backend available). Keeps the UI usable.
|
|
56
|
+
autoResponse: !chatApi,
|
|
14
57
|
autoResponseText: objectNames
|
|
15
58
|
? `I can help you work with ${objectNames}. What would you like to do?`
|
|
16
|
-
:
|
|
17
|
-
autoResponseDelay:
|
|
59
|
+
: "Thanks for your message! I'm here to help you navigate and manage your data.",
|
|
60
|
+
autoResponseDelay: 600,
|
|
18
61
|
});
|
|
62
|
+
const headerExtra = agents.length > 0 ? (_jsxs(Select, { value: activeAgent, onValueChange: onAgentChange, disabled: agentsLoading, children: [_jsx(SelectTrigger, { className: "h-7 w-[180px] text-xs", "data-testid": "floating-chatbot-agent-picker", children: _jsx(SelectValue, { placeholder: "Choose agent..." }) }), _jsx(SelectContent, { align: "end", children: agents.map((agent) => (_jsxs(SelectItem, { value: agent.name, className: "text-xs", children: [_jsx("span", { className: "font-medium", children: agent.label }), agent.description ? (_jsx("span", { className: "block text-muted-foreground text-[10px] truncate max-w-[220px]", children: agent.description })) : null] }, agent.name))) })] })) : null;
|
|
19
63
|
return (_jsx(FloatingChatbot, { floatingConfig: {
|
|
20
64
|
position: 'bottom-right',
|
|
21
65
|
defaultOpen: false,
|
|
22
|
-
panelWidth:
|
|
23
|
-
panelHeight:
|
|
66
|
+
panelWidth: 420,
|
|
67
|
+
panelHeight: 560,
|
|
24
68
|
title: `${appLabel} Assistant`,
|
|
25
69
|
triggerSize: 56,
|
|
26
|
-
},
|
|
70
|
+
}, headerExtra: headerExtra, messages: messages, placeholder: activeAgent
|
|
71
|
+
? `Ask ${activeAgentLabel}...`
|
|
72
|
+
: agentsLoading
|
|
73
|
+
? 'Loading agents...'
|
|
74
|
+
: 'Ask anything...', onSendMessage: (content) => sendMessage(content), onClear: clear, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: error, enableMarkdown: true }));
|
|
75
|
+
}
|
|
76
|
+
export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, }) {
|
|
77
|
+
const apiBase = React.useMemo(() => resolveApiBase(apiBaseProp), [apiBaseProp]);
|
|
78
|
+
const env = import.meta.env ?? {};
|
|
79
|
+
const envDefaultAgent = env.VITE_AI_DEFAULT_AGENT;
|
|
80
|
+
const { agents, isLoading: agentsLoading, error: agentsError } = useAgents({ apiBase });
|
|
81
|
+
const [activeAgent, setActiveAgent] = React.useState(undefined);
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
if (!activeAgent && agents.length > 0) {
|
|
84
|
+
const preferred = defaultAgentProp ?? envDefaultAgent;
|
|
85
|
+
const match = preferred ? agents.find((a) => a.name === preferred) : undefined;
|
|
86
|
+
setActiveAgent((match ?? agents[0]).name);
|
|
87
|
+
}
|
|
88
|
+
}, [agents, activeAgent, defaultAgentProp, envDefaultAgent]);
|
|
89
|
+
const chatApi = activeAgent
|
|
90
|
+
? `${apiBase}/agents/${encodeURIComponent(activeAgent)}/chat`
|
|
91
|
+
: undefined;
|
|
92
|
+
// `key` forces a clean remount whenever the active agent (and therefore the
|
|
93
|
+
// chat API URL) changes — required because `useObjectChat` locks its mode
|
|
94
|
+
// (api vs local) on first render.
|
|
95
|
+
return (_jsx(ChatbotInner, { appLabel: appLabel, objects: objects, agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, onAgentChange: setActiveAgent, chatApi: chatApi }, chatApi ?? 'local'));
|
|
27
96
|
}
|
|
@@ -29,6 +29,10 @@ function ConsoleLayoutInner({ children }) {
|
|
|
29
29
|
export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState }) {
|
|
30
30
|
const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName;
|
|
31
31
|
const { isAiEnabled } = useDiscovery();
|
|
32
|
+
// Trust an explicit `VITE_AI_BASE_URL` opt-in even when discovery reports
|
|
33
|
+
// AI as disabled (e.g. framework started without `--preset full`).
|
|
34
|
+
const aiBaseUrlConfigured = Boolean(import.meta.env?.VITE_AI_BASE_URL);
|
|
35
|
+
const showChatbot = isAiEnabled || aiBaseUrlConfigured;
|
|
32
36
|
const { setContext, setCurrentAppName } = useNavigationContext();
|
|
33
37
|
// Set navigation context to 'app' when this layout mounts
|
|
34
38
|
useEffect(() => {
|
|
@@ -45,5 +49,5 @@ export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange,
|
|
|
45
49
|
? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
|
|
46
50
|
: undefined,
|
|
47
51
|
}
|
|
48
|
-
: undefined, children: [_jsx(ConsoleLayoutInner, { children: children }),
|
|
52
|
+
: undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { appLabel: appLabel, objects: objects }) }))] }));
|
|
49
53
|
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility functions for ObjectStack Console
|
|
3
3
|
*/
|
|
4
|
+
export { resolveRecordFormTarget, } from './recordFormNavigation';
|
|
5
|
+
export type { ObjectDefinitionForNavigation, RecordFormTarget, } from './recordFormNavigation';
|
|
4
6
|
/**
|
|
5
7
|
* Resolves an I18nLabel to a plain string.
|
|
6
8
|
* I18nLabel can be either a string or an object { key, defaultValue?, params? }.
|
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolvers for record-form navigation targets.
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers extracted from `AppContent.tsx` so the routing rules behind
|
|
5
|
+
* the modal-vs-page decision are unit-testable in isolation. Keep this file
|
|
6
|
+
* free of React / router imports — it must run in a node test environment.
|
|
7
|
+
*
|
|
8
|
+
* @module utils/recordFormNavigation
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Subset of the object metadata shape consumed by the resolver. Mirrors the
|
|
12
|
+
* fields read from the runtime objects list (`useMetadata().objects`).
|
|
13
|
+
*/
|
|
14
|
+
export interface ObjectDefinitionForNavigation {
|
|
15
|
+
/** API name used in URLs. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Optional UI mode override for create/edit interactions. */
|
|
18
|
+
editMode?: 'modal' | 'page';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Result of the modal-vs-page decision. `kind: 'modal'` means the caller
|
|
22
|
+
* should open the global `<ModalForm>`; `kind: 'page'` means the caller
|
|
23
|
+
* should `navigate(url)` to the full-screen create/edit route.
|
|
24
|
+
*/
|
|
25
|
+
export type RecordFormTarget = {
|
|
26
|
+
kind: 'modal';
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'page';
|
|
29
|
+
url: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Build a record-form navigation target.
|
|
33
|
+
*
|
|
34
|
+
* Returns:
|
|
35
|
+
* - `{ kind: 'modal' }` when the object metadata does not opt in to page
|
|
36
|
+
* mode (default behavior — preserves backward compatibility).
|
|
37
|
+
* - `{ kind: 'page', url }` when `objectDef.editMode === 'page'`. The
|
|
38
|
+
* URL points at `/{baseUrl}/{objectName}/new` for create, or
|
|
39
|
+
* `/{baseUrl}/{objectName}/record/{recordId}/edit` for edit. The
|
|
40
|
+
* `recordId` is derived from `record.id` or `record._id`, falling
|
|
41
|
+
* back to create-mode if neither is present.
|
|
42
|
+
*
|
|
43
|
+
* Notes:
|
|
44
|
+
* - Returns `{ kind: 'modal' }` when `objectDef` is missing or has no
|
|
45
|
+
* `name` — the caller (AppContent) treats this as "no actionable
|
|
46
|
+
* state" and falls back to the existing modal flow.
|
|
47
|
+
* - `recordId` is URL-encoded so non-ASCII / reserved characters in
|
|
48
|
+
* object IDs (e.g. UUIDs with `:` separators) are safe.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveRecordFormTarget(opts: {
|
|
51
|
+
objectDef: ObjectDefinitionForNavigation | null | undefined;
|
|
52
|
+
baseUrl: string;
|
|
53
|
+
record: {
|
|
54
|
+
id?: string | number;
|
|
55
|
+
_id?: string | number;
|
|
56
|
+
} | null | undefined;
|
|
57
|
+
}): RecordFormTarget;
|
|
58
|
+
/**
|
|
59
|
+
* Action descriptor accepted by the navigate-create / navigate-edit
|
|
60
|
+
* handlers. Loose-typed because the same shape is constructed dynamically
|
|
61
|
+
* from JSON metadata at runtime and we want the helpers to be tolerant of
|
|
62
|
+
* legacy or hand-authored inputs.
|
|
63
|
+
*/
|
|
64
|
+
export interface NavigationActionDef {
|
|
65
|
+
/** Optional explicit object name (overrides any context). */
|
|
66
|
+
objectName?: string;
|
|
67
|
+
/** Optional explicit record id (only meaningful for `navigate_edit`). */
|
|
68
|
+
recordId?: string | number;
|
|
69
|
+
/** Standard params bag — preferred location for objectName / recordId. */
|
|
70
|
+
params?: {
|
|
71
|
+
objectName?: string;
|
|
72
|
+
recordId?: string | number;
|
|
73
|
+
[key: string]: any;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Optional context typically supplied by an `ActionRunner` (e.g. when the
|
|
78
|
+
* action button is mounted inside an `ObjectView`, the view registers its
|
|
79
|
+
* `objectName` and `baseUrl` on the runner so action JSON can omit them).
|
|
80
|
+
*
|
|
81
|
+
* Tolerant of additional unknown keys because the upstream
|
|
82
|
+
* `ActionRunner.getContext()` returns a generic `ActionContext` with an
|
|
83
|
+
* open index signature; we only read the two fields we care about.
|
|
84
|
+
*/
|
|
85
|
+
export interface NavigationActionContext {
|
|
86
|
+
objectName?: string;
|
|
87
|
+
baseUrl?: string;
|
|
88
|
+
[key: string]: unknown;
|
|
89
|
+
}
|
|
90
|
+
export type NavigationActionResult = {
|
|
91
|
+
success: true;
|
|
92
|
+
url: string;
|
|
93
|
+
} | {
|
|
94
|
+
success: false;
|
|
95
|
+
error: string;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the URL for a `navigate_create` action.
|
|
99
|
+
*
|
|
100
|
+
* Order of precedence for `objectName`:
|
|
101
|
+
* 1. `action.params.objectName`
|
|
102
|
+
* 2. `action.objectName`
|
|
103
|
+
* 3. `context.objectName`
|
|
104
|
+
*
|
|
105
|
+
* `baseUrl` falls back to `context.baseUrl`, then to the supplied
|
|
106
|
+
* `defaultBaseUrl` (typically `/apps/{appName}`).
|
|
107
|
+
*
|
|
108
|
+
* Returns `{ success: false }` with a descriptive error when `objectName`
|
|
109
|
+
* cannot be resolved — the caller (`ActionRunner`) surfaces this to the UI.
|
|
110
|
+
*/
|
|
111
|
+
export declare function resolveNavigateCreateUrl(opts: {
|
|
112
|
+
action: NavigationActionDef;
|
|
113
|
+
context?: NavigationActionContext;
|
|
114
|
+
defaultBaseUrl: string;
|
|
115
|
+
}): NavigationActionResult;
|
|
116
|
+
/**
|
|
117
|
+
* Resolve the URL for a `navigate_edit` action.
|
|
118
|
+
*
|
|
119
|
+
* Same `objectName` precedence as {@link resolveNavigateCreateUrl}.
|
|
120
|
+
* `recordId` is read from `action.params.recordId` or `action.recordId`
|
|
121
|
+
* (no context fallback — record ids are intrinsically per-action).
|
|
122
|
+
*
|
|
123
|
+
* Returns `{ success: false }` with a descriptive error when either
|
|
124
|
+
* `objectName` or `recordId` is missing.
|
|
125
|
+
*/
|
|
126
|
+
export declare function resolveNavigateEditUrl(opts: {
|
|
127
|
+
action: NavigationActionDef;
|
|
128
|
+
context?: NavigationActionContext;
|
|
129
|
+
defaultBaseUrl: string;
|
|
130
|
+
}): NavigationActionResult;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolvers for record-form navigation targets.
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers extracted from `AppContent.tsx` so the routing rules behind
|
|
5
|
+
* the modal-vs-page decision are unit-testable in isolation. Keep this file
|
|
6
|
+
* free of React / router imports — it must run in a node test environment.
|
|
7
|
+
*
|
|
8
|
+
* @module utils/recordFormNavigation
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Build a record-form navigation target.
|
|
12
|
+
*
|
|
13
|
+
* Returns:
|
|
14
|
+
* - `{ kind: 'modal' }` when the object metadata does not opt in to page
|
|
15
|
+
* mode (default behavior — preserves backward compatibility).
|
|
16
|
+
* - `{ kind: 'page', url }` when `objectDef.editMode === 'page'`. The
|
|
17
|
+
* URL points at `/{baseUrl}/{objectName}/new` for create, or
|
|
18
|
+
* `/{baseUrl}/{objectName}/record/{recordId}/edit` for edit. The
|
|
19
|
+
* `recordId` is derived from `record.id` or `record._id`, falling
|
|
20
|
+
* back to create-mode if neither is present.
|
|
21
|
+
*
|
|
22
|
+
* Notes:
|
|
23
|
+
* - Returns `{ kind: 'modal' }` when `objectDef` is missing or has no
|
|
24
|
+
* `name` — the caller (AppContent) treats this as "no actionable
|
|
25
|
+
* state" and falls back to the existing modal flow.
|
|
26
|
+
* - `recordId` is URL-encoded so non-ASCII / reserved characters in
|
|
27
|
+
* object IDs (e.g. UUIDs with `:` separators) are safe.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveRecordFormTarget(opts) {
|
|
30
|
+
const { objectDef, baseUrl, record } = opts;
|
|
31
|
+
if (!objectDef?.name || objectDef.editMode !== 'page') {
|
|
32
|
+
return { kind: 'modal' };
|
|
33
|
+
}
|
|
34
|
+
const rawId = record?.id ?? record?._id;
|
|
35
|
+
if (record && rawId != null && rawId !== '') {
|
|
36
|
+
const encoded = encodeURIComponent(String(rawId));
|
|
37
|
+
return {
|
|
38
|
+
kind: 'page',
|
|
39
|
+
url: `${baseUrl}/${objectDef.name}/record/${encoded}/edit`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { kind: 'page', url: `${baseUrl}/${objectDef.name}/new` };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the URL for a `navigate_create` action.
|
|
46
|
+
*
|
|
47
|
+
* Order of precedence for `objectName`:
|
|
48
|
+
* 1. `action.params.objectName`
|
|
49
|
+
* 2. `action.objectName`
|
|
50
|
+
* 3. `context.objectName`
|
|
51
|
+
*
|
|
52
|
+
* `baseUrl` falls back to `context.baseUrl`, then to the supplied
|
|
53
|
+
* `defaultBaseUrl` (typically `/apps/{appName}`).
|
|
54
|
+
*
|
|
55
|
+
* Returns `{ success: false }` with a descriptive error when `objectName`
|
|
56
|
+
* cannot be resolved — the caller (`ActionRunner`) surfaces this to the UI.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveNavigateCreateUrl(opts) {
|
|
59
|
+
const { action, context = {}, defaultBaseUrl } = opts;
|
|
60
|
+
const objectName = action.params?.objectName ?? action.objectName ?? context.objectName;
|
|
61
|
+
const baseUrl = context.baseUrl ?? defaultBaseUrl;
|
|
62
|
+
if (!objectName) {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
error: 'navigate_create: objectName is required',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { success: true, url: `${baseUrl}/${objectName}/new` };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the URL for a `navigate_edit` action.
|
|
72
|
+
*
|
|
73
|
+
* Same `objectName` precedence as {@link resolveNavigateCreateUrl}.
|
|
74
|
+
* `recordId` is read from `action.params.recordId` or `action.recordId`
|
|
75
|
+
* (no context fallback — record ids are intrinsically per-action).
|
|
76
|
+
*
|
|
77
|
+
* Returns `{ success: false }` with a descriptive error when either
|
|
78
|
+
* `objectName` or `recordId` is missing.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveNavigateEditUrl(opts) {
|
|
81
|
+
const { action, context = {}, defaultBaseUrl } = opts;
|
|
82
|
+
const objectName = action.params?.objectName ?? action.objectName ?? context.objectName;
|
|
83
|
+
const recordId = action.params?.recordId ?? action.recordId;
|
|
84
|
+
const baseUrl = context.baseUrl ?? defaultBaseUrl;
|
|
85
|
+
if (!objectName || recordId == null || recordId === '') {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
error: 'navigate_edit: objectName and recordId are required',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const encoded = encodeURIComponent(String(recordId));
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
url: `${baseUrl}/${objectName}/record/${encoded}/edit`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecordFormPage Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a full-screen create or edit page for a record. This is the
|
|
5
|
+
* page-mode counterpart to the global `ModalForm` mounted by `AppContent`
|
|
6
|
+
* for objects whose metadata declares `editMode: 'page'`.
|
|
7
|
+
*
|
|
8
|
+
* Routes (mounted by `AppContent`):
|
|
9
|
+
* - `/apps/:appName/:objectName/new` — create mode
|
|
10
|
+
* - `/apps/:appName/:objectName/record/:recordId/edit` — edit mode
|
|
11
|
+
*
|
|
12
|
+
* Behavior:
|
|
13
|
+
* - Resolves the object definition via `useMetadata()`. While metadata is
|
|
14
|
+
* still loading the page renders a `SkeletonDetail` to avoid a flash of
|
|
15
|
+
* "Object Not Found".
|
|
16
|
+
* - Delegates form rendering and data fetching to `<ObjectForm>` from
|
|
17
|
+
* `@object-ui/plugin-form` with `formType: 'simple'`. ObjectForm itself
|
|
18
|
+
* fetches the existing record (in edit mode) via `dataSource.findOne`,
|
|
19
|
+
* so this page does not need to manage its own loading state for
|
|
20
|
+
* record data.
|
|
21
|
+
* - On success / cancel, navigates back if the user has history, otherwise
|
|
22
|
+
* falls back to a sensible parent route (record detail in edit mode,
|
|
23
|
+
* object list in create mode). Cancel always navigates back without a
|
|
24
|
+
* toast.
|
|
25
|
+
* - Wraps the form in a sticky page header (back button + title) for a
|
|
26
|
+
* consistent full-screen chrome.
|
|
27
|
+
*
|
|
28
|
+
* @module views/RecordFormPage
|
|
29
|
+
*/
|
|
30
|
+
export interface RecordFormPageProps {
|
|
31
|
+
/** Form mode — `'create'` for the `/new` route, `'edit'` for the `/edit` route. */
|
|
32
|
+
mode: 'create' | 'edit';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Full-screen record create/edit page.
|
|
36
|
+
*
|
|
37
|
+
* Reads `:objectName` (and `:recordId` when editing) from the URL and
|
|
38
|
+
* resolves the object definition. Renders an `<ObjectForm>` configured with
|
|
39
|
+
* `formType: 'simple'` (i.e. a flat in-page form), wrapped in a page header
|
|
40
|
+
* that mirrors the look of `RecordDetailView`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function RecordFormPage({ mode }: RecordFormPageProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* RecordFormPage Component
|
|
4
|
+
*
|
|
5
|
+
* Renders a full-screen create or edit page for a record. This is the
|
|
6
|
+
* page-mode counterpart to the global `ModalForm` mounted by `AppContent`
|
|
7
|
+
* for objects whose metadata declares `editMode: 'page'`.
|
|
8
|
+
*
|
|
9
|
+
* Routes (mounted by `AppContent`):
|
|
10
|
+
* - `/apps/:appName/:objectName/new` — create mode
|
|
11
|
+
* - `/apps/:appName/:objectName/record/:recordId/edit` — edit mode
|
|
12
|
+
*
|
|
13
|
+
* Behavior:
|
|
14
|
+
* - Resolves the object definition via `useMetadata()`. While metadata is
|
|
15
|
+
* still loading the page renders a `SkeletonDetail` to avoid a flash of
|
|
16
|
+
* "Object Not Found".
|
|
17
|
+
* - Delegates form rendering and data fetching to `<ObjectForm>` from
|
|
18
|
+
* `@object-ui/plugin-form` with `formType: 'simple'`. ObjectForm itself
|
|
19
|
+
* fetches the existing record (in edit mode) via `dataSource.findOne`,
|
|
20
|
+
* so this page does not need to manage its own loading state for
|
|
21
|
+
* record data.
|
|
22
|
+
* - On success / cancel, navigates back if the user has history, otherwise
|
|
23
|
+
* falls back to a sensible parent route (record detail in edit mode,
|
|
24
|
+
* object list in create mode). Cancel always navigates back without a
|
|
25
|
+
* toast.
|
|
26
|
+
* - Wraps the form in a sticky page header (back button + title) for a
|
|
27
|
+
* consistent full-screen chrome.
|
|
28
|
+
*
|
|
29
|
+
* @module views/RecordFormPage
|
|
30
|
+
*/
|
|
31
|
+
import { useCallback, useMemo } from 'react';
|
|
32
|
+
import { useParams, useNavigate, Link } from 'react-router-dom';
|
|
33
|
+
import { ObjectForm } from '@object-ui/plugin-form';
|
|
34
|
+
import { Button, Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
35
|
+
import { ArrowLeft, Database } from 'lucide-react';
|
|
36
|
+
import { toast } from 'sonner';
|
|
37
|
+
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
38
|
+
import { useMetadata } from '../providers/MetadataProvider';
|
|
39
|
+
import { useAdapter } from '../providers/AdapterProvider';
|
|
40
|
+
import { ExpressionProvider, evaluateVisibility } from '../providers/ExpressionProvider';
|
|
41
|
+
import { SkeletonDetail } from '../skeletons';
|
|
42
|
+
import { useAuth } from '@object-ui/auth';
|
|
43
|
+
import { ExpressionEvaluator } from '@object-ui/core';
|
|
44
|
+
/**
|
|
45
|
+
* Full-screen record create/edit page.
|
|
46
|
+
*
|
|
47
|
+
* Reads `:objectName` (and `:recordId` when editing) from the URL and
|
|
48
|
+
* resolves the object definition. Renders an `<ObjectForm>` configured with
|
|
49
|
+
* `formType: 'simple'` (i.e. a flat in-page form), wrapped in a page header
|
|
50
|
+
* that mirrors the look of `RecordDetailView`.
|
|
51
|
+
*/
|
|
52
|
+
export function RecordFormPage({ mode }) {
|
|
53
|
+
const { appName, objectName, recordId } = useParams();
|
|
54
|
+
const navigate = useNavigate();
|
|
55
|
+
const dataSource = useAdapter();
|
|
56
|
+
const { objects, loading: metadataLoading } = useMetadata();
|
|
57
|
+
const { t } = useObjectTranslation();
|
|
58
|
+
const { objectLabel } = useObjectLabel();
|
|
59
|
+
const { user } = useAuth();
|
|
60
|
+
const objectDef = useMemo(() => objects.find((o) => o.name === objectName), [objects, objectName]);
|
|
61
|
+
const baseUrl = `/apps/${appName}`;
|
|
62
|
+
const objectListUrl = `${baseUrl}/${objectName}`;
|
|
63
|
+
const recordDetailUrl = mode === 'edit' && recordId
|
|
64
|
+
? `${baseUrl}/${objectName}/record/${encodeURIComponent(recordId)}`
|
|
65
|
+
: objectListUrl;
|
|
66
|
+
/**
|
|
67
|
+
* Navigate back to the most relevant location.
|
|
68
|
+
* Prefer `history.back()` so users return to the exact list/view they came
|
|
69
|
+
* from (preserving filters, scroll position, etc.). Fall back to the
|
|
70
|
+
* record detail (edit mode) or list (create mode) when there is no
|
|
71
|
+
* history entry — happens on direct/refreshed loads of the URL.
|
|
72
|
+
*/
|
|
73
|
+
const goBack = useCallback(() => {
|
|
74
|
+
if (typeof window !== 'undefined' && window.history.length > 1) {
|
|
75
|
+
navigate(-1);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
navigate(recordDetailUrl, { replace: true });
|
|
79
|
+
}
|
|
80
|
+
}, [navigate, recordDetailUrl]);
|
|
81
|
+
const label = objectDef ? objectLabel(objectDef) : objectName ?? '';
|
|
82
|
+
const pageTitle = mode === 'create'
|
|
83
|
+
? t('form.createTitle', { object: label, defaultValue: `New ${label}` })
|
|
84
|
+
: t('form.editTitle', { object: label, defaultValue: `Edit ${label}` });
|
|
85
|
+
const handleSuccess = useCallback(() => {
|
|
86
|
+
toast.success(mode === 'create'
|
|
87
|
+
? t('form.createSuccess', {
|
|
88
|
+
object: label,
|
|
89
|
+
defaultValue: `${label} created successfully`,
|
|
90
|
+
})
|
|
91
|
+
: t('form.updateSuccess', {
|
|
92
|
+
object: label,
|
|
93
|
+
defaultValue: `${label} updated successfully`,
|
|
94
|
+
}));
|
|
95
|
+
goBack();
|
|
96
|
+
}, [mode, t, label, goBack]);
|
|
97
|
+
const handleCancel = useCallback(() => {
|
|
98
|
+
goBack();
|
|
99
|
+
}, [goBack]);
|
|
100
|
+
// Authenticated-user descriptor — shared by the ExpressionEvaluator (used
|
|
101
|
+
// for field-visibility expression evaluation) and the ExpressionProvider
|
|
102
|
+
// wrapping the form (which exposes the same descriptor to descendant
|
|
103
|
+
// expression consumers). Memoised on the underlying user identity so a
|
|
104
|
+
// re-render that doesn't change the user does not invalidate downstream
|
|
105
|
+
// memoisations.
|
|
106
|
+
const expressionUser = useMemo(() => user
|
|
107
|
+
? { name: user.name, email: user.email, role: user.role ?? 'user' }
|
|
108
|
+
: { name: 'Anonymous', email: '', role: 'guest' }, [user]);
|
|
109
|
+
// Build expression evaluator for field-visibility expressions, mirroring
|
|
110
|
+
// the global ModalForm setup in AppContent.
|
|
111
|
+
const expressionEvaluator = useMemo(() => new ExpressionEvaluator({
|
|
112
|
+
// expressionUser already handles the anonymous fallback, so we can
|
|
113
|
+
// pass it through unconditionally.
|
|
114
|
+
user: expressionUser,
|
|
115
|
+
app: { name: appName },
|
|
116
|
+
data: {},
|
|
117
|
+
}), [expressionUser, appName]);
|
|
118
|
+
// Resolve the field list using the same visibility-aware logic as the
|
|
119
|
+
// ModalForm in AppContent so page-mode and modal-mode show the same
|
|
120
|
+
// fields for a given user.
|
|
121
|
+
const fields = useMemo(() => {
|
|
122
|
+
if (!objectDef?.fields)
|
|
123
|
+
return [];
|
|
124
|
+
if (Array.isArray(objectDef.fields)) {
|
|
125
|
+
return objectDef.fields
|
|
126
|
+
.filter((f) => {
|
|
127
|
+
if (typeof f === 'string')
|
|
128
|
+
return true;
|
|
129
|
+
return evaluateVisibility(f.visible, expressionEvaluator);
|
|
130
|
+
})
|
|
131
|
+
.map((f) => (typeof f === 'string' ? f : f.name));
|
|
132
|
+
}
|
|
133
|
+
return Object.entries(objectDef.fields)
|
|
134
|
+
.filter(([, f]) => evaluateVisibility(f.visible, expressionEvaluator))
|
|
135
|
+
.map(([key]) => key);
|
|
136
|
+
}, [objectDef, expressionEvaluator]);
|
|
137
|
+
// Show skeleton while metadata is still loading rather than the
|
|
138
|
+
// "Object Not Found" empty state — otherwise direct/refreshed loads of
|
|
139
|
+
// the URL flash an error before the metadata resolves.
|
|
140
|
+
if (metadataLoading) {
|
|
141
|
+
return _jsx(SkeletonDetail, {});
|
|
142
|
+
}
|
|
143
|
+
if (!objectDef) {
|
|
144
|
+
return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: "Object Not Found" }), _jsxs(EmptyDescription, { children: ["Object \"", objectName, "\" definition missing. Check your configuration or navigate back to select a valid object."] }), _jsx("div", { className: "mt-4", children: _jsxs(Button, { variant: "outline", onClick: () => navigate(baseUrl), children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), "Back"] }) })] }) }));
|
|
145
|
+
}
|
|
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
|
+
type: 'object-form',
|
|
148
|
+
formType: 'simple',
|
|
149
|
+
objectName: objectDef.name,
|
|
150
|
+
mode,
|
|
151
|
+
recordId: mode === 'edit' ? recordId : undefined,
|
|
152
|
+
title: pageTitle,
|
|
153
|
+
description: mode === 'create'
|
|
154
|
+
? t('form.createDescription', {
|
|
155
|
+
object: label,
|
|
156
|
+
defaultValue: `Create a new ${label}.`,
|
|
157
|
+
})
|
|
158
|
+
: t('form.editDescription', {
|
|
159
|
+
object: label,
|
|
160
|
+
defaultValue: `Edit this ${label}.`,
|
|
161
|
+
}),
|
|
162
|
+
layout: 'vertical',
|
|
163
|
+
fields,
|
|
164
|
+
onSuccess: handleSuccess,
|
|
165
|
+
onCancel: handleCancel,
|
|
166
|
+
showSubmit: true,
|
|
167
|
+
showCancel: true,
|
|
168
|
+
submitText: t('form.saveRecord', { defaultValue: 'Save' }),
|
|
169
|
+
cancelText: t('common.cancel', { defaultValue: 'Cancel' }),
|
|
170
|
+
}, dataSource: dataSource ?? undefined }, `${mode}:${objectName}:${recordId ?? 'new'}`) }) })] }) }));
|
|
171
|
+
}
|
package/dist/views/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { ObjectView } from './ObjectView';
|
|
2
2
|
export { RecordDetailView } from './RecordDetailView';
|
|
3
|
+
export { RecordFormPage } from './RecordFormPage';
|
|
4
|
+
export type { RecordFormPageProps } from './RecordFormPage';
|
|
3
5
|
export { DashboardView } from './DashboardView';
|
|
4
6
|
export { PageView } from './PageView';
|
|
5
7
|
export { ReportView } from './ReportView';
|
package/dist/views/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { ObjectView } from './ObjectView';
|
|
2
2
|
export { RecordDetailView } from './RecordDetailView';
|
|
3
|
+
export { RecordFormPage } from './RecordFormPage';
|
|
3
4
|
export { DashboardView } from './DashboardView';
|
|
4
5
|
export { PageView } from './PageView';
|
|
5
6
|
export { ReportView } from './ReportView';
|
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.4",
|
|
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.4",
|
|
31
|
+
"@object-ui/collaboration": "4.0.4",
|
|
32
|
+
"@object-ui/components": "4.0.4",
|
|
33
|
+
"@object-ui/core": "4.0.4",
|
|
34
|
+
"@object-ui/data-objectstack": "4.0.4",
|
|
35
|
+
"@object-ui/fields": "4.0.4",
|
|
36
|
+
"@object-ui/i18n": "4.0.4",
|
|
37
|
+
"@object-ui/layout": "4.0.4",
|
|
38
|
+
"@object-ui/permissions": "4.0.4",
|
|
39
|
+
"@object-ui/react": "4.0.4",
|
|
40
|
+
"@object-ui/types": "4.0.4"
|
|
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.4",
|
|
47
|
+
"@object-ui/plugin-charts": "4.0.4",
|
|
48
|
+
"@object-ui/plugin-chatbot": "4.0.4",
|
|
49
|
+
"@object-ui/plugin-dashboard": "4.0.4",
|
|
50
|
+
"@object-ui/plugin-designer": "4.0.4",
|
|
51
|
+
"@object-ui/plugin-detail": "4.0.4",
|
|
52
|
+
"@object-ui/plugin-form": "4.0.4",
|
|
53
|
+
"@object-ui/plugin-grid": "4.0.4",
|
|
54
|
+
"@object-ui/plugin-kanban": "4.0.4",
|
|
55
|
+
"@object-ui/plugin-list": "4.0.4",
|
|
56
|
+
"@object-ui/plugin-report": "4.0.4",
|
|
57
|
+
"@object-ui/plugin-view": "4.0.4"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^25.6.0",
|