Skip to content

Commit 1d5e6e9

Browse files
authored
feat(detail): wire object fieldGroups into detail sections; read hints from spec-writable detail.* block (#2148) (#2149)
问题一: 对象上定义的 fieldGroups(字段分组)此前只在表单里生效, 详情页(默认详情 + synth 页面)完全忽略。现在: - plugin-detail synth 新增 deriveFieldGroupDetailSections():按声明顺序 把 fields[].group 归入 fieldGroups[] 定义的分组,透传 collapsible/ collapsed(映射为 defaultCollapsed),空分组丢弃,未分组字段进末尾 无标题分区(跳过审计/系统字段,显式分组的审计字段保留)。 - 新增 resolveDetailSections():options.sections > detail.sections > fieldGroups 推导;buildDefaultDetails 全部 synth 路径接入。 - RecordDetailView 复用该推导:分组标题走 sectionLabel i18n,未分组 剩余字段保留 primary/"更多详情" 折叠拆分。 问题二: console 此前从 spec ObjectSchema 拒绝/剥离的键读取详情提示 (views.detail.highlightFields / views.form.sections / 顶层 stageField)。 现在优先读 spec 可写的 detail.* 透传块,旧键保留为向后兼容回退: - detail.stageField: string 指定 / false 关闭(收编 #2065) - detail.highlightFields: 支持 string 或 {name} 条目 - detail.sections / detail.sectionGroups - detail.useFieldGroups: false 可关闭 fieldGroups 推导 行为变化: 已声明 fieldGroups 的对象,详情页从自动两段式变为按分组 渲染;可用 detail.useFieldGroups: false 退回旧行为。 抽屉(RecordDetailDrawer)分组渲染留待后续 PR,见 #2148
1 parent 1499512 commit 1d5e6e9

4 files changed

Lines changed: 493 additions & 40 deletions

File tree

packages/app-shell/src/views/RecordDetailView.tsx

Lines changed: 89 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
1010
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';
1212
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
1313
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
1414
import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider, useGlobalUndo } from '@object-ui/react';
@@ -1225,8 +1225,14 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
12251225
(key) => key === 'name' || key === 'title'
12261226
);
12271227

1228-
// Build sections: prefer form sections from objectDef, fallback to flat field list
1229-
const formSections = objectDef.views?.form?.sections;
1228+
// Build sections, in priority order:
1229+
// 1) explicit sections — the spec-writable `detail.sections` block,
1230+
// else legacy `views.form.sections` (the spec's ObjectSchema has no
1231+
// `views` key, so that source only exists on non-spec metadata);
1232+
// 2) sections derived from the designer's `fieldGroups` metadata
1233+
// (see the fieldGroups branch in the fallback below);
1234+
// 3) auto-grouping (primary + collapsible "More details").
1235+
const formSections = (objectDef as any).detail?.sections ?? objectDef.views?.form?.sections;
12301236
const sections = formSections && formSections.length > 0
12311237
? formSections.map((sec: any) => ({
12321238
title: sec.name ? sectionLabel(objectDef.name, sec.name, sec.title || sec.name) : sec.title,
@@ -1262,14 +1268,6 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
12621268
}),
12631269
}))
12641270
: (() => {
1265-
// Auto-grouping (platform B): when no form sections are authored,
1266-
// split fields into a primary section and a collapsible
1267-
// "More details" section so long-form/secondary fields don't
1268-
// dilute the main grid. The primary section stays untitled so
1269-
// DetailSection still flattens its chrome when alone.
1270-
const allFields = Object.keys(objectDef.fields || {})
1271-
.filter((key) => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden);
1272-
12731271
const toField = (key: string) => {
12741272
const fieldDef = objectDef.fields[key];
12751273
const refTarget = fieldDef.reference_to || fieldDef.reference;
@@ -1284,35 +1282,70 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
12841282
};
12851283
};
12861284

1287-
const primaryKeys = allFields.filter((k) => !isSecondaryField(k, objectDef.fields[k]));
1288-
const secondaryKeys = allFields.filter((k) => isSecondaryField(k, objectDef.fields[k]));
1285+
// Auto-grouping (platform B): split fields into a primary section
1286+
// and a collapsible "More details" section so long-form/secondary
1287+
// fields don't dilute the main grid. The primary section stays
1288+
// untitled so DetailSection still flattens its chrome when alone.
1289+
// Shared by the pure-fallback path and the ungrouped remainder of
1290+
// the fieldGroups path below.
1291+
const splitPrimarySecondary = (keys: string[]) => {
1292+
const primaryKeys = keys.filter((k) => !isSecondaryField(k, objectDef.fields[k]));
1293+
const secondaryKeys = keys.filter((k) => isSecondaryField(k, objectDef.fields[k]));
1294+
1295+
// Keep the legacy single-untitled-section behaviour when the
1296+
// split would leave one side empty.
1297+
if (secondaryKeys.length === 0 || primaryKeys.length === 0) {
1298+
return [
1299+
{
1300+
showBorder: false as const,
1301+
fields: keys.map(toField),
1302+
},
1303+
];
1304+
}
12891305

1290-
// Below ~6 primary fields the second section often looks awkward
1291-
// — keep the legacy single-untitled-section behaviour. Also
1292-
// honour the "no secondary fields" case the same way.
1293-
if (secondaryKeys.length === 0 || primaryKeys.length === 0) {
12941306
return [
12951307
{
12961308
showBorder: false as const,
1297-
fields: allFields.map(toField),
1309+
fields: primaryKeys.map(toField),
1310+
},
1311+
{
1312+
name: 'details',
1313+
title: sectionLabel(objectDef.name, 'details', t('detail.sectionMoreDetails', 'More details')),
1314+
collapsible: true,
1315+
defaultCollapsed: false,
1316+
showBorder: true as const,
1317+
fields: secondaryKeys.map(toField),
12981318
},
12991319
];
1320+
};
1321+
1322+
// 2) fieldGroups-derived sections (object-designer metadata,
1323+
// same source the runtime form honours). Declared groups
1324+
// render as titled cards in declared order; the helper's
1325+
// trailing untitled bucket (ungrouped fields) still goes
1326+
// through the primary/"More details" split so long-form
1327+
// fields stay tucked away. Objects can opt out via
1328+
// `detail.useFieldGroups: false`.
1329+
const grouped = deriveFieldGroupDetailSections(objectDef as any);
1330+
if (grouped) {
1331+
return grouped.flatMap((sec: any) => {
1332+
if (!sec.name) {
1333+
return splitPrimarySecondary(
1334+
(sec.fields as any[]).map((f: any) => f.name),
1335+
);
1336+
}
1337+
return [{
1338+
...sec,
1339+
title: sectionLabel(objectDef.name, sec.name, sec.title),
1340+
showBorder: true as const,
1341+
}];
1342+
});
13001343
}
13011344

1302-
return [
1303-
{
1304-
showBorder: false as const,
1305-
fields: primaryKeys.map(toField),
1306-
},
1307-
{
1308-
name: 'details',
1309-
title: sectionLabel(objectDef.name, 'details', t('detail.sectionMoreDetails', 'More details')),
1310-
collapsible: true,
1311-
defaultCollapsed: false,
1312-
showBorder: true as const,
1313-
fields: secondaryKeys.map(toField),
1314-
},
1315-
];
1345+
// 3) Pure auto-grouping fallback.
1346+
const allFields = Object.keys(objectDef.fields || {})
1347+
.filter((key) => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden);
1348+
return splitPrimarySecondary(allFields);
13161349
})();
13171350

