@object-ui/app-shell 4.0.12 → 4.3.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 +102 -0
- package/dist/components/ManagedByBadge.d.ts +10 -0
- package/dist/components/ManagedByBadge.js +44 -0
- package/dist/layout/ActivityFeed.js +3 -3
- package/dist/layout/AppHeader.js +122 -15
- package/dist/utils/crudAffordances.d.ts +41 -0
- package/dist/utils/crudAffordances.js +35 -0
- 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 +160 -13
- package/dist/views/RecordDetailView.js +32 -35
- package/dist/views/RecordFormPage.js +2 -1
- package/dist/views/ReportView.js +36 -19
- package/package.json +24 -24
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Normalise an options entry (allowing bare strings) into label/value pairs. */
|
|
2
|
+
function normaliseOptions(options, objectName, fieldName, optionLabel) {
|
|
3
|
+
if (!Array.isArray(options) || options.length === 0)
|
|
4
|
+
return undefined;
|
|
5
|
+
return options.map((o) => {
|
|
6
|
+
const raw = typeof o === 'string' ? { label: o, value: o } : o;
|
|
7
|
+
const label = optionLabel
|
|
8
|
+
? optionLabel(objectName, fieldName, raw.value, raw.label)
|
|
9
|
+
: raw.label;
|
|
10
|
+
return { label, value: raw.value };
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a single raw param against object metadata. Inline params pass
|
|
15
|
+
* through (with safe defaults); field-backed params inherit from the
|
|
16
|
+
* referenced field and accept inline overrides on top.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveActionParam(param, ctx) {
|
|
19
|
+
/** Row-context default: when `defaultFromRow` and a row is present, the
|
|
20
|
+
* param's defaultValue is the row's value at the field key (or `name`). */
|
|
21
|
+
const rowKey = param.field ?? param.name;
|
|
22
|
+
const rowDefault = param.defaultFromRow && ctx.row && rowKey != null && Object.prototype.hasOwnProperty.call(ctx.row, rowKey)
|
|
23
|
+
? ctx.row[rowKey]
|
|
24
|
+
: undefined;
|
|
25
|
+
// Inline param — no field reference, just normalise.
|
|
26
|
+
if (!param.field) {
|
|
27
|
+
return {
|
|
28
|
+
name: param.name ?? '',
|
|
29
|
+
label: param.label ?? param.name ?? '',
|
|
30
|
+
type: param.type ?? 'text',
|
|
31
|
+
required: param.required ?? false,
|
|
32
|
+
options: param.options,
|
|
33
|
+
placeholder: param.placeholder,
|
|
34
|
+
helpText: param.helpText,
|
|
35
|
+
defaultValue: rowDefault ?? param.defaultValue,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const ownerName = param.objectOverride ?? ctx.objectName;
|
|
39
|
+
const owner = ctx.objects.find((o) => o?.name === ownerName);
|
|
40
|
+
const field = owner?.fields?.[param.field];
|
|
41
|
+
if (!field) {
|
|
42
|
+
// Reference target missing — fall back to a plain text input so the
|
|
43
|
+
// action remains usable in environments where the metadata cache is
|
|
44
|
+
// partial (e.g. tests).
|
|
45
|
+
return {
|
|
46
|
+
name: param.name ?? param.field,
|
|
47
|
+
label: param.label ?? ctx.fieldLabel(ownerName, param.field, param.field),
|
|
48
|
+
type: param.type ?? 'text',
|
|
49
|
+
required: param.required ?? false,
|
|
50
|
+
options: param.options,
|
|
51
|
+
placeholder: param.placeholder,
|
|
52
|
+
helpText: param.helpText,
|
|
53
|
+
defaultValue: rowDefault ?? param.defaultValue,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const resolvedType = param.type ?? field.type ?? 'text';
|
|
57
|
+
const resolvedOptions = param.options
|
|
58
|
+
?? normaliseOptions(field.options, ownerName, param.field, ctx.fieldOptionLabel);
|
|
59
|
+
const resolvedLabel = param.label
|
|
60
|
+
?? ctx.fieldLabel(ownerName, param.field, field.label ?? param.field);
|
|
61
|
+
return {
|
|
62
|
+
name: param.name ?? param.field,
|
|
63
|
+
label: resolvedLabel,
|
|
64
|
+
type: resolvedType,
|
|
65
|
+
required: param.required ?? field.required ?? false,
|
|
66
|
+
options: resolvedOptions,
|
|
67
|
+
placeholder: param.placeholder ?? field.placeholder,
|
|
68
|
+
helpText: param.helpText ?? field.help ?? field.description,
|
|
69
|
+
defaultValue: rowDefault ?? param.defaultValue ?? field.defaultValue,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Resolve an array of raw action params. */
|
|
73
|
+
export function resolveActionParams(params, ctx) {
|
|
74
|
+
if (!Array.isArray(params))
|
|
75
|
+
return [];
|
|
76
|
+
return params.map((p) => resolveActionParam(p, ctx));
|
|
77
|
+
}
|
package/dist/views/ObjectView.js
CHANGED
|
@@ -26,6 +26,9 @@ import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
|
26
26
|
import { ViewConfigPanel } from './ViewConfigPanel';
|
|
27
27
|
import { CreateViewDialog } from './CreateViewDialog';
|
|
28
28
|
import { PageHeader } from '../layout/PageHeader';
|
|
29
|
+
import { ManagedByBadge } from '../components/ManagedByBadge';
|
|
30
|
+
import { resolveCrudAffordances } from '../utils/crudAffordances';
|
|
31
|
+
import { resolveManagedByEmptyState } from '../utils/managedByEmptyState';
|
|
29
32
|
import { useObjectActions } from '../hooks/useObjectActions';
|
|
30
33
|
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
31
34
|
import { usePermissions } from '@object-ui/permissions';
|
|
@@ -35,6 +38,7 @@ import { ActionProvider, useNavigationOverlay, SchemaRenderer } from '@object-ui
|
|
|
35
38
|
import { toast } from 'sonner';
|
|
36
39
|
import { ActionConfirmDialog } from './ActionConfirmDialog';
|
|
37
40
|
import { ActionParamDialog } from './ActionParamDialog';
|
|
41
|
+
import { resolveActionParams } from '../utils/resolveActionParams';
|
|
38
42
|
/** Map view types to Lucide icons (Airtable-style) */
|
|
39
43
|
const VIEW_TYPE_ICONS = {
|
|
40
44
|
grid: TableIcon,
|
|
@@ -47,6 +51,40 @@ const VIEW_TYPE_ICONS = {
|
|
|
47
51
|
chart: BarChart3,
|
|
48
52
|
};
|
|
49
53
|
const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
|
|
54
|
+
/**
|
|
55
|
+
* Replace built-in tokens (e.g. `{current_user_id}`) inside a filter array
|
|
56
|
+
* with concrete values. Filters from platform-shipped `listViews` or saved
|
|
57
|
+
* `sys_view` rows may declare context-sensitive predicates like
|
|
58
|
+
* `{ field: 'submitter_id', operator: 'equals', value: '{current_user_id}' }`
|
|
59
|
+
* — those need to be substituted before the query reaches the API.
|
|
60
|
+
*
|
|
61
|
+
* Recognised tokens:
|
|
62
|
+
* • `{current_user_id}` → the authenticated user's id
|
|
63
|
+
*
|
|
64
|
+
* Returns a deep-cloned copy with substitutions applied. Non-array input
|
|
65
|
+
* is returned unchanged.
|
|
66
|
+
*/
|
|
67
|
+
function substituteFilterTokens(filter, currentUserId) {
|
|
68
|
+
if (!Array.isArray(filter))
|
|
69
|
+
return filter;
|
|
70
|
+
const sub = (v) => {
|
|
71
|
+
if (typeof v === 'string') {
|
|
72
|
+
if (v === '{current_user_id}')
|
|
73
|
+
return currentUserId ?? v;
|
|
74
|
+
return v;
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(v))
|
|
77
|
+
return v.map(sub);
|
|
78
|
+
if (v && typeof v === 'object') {
|
|
79
|
+
const out = {};
|
|
80
|
+
for (const k of Object.keys(v))
|
|
81
|
+
out[k] = sub(v[k]);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
return v;
|
|
85
|
+
};
|
|
86
|
+
return filter.map(sub);
|
|
87
|
+
}
|
|
50
88
|
/**
|
|
51
89
|
* Translate a NamedListView spec object (shape: `type`, `label`, `columns`,
|
|
52
90
|
* `filter`, `sort`, `kanban`/`chart`/`gantt`/... sub-blocks, `bulkActions`,
|
|
@@ -310,7 +348,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
310
348
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
311
349
|
const { showDebug } = useMetadataInspector();
|
|
312
350
|
const { t } = useObjectTranslation();
|
|
313
|
-
const { objectLabel, objectDescription: objectDesc, viewLabel, actionLabel, actionConfirm, actionSuccess } = useObjectLabel();
|
|
351
|
+
const { objectLabel, objectDescription: objectDesc, viewLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
|
|
314
352
|
// Inline view config panel state (Airtable-style right sidebar)
|
|
315
353
|
const [showViewConfigPanel, setShowViewConfigPanel] = useState(false);
|
|
316
354
|
const [viewConfigPanelMode, setViewConfigPanelMode] = useState('edit');
|
|
@@ -447,7 +485,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
447
485
|
// Record count tracking for footer
|
|
448
486
|
const [recordCount, setRecordCount] = useState(undefined);
|
|
449
487
|
// Admin users automatically get design tools (no toggle needed)
|
|
450
|
-
const { user } = useAuth();
|
|
488
|
+
const { user, activeOrganization } = useAuth();
|
|
451
489
|
const isAdmin = user?.role === 'admin';
|
|
452
490
|
const { can } = usePermissions();
|
|
453
491
|
// Get Object Definition
|
|
@@ -457,6 +495,13 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
457
495
|
}
|
|
458
496
|
// Refresh trigger — bumped after view CRUD or external data mutations.
|
|
459
497
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
498
|
+
// Resolve which generic CRUD affordances belong in the toolbar for
|
|
499
|
+
// this object's lifecycle bucket (`managedBy`). config tables show
|
|
500
|
+
// New/Edit/Delete but no CSV Import; system / append-only / better-auth
|
|
501
|
+
// hide the lot — those flows go through purpose-built actions on the
|
|
502
|
+
// source record (e.g. "Submit for Approval" on an Opportunity creates
|
|
503
|
+
// an `sys_approval_request`). Permissions still gate the buttons.
|
|
504
|
+
const affordances = useMemo(() => resolveCrudAffordances(objectDef), [objectDef]);
|
|
460
505
|
// Propagate externally-triggered refreshes (e.g. global ModalForm submit)
|
|
461
506
|
// into our internal refreshKey so list/data effects re-run.
|
|
462
507
|
useEffect(() => {
|
|
@@ -828,11 +873,24 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
828
873
|
setConfirmState({ open: true, message, options, resolve });
|
|
829
874
|
});
|
|
830
875
|
}, []);
|
|
831
|
-
const paramCollectionHandler = useCallback((params) => {
|
|
876
|
+
const paramCollectionHandler = useCallback((params, action) => {
|
|
832
877
|
return new Promise((resolve) => {
|
|
833
|
-
|
|
878
|
+
// List_item actions stash the row record under params._rowRecord
|
|
879
|
+
// (see ObjectGrid → onRowAction). Pull it out so resolveActionParams
|
|
880
|
+
// can pre-fill `defaultFromRow` params from the row's current values.
|
|
881
|
+
const row = action?.params && !Array.isArray(action.params)
|
|
882
|
+
? action.params._rowRecord
|
|
883
|
+
: undefined;
|
|
884
|
+
const resolved = resolveActionParams(params, {
|
|
885
|
+
objectName: objectName || objectDef?.name || '',
|
|
886
|
+
objects: objects || [],
|
|
887
|
+
fieldLabel,
|
|
888
|
+
fieldOptionLabel,
|
|
889
|
+
row,
|
|
890
|
+
});
|
|
891
|
+
setParamState({ open: true, params: resolved, resolve });
|
|
834
892
|
});
|
|
835
|
-
}, []);
|
|
893
|
+
}, [objectName, objectDef, objects, fieldLabel, fieldOptionLabel]);
|
|
836
894
|
const handleDeleteView = useCallback(async (vid) => {
|
|
837
895
|
if (!dataSource)
|
|
838
896
|
return;
|
|
@@ -1049,10 +1107,78 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1049
1107
|
navigate(url);
|
|
1050
1108
|
}
|
|
1051
1109
|
}, [navigate]);
|
|
1110
|
+
// Authenticated fetch for direct backend calls (e.g. flow trigger,
|
|
1111
|
+
// schema actions targeting absolute API paths). Declared before
|
|
1112
|
+
// apiHandler so the latter can close over it.
|
|
1113
|
+
const authFetch = useMemo(() => createAuthenticatedFetch(), []);
|
|
1052
1114
|
const apiHandler = useCallback(async (action) => {
|
|
1053
1115
|
try {
|
|
1054
1116
|
const target = action.target || action.name;
|
|
1055
1117
|
const params = action.params || {};
|
|
1118
|
+
// Absolute HTTP target (e.g. /api/v1/auth/organization/invite-member,
|
|
1119
|
+
// http://..., https://...) — bypass dataSource and call the API
|
|
1120
|
+
// directly through the authenticated fetch wrapper so:
|
|
1121
|
+
// - Authorization: Bearer <token> is injected
|
|
1122
|
+
// - X-Tenant-ID is injected (multi-tenant routing)
|
|
1123
|
+
// - Same-origin cookies (better-auth.session_token) ride along
|
|
1124
|
+
//
|
|
1125
|
+
// This is the canonical path for schema actions on managed-by
|
|
1126
|
+
// tables (sys_user/invite_user, sys_session/revoke, …) where
|
|
1127
|
+
// generic CRUD does not apply.
|
|
1128
|
+
const targetStr = typeof target === 'string' ? target : '';
|
|
1129
|
+
const isAbsolute = targetStr.startsWith('/') || /^https?:\/\//i.test(targetStr);
|
|
1130
|
+
if (isAbsolute) {
|
|
1131
|
+
const baseUrl = import.meta.env.VITE_SERVER_URL || '';
|
|
1132
|
+
const url = targetStr.startsWith('http') ? targetStr : `${baseUrl}${targetStr}`;
|
|
1133
|
+
// Row context is stashed on params under `_rowRecord` by the
|
|
1134
|
+
// row-action dispatcher (see ObjectGrid → onRowAction). Pull
|
|
1135
|
+
// it out before assembling the request body.
|
|
1136
|
+
const rawParams = { ...params };
|
|
1137
|
+
const rowRecord = rawParams._rowRecord;
|
|
1138
|
+
delete rawParams._rowRecord;
|
|
1139
|
+
// Apply bodyShape: 'flat' (default) keeps user params at top
|
|
1140
|
+
// level; { wrap: 'data' } nests them under that key while
|
|
1141
|
+
// `recordIdParam` / `organizationId` stay flat (better-auth
|
|
1142
|
+
// organization/update semantics).
|
|
1143
|
+
const wrap = action.bodyShape && typeof action.bodyShape === 'object' && action.bodyShape.wrap
|
|
1144
|
+
? action.bodyShape.wrap
|
|
1145
|
+
: undefined;
|
|
1146
|
+
const body = wrap
|
|
1147
|
+
? { [wrap]: rawParams }
|
|
1148
|
+
: { ...rawParams };
|
|
1149
|
+
// Inject row id (or chosen row field) for list_item actions.
|
|
1150
|
+
if (rowRecord && action.recordIdParam) {
|
|
1151
|
+
const rowField = action.recordIdField || 'id';
|
|
1152
|
+
const rowValue = rowRecord[rowField];
|
|
1153
|
+
if (rowValue != null)
|
|
1154
|
+
body[action.recordIdParam] = rowValue;
|
|
1155
|
+
}
|
|
1156
|
+
// Auto-inject organizationId when the active org is known and
|
|
1157
|
+
// not already set. Most better-auth org-scoped endpoints
|
|
1158
|
+
// accept this shape; harmless for endpoints that ignore it.
|
|
1159
|
+
if (!body.organizationId && activeOrganization?.id) {
|
|
1160
|
+
body.organizationId = activeOrganization.id;
|
|
1161
|
+
}
|
|
1162
|
+
const res = await authFetch(url, {
|
|
1163
|
+
method: 'POST',
|
|
1164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1165
|
+
credentials: 'include',
|
|
1166
|
+
body: JSON.stringify(body),
|
|
1167
|
+
});
|
|
1168
|
+
if (!res.ok) {
|
|
1169
|
+
let detail = `HTTP ${res.status}`;
|
|
1170
|
+
try {
|
|
1171
|
+
const j = await res.json();
|
|
1172
|
+
detail = j?.error || j?.message || detail;
|
|
1173
|
+
}
|
|
1174
|
+
catch { /* response body not JSON */ }
|
|
1175
|
+
return { success: false, error: detail };
|
|
1176
|
+
}
|
|
1177
|
+
const data = await res.json().catch(() => ({}));
|
|
1178
|
+
if (action.refreshAfter !== false)
|
|
1179
|
+
setRefreshKey(k => k + 1);
|
|
1180
|
+
return { success: true, data, reload: action.refreshAfter !== false };
|
|
1181
|
+
}
|
|
1056
1182
|
// Generic list-level API handler: update/execute via dataSource
|
|
1057
1183
|
if (typeof dataSource.execute === 'function') {
|
|
1058
1184
|
await dataSource.execute(objectDef.name, target, params);
|
|
@@ -1069,9 +1195,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1069
1195
|
catch (error) {
|
|
1070
1196
|
return { success: false, error: error.message };
|
|
1071
1197
|
}
|
|
1072
|
-
}, [dataSource, objectDef.name]);
|
|
1073
|
-
// Authenticated fetch for direct backend calls (e.g. flow trigger).
|
|
1074
|
-
const authFetch = useMemo(() => createAuthenticatedFetch(), []);
|
|
1198
|
+
}, [dataSource, objectDef.name, authFetch, activeOrganization]);
|
|
1075
1199
|
// Flow action handler — POST to /api/v1/automation/{name}/trigger.
|
|
1076
1200
|
// Triggered when an Action with `type: 'flow'` is invoked from list-level
|
|
1077
1201
|
// locations (list_toolbar, list_item). For list_item the row's recordId is
|
|
@@ -1285,9 +1409,10 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1285
1409
|
sort: viewDef.sort ?? listSchema.sort,
|
|
1286
1410
|
filter: (() => {
|
|
1287
1411
|
const base = viewDef.filter ?? listSchema.filter;
|
|
1412
|
+
const substituted = substituteFilterTokens(base, currentUser.id);
|
|
1288
1413
|
if (!urlFilters.length)
|
|
1289
|
-
return
|
|
1290
|
-
const baseArr = Array.isArray(
|
|
1414
|
+
return substituted;
|
|
1415
|
+
const baseArr = Array.isArray(substituted) ? substituted : [];
|
|
1291
1416
|
return [...baseArr, ...urlFilters];
|
|
1292
1417
|
})(),
|
|
1293
1418
|
hiddenFields: viewDef.hiddenFields ?? listSchema.hiddenFields,
|
|
@@ -1341,6 +1466,17 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1341
1466
|
filterableFields: viewDef.filterableFields ?? listSchema.filterableFields,
|
|
1342
1467
|
resizable: viewDef.resizable ?? listSchema.resizable,
|
|
1343
1468
|
rowActions: viewDef.rowActions ?? listSchema.rowActions,
|
|
1469
|
+
/**
|
|
1470
|
+
* Row-context action definitions derived from `objectDef.actions`
|
|
1471
|
+
* filtered by `locations.includes('list_item')`. These are full
|
|
1472
|
+
* `ActionDef` records (with label/icon/variant/params/recordIdParam
|
|
1473
|
+
* /bodyShape) the row menu renders with i18n-correct labels and
|
|
1474
|
+
* dispatches via the action runner; legacy `rowActions: string[]`
|
|
1475
|
+
* remains for back-compat where the action lives elsewhere.
|
|
1476
|
+
*/
|
|
1477
|
+
rowActionDefs: (Array.isArray(objectDef?.actions)
|
|
1478
|
+
? objectDef.actions.filter((a) => Array.isArray(a?.locations) && a.locations.includes('list_item'))
|
|
1479
|
+
: []),
|
|
1344
1480
|
bulkActions: viewDef.bulkActions ?? listSchema.bulkActions,
|
|
1345
1481
|
sharing: viewDef.sharing ?? listSchema.sharing,
|
|
1346
1482
|
addRecord: viewDef.addRecord ?? listSchema.addRecord,
|
|
@@ -1350,7 +1486,9 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1350
1486
|
showRecordCount: viewDef.showRecordCount ?? listSchema.showRecordCount,
|
|
1351
1487
|
allowPrinting: viewDef.allowPrinting ?? listSchema.allowPrinting,
|
|
1352
1488
|
virtualScroll: viewDef.virtualScroll ?? listSchema.virtualScroll,
|
|
1353
|
-
emptyState: viewDef.emptyState
|
|
1489
|
+
emptyState: viewDef.emptyState
|
|
1490
|
+
?? listSchema.emptyState
|
|
1491
|
+
?? resolveManagedByEmptyState(objectDef?.managedBy),
|
|
1354
1492
|
aria: viewDef.aria ?? listSchema.aria,
|
|
1355
1493
|
tabs: listSchema.tabs,
|
|
1356
1494
|
// Propagate filter/sort as default filters/sort for data flow
|
|
@@ -1359,7 +1497,8 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1359
1497
|
...(Array.isArray(viewDef.filter) ? viewDef.filter : []),
|
|
1360
1498
|
...urlFilters,
|
|
1361
1499
|
];
|
|
1362
|
-
|
|
1500
|
+
const substituted = substituteFilterTokens(combined, currentUser.id);
|
|
1501
|
+
return Array.isArray(substituted) && substituted.length ? { filters: substituted } : {};
|
|
1363
1502
|
})()),
|
|
1364
1503
|
...(viewDef.sort?.length ? { sort: viewDef.sort } : {}),
|
|
1365
1504
|
options: {
|
|
@@ -1489,7 +1628,15 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1489
1628
|
}
|
|
1490
1629
|
},
|
|
1491
1630
|
}), [objectDef.name, onEdit, activeView?.showSearch, activeView?.showFilters, activeView?.showSort, navigate, viewId, isAdmin]);
|
|
1492
|
-
return (_jsxs(ActionProvider, { context: {
|
|
1631
|
+
return (_jsxs(ActionProvider, { context: {
|
|
1632
|
+
objectName: objectDef.name,
|
|
1633
|
+
user: currentUser,
|
|
1634
|
+
// Expose active org so `type: 'url'` actions can template
|
|
1635
|
+
// `/organizations/${activeOrganization.slug}/...` etc.
|
|
1636
|
+
activeOrganization: activeOrganization
|
|
1637
|
+
? { id: activeOrganization.id, slug: activeOrganization.slug, name: activeOrganization.name }
|
|
1638
|
+
: null,
|
|
1639
|
+
}, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [_jsx(PageHeader, { title: _jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: objectLabel(objectDef) }), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), description: objectDef.description ? objectDesc(objectDef) : undefined, icon: (() => { const I = getIcon(objectDef?.icon); return _jsx(I, { className: "h-4 w-4" }); })(), actions: _jsxs(_Fragment, { children: [affordances.create && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", onClick: actions.create, className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.new') })] })), affordances.import && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setShowImport(true), className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", title: t('console.objectView.importTitle'), "data-testid": "object-view-import-button", children: [_jsx(Upload, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.import') })] })), objectDef.actions?.some((a) => a.locations?.includes('list_toolbar')) && (_jsx(SchemaRenderer, { schema: {
|
|
1493
1640
|
type: 'action:bar',
|
|
1494
1641
|
location: 'list_toolbar',
|
|
1495
1642
|
actions: (objectDef.actions || []).map((a) => ({
|
|
@@ -17,17 +17,22 @@ import { toast } from 'sonner';
|
|
|
17
17
|
import { Database, Users } from 'lucide-react';
|
|
18
18
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
19
19
|
import { SkeletonDetail } from '../skeletons';
|
|
20
|
+
import { ManagedByBadge } from '../components/ManagedByBadge';
|
|
21
|
+
import { resolveCrudAffordances } from '../utils/crudAffordances';
|
|
20
22
|
import { ActionConfirmDialog } from './ActionConfirmDialog';
|
|
21
23
|
import { ActionParamDialog } from './ActionParamDialog';
|
|
24
|
+
import { resolveActionParams } from '../utils/resolveActionParams';
|
|
22
25
|
import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
|
|
23
26
|
import { getRecordDisplayName } from '../utils';
|
|
24
27
|
const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
|
|
25
28
|
/**
|
|
26
29
|
* Audit field names auto-injected by the framework's `applySystemFields`.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
30
|
+
* Filtered out of the auto-generated body sections — they are rendered
|
|
31
|
+
* separately as a single subtle one-line `<RecordMetaFooter>` (see
|
|
32
|
+
* `@object-ui/plugin-detail`) so provenance stays discoverable without a
|
|
33
|
+
* heavy "System Information" panel. The inline-edit drawer also hides
|
|
34
|
+
* them via `DEFAULT_SYSTEM_FIELDS` in
|
|
35
|
+
* `@object-ui/plugin-detail/RecordDetailDrawer`.
|
|
31
36
|
*/
|
|
32
37
|
const AUDIT_FIELD_NAMES = new Set(['created_at', 'created_by', 'updated_at', 'updated_by']);
|
|
33
38
|
export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
@@ -36,7 +41,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
36
41
|
const { user } = useAuth();
|
|
37
42
|
const navigate = useNavigate();
|
|
38
43
|
const { t } = useObjectTranslation();
|
|
39
|
-
const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess } = useObjectLabel();
|
|
44
|
+
const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
|
|
40
45
|
const [isLoading, setIsLoading] = useState(true);
|
|
41
46
|
const [feedItems, setFeedItems] = useState([]);
|
|
42
47
|
const [recordViewers, setRecordViewers] = useState([]);
|
|
@@ -63,9 +68,15 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
63
68
|
}, []);
|
|
64
69
|
const paramCollectionHandler = useCallback((params) => {
|
|
65
70
|
return new Promise((resolve) => {
|
|
66
|
-
|
|
71
|
+
const resolved = resolveActionParams(params, {
|
|
72
|
+
objectName: objectName || objectDef?.name || '',
|
|
73
|
+
objects: objects || [],
|
|
74
|
+
fieldLabel,
|
|
75
|
+
fieldOptionLabel,
|
|
76
|
+
});
|
|
77
|
+
setParamState({ open: true, params: resolved, resolve });
|
|
67
78
|
});
|
|
68
|
-
}, []);
|
|
79
|
+
}, [objectName, objectDef, objects, fieldLabel, fieldOptionLabel]);
|
|
69
80
|
const toastHandler = useCallback((message, options) => {
|
|
70
81
|
if (options?.type === 'error')
|
|
71
82
|
toast.error(message);
|
|
@@ -561,32 +572,12 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
561
572
|
}),
|
|
562
573
|
},
|
|
563
574
|
];
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
const fieldsAlreadyShown = new Set(sections.flatMap((s) => (s.fields || []).map((f) => f.name)));
|
|
571
|
-
const auditFieldsToShow = Array.from(AUDIT_FIELD_NAMES).filter(name => objectDef.fields?.[name] && !fieldsAlreadyShown.has(name));
|
|
572
|
-
if (auditFieldsToShow.length > 0) {
|
|
573
|
-
sections.push({
|
|
574
|
-
title: sectionLabel(objectDef.name, 'system_info', 'System Information'),
|
|
575
|
-
collapsible: true,
|
|
576
|
-
defaultCollapsed: true,
|
|
577
|
-
fields: auditFieldsToShow.map(key => {
|
|
578
|
-
const fieldDef = objectDef.fields[key];
|
|
579
|
-
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
580
|
-
return {
|
|
581
|
-
name: key,
|
|
582
|
-
label: fieldDef.label || key,
|
|
583
|
-
type: fieldDef.type || 'text',
|
|
584
|
-
readonly: true,
|
|
585
|
-
...(refTarget && { reference_to: refTarget }),
|
|
586
|
-
};
|
|
587
|
-
}),
|
|
588
|
-
});
|
|
589
|
-
}
|
|
575
|
+
// Audit fields (created_at/created_by/updated_at/updated_by) are NOT
|
|
576
|
+
// appended as a section here — they are surfaced by `<RecordMetaFooter>`
|
|
577
|
+
// (rendered by DetailView) as a single subtle line below the content,
|
|
578
|
+
// replacing the old card-style "System Information" panel. The inline-edit
|
|
579
|
+
// drawer continues to hide them via `DEFAULT_SYSTEM_FIELDS` in
|
|
580
|
+
// `@object-ui/plugin-detail/RecordDetailDrawer`.
|
|
590
581
|
// Filter actions for record_header location and deduplicate by name
|
|
591
582
|
const recordHeaderActions = (() => {
|
|
592
583
|
const seen = new Set();
|
|
@@ -701,13 +692,19 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
701
692
|
onRowDelete,
|
|
702
693
|
};
|
|
703
694
|
});
|
|
695
|
+
const affordances = resolveCrudAffordances(objectDef);
|
|
704
696
|
return {
|
|
705
697
|
type: 'detail-view',
|
|
706
698
|
objectName: objectDef.name,
|
|
707
699
|
resourceId: pureRecordId,
|
|
708
700
|
showBack: true,
|
|
709
701
|
onBack: 'history',
|
|
710
|
-
|
|
702
|
+
// Hide the Edit button for objects whose lifecycle isn't user-managed
|
|
703
|
+
// (approval requests, audit logs, better-auth tables, …). The
|
|
704
|
+
// underlying form is also disabled when `managedBy !== 'platform'`
|
|
705
|
+
// (see plugin-form/ObjectForm), so even if a stray Edit URL is
|
|
706
|
+
// visited the inputs render read-only.
|
|
707
|
+
showEdit: affordances.edit,
|
|
711
708
|
title: objectDef.label,
|
|
712
709
|
primaryField,
|
|
713
710
|
sections,
|
|
@@ -732,7 +729,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
732
729
|
if (!objectDef) {
|
|
733
730
|
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 }) })] }) }));
|
|
734
731
|
}
|
|
735
|
-
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [
|
|
732
|
+
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 }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), onDataLoaded: (record) => {
|
|
736
733
|
if (!record || typeof record !== 'object')
|
|
737
734
|
return;
|
|
738
735
|
// Resolve the same way DetailView's header does, so the
|
|
@@ -39,6 +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 { ManagedByBadge } from '../components/ManagedByBadge';
|
|
42
43
|
import { useAuth } from '@object-ui/auth';
|
|
43
44
|
import { ExpressionEvaluator } from '@object-ui/core';
|
|
44
45
|
/**
|
|
@@ -161,7 +162,7 @@ export function RecordFormPage({ mode }) {
|
|
|
161
162
|
if (!objectDef) {
|
|
162
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')] }) })] }) }));
|
|
163
164
|
}
|
|
164
|
-
return (_jsx(ExpressionProvider, { user: expressionUser, app: { name: appName }, data: {}, children: _jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", "data-testid": "record-form-page", "data-mode": mode, children: [_jsxs("header", { className: "sticky top-0 z-10 flex items-center gap-3 border-b bg-background px-4 py-3 sm:px-6", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: goBack, "data-testid": "record-form-page-back", "aria-label": t('common.back', { defaultValue: 'Back' }), children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Link, { to: objectListUrl, className: "hover:text-foreground transition-colors", children: label }), _jsx("span", { "aria-hidden": "true", children: "/" }), _jsx("span", { className: "text-foreground font-medium", "data-testid": "record-form-page-title", children: pageTitle })] })] }), _jsx("div", { className: "flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto max-w-4xl", children: _jsx(ObjectForm, { schema: {
|
|
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: {
|
|
165
166
|
type: 'object-form',
|
|
166
167
|
formType: 'simple',
|
|
167
168
|
objectName: objectDef.name,
|
package/dist/views/ReportView.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState, useEffect, useCallback, useMemo, lazy, Suspense } from 'react';
|
|
3
3
|
import { useParams } from 'react-router-dom';
|
|
4
4
|
const ReportViewer = lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportViewer })));
|
|
5
|
+
const ReportRenderer = lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportRenderer })));
|
|
5
6
|
const ReportConfigPanel = lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportConfigPanel })));
|
|
6
7
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
7
8
|
import { Pencil, BarChart3, Loader2 } from 'lucide-react';
|
|
@@ -37,29 +38,31 @@ export function ReportView({ dataSource }) {
|
|
|
37
38
|
// State for report runtime data
|
|
38
39
|
const [reportRuntimeData, setReportRuntimeData] = useState([]);
|
|
39
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]);
|
|
40
59
|
// Derive available fields from object schema for filter/sort editors
|
|
41
60
|
// Uses live editSchema when available to respond to objectName changes
|
|
42
61
|
const availableFields = useMemo(() => {
|
|
43
62
|
const liveReport = editSchema || reportData;
|
|
44
63
|
const objName = liveReport?.objectName || liveReport?.dataSource?.object || liveReport?.dataSource?.resource;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (objDef?.fields) {
|
|
48
|
-
const fields = objDef.fields;
|
|
49
|
-
if (Array.isArray(fields)) {
|
|
50
|
-
return fields.map((f) => typeof f === 'string'
|
|
51
|
-
? { value: f, label: f, type: 'text' }
|
|
52
|
-
: { value: f.name, label: f.label || f.name, type: f.type || 'text' });
|
|
53
|
-
}
|
|
54
|
-
return Object.entries(fields).map(([name, def]) => ({
|
|
55
|
-
value: name,
|
|
56
|
-
label: def.label || name,
|
|
57
|
-
type: def.type || 'text',
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return FALLBACK_FIELDS;
|
|
62
|
-
}, [editSchema, reportData, objects]);
|
|
64
|
+
return getFieldsForObject(objName) ?? FALLBACK_FIELDS;
|
|
65
|
+
}, [editSchema, reportData, getFieldsForObject]);
|
|
63
66
|
// ---- Save helper --------------------------------------------------------
|
|
64
67
|
const saveSchema = useCallback(async (schema) => {
|
|
65
68
|
try {
|
|
@@ -326,6 +329,20 @@ export function ReportView({ dataSource }) {
|
|
|
326
329
|
};
|
|
327
330
|
// Use live-edited schema for preview (persists after closing panel until metadata refreshes)
|
|
328
331
|
const previewReport = editSchema || reportData;
|
|
332
|
+
// Route any object-backed spec report (matrix/joined/tabular/summary) through
|
|
333
|
+
// the spec ReportRenderer dispatcher. It handles aggregation, charts, KPIs
|
|
334
|
+
// and drill protocol end-to-end. The legacy ReportViewer is only used as a
|
|
335
|
+
// last resort for fully-legacy schemas that lack `objectName` (e.g. inline
|
|
336
|
+
// `fields` + `data` arrays from older app code).
|
|
337
|
+
const useSpecRenderer = Boolean(previewReport &&
|
|
338
|
+
previewReport.objectName &&
|
|
339
|
+
(previewReport.type === 'matrix' ||
|
|
340
|
+
previewReport.type === 'joined' ||
|
|
341
|
+
previewReport.type === 'summary' ||
|
|
342
|
+
previewReport.type === 'tabular' ||
|
|
343
|
+
previewReport.type === undefined ||
|
|
344
|
+
(Array.isArray(previewReport.groupingsAcross) && previewReport.groupingsAcross.length > 0) ||
|
|
345
|
+
Array.isArray(previewReport.columns)));
|
|
329
346
|
const reportForViewer = mapReportForViewer(previewReport);
|
|
330
347
|
const viewerSchema = {
|
|
331
348
|
type: 'report-viewer',
|
|
@@ -335,5 +352,5 @@ export function ReportView({ dataSource }) {
|
|
|
335
352
|
allowExport: true,
|
|
336
353
|
loading: dataLoading, // Loading state for data fetching
|
|
337
354
|
};
|
|
338
|
-
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: "
|
|
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 }] })] })] }));
|
|
339
356
|
}
|