@object-ui/app-shell 4.2.1 → 4.4.0
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 +125 -0
- package/dist/components/ManagedByBadge.d.ts +10 -0
- package/dist/components/ManagedByBadge.js +44 -0
- package/dist/console/AppContent.js +2 -2
- package/dist/console/home/HomePage.js +1 -2
- package/dist/console/organizations/OrganizationsPage.js +28 -2
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useRecordApprovals.d.ts +49 -0
- package/dist/hooks/useRecordApprovals.js +132 -0
- package/dist/layout/ActivityFeed.js +3 -3
- package/dist/layout/AppHeader.d.ts +1 -1
- package/dist/layout/AppHeader.js +3 -16
- package/dist/layout/ConsoleLayout.js +1 -1
- package/dist/layout/InboxPopover.d.ts +36 -0
- package/dist/layout/InboxPopover.js +84 -0
- package/dist/utils/index.js +17 -1
- package/dist/utils/managedByEmptyState.d.ts +29 -0
- package/dist/utils/managedByEmptyState.js +24 -0
- package/dist/utils/resolveActionParams.d.ts +86 -0
- package/dist/utils/resolveActionParams.js +77 -0
- package/dist/views/ObjectView.js +140 -12
- package/dist/views/RecordDetailView.js +229 -40
- package/dist/views/RecordFormPage.js +2 -2
- package/dist/views/ReportView.js +21 -19
- package/package.json +24 -24
- package/dist/components/ManagedByBanner.d.ts +0 -16
- package/dist/components/ManagedByBanner.js +0 -38
|
@@ -6,44 +6,61 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* Uses the DetailView plugin component with auto-generated sections from
|
|
7
7
|
* the object field definitions.
|
|
8
8
|
*/
|
|
9
|
-
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
9
|
+
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
10
10
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
11
11
|
import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
|
|
12
12
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
13
13
|
import { PresenceAvatars } from '@object-ui/collaboration';
|
|
14
14
|
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
|
|
15
|
-
import { ActionProvider, useObjectTranslation, useObjectLabel } from '@object-ui/react';
|
|
15
|
+
import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer } from '@object-ui/react';
|
|
16
|
+
import { buildExpandFields } from '@object-ui/core';
|
|
16
17
|
import { toast } from 'sonner';
|
|
17
18
|
import { Database, Users } from 'lucide-react';
|
|
18
19
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
19
20
|
import { SkeletonDetail } from '../skeletons';
|
|
20
|
-
import {
|
|
21
|
+
import { ManagedByBadge } from '../components/ManagedByBadge';
|
|
21
22
|
import { resolveCrudAffordances } from '../utils/crudAffordances';
|
|
22
23
|
import { ActionConfirmDialog } from './ActionConfirmDialog';
|
|
23
24
|
import { ActionParamDialog } from './ActionParamDialog';
|
|
25
|
+
import { resolveActionParams } from '../utils/resolveActionParams';
|
|
24
26
|
import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
|
|
27
|
+
import { useRecordApprovals } from '../hooks/useRecordApprovals';
|
|
25
28
|
import { getRecordDisplayName } from '../utils';
|
|
26
29
|
const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
|
|
27
30
|
/**
|
|
28
31
|
* Audit field names auto-injected by the framework's `applySystemFields`.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
32
|
+
* Filtered out of the auto-generated body sections — they are rendered
|
|
33
|
+
* separately as a single subtle one-line `<RecordMetaFooter>` (see
|
|
34
|
+
* `@object-ui/plugin-detail`) so provenance stays discoverable without a
|
|
35
|
+
* heavy "System Information" panel. The inline-edit drawer also hides
|
|
36
|
+
* them via `DEFAULT_SYSTEM_FIELDS` in
|
|
37
|
+
* `@object-ui/plugin-detail/RecordDetailDrawer`.
|
|
33
38
|
*/
|
|
34
39
|
const AUDIT_FIELD_NAMES = new Set(['created_at', 'created_by', 'updated_at', 'updated_by']);
|
|
40
|
+
/**
|
|
41
|
+
* System/tenant fields that the framework auto-injects on every record but
|
|
42
|
+
* which carry no business value on a detail page. Hidden from the
|
|
43
|
+
* auto-generated section (when the object has no explicit form sections).
|
|
44
|
+
* Authors who really want to show these can still list them in
|
|
45
|
+
* `objectDef.views.form.sections`.
|
|
46
|
+
*/
|
|
47
|
+
const HIDDEN_SYSTEM_FIELD_NAMES = new Set([
|
|
48
|
+
'organization_id', 'tenant_id', 'is_deleted', 'deleted_at',
|
|
49
|
+
]);
|
|
35
50
|
export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
36
51
|
const { appName, objectName, recordId } = useParams();
|
|
37
52
|
const { showDebug } = useMetadataInspector();
|
|
38
53
|
const { user } = useAuth();
|
|
39
54
|
const navigate = useNavigate();
|
|
40
55
|
const { t } = useObjectTranslation();
|
|
41
|
-
const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess } = useObjectLabel();
|
|
56
|
+
const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
|
|
42
57
|
const [isLoading, setIsLoading] = useState(true);
|
|
43
58
|
const [feedItems, setFeedItems] = useState([]);
|
|
44
59
|
const [recordViewers, setRecordViewers] = useState([]);
|
|
45
60
|
const [actionRefreshKey, setActionRefreshKey] = useState(0);
|
|
46
61
|
const [childRelatedData, setChildRelatedData] = useState({});
|
|
62
|
+
const [historyEntries, setHistoryEntries] = useState(null);
|
|
63
|
+
const [historyLoading, setHistoryLoading] = useState(false);
|
|
47
64
|
const [recordTitle, setRecordTitle] = useState();
|
|
48
65
|
const objectDef = objects.find((o) => o.name === objectName);
|
|
49
66
|
// Publish record title to the navigation context so the top-bar breadcrumb
|
|
@@ -53,6 +70,40 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
53
70
|
// Navigation code passes `record.id || record._id` directly into the URL
|
|
54
71
|
// without adding any prefix, so no stripping is needed.
|
|
55
72
|
const pureRecordId = recordId;
|
|
73
|
+
// ─── Page Assignment (Salesforce Lightning-style record Pages) ──────
|
|
74
|
+
// If a PageSchema(pageType='record') is authored for this object, render
|
|
75
|
+
// it via SchemaRenderer (which dispatches to the registered 'record'
|
|
76
|
+
// PageRenderer in @object-ui/components). Otherwise we fall through to
|
|
77
|
+
// the legacy auto-generated DetailView path below.
|
|
78
|
+
const { page: assignedPage } = usePageAssignment(objectName);
|
|
79
|
+
const [pageRecord, setPageRecord] = useState(null);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
let cancelled = false;
|
|
82
|
+
if (!assignedPage || !pureRecordId || !objectName || !dataSource?.findOne) {
|
|
83
|
+
setPageRecord(null);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Expand lookup/master_detail fields so the page receives display
|
|
87
|
+
// names (e.g. account.name) rather than raw foreign-key IDs. The
|
|
88
|
+
// page subtitle interpolation and record:* renderers depend on this.
|
|
89
|
+
const expandFields = buildExpandFields(objectDef?.fields);
|
|
90
|
+
const params = expandFields.length > 0 ? { $expand: expandFields } : undefined;
|
|
91
|
+
const findOnePromise = params
|
|
92
|
+
? dataSource.findOne(objectName, pureRecordId, params)
|
|
93
|
+
: dataSource.findOne(objectName, pureRecordId);
|
|
94
|
+
findOnePromise
|
|
95
|
+
.then((rec) => {
|
|
96
|
+
if (!cancelled)
|
|
97
|
+
setPageRecord(rec);
|
|
98
|
+
})
|
|
99
|
+
.catch(() => {
|
|
100
|
+
if (!cancelled)
|
|
101
|
+
setPageRecord(null);
|
|
102
|
+
});
|
|
103
|
+
return () => {
|
|
104
|
+
cancelled = true;
|
|
105
|
+
};
|
|
106
|
+
}, [assignedPage, objectName, pureRecordId, dataSource, objectDef]);
|
|
56
107
|
// ─── Action Provider Handlers ───────────────────────────────────────
|
|
57
108
|
// Confirm dialog state (promise-based)
|
|
58
109
|
const [confirmState, setConfirmState] = useState({ open: false, message: '' });
|
|
@@ -65,9 +116,15 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
65
116
|
}, []);
|
|
66
117
|
const paramCollectionHandler = useCallback((params) => {
|
|
67
118
|
return new Promise((resolve) => {
|
|
68
|
-
|
|
119
|
+
const resolved = resolveActionParams(params, {
|
|
120
|
+
objectName: objectName || objectDef?.name || '',
|
|
121
|
+
objects: objects || [],
|
|
122
|
+
fieldLabel,
|
|
123
|
+
fieldOptionLabel,
|
|
124
|
+
});
|
|
125
|
+
setParamState({ open: true, params: resolved, resolve });
|
|
69
126
|
});
|
|
70
|
-
}, []);
|
|
127
|
+
}, [objectName, objectDef, objects, fieldLabel, fieldOptionLabel]);
|
|
71
128
|
const toastHandler = useCallback((message, options) => {
|
|
72
129
|
if (options?.type === 'error')
|
|
73
130
|
toast.error(message);
|
|
@@ -187,6 +244,41 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
187
244
|
return { success: false, error: error.message };
|
|
188
245
|
}
|
|
189
246
|
}, [authFetch, pureRecordId, objectName]);
|
|
247
|
+
// ─── Approvals ─────────────────────────────────────────────────────
|
|
248
|
+
// Surfaces "Submit for Approval" / "Recall" buttons on the record header
|
|
249
|
+
// when an active approval process is registered for this object, and a
|
|
250
|
+
// status badge when a request exists.
|
|
251
|
+
const approvals = useRecordApprovals(objectName, pureRecordId, user?.id);
|
|
252
|
+
// Hold latest approvals snapshot in a ref so the action handler
|
|
253
|
+
// (memoized once inside ActionRunner) always sees fresh state instead of
|
|
254
|
+
// the stale closure captured at the first render.
|
|
255
|
+
const approvalsRef = useRef(approvals);
|
|
256
|
+
approvalsRef.current = approvals;
|
|
257
|
+
const approvalHandler = useCallback(async (action) => {
|
|
258
|
+
const target = action.target || action.name;
|
|
259
|
+
const params = (action.params && !Array.isArray(action.params))
|
|
260
|
+
? action.params
|
|
261
|
+
: {};
|
|
262
|
+
try {
|
|
263
|
+
if (target === 'submit_approval') {
|
|
264
|
+
await approvalsRef.current.submit({
|
|
265
|
+
processName: params.processName,
|
|
266
|
+
comment: params.comment,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
else if (target === 'recall_approval') {
|
|
270
|
+
await approvalsRef.current.recall({ comment: params.comment });
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
return { success: false, error: `Unknown approval target: ${target}` };
|
|
274
|
+
}
|
|
275
|
+
setActionRefreshKey((k) => k + 1);
|
|
276
|
+
return { success: true, reload: true };
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
return { success: false, error: err?.message || String(err) };
|
|
280
|
+
}
|
|
281
|
+
}, []);
|
|
190
282
|
// Discover reverse references: other objects with lookup/master_detail fields
|
|
191
283
|
// pointing to the current object (e.g., order_item.order → order).
|
|
192
284
|
const childRelations = useMemo(() => {
|
|
@@ -236,6 +328,61 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
236
328
|
});
|
|
237
329
|
return () => { cancelled = true; };
|
|
238
330
|
}, [dataSource, pureRecordId, childRelations]);
|
|
331
|
+
// ── Audit history fetch ────────────────────────────────────────────
|
|
332
|
+
// Loads recent sys_audit_log entries for this record so the DetailView can
|
|
333
|
+
// render a read-only "History" tab. Gated on three preconditions to keep
|
|
334
|
+
// the network and the UI quiet for objects that opt out of history:
|
|
335
|
+
// 1) trackHistory must be explicitly true on the object capabilities
|
|
336
|
+
// (the framework default is false, so we never speculatively fetch).
|
|
337
|
+
// 2) sys_audit_log must be present in the registered objects list — if
|
|
338
|
+
// the platform-objects package isn't deployed the tab makes no sense.
|
|
339
|
+
// 3) The object being viewed must not be sys_audit_log itself, to avoid
|
|
340
|
+
// a recursive tab on the audit log detail page.
|
|
341
|
+
// We request only the safe projection (created_at, action, user_id) so the
|
|
342
|
+
// browser never receives serialized old_value/new_value payloads, which
|
|
343
|
+
// can contain restricted fields. Field-level redaction in PR2 will harden
|
|
344
|
+
// this further once a backend-scoped audit endpoint exists.
|
|
345
|
+
const historyEnabled = useMemo(() => {
|
|
346
|
+
if (!objectDef)
|
|
347
|
+
return false;
|
|
348
|
+
if (objectDef.name === 'sys_audit_log')
|
|
349
|
+
return false;
|
|
350
|
+
if (objectDef.enable?.trackHistory !== true)
|
|
351
|
+
return false;
|
|
352
|
+
return objects.some((o) => o.name === 'sys_audit_log');
|
|
353
|
+
}, [objectDef, objects]);
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
if (!dataSource || !pureRecordId || !objectDef || !historyEnabled) {
|
|
356
|
+
setHistoryEntries(null);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
let cancelled = false;
|
|
360
|
+
setHistoryLoading(true);
|
|
361
|
+
dataSource
|
|
362
|
+
.find('sys_audit_log', {
|
|
363
|
+
$filter: { record_id: pureRecordId, object_name: objectDef.name },
|
|
364
|
+
$orderby: { created_at: 'desc' },
|
|
365
|
+
$top: 50,
|
|
366
|
+
$select: ['id', 'created_at', 'action', 'user_id'],
|
|
367
|
+
})
|
|
368
|
+
.then((res) => {
|
|
369
|
+
if (cancelled)
|
|
370
|
+
return;
|
|
371
|
+
const items = Array.isArray(res) ? res : res?.data || [];
|
|
372
|
+
setHistoryEntries(items);
|
|
373
|
+
})
|
|
374
|
+
.catch((err) => {
|
|
375
|
+
if (cancelled)
|
|
376
|
+
return;
|
|
377
|
+
console.warn('[RecordDetailView] Failed to fetch sys_audit_log:', err);
|
|
378
|
+
setHistoryEntries([]);
|
|
379
|
+
})
|
|
380
|
+
.finally(() => {
|
|
381
|
+
if (!cancelled)
|
|
382
|
+
setHistoryLoading(false);
|
|
383
|
+
});
|
|
384
|
+
return () => { cancelled = true; };
|
|
385
|
+
}, [dataSource, pureRecordId, objectDef, historyEnabled]);
|
|
239
386
|
// Memoize so the object identity is stable across renders — otherwise
|
|
240
387
|
// any effect that depends on it (e.g. the feed loader below) would
|
|
241
388
|
// re-fire every render and create an infinite request loop.
|
|
@@ -547,7 +694,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
547
694
|
// redundant "Details" heading).
|
|
548
695
|
showBorder: false,
|
|
549
696
|
fields: Object.keys(objectDef.fields || {})
|
|
550
|
-
.filter(key => !AUDIT_FIELD_NAMES.has(key))
|
|
697
|
+
.filter(key => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key))
|
|
551
698
|
.map(key => {
|
|
552
699
|
const fieldDef = objectDef.fields[key];
|
|
553
700
|
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
@@ -563,36 +710,16 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
563
710
|
}),
|
|
564
711
|
},
|
|
565
712
|
];
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
const fieldsAlreadyShown = new Set(sections.flatMap((s) => (s.fields || []).map((f) => f.name)));
|
|
573
|
-
const auditFieldsToShow = Array.from(AUDIT_FIELD_NAMES).filter(name => objectDef.fields?.[name] && !fieldsAlreadyShown.has(name));
|
|
574
|
-
if (auditFieldsToShow.length > 0) {
|
|
575
|
-
sections.push({
|
|
576
|
-
title: sectionLabel(objectDef.name, 'system_info', 'System Information'),
|
|
577
|
-
collapsible: true,
|
|
578
|
-
defaultCollapsed: true,
|
|
579
|
-
fields: auditFieldsToShow.map(key => {
|
|
580
|
-
const fieldDef = objectDef.fields[key];
|
|
581
|
-
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
582
|
-
return {
|
|
583
|
-
name: key,
|
|
584
|
-
label: fieldDef.label || key,
|
|
585
|
-
type: fieldDef.type || 'text',
|
|
586
|
-
readonly: true,
|
|
587
|
-
...(refTarget && { reference_to: refTarget }),
|
|
588
|
-
};
|
|
589
|
-
}),
|
|
590
|
-
});
|
|
591
|
-
}
|
|
713
|
+
// Audit fields (created_at/created_by/updated_at/updated_by) are NOT
|
|
714
|
+
// appended as a section here — they are surfaced by `<RecordMetaFooter>`
|
|
715
|
+
// (rendered by DetailView) as a single subtle line below the content,
|
|
716
|
+
// replacing the old card-style "System Information" panel. The inline-edit
|
|
717
|
+
// drawer continues to hide them via `DEFAULT_SYSTEM_FIELDS` in
|
|
718
|
+
// `@object-ui/plugin-detail/RecordDetailDrawer`.
|
|
592
719
|
// Filter actions for record_header location and deduplicate by name
|
|
593
720
|
const recordHeaderActions = (() => {
|
|
594
721
|
const seen = new Set();
|
|
595
|
-
|
|
722
|
+
const base = (objectDef.actions || []).filter((a) => {
|
|
596
723
|
if (!a.locations?.includes('record_header'))
|
|
597
724
|
return false;
|
|
598
725
|
if (!a.name)
|
|
@@ -611,6 +738,59 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
611
738
|
successMessage: actionSuccess(objectDef.name, a.name, a.successMessage),
|
|
612
739
|
}),
|
|
613
740
|
}));
|
|
741
|
+
// Inject approval actions — only when the approvals plugin is
|
|
742
|
+
// available and an active process exists for this object.
|
|
743
|
+
if (approvals.available && approvals.processes.length > 0) {
|
|
744
|
+
if (approvals.canSubmit) {
|
|
745
|
+
base.push({
|
|
746
|
+
name: 'submit_approval',
|
|
747
|
+
type: 'approval',
|
|
748
|
+
target: 'submit_approval',
|
|
749
|
+
label: t('approvals.submitForApproval', { defaultValue: 'Submit for Approval' }),
|
|
750
|
+
icon: 'send',
|
|
751
|
+
variant: 'default',
|
|
752
|
+
locations: ['record_header'],
|
|
753
|
+
refreshAfter: true,
|
|
754
|
+
successMessage: t('approvals.submitSuccess', { defaultValue: 'Approval request submitted' }),
|
|
755
|
+
...(approvals.processes.length === 1
|
|
756
|
+
? { params: { processName: approvals.processes[0].name } }
|
|
757
|
+
: {
|
|
758
|
+
collectParams: [{
|
|
759
|
+
name: 'processName',
|
|
760
|
+
label: t('approvals.process', { defaultValue: 'Process' }),
|
|
761
|
+
type: 'select',
|
|
762
|
+
required: true,
|
|
763
|
+
options: approvals.processes.map((p) => ({
|
|
764
|
+
value: p.name,
|
|
765
|
+
label: p.label || p.name,
|
|
766
|
+
})),
|
|
767
|
+
}, {
|
|
768
|
+
name: 'comment',
|
|
769
|
+
label: t('approvals.comment', { defaultValue: 'Comment (optional)' }),
|
|
770
|
+
type: 'text',
|
|
771
|
+
multiline: true,
|
|
772
|
+
}],
|
|
773
|
+
}),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
if (approvals.canRecall) {
|
|
777
|
+
base.push({
|
|
778
|
+
name: 'recall_approval',
|
|
779
|
+
type: 'approval',
|
|
780
|
+
target: 'recall_approval',
|
|
781
|
+
label: t('approvals.recall', { defaultValue: 'Recall' }),
|
|
782
|
+
icon: 'undo',
|
|
783
|
+
variant: 'outline',
|
|
784
|
+
locations: ['record_header'],
|
|
785
|
+
refreshAfter: true,
|
|
786
|
+
confirmText: t('approvals.recallConfirm', {
|
|
787
|
+
defaultValue: 'Recall this pending approval request?',
|
|
788
|
+
}),
|
|
789
|
+
successMessage: t('approvals.recallSuccess', { defaultValue: 'Approval recalled' }),
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return base;
|
|
614
794
|
})();
|
|
615
795
|
// Build highlightFields: exclusively from objectDef metadata (no hardcoded fallback)
|
|
616
796
|
const highlightFields = objectDef.views?.detail?.highlightFields ?? [];
|
|
@@ -721,6 +901,12 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
721
901
|
sections,
|
|
722
902
|
autoTabs: true,
|
|
723
903
|
autoDiscoverRelated: true,
|
|
904
|
+
...(historyEnabled && {
|
|
905
|
+
history: {
|
|
906
|
+
entries: historyEntries ?? [],
|
|
907
|
+
loading: historyLoading && historyEntries === null,
|
|
908
|
+
},
|
|
909
|
+
}),
|
|
724
910
|
...(related.length > 0 && { related }),
|
|
725
911
|
...(highlightFields.length > 0 && { highlightFields }),
|
|
726
912
|
...(sectionGroups && sectionGroups.length > 0 && { sectionGroups }),
|
|
@@ -733,14 +919,17 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
733
919
|
}),
|
|
734
920
|
};
|
|
735
921
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
736
|
-
}, [objectDef?.name, pureRecordId, childRelatedData, actionRefreshKey, appName, navigate, dataSource, t, objectLabel, objects]);
|
|
922
|
+
}, [objectDef?.name, pureRecordId, childRelatedData, actionRefreshKey, appName, navigate, dataSource, t, objectLabel, objects, historyEnabled, historyEntries, historyLoading, approvals.available, approvals.processes, approvals.canSubmit, approvals.canRecall, approvals.pendingRequest, approvals.latestRequest]);
|
|
737
923
|
if (isLoading) {
|
|
738
924
|
return _jsx(SkeletonDetail, {});
|
|
739
925
|
}
|
|
740
926
|
if (!objectDef) {
|
|
741
927
|
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 }) })] }) }));
|
|
742
928
|
}
|
|
743
|
-
|
|
929
|
+
if (assignedPage) {
|
|
930
|
+
return (_jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, 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: _jsx("div", { className: "bg-background p-3 sm:p-4 lg:p-6", children: _jsx(SchemaRenderer, { schema: assignedPage }) }) }) }));
|
|
931
|
+
}
|
|
932
|
+
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: [_jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsx("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: _jsx(ActionProvider, { context: { record: {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), onDataLoaded: (record) => {
|
|
744
933
|
if (!record || typeof record !== 'object')
|
|
745
934
|
return;
|
|
746
935
|
// Resolve the same way DetailView's header does, so the
|
|
@@ -39,7 +39,7 @@ import { useMetadata } from '../providers/MetadataProvider';
|
|
|
39
39
|
import { useAdapter } from '../providers/AdapterProvider';
|
|
40
40
|
import { ExpressionProvider, evaluateVisibility } from '../providers/ExpressionProvider';
|
|
41
41
|
import { SkeletonDetail } from '../skeletons';
|
|
42
|
-
import {
|
|
42
|
+
import { ManagedByBadge } from '../components/ManagedByBadge';
|
|
43
43
|
import { useAuth } from '@object-ui/auth';
|
|
44
44
|
import { ExpressionEvaluator } from '@object-ui/core';
|
|
45
45
|
/**
|
|
@@ -162,7 +162,7 @@ export function RecordFormPage({ mode }) {
|
|
|
162
162
|
if (!objectDef) {
|
|
163
163
|
return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.objectNotFound') }), _jsx(EmptyDescription, { children: t('empty.objectNotFoundDescription', { name: objectName }) }), _jsx("div", { className: "mt-4", children: _jsxs(Button, { variant: "outline", onClick: () => navigate(baseUrl), children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), t('empty.back')] }) })] }) }));
|
|
164
164
|
}
|
|
165
|
-
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 })
|
|
165
|
+
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(ManagedByBadge, { managedBy: objectDef?.managedBy, className: "ml-1" })] })] }), _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: {
|
|
166
166
|
type: 'object-form',
|
|
167
167
|
formType: 'simple',
|
|
168
168
|
objectName: objectDef.name,
|
package/dist/views/ReportView.js
CHANGED
|
@@ -38,29 +38,31 @@ export function ReportView({ dataSource }) {
|
|
|
38
38
|
// State for report runtime data
|
|
39
39
|
const [reportRuntimeData, setReportRuntimeData] = useState([]);
|
|
40
40
|
const [dataLoading, setDataLoading] = useState(false);
|
|
41
|
+
const getFieldsForObject = useCallback((objName) => {
|
|
42
|
+
if (!objName || !objects?.length)
|
|
43
|
+
return undefined;
|
|
44
|
+
const objDef = objects.find((o) => o.name === objName);
|
|
45
|
+
if (!objDef?.fields)
|
|
46
|
+
return undefined;
|
|
47
|
+
const fields = objDef.fields;
|
|
48
|
+
if (Array.isArray(fields)) {
|
|
49
|
+
return fields.map((f) => typeof f === 'string'
|
|
50
|
+
? { value: f, label: f, type: 'text' }
|
|
51
|
+
: { value: f.name, label: f.label || f.name, type: f.type || 'text' });
|
|
52
|
+
}
|
|
53
|
+
return Object.entries(fields).map(([name, def]) => ({
|
|
54
|
+
value: name,
|
|
55
|
+
label: def.label || name,
|
|
56
|
+
type: def.type || 'text',
|
|
57
|
+
}));
|
|
58
|
+
}, [objects]);
|
|
41
59
|
// Derive available fields from object schema for filter/sort editors
|
|
42
60
|
// Uses live editSchema when available to respond to objectName changes
|
|
43
61
|
const availableFields = useMemo(() => {
|
|
44
62
|
const liveReport = editSchema || reportData;
|
|
45
63
|
const objName = liveReport?.objectName || liveReport?.dataSource?.object || liveReport?.dataSource?.resource;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (objDef?.fields) {
|
|
49
|
-
const fields = objDef.fields;
|
|
50
|
-
if (Array.isArray(fields)) {
|
|
51
|
-
return fields.map((f) => typeof f === 'string'
|
|
52
|
-
? { value: f, label: f, type: 'text' }
|
|
53
|
-
: { value: f.name, label: f.label || f.name, type: f.type || 'text' });
|
|
54
|
-
}
|
|
55
|
-
return Object.entries(fields).map(([name, def]) => ({
|
|
56
|
-
value: name,
|
|
57
|
-
label: def.label || name,
|
|
58
|
-
type: def.type || 'text',
|
|
59
|
-
}));
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return FALLBACK_FIELDS;
|
|
63
|
-
}, [editSchema, reportData, objects]);
|
|
64
|
+
return getFieldsForObject(objName) ?? FALLBACK_FIELDS;
|
|
65
|
+
}, [editSchema, reportData, getFieldsForObject]);
|
|
64
66
|
// ---- Save helper --------------------------------------------------------
|
|
65
67
|
const saveSchema = useCallback(async (schema) => {
|
|
66
68
|
try {
|
|
@@ -350,5 +352,5 @@ export function ReportView({ dataSource }) {
|
|
|
350
352
|
allowExport: true,
|
|
351
353
|
loading: dataLoading, // Loading state for data fetching
|
|
352
354
|
};
|
|
353
|
-
return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [_jsxs("div", { className: "flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate", children: previewReport.title || previewReport.label || 'Report Viewer' }), previewReport.description && (_jsx("p", { className: "text-sm text-muted-foreground mt-1 line-clamp-2", children: previewReport.description }))] }), _jsx("div", { className: "shrink-0 flex items-center gap-1.5", children: _jsxs("button", { type: "button", onClick: handleOpenConfigPanel, className: "inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", "data-testid": "report-edit-button", children: [_jsx(Pencil, { className: "h-3.5 w-3.5" }), "Edit"] }) })] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-col sm:flex-row relative", children: [_jsx("div", { className: "flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5", children: _jsx("div", { className: "w-full shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150", children: _jsx(Suspense, { fallback: _jsx("div", { className: "p-8 text-sm text-muted-foreground", children: "Loading report\u2026" }), children: useSpecRenderer ? (_jsx("div", { className: "p-4 sm:p-6", children: _jsx(ReportRenderer, { schema: previewReport, dataSource: dataSource }) })) : (_jsx(ReportViewer, { schema: viewerSchema })) }) }) }), _jsx(Suspense, { fallback: null, children: _jsx(ReportConfigPanel, { open: configPanelOpen, onClose: handleCloseConfigPanel, config: reportConfig, onSave: handleReportConfigSave, onFieldChange: handleReportFieldChange, availableFields: availableFields }) }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Report Configuration', data: previewReport }] })] })] }));
|
|
355
|
+
return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [_jsxs("div", { className: "flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate", children: previewReport.title || previewReport.label || 'Report Viewer' }), previewReport.description && (_jsx("p", { className: "text-sm text-muted-foreground mt-1 line-clamp-2", children: previewReport.description }))] }), _jsx("div", { className: "shrink-0 flex items-center gap-1.5", children: _jsxs("button", { type: "button", onClick: handleOpenConfigPanel, className: "inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", "data-testid": "report-edit-button", children: [_jsx(Pencil, { className: "h-3.5 w-3.5" }), "Edit"] }) })] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-col sm:flex-row relative", children: [_jsx("div", { className: "flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5", children: _jsx("div", { className: "w-full shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150", children: _jsx(Suspense, { fallback: _jsx("div", { className: "p-8 text-sm text-muted-foreground", children: "Loading report\u2026" }), children: useSpecRenderer ? (_jsx("div", { className: "p-4 sm:p-6", children: _jsx(ReportRenderer, { schema: previewReport, dataSource: dataSource }) })) : (_jsx(ReportViewer, { schema: viewerSchema })) }) }) }), _jsx(Suspense, { fallback: null, children: _jsx(ReportConfigPanel, { open: configPanelOpen, onClose: handleCloseConfigPanel, config: reportConfig, onSave: handleReportConfigSave, onFieldChange: handleReportFieldChange, availableFields: availableFields, getFieldsForObject: getFieldsForObject }) }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Report Configuration', data: previewReport }] })] })] }));
|
|
354
356
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/app-shell",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
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.16.0",
|
|
29
29
|
"sonner": "^2.0.7",
|
|
30
|
-
"@object-ui/auth": "4.
|
|
31
|
-
"@object-ui/collaboration": "4.
|
|
32
|
-
"@object-ui/components": "4.
|
|
33
|
-
"@object-ui/core": "4.
|
|
34
|
-
"@object-ui/data-objectstack": "4.
|
|
35
|
-
"@object-ui/fields": "4.
|
|
36
|
-
"@object-ui/i18n": "4.
|
|
37
|
-
"@object-ui/layout": "4.
|
|
38
|
-
"@object-ui/permissions": "4.
|
|
39
|
-
"@object-ui/react": "4.
|
|
40
|
-
"@object-ui/types": "4.
|
|
30
|
+
"@object-ui/auth": "4.4.0",
|
|
31
|
+
"@object-ui/collaboration": "4.4.0",
|
|
32
|
+
"@object-ui/components": "4.4.0",
|
|
33
|
+
"@object-ui/core": "4.4.0",
|
|
34
|
+
"@object-ui/data-objectstack": "4.4.0",
|
|
35
|
+
"@object-ui/fields": "4.4.0",
|
|
36
|
+
"@object-ui/i18n": "4.4.0",
|
|
37
|
+
"@object-ui/layout": "4.4.0",
|
|
38
|
+
"@object-ui/permissions": "4.4.0",
|
|
39
|
+
"@object-ui/react": "4.4.0",
|
|
40
|
+
"@object-ui/types": "4.4.0"
|
|
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.
|
|
47
|
-
"@object-ui/plugin-charts": "^4.
|
|
48
|
-
"@object-ui/plugin-chatbot": "^4.
|
|
49
|
-
"@object-ui/plugin-dashboard": "^4.
|
|
50
|
-
"@object-ui/plugin-designer": "^4.
|
|
51
|
-
"@object-ui/plugin-detail": "^4.
|
|
52
|
-
"@object-ui/plugin-form": "^4.
|
|
53
|
-
"@object-ui/plugin-grid": "^4.
|
|
54
|
-
"@object-ui/plugin-kanban": "^4.
|
|
55
|
-
"@object-ui/plugin-list": "^4.
|
|
56
|
-
"@object-ui/plugin-report": "^4.
|
|
57
|
-
"@object-ui/plugin-view": "^4.
|
|
46
|
+
"@object-ui/plugin-calendar": "^4.4.0",
|
|
47
|
+
"@object-ui/plugin-charts": "^4.4.0",
|
|
48
|
+
"@object-ui/plugin-chatbot": "^4.4.0",
|
|
49
|
+
"@object-ui/plugin-dashboard": "^4.4.0",
|
|
50
|
+
"@object-ui/plugin-designer": "^4.4.0",
|
|
51
|
+
"@object-ui/plugin-detail": "^4.4.0",
|
|
52
|
+
"@object-ui/plugin-form": "^4.4.0",
|
|
53
|
+
"@object-ui/plugin-grid": "^4.4.0",
|
|
54
|
+
"@object-ui/plugin-kanban": "^4.4.0",
|
|
55
|
+
"@object-ui/plugin-list": "^4.4.0",
|
|
56
|
+
"@object-ui/plugin-report": "^4.4.0",
|
|
57
|
+
"@object-ui/plugin-view": "^4.4.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^25.9.0",
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export interface ManagedByBannerProps {
|
|
2
|
-
/** The `managedBy` flag from the object schema. */
|
|
3
|
-
managedBy?: string;
|
|
4
|
-
/** Optional override for the human-readable system name. */
|
|
5
|
-
label?: string;
|
|
6
|
-
/** Optional documentation link rendered as "Learn more →". */
|
|
7
|
-
docHref?: string;
|
|
8
|
-
/**
|
|
9
|
-
* Optional human-readable name for the source record / parent workflow
|
|
10
|
-
* referenced in `system`-bucket banners (e.g. "Opportunity"). When
|
|
11
|
-
* provided the banner reads "Use the Opportunity record's Submit for
|
|
12
|
-
* Approval action…" instead of the generic phrasing.
|
|
13
|
-
*/
|
|
14
|
-
sourceRecordLabel?: string;
|
|
15
|
-
}
|
|
16
|
-
export declare function ManagedByBanner({ managedBy, label, docHref, sourceRecordLabel }: ManagedByBannerProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { ShieldAlert, Info, Lock, Archive } from 'lucide-react';
|
|
3
|
-
const VARIANTS = {
|
|
4
|
-
config: {
|
|
5
|
-
icon: Info,
|
|
6
|
-
tone: 'border-sky-300/60 bg-sky-50 text-sky-900 dark:border-sky-500/40 dark:bg-sky-950/40 dark:text-sky-100',
|
|
7
|
-
title: 'Administrator configuration',
|
|
8
|
-
body: () => "These rows define how the platform behaves at runtime — they're authored here, but the runtime data they produce lives in a separate table. Changes take effect once a row is marked Active.",
|
|
9
|
-
},
|
|
10
|
-
system: {
|
|
11
|
-
icon: Lock,
|
|
12
|
-
tone: 'border-slate-300/60 bg-slate-50 text-slate-900 dark:border-slate-500/40 dark:bg-slate-950/40 dark:text-slate-100',
|
|
13
|
-
title: 'Managed by the platform',
|
|
14
|
-
body: (_display, src) => `Rows here are created and updated automatically by the platform engine. To start a new one, use the action button on the ${src ?? 'source record'} (e.g. "Submit for Approval", "Share", "Invite"). The list below is the audit / monitoring surface — actions like Approve, Recall, or Resend live on the row.`,
|
|
15
|
-
},
|
|
16
|
-
'append-only': {
|
|
17
|
-
icon: Archive,
|
|
18
|
-
tone: 'border-zinc-300/60 bg-zinc-50 text-zinc-900 dark:border-zinc-500/40 dark:bg-zinc-950/40 dark:text-zinc-100',
|
|
19
|
-
title: 'Read-only historical record',
|
|
20
|
-
body: () => "This is an immutable audit log. Rows cannot be created, edited, or deleted from the UI — they're written by the platform when events occur. Use Export to download for compliance review.",
|
|
21
|
-
},
|
|
22
|
-
'better-auth': {
|
|
23
|
-
icon: ShieldAlert,
|
|
24
|
-
tone: 'border-amber-300/60 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-950/40 dark:text-amber-100',
|
|
25
|
-
title: 'Managed by better-auth',
|
|
26
|
-
body: (display) => `This object's schema is owned by ${display}. Direct edits here bypass password hashing, session validation, two-factor checks, and audit hooks — and may corrupt account state. Use the dedicated identity workflows instead (Invite User, Reset Password, Revoke Session, Rotate Key, …).`,
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
export function ManagedByBanner({ managedBy, label, docHref, sourceRecordLabel }) {
|
|
30
|
-
if (!managedBy || managedBy === 'platform')
|
|
31
|
-
return null;
|
|
32
|
-
const variant = VARIANTS[managedBy];
|
|
33
|
-
if (!variant)
|
|
34
|
-
return null;
|
|
35
|
-
const display = label ?? managedBy;
|
|
36
|
-
const Icon = variant.icon;
|
|
37
|
-
return (_jsxs("div", { role: "note", "data-testid": "managed-by-banner", "data-bucket": managedBy, className: `flex items-start gap-3 border-b px-4 py-2.5 text-sm ${variant.tone}`, children: [_jsx(Icon, { className: "mt-0.5 h-4 w-4 flex-none" }), _jsxs("div", { className: "flex-1", children: [_jsxs("strong", { className: "font-semibold", children: [variant.title, "."] }), ' ', variant.body(display, sourceRecordLabel), docHref && (_jsxs(_Fragment, { children: [' ', _jsx("a", { href: docHref, target: "_blank", rel: "noreferrer", className: "underline underline-offset-2 hover:opacity-80", children: "Learn more \u2192" })] }))] })] }));
|
|
38
|
-
}
|