@object-ui/app-shell 11.2.0 → 11.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 +562 -0
- package/README.md +23 -0
- package/dist/console/ConsoleShell.js +17 -2
- package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
- package/dist/console/home/CloudOnboardingNext.js +14 -4
- package/dist/console/home/HomePage.js +34 -7
- package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
- package/dist/console/organizations/OrganizationsPage.js +16 -7
- package/dist/hooks/useConsoleActionRuntime.js +32 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/preview/DraftChangesPanel.d.ts +3 -1
- package/dist/preview/DraftChangesPanel.js +6 -5
- package/dist/utils/deriveRelatedLists.d.ts +20 -5
- package/dist/utils/deriveRelatedLists.js +31 -13
- package/dist/utils/index.d.ts +2 -24
- package/dist/utils/index.js +14 -101
- package/dist/utils/resolveViewId.d.ts +23 -0
- package/dist/utils/resolveViewId.js +37 -0
- package/dist/utils/warnSuppressedListNav.d.ts +10 -0
- package/dist/utils/warnSuppressedListNav.js +40 -0
- package/dist/views/DashboardView.js +2 -3
- package/dist/views/InterfaceListPage.js +10 -5
- package/dist/views/ObjectView.js +65 -12
- package/dist/views/PageView.js +2 -3
- package/dist/views/RecordDetailView.js +131 -104
- package/dist/views/RecordFormPage.js +7 -1
- package/dist/views/RelatedRecordActionsBridge.d.ts +24 -0
- package/dist/views/RelatedRecordActionsBridge.js +114 -0
- package/dist/views/ReportView.js +2 -3
- package/dist/views/metadata-admin/PackagesPage.js +18 -7
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
- package/dist/views/metadata-admin/clientValidation.js +8 -2
- package/dist/views/metadata-admin/color-variant-field.d.ts +1 -12
- package/dist/views/metadata-admin/color-variant-field.js +11 -0
- package/dist/views/metadata-admin/i18n.d.ts +12 -21
- package/dist/views/metadata-admin/i18n.js +343 -2
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +25 -11
- package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
- package/dist/views/metadata-admin/permission-slice.js +70 -0
- package/dist/views/metadata-admin/previews/AppNavCanvas.js +11 -7
- package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
- package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
- package/dist/views/metadata-admin/previews/OutlineStrip.d.ts +1 -13
- package/dist/views/metadata-admin/previews/OutlineStrip.js +12 -0
- package/dist/views/metadata-admin/previews/PagePreview.js +9 -0
- package/dist/views/metadata-admin/previews/SourcePageEditor.d.ts +28 -0
- package/dist/views/metadata-admin/previews/SourcePageEditor.js +83 -0
- package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
- package/dist/views/studio-design/BuilderLanding.js +133 -0
- package/dist/views/studio-design/ObjectFormDesigner.d.ts +31 -0
- package/dist/views/studio-design/ObjectFormDesigner.js +226 -0
- package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectSettingsPanel.js +45 -0
- package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectValidationsPanel.js +78 -0
- package/dist/views/studio-design/StudioDesignSurface.d.ts +20 -0
- package/dist/views/studio-design/StudioDesignSurface.js +1306 -0
- package/dist/views/studio-design/metadataError.d.ts +23 -0
- package/dist/views/studio-design/metadataError.js +44 -0
- package/dist/views/studio-design/packages-io.d.ts +27 -0
- package/dist/views/studio-design/packages-io.js +61 -0
- package/package.json +46 -43
|
@@ -8,7 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
*/
|
|
9
9
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
10
10
|
import { useParams, useNavigate, useLocation, Link } from 'react-router-dom';
|
|
11
|
-
import { DetailView, RecordChatterPanel, buildDefaultPageSchema, extractMentions } from '@object-ui/plugin-detail';
|
|
11
|
+
import { DetailView, RecordChatterPanel, buildDefaultPageSchema, deriveFieldGroupDetailSections, extractMentions } from '@object-ui/plugin-detail';
|
|
12
12
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
13
13
|
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
|
|
14
14
|
import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider, useGlobalUndo } from '@object-ui/react';
|
|
@@ -26,6 +26,7 @@ import { ActionConfirmDialog } from './ActionConfirmDialog';
|
|
|
26
26
|
import { ActionParamDialog } from './ActionParamDialog';
|
|
27
27
|
import { ActionResultDialog } from './ActionResultDialog';
|
|
28
28
|
import { FlowRunner } from './FlowRunner';
|
|
29
|
+
import { RelatedRecordActionsBridge } from './RelatedRecordActionsBridge';
|
|
29
30
|
import { resolveActionParams } from '../utils/resolveActionParams';
|
|
30
31
|
import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
|
|
31
32
|
import { useRecordApprovals } from '../hooks/useRecordApprovals';
|
|
@@ -47,9 +48,8 @@ const AUDIT_FIELD_NAMES = new Set(['created_at', 'created_by', 'updated_at', 'up
|
|
|
47
48
|
/**
|
|
48
49
|
* System/tenant fields that the framework auto-injects on every record but
|
|
49
50
|
* which carry no business value on a detail page. Hidden from the
|
|
50
|
-
* auto-generated
|
|
51
|
-
*
|
|
52
|
-
* `objectDef.views.form.sections`.
|
|
51
|
+
* auto-generated sections. Authors who really want to surface one can
|
|
52
|
+
* assign it to a `fieldGroups` group explicitly (explicit listing wins).
|
|
53
53
|
*/
|
|
54
54
|
const HIDDEN_SYSTEM_FIELD_NAMES = new Set([
|
|
55
55
|
'organization_id', 'tenant_id', 'is_deleted', 'deleted_at',
|
|
@@ -359,29 +359,37 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
359
359
|
case 'opportunity_mark_lost':
|
|
360
360
|
await dataSource.update(objectName, pureRecordId, { stage: 'closed_lost', loss_reason: params.loss_reason });
|
|
361
361
|
break;
|
|
362
|
-
default:
|
|
363
|
-
// Generic: update record with collected params
|
|
364
|
-
|
|
362
|
+
default: {
|
|
363
|
+
// Generic: update record with collected params. Related-list row
|
|
364
|
+
// actions retarget a CHILD record via explicit `objectName`/`recordId`;
|
|
365
|
+
// otherwise the update falls back to this page's record.
|
|
366
|
+
const targetObject = action.objectName ?? objectName;
|
|
367
|
+
const targetId = action.recordId ?? pureRecordId;
|
|
368
|
+
const isThisRecord = targetObject === objectName && String(targetId) === String(pureRecordId);
|
|
369
|
+
if (Object.keys(params).length > 0 && targetObject && targetId != null) {
|
|
365
370
|
// Undoable single-record update: capture the changed fields' prior
|
|
366
371
|
// values from the loaded record so the success toast can offer Undo.
|
|
367
|
-
|
|
372
|
+
// Only this page's record has its prior values loaded, so child-row
|
|
373
|
+
// updates skip undo capture.
|
|
374
|
+
if (action.undoable && isThisRecord && pageRecord) {
|
|
368
375
|
const undoData = {};
|
|
369
376
|
for (const k of Object.keys(params))
|
|
370
377
|
undoData[k] = pageRecord[k] ?? null;
|
|
371
378
|
undo = {
|
|
372
|
-
id: `undo-${
|
|
379
|
+
id: `undo-${targetObject}-${targetId}-${Date.now()}`,
|
|
373
380
|
type: 'update',
|
|
374
|
-
objectName,
|
|
375
|
-
recordId: String(
|
|
381
|
+
objectName: targetObject,
|
|
382
|
+
recordId: String(targetId),
|
|
376
383
|
timestamp: Date.now(),
|
|
377
|
-
description: action.label || `Undo ${
|
|
384
|
+
description: action.label || `Undo ${targetObject}`,
|
|
378
385
|
undoData,
|
|
379
386
|
redoData: { ...params },
|
|
380
387
|
};
|
|
381
388
|
}
|
|
382
|
-
await dataSource.update(
|
|
389
|
+
await dataSource.update(targetObject, String(targetId), params);
|
|
383
390
|
}
|
|
384
391
|
break;
|
|
392
|
+
}
|
|
385
393
|
}
|
|
386
394
|
const shouldRefresh = action.refreshAfter === true;
|
|
387
395
|
if (shouldRefresh) {
|
|
@@ -419,8 +427,11 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
419
427
|
method: 'POST',
|
|
420
428
|
headers: { 'Content-Type': 'application/json' },
|
|
421
429
|
body: JSON.stringify({
|
|
422
|
-
|
|
423
|
-
objectName
|
|
430
|
+
// Related-list row actions retarget the flow at a CHILD record via
|
|
431
|
+
// an explicit `recordId` / `objectName`; fall back to this page's
|
|
432
|
+
// record when the action carries none (header/more actions).
|
|
433
|
+
recordId: action.recordId ?? pureRecordId,
|
|
434
|
+
objectName: action.objectName ?? objectName,
|
|
424
435
|
params: action.params ?? {},
|
|
425
436
|
}),
|
|
426
437
|
});
|
|
@@ -561,7 +572,9 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
561
572
|
const res = await authFetch(`${baseUrl}/api/v1/actions/${encodeURIComponent(obj)}/${encodeURIComponent(targetName)}`, {
|
|
562
573
|
method: 'POST',
|
|
563
574
|
headers: { 'Content-Type': 'application/json' },
|
|
564
|
-
|
|
575
|
+
// Related-list row actions retarget a CHILD record via explicit
|
|
576
|
+
// `recordId`; header/more actions carry none and use this page's id.
|
|
577
|
+
body: JSON.stringify({ recordId: action.recordId ?? pureRecordId, params }),
|
|
565
578
|
});
|
|
566
579
|
const json = await res.json().catch(() => null);
|
|
567
580
|
// The action route wraps the handler's return value in a {success, data}
|
|
@@ -1215,76 +1228,49 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1215
1228
|
if (!objectDef) {
|
|
1216
1229
|
return { type: 'detail-view' };
|
|
1217
1230
|
}
|
|
1218
|
-
// Auto-detect primary field: prefer objectDef metadata
|
|
1231
|
+
// Auto-detect primary field: prefer objectDef metadata — `primaryField`
|
|
1232
|
+
// (objectui-local override), then the spec-canonical `nameField` and its
|
|
1233
|
+
// deprecated `displayNameField` alias (ADR-0079) — then the 'name'/'title'
|
|
1234
|
+
// heuristic.
|
|
1219
1235
|
const primaryField = objectDef.primaryField
|
|
1236
|
+
|| objectDef.nameField
|
|
1237
|
+
|| objectDef.displayNameField
|
|
1220
1238
|
|| Object.keys(objectDef.fields || {}).find((key) => key === 'name' || key === 'title');
|
|
1221
|
-
// Build sections:
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
.map((f) => {
|
|
1239
|
-
const fieldName = typeof f === 'string' ? f : f.name;
|
|
1240
|
-
const fieldDef = objectDef.fields[fieldName];
|
|
1241
|
-
if (!fieldDef) {
|
|
1242
|
-
console.warn(`[RecordDetailView] Field "${fieldName}" not found in ${objectDef.name} definition`);
|
|
1243
|
-
return { name: fieldName, label: fieldName };
|
|
1244
|
-
}
|
|
1245
|
-
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
1246
|
-
return {
|
|
1247
|
-
name: fieldName,
|
|
1248
|
-
label: fieldDef.label || fieldName,
|
|
1249
|
-
type: fieldDef.type || 'text',
|
|
1250
|
-
...(fieldDef.options && { options: fieldDef.options }),
|
|
1251
|
-
...(refTarget && { reference_to: refTarget }),
|
|
1252
|
-
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
|
|
1253
|
-
...(fieldDef.currency && { currency: fieldDef.currency }),
|
|
1254
|
-
};
|
|
1255
|
-
}),
|
|
1256
|
-
}))
|
|
1257
|
-
: (() => {
|
|
1258
|
-
// Auto-grouping (platform B): when no form sections are authored,
|
|
1259
|
-
// split fields into a primary section and a collapsible
|
|
1260
|
-
// "More details" section so long-form/secondary fields don't
|
|
1261
|
-
// dilute the main grid. The primary section stays untitled so
|
|
1262
|
-
// DetailSection still flattens its chrome when alone.
|
|
1263
|
-
const allFields = Object.keys(objectDef.fields || {})
|
|
1264
|
-
.filter((key) => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden);
|
|
1265
|
-
const toField = (key) => {
|
|
1266
|
-
const fieldDef = objectDef.fields[key];
|
|
1267
|
-
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
1268
|
-
return {
|
|
1269
|
-
name: key,
|
|
1270
|
-
label: fieldDef.label || key,
|
|
1271
|
-
type: fieldDef.type || 'text',
|
|
1272
|
-
...(fieldDef.options && { options: fieldDef.options }),
|
|
1273
|
-
...(refTarget && { reference_to: refTarget }),
|
|
1274
|
-
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
|
|
1275
|
-
...(fieldDef.currency && { currency: fieldDef.currency }),
|
|
1276
|
-
};
|
|
1239
|
+
// Build sections (ADR-0085: grouping is the `fieldGroups` semantic
|
|
1240
|
+
// role — there is no per-surface sections override; per-page
|
|
1241
|
+
// customization goes through an assigned Page schema):
|
|
1242
|
+
// 1) sections derived from the object's `fieldGroups`;
|
|
1243
|
+
// 2) auto-grouping (primary + collapsible "More details").
|
|
1244
|
+
const sections = (() => {
|
|
1245
|
+
const toField = (key) => {
|
|
1246
|
+
const fieldDef = objectDef.fields[key];
|
|
1247
|
+
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
1248
|
+
return {
|
|
1249
|
+
name: key,
|
|
1250
|
+
label: fieldDef.label || key,
|
|
1251
|
+
type: fieldDef.type || 'text',
|
|
1252
|
+
...(fieldDef.options && { options: fieldDef.options }),
|
|
1253
|
+
...(refTarget && { reference_to: refTarget }),
|
|
1254
|
+
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
|
|
1255
|
+
...(fieldDef.currency && { currency: fieldDef.currency }),
|
|
1277
1256
|
};
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1257
|
+
};
|
|
1258
|
+
// Auto-grouping (platform B): split fields into a primary section
|
|
1259
|
+
// and a collapsible "More details" section so long-form/secondary
|
|
1260
|
+
// fields don't dilute the main grid. The primary section stays
|
|
1261
|
+
// untitled so DetailSection still flattens its chrome when alone.
|
|
1262
|
+
// Shared by the pure-fallback path and the ungrouped remainder of
|
|
1263
|
+
// the fieldGroups path below.
|
|
1264
|
+
const splitPrimarySecondary = (keys) => {
|
|
1265
|
+
const primaryKeys = keys.filter((k) => !isSecondaryField(k, objectDef.fields[k]));
|
|
1266
|
+
const secondaryKeys = keys.filter((k) => isSecondaryField(k, objectDef.fields[k]));
|
|
1267
|
+
// Keep the legacy single-untitled-section behaviour when the
|
|
1268
|
+
// split would leave one side empty.
|
|
1283
1269
|
if (secondaryKeys.length === 0 || primaryKeys.length === 0) {
|
|
1284
1270
|
return [
|
|
1285
1271
|
{
|
|
1286
1272
|
showBorder: false,
|
|
1287
|
-
fields:
|
|
1273
|
+
fields: keys.map(toField),
|
|
1288
1274
|
},
|
|
1289
1275
|
];
|
|
1290
1276
|
}
|
|
@@ -1302,7 +1288,31 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1302
1288
|
fields: secondaryKeys.map(toField),
|
|
1303
1289
|
},
|
|
1304
1290
|
];
|
|
1305
|
-
}
|
|
1291
|
+
};
|
|
1292
|
+
// 1) fieldGroups-derived sections (the ADR-0085 semantic role,
|
|
1293
|
+
// same shared derivation the runtime form honours). Declared
|
|
1294
|
+
// groups render as titled cards in declared order; the
|
|
1295
|
+
// trailing untitled bucket (ungrouped fields) still goes
|
|
1296
|
+
// through the primary/"More details" split so long-form
|
|
1297
|
+
// fields stay tucked away.
|
|
1298
|
+
const grouped = deriveFieldGroupDetailSections(objectDef);
|
|
1299
|
+
if (grouped) {
|
|
1300
|
+
return grouped.flatMap((sec) => {
|
|
1301
|
+
if (!sec.name) {
|
|
1302
|
+
return splitPrimarySecondary(sec.fields.map((f) => f.name));
|
|
1303
|
+
}
|
|
1304
|
+
return [{
|
|
1305
|
+
...sec,
|
|
1306
|
+
title: sectionLabel(objectDef.name, sec.name, sec.title),
|
|
1307
|
+
showBorder: true,
|
|
1308
|
+
}];
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
// 3) Pure auto-grouping fallback.
|
|
1312
|
+
const allFields = Object.keys(objectDef.fields || {})
|
|
1313
|
+
.filter((key) => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden);
|
|
1314
|
+
return splitPrimarySecondary(allFields);
|
|
1315
|
+
})();
|
|
1306
1316
|
// Audit fields (created_at/created_by/updated_at/updated_by) are NOT
|
|
1307
1317
|
// appended as a section here — they are surfaced by `<RecordMetaFooter>`
|
|
1308
1318
|
// (rendered by DetailView) as a single subtle line below the content,
|
|
@@ -1372,10 +1382,22 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1372
1382
|
}
|
|
1373
1383
|
return base;
|
|
1374
1384
|
})();
|
|
1375
|
-
// Build highlightFields
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
const
|
|
1385
|
+
// Build highlightFields from the object's semantic role (ADR-0085).
|
|
1386
|
+
// Bare field names resolve label/type from the field def.
|
|
1387
|
+
const rawHighlightFields = objectDef.highlightFields ?? [];
|
|
1388
|
+
const highlightFields = (Array.isArray(rawHighlightFields) ? rawHighlightFields : [])
|
|
1389
|
+
.map((f) => {
|
|
1390
|
+
const name = typeof f === 'string' ? f : f?.name;
|
|
1391
|
+
if (!name)
|
|
1392
|
+
return null;
|
|
1393
|
+
const fieldDef = objectDef.fields?.[name];
|
|
1394
|
+
return {
|
|
1395
|
+
name,
|
|
1396
|
+
label: fieldDef?.label || name,
|
|
1397
|
+
...(fieldDef?.type ? { type: fieldDef.type } : {}),
|
|
1398
|
+
};
|
|
1399
|
+
})
|
|
1400
|
+
.filter((f) => !!f);
|
|
1379
1401
|
// Build related entries from reverse-reference child objects.
|
|
1380
1402
|
// `referenceField` is the FK field on the child pointing back to this
|
|
1381
1403
|
// record — passed so the related-list renderer can hide the redundant
|
|
@@ -1468,9 +1490,10 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1468
1490
|
// Surface the child object's canonical display field so the
|
|
1469
1491
|
// right-rail can show meaningful labels (`user_agent`, `email`,
|
|
1470
1492
|
// …) instead of opaque IDs like `kCc8mhJr0bRs0r9Ykd09…`.
|
|
1471
|
-
displayField: childObjectDef?.
|
|
1472
|
-
|
|
1473
|
-
|
|
1493
|
+
displayField: childObjectDef?.nameField ||
|
|
1494
|
+
childObjectDef?.displayNameField ||
|
|
1495
|
+
(Array.isArray(childObjectDef?.highlightFields)
|
|
1496
|
+
? childObjectDef.highlightFields[0]
|
|
1474
1497
|
: undefined),
|
|
1475
1498
|
onNew,
|
|
1476
1499
|
onViewAll,
|
|
@@ -1505,7 +1528,6 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1505
1528
|
}),
|
|
1506
1529
|
...(related.length > 0 && { related }),
|
|
1507
1530
|
...(highlightFields.length > 0 && { highlightFields }),
|
|
1508
|
-
...(sectionGroups && sectionGroups.length > 0 && { sectionGroups }),
|
|
1509
1531
|
...(recordHeaderActions.length > 0 && {
|
|
1510
1532
|
actions: [{
|
|
1511
1533
|
type: 'action:bar',
|
|
@@ -1666,14 +1688,21 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1666
1688
|
const synthRelated = Array.isArray(detailSchema.related)
|
|
1667
1689
|
? detailSchema.related
|
|
1668
1690
|
.filter((r) => r?.api && r?.referenceField)
|
|
1669
|
-
.map((r) =>
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1691
|
+
.map((r) => {
|
|
1692
|
+
// Carry the `relatedList: 'primary'` prominence flag from the derived
|
|
1693
|
+
// relationship graph. Matched by (childObject, referenceField) — the
|
|
1694
|
+
// unique key of a related list — so it is robust to ordering/filtering.
|
|
1695
|
+
const derived = childRelations.find((c) => c.childObject === r.api && c.referenceField === r.referenceField);
|
|
1696
|
+
return {
|
|
1697
|
+
title: r.title,
|
|
1698
|
+
objectName: r.api,
|
|
1699
|
+
relationshipField: r.referenceField,
|
|
1700
|
+
...(Array.isArray(r.columns) ? { columns: r.columns } : {}),
|
|
1701
|
+
...(typeof r.pageSize === 'number' ? { limit: r.pageSize } : {}),
|
|
1702
|
+
...(r.icon ? { icon: r.icon } : {}),
|
|
1703
|
+
...(derived?.isPrimary ? { isPrimary: true } : {}),
|
|
1704
|
+
};
|
|
1705
|
+
})
|
|
1677
1706
|
: undefined;
|
|
1678
1707
|
const synthHistory = detailSchema.history
|
|
1679
1708
|
? {
|
|
@@ -1694,16 +1723,14 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1694
1723
|
headerActions: synthHeaderActions,
|
|
1695
1724
|
related: synthRelated,
|
|
1696
1725
|
history: synthHistory,
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
//
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
hideRelatedTab: objectDef?.detail?.hideRelatedTab === true || undefined,
|
|
1703
|
-
relatedLayout: objectDef?.detail?.relatedLayout === 'tabs' ? 'tabs' : undefined,
|
|
1726
|
+
// ADR-0085 removed the per-object `detail.*` presentation
|
|
1727
|
+
// toggles (show/hideReferenceRail, hideRelatedTab, relatedLayout)
|
|
1728
|
+
// — the synth defaults apply; per-page layout goes through an
|
|
1729
|
+
// assigned Page schema (`record:reference_rail` stays available
|
|
1730
|
+
// there as a renderer capability).
|
|
1704
1731
|
...(assignedSlots ? { slots: assignedSlots } : {}),
|
|
1705
1732
|
});
|
|
1706
|
-
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: _jsxs(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, onResultDialog: resultDialogHandler, onModal: modalHandler, handlers: { api: apiHandler, flow: flowHandler, script: 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: {
|
|
1733
|
+
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: _jsxs(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, onResultDialog: resultDialogHandler, onModal: modalHandler, handlers: { api: apiHandler, flow: flowHandler, script: 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(RelatedRecordActionsBridge, { appName: appName, objects: objects, dataSource: dataSource, actionLabel: actionLabel, children: _jsx(SchemaRenderer, { schema: renderedPage }) }), showAutoDiscussion && (_jsx("div", { className: "mt-6", children: _jsx(RecordChatterPanel, { config: {
|
|
1707
1734
|
position: 'bottom',
|
|
1708
1735
|
collapsible: false,
|
|
1709
1736
|
feed: {
|
|
@@ -181,7 +181,13 @@ export function RecordFormPage({ mode }) {
|
|
|
181
181
|
// `simple` here (see the form-layout-vs-presentation modelling note in #1890).
|
|
182
182
|
const formDef = objectDef.form ?? objectDef.formViews?.default ?? {};
|
|
183
183
|
const pageFormType = ['tabbed', 'wizard', 'split'].includes(formDef.type) ? formDef.type : 'simple';
|
|
184
|
-
|
|
184
|
+
// Curated `sections` are wired through for EVERY layout family — a `simple`
|
|
185
|
+
// form view's sections still carry the authored field selection, order,
|
|
186
|
+
// grouping and per-field `visibleOn` predicates (#2212). Dropping them for
|
|
187
|
+
// `simple` made the page-mode form fall back to the raw schema (every field,
|
|
188
|
+
// no conditional visibility) while the New/Edit modal honored the same view
|
|
189
|
+
// via resolveFormViewLayout.
|
|
190
|
+
const formLayoutProps = Array.isArray(formDef.sections) && formDef.sections.length > 0
|
|
185
191
|
? {
|
|
186
192
|
sections: formDef.sections,
|
|
187
193
|
defaultTab: formDef.defaultTab,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/** Notify open related lists for `objectName` to refetch (see RelatedList). */
|
|
9
|
+
export declare function notifyRelatedChanged(objectName: string): void;
|
|
10
|
+
/** i18n label resolver signature (matches `useObjectLabel().actionLabel`). */
|
|
11
|
+
type ActionLabelFn = (objectName: string | undefined, actionName: string, fallback: string) => string;
|
|
12
|
+
export interface RelatedRecordActionsBridgeProps {
|
|
13
|
+
/** Current app segment used to build `/apps/:appName/...` routes. */
|
|
14
|
+
appName?: string;
|
|
15
|
+
/** All object definitions (to resolve the child object + its actions). */
|
|
16
|
+
objects: any[];
|
|
17
|
+
/** Data source for delete + action dispatch. */
|
|
18
|
+
dataSource: any;
|
|
19
|
+
/** Localizes a child action's label (falls back to the raw label). */
|
|
20
|
+
actionLabel: ActionLabelFn;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
export declare function RelatedRecordActionsBridge({ appName, objects, dataSource, actionLabel, children, }: RelatedRecordActionsBridgeProps): import("react").JSX.Element;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ObjectUI
|
|
4
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* RelatedRecordActionsBridge — supplies the console's object-aware CRUD +
|
|
11
|
+
* action handlers to the `record:related_list` renderers on a detail page.
|
|
12
|
+
*
|
|
13
|
+
* The renderer (in `@object-ui/plugin-detail`) knows the child object, the FK
|
|
14
|
+
* back to the parent, and the parent id — but not the SPA routes, the
|
|
15
|
+
* create/edit form pages, or the per-object lifecycle affordances. This bridge
|
|
16
|
+
* (mounted inside the page's `ActionProvider`) closes that gap:
|
|
17
|
+
*
|
|
18
|
+
* - 查看详情 → navigate to the child record's detail route
|
|
19
|
+
* - 增 → navigate to the child's `/new` page, pre-linking the parent
|
|
20
|
+
* via `?<relationshipField>=<parentId>` (the convention
|
|
21
|
+
* RecordFormPage already reads as create-mode initial values)
|
|
22
|
+
* - 改 → navigate to the child record's `/edit` page
|
|
23
|
+
* - 删 → `dataSource.delete(child, id)` (RelatedList shows the confirm
|
|
24
|
+
* dialog and refreshes afterwards)
|
|
25
|
+
* - 子对象 action → the child object's `list_item` actions, executed against
|
|
26
|
+
* the clicked row through the page's shared ActionRunner
|
|
27
|
+
*
|
|
28
|
+
* Each affordance is gated by {@link resolveCrudAffordances} so system /
|
|
29
|
+
* append-only children never show New / Edit / Delete. When this bridge is
|
|
30
|
+
* absent (e.g. the Studio designer) the related list stays read-only.
|
|
31
|
+
*/
|
|
32
|
+
import { useCallback, useMemo } from 'react';
|
|
33
|
+
import { useNavigate } from 'react-router-dom';
|
|
34
|
+
import { RelatedRecordActionsProvider, useAction, } from '@object-ui/react';
|
|
35
|
+
import { resolveCrudAffordances } from '../utils/crudAffordances';
|
|
36
|
+
/** Notify open related lists for `objectName` to refetch (see RelatedList). */
|
|
37
|
+
export function notifyRelatedChanged(objectName) {
|
|
38
|
+
if (typeof window === 'undefined')
|
|
39
|
+
return;
|
|
40
|
+
window.dispatchEvent(new CustomEvent('objectui:related-changed', { detail: { objectName } }));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Derive the child object's row actions (metadata `actions` filtered to the
|
|
44
|
+
* `list_item` location), localized and shaped for the related-list row menu.
|
|
45
|
+
*/
|
|
46
|
+
function deriveRowActions(childDef, actionLabel) {
|
|
47
|
+
const actions = Array.isArray(childDef?.actions) ? childDef.actions : [];
|
|
48
|
+
return actions
|
|
49
|
+
.filter((a) => Array.isArray(a?.locations) && a.locations.includes('list_item'))
|
|
50
|
+
.map((a) => ({
|
|
51
|
+
...a,
|
|
52
|
+
label: actionLabel(childDef.name, a.name, a.label || a.name),
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
export function RelatedRecordActionsBridge({ appName, objects, dataSource, actionLabel, children, }) {
|
|
56
|
+
const navigate = useNavigate();
|
|
57
|
+
const { execute } = useAction();
|
|
58
|
+
const base = appName ? `/apps/${appName}` : '';
|
|
59
|
+
// Execute a child object's row action against the clicked record. Reuses the
|
|
60
|
+
// page's ActionRunner (confirm dialog, toast, param collection are handled by
|
|
61
|
+
// it) but retargets it at the CHILD object + row via the action's
|
|
62
|
+
// `objectName` / `recordId`, which the record-detail action handlers honor.
|
|
63
|
+
const runRowAction = useCallback(async (childObject, record, action) => {
|
|
64
|
+
const id = record?.id ?? record?._id;
|
|
65
|
+
const def = {
|
|
66
|
+
...action,
|
|
67
|
+
objectName: childObject,
|
|
68
|
+
...(id != null ? { recordId: String(id) } : {}),
|
|
69
|
+
params: { ...action.params },
|
|
70
|
+
};
|
|
71
|
+
const res = await execute(def);
|
|
72
|
+
// Refresh open related lists for this child object after a successful
|
|
73
|
+
// mutating action (the row menu handler is otherwise fire-and-forget).
|
|
74
|
+
if (res?.success)
|
|
75
|
+
notifyRelatedChanged(childObject);
|
|
76
|
+
}, [execute]);
|
|
77
|
+
const value = useMemo(() => ({
|
|
78
|
+
resolve: ({ objectName, relationshipField, parentId }) => {
|
|
79
|
+
const childDef = objects.find((o) => o?.name === objectName);
|
|
80
|
+
if (!childDef || !base)
|
|
81
|
+
return {};
|
|
82
|
+
const aff = resolveCrudAffordances(childDef);
|
|
83
|
+
const detailUrl = (id) => `${base}/${objectName}/record/${encodeURIComponent(String(id))}`;
|
|
84
|
+
const handlers = {
|
|
85
|
+
// Viewing a child record is always allowed when the list is visible.
|
|
86
|
+
onView: (id) => navigate(detailUrl(id)),
|
|
87
|
+
};
|
|
88
|
+
if (aff.create) {
|
|
89
|
+
handlers.onCreate = () => {
|
|
90
|
+
const canLink = relationshipField && parentId != null && parentId !== '';
|
|
91
|
+
const qs = canLink
|
|
92
|
+
? `?${encodeURIComponent(relationshipField)}=${encodeURIComponent(String(parentId))}`
|
|
93
|
+
: '';
|
|
94
|
+
navigate(`${base}/${objectName}/new${qs}`);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (aff.edit) {
|
|
98
|
+
handlers.onEdit = (id) => navigate(`${detailUrl(id)}/edit`);
|
|
99
|
+
}
|
|
100
|
+
if (aff.delete) {
|
|
101
|
+
handlers.onDelete = async (id) => {
|
|
102
|
+
await dataSource?.delete?.(objectName, String(id));
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const rowActions = deriveRowActions(childDef, actionLabel);
|
|
106
|
+
if (rowActions.length > 0) {
|
|
107
|
+
handlers.rowActions = rowActions;
|
|
108
|
+
handlers.onRowAction = (action, record) => runRowAction(objectName, record, action);
|
|
109
|
+
}
|
|
110
|
+
return handlers;
|
|
111
|
+
},
|
|
112
|
+
}), [objects, base, navigate, dataSource, actionLabel, runRowAction]);
|
|
113
|
+
return (_jsx(RelatedRecordActionsProvider, { value: value, children: children }));
|
|
114
|
+
}
|
package/dist/views/ReportView.js
CHANGED
|
@@ -16,7 +16,7 @@ import { preferLocal } from '../utils/preferLocal';
|
|
|
16
16
|
import { useAdapter } from '../providers/AdapterProvider';
|
|
17
17
|
import { useMetadataClient } from './metadata-admin/useMetadata';
|
|
18
18
|
import { persistRuntimeMetadata } from './runtime-metadata-persistence';
|
|
19
|
-
import {
|
|
19
|
+
import { useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
20
20
|
import { DrillDownDrawer } from '@object-ui/plugin-dashboard';
|
|
21
21
|
import { DrillNavigationProvider } from '@object-ui/react';
|
|
22
22
|
import { useOpenRecordList } from './useOpenRecordList';
|
|
@@ -40,8 +40,7 @@ export function ReportView({ dataSource }) {
|
|
|
40
40
|
const metadataClient = useMetadataClient();
|
|
41
41
|
// Editing a report mutates the SHARED definition, so it is an admin-only
|
|
42
42
|
// quick-edit affordance (mirrors ObjectView's view-config gate).
|
|
43
|
-
const
|
|
44
|
-
const isAdmin = user?.role === 'admin';
|
|
43
|
+
const isAdmin = useIsWorkspaceAdmin();
|
|
45
44
|
const [configPanelOpen, setConfigPanelOpen] = useState(false);
|
|
46
45
|
// Version counter — incremented on save to refresh the stable config reference
|
|
47
46
|
const [configVersion, setConfigVersion] = useState(0);
|
|
@@ -44,16 +44,23 @@ async function apiJson(path, init) {
|
|
|
44
44
|
/* -------------------------------------------------------------------------- */
|
|
45
45
|
function ScopeBadge({ scope }) {
|
|
46
46
|
const locale = React.useMemo(() => detectLocale(), []);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
// Writability semantics, aligned with the builder (studio-design/packages-io):
|
|
48
|
+
// a SCOPE-LESS entry is a database base package (writable — authoring lives
|
|
49
|
+
// there), while `project` marks a read-only code package. Defaulting the
|
|
50
|
+
// missing scope to 'project' used to render both with the same badge, which
|
|
51
|
+
// contradicted the builder's 可写/只读 labeling for the very same package.
|
|
52
|
+
if (!scope) {
|
|
53
|
+
return (_jsx(Badge, { className: "bg-emerald-400/15 text-emerald-600 hover:bg-emerald-400/15 dark:text-emerald-300", children: t('engine.packages.scope.writable', locale) }));
|
|
54
|
+
}
|
|
55
|
+
const variant = scope === 'project' ? 'default' : scope === 'system' ? 'secondary' : 'outline';
|
|
56
|
+
const labelKey = scope === 'project'
|
|
50
57
|
? 'engine.packages.scope.project'
|
|
51
|
-
:
|
|
58
|
+
: scope === 'system'
|
|
52
59
|
? 'engine.packages.scope.system'
|
|
53
|
-
:
|
|
60
|
+
: scope === 'cloud'
|
|
54
61
|
? 'engine.packages.scope.cloud'
|
|
55
62
|
: '';
|
|
56
|
-
return _jsx(Badge, { variant: variant, children: labelKey ? t(labelKey, locale) :
|
|
63
|
+
return _jsx(Badge, { variant: variant, children: labelKey ? t(labelKey, locale) : scope });
|
|
57
64
|
}
|
|
58
65
|
function StatusBadge({ pkg }) {
|
|
59
66
|
const locale = React.useMemo(() => detectLocale(), []);
|
|
@@ -99,7 +106,11 @@ export function CreatePackageDialog({ open, onOpenChange, onCreated, }) {
|
|
|
99
106
|
name: name.trim(),
|
|
100
107
|
version: version.trim(),
|
|
101
108
|
type: 'app',
|
|
102
|
-
scope
|
|
109
|
+
// No `scope`: runtime-created base packages are writable authoring
|
|
110
|
+
// targets. `scope: 'project'` marks read-only CODE packages — the
|
|
111
|
+
// old hardcode here made Setup-created bases read as 只读 in the
|
|
112
|
+
// builder's switcher/landing while the builder's own creator made
|
|
113
|
+
// writable ones. One creation semantic everywhere now.
|
|
103
114
|
},
|
|
104
115
|
}),
|
|
105
116
|
});
|
|
@@ -35,5 +35,22 @@ import * as React from 'react';
|
|
|
35
35
|
export interface PermissionMatrixEditPageProps {
|
|
36
36
|
type: string;
|
|
37
37
|
name: string;
|
|
38
|
+
/**
|
|
39
|
+
* When set, the matrix is scoped to a single package (ADR-0086 P0): it lists
|
|
40
|
+
* only the objects that package declares, and Save merges just that slice
|
|
41
|
+
* back — other packages' contributed rows are left untouched. When omitted,
|
|
42
|
+
* the matrix operates at environment scope (all objects, whole-record save).
|
|
43
|
+
*/
|
|
44
|
+
packageId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* ADR-0086 P2 (D6/D7 — the package door). When editing under a `packageId`,
|
|
47
|
+
* a permission set is package **metadata**: Save writes a **draft** (not a
|
|
48
|
+
* live record), published atomically with the rest of the package. `onDraftSaved`
|
|
49
|
+
* notifies the surface so its pending-changes counter refreshes; `publishNonce`
|
|
50
|
+
* bumps on publish so the editor re-reads the now-published baseline (its draft
|
|
51
|
+
* is gone). Both are no-ops at environment scope, where Save stays live (D7).
|
|
52
|
+
*/
|
|
53
|
+
onDraftSaved?: () => void;
|
|
54
|
+
publishNonce?: number;
|
|
38
55
|
}
|
|
39
|
-
export declare function PermissionMatrixEditPage({ type, name }: PermissionMatrixEditPageProps): React.JSX.Element;
|
|
56
|
+
export declare function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, publishNonce }: PermissionMatrixEditPageProps): React.JSX.Element;
|