13181351
// Audit fields (created_at/created_by/updated_at/updated_by) are NOT
@@ -1385,12 +1418,33 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
13851418
return base;
13861419
})();
13871420

1388-
// Build highlightFields: exclusively from objectDef metadata (no hardcoded fallback)
1389-
const highlightFields: HighlightField[] = objectDef.views?.detail?.highlightFields ?? [];
1421+
// Build highlightFields: exclusively from objectDef metadata (no
1422+
// hardcoded fallback). The spec-writable `detail.highlightFields`
1423+
// wins; `views.detail.highlightFields` stays as back-compat for
1424+
// non-spec metadata (the spec's ObjectSchema has no `views` key).
1425+
// Entries may be bare field names — normalize them to the
1426+
// HighlightField shape by resolving label/type from the field def.
1427+
const rawHighlightFields =
1428+
(objectDef as any).detail?.highlightFields ?? objectDef.views?.detail?.highlightFields ?? [];
1429+
const highlightFields: HighlightField[] = (Array.isArray(rawHighlightFields) ? rawHighlightFields : [])
1430+
.map((f: any): HighlightField | null => {
1431+
const name = typeof f === 'string' ? f : f?.name;
1432+
if (!name) return null;
1433+
if (typeof f === 'object' && f.label) return f as HighlightField;
1434+
const fieldDef = objectDef.fields?.[name];
1435+
return {
1436+
name,
1437+
label: fieldDef?.label || name,
1438+
...(fieldDef?.type ? { type: fieldDef.type } : {}),
1439+
};
1440+
})
1441+
.filter((f): f is HighlightField => !!f);
13901442

13911443
// Build sectionGroups from objectDef detail/form config if available
13921444
const sectionGroups: SectionGroup[] | undefined =
1393-
objectDef.views?.detail?.sectionGroups ?? objectDef.views?.form?.sectionGroups;
1445+
(objectDef as any).detail?.sectionGroups
1446+
?? objectDef.views?.detail?.sectionGroups
1447+
?? objectDef.views?.form?.sectionGroups;
13941448

13951449
// Build related entries from reverse-reference child objects.
13961450
// `referenceField` is the FK field on the child pointing back to this

packages/plugin-detail/src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ export {
110110
detectStatusField,
111111
deriveStages,
112112
deriveHighlightFields,
113+
deriveFieldGroupDetailSections,
114+
resolveDetailSections,
113115
} from './synth/buildDefaultPageSchema';
114116
export type {
115117
ObjectDefLike,

0 commit comments

Comments
 (0)