Commit f99189d9 authored by drigle's avatar drigle

fix: 完善引用选择与表格列分组显示

parent b08715fd
{
"name": "integrity_practice_management_new",
"version": "1.0.11",
"version": "1.0.13",
"build": "abc",
"description": "廉洁从业管理",
"date": "2025-07-14 19:27:20",
......
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import { ChevronDown, Close, Search } from '@/components/AppIcons';
import useFeedbackState from '@/hooks/useFeedbackState';
import { queryRecords } from '@/services/DataService';
import { Label } from '@heroui/react';
import { Button, Label, ListBox, Popover, SearchField, Spinner } from '@heroui/react';
import { useEffect, useMemo, useRef, useState } from 'react';
const DEFAULT_ID_LOOKUP_CHUNK_SIZE = 50;
......@@ -541,26 +541,16 @@ export default function ReferenceSearchSelect({
.join('、')} 等 ${selectedOptions.length} 项`
: selectedOptions[0]?.label || placeholder;
const selectionProps = multiple
? {
selectedKeys: new Set(selectedIds),
onSelectionChange: (keys) => {
const nextIds =
keys === 'all' ? menuOptions.map((item) => item.id) : [...keys];
commitChange(nextIds);
},
}
: {
selectedKey: selectedIds[0] || null,
onSelectionChange: (key) => {
const nextKey = key ? String(key) : '';
commitChange(nextKey ? [String(nextKey)] : []);
setActive(false);
},
};
const handleSelectionChange = (keys) => {
const nextIds = keys === 'all' ? menuOptions.map((item) => item.id) : Array.from(keys || []).map(String);
commitChange(nextIds);
if (!multiple) handleOpenChange(false);
};
const handleOpenChange = (isOpen) => {
if (disabled) return;
setActive(isOpen);
if (isOpen) setLoading(true);
if (!isOpen) {
setQuery('');
setError('');
......@@ -568,42 +558,62 @@ export default function ReferenceSearchSelect({
};
return (
<XinyuanAutocomplete
{...selectionProps}
aria-label={placeholder}
emptyText="无匹配结果"
errorText={error}
inputValue={query}
isDisabled={disabled}
isClearable={allowClear}
isLoading={loading}
isOpen={active}
options={menuOptions}
placement={optionsAnchor || 'bottom start'}
placeholder={placeholder}
searchPlaceholder={placeholder}
selectionMode={multiple ? 'multiple' : 'single'}
shouldFilter={false}
valueText={displayText}
variant={variant}
onInputChange={setQuery}
onClear={clearSelection}
onOpenChange={handleOpenChange}
renderOption={(option) => (
<div className="min-w-0 flex-1 overflow-hidden">
<Label className="block truncate" title={option.label}>
{option.label}
</Label>
{option.description ? (
<div
className="truncate text-xs text-muted"
title={option.description}
<Popover.Root isOpen={active} onOpenChange={handleOpenChange}>
<Button
aria-label={placeholder}
className="rc-reference-select-trigger"
fullWidth
isDisabled={disabled}
variant={variant || 'secondary'}
>
<span className="rc-reference-select-trigger__value" title={selectedOptions.map((option) => option.label).join('、') || placeholder}>
{displayText}
</span>
<span className="rc-reference-select-trigger__indicator"><ChevronDown size={16} /></span>
</Button>
<Popover.Content className="rc-reference-select-popover" placement={optionsAnchor || 'bottom start'}>
<Popover.Dialog aria-label={placeholder} className="min-w-0 p-0">
<div className="flex items-center justify-between gap-3 px-3 pb-2 pt-3">
<span className="text-sm text-muted">已选择 {selectedIds.length} 项</span>
{allowClear ? <Button size="sm" variant="tertiary" onPress={clearSelection}>清空</Button> : null}
</div>
<div className="px-2 pb-2">
<SearchField aria-label="搜索引用记录" fullWidth value={query} onChange={setQuery} variant={variant || 'secondary'}>
<SearchField.Group>
<SearchField.SearchIcon><Search size={16} /></SearchField.SearchIcon>
<SearchField.Input autoFocus placeholder={placeholder} />
<SearchField.ClearButton aria-label="清空搜索"><Close size={14} /></SearchField.ClearButton>
</SearchField.Group>
</SearchField>
</div>
{loading ? (
<div className="grid min-h-24 place-items-center" role="status"><Spinner size="sm" aria-label="搜索中" /></div>
) : error ? (
<div className="px-3 py-4 text-sm text-danger" role="alert">{error}</div>
) : menuOptions.length === 0 ? (
<div className="px-3 py-4 text-center text-sm text-muted" role="status">无匹配结果</div>
) : (
<ListBox
aria-label={placeholder}
className="rc-reference-select-options"
items={menuOptions}
selectedKeys={new Set(selectedIds)}
selectionMode={multiple ? 'multiple' : 'single'}
onSelectionChange={handleSelectionChange}
>
{option.description}
</div>
) : null}
</div>
)}
/>
{(option) => (
<ListBox.Item className="rc-reference-select-option" id={option.id} textValue={option.label}>
<div className="min-w-0 flex-1 overflow-hidden">
<Label className="block truncate" title={option.label}>{option.label}</Label>
{option.description ? <div className="truncate text-xs text-muted" title={option.description}>{option.description}</div> : null}
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</ListBox>
)}
</Popover.Dialog>
</Popover.Content>
</Popover.Root>
);
}
......@@ -10,10 +10,174 @@ import { formatFieldValue, getFieldValue } from '@/utils/subjectView';
import { ActionBar } from '@heroui-pro/react/action-bar';
import { DataGrid } from '@heroui-pro/react/data-grid';
import { EmptyState } from '@heroui-pro/react/empty-state';
import { Alert, Button, Disclosure, Dropdown, Input, Label, SearchField, Spinner, Surface, Tabs, Tooltip, buttonVariants } from '@heroui/react';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { Alert, Button, Disclosure, Dropdown, Input, Label, SearchField, Spinner, Surface, Tabs, ToggleButton, Tooltip, buttonVariants } from '@heroui/react';
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
function getPinnedSide(column) {
const side = column?.lockPosition || column?.pinned;
return side === 'left' || side === 'right' ? side : null;
}
function buildColumnGroupMeta(columnGroups = [], columns = []) {
const fieldLookup = columns.reduce((lookup, column) => {
const field = String(column?.field || '').trim();
if (!field) {
return lookup;
}
lookup.set(field, field);
lookup.set(field.replace(/^metadata\./, ''), field);
return lookup;
}, new Map());
const normalizedGroups = (Array.isArray(columnGroups) ? columnGroups : [])
.map((group, index) => {
const id = group?.id || group?.name || `group-${index}`;
const configuredChildren = group?.children || group?.fields || group?.columns;
const children = Array.from(
new Set(
(Array.isArray(configuredChildren) ? configuredChildren : [])
.map((child) => {
const configuredField =
typeof child === 'string'
? child
: child?.field || child?.name || child?.key || '';
const normalizedField = String(configuredField).trim();
return (
fieldLookup.get(normalizedField) ||
fieldLookup.get(normalizedField.replace(/^metadata\./, '')) ||
''
);
})
.filter(Boolean),
),
);
return {
...group,
id,
name:
String(group?.label || '').trim() ||
String(group?.title || '').trim() ||
String(group?.name || '').trim() ||
id,
children,
columnLabels: children.map((field) => {
const column = columns.find((item) => item.field === field);
return String(column?.label || field).trim();
}),
};
})
.filter((group) => group.children.length > 0);
const fieldToGroup = normalizedGroups.reduce((mapping, group) => {
group.children.forEach((field) => {
if (!mapping[field]) {
mapping[field] = group.id;
}
});
return mapping;
}, {});
const groupById = normalizedGroups.reduce((mapping, group) => {
mapping[group.id] = group;
return mapping;
}, {});
return {
groups: normalizedGroups,
fieldToGroup,
groupById,
};
}
function orderColumnsByGroups(columns = [], fieldToGroup = {}, groupById = {}) {
const columnsByField = new Map(columns.map((column) => [column.field, column]));
const emittedFields = new Set();
const orderedColumns = [];
columns.forEach((column) => {
if (emittedFields.has(column.field)) {
return;
}
const groupId = fieldToGroup[column.field];
const group = groupId ? groupById[groupId] : null;
if (!group) {
emittedFields.add(column.field);
orderedColumns.push(column);
return;
}
const pinnedSide = getPinnedSide(column) || 'center';
group.children.forEach((field) => {
const groupedColumn = columnsByField.get(field);
if (!groupedColumn || emittedFields.has(field) || fieldToGroup[field] !== groupId) {
return;
}
if ((getPinnedSide(groupedColumn) || 'center') !== pinnedSide) {
return;
}
emittedFields.add(field);
orderedColumns.push(groupedColumn);
});
});
return orderedColumns;
}
function ColumnGroupControl({ group, collapsed, onPreviewChange, onToggle }) {
const actionLabel = `${collapsed ? '显示' : '隐藏'}${group.name}列`;
const columnSummary = group.columnLabels.join('、');
return (
<Tooltip closeDelay={80} delay={150}>
<ToggleButton
size="sm"
isSelected={collapsed}
onChange={onToggle}
onBlur={() => onPreviewChange?.(null)}
onFocus={() => onPreviewChange?.(group.id)}
onHoverEnd={() => onPreviewChange?.(null)}
onHoverStart={() => onPreviewChange?.(group.id)}
aria-label={actionLabel}
>
<span>{group.name}</span>
<span aria-hidden="true" className="text-xs opacity-70">
{group.children.length} 列
</span>
</ToggleButton>
<Tooltip.Content className="max-w-80" placement="bottom">
<div className="space-y-1">
<div className="font-medium">{actionLabel}</div>
<div className="text-xs opacity-80">包含:{columnSummary}</div>
</div>
</Tooltip.Content>
</Tooltip>
);
}
function ColumnHeaderLabel({ label, groupName }) {
if (!groupName) {
return label;
}
return (
<span className="flex min-w-0 flex-col items-start gap-0.5 py-0.5">
<span aria-hidden="true" className="text-[11px] font-normal leading-4 text-muted">
{groupName}
</span>
<span className="text-xs font-medium leading-4 text-foreground">
{label}
</span>
</span>
);
}
const EMPTY_ARRAY = [];
const EMPTY_GROUPS = {};
const ACTION_ICON_CLASS = '!grid size-4 shrink-0 place-items-center !leading-none [&>svg]:!m-0 [&>svg]:block [&>svg]:!size-4';
function getRecordId(record) {
......@@ -180,6 +344,8 @@ export default function RecordWorkbench({
title,
listTitle,
columns = EMPTY_ARRAY,
columnGroups = EMPTY_ARRAY,
columnGroupScope,
records = EMPTY_ARRAY,
selectedIds = EMPTY_ARRAY,
onSelectionChange,
......@@ -195,6 +361,36 @@ export default function RecordWorkbench({
const [detailMode, setDetailMode] = useState('view');
const [hasHorizontalOverflow, setHasHorizontalOverflow] = useState(false);
const gridWrapRef = useRef(null);
const [groupVisibility, setGroupVisibility] = useState({ scope: null, collapsed: {} });
const [previewColumnGroupId, setPreviewColumnGroupId] = useState(null);
const [columnGroupAnnouncement, setColumnGroupAnnouncement] = useState('');
const pendingScrollRef = useRef(null);
const groupScope = columnGroupScope || activeViewName || title;
const collapsedColumnGroups = groupVisibility.scope === groupScope ? groupVisibility.collapsed : EMPTY_GROUPS;
useEffect(() => {
setGroupVisibility({ scope: groupScope, collapsed: {} });
setPreviewColumnGroupId(null);
setColumnGroupAnnouncement('');
}, [groupScope]);
const columnGroupMeta = useMemo(() => buildColumnGroupMeta(columnGroups, columns), [columnGroups, columns]);
const visibleColumns = useMemo(() => orderColumnsByGroups(
columns.filter((column) => !collapsedColumnGroups[columnGroupMeta.fieldToGroup[column.field]]),
columnGroupMeta.fieldToGroup,
columnGroupMeta.groupById,
), [columns, columnGroupMeta, collapsedColumnGroups]);
const toggleColumnGroup = (groupId) => {
const scrollNode = gridWrapRef.current?.querySelector('.table__resizable-container, .data-table-scroll');
if (scrollNode) pendingScrollRef.current = scrollNode.scrollLeft;
const willShow = Boolean(collapsedColumnGroups[groupId]);
setGroupVisibility({ scope: groupScope, collapsed: { ...collapsedColumnGroups, [groupId]: !willShow } });
const group = columnGroupMeta.groupById[groupId];
setColumnGroupAnnouncement(`${group.name}${willShow ? '已显示' : '已隐藏'},${group.children.length} 列`);
};
useLayoutEffect(() => {
const scrollNode = gridWrapRef.current?.querySelector('.table__resizable-container, .data-table-scroll');
if (scrollNode && pendingScrollRef.current !== null) scrollNode.scrollLeft = pendingScrollRef.current;
pendingScrollRef.current = null;
}, [groupVisibility]);
const selectedRecords = useMemo(() => records.filter((record) => selectedIds.includes(getRecordId(record))), [records, selectedIds]);
const openDetail = (record) => {
const recordName = detailPanel?.getRecordName?.(record) || record?.name || getRecordId(record);
......@@ -218,12 +414,12 @@ export default function RecordWorkbench({
);
const hasDetailView = Boolean(detailPanel?.render);
const totalRowActionCount = rowActions.length + (hasDetailView ? 1 : 0);
const columnWidths = useMemo(() => columns.map((column, index) => {
const columnWidths = useMemo(() => visibleColumns.map((column, index) => {
const configuredWidth = Number(column.width || 180);
return index === 0 && hasDetailView
? Math.max(Math.min(Math.max(configuredWidth, 220), 320), estimatePrimaryColumnWidth(records, column))
: Math.min(Math.max(configuredWidth, 150), 360);
}), [columns, hasDetailView, records]);
}), [visibleColumns, hasDetailView, records]);
const actionColumnWidth = resolveActionColumnWidth(totalRowActionCount);
const gridMinimumWidth = columnWidths.reduce((total, width) => total + width, 0) + actionColumnWidth + (onSelectionChange ? 44 : 0);
......@@ -238,11 +434,12 @@ export default function RecordWorkbench({
}, [gridMinimumWidth]);
const gridColumns = useMemo(() => {
const mapped = columns.map((column, index) => {
const mapped = visibleColumns.map((column, index) => {
const width = columnWidths[index];
return {
id: String(column.field),
header: column.label,
header: <ColumnHeaderLabel label={column.label} groupName={columnGroupMeta.groupById[columnGroupMeta.fieldToGroup[column.field]]?.name} />,
headerClassName: columnGroupMeta.fieldToGroup[column.field] === previewColumnGroupId ? 'bg-accent-soft text-accent-soft-foreground' : undefined,
isRowHeader: index === 0,
allowsSorting: true,
sortFn: (left, right) => {
......@@ -304,7 +501,7 @@ export default function RecordWorkbench({
});
}
return mapped;
}, [actionColumnWidth, baseContext, columnWidths, columns, hasDetailView, hasHorizontalOverflow, renderCell, rowActions, totalRowActionCount]);
}, [actionColumnWidth, baseContext, columnWidths, visibleColumns, columnGroupMeta, previewColumnGroupId, hasDetailView, hasHorizontalOverflow, renderCell, rowActions, totalRowActionCount]);
const handleSelectionChange = (selection) => {
if (selection === 'all') onSelectionChange?.(records.map(getRecordId));
......@@ -469,6 +666,15 @@ export default function RecordWorkbench({
) : null}
</header>
) : null}
{!accessDenied && columnGroupMeta.groups.length > 0 ? (
<div className="flex flex-wrap items-center gap-2" aria-label="列分组显示控制">
{columnGroupMeta.groups.map((group) => (
<ColumnGroupControl key={group.id} group={group} collapsed={Boolean(collapsedColumnGroups[group.id])}
onPreviewChange={setPreviewColumnGroupId} onToggle={() => toggleColumnGroup(group.id)} />
))}
<span className="sr-only" aria-live="polite" aria-atomic="true">{columnGroupAnnouncement}</span>
</div>
) : null}
{states.warning ? <Alert status="warning"><Alert.Content><Alert.Description>{states.warning}</Alert.Description></Alert.Content></Alert> : null}
<div className="rc-list-data-stack rc-data-table-sticky relative min-w-0" ref={gridWrapRef}>
{!states.loading && states.error ? (
......
......@@ -1716,3 +1716,48 @@
outline: 2px solid var(--rc-sidebar-focus);
outline-offset: 2px;
}
.rc-reference-select-trigger {
display: flex;
justify-content: space-between;
width: 100%;
min-width: 0;
height: 40px;
padding: 0 12px;
border: 1px solid var(--rc-field-border, var(--rc-border-default));
border-radius: 8px;
background: var(--rc-field-background);
color: var(--rc-text-primary);
font-weight: 400;
}
.rc-reference-select-trigger__value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rc-reference-select-trigger__indicator {
display: inline-grid;
place-items: center;
flex: 0 0 16px;
color: var(--rc-text-muted);
}
.rc-reference-select-popover {
width: var(--trigger-width);
max-width: min(24rem, calc(100vw - 24px));
border: 1px solid var(--rc-border-default);
border-radius: 12px;
background: var(--rc-bg-surface);
}
.rc-reference-select-options {
max-height: 288px;
overflow-y: auto;
}
.rc-reference-select-option[data-selected] {
background: var(--rc-brand-primary-soft, var(--accent-soft));
color: var(--rc-brand-primary);
}
......@@ -10,6 +10,7 @@ import { useLocation } from 'umi';
import UserAssignmentDialog from './UserAssignmentDialog';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import FormDatePicker from '@/components/FormDatePicker';
import ReferenceSearchSelect from '@/components/ReferenceSearchSelect';
import {
getSubjectNameFromPath,
INTEGRITY_PRACTICE_SUBJECT_CONFIG,
......@@ -27,7 +28,7 @@ import {
buildIntegrityCreatePayload,
} from './integrityPracticeWorkbenchService';
import { createRecord } from '@/services/DataService';
import { getViewRestrictionDefaults, getSelectOptions, emptyValueForField, toArray } from '@/utils/subjectView';
import { getViewRestrictionDefaults, getSelectOptions, getReferenceSubjectName, isReferenceFieldType, emptyValueForField, toArray } from '@/utils/subjectView';
const EMPTY_ARRAY = [];
const MUTATING_ACTION_PATTERN = /delete|archive|create|push/i;
......@@ -347,6 +348,8 @@ export default function SubjectWorkbench() {
: '字段、列顺序和视图均来自当前 Subject 配置。'
}
columns={columns}
columnGroups={activeView?.settings?.groups || EMPTY_ARRAY}
columnGroupScope={`${subjectName}:${activeView?.name || ''}`}
records={records}
selectedIds={selectedIds}
onSelectionChange={hasAccessibleView ? setSelectedIds : undefined}
......@@ -446,7 +449,18 @@ export default function SubjectWorkbench() {
key={field.field}
>
<Label>{fieldLabel}</Label>
{options.length ? (
{isReferenceFieldType(fieldType) ? (
<ReferenceSearchSelect
subject={getReferenceSubjectName(field.fieldDef)}
value={value}
multiple={isMultiple}
disabled={isFieldDisabled || actionLoading}
placeholder={`搜索并选择${fieldLabel}`}
optionsAnchor="bottom start"
selectedSummaryMode={isMultiple ? 'count' : 'labels'}
onChange={(nextValue) => setCreateValues((current) => ({ ...current, [name]: nextValue }))}
/>
) : options.length ? (
<XinyuanAutocomplete
aria-label={fieldLabel}
isClearable
......
......@@ -33,6 +33,7 @@
9. 当前 Detail 有有效 `settings.auth` 或 `settings.disableColumns` 时,字段权限整套使用 Detail;两项都未配置或没有实际 Detail 时,整体回退打开详情的当前 Grid。两套权限不合并、不逐字段回退;`disableColumns` 对管理员也始终只读,普通用户在选定权限来源中缺少字段 `auth` 时只读。切换 Detail 后立即重算字段状态和编辑入口,编辑入口只检查当前 Detail 展示字段。Grid `viewAuth` 只控制 Tab 可见性,不隐藏详情字段。
10. 编辑态远程引用字段通过 HeroUI 弹层搜索选择。多值字段在触发器中显示已选数量,可在弹层内连续选择、取消或清空;单值字段选中后关闭弹层。候选标题过长时单行省略但保留全文提示,写入值仍严格为 `_id` 或 `_id[]`,不改变保存时机和 `metadata` 协议。
11. 编辑态的单选和多选属性使用 HeroUI Select/Autocomplete,日期使用 DatePicker,附件使用 HeroUI Pro DropZone;选择后立即更新当前弹窗草稿,最终仍由弹窗底部统一“保存”提交。
12. 自查底稿填写表的“模板”和“项目结果”等引用字段使用独立受控的 HeroUI Popover + ListBox,点击即打开,候选尚未加载或当前值为空也可展开。打开后显示加载状态并查询目标对象,支持远程搜索、单选/多选与清空;失败时显示错误并可重新搜索或关闭后重开。修改只更新弹窗草稿,取消不写入,保存仍通过 `PUT /record/:name` 提交引用 `_id` / `_id[]`。
异常处理:
......@@ -40,8 +41,18 @@
- 没有可访问 `grid` 视图时不创建兜底 View、不查询记录,并隐藏新增、批量和行操作;只有一个可访问 View 时隐藏 Tabs 轨道。
- 配置动作执行期间禁用重复点击;成功后使用顶部 Toast,失败时保留当前列表并显示服务端错误。
### 2.0 字段分组表头与显示控制
- 各业务列表读取当前 Grid View 的 `settings.groups`;兼容 `children` / `fields` / `columns` 字段列表及带或不带 `metadata.` 的字段路径,仅显示当前视图中有效的列,空分组忽略。
- 同组字段按配置顺序相邻展示,并保留固定列区域。表头采用与廉洁风险防控一致的上下两行:上方为弱化组名,下方为字段名称;未分组字段保留普通表头。
- 表格上方提供分组开关和列数,默认全部展开。点击可隐藏整组字段,再次点击恢复;悬停或键盘聚焦显示字段清单,并突出对应表头。隐藏后保留分组开关,可随时恢复。
- 分组切换只改变当前表格展示,保留勾选、筛选、分页和记录数据,并按可见列宽重新计算横向滚动与固定列;切换 Subject 或视图后恢复默认展开。分组字段全部隐藏时保留选择及操作列。
- 本交互不新增请求、不修改 Subject 配置,也不改变 `metadata`、审批或导出协议。
### 2.1 新建记录与 View 限制
- 自查底稿模板列表点击“新增记录”后,“填写项目”(`entry_item`)按字段配置远程查询 `self_check_the_working_draft`,使用共享引用选择器搜索、多选、取消选择或清空,草稿和最终 `metadata.entry_item` 只保存 `_id[]`。其它引用字段同样读取目标 Subject 和 `multiple` 配置,单值保存 `_id`,选择后关闭弹层;多值可连续选择。
- 引用候选查询通过 `POST /record/query`,搜索期间显示加载状态,无结果或失败时显示对应提示,可重新搜索。选择仅更新新建草稿,点击弹窗保存才调用创建接口;视图限制字段及提交期间禁用选择器。
- 新建记录默认继承当前 Grid View 的 `settings.restrictions`。`$eq` 或单值 `$in` 条件会将唯一值写入 `metadata`,对应控件禁用;即使字段未列入弹窗,也会随请求写入。
- 多值集合、否定条件或同一字段互相冲突时无法确定唯一值,禁止创建并提示“请先调整视图配置”。
- 新建请求使用 `POST /record`,所有业务字段放在 `metadata`;编辑已有记录只提交用户修改字段,不使用 Grid View 限制覆盖原值。
......@@ -50,6 +61,8 @@
入口为 streams-admin 的应用布局配置或 Subject 的 Grid 视图配置,作用于本模块七个业务列表的共享 RecordWorkbench 表格中的引用、选择、子对象及 form 字段。页面加载应用及视图配置后按列渲染,切换视图时使用该视图的配置。
- 视图中配置的 `metadata.*` 业务列必须存在于当前 Subject 字段定义中;残留的无效列不展示,也不作为默认筛选项。Record 根级标题、创建时间等系统列保留原规则。“附件1:廉洁从业自查底稿”对应 `self_inspection_submission_form`,不包含 `archive_status`,因此不显示“归档状态”列;自查底稿填写表中真实定义的归档状态字段继续显示。
- 应用默认值读取 `layouts.default.subjects.<subjectName>.gridRecordConfig.displayConfig`,兼容 streams-admin 的 `displayConfig.maxTagCount`。
- 视图默认值读取 `settings.displayConfig`,同名属性覆盖应用默认值;`settings.columns[]` 中的单列 `displayConfig` 再覆盖视图默认值,列上的 `displayMode`、`maxTagCount`、`titleField` 优先级最高。
- `displayMode: "labels"` 显示已选选项或关联标题;选项文案来自字段 `settings.options`,仅展示当前记录实际选中的值。
......
......@@ -286,6 +286,7 @@ export function buildViewColumns(view, subject, { includeHidden = false, display
return configuredColumns
.filter((column) =>
(includeHidden || !column.hide) && (!hideTitleColumn || column.field !== 'title'))
.filter((column) => !column.field?.startsWith('metadata.') || getFieldDefinition(column.field, subject))
.map((column) => {
const fieldDef = getFieldDefinition(column.field, subject);
const label = getFieldLabel(column.field, fieldDef, column.label);
......
......@@ -35,6 +35,21 @@ const { getTableFieldDisplay, getTableFieldDisplayConfig, formatTableFieldValue
const { buildViewColumns } = loadModule('utils/subjectView.js');
const column = (type, config = {}) => ({ field: 'metadata.values', fieldDef: { type }, ...config });
test('view columns omit stale metadata fields while retaining real archive and system fields', () => {
const view = { settings: { columns: [
{ field: 'title' },
{ field: 'metadata.inspection_item' },
{ field: 'metadata.archive_status' },
{ field: 'created_at' },
] } };
const subject = { fields: [{ name: 'inspection_item', type: 'select' }] };
assert.deepEqual(buildViewColumns(view, subject).map(({ field }) => field), [
'title', 'metadata.inspection_item', 'created_at',
]);
subject.fields.push({ name: 'archive_status', type: 'select' });
assert.equal(buildViewColumns(view, subject)[2].field, 'metadata.archive_status');
});
test('count mode counts selected values, including zero, and excludes empty entries', () => {
const config = column('select', { displayMode: 'count' });
assert.equal(formatTableFieldValue([0, '选中', null, undefined, '', ' '], config), '2');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment