@object-ui/app-shell 5.0.2 → 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 +401 -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 +25 -21
- package/dist/layout/AppSidebar.js +15 -11
- package/dist/layout/InboxPopover.js +43 -3
- package/dist/types.d.ts +0 -46
- package/dist/views/ObjectView.js +20 -7
- package/dist/views/RecordDetailView.js +213 -45
- 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
|
@@ -7,15 +7,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
* the object field definitions.
|
|
8
8
|
*/
|
|
9
9
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
10
|
-
import { useParams, useNavigate } from 'react-router-dom';
|
|
11
|
-
import { DetailView, RecordChatterPanel, buildDefaultPageSchema } from '@object-ui/plugin-detail';
|
|
10
|
+
import { useParams, useNavigate, useLocation, Link } from 'react-router-dom';
|
|
11
|
+
import { DetailView, RecordChatterPanel, buildDefaultPageSchema, extractMentions } from '@object-ui/plugin-detail';
|
|
12
12
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
13
|
-
import { PresenceAvatars } from '@object-ui/collaboration';
|
|
14
13
|
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
|
|
15
14
|
import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider } from '@object-ui/react';
|
|
16
15
|
import { buildExpandFields } from '@object-ui/core';
|
|
17
16
|
import { toast } from 'sonner';
|
|
18
|
-
import {
|
|
17
|
+
import { useRecordPresence, PresenceAvatars } from '@object-ui/collaboration';
|
|
18
|
+
import { Database, ChevronLeft } from 'lucide-react';
|
|
19
19
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
20
20
|
import { SkeletonDetail } from '../skeletons';
|
|
21
21
|
import { ManagedByBadge } from '../components/ManagedByBadge';
|
|
@@ -50,6 +50,24 @@ const AUDIT_FIELD_NAMES = new Set(['created_at', 'created_by', 'updated_at', 'up
|
|
|
50
50
|
const HIDDEN_SYSTEM_FIELD_NAMES = new Set([
|
|
51
51
|
'organization_id', 'tenant_id', 'is_deleted', 'deleted_at',
|
|
52
52
|
]);
|
|
53
|
+
/**
|
|
54
|
+
* Field-type signals that suggest a "secondary / system / metadata"
|
|
55
|
+
* placement when auto-grouping fields. These move out of the main
|
|
56
|
+
* section and into a collapsible "More details" section by default,
|
|
57
|
+
* keeping the primary section dense with business-critical fields.
|
|
58
|
+
*
|
|
59
|
+
* The heuristic is conservative: when no objectDef metadata is available
|
|
60
|
+
* we surface most fields in the main section; long-form text and
|
|
61
|
+
* audit-by-name fields drop down.
|
|
62
|
+
*/
|
|
63
|
+
const SECONDARY_FIELD_NAME_HINTS = ['description', 'notes', 'note', 'remark', 'remarks', 'comments'];
|
|
64
|
+
const SECONDARY_FIELD_TYPES = new Set(['textarea', 'markdown', 'html', 'rich-text', 'json', 'code']);
|
|
65
|
+
function isSecondaryField(fieldName, fieldDef) {
|
|
66
|
+
if (SECONDARY_FIELD_TYPES.has(fieldDef?.type))
|
|
67
|
+
return true;
|
|
68
|
+
const lc = fieldName.toLowerCase();
|
|
69
|
+
return SECONDARY_FIELD_NAME_HINTS.some((hint) => lc === hint || lc.endsWith(`_${hint}`));
|
|
70
|
+
}
|
|
53
71
|
export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverride, recordIdOverride, embedded }) {
|
|
54
72
|
const params = useParams();
|
|
55
73
|
const appName = params.appName;
|
|
@@ -58,13 +76,15 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
58
76
|
const { showDebug } = useMetadataInspector();
|
|
59
77
|
const { user } = useAuth();
|
|
60
78
|
const navigate = useNavigate();
|
|
79
|
+
const location = useLocation();
|
|
80
|
+
const originFrom = location.state?.from;
|
|
61
81
|
const { t } = useObjectTranslation();
|
|
62
82
|
const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
|
|
63
83
|
const { isFavorite, toggleFavorite, refreshLabel: refreshFavoriteLabel } = useFavorites();
|
|
64
84
|
const { addRecentItem } = useRecentItems();
|
|
65
85
|
const [isLoading, setIsLoading] = useState(true);
|
|
66
86
|
const [feedItems, setFeedItems] = useState([]);
|
|
67
|
-
const [
|
|
87
|
+
const [mentionSuggestions, setMentionSuggestions] = useState([]);
|
|
68
88
|
const [actionRefreshKey, setActionRefreshKey] = useState(0);
|
|
69
89
|
const [childRelatedData, setChildRelatedData] = useState({});
|
|
70
90
|
const [historyEntries, setHistoryEntries] = useState(null);
|
|
@@ -79,6 +99,13 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
79
99
|
// Navigation code passes `record.id || record._id` directly into the URL
|
|
80
100
|
// without adding any prefix, so no stripping is needed.
|
|
81
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);
|
|
82
109
|
const favoriteRecord = useMemo(() => {
|
|
83
110
|
if (!objectName || !pureRecordId)
|
|
84
111
|
return null;
|
|
@@ -140,10 +167,16 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
140
167
|
}, [renderViaSchemaFlag, assignedPage, assignedSlots, objectDef]);
|
|
141
168
|
const effectivePage = assignedPage || synthesizedPage;
|
|
142
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');
|
|
143
175
|
useEffect(() => {
|
|
144
176
|
let cancelled = false;
|
|
145
177
|
if (!effectivePage || !pureRecordId || !objectName || !dataSource?.findOne) {
|
|
146
178
|
setPageRecord(null);
|
|
179
|
+
setPageRecordStatus('idle');
|
|
147
180
|
return;
|
|
148
181
|
}
|
|
149
182
|
// Expand lookup/master_detail fields so the page receives display
|
|
@@ -152,17 +185,28 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
152
185
|
const expandFields = buildExpandFields(objectDef?.fields);
|
|
153
186
|
const params = expandFields.length > 0 ? { $expand: expandFields } : undefined;
|
|
154
187
|
const loadRecord = () => {
|
|
188
|
+
setPageRecordStatus('loading');
|
|
155
189
|
const findOnePromise = params
|
|
156
190
|
? dataSource.findOne(objectName, pureRecordId, params)
|
|
157
191
|
: dataSource.findOne(objectName, pureRecordId);
|
|
158
192
|
findOnePromise
|
|
159
193
|
.then((rec) => {
|
|
160
|
-
if (
|
|
194
|
+
if (cancelled)
|
|
195
|
+
return;
|
|
196
|
+
if (rec && typeof rec === 'object') {
|
|
161
197
|
setPageRecord(rec);
|
|
198
|
+
setPageRecordStatus('loaded');
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
setPageRecord(null);
|
|
202
|
+
setPageRecordStatus('missing');
|
|
203
|
+
}
|
|
162
204
|
})
|
|
163
205
|
.catch(() => {
|
|
164
|
-
if (
|
|
165
|
-
|
|
206
|
+
if (cancelled)
|
|
207
|
+
return;
|
|
208
|
+
setPageRecord(null);
|
|
209
|
+
setPageRecordStatus('missing');
|
|
166
210
|
});
|
|
167
211
|
};
|
|
168
212
|
loadRecord();
|
|
@@ -553,20 +597,74 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
553
597
|
});
|
|
554
598
|
return () => { cancelled = true; };
|
|
555
599
|
}, [dataSource, pureRecordId, objectDef, historyEnabled]);
|
|
600
|
+
// Fetch a directory of active users once per dataSource mount and expose
|
|
601
|
+
// them as @-mention suggestions to the DiscussionContext. Capped at 50 to
|
|
602
|
+
// keep the dropdown tight; hosts can swap in a paginated/server-search
|
|
603
|
+
// implementation later by mounting their own DiscussionContextProvider.
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
if (!dataSource)
|
|
606
|
+
return;
|
|
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
|
+
: [];
|
|
618
|
+
(async () => {
|
|
619
|
+
try {
|
|
620
|
+
const res = await dataSource.find('sys_user', {
|
|
621
|
+
$top: 50,
|
|
622
|
+
$select: ['id', 'name', 'email', 'image'],
|
|
623
|
+
});
|
|
624
|
+
if (cancelled)
|
|
625
|
+
return;
|
|
626
|
+
const rows = Array.isArray(res) ? res : res?.data || [];
|
|
627
|
+
const fetched = rows
|
|
628
|
+
.map((u) => ({
|
|
629
|
+
id: String(u.id),
|
|
630
|
+
label: u.name || u.email || String(u.id),
|
|
631
|
+
avatarUrl: u.image || undefined,
|
|
632
|
+
}))
|
|
633
|
+
.filter((s) => s.label);
|
|
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);
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
if (cancelled)
|
|
644
|
+
return;
|
|
645
|
+
// Fall back to just the current user so mentions still work.
|
|
646
|
+
setMentionSuggestions(selfSuggestion);
|
|
647
|
+
}
|
|
648
|
+
})();
|
|
649
|
+
return () => { cancelled = true; };
|
|
650
|
+
}, [dataSource, user?.id, user?.name, user?.email]);
|
|
556
651
|
// Memoize so the object identity is stable across renders — otherwise
|
|
557
652
|
// any effect that depends on it (e.g. the feed loader below) would
|
|
558
653
|
// re-fire every render and create an infinite request loop.
|
|
559
654
|
const currentUser = useMemo(() => (user ? { id: user.id, name: user.name, avatar: user.image } : FALLBACK_USER), [user?.id, user?.name, user?.image]);
|
|
560
|
-
// Fetch
|
|
655
|
+
// Fetch comments from API.
|
|
656
|
+
//
|
|
657
|
+
// NOTE: Record-level presence ("who else is viewing this record") used to
|
|
658
|
+
// be probed here by `dataSource.find('sys_presence', …)`, but that was an
|
|
659
|
+
// architectural mistake: presence is real-time ephemeral state and does
|
|
660
|
+
// not belong in a regular REST collection. The probe has been removed
|
|
661
|
+
// pending a proper transport-level design (WebSocket-backed
|
|
662
|
+
// `<PresenceProvider>` in @object-ui/collaboration). See ROADMAP for the
|
|
663
|
+
// realtime / OCC plan.
|
|
561
664
|
useEffect(() => {
|
|
562
665
|
if (!dataSource || !objectName || !pureRecordId)
|
|
563
666
|
return;
|
|
564
667
|
const threadId = `${objectName}:${pureRecordId}`;
|
|
565
|
-
// Fetch record viewers
|
|
566
|
-
dataSource.find('sys_presence', { $filter: { recordId: pureRecordId } })
|
|
567
|
-
.then((res) => { if (res.data?.length)
|
|
568
|
-
setRecordViewers(res.data); })
|
|
569
|
-
.catch(() => { });
|
|
570
668
|
// M10.10: Fetch persisted comments from sys_comment. Field names
|
|
571
669
|
// are snake_case to match the platform-objects schema
|
|
572
670
|
// (`packages/platform-objects/src/audit/sys-comment.object.ts`):
|
|
@@ -705,6 +803,15 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
705
803
|
})
|
|
706
804
|
.catch(() => { });
|
|
707
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
|
+
*/
|
|
708
815
|
const handleAddComment = useCallback(async (text) => {
|
|
709
816
|
const newItem = {
|
|
710
817
|
id: crypto.randomUUID(),
|
|
@@ -718,6 +825,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
718
825
|
// Persist to backend (M10.10: snake_case fields per sys_comment schema)
|
|
719
826
|
if (dataSource) {
|
|
720
827
|
const threadId = `${objectName}:${pureRecordId}`;
|
|
828
|
+
const mentionIds = extractMentions(text, mentionSuggestions);
|
|
721
829
|
dataSource.create('sys_comment', {
|
|
722
830
|
id: newItem.id,
|
|
723
831
|
thread_id: threadId,
|
|
@@ -725,11 +833,11 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
725
833
|
author_name: currentUser.name,
|
|
726
834
|
author_avatar_url: 'avatar' in currentUser ? currentUser.avatar : undefined,
|
|
727
835
|
body: text,
|
|
728
|
-
mentions:
|
|
836
|
+
mentions: JSON.stringify(mentionIds),
|
|
729
837
|
created_at: newItem.createdAt,
|
|
730
838
|
}).catch(() => { });
|
|
731
839
|
}
|
|
732
|
-
}, [currentUser, dataSource, objectName, pureRecordId]);
|
|
840
|
+
}, [currentUser, dataSource, objectName, pureRecordId, mentionSuggestions]);
|
|
733
841
|
const handleAddReply = useCallback(async (parentId, text) => {
|
|
734
842
|
const newItem = {
|
|
735
843
|
id: crypto.randomUUID(),
|
|
@@ -749,6 +857,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
749
857
|
});
|
|
750
858
|
if (dataSource) {
|
|
751
859
|
const threadId = `${objectName}:${pureRecordId}`;
|
|
860
|
+
const mentionIds = extractMentions(text, mentionSuggestions);
|
|
752
861
|
dataSource.create('sys_comment', {
|
|
753
862
|
id: newItem.id,
|
|
754
863
|
thread_id: threadId,
|
|
@@ -756,12 +865,12 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
756
865
|
author_name: currentUser.name,
|
|
757
866
|
author_avatar_url: 'avatar' in currentUser ? currentUser.avatar : undefined,
|
|
758
867
|
body: text,
|
|
759
|
-
mentions:
|
|
868
|
+
mentions: JSON.stringify(mentionIds),
|
|
760
869
|
created_at: newItem.createdAt,
|
|
761
870
|
parent_id: parentId,
|
|
762
871
|
}).catch(() => { });
|
|
763
872
|
}
|
|
764
|
-
}, [currentUser, dataSource, objectName, pureRecordId]);
|
|
873
|
+
}, [currentUser, dataSource, objectName, pureRecordId, mentionSuggestions]);
|
|
765
874
|
const handleToggleReaction = useCallback((itemId, emoji) => {
|
|
766
875
|
setFeedItems(prev => prev.map(item => {
|
|
767
876
|
if (item.id !== itemId)
|
|
@@ -867,29 +976,55 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
867
976
|
};
|
|
868
977
|
}),
|
|
869
978
|
}))
|
|
870
|
-
:
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
979
|
+
: (() => {
|
|
980
|
+
// Auto-grouping (platform B): when no form sections are authored,
|
|
981
|
+
// split fields into a primary section and a collapsible
|
|
982
|
+
// "More details" section so long-form/secondary fields don't
|
|
983
|
+
// dilute the main grid. The primary section stays untitled so
|
|
984
|
+
// DetailSection still flattens its chrome when alone.
|
|
985
|
+
const allFields = Object.keys(objectDef.fields || {})
|
|
986
|
+
.filter((key) => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden);
|
|
987
|
+
const toField = (key) => {
|
|
988
|
+
const fieldDef = objectDef.fields[key];
|
|
989
|
+
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
990
|
+
return {
|
|
991
|
+
name: key,
|
|
992
|
+
label: fieldDef.label || key,
|
|
993
|
+
type: fieldDef.type || 'text',
|
|
994
|
+
...(fieldDef.options && { options: fieldDef.options }),
|
|
995
|
+
...(refTarget && { reference_to: refTarget }),
|
|
996
|
+
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
|
|
997
|
+
...(fieldDef.currency && { currency: fieldDef.currency }),
|
|
998
|
+
};
|
|
999
|
+
};
|
|
1000
|
+
const primaryKeys = allFields.filter((k) => !isSecondaryField(k, objectDef.fields[k]));
|
|
1001
|
+
const secondaryKeys = allFields.filter((k) => isSecondaryField(k, objectDef.fields[k]));
|
|
1002
|
+
// Below ~6 primary fields the second section often looks awkward
|
|
1003
|
+
// — keep the legacy single-untitled-section behaviour. Also
|
|
1004
|
+
// honour the "no secondary fields" case the same way.
|
|
1005
|
+
if (secondaryKeys.length === 0 || primaryKeys.length === 0) {
|
|
1006
|
+
return [
|
|
1007
|
+
{
|
|
1008
|
+
showBorder: false,
|
|
1009
|
+
fields: allFields.map(toField),
|
|
1010
|
+
},
|
|
1011
|
+
];
|
|
1012
|
+
}
|
|
1013
|
+
return [
|
|
1014
|
+
{
|
|
1015
|
+
showBorder: false,
|
|
1016
|
+
fields: primaryKeys.map(toField),
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
name: 'details',
|
|
1020
|
+
title: sectionLabel(objectDef.name, 'details', t('detail.sectionMoreDetails', 'More details')),
|
|
1021
|
+
collapsible: true,
|
|
1022
|
+
defaultCollapsed: false,
|
|
1023
|
+
showBorder: true,
|
|
1024
|
+
fields: secondaryKeys.map(toField),
|
|
1025
|
+
},
|
|
1026
|
+
];
|
|
1027
|
+
})();
|
|
893
1028
|
// Audit fields (created_at/created_by/updated_at/updated_by) are NOT
|
|
894
1029
|
// appended as a section here — they are surfaced by `<RecordMetaFooter>`
|
|
895
1030
|
// (rendered by DetailView) as a single subtle line below the content,
|
|
@@ -1106,6 +1241,16 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1106
1241
|
if (!objectDef) {
|
|
1107
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 }) })] }) }));
|
|
1108
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
|
+
}
|
|
1109
1254
|
if (effectivePage) {
|
|
1110
1255
|
const disableDiscussion = effectivePage?.disableDiscussion === true;
|
|
1111
1256
|
// When the page schema embeds an explicit `record:discussion` /
|
|
@@ -1143,6 +1288,24 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1143
1288
|
const affordances = resolveCrudAffordances(objectDef);
|
|
1144
1289
|
const items = [];
|
|
1145
1290
|
if (affordances.edit) {
|
|
1291
|
+
// Inline-edit toggle. Surfaced ABOVE `sys_edit` so the
|
|
1292
|
+
// overflow menu lists field-level editing first — Lightning /
|
|
1293
|
+
// HubSpot put inline edit ("Edit details") above the modal /
|
|
1294
|
+
// form-page edit because in-page editing is the higher-frequency
|
|
1295
|
+
// interaction. Communicates with DetailView via a window event
|
|
1296
|
+
// so we don't need to lift inline-edit state out of the plugin.
|
|
1297
|
+
items.push({
|
|
1298
|
+
name: 'sys_inline_edit',
|
|
1299
|
+
label: t('detail.editFieldsInline', { defaultValue: 'Edit fields' }),
|
|
1300
|
+
type: 'script',
|
|
1301
|
+
locations: ['record_header'],
|
|
1302
|
+
variant: 'outline',
|
|
1303
|
+
onClick: () => {
|
|
1304
|
+
window.dispatchEvent(new CustomEvent('objectui:record:inline-edit-toggle', {
|
|
1305
|
+
detail: { recordId: pureRecordId, objectName },
|
|
1306
|
+
}));
|
|
1307
|
+
},
|
|
1308
|
+
});
|
|
1146
1309
|
items.push({
|
|
1147
1310
|
name: 'sys_edit',
|
|
1148
1311
|
label: t('detail.edit', { defaultValue: 'Edit' }),
|
|
@@ -1191,7 +1354,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1191
1354
|
label: t('detail.delete', { defaultValue: 'Delete' }),
|
|
1192
1355
|
type: 'script',
|
|
1193
1356
|
locations: ['record_header'],
|
|
1194
|
-
variant: '
|
|
1357
|
+
variant: 'destructive',
|
|
1195
1358
|
onClick: async () => {
|
|
1196
1359
|
const msg = t('detail.deleteConfirmation', {
|
|
1197
1360
|
defaultValue: 'Are you sure you want to delete this record?',
|
|
@@ -1250,9 +1413,14 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1250
1413
|
headerActions: synthHeaderActions,
|
|
1251
1414
|
related: synthRelated,
|
|
1252
1415
|
history: synthHistory,
|
|
1416
|
+
// Per-object opt-outs read from `objectDef.detail.*`. Lets
|
|
1417
|
+
// catalog/atomic objects (product, task, ...) keep a focused
|
|
1418
|
+
// single-column layout instead of inheriting the rail.
|
|
1419
|
+
hideReferenceRail: objectDef?.detail?.hideReferenceRail === true || undefined,
|
|
1420
|
+
hideRelatedTab: objectDef?.detail?.hideRelatedTab === true || undefined,
|
|
1253
1421
|
...(assignedSlots ? { slots: assignedSlots } : {}),
|
|
1254
1422
|
});
|
|
1255
|
-
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: [
|
|
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: {
|
|
1256
1424
|
position: 'bottom',
|
|
1257
1425
|
collapsible: false,
|
|
1258
1426
|
feed: {
|
|
@@ -1260,7 +1428,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1260
1428
|
enableThreading: true,
|
|
1261
1429
|
showCommentInput: true,
|
|
1262
1430
|
},
|
|
1263
|
-
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) }))] }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Page Schema', data: renderedPage }] })] }) }) }) }) }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
|
|
1431
|
+
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, mentionSuggestions: mentionSuggestions }) }))] }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Page Schema', data: renderedPage }] })] }) }) }) }) }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
|
|
1264
1432
|
if (!open)
|
|
1265
1433
|
setConfirmState(s => ({ ...s, open: false }));
|
|
1266
1434
|
} }), _jsx(ActionParamDialog, { state: paramState, onOpenChange: (open) => {
|
|
@@ -1268,7 +1436,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1268
1436
|
setParamState(s => ({ ...s, open: false }));
|
|
1269
1437
|
} })] }));
|
|
1270
1438
|
}
|
|
1271
|
-
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: [
|
|
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) => {
|
|
1272
1440
|
if (!record || typeof record !== 'object')
|
|
1273
1441
|
return;
|
|
1274
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;
|