Commit 6c54c130 authored by David Yang's avatar David Yang

fix: 恢复廉洁风险配置表格交互

parent e446f89a
...@@ -40,6 +40,7 @@ import { ...@@ -40,6 +40,7 @@ import {
} from '@/catalyst-ui-kit/javascript/table'; } from '@/catalyst-ui-kit/javascript/table';
import { Textarea } from '@/catalyst-ui-kit/javascript/textarea'; import { Textarea } from '@/catalyst-ui-kit/javascript/textarea';
import { UserContext } from '@/wrapper/Auth'; import { UserContext } from '@/wrapper/Auth';
import * as Headless from '@headlessui/react';
import { import {
ArchiveBoxArrowDownIcon, ArchiveBoxArrowDownIcon,
ArrowDownTrayIcon, ArrowDownTrayIcon,
...@@ -64,6 +65,7 @@ import dayjs from 'dayjs'; ...@@ -64,6 +65,7 @@ import dayjs from 'dayjs';
import { import {
useContext, useContext,
useEffect, useEffect,
Fragment,
useMemo, useMemo,
useRef, useRef,
useState, useState,
...@@ -77,10 +79,13 @@ import { ...@@ -77,10 +79,13 @@ import {
exportRiskWorkbook, exportRiskWorkbook,
formatCellValue, formatCellValue,
getColumnLabel, getColumnLabel,
getColumnFieldName,
getConfiguredColumns,
getFieldLabel, getFieldLabel,
getFieldName, getFieldName,
getFieldOptions, getFieldOptions,
getFieldType, getFieldType,
getTableHeaderGroups,
importDisplayWorkbook, importDisplayWorkbook,
importMeasureWorkbook, importMeasureWorkbook,
isFieldMultiple, isFieldMultiple,
...@@ -146,8 +151,9 @@ function getRecordTone(value) { ...@@ -146,8 +151,9 @@ function getRecordTone(value) {
return 'zinc'; return 'zinc';
} }
function CellValue({ record, name }) { function CellValue({ record, column }) {
const value = formatCellValue(record, name); const name = getColumnFieldName(column);
const value = formatCellValue(record, column);
const isStatus = [ const isStatus = [
'probability', 'probability',
'hazard_degree', 'hazard_degree',
...@@ -339,18 +345,76 @@ function DynamicField({ field, value, onChange, disabled, error }) { ...@@ -339,18 +345,76 @@ function DynamicField({ field, value, onChange, disabled, error }) {
); );
} }
export function RiskRecordDialog({ open, onClose, config, subject, record, onSaved }) { function getDrawerFieldGroups(fields, view) {
const groups = view?.settings?.groups || view?.groups || [];
if (!Array.isArray(groups) || groups.length === 0) return [];
const groupByField = new Map();
groups.forEach((group) => {
const id = group.id || group.name;
(group.children || []).forEach((child) => {
groupByField.set(String(child).replace(/^metadata\./, ''), {
id,
label: group.name || group.label || id,
});
});
});
const renderedGroups = new Set();
const renderedFields = new Set();
const sections = [];
fields.forEach((field) => {
const name = getFieldName(field);
if (renderedFields.has(name)) return;
const group = groupByField.get(name);
if (!group || renderedGroups.has(group.id)) {
sections.push({ id: name, label: '', fields: [field] });
renderedFields.add(name);
return;
}
const groupFields = fields.filter((item) => groupByField.get(getFieldName(item))?.id === group.id);
groupFields.forEach((item) => renderedFields.add(getFieldName(item)));
renderedGroups.add(group.id);
sections.push({ id: group.id, label: group.label, fields: groupFields });
});
return sections;
}
export function RiskRecordDialog({ open, onClose, config, subject, view, record, onSaved }) {
const [editing, setEditing] = useState(!record); const [editing, setEditing] = useState(!record);
const [metadata, setMetadata] = useState({}); const [metadata, setMetadata] = useState({});
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const fields = useMemo( const displayColumns = useMemo(
() => () =>
fieldsOf(subject).filter((field) => { getConfiguredColumns({
subject,
view,
fallbackColumns: config.columns,
sampleRecord: record,
hideRecordTitle: config.drawerConfig?.hideRecordTitle,
}),
[subject, view, config, record],
);
const fields = useMemo(
() => {
const subjectFields = fieldsOf(subject);
const configuredFields = displayColumns
.map((column) => column.fieldDef)
.filter((field) =>
subjectFields.some((subjectField) => getFieldName(subjectField) === getFieldName(field)),
);
const source = configuredFields.length > 0 ? configuredFields : subjectFields;
return source.filter((field) => {
const type = getFieldType(field); const type = getFieldType(field);
return !['formula', 'computed'].includes(type) && !field?.settings?.hidden && !field?.hidden; return !['formula', 'computed'].includes(type) && !field?.settings?.hidden && !field?.hidden;
}), });
[subject], },
[subject, displayColumns],
);
const drawerFieldGroups = useMemo(
() => getDrawerFieldGroups(fields, view),
[fields, view],
); );
useEffect(() => { useEffect(() => {
...@@ -397,16 +461,7 @@ export function RiskRecordDialog({ open, onClose, config, subject, record, onSav ...@@ -397,16 +461,7 @@ export function RiskRecordDialog({ open, onClose, config, subject, record, onSav
} }
}; };
return ( const renderField = (field) => {
<Dialog open={open} onClose={onClose} size="5xl">
<DialogTitle>{record ? '记录详情' : `新增${config.title}`}</DialogTitle>
<DialogDescription>
{editing ? '业务字段将统一保存到 metadata 对象中。' : '查看完整字段信息。'}
</DialogDescription>
<DialogBody>
{fields.length > 0 ? (
<div className="grid gap-x-6 gap-y-5 md:grid-cols-2">
{fields.map((field) => {
const name = getFieldName(field); const name = getFieldName(field);
return ( return (
<Field <Field
...@@ -429,15 +484,61 @@ export function RiskRecordDialog({ open, onClose, config, subject, record, onSav ...@@ -429,15 +484,61 @@ export function RiskRecordDialog({ open, onClose, config, subject, record, onSav
{errors[name] ? <ErrorMessage>{errors[name]}</ErrorMessage> : null} {errors[name] ? <ErrorMessage>{errors[name]}</ErrorMessage> : null}
</Field> </Field>
); );
})} };
return (
<Headless.Dialog open={open} onClose={onClose} className="relative z-50">
<Headless.DialogBackdrop
transition
className="fixed inset-0 bg-zinc-950/25 transition duration-150 data-closed:opacity-0"
/>
<div className="fixed inset-0 overflow-hidden">
<div className="absolute inset-0 overflow-hidden">
<div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-8 sm:pl-16">
<Headless.DialogPanel
transition
className="pointer-events-auto flex h-full w-screen max-w-5xl flex-col bg-white shadow-2xl ring-1 ring-zinc-950/10 transition duration-150 data-closed:translate-x-full"
>
<div className="flex items-start justify-between gap-4 border-b border-zinc-200 px-5 py-4 sm:px-6">
<div>
<Headless.DialogTitle className="text-base font-semibold text-zinc-950">
{record ? '记录详情' : `新增${config.title}`}
</Headless.DialogTitle>
<p className="mt-1 text-sm text-zinc-500">
{editing ? '业务字段将统一保存到 metadata 对象中。' : '查看完整字段信息。'}
</p>
</div>
<Button plain onClick={onClose} aria-label="关闭详情" title="关闭详情">
<XMarkIcon />
</Button>
</div>
<div className="flex-1 overflow-y-auto px-5 py-5 sm:px-6">
{fields.length > 0 ? (
drawerFieldGroups.length > 0 ? (
<div className="space-y-6">
{drawerFieldGroups.map((section) => (
<section key={section.id} className={section.label ? 'border-t border-zinc-200 pt-5 first:border-t-0 first:pt-0' : ''}>
{section.label ? (
<h3 className="mb-4 text-sm font-semibold text-zinc-900">{section.label}</h3>
) : null}
<div className="grid gap-x-6 gap-y-5 md:grid-cols-2">
{section.fields.map(renderField)}
</div>
</section>
))}
</div>
) : (
<div className="grid gap-x-6 gap-y-5 md:grid-cols-2">
{fields.map(renderField)}
</div> </div>
)
) : ( ) : (
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800"> <div className="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
当前 Subject 未返回可编辑字段,请检查线上字段配置。 当前 Subject 未返回可编辑字段,请检查线上字段配置。
</div> </div>
)} )}
</DialogBody> </div>
<DialogActions> <div className="flex flex-col-reverse gap-3 border-t border-zinc-200 px-5 py-4 sm:flex-row sm:justify-end sm:px-6">
<Button plain onClick={onClose}>关闭</Button> <Button plain onClick={onClose}>关闭</Button>
{record && config.canEdit && !editing ? ( {record && config.canEdit && !editing ? (
<Button color="blue" onClick={() => setEditing(true)}> <Button color="blue" onClick={() => setEditing(true)}>
...@@ -461,8 +562,12 @@ export function RiskRecordDialog({ open, onClose, config, subject, record, onSav ...@@ -461,8 +562,12 @@ export function RiskRecordDialog({ open, onClose, config, subject, record, onSav
</Button> </Button>
</> </>
) : null} ) : null}
</DialogActions> </div>
</Dialog> </Headless.DialogPanel>
</div>
</div>
</div>
</Headless.Dialog>
); );
} }
...@@ -547,6 +652,7 @@ export default function RiskDataWorkspace({ workspaceKey }) { ...@@ -547,6 +652,7 @@ export default function RiskDataWorkspace({ workspaceKey }) {
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const [records, setRecords] = useState([]); const [records, setRecords] = useState([]);
const [subject, setSubject] = useState(null); const [subject, setSubject] = useState(null);
const [view, setView] = useState(null);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(20); const [pageSize, setPageSize] = useState(20);
...@@ -572,6 +678,47 @@ export default function RiskDataWorkspace({ workspaceKey }) { ...@@ -572,6 +678,47 @@ export default function RiskDataWorkspace({ workspaceKey }) {
() => new Map(fieldsOf(subject).map((field) => [getFieldName(field), field])), () => new Map(fieldsOf(subject).map((field) => [getFieldName(field), field])),
[subject], [subject],
); );
const tableColumns = useMemo(
() =>
getConfiguredColumns({
subject,
view,
fallbackColumns: config.columns,
sampleRecord: records[0],
hideRecordTitle: config.drawerConfig?.hideRecordTitle,
}),
[subject, view, config, records],
);
const tableHeaderGroups = useMemo(
() => getTableHeaderGroups(tableColumns, view, config.subject),
[tableColumns, view, config.subject],
);
const hasGroupedHeaders = tableHeaderGroups.some((item) => item.type === 'group');
const rowGroupFields = useMemo(
() =>
(view?.settings?.groupCols || [])
.map((item) => item?.field)
.filter(Boolean),
[view],
);
const groupedRecordSections = useMemo(() => {
if (!rowGroupFields.length) return [{ key: '__all__', label: '', records }];
const sectionMap = new Map();
records.forEach((record) => {
const labels = rowGroupFields.map((field) => {
const value = formatCellValue(record, field);
return `${getColumnLabel(field, subject)}:${value === '-' ? '空' : value}`;
});
const key = labels.join(' / ');
if (!sectionMap.has(key)) sectionMap.set(key, []);
sectionMap.get(key).push(record);
});
return Array.from(sectionMap.entries()).map(([key, items]) => ({
key,
label: key,
records: items,
}));
}, [records, rowGroupFields, subject]);
const totalPages = Math.max(1, Math.ceil(total / pageSize)); const totalPages = Math.max(1, Math.ceil(total / pageSize));
const load = async () => { const load = async () => {
...@@ -585,6 +732,7 @@ export default function RiskDataWorkspace({ workspaceKey }) { ...@@ -585,6 +732,7 @@ export default function RiskDataWorkspace({ workspaceKey }) {
}); });
setRecords(result.records); setRecords(result.records);
setSubject(result.subject); setSubject(result.subject);
setView(result.view);
setTotal(result.total); setTotal(result.total);
setSelectedIds(new Set()); setSelectedIds(new Set());
} catch (loadError) { } catch (loadError) {
...@@ -819,7 +967,7 @@ export default function RiskDataWorkspace({ workspaceKey }) { ...@@ -819,7 +967,7 @@ export default function RiskDataWorkspace({ workspaceKey }) {
<Table dense striped bleed> <Table dense striped bleed>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableHeader className="w-12"> <TableHeader className="w-12" rowSpan={hasGroupedHeaders ? 2 : undefined}>
<Checkbox <Checkbox
color="sky" color="sky"
checked={allCurrentSelected} checked={allCurrentSelected}
...@@ -830,14 +978,43 @@ export default function RiskDataWorkspace({ workspaceKey }) { ...@@ -830,14 +978,43 @@ export default function RiskDataWorkspace({ workspaceKey }) {
aria-label="全选当前页" aria-label="全选当前页"
/> />
</TableHeader> </TableHeader>
{config.columns.map((name) => ( {(hasGroupedHeaders ? tableHeaderGroups : tableColumns.map((column) => ({ type: 'column', column }))).map((item) => (
<TableHeader key={name}>{getColumnLabel(name, subject)}</TableHeader> item.type === 'group' ? (
<TableHeader key={item.id} colSpan={item.columns.length} className="border-x border-zinc-950/5 text-center text-zinc-700">
{item.label}
</TableHeader>
) : (
<TableHeader key={item.column.field} rowSpan={hasGroupedHeaders ? 2 : undefined}>
{getColumnLabel(item.column, subject)}
</TableHeader>
)
))}
<TableHeader className="w-16" rowSpan={hasGroupedHeaders ? 2 : undefined}>
<span className="sr-only">操作</span>
</TableHeader>
</TableRow>
{hasGroupedHeaders ? (
<TableRow>
{tableHeaderGroups
.filter((item) => item.type === 'group')
.flatMap((item) => item.columns)
.map((column) => (
<TableHeader key={column.field}>{getColumnLabel(column, subject)}</TableHeader>
))} ))}
<TableHeader className="w-16"><span className="sr-only">操作</span></TableHeader>
</TableRow> </TableRow>
) : null}
</TableHead> </TableHead>
<TableBody> <TableBody>
{records.map((record) => ( {groupedRecordSections.map((section) => (
<Fragment key={section.key}>
{section.label ? (
<TableRow>
<TableCell colSpan={tableColumns.length + 2} className="bg-zinc-50 py-2 text-xs font-medium text-zinc-600">
{section.label}({section.records.length})
</TableCell>
</TableRow>
) : null}
{section.records.map((record) => (
<TableRow key={recordKey(record)} className="hover:bg-zinc-50"> <TableRow key={recordKey(record)} className="hover:bg-zinc-50">
<TableCell> <TableCell>
<Checkbox <Checkbox
...@@ -847,9 +1024,9 @@ export default function RiskDataWorkspace({ workspaceKey }) { ...@@ -847,9 +1024,9 @@ export default function RiskDataWorkspace({ workspaceKey }) {
aria-label={`选择记录 ${record.title || recordKey(record)}`} aria-label={`选择记录 ${record.title || recordKey(record)}`}
/> />
</TableCell> </TableCell>
{config.columns.map((name) => ( {tableColumns.map((column) => (
<TableCell key={name} onClick={() => openRecord(record)} className="cursor-pointer"> <TableCell key={column.field} onClick={() => openRecord(record)} className="cursor-pointer">
<CellValue record={record} name={name} /> <CellValue record={record} column={column} />
</TableCell> </TableCell>
))} ))}
<TableCell> <TableCell>
...@@ -885,6 +1062,8 @@ export default function RiskDataWorkspace({ workspaceKey }) { ...@@ -885,6 +1062,8 @@ export default function RiskDataWorkspace({ workspaceKey }) {
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</Fragment>
))}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
...@@ -901,15 +1080,20 @@ export default function RiskDataWorkspace({ workspaceKey }) { ...@@ -901,15 +1080,20 @@ export default function RiskDataWorkspace({ workspaceKey }) {
/> />
<button type="button" onClick={() => openRecord(record)} className="min-w-0 flex-1 text-left"> <button type="button" onClick={() => openRecord(record)} className="min-w-0 flex-1 text-left">
<div className="font-medium text-zinc-950"> <div className="font-medium text-zinc-950">
{formatCellValue(record, config.columns[0])} {formatCellValue(record, tableColumns[0] || config.columns[0])}
</div> </div>
<div className="mt-2 line-clamp-3 text-sm text-zinc-600"> <div className="mt-2 line-clamp-3 text-sm text-zinc-600">
{formatCellValue(record, config.columns.find((name) => ['integrity_risk_points', 'description', 'title'].includes(name)) || config.columns[1])} {formatCellValue(
record,
tableColumns.find((column) =>
['integrity_risk_points', 'description', 'title'].includes(getColumnFieldName(column)),
) || tableColumns[1] || config.columns[1],
)}
</div> </div>
<div className="mt-3 flex flex-wrap gap-2"> <div className="mt-3 flex flex-wrap gap-2">
{config.columns.slice(1, 5).map((name) => { {tableColumns.slice(1, 5).map((column) => {
const value = formatCellValue(record, name); const value = formatCellValue(record, column);
return value === '-' ? null : <Badge key={name} color={getRecordTone(value)}>{value}</Badge>; return value === '-' ? null : <Badge key={column.field} color={getRecordTone(value)}>{value}</Badge>;
})} })}
</div> </div>
</button> </button>
...@@ -949,6 +1133,7 @@ export default function RiskDataWorkspace({ workspaceKey }) { ...@@ -949,6 +1133,7 @@ export default function RiskDataWorkspace({ workspaceKey }) {
onClose={() => setRecordDialogOpen(false)} onClose={() => setRecordDialogOpen(false)}
config={config} config={config}
subject={subject} subject={subject}
view={view}
record={activeRecord} record={activeRecord}
onSaved={async (message) => { onSaved={async (message) => {
setNotice(message); setNotice(message);
......
...@@ -14,11 +14,11 @@ ...@@ -14,11 +14,11 @@
## 3. 通用交互流程 ## 3. 通用交互流程
1. 页面进入时调用 `GET /subject/:name` 加载字段定义,并调用 `POST /record/query` 与 `POST /record/count` 分页加载记录。 1. 页面进入时调用 `GET /subject/:name` 加载字段定义;若模块配置了 `filterView`,同时调用 `GET /subject/:name/view/:viewName` 读取视图列、列分组和行分组配置;随后调用 `POST /record/query` 与 `POST /record/count` 分页加载记录。
2. 关键词可搜索标题、廉洁风险点、主要防控措施或措施描述。 2. 关键词可搜索标题、廉洁风险点、主要防控措施或措施描述。
3. 分类、概率、危害程度、审批状态和类别筛选只在对应字段存在 `settings.options` 时展示,筛选值不在前端硬编码。 3. 分类、概率、危害程度、审批状态和类别筛选只在对应字段存在 `settings.options` 时展示,筛选值不在前端硬编码。
4. 桌面端使用 Catalyst `Table`,移动端使用可点击的记录列表;两端都支持当前页勾选。 4. 桌面端使用 Catalyst `Table`,表头优先按视图 `settings.columns` 生成,并兼容 `settings.groups` 的两层分组表头;若视图配置 `settings.groupCols`,当前页记录按配置字段分块展示。移动端使用同一列配置生成可点击记录列表;两端都支持当前页勾选。
5. 点击记录打开 Headless UI/Catalyst 详情弹框。具备编辑权限的页面可切换到编辑态。 5. 点击记录打开 Headless UI 右侧详情抽屉。详情字段优先沿用当前视图列顺序和分组配置,具备编辑权限的页面可切换到编辑态。
6. 新增和编辑表单根据 Subject 字段类型动态生成;业务字段统一写入 `metadata`。 6. 新增和编辑表单根据 Subject 字段类型动态生成;业务字段统一写入 `metadata`。
7. 删除前必须二次确认,默认执行软删除。接口异常保留当前页面状态并显示可恢复的错误提示。 7. 删除前必须二次确认,默认执行软删除。接口异常保留当前页面状态并显示可恢复的错误提示。
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
importRecords, importRecords,
loadRecord, loadRecord,
loadSubject, loadSubject,
getSubjectViewDetail,
loadUser, loadUser,
queryRecords, queryRecords,
queryUsers, queryUsers,
...@@ -54,6 +55,7 @@ export const WORKSPACE_CONFIG = { ...@@ -54,6 +55,7 @@ export const WORKSPACE_CONFIG = {
canArchive: true, canArchive: true,
canCopy: true, canCopy: true,
exportKind: 'process', exportKind: 'process',
filterView: ['view_RAGm05'],
}, },
display: { display: {
subject: RISK_SUBJECTS.display, subject: RISK_SUBJECTS.display,
...@@ -152,6 +154,45 @@ const EXPORT_COLUMNS = [ ...@@ -152,6 +154,45 @@ const EXPORT_COLUMNS = [
['类别', 'type'], ['类别', 'type'],
]; ];
const RISK_TABLE_GROUPS = [
{
id: 'risk_identification',
name: '风险识别',
children: [
'original_index',
'classification',
'involving_major_aspects_business_transaction_processes',
'involving_major_aspects_business_transaction_processes_add',
'integrity_risk_points',
'involving_positions_or_personnel',
'involving_positions_or_personnel_add',
],
},
{
id: 'risk_assessment',
name: '风险评估',
children: ['probability', 'hazard_degree', '__risk_level'],
},
{
id: 'prevention_measures',
name: '防控措施',
children: [
'description_of_main_prevention_and_control_measures',
'file_name_and_document_number',
'file_name_other',
'subsidiary_system',
'subsidiary_system_add',
'business_unit',
'business_unit_add',
],
},
{
id: 'approval_flow',
name: '填报审批',
children: ['filled_by', 'approved_by', 'approval_status', 'final_approval_status', 'type'],
},
];
function extractRecords(response) { function extractRecords(response) {
if (Array.isArray(response)) return response; if (Array.isArray(response)) return response;
if (Array.isArray(response?.records)) return response.records; if (Array.isArray(response?.records)) return response.records;
...@@ -172,6 +213,70 @@ function extractCount(response) { ...@@ -172,6 +213,70 @@ function extractCount(response) {
return values.find((value) => Number.isFinite(value)) || 0; return values.find((value) => Number.isFinite(value)) || 0;
} }
function fieldsOf(subject) {
if (Array.isArray(subject?.fields)) return subject.fields;
if (subject?.fields && typeof subject.fields === 'object') return Object.values(subject.fields);
return [];
}
function stripMetadataPrefix(fieldPath = '') {
const normalized = String(fieldPath || '');
if (normalized.startsWith('metadata.')) return normalized.replace('metadata.', '');
return normalized.split('.').pop() || normalized;
}
function getNestedValue(source, path) {
if (!source || !path) return undefined;
return String(path)
.split('.')
.reduce((value, key) => (value === undefined || value === null ? undefined : value[key]), source);
}
function getFieldDefinition(subject, columnField) {
const name = stripMetadataPrefix(columnField);
return fieldsOf(subject).find((field) => getFieldName(field) === name) || null;
}
function isHiddenField(field = {}) {
const type = getFieldType(field);
return Boolean(field.hidden || field?.settings?.hidden || ['formula', 'computed'].includes(type));
}
function normalizeColumnConfig(column, subject) {
const field = typeof column === 'string' ? column : column?.field || column?.name;
if (!field) return null;
const name = stripMetadataPrefix(field);
const fieldDef = getFieldDefinition(subject, field);
return {
field,
name,
width: typeof column === 'object' ? column.width : undefined,
pinned: typeof column === 'object' ? column.pinned : undefined,
fieldDef: fieldDef || { name, label: RISK_FIELD_LABELS[name], type: 'text' },
label: typeof column === 'object'
? column.label || column.headerName || column.title || getColumnLabel(name, subject)
: getColumnLabel(name, subject),
};
}
function workspaceConfigBySubject(subjectName) {
return Object.values(WORKSPACE_CONFIG).find((item) => item.subject === subjectName) || {};
}
async function loadConfiguredView(subjectName) {
const config = workspaceConfigBySubject(subjectName);
const viewNames = config.filterView || [];
for (const viewName of viewNames) {
try {
const view = await getSubjectViewDetail(subjectName, viewName);
if (view) return view;
} catch {
// View configuration is optional. The table falls back to Subject fields.
}
}
return null;
}
export function getFieldName(field = {}) { export function getFieldName(field = {}) {
return field.name || field.alias || field.key || ''; return field.name || field.alias || field.key || '';
} }
...@@ -212,8 +317,12 @@ export function isFieldMultiple(field = {}) { ...@@ -212,8 +317,12 @@ export function isFieldMultiple(field = {}) {
} }
export function getColumnLabel(name, subject) { export function getColumnLabel(name, subject) {
if (RISK_FIELD_LABELS[name]) return RISK_FIELD_LABELS[name]; const fieldName = stripMetadataPrefix(typeof name === 'object' ? name.field || name.name : name);
return getFieldLabel(subject?.fields?.find((field) => getFieldName(field) === name)); if (typeof name === 'object' && (name.label || name.headerName || name.title)) {
return name.label || name.headerName || name.title;
}
if (RISK_FIELD_LABELS[fieldName]) return RISK_FIELD_LABELS[fieldName];
return getFieldLabel(fieldsOf(subject).find((field) => getFieldName(field) === fieldName));
} }
export function calculateRiskLevel(probability, hazardDegree) { export function calculateRiskLevel(probability, hazardDegree) {
...@@ -231,7 +340,13 @@ function unwrapReference(value) { ...@@ -231,7 +340,13 @@ function unwrapReference(value) {
return value ?? ''; return value ?? '';
} }
export function formatCellValue(record, name) { export function getColumnFieldName(column) {
return stripMetadataPrefix(typeof column === 'object' ? column.field || column.name : column);
}
export function formatCellValue(record, column) {
const fieldPath = typeof column === 'object' ? column.field || column.name : column;
const name = stripMetadataPrefix(fieldPath);
if (name === 'title') return record?.title || '-'; if (name === 'title') return record?.title || '-';
if (name === 'created_at' || name === 'updated_at') { if (name === 'created_at' || name === 'updated_at') {
const value = record?.[name]; const value = record?.[name];
...@@ -249,7 +364,9 @@ export function formatCellValue(record, name) { ...@@ -249,7 +364,9 @@ export function formatCellValue(record, name) {
].reduce((total, value) => total + (Array.isArray(value) ? value.length : value ? 1 : 0), 0); ].reduce((total, value) => total + (Array.isArray(value) ? value.length : value ? 1 : 0), 0);
} }
const value = record?.metadata?.[name]; const value = String(fieldPath || '').includes('.')
? getNestedValue(record, fieldPath)
: record?.[name] ?? record?.metadata?.[name];
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map(unwrapReference).filter(Boolean).join('、') || '-'; return value.map(unwrapReference).filter(Boolean).join('、') || '-';
} }
...@@ -261,6 +378,108 @@ export function formatCellValue(record, name) { ...@@ -261,6 +378,108 @@ export function formatCellValue(record, name) {
return value === 0 ? '0' : value || '-'; return value === 0 ? '0' : value || '-';
} }
export function getConfiguredColumns({ subject, view, fallbackColumns = [], sampleRecord, hideRecordTitle = false }) {
const viewColumns = view?.settings?.columns || view?.columns || [];
const hasViewColumns = Array.isArray(viewColumns) && viewColumns.length > 0;
const configuredNames = new Set();
const columns = [];
if (hasViewColumns) {
viewColumns.forEach((column) => {
const field = column?.field || column?.name;
if (!field) return;
const name = stripMetadataPrefix(field);
configuredNames.add(name);
if (column.hide || (name === 'title' && hideRecordTitle)) return;
const normalized = normalizeColumnConfig(column, subject);
if (normalized) columns.push(normalized);
});
}
const sourceFields = fieldsOf(subject).filter((field) => {
const name = getFieldName(field);
return name && !configuredNames.has(name) && !isHiddenField(field);
});
if (hasViewColumns) {
sourceFields.forEach((field) => {
const name = getFieldName(field);
const fieldPath = sampleRecord?.metadata && Object.prototype.hasOwnProperty.call(sampleRecord.metadata, name)
? `metadata.${name}`
: name;
columns.push({
field: fieldPath,
name,
label: getFieldLabel(field),
fieldDef: field,
});
});
}
if (!hasViewColumns && sourceFields.length > 0) {
if (!hideRecordTitle && (sampleRecord?.title || fallbackColumns.includes('title'))) {
columns.push(normalizeColumnConfig('title', subject));
}
sourceFields.forEach((field) => {
const name = getFieldName(field);
columns.push({
field: `metadata.${name}`,
name,
label: getFieldLabel(field),
fieldDef: field,
});
});
}
fallbackColumns.forEach((field) => {
const name = stripMetadataPrefix(field);
if (!columns.some((column) => column.name === name) && (name.startsWith('__') || columns.length === 0)) {
const normalized = normalizeColumnConfig(field, subject);
if (normalized) columns.push(normalized);
}
});
return columns.filter(Boolean);
}
export function getTableHeaderGroups(columns, view, subjectName) {
const configuredGroups = view?.settings?.groups || view?.groups || [];
const groups = Array.isArray(configuredGroups) && configuredGroups.length > 0
? configuredGroups
: [RISK_SUBJECTS.process, RISK_SUBJECTS.display].includes(subjectName)
? RISK_TABLE_GROUPS
: [];
if (!groups.length) return [];
const groupByField = new Map();
groups.forEach((group) => {
const id = group.id || group.name;
(group.children || []).forEach((child) => {
groupByField.set(stripMetadataPrefix(child), { id, name: group.name || group.label || id });
});
});
const renderedGroups = new Set();
const renderedColumns = new Set();
const headerGroups = [];
columns.forEach((column) => {
if (renderedColumns.has(column.name)) return;
const group = groupByField.get(column.name);
if (!group || renderedGroups.has(group.id)) {
headerGroups.push({ type: 'column', column });
renderedColumns.add(column.name);
return;
}
const groupedColumns = columns.filter((item) => groupByField.get(item.name)?.id === group.id);
groupedColumns.forEach((item) => renderedColumns.add(item.name));
renderedGroups.add(group.id);
headerGroups.push({ type: 'group', id: group.id, label: group.name, columns: groupedColumns });
});
return headerGroups;
}
function buildRecordFilter(filters = {}) { function buildRecordFilter(filters = {}) {
const filter = {}; const filter = {};
const keyword = String(filters.keyword || '').trim(); const keyword = String(filters.keyword || '').trim();
...@@ -291,8 +510,9 @@ export async function loadWorkspaceData(subjectName, filters = {}) { ...@@ -291,8 +510,9 @@ export async function loadWorkspaceData(subjectName, filters = {}) {
const filter = buildRecordFilter(filters); const filter = buildRecordFilter(filters);
const page = Number(filters.page || 0); const page = Number(filters.page || 0);
const pageSize = Number(filters.pageSize || 20); const pageSize = Number(filters.pageSize || 20);
const [subject, response, countResponse] = await Promise.all([ const [subject, view, response, countResponse] = await Promise.all([
loadSubject(subjectName), loadSubject(subjectName),
loadConfiguredView(subjectName),
queryRecords({ queryRecords({
subject: subjectName, subject: subjectName,
page, page,
...@@ -308,6 +528,7 @@ export async function loadWorkspaceData(subjectName, filters = {}) { ...@@ -308,6 +528,7 @@ export async function loadWorkspaceData(subjectName, filters = {}) {
return { return {
subject, subject,
view,
records: extractRecords(response), records: extractRecords(response),
total: extractCount(countResponse), total: extractCount(countResponse),
}; };
......
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