@object-ui/app-shell 5.1.1 → 5.2.1
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 +273 -0
- package/dist/chrome/CommandPalette.d.ts +6 -1
- package/dist/chrome/CommandPalette.js +40 -4
- package/dist/chrome/ConsoleToaster.js +8 -1
- package/dist/chrome/RouteFader.d.ts +32 -0
- package/dist/chrome/RouteFader.js +52 -0
- package/dist/chrome/index.d.ts +2 -0
- package/dist/chrome/index.js +2 -0
- package/dist/chrome/toast-helpers.d.ts +48 -0
- package/dist/chrome/toast-helpers.js +59 -0
- package/dist/console/AppContent.js +108 -2
- package/dist/console/home/HomePage.js +5 -4
- package/dist/console/marketplace/MarketplaceInstalledPage.d.ts +17 -0
- package/dist/console/marketplace/MarketplaceInstalledPage.js +64 -0
- package/dist/console/marketplace/MarketplacePackagePage.d.ts +7 -0
- package/dist/console/marketplace/MarketplacePackagePage.js +199 -0
- package/dist/console/marketplace/MarketplacePage.d.ts +8 -0
- package/dist/console/marketplace/MarketplacePage.js +94 -0
- package/dist/console/marketplace/PackageIcon.d.ts +19 -0
- package/dist/console/marketplace/PackageIcon.js +17 -0
- package/dist/console/marketplace/marketplaceApi.d.ts +122 -0
- package/dist/console/marketplace/marketplaceApi.js +207 -0
- package/dist/index.d.ts +5 -6
- package/dist/index.js +4 -5
- package/dist/layout/AppHeader.js +16 -2
- package/dist/layout/AppSidebar.js +15 -11
- package/dist/layout/InboxPopover.js +43 -3
- package/dist/types.d.ts +0 -46
- package/dist/views/RecordDetailView.js +79 -15
- package/package.json +25 -25
- package/src/styles.css +49 -0
- package/dist/components/DashboardRenderer.d.ts +0 -8
- package/dist/components/DashboardRenderer.js +0 -16
- package/dist/components/FormRenderer.d.ts +0 -8
- package/dist/components/FormRenderer.js +0 -31
- package/dist/components/ObjectRenderer.d.ts +0 -8
- package/dist/components/ObjectRenderer.js +0 -74
- package/dist/components/PageRenderer.d.ts +0 -7
- package/dist/components/PageRenderer.js +0 -14
package/dist/types.d.ts
CHANGED
|
@@ -24,49 +24,3 @@ export interface AppShellProps {
|
|
|
24
24
|
/** Custom className */
|
|
25
25
|
className?: string;
|
|
26
26
|
}
|
|
27
|
-
export interface ObjectRendererProps {
|
|
28
|
-
/** Object API name */
|
|
29
|
-
objectName: string;
|
|
30
|
-
/** View ID (optional) */
|
|
31
|
-
viewId?: string;
|
|
32
|
-
/** Data source for CRUD operations */
|
|
33
|
-
dataSource: DataSource;
|
|
34
|
-
/** Callback when a record is clicked */
|
|
35
|
-
onRecordClick?: (record: any) => void;
|
|
36
|
-
/** Callback when edit is triggered */
|
|
37
|
-
onEdit?: (record: any) => void;
|
|
38
|
-
/** Object metadata (optional, will fetch if not provided) */
|
|
39
|
-
objectDef?: any;
|
|
40
|
-
/** Refresh key to force re-render */
|
|
41
|
-
refreshKey?: number;
|
|
42
|
-
}
|
|
43
|
-
export interface DashboardRendererProps {
|
|
44
|
-
/** Dashboard schema */
|
|
45
|
-
schema: any;
|
|
46
|
-
/** Data source for widgets */
|
|
47
|
-
dataSource: DataSource;
|
|
48
|
-
/** Dashboard name */
|
|
49
|
-
dashboardName?: string;
|
|
50
|
-
}
|
|
51
|
-
export interface PageRendererProps {
|
|
52
|
-
/** Page schema */
|
|
53
|
-
schema: any;
|
|
54
|
-
/** Page name */
|
|
55
|
-
pageName?: string;
|
|
56
|
-
}
|
|
57
|
-
export interface FormRendererProps {
|
|
58
|
-
/** Form schema */
|
|
59
|
-
schema: any;
|
|
60
|
-
/** Data source for form submission */
|
|
61
|
-
dataSource: DataSource;
|
|
62
|
-
/** Form mode */
|
|
63
|
-
mode?: 'create' | 'edit';
|
|
64
|
-
/** Record ID (for edit mode) */
|
|
65
|
-
recordId?: string;
|
|
66
|
-
/** Success callback */
|
|
67
|
-
onSuccess?: (result: any) => void;
|
|
68
|
-
/** Cancel callback */
|
|
69
|
-
onCancel?: () => void;
|
|
70
|
-
/** Object definition */
|
|
71
|
-
objectDef?: any;
|
|
72
|
-
}
|
|
@@ -8,12 +8,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
*/
|
|
9
9
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
10
10
|
import { useParams, useNavigate, useLocation, Link } from 'react-router-dom';
|
|
11
|
-
import { DetailView, RecordChatterPanel, buildDefaultPageSchema } from '@object-ui/plugin-detail';
|
|
11
|
+
import { DetailView, RecordChatterPanel, buildDefaultPageSchema, extractMentions } from '@object-ui/plugin-detail';
|
|
12
12
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
13
13
|
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
|
|
14
14
|
import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider } from '@object-ui/react';
|
|
15
15
|
import { buildExpandFields } from '@object-ui/core';
|
|
16
16
|
import { toast } from 'sonner';
|
|
17
|
+
import { useRecordPresence, PresenceAvatars } from '@object-ui/collaboration';
|
|
17
18
|
import { Database, ChevronLeft } from 'lucide-react';
|
|
18
19
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
19
20
|
import { SkeletonDetail } from '../skeletons';
|
|
@@ -98,6 +99,13 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
98
99
|
// Navigation code passes `record.id || record._id` directly into the URL
|
|
99
100
|
// without adding any prefix, so no stripping is needed.
|
|
100
101
|
const pureRecordId = recordId;
|
|
102
|
+
// Record-scoped presence ("who else is viewing this record"). The default
|
|
103
|
+
// PresenceProvider source is a no-op, so this resolves to `[]` until a
|
|
104
|
+
// realtime transport (WebSocket-backed source) is wired in by the host
|
|
105
|
+
// app — see `@object-ui/collaboration`'s `<PresenceProvider>`. The
|
|
106
|
+
// PresenceAvatars row is hidden when the array is empty, so the
|
|
107
|
+
// affordance is invisible until the transport lights up.
|
|
108
|
+
const recordPresence = useRecordPresence(objectName, pureRecordId);
|
|
101
109
|
const favoriteRecord = useMemo(() => {
|
|
102
110
|
if (!objectName || !pureRecordId)
|
|
103
111
|
return null;
|
|
@@ -159,10 +167,16 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
159
167
|
}, [renderViaSchemaFlag, assignedPage, assignedSlots, objectDef]);
|
|
160
168
|
const effectivePage = assignedPage || synthesizedPage;
|
|
161
169
|
const [pageRecord, setPageRecord] = useState(null);
|
|
170
|
+
// 'idle' | 'loading' | 'loaded' | 'missing' — distinguishes "haven't
|
|
171
|
+
// tried yet" from "tried and the record really doesn't exist". The
|
|
172
|
+
// not-found short-circuit below uses `missing` to render a clean empty
|
|
173
|
+
// state instead of a half-broken page chrome (rail + discussion).
|
|
174
|
+
const [pageRecordStatus, setPageRecordStatus] = useState('idle');
|
|
162
175
|
useEffect(() => {
|
|
163
176
|
let cancelled = false;
|
|
164
177
|
if (!effectivePage || !pureRecordId || !objectName || !dataSource?.findOne) {
|
|
165
178
|
setPageRecord(null);
|
|
179
|
+
setPageRecordStatus('idle');
|
|
166
180
|
return;
|
|
167
181
|
}
|
|
168
182
|
// Expand lookup/master_detail fields so the page receives display
|
|
@@ -171,17 +185,28 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
171
185
|
const expandFields = buildExpandFields(objectDef?.fields);
|
|
172
186
|
const params = expandFields.length > 0 ? { $expand: expandFields } : undefined;
|
|
173
187
|
const loadRecord = () => {
|
|
188
|
+
setPageRecordStatus('loading');
|
|
174
189
|
const findOnePromise = params
|
|
175
190
|
? dataSource.findOne(objectName, pureRecordId, params)
|
|
176
191
|
: dataSource.findOne(objectName, pureRecordId);
|
|
177
192
|
findOnePromise
|
|
178
193
|
.then((rec) => {
|
|
179
|
-
if (
|
|
194
|
+
if (cancelled)
|
|
195
|
+
return;
|
|
196
|
+
if (rec && typeof rec === 'object') {
|
|
180
197
|
setPageRecord(rec);
|
|
198
|
+
setPageRecordStatus('loaded');
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
setPageRecord(null);
|
|
202
|
+
setPageRecordStatus('missing');
|
|
203
|
+
}
|
|
181
204
|
})
|
|
182
205
|
.catch(() => {
|
|
183
|
-
if (
|
|
184
|
-
|
|
206
|
+
if (cancelled)
|
|
207
|
+
return;
|
|
208
|
+
setPageRecord(null);
|
|
209
|
+
setPageRecordStatus('missing');
|
|
185
210
|
});
|
|
186
211
|
};
|
|
187
212
|
loadRecord();
|
|
@@ -580,6 +605,16 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
580
605
|
if (!dataSource)
|
|
581
606
|
return;
|
|
582
607
|
let cancelled = false;
|
|
608
|
+
// Always seed with the current user so the @-mention dropdown shows at
|
|
609
|
+
// least one entry even when the backend has no sys_user directory.
|
|
610
|
+
// Hosts wanting a richer roster should provide it via dataSource.
|
|
611
|
+
const selfSuggestion = user
|
|
612
|
+
? [{
|
|
613
|
+
id: String(user.id),
|
|
614
|
+
label: user.name || user.email || String(user.id),
|
|
615
|
+
avatarUrl: user.image || undefined,
|
|
616
|
+
}]
|
|
617
|
+
: [];
|
|
583
618
|
(async () => {
|
|
584
619
|
try {
|
|
585
620
|
const res = await dataSource.find('sys_user', {
|
|
@@ -589,22 +624,30 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
589
624
|
if (cancelled)
|
|
590
625
|
return;
|
|
591
626
|
const rows = Array.isArray(res) ? res : res?.data || [];
|
|
592
|
-
const
|
|
627
|
+
const fetched = rows
|
|
593
628
|
.map((u) => ({
|
|
594
629
|
id: String(u.id),
|
|
595
630
|
label: u.name || u.email || String(u.id),
|
|
596
631
|
avatarUrl: u.image || undefined,
|
|
597
632
|
}))
|
|
598
633
|
.filter((s) => s.label);
|
|
599
|
-
|
|
634
|
+
// Merge: current user first, then directory (de-duped by id).
|
|
635
|
+
const seen = new Set(selfSuggestion.map((s) => s.id));
|
|
636
|
+
const merged = [
|
|
637
|
+
...selfSuggestion,
|
|
638
|
+
...fetched.filter((s) => !seen.has(s.id)),
|
|
639
|
+
];
|
|
640
|
+
setMentionSuggestions(merged.length > 0 ? merged : selfSuggestion);
|
|
600
641
|
}
|
|
601
642
|
catch {
|
|
602
|
-
|
|
603
|
-
|
|
643
|
+
if (cancelled)
|
|
644
|
+
return;
|
|
645
|
+
// Fall back to just the current user so mentions still work.
|
|
646
|
+
setMentionSuggestions(selfSuggestion);
|
|
604
647
|
}
|
|
605
648
|
})();
|
|
606
649
|
return () => { cancelled = true; };
|
|
607
|
-
}, [dataSource]);
|
|
650
|
+
}, [dataSource, user?.id, user?.name, user?.email]);
|
|
608
651
|
// Memoize so the object identity is stable across renders — otherwise
|
|
609
652
|
// any effect that depends on it (e.g. the feed loader below) would
|
|
610
653
|
// re-fire every render and create an infinite request loop.
|
|
@@ -760,6 +803,15 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
760
803
|
})
|
|
761
804
|
.catch(() => { });
|
|
762
805
|
}, [dataSource, objectName, pureRecordId, currentUser]);
|
|
806
|
+
/**
|
|
807
|
+
* Note: comment-mention → notification fan-out lives on the server
|
|
808
|
+
* (`@objectstack/plugin-audit` registers a `sys_comment` afterInsert
|
|
809
|
+
* hook that parses the `mentions` JSON and writes one `sys_notification`
|
|
810
|
+
* row per recipient). The client's only job is to ensure
|
|
811
|
+
* `sys_comment.mentions` carries the real id list (see handleAddComment
|
|
812
|
+
* /handleAddReply below). Deployments without plugin-audit will not
|
|
813
|
+
* deliver bell notifications, which is the expected degradation.
|
|
814
|
+
*/
|
|
763
815
|
const handleAddComment = useCallback(async (text) => {
|
|
764
816
|
const newItem = {
|
|
765
817
|
id: crypto.randomUUID(),
|
|
@@ -773,6 +825,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
773
825
|
// Persist to backend (M10.10: snake_case fields per sys_comment schema)
|
|
774
826
|
if (dataSource) {
|
|
775
827
|
const threadId = `${objectName}:${pureRecordId}`;
|
|
828
|
+
const mentionIds = extractMentions(text, mentionSuggestions);
|
|
776
829
|
dataSource.create('sys_comment', {
|
|
777
830
|
id: newItem.id,
|
|
778
831
|
thread_id: threadId,
|
|
@@ -780,11 +833,11 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
780
833
|
author_name: currentUser.name,
|
|
781
834
|
author_avatar_url: 'avatar' in currentUser ? currentUser.avatar : undefined,
|
|
782
835
|
body: text,
|
|
783
|
-
mentions:
|
|
836
|
+
mentions: JSON.stringify(mentionIds),
|
|
784
837
|
created_at: newItem.createdAt,
|
|
785
838
|
}).catch(() => { });
|
|
786
839
|
}
|
|
787
|
-
}, [currentUser, dataSource, objectName, pureRecordId]);
|
|
840
|
+
}, [currentUser, dataSource, objectName, pureRecordId, mentionSuggestions]);
|
|
788
841
|
const handleAddReply = useCallback(async (parentId, text) => {
|
|
789
842
|
const newItem = {
|
|
790
843
|
id: crypto.randomUUID(),
|
|
@@ -804,6 +857,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
804
857
|
});
|
|
805
858
|
if (dataSource) {
|
|
806
859
|
const threadId = `${objectName}:${pureRecordId}`;
|
|
860
|
+
const mentionIds = extractMentions(text, mentionSuggestions);
|
|
807
861
|
dataSource.create('sys_comment', {
|
|
808
862
|
id: newItem.id,
|
|
809
863
|
thread_id: threadId,
|
|
@@ -811,12 +865,12 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
811
865
|
author_name: currentUser.name,
|
|
812
866
|
author_avatar_url: 'avatar' in currentUser ? currentUser.avatar : undefined,
|
|
813
867
|
body: text,
|
|
814
|
-
mentions:
|
|
868
|
+
mentions: JSON.stringify(mentionIds),
|
|
815
869
|
created_at: newItem.createdAt,
|
|
816
870
|
parent_id: parentId,
|
|
817
871
|
}).catch(() => { });
|
|
818
872
|
}
|
|
819
|
-
}, [currentUser, dataSource, objectName, pureRecordId]);
|
|
873
|
+
}, [currentUser, dataSource, objectName, pureRecordId, mentionSuggestions]);
|
|
820
874
|
const handleToggleReaction = useCallback((itemId, emoji) => {
|
|
821
875
|
setFeedItems(prev => prev.map(item => {
|
|
822
876
|
if (item.id !== itemId)
|
|
@@ -1187,6 +1241,16 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1187
1241
|
if (!objectDef) {
|
|
1188
1242
|
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 }) })] }) }));
|
|
1189
1243
|
}
|
|
1244
|
+
// Record-not-found short-circuit. Previously we rendered the page chrome
|
|
1245
|
+
// (rail, discussion, breadcrumb with the truncated raw id) even when the
|
|
1246
|
+
// record didn't exist, which made invalid links look like a partially-
|
|
1247
|
+
// broken page instead of a clean 404. Only triggers on the synth/page
|
|
1248
|
+
// path; the legacy DetailView path handles missing records itself.
|
|
1249
|
+
if (effectivePage && pageRecordStatus === 'missing') {
|
|
1250
|
+
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.recordNotFound', { defaultValue: 'Record not found' }) }), _jsx(EmptyDescription, { children: t('empty.recordNotFoundDescription', {
|
|
1251
|
+
defaultValue: 'The record you are looking for does not exist or may have been deleted.',
|
|
1252
|
+
}) })] }) }));
|
|
1253
|
+
}
|
|
1190
1254
|
if (effectivePage) {
|
|
1191
1255
|
const disableDiscussion = effectivePage?.disableDiscussion === true;
|
|
1192
1256
|
// When the page schema embeds an explicit `record:discussion` /
|
|
@@ -1356,7 +1420,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1356
1420
|
hideRelatedTab: objectDef?.detail?.hideRelatedTab === true || undefined,
|
|
1357
1421
|
...(assignedSlots ? { slots: assignedSlots } : {}),
|
|
1358
1422
|
});
|
|
1359
|
-
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [
|
|
1423
|
+
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [recordPresence.length > 0 && (_jsx(PresenceAvatars, { users: recordPresence, size: "sm", maxVisible: 3, showStatus: true })), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), _jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, headerSystemActions: synthSystemActions, isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, children: _jsx(HighlightFieldsProvider, { children: _jsx(DiscussionContextProvider, { items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, mentionSuggestions: mentionSuggestions, children: _jsx(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsxs("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: [originFrom?.pathname && originFrom?.label && (_jsxs(Link, { to: originFrom.pathname, className: "inline-flex items-center gap-1 mb-3 text-sm text-muted-foreground hover:text-foreground transition-colors", children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), _jsx("span", { children: originFrom.label })] })), _jsx(SchemaRenderer, { schema: renderedPage }), showAutoDiscussion && (_jsx("div", { className: "mt-6", children: _jsx(RecordChatterPanel, { config: {
|
|
1360
1424
|
position: 'bottom',
|
|
1361
1425
|
collapsible: false,
|
|
1362
1426
|
feed: {
|
|
@@ -1372,7 +1436,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1372
1436
|
setParamState(s => ({ ...s, open: false }));
|
|
1373
1437
|
} })] }));
|
|
1374
1438
|
}
|
|
1375
|
-
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [
|
|
1439
|
+
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [recordPresence.length > 0 && (_jsx(PresenceAvatars, { users: recordPresence, size: "sm", maxVisible: 3, showStatus: true })), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), _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, approval: approvalHandler }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, onDataLoaded: (record) => {
|
|
1376
1440
|
if (!record || typeof record !== 'object')
|
|
1377
1441
|
return;
|
|
1378
1442
|
// Resolve the same way DetailView's header does, so the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/app-shell",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
|
|
@@ -27,35 +27,35 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"lucide-react": "^1.16.0",
|
|
29
29
|
"sonner": "^2.0.7",
|
|
30
|
-
"@object-ui/auth": "5.
|
|
31
|
-
"@object-ui/collaboration": "5.
|
|
32
|
-
"@object-ui/components": "5.
|
|
33
|
-
"@object-ui/core": "5.
|
|
34
|
-
"@object-ui/data-objectstack": "5.
|
|
35
|
-
"@object-ui/fields": "5.
|
|
36
|
-
"@object-ui/i18n": "5.
|
|
37
|
-
"@object-ui/layout": "5.
|
|
38
|
-
"@object-ui/permissions": "5.
|
|
39
|
-
"@object-ui/providers": "5.
|
|
40
|
-
"@object-ui/react": "5.
|
|
41
|
-
"@object-ui/types": "5.
|
|
30
|
+
"@object-ui/auth": "5.2.1",
|
|
31
|
+
"@object-ui/collaboration": "5.2.1",
|
|
32
|
+
"@object-ui/components": "5.2.1",
|
|
33
|
+
"@object-ui/core": "5.2.1",
|
|
34
|
+
"@object-ui/data-objectstack": "5.2.1",
|
|
35
|
+
"@object-ui/fields": "5.2.1",
|
|
36
|
+
"@object-ui/i18n": "5.2.1",
|
|
37
|
+
"@object-ui/layout": "5.2.1",
|
|
38
|
+
"@object-ui/permissions": "5.2.1",
|
|
39
|
+
"@object-ui/providers": "5.2.1",
|
|
40
|
+
"@object-ui/react": "5.2.1",
|
|
41
|
+
"@object-ui/types": "5.2.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"react": "^18.0.0 || ^19.0.0",
|
|
45
45
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
46
46
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
47
|
-
"@object-ui/plugin-calendar": "^5.
|
|
48
|
-
"@object-ui/plugin-charts": "^5.
|
|
49
|
-
"@object-ui/plugin-chatbot": "^5.
|
|
50
|
-
"@object-ui/plugin-dashboard": "^5.
|
|
51
|
-
"@object-ui/plugin-designer": "^5.
|
|
52
|
-
"@object-ui/plugin-detail": "^5.
|
|
53
|
-
"@object-ui/plugin-form": "^5.
|
|
54
|
-
"@object-ui/plugin-grid": "^5.
|
|
55
|
-
"@object-ui/plugin-kanban": "^5.
|
|
56
|
-
"@object-ui/plugin-list": "^5.
|
|
57
|
-
"@object-ui/plugin-report": "^5.
|
|
58
|
-
"@object-ui/plugin-view": "^5.
|
|
47
|
+
"@object-ui/plugin-calendar": "^5.2.1",
|
|
48
|
+
"@object-ui/plugin-charts": "^5.2.1",
|
|
49
|
+
"@object-ui/plugin-chatbot": "^5.2.1",
|
|
50
|
+
"@object-ui/plugin-dashboard": "^5.2.1",
|
|
51
|
+
"@object-ui/plugin-designer": "^5.2.1",
|
|
52
|
+
"@object-ui/plugin-detail": "^5.2.1",
|
|
53
|
+
"@object-ui/plugin-form": "^5.2.1",
|
|
54
|
+
"@object-ui/plugin-grid": "^5.2.1",
|
|
55
|
+
"@object-ui/plugin-kanban": "^5.2.1",
|
|
56
|
+
"@object-ui/plugin-list": "^5.2.1",
|
|
57
|
+
"@object-ui/plugin-report": "^5.2.1",
|
|
58
|
+
"@object-ui/plugin-view": "^5.2.1"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^25.9.0",
|
package/src/styles.css
CHANGED
|
@@ -14,3 +14,52 @@
|
|
|
14
14
|
* ancestor) instead of the OS `prefers-color-scheme`. Required so the
|
|
15
15
|
* in-app theme toggle fully controls light/dark UI under Tailwind v4. */
|
|
16
16
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
17
|
+
|
|
18
|
+
/* Shimmer keyframe + utility for skeleton loading placeholders.
|
|
19
|
+
*
|
|
20
|
+
* Usage: <div class="animate-shimmer ..."> or via ShimmerSkeleton
|
|
21
|
+
* from @object-ui/components.
|
|
22
|
+
*
|
|
23
|
+
* The keyframe slides a 200%-wide gradient across an element's
|
|
24
|
+
* background. Pair with `bg-[length:200%_100%]` and a gradient that
|
|
25
|
+
* uses the design-system muted tokens, e.g.
|
|
26
|
+
* bg-[linear-gradient(90deg,var(--muted),var(--muted-foreground)/10,var(--muted))]
|
|
27
|
+
*
|
|
28
|
+
* Respects `prefers-reduced-motion` automatically via the
|
|
29
|
+
* `motion-safe:` Tailwind variant on the consuming site.
|
|
30
|
+
*/
|
|
31
|
+
@keyframes shimmer {
|
|
32
|
+
0% { background-position: 200% 0; }
|
|
33
|
+
100% { background-position: -200% 0; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@utility animate-shimmer {
|
|
37
|
+
animation: shimmer 1.6s linear infinite;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Press feedback for shadcn-style Button components.
|
|
41
|
+
*
|
|
42
|
+
* The cva base in `packages/components/src/ui/button.tsx` always emits
|
|
43
|
+
* this 6-class combination on the outermost element:
|
|
44
|
+
* inline-flex + whitespace-nowrap + rounded-md + text-sm +
|
|
45
|
+
* font-medium + ring-offset-background
|
|
46
|
+
*
|
|
47
|
+
* That intersection is specific enough to scope the rule to Button
|
|
48
|
+
* (and `asChild` <a> Buttons) while leaving cmdk rows, SidebarNav
|
|
49
|
+
* links, native input[type=submit], and bare <button> in third-party
|
|
50
|
+
* libraries untouched.
|
|
51
|
+
*
|
|
52
|
+
* Gated on `prefers-reduced-motion: no-preference` so reduced-motion
|
|
53
|
+
* users see a hard click with no transform.
|
|
54
|
+
*
|
|
55
|
+
* Failure mode: if shadcn upstream changes the cva base class
|
|
56
|
+
* combination, the selector silently no-longer matches and we lose
|
|
57
|
+
* the animation — no UI breakage. Search the cva base string in
|
|
58
|
+
* `ui/button.tsx` to locate and update this rule when that happens.
|
|
59
|
+
*/
|
|
60
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
61
|
+
:is(button, a).inline-flex.whitespace-nowrap.rounded-md.text-sm.font-medium.ring-offset-background:not(:disabled):active {
|
|
62
|
+
transform: scale(0.97);
|
|
63
|
+
transition: transform 100ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { DashboardRendererProps } from '../types';
|
|
2
|
-
/**
|
|
3
|
-
* DashboardRenderer - Renders dashboard layouts from schema
|
|
4
|
-
*
|
|
5
|
-
* Framework-agnostic component that renders a dashboard based on JSON schema.
|
|
6
|
-
* Delegates to registered dashboard plugins.
|
|
7
|
-
*/
|
|
8
|
-
export declare function DashboardRenderer({ schema, dataSource, dashboardName, }: DashboardRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { SchemaRendererProvider } from '@object-ui/react';
|
|
3
|
-
import { useObjectTranslation } from '@object-ui/i18n';
|
|
4
|
-
/**
|
|
5
|
-
* DashboardRenderer - Renders dashboard layouts from schema
|
|
6
|
-
*
|
|
7
|
-
* Framework-agnostic component that renders a dashboard based on JSON schema.
|
|
8
|
-
* Delegates to registered dashboard plugins.
|
|
9
|
-
*/
|
|
10
|
-
export function DashboardRenderer({ schema, dataSource, dashboardName, }) {
|
|
11
|
-
const { t } = useObjectTranslation();
|
|
12
|
-
if (!schema) {
|
|
13
|
-
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: t('renderer.noDashboardSchema') }) }));
|
|
14
|
-
}
|
|
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 }) })] }) }));
|
|
16
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { FormRendererProps } from '../types';
|
|
2
|
-
/**
|
|
3
|
-
* FormRenderer - Renders forms (modal or inline)
|
|
4
|
-
*
|
|
5
|
-
* Framework-agnostic component that renders a form based on schema.
|
|
6
|
-
* Handles both create and edit modes.
|
|
7
|
-
*/
|
|
8
|
-
export declare function FormRenderer({ schema, dataSource, mode, recordId, onSuccess, onCancel, objectDef, }: FormRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { SchemaRendererProvider } from '@object-ui/react';
|
|
3
|
-
import { useObjectTranslation } from '@object-ui/i18n';
|
|
4
|
-
/**
|
|
5
|
-
* FormRenderer - Renders forms (modal or inline)
|
|
6
|
-
*
|
|
7
|
-
* Framework-agnostic component that renders a form based on schema.
|
|
8
|
-
* Handles both create and edit modes.
|
|
9
|
-
*/
|
|
10
|
-
export function FormRenderer({ schema, dataSource, mode = 'create', recordId, onSuccess, onCancel, objectDef, }) {
|
|
11
|
-
const { t } = useObjectTranslation();
|
|
12
|
-
if (!schema) {
|
|
13
|
-
return (_jsx("div", { className: "flex items-center justify-center p-4", children: _jsx("div", { className: "text-muted-foreground", children: t('renderer.noFormSchema') }) }));
|
|
14
|
-
}
|
|
15
|
-
const handleSubmit = async (data) => {
|
|
16
|
-
try {
|
|
17
|
-
if (mode === 'create' && objectDef) {
|
|
18
|
-
const result = await dataSource.create(objectDef.name, data);
|
|
19
|
-
onSuccess?.(result);
|
|
20
|
-
}
|
|
21
|
-
else if (mode === 'edit' && recordId && objectDef) {
|
|
22
|
-
const result = await dataSource.update(objectDef.name, recordId, data);
|
|
23
|
-
onSuccess?.(result);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
console.error('Form submission error:', error);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
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') }))] })] }) }));
|
|
31
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { ObjectRendererProps } from '../types';
|
|
2
|
-
/**
|
|
3
|
-
* ObjectRenderer - Renders object views (Grid, Kanban, List, etc.)
|
|
4
|
-
*
|
|
5
|
-
* Framework-agnostic component that renders an object view based on schema.
|
|
6
|
-
* Supports all view types registered in the ComponentRegistry.
|
|
7
|
-
*/
|
|
8
|
-
export declare function ObjectRenderer({ objectName, viewId, dataSource, onRecordClick: _onRecordClick, onEdit: _onEdit, objectDef: externalObjectDef, refreshKey, }: ObjectRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
3
|
-
import { SchemaRendererProvider } from '@object-ui/react';
|
|
4
|
-
/**
|
|
5
|
-
* ObjectRenderer - Renders object views (Grid, Kanban, List, etc.)
|
|
6
|
-
*
|
|
7
|
-
* Framework-agnostic component that renders an object view based on schema.
|
|
8
|
-
* Supports all view types registered in the ComponentRegistry.
|
|
9
|
-
*/
|
|
10
|
-
export function ObjectRenderer({ objectName, viewId, dataSource, onRecordClick: _onRecordClick, onEdit: _onEdit, objectDef: externalObjectDef, refreshKey = 0, }) {
|
|
11
|
-
const [objectDef, setObjectDef] = useState(externalObjectDef);
|
|
12
|
-
const [loading, setLoading] = useState(!externalObjectDef);
|
|
13
|
-
const [error, setError] = useState(null);
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (externalObjectDef) {
|
|
16
|
-
setObjectDef(externalObjectDef);
|
|
17
|
-
setLoading(false);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
// Fetch object metadata if not provided
|
|
21
|
-
if (dataSource.getMetadata) {
|
|
22
|
-
setLoading(true);
|
|
23
|
-
dataSource
|
|
24
|
-
.getMetadata()
|
|
25
|
-
.then((metadata) => {
|
|
26
|
-
const obj = metadata.objects?.find((o) => o.name === objectName);
|
|
27
|
-
if (obj) {
|
|
28
|
-
setObjectDef(obj);
|
|
29
|
-
setError(null);
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
setError(`Object "${objectName}" not found`);
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
.catch((err) => {
|
|
36
|
-
setError(err.message);
|
|
37
|
-
})
|
|
38
|
-
.finally(() => {
|
|
39
|
-
setLoading(false);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
setError('Data source does not support metadata fetching');
|
|
44
|
-
setLoading(false);
|
|
45
|
-
}
|
|
46
|
-
}, [objectName, dataSource, externalObjectDef]);
|
|
47
|
-
if (loading) {
|
|
48
|
-
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: "Loading\u2026" }) }));
|
|
49
|
-
}
|
|
50
|
-
if (error) {
|
|
51
|
-
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "text-destructive", children: ["Error: ", error] }) }));
|
|
52
|
-
}
|
|
53
|
-
if (!objectDef) {
|
|
54
|
-
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: "Object definition not found" }) }));
|
|
55
|
-
}
|
|
56
|
-
// Build view schema
|
|
57
|
-
const viewSchema = buildObjectViewSchema(objectDef, viewId);
|
|
58
|
-
return (_jsx(SchemaRendererProvider, { dataSource: dataSource, children: _jsx("div", { className: "object-renderer h-full", children: _jsxs("div", { className: "h-full p-4", children: [_jsx("h1", { className: "mb-4 text-2xl font-bold", children: objectDef.label || objectDef.name }), _jsxs("div", { className: "text-muted-foreground", children: ["View rendering for ", objectName, " (viewId: ", viewId || 'default', ")"] })] }) }, refreshKey) }));
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Helper to build view schema from object definition
|
|
62
|
-
*/
|
|
63
|
-
function buildObjectViewSchema(objectDef, viewId) {
|
|
64
|
-
// Find the requested view or use default
|
|
65
|
-
const view = viewId
|
|
66
|
-
? objectDef.views?.find((v) => v.id === viewId || v.name === viewId)
|
|
67
|
-
: objectDef.views?.[0];
|
|
68
|
-
return {
|
|
69
|
-
type: view?.type || 'grid',
|
|
70
|
-
objectName: objectDef.name,
|
|
71
|
-
viewId: view?.id,
|
|
72
|
-
...view,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { PageRendererProps } from '../types';
|
|
2
|
-
/**
|
|
3
|
-
* PageRenderer - Renders custom page schemas
|
|
4
|
-
*
|
|
5
|
-
* Framework-agnostic component that renders a page based on JSON schema.
|
|
6
|
-
*/
|
|
7
|
-
export declare function PageRenderer({ schema, pageName }: PageRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useObjectTranslation } from '@object-ui/i18n';
|
|
3
|
-
/**
|
|
4
|
-
* PageRenderer - Renders custom page schemas
|
|
5
|
-
*
|
|
6
|
-
* Framework-agnostic component that renders a page based on JSON schema.
|
|
7
|
-
*/
|
|
8
|
-
export function PageRenderer({ schema, pageName }) {
|
|
9
|
-
const { t } = useObjectTranslation();
|
|
10
|
-
if (!schema) {
|
|
11
|
-
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: t('renderer.noPageSchema') }) }));
|
|
12
|
-
}
|
|
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 }) })] }));
|
|
14
|
-
}
|