@object-ui/app-shell 4.0.0 → 4.0.3

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.
@@ -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
+ }
@@ -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';
@@ -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.0",
3
+ "version": "4.0.3",
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": "3.4.0",
31
- "@object-ui/collaboration": "3.4.0",
32
- "@object-ui/components": "3.4.0",
33
- "@object-ui/core": "3.4.0",
34
- "@object-ui/data-objectstack": "3.4.0",
35
- "@object-ui/fields": "3.4.0",
36
- "@object-ui/i18n": "3.4.0",
37
- "@object-ui/layout": "3.4.0",
38
- "@object-ui/permissions": "3.4.0",
39
- "@object-ui/react": "3.4.0",
40
- "@object-ui/types": "3.4.0"
30
+ "@object-ui/auth": "4.0.3",
31
+ "@object-ui/collaboration": "4.0.3",
32
+ "@object-ui/components": "4.0.3",
33
+ "@object-ui/core": "4.0.3",
34
+ "@object-ui/data-objectstack": "4.0.3",
35
+ "@object-ui/fields": "4.0.3",
36
+ "@object-ui/i18n": "4.0.3",
37
+ "@object-ui/layout": "4.0.3",
38
+ "@object-ui/permissions": "4.0.3",
39
+ "@object-ui/react": "4.0.3",
40
+ "@object-ui/types": "4.0.3"
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": "3.4.0",
47
- "@object-ui/plugin-charts": "3.4.0",
48
- "@object-ui/plugin-chatbot": "3.4.0",
49
- "@object-ui/plugin-dashboard": "3.4.0",
50
- "@object-ui/plugin-designer": "3.4.0",
51
- "@object-ui/plugin-detail": "3.4.0",
52
- "@object-ui/plugin-form": "3.4.0",
53
- "@object-ui/plugin-grid": "3.4.0",
54
- "@object-ui/plugin-kanban": "3.4.0",
55
- "@object-ui/plugin-list": "3.4.0",
56
- "@object-ui/plugin-report": "3.4.0",
57
- "@object-ui/plugin-view": "3.4.0"
46
+ "@object-ui/plugin-calendar": "4.0.3",
47
+ "@object-ui/plugin-charts": "4.0.3",
48
+ "@object-ui/plugin-chatbot": "4.0.3",
49
+ "@object-ui/plugin-dashboard": "4.0.3",
50
+ "@object-ui/plugin-designer": "4.0.3",
51
+ "@object-ui/plugin-detail": "4.0.3",
52
+ "@object-ui/plugin-form": "4.0.3",
53
+ "@object-ui/plugin-grid": "4.0.3",
54
+ "@object-ui/plugin-kanban": "4.0.3",
55
+ "@object-ui/plugin-list": "4.0.3",
56
+ "@object-ui/plugin-report": "4.0.3",
57
+ "@object-ui/plugin-view": "4.0.3"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^25.6.0",