Commit 3c5cc2ab authored by drigle's avatar drigle

feat: configure table field count and label display

parent e2c3190a
import { Chip, Tooltip } from '@heroui/react';
import { DataTableText } from './DataTable';
import { getTableFieldDisplay } from '@/utils/tableFieldDisplay';
export default function TableFieldValue({ value, column }) {
const display = getTableFieldDisplay(value, column);
if (display.showCount) {
const count = (
<Chip size="sm" aria-label={`${column.label || '关联条目'}:${display.count} 项`}>
{display.count}
</Chip>
);
return display.labels.length > 0 ? (
<Tooltip>
<Tooltip.Trigger aria-label={`${column.label || '关联条目'}:${display.count} 项,查看标题`}>
{count}
</Tooltip.Trigger>
<Tooltip.Content>
<div className="max-h-64 max-w-80 overflow-auto whitespace-pre-wrap break-words">
{display.labels.join('、')}
</div>
</Tooltip.Content>
</Tooltip>
) : count;
}
return <DataTableText text={display.text || '-'} maxLines={2} showTooltip />;
}
......@@ -24,7 +24,9 @@ import FilterSelect from '@/components/FilterSelect';
import FormDatePicker from '@/components/FormDatePicker';
import ReferenceSearchSelect from '@/pages/RecordDetailDrawer/components/ReferenceSearchSelect';
import UserSearchSelect from '@/components/UserSearchSelect';
import TableFieldValue from '@/components/TableFieldValue';
import { formatFieldValue, getFieldValue, normalizeText } from '@/utils/subjectView';
import { formatTableFieldValue, getTableFieldDisplayConfig } from '@/utils/tableFieldDisplay';
import {
Alert,
Button,
......@@ -292,6 +294,9 @@ function defaultGetRecordLabel(record) {
function defaultRenderCell(record, column) {
const sourceRecord = record?.raw || record;
const value = getFieldValue(sourceRecord, column.field);
if (getTableFieldDisplayConfig(column).isConfigured) {
return <TableFieldValue value={value} column={column} />;
}
const text = formatFieldValue(value, column.fieldDef) || '-';
return (
<DataTableText
......@@ -323,6 +328,10 @@ const SHORT_VALUE_FIELD_TYPES = new Set([
]);
function resolveColumnDataGridPresentation(column, records = []) {
const display = getTableFieldDisplayConfig(column);
if (display.isConfigured && display.displayMode === 'count') {
return { isLongText: false, maxLines: 1, showTooltip: false };
}
const label = String(column?.label || '');
const field = String(column?.field || '').toLowerCase();
const fieldType = String(column?.fieldDef?.type || '').toLowerCase();
......@@ -334,7 +343,7 @@ function resolveColumnDataGridPresentation(column, records = []) {
? records.slice(0, 20).reduce((longest, record) => {
const sourceRecord = record?.raw || record;
const value = getFieldValue(sourceRecord, column.field);
const text = normalizeText(formatFieldValue(value, column.fieldDef));
const text = normalizeText(formatTableFieldValue(value, column));
return Math.max(longest, Array.from(text).length);
}, 0)
: 0;
......@@ -1280,6 +1289,12 @@ export default function RecordWorkbench({
</Alert>
) : null}
{states?.warning ? (
<Alert status="warning">
<Alert.Content><Alert.Description>{states.warning}</Alert.Description></Alert.Content>
</Alert>
) : null}
{states?.success ? (
<Alert status="success">
<Alert.Content><Alert.Description>{states.success}</Alert.Description></Alert.Content>
......
......@@ -11,6 +11,7 @@ import {
sortRecordsByView,
} from '@/utils/subjectView';
import { ARCHIVE_SNAPSHOT_ITEM_FIELDS } from './archiveSnapshotItems.mjs';
import { loadApplicationTableDisplayConfig } from '@/services/tableFieldDisplayService';
export const ISSUE_RECTIFICATION_ARCHIVE_SUBJECT = 'issue_rectification_archive';
export const PROBLEM_RECTIFICATION_ARCHIVE_RECORDS_SUBJECT = 'problem_rectification_archive_records';
......@@ -42,9 +43,10 @@ export function normalizeArchiveBatchRecord(record) {
}
export async function loadArchiveWorkbenchResources() {
const [archiveSubject, archiveViews] = await Promise.all([
const [archiveSubject, archiveViews, tableDisplayConfig] = await Promise.all([
loadSubject(ISSUE_RECTIFICATION_ARCHIVE_SUBJECT),
getSubjectView(ISSUE_RECTIFICATION_ARCHIVE_SUBJECT),
loadApplicationTableDisplayConfig(ISSUE_RECTIFICATION_ARCHIVE_SUBJECT),
]);
const accessibleViews = buildArchiveGridViews(archiveViews || []);
const archiveRecordsResult = accessibleViews.length > 0
......@@ -62,6 +64,7 @@ export async function loadArchiveWorkbenchResources() {
records,
subject: archiveSubject,
views: archiveViews || [],
...tableDisplayConfig,
};
}
......@@ -82,11 +85,11 @@ export function buildArchiveGridViews(views = []) {
return isViewAccessible(fallbackView) ? [fallbackView] : [];
}
export function buildArchiveColumns(view, subject) {
return buildViewColumns(view, subject);
export function buildArchiveColumns(view, subject, displayConfig = {}) {
return buildViewColumns(view, subject, { displayConfig });
}
export function buildArchiveConfiguredView(records, filters, gridView, subject) {
export function buildArchiveConfiguredView(records, filters, gridView, subject, displayConfig = {}) {
if (!gridView) {
return {
filteredRecords: [],
......@@ -95,7 +98,7 @@ export function buildArchiveConfiguredView(records, filters, gridView, subject)
}
const keyword = normalizeText(filters?.keyword).toLowerCase();
const columns = buildArchiveColumns(gridView, subject);
const columns = buildArchiveColumns(gridView, subject, displayConfig);
const searchColumns = buildViewSearchColumns(gridView, subject);
const filteredRecords = records.filter((record) => {
const rawRecord = record.raw || record;
......
import RecordWorkbench from '@/components/record-workbench';
import TableFieldValue from '@/components/TableFieldValue';
import { getTableFieldDisplayConfig } from '@/utils/tableFieldDisplay';
import { getFieldValue } from '@/utils/subjectView';
import { Archive, Database } from '@/components/AppIcons';
import RecordDetailDrawer from '@/pages/RecordDetailDrawer';
import { useCallback, useEffect, useMemo, useState } from 'react';
......@@ -17,6 +20,8 @@ export default function ProblemRectificationArchivePage() {
const [records, setRecords] = useState([]);
const [subject, setSubject] = useState(null);
const [views, setViews] = useState([]);
const [tableDisplayConfig, setTableDisplayConfig] = useState({});
const [tableDisplayConfigError, setTableDisplayConfigError] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [filters, setFilters] = useState({
......@@ -36,10 +41,14 @@ export default function ProblemRectificationArchivePage() {
setRecords(resources.records || []);
setSubject(resources.subject || null);
setViews(resources.views || []);
setTableDisplayConfig(resources.displayConfig || {});
setTableDisplayConfigError(resources.displayConfigError || '');
} catch (requestError) {
setRecords([]);
setSubject(null);
setViews([]);
setTableDisplayConfig({});
setTableDisplayConfigError('');
setError(requestError?.message || '归档批次列表加载失败,请稍后重试。');
} finally {
setLoading(false);
......@@ -69,8 +78,8 @@ export default function ProblemRectificationArchivePage() {
);
const configuredView = useMemo(
() => buildArchiveConfiguredView(records, filters, activeGridView, subject),
[records, filters, activeGridView, subject],
() => buildArchiveConfiguredView(records, filters, activeGridView, subject, tableDisplayConfig),
[records, filters, activeGridView, subject, tableDisplayConfig],
);
const filteredRecords = configuredView.filteredRecords || [];
......@@ -185,6 +194,7 @@ export default function ProblemRectificationArchivePage() {
states={{
loading,
error,
warning: tableDisplayConfigError,
loadingTitle: '正在加载归档批次',
loadingDescription: '正在读取归档对象、视图配置和批次记录,请稍候。',
emptyTitle: '暂无归档批次',
......@@ -192,6 +202,9 @@ export default function ProblemRectificationArchivePage() {
}}
renderCell={({ record, column }) => {
if (column.field === 'metadata.reference_records') {
if (getTableFieldDisplayConfig(column).isConfigured) {
return <TableFieldValue column={column} value={getFieldValue(record.raw || record, column.field)} />;
}
return (
<span className="tabular-nums">
{record.archiveCount || 0} 条明细
......
......@@ -17,6 +17,8 @@ import {
} from '@/components/TableActionButton';
import XinyuanModal from '@/components/XinyuanModal';
import RecordWorkbench from '@/components/record-workbench';
import TableFieldValue from '@/components/TableFieldValue';
import { getTableFieldDisplayConfig } from '@/utils/tableFieldDisplay';
import RecordDetailDrawer from '@/pages/RecordDetailDrawer';
import {
batchRemoveRecords,
......@@ -625,6 +627,10 @@ function FieldValuePreview({ column, record }) {
return <div className="font-medium">{record.title || '(无标题)'}</div>;
}
if (getTableFieldDisplayConfig(column).isConfigured) {
return <TableFieldValue value={value} column={column} />;
}
if (fieldName === 'rectification_progress') {
const text = formatFieldValue(value, column.fieldDef) || '未填写';
return <StatusPill text={text} tone={resolveProgressTone(text)} />;
......@@ -788,6 +794,8 @@ export default function ProblemRectificationListPage() {
const [records, setRecords] = useState([]);
const [subject, setSubject] = useState(null);
const [views, setViews] = useState([]);
const [tableDisplayConfig, setTableDisplayConfig] = useState({});
const [tableDisplayConfigError, setTableDisplayConfigError] = useState('');
const [loading, setLoading] = useState(true);
const [recordsLoading, setRecordsLoading] = useState(false);
const [error, setError] = useState('');
......@@ -843,10 +851,14 @@ export default function ProblemRectificationListPage() {
const resources = await loadProblemRectificationListResources();
setSubject(resources.subject || null);
setViews(resources.views || []);
setTableDisplayConfig(resources.displayConfig || {});
setTableDisplayConfigError(resources.displayConfigError || '');
} catch (requestError) {
setRecords([]);
setSubject(null);
setViews([]);
setTableDisplayConfig({});
setTableDisplayConfigError('');
setError(requestError?.message || '问题整改列表加载失败,请稍后重试。');
} finally {
setLoading(false);
......@@ -1087,8 +1099,8 @@ export default function ProblemRectificationListPage() {
}, [reloadList]);
const configuredView = useMemo(
() => buildProblemRectificationConfiguredListView(records, {}, activeGridView, subject),
[records, activeGridView, subject],
() => buildProblemRectificationConfiguredListView(records, {}, activeGridView, subject, tableDisplayConfig),
[records, activeGridView, subject, tableDisplayConfig],
);
const dynamicFilterSchema = useMemo(
() =>
......@@ -2070,6 +2082,7 @@ export default function ProblemRectificationListPage() {
states={{
loading: loading || recordsLoading,
error,
warning: tableDisplayConfigError,
success: '',
loadingTitle: '正在加载问题整改列表',
loadingDescription: '正在读取对象结构、视图配置和记录数据,请稍候。',
......
......@@ -287,6 +287,65 @@
---
### 5.10 表格字段显示配置
入口为 streams-admin 的应用布局配置或 Subject 的 Grid 视图配置,作用于问题清单、归档批次和共享 RecordWorkbench 表格中的引用、选择、子对象及 form 字段。页面加载应用及视图配置后按列渲染,切换视图时使用该视图的配置。
- 应用默认值读取 `layouts.default.subjects.<subjectName>.gridRecordConfig.displayConfig`,兼容 streams-admin 的 `displayConfig.maxTagCount`。
- 视图默认值读取 `settings.displayConfig`,同名属性覆盖应用默认值;`settings.columns[]` 中的单列 `displayConfig` 再覆盖视图默认值,列上的 `displayMode`、`maxTagCount`、`titleField` 优先级最高。
- `displayMode: "labels"` 显示已选选项或关联标题;选项文案来自字段 `settings.options`,仅展示当前记录实际选中的值。
- `displayMode: "count"` 显示有效条目数量,空值显示 `0`,单值显示 `1`。数量可通过鼠标悬停或键盘聚焦查看完整标题;空值不产生提示。计数只基于该字段已有数据,不额外查询关联记录总数。
- `displayMode: "auto"` 在条目数达到 `maxTagCount` 时显示数量,否则显示标题或选项。只配置 `maxTagCount` 时自动启用此模式,判断使用 `>=`,与 streams-admin 一致。显式 `auto` 未配置合法阈值时使用 `3`;阈值为 `0` 时所有非空字段显示数量。
- 子对象标签优先使用 `titleField` 指定的子字段,例如 `responsible_department`;也支持 `metadata` 下的子字段。未配置或值为空时使用已有 `title/name/_id`,仍无标题时展示“第 N 条”,不猜测业务字段。form 对象为 `{form,data}` 时按 `data` 中的条目计数。
- 未配置显示方式或阈值时保留现有单元格行为,包括归档批次的“X 条明细”;非法显示方式或阈值不修改记录。普通文本、数字、日期、布尔、附件和人员字段不受这项集合显示配置影响。
- 应用配置读取失败时显示警告并使用视图配置;列表数据和详情入口仍可使用。重新加载页面会重试读取配置。配置不会改变筛选条件、排序、分页、审批状态、详情编辑、关联 ID 或 Record 的 `metadata`。
应用配置示例(`problem_rectification_new` 应用):
```json
{
"layouts": {
"default": {
"subjects": {
"problem_rectification": {
"gridRecordConfig": {
"displayConfig": { "maxTagCount": 3 }
}
},
"issue_rectification_archive": {
"gridRecordConfig": {
"displayConfig": { "displayMode": "count" }
}
}
}
}
}
}
```
Grid 视图配置示例(列举的是配置片段,修改时应保留完整原列列表):
```json
{
"settings": {
"displayConfig": { "displayMode": "auto", "maxTagCount": 3 },
"columns": [
{ "field": "metadata.responsible_department", "displayMode": "labels" },
{
"field": "metadata.department_rectification_items",
"displayMode": "count",
"titleField": "responsible_department"
},
{
"field": "metadata.problem_rectification_delay_request_items",
"displayMode": "labels",
"titleField": "responsible_department"
}
]
}
}
```
## 6. 流程角色
- 发起人:通常为问题整改列表操作人,负责发起填报和最终审核;可编辑本次推送关联的父问题记录以及全部部门整改明细业务字段
......
......@@ -13,11 +13,12 @@ import {
DEPARTMENT_GROUPS,
} from './problemRectificationDashboardService';
import { calculateProblemSeverityLevel } from './problemSeverity';
import { loadApplicationTableDisplayConfig } from '@/services/tableFieldDisplayService';
import { formatTableFieldValue } from '@/utils/tableFieldDisplay';
import {
buildViewColumns,
checkViewCondition,
emptyValueForField,
formatFieldValue,
getFieldNameFromPath,
getFieldValue,
getSelectOptions,
......@@ -1346,9 +1347,10 @@ export async function loadProblemRectificationListData({
}
export async function loadProblemRectificationListResources() {
const [subject, viewSummaries] = await Promise.all([
const [subject, viewSummaries, tableDisplayConfig] = await Promise.all([
loadSubject(PROBLEM_RECTIFICATION_SUBJECT),
getSubjectView(PROBLEM_RECTIFICATION_SUBJECT),
loadApplicationTableDisplayConfig(PROBLEM_RECTIFICATION_SUBJECT),
]);
const views = await Promise.all(
toArray(viewSummaries).map(async (view) => {
......@@ -1375,6 +1377,7 @@ export async function loadProblemRectificationListResources() {
return {
subject,
views,
...tableDisplayConfig,
};
}
......@@ -1429,8 +1432,8 @@ export function buildGridViews(views = []) {
return isViewAccessible(fallbackView) ? [fallbackView] : [];
}
export function buildViewColumnsForList(view, subject) {
return buildViewColumns(view, subject);
export function buildViewColumnsForList(view, subject, displayConfig = {}) {
return buildViewColumns(view, subject, { displayConfig });
}
export function getRecordCellText(record, column) {
......@@ -1441,10 +1444,10 @@ export function getRecordCellText(record, column) {
return formatDateText(value) || '-';
}
return formatFieldValue(value, column.fieldDef) || '-';
return formatTableFieldValue(value, column) || '-';
}
export function buildProblemRectificationConfiguredListView(records, filters, gridView, subject) {
export function buildProblemRectificationConfiguredListView(records, filters, gridView, subject, displayConfig = {}) {
if (!gridView) {
return {
filteredRecords: [],
......@@ -1460,7 +1463,7 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
};
}
const columns = buildViewColumnsForList(gridView, subject);
const columns = buildViewColumnsForList(gridView, subject, displayConfig);
const filteredRecords = records.filter((record) => {
const rawRecord = record.raw || record;
......
......@@ -44,6 +44,14 @@
- `404` `{"code":404,"message":"record '<name>' not found"}`
- `400` 业务校验失败(缺少必填字段、主键冲突、类型不匹配等)
### 1.2 表格显示配置的关联读取接口
问题清单与归档批次通过已有 `DataService.loadApplication(APPLICATION_NAME)` 调用 `GET /application/:name`,读取应用的 `layouts.default.subjects.<subjectName>.gridRecordConfig.displayConfig`,路径与 streams-admin 的 `useAppLayout().getSubjectConfig()` / `RecordGridView` 一致。
视图继续使用 `GET /subject/:subjectName/view` 与 `GET /subject/:subjectName/view/:viewName`,分别对应 `getSubjectView` / `getSubjectViewDetail`。视图级显示配置位于 `settings.displayConfig`,单列覆盖位于 `settings.columns[]`。支持 `displayMode: labels | count | auto`、`maxTagCount`(非负整数)和子对象列 `titleField`(子字段路径)。
这里只读取配置,不更新应用、视图、Subject 或 Record。应用配置请求失败时仍加载记录,提示配置读取失败并使用视图配置;记录查询和写入协议不变。完整优先级和示例见 `src/pages/problem_rectification/problem-rectification.business-flow.md` 的“5.10 表格字段显示配置”。
## 2. Record 核心模型字段
不同 `subject` 通过 `metadata` 扩展业务字段。
......
import { APPLICATION_NAME } from '@/config/application';
import { loadApplication } from '@/services/DataService';
export async function loadApplicationTableDisplayConfig(subjectName) {
try {
const application = await loadApplication(APPLICATION_NAME);
return {
displayConfig: application?.layouts?.default?.subjects?.[subjectName]?.gridRecordConfig?.displayConfig || {},
displayConfigError: '',
};
} catch (error) {
return {
displayConfig: {},
displayConfigError: `应用表格显示配置读取失败,暂使用视图默认显示:${error?.message || '请稍后重试'}`,
};
}
}
......@@ -290,9 +290,10 @@ export function shouldHideViewTitleColumn(view) {
return view?.settings?.shortTitle === true;
}
export function buildViewColumns(view, subject, { includeHidden = false } = {}) {
export function buildViewColumns(view, subject, { includeHidden = false, displayConfig = {} } = {}) {
const configuredColumns = view?.settings?.columns || [];
const hideTitleColumn = !includeHidden && shouldHideViewTitleColumn(view);
const viewDisplayConfig = { ...displayConfig, ...view?.settings?.displayConfig };
if (configuredColumns.length > 0) {
return configuredColumns
......@@ -305,6 +306,7 @@ export function buildViewColumns(view, subject, { includeHidden = false } = {})
const fieldDef = getFieldDefinition(column.field, subject);
return {
...column,
displayConfig: { ...viewDisplayConfig, ...column.displayConfig },
width: column.width || 180,
label: getConfiguredColumnLabel(column, fieldDef),
fieldDef: fieldDef || { name: column.field, label: column.field, type: 'text' },
......@@ -316,6 +318,7 @@ export function buildViewColumns(view, subject, { includeHidden = false } = {})
if (!hideTitleColumn) {
defaultColumns.push({
field: 'title',
displayConfig: viewDisplayConfig,
width: 180,
label: '标题',
fieldDef: { name: 'title', label: '标题', type: 'text' },
......@@ -325,6 +328,7 @@ export function buildViewColumns(view, subject, { includeHidden = false } = {})
toArray(subject?.fields).forEach((field) => {
defaultColumns.push({
field: `metadata.${field.name}`,
displayConfig: viewDisplayConfig,
width: 180,
label: field.label || field.name,
fieldDef: field,
......
import {
formatFieldValue,
getFieldValue,
getSelectOptions,
isReferenceFieldType,
normalizeRecordLabel,
normalizeText,
toArray,
} from './subjectView';
export const TABLE_FIELD_DISPLAY_MODES = ['labels', 'count', 'auto'];
export const DEFAULT_TABLE_MAX_TAG_COUNT = 3;
export function isTableFieldDisplayConfigurable(column) {
const type = column?.fieldDef?.type;
return isReferenceFieldType(type) || ['select', 'subject', 'form'].includes(type);
}
export function getTableFieldDisplayConfig(column) {
const config = column?.displayConfig || {};
const configuredMode = column?.displayMode ?? config.displayMode;
const configuredThreshold = column?.maxTagCount ?? config.maxTagCount;
const threshold = Number(configuredThreshold);
const hasThreshold = ['number', 'string'].includes(typeof configuredThreshold) &&
normalizeText(configuredThreshold) !== '' && Number.isInteger(threshold) && threshold >= 0;
const hasMode = TABLE_FIELD_DISPLAY_MODES.includes(configuredMode);
return {
isConfigured: isTableFieldDisplayConfigurable(column) && (hasMode || hasThreshold),
displayMode: hasMode ? configuredMode : hasThreshold ? 'auto' : 'labels',
maxTagCount: hasThreshold ? threshold : DEFAULT_TABLE_MAX_TAG_COUNT,
titleField: normalizeText(column?.titleField ?? config.titleField),
};
}
function getDisplayItems(value, fieldDef) {
const items = fieldDef?.type === 'form' && value && !Array.isArray(value) && Array.isArray(value.data)
? value.data
: toArray(value);
return items.filter((item) =>
item !== null && item !== undefined && !(typeof item === 'string' && !item.trim()),
);
}
export function getTableFieldDisplay(value, column) {
const config = getTableFieldDisplayConfig(column);
const items = getDisplayItems(value, column?.fieldDef);
const type = column?.fieldDef?.type;
const options = type === 'select' ? getSelectOptions(column.fieldDef) : [];
const labels = items.map((item, index) => {
if (type === 'select') {
const optionValue = item && typeof item === 'object' ? item.value : item;
const option = options.find((candidate) => String(candidate.value) === String(optionValue));
return normalizeText(option?.label ?? optionValue);
}
if (['subject', 'form'].includes(type)) {
const titleValue = config.titleField
? getFieldValue(item, config.titleField) ?? getFieldValue(item?.metadata, config.titleField)
: null;
return formatFieldValue(titleValue) || normalizeRecordLabel(item) || `第 ${index + 1} 条`;
}
if (type === 'tag') return normalizeText(item?.label) || normalizeRecordLabel(item);
return normalizeRecordLabel(item);
}).filter(Boolean);
const showCount = config.displayMode === 'count' ||
(config.displayMode === 'auto' && items.length > 0 && items.length >= config.maxTagCount);
return {
...config,
count: items.length,
labels,
showCount,
text: showCount ? String(items.length) : labels.join('、'),
};
}
export function formatTableFieldValue(value, column) {
const display = getTableFieldDisplay(value, column);
return display.isConfigured ? display.text : formatFieldValue(value, column?.fieldDef);
}
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import test from 'node:test';
import ts from 'typescript';
const require = createRequire(import.meta.url);
const sourceRoot = new URL('../src/', import.meta.url).pathname;
function loadModule(entry, dataService = {}) {
const modules = new Map();
const load = (path) => {
if (modules.has(path)) return modules.get(path);
const exports = {};
modules.set(path, exports);
const { outputText } = ts.transpileModule(readFileSync(path, 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: true },
});
const localRequire = (specifier) => {
if (specifier === './DataService' || specifier === '@/services/DataService') return dataService;
if (!specifier.startsWith('.') && !specifier.startsWith('@/')) return require(specifier);
const dependencyPath = specifier.startsWith('@/')
? resolve(sourceRoot, specifier.slice(2))
: resolve(dirname(path), specifier);
return load(`${dependencyPath}.js`);
};
new Function('require', 'exports', outputText)(localRequire, exports);
return exports;
};
return load(resolve(sourceRoot, entry));
}
const { getTableFieldDisplay, getTableFieldDisplayConfig, formatTableFieldValue } = loadModule('utils/tableFieldDisplay.js');
const { buildViewColumns } = loadModule('utils/subjectView.js');
const column = (type, config = {}) => ({ field: 'metadata.values', fieldDef: { type }, ...config });
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');
for (const value of [null, undefined, '', []]) assert.equal(formatTableFieldValue(value, config), '0');
assert.equal(formatTableFieldValue(0, config), '1');
assert.equal(formatTableFieldValue({ _id: 'one', title: '关联标题' }, column('reference', { displayMode: 'count' })), '1');
});
test('labels show selected option labels and populated record titles', () => {
const config = column('select', { displayMode: 'labels' });
config.fieldDef.settings = { options: [{ value: 0, label: '零号' }, { value: 'a', label: '选项 A' }, { value: 'b', label: '未选项' }] };
assert.equal(formatTableFieldValue(['0', 'a'], config), '零号、选项 A');
assert.equal(formatTableFieldValue([{ _id: 'id-1', title: '记录 A' }, { _id: 'id-2', title: '记录 B' }], column('reference', { displayMode: 'labels' })), '记录 A、记录 B');
assert.equal(formatTableFieldValue([{ name: 'risk', label: '风险标签' }], column('tag', { displayMode: 'labels' })), '风险标签');
});
test('maxTagCount alone matches streams-admin at the exact threshold', () => {
const config = column('reference', { displayConfig: { maxTagCount: 2 } });
assert.equal(formatTableFieldValue([{ title: 'A' }], config), 'A');
assert.equal(formatTableFieldValue([{ title: 'A' }, { title: 'B' }], config), '2');
assert.equal(formatTableFieldValue([], config), '');
assert.equal(formatTableFieldValue(['A'], column('reference', { displayMode: 'auto', maxTagCount: 0 })), '1');
assert.equal(getTableFieldDisplayConfig(column('reference', { displayMode: 'auto' })).maxTagCount, 3);
});
test('embedded subjects and forms use configured child labels without changing data', () => {
const value = [{ responsible_department: '审计部' }, { metadata: { responsible_department: '风险管理部' } }];
const original = JSON.stringify(value);
const config = column('subject', { displayMode: 'labels', titleField: 'responsible_department' });
assert.equal(formatTableFieldValue(value, config), '审计部、风险管理部');
assert.equal(getTableFieldDisplay(value, { ...config, displayMode: 'count' }).count, 2);
assert.equal(formatTableFieldValue([{}, {}], config), '第 1 条、第 2 条');
assert.equal(formatTableFieldValue({ form: { title: '表单' }, data: value }, column('form', { displayMode: 'count' })), '2');
assert.equal(JSON.stringify(value), original);
});
test('application defaults, view settings, and column overrides keep independent modes', () => {
const subject = { fields: [{ name: 'values', type: 'select' }] };
const view = { settings: {
displayConfig: { maxTagCount: 2 },
columns: [{ field: 'metadata.values', displayMode: 'labels' }],
} };
const original = JSON.stringify(view);
const [config] = buildViewColumns(view, subject, { displayConfig: { displayMode: 'count', maxTagCount: 5 } });
assert.equal(getTableFieldDisplayConfig(config).maxTagCount, 2);
assert.equal(formatTableFieldValue(['A', 'B'], config), 'A、B');
const [nestedConfig] = buildViewColumns({ settings: {
displayConfig: { displayMode: 'count', maxTagCount: 5 },
columns: [{ field: 'metadata.values', displayConfig: { displayMode: 'auto', maxTagCount: 2 } }],
} }, subject);
assert.equal(formatTableFieldValue(['A', 'B'], nestedConfig), '2');
const fallback = buildViewColumns({ settings: {} }, subject, { displayConfig: { displayMode: 'count' } });
assert.equal(formatTableFieldValue(['A', 'B'], fallback.find((item) => item.field === 'metadata.values')), '2');
assert.equal(JSON.stringify(view), original);
});
test('missing or invalid config preserves current behavior and does not affect scalar fields', () => {
const value = [{ responsible_department: '审计部' }];
assert.equal(formatTableFieldValue(value, column('subject')), JSON.stringify(value));
assert.equal(formatTableFieldValue('A', column('select', { displayMode: 'invalid', maxTagCount: -1 })), 'A');
for (const type of ['text', 'number', 'date', 'boolean', 'user', 'file', 'object']) {
assert.equal(getTableFieldDisplayConfig(column(type, { displayMode: 'count' })).isConfigured, false);
}
for (const maxTagCount of ['invalid', -1, 1.5, null, '', ' ', false, true, []]) {
assert.equal(getTableFieldDisplayConfig(column('reference', { displayConfig: { maxTagCount } })).isConfigured, false);
}
});
test('application config is read from the same layout path as streams-admin', async () => {
const requests = [];
const service = loadModule('services/tableFieldDisplayService.js', {
loadApplication: async (name) => {
requests.push(name);
return { layouts: { default: { subjects: { problem_rectification: {
gridRecordConfig: { displayConfig: { maxTagCount: 2 } },
} } } } };
},
});
assert.deepEqual(await service.loadApplicationTableDisplayConfig('problem_rectification'), {
displayConfig: { maxTagCount: 2 }, displayConfigError: '',
});
assert.deepEqual(requests, ['problem_rectification_new']);
assert.deepEqual(await service.loadApplicationTableDisplayConfig('issue_rectification_archive'), {
displayConfig: {}, displayConfigError: '',
});
});
test('unavailable application config returns a warning and usable view fallback', async () => {
const service = loadModule('services/tableFieldDisplayService.js', {
loadApplication: async () => { throw new Error('无权限'); },
});
const result = await service.loadApplicationTableDisplayConfig('problem_rectification');
assert.deepEqual(result.displayConfig, {});
assert.match(result.displayConfigError, /暂使用视图默认显示.*无权限/);
});
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