Commit 558ad817 authored by drigle's avatar drigle

fix: sync view restrictions and short title behavior

parent 91a90cce
...@@ -92,6 +92,7 @@ import { ...@@ -92,6 +92,7 @@ import {
normalizeDelayRequest, normalizeDelayRequest,
normalizeDelayRequestItems, normalizeDelayRequestItems,
normalizeDepartmentRectificationItems, normalizeDepartmentRectificationItems,
getViewRestrictionDefaults,
} from '../problemRectificationListService'; } from '../problemRectificationListService';
import { import {
calculateProblemSeverityLevel, calculateProblemSeverityLevel,
...@@ -399,7 +400,7 @@ function RemoteUserSelect({ label, value, onChange, placeholder, onSearch }) { ...@@ -399,7 +400,7 @@ function RemoteUserSelect({ label, value, onChange, placeholder, onSearch }) {
); );
} }
function MultiSelectDropdown({ value, options, onChange, placeholder = '请选择', variant }) { function MultiSelectDropdown({ value, options, onChange, placeholder = '请选择', variant, disabled = false }) {
return ( return (
<ResourceFilterMultiSelect <ResourceFilterMultiSelect
onChange={onChange} onChange={onChange}
...@@ -408,11 +409,12 @@ function MultiSelectDropdown({ value, options, onChange, placeholder = '请选 ...@@ -408,11 +409,12 @@ function MultiSelectDropdown({ value, options, onChange, placeholder = '请选
searchable searchable
value={Array.isArray(value) ? value : []} value={Array.isArray(value) ? value : []}
variant={variant} variant={variant}
disabled={disabled}
/> />
); );
} }
function CreateFieldSelect({ value, options, onChange, placeholder = '请选择', variant }) { function CreateFieldSelect({ value, options, onChange, placeholder = '请选择', variant, disabled = false }) {
return ( return (
<XinyuanAutocomplete <XinyuanAutocomplete
aria-label={placeholder} aria-label={placeholder}
...@@ -423,12 +425,13 @@ function CreateFieldSelect({ value, options, onChange, placeholder = '请选择' ...@@ -423,12 +425,13 @@ function CreateFieldSelect({ value, options, onChange, placeholder = '请选择'
placeholder={placeholder} placeholder={placeholder}
selectedKey={value || null} selectedKey={value || null}
variant={variant} variant={variant}
isDisabled={disabled}
onSelectionChange={(key) => onChange(key ? String(key) : '')} onSelectionChange={(key) => onChange(key ? String(key) : '')}
/> />
); );
} }
function CreateField({ fieldDef, value, onChange, variant }) { function CreateField({ fieldDef, value, onChange, variant, disabled = false }) {
const options = getSelectOptions(fieldDef); const options = getSelectOptions(fieldDef);
const isMultiple = Boolean(fieldDef?.multiple); const isMultiple = Boolean(fieldDef?.multiple);
const fieldType = CREATE_FIELD_TYPE_OVERRIDES[fieldDef?.name] || fieldDef?.type || 'text'; const fieldType = CREATE_FIELD_TYPE_OVERRIDES[fieldDef?.name] || fieldDef?.type || 'text';
...@@ -458,6 +461,7 @@ function CreateField({ fieldDef, value, onChange, variant }) { ...@@ -458,6 +461,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
variant={variant} variant={variant}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
placeholder={placeholder} placeholder={placeholder}
disabled={disabled}
/> />
); );
} }
...@@ -467,6 +471,7 @@ function CreateField({ fieldDef, value, onChange, variant }) { ...@@ -467,6 +471,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
<FormDatePicker <FormDatePicker
value={value || ''} value={value || ''}
variant={variant} variant={variant}
disabled={disabled}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
/> />
); );
...@@ -481,6 +486,7 @@ function CreateField({ fieldDef, value, onChange, variant }) { ...@@ -481,6 +486,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
onChange={onChange} onChange={onChange}
placeholder="请选择" placeholder="请选择"
variant={variant} variant={variant}
disabled={disabled}
/> />
); );
} }
...@@ -491,6 +497,7 @@ function CreateField({ fieldDef, value, onChange, variant }) { ...@@ -491,6 +497,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
options={options} options={options}
onChange={onChange} onChange={onChange}
variant={variant} variant={variant}
disabled={disabled}
/> />
); );
} }
...@@ -502,6 +509,7 @@ function CreateField({ fieldDef, value, onChange, variant }) { ...@@ -502,6 +509,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
options={overrideOptions.map((option) => ({ label: option, value: option }))} options={overrideOptions.map((option) => ({ label: option, value: option }))}
onChange={onChange} onChange={onChange}
variant={variant} variant={variant}
disabled={disabled}
/> />
); );
} }
...@@ -515,6 +523,7 @@ function CreateField({ fieldDef, value, onChange, variant }) { ...@@ -515,6 +523,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
variant={variant} variant={variant}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
placeholder={placeholder} placeholder={placeholder}
disabled={disabled}
/> />
); );
} }
...@@ -1058,9 +1067,14 @@ export default function ProblemRectificationListPage() { ...@@ -1058,9 +1067,14 @@ export default function ProblemRectificationListPage() {
[activeGridView, subject], [activeGridView, subject],
); );
const restrictionDefaults = useMemo(
() => getViewRestrictionDefaults(activeGridView, subject),
[activeGridView, subject],
);
const initialCreateValues = useMemo( const initialCreateValues = useMemo(
() => buildInitialCreateValues(createFields), () => buildInitialCreateValues(createFields, restrictionDefaults),
[createFields], [createFields, restrictionDefaults],
); );
useEffect(() => { useEffect(() => {
...@@ -1103,12 +1117,20 @@ export default function ProblemRectificationListPage() { ...@@ -1103,12 +1117,20 @@ export default function ProblemRectificationListPage() {
}, [showPushModal, selectedRecords]); }, [showPushModal, selectedRecords]);
const handleCreate = async () => { const handleCreate = async () => {
if (restrictionDefaults.errors.length > 0) {
setCreateValidationMessage(restrictionDefaults.errors.join('\n'));
return;
}
const hideTitle = shouldHideViewTitleColumn(activeGridView);
if ( if (
!normalizeText(formValues.title) && (!hideTitle && !normalizeText(formValues.title) && !normalizeText(formValues.project_name) && !normalizeText(formValues.problem_brief)) ||
!normalizeText(formValues.project_name) && (hideTitle && !normalizeText(formValues.project_name) && !normalizeText(formValues.problem_brief))
!normalizeText(formValues.problem_brief)
) { ) {
setCreateValidationMessage('记录标题、检查项目名称、问题简述至少填写一项。'); setCreateValidationMessage(
hideTitle
? '检查项目名称、问题简述至少填写一项。'
: '记录标题、检查项目名称、问题简述至少填写一项。',
);
return; return;
} }
...@@ -1116,7 +1138,7 @@ export default function ProblemRectificationListPage() { ...@@ -1116,7 +1138,7 @@ export default function ProblemRectificationListPage() {
setError(''); setError('');
try { try {
const payload = buildProblemRectificationCreatePayload(formValues, createFields); const payload = buildProblemRectificationCreatePayload(formValues, createFields, restrictionDefaults, { hideTitle });
await createRecord(payload); await createRecord(payload);
setCreateValidationMessage(''); setCreateValidationMessage('');
setShowCreateModal(false); setShowCreateModal(false);
...@@ -2123,6 +2145,7 @@ export default function ProblemRectificationListPage() { ...@@ -2123,6 +2145,7 @@ export default function ProblemRectificationListPage() {
<CreateField <CreateField
fieldDef={fieldDef} fieldDef={fieldDef}
value={formValues[fieldDef.name]} value={formValues[fieldDef.name]}
disabled={actionLoading || Object.prototype.hasOwnProperty.call(restrictionDefaults.metadata, fieldDef.name)}
variant={createFieldVariant} variant={createFieldVariant}
onChange={(nextValue) => onChange={(nextValue) =>
setFormValues((current) => { setFormValues((current) => {
......
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
问题整改列表的新增表单、筛选和表格均读取当前激活 Grid View: 问题整改列表的新增表单、筛选和表格均读取当前激活 Grid View:
- 新增表单按 `settings.columns` 顺序渲染可见字段,并包含记录标题字段;标题是否必填跟随视图列配置,未配置短标题时使用标题作为默认回退值。 - 新增表单按 `settings.columns` 顺序渲染可见字段,并包含记录标题字段;标题是否必填跟随视图列配置,未配置短标题时使用标题作为默认回退值。`settings.shortTitle = true` 时隐藏列表及新增表单的根级记录标题,不校验或提交根级 `title`。
- 筛选控件由视图列的 Subject 字段类型自动生成。`select` 选项来自 `settings.options`,日期、数字、布尔、用户和引用字段使用对应控件;提交时按 `field:<path>` 通过 `/record/query` 远程筛选,不再提供独立关键词搜索。 - 筛选控件由视图列的 Subject 字段类型自动生成。`select` 选项来自 `settings.options`,日期、数字、布尔、用户和引用字段使用对应控件;提交时按 `field:<path>` 通过 `/record/query` 远程筛选,不再提供独立关键词搜索。
- 引用和用户多选的输入摘要、列表勾选与草稿统一使用归一化 ID;引用写入只保存目标记录 `_id` 或 `_id[]`。 - 引用和用户多选的输入摘要、列表勾选与草稿统一使用归一化 ID;引用写入只保存目标记录 `_id` 或 `_id[]`。
- 表格请求期间 DataGrid 使用居中的 loading 遮罩,空结果与加载中状态分开处理。 - 表格请求期间 DataGrid 使用居中的 loading 遮罩,空结果与加载中状态分开处理。
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
- `settings.groups` 决定列表列分组,例如把多个问题分类字段归入同一分组;前端需展示分组表头,同一分组下的列即使在 `settings.columns` 中不连续,也应在表格内聚拢展示,并支持每个分组独立折叠 / 展开,折叠后隐藏该分组子列 - `settings.groups` 决定列表列分组,例如把多个问题分类字段归入同一分组;前端需展示分组表头,同一分组下的列即使在 `settings.columns` 中不连续,也应在表格内聚拢展示,并支持每个分组独立折叠 / 展开,折叠后隐藏该分组子列
- `settings.shortTitle = true` 仅隐藏列表中的 `title` 展示列,不删除记录标题数据,也不改变标题字段的视图排序、详情标题或权限判断;标题列原本承载的部门明细数量 / 展开、延期申请阶段 / 详情和锁定状态迁移到固定的“问题动态”上下文列,详情、延期申请、锁定、复制和删除继续由固定行操作列提供 - `settings.shortTitle = true` 仅隐藏列表中的 `title` 展示列,不删除记录标题数据,也不改变标题字段的视图排序、详情标题或权限判断;标题列原本承载的部门明细数量 / 展开、延期申请阶段 / 详情和锁定状态迁移到固定的“问题动态”上下文列,详情、延期申请、锁定、复制和删除继续由固定行操作列提供
- `settings.quickSelect` 决定列表是否展示勾选列,用于批量推送、删除等操作 - `settings.quickSelect` 决定列表是否展示勾选列,用于批量推送、删除等操作
- `settings.restrictions` 与 `settings.filters.conditions` 共同决定该视图下可见的数据范围 - `settings.restrictions` 与 `settings.filters.conditions` 共同决定该视图下可见的数据范围;从当前 Grid View 新建时,`$eq` 或单值 `$in` 约束会自动写入 `metadata` 并锁定对应字段(即使该字段不在弹窗列中也会写入)。多值集合、否定条件或冲突约束无法确定唯一值时禁止创建并提示调整视图,编辑已有记录不覆盖原值
- Grid 视图的 `settings.viewAuth` 决定当前用户是否能看到该视图;普通用户未配置 `viewAuth` 时默认不可见,仅系统管理员和问题整改应用 `problem_rectification_new` 自身的“管理”角色可见。全部 Grid 视图均无权限时不得生成绕过授权的兜底视图、查询记录或展示新增/批量/行操作入口 - Grid 视图的 `settings.viewAuth` 决定当前用户是否能看到该视图;普通用户未配置 `viewAuth` 时默认不可见,仅系统管理员和问题整改应用 `problem_rectification_new` 自身的“管理”角色可见。全部 Grid 视图均无权限时不得生成绕过授权的兜底视图、查询记录或展示新增/批量/行操作入口
- 用户获得 Grid Tab 的查看权限后,可以打开该 Tab 中记录的详情并查看 Detail `columns` 配置的字段;Detail 不使用 `viewAuth` 再次隐藏详情字段,详情字段权限只控制是否允许编辑 - 用户获得 Grid Tab 的查看权限后,可以打开该 Tab 中记录的详情并查看 Detail `columns` 配置的字段;Detail 不使用 `viewAuth` 再次隐藏详情字段,详情字段权限只控制是否允许编辑
- `type = "detail"` 的视图用于详情抽屉字段编排,按 `columns` 顺序渲染字段;当前详情视图 `settings.shortTitle = true` 时,同时隐藏弹框 Header 中的记录标题和字段区中的 `title` 字段,未配置或为 `false` 时正常显示。当前 Detail 的 `settings.auth` 或 `settings.disableColumns` 存在有效字段配置时,该 Detail 同时作为详情字段编辑权限来源,切换详情页签后立即按新视图更新展示和权限 - `type = "detail"` 的视图用于详情抽屉字段编排,按 `columns` 顺序渲染字段;当前详情视图 `settings.shortTitle = true` 时,同时隐藏弹框 Header 中的记录标题和字段区中的 `title` 字段,未配置或为 `false` 时正常显示。当前 Detail 的 `settings.auth` 或 `settings.disableColumns` 存在有效字段配置时,该 Detail 同时作为详情字段编辑权限来源,切换详情页签后立即按新视图更新展示和权限
......
...@@ -27,6 +27,7 @@ import { ...@@ -27,6 +27,7 @@ import {
normalizeText, normalizeText,
recordMatchesRestrictions, recordMatchesRestrictions,
sortRecordsByView, sortRecordsByView,
getViewRestrictionDefaults,
} from '@/utils/subjectView'; } from '@/utils/subjectView';
export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification'; export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification';
...@@ -1567,19 +1568,20 @@ export function buildProblemRectificationCreateFormFields(gridView, subject) { ...@@ -1567,19 +1568,20 @@ export function buildProblemRectificationCreateFormFields(gridView, subject) {
: fallbackFields; : fallbackFields;
} }
export function buildInitialCreateValues(createFields) { export function buildInitialCreateValues(createFields, restrictionDefaults = {}) {
const values = createFields.reduce((nextValues, field) => ({ const values = createFields.reduce((nextValues, field) => ({
...nextValues, ...nextValues,
[field.fieldDef.name]: emptyValueForField(field.fieldDef), [field.fieldDef.name]: emptyValueForField(field.fieldDef),
}), {}); }), { ...(restrictionDefaults.metadata || {}) });
const resolvedValues = { ...values, ...(restrictionDefaults.metadata || {}) };
return { return {
...values, ...resolvedValues,
problem_severity_level: calculateProblemSeverityLevel(values), problem_severity_level: calculateProblemSeverityLevel(resolvedValues),
}; };
} }
export function buildProblemRectificationCreatePayload(formValues, createFields = []) { export function buildProblemRectificationCreatePayload(formValues, createFields = [], restrictionDefaults = {}, { hideTitle = false } = {}) {
const metadata = createFields.reduce((nextMetadata, field) => { const metadata = createFields.reduce((nextMetadata, field) => {
const fieldName = field.fieldDef?.name; const fieldName = field.fieldDef?.name;
if (!fieldName) { if (!fieldName) {
...@@ -1599,13 +1601,22 @@ export function buildProblemRectificationCreatePayload(formValues, createFields ...@@ -1599,13 +1601,22 @@ export function buildProblemRectificationCreatePayload(formValues, createFields
const projectName = normalizeText(metadata.project_name); const projectName = normalizeText(metadata.project_name);
const problemBrief = normalizeText(metadata.problem_brief); const problemBrief = normalizeText(metadata.problem_brief);
const recordTitle = normalizeText(formValues.title); const recordTitle = normalizeText(formValues.title);
const resolvedTitle = hideTitle
? undefined
: (recordTitle || restrictionDefaults.title || projectName || problemBrief || '问题整改记录');
return { return {
subject: PROBLEM_RECTIFICATION_SUBJECT, subject: PROBLEM_RECTIFICATION_SUBJECT,
title: recordTitle || projectName || problemBrief || '问题整改记录', ...(resolvedTitle ? { title: resolvedTitle } : {}),
metadata: { metadata: {
...metadata, ...metadata,
problem_severity_level: calculateProblemSeverityLevel(metadata), ...(restrictionDefaults.metadata || {}),
problem_severity_level: calculateProblemSeverityLevel({
...metadata,
...(restrictionDefaults.metadata || {}),
}),
}, },
}; };
} }
export { getViewRestrictionDefaults };
...@@ -480,6 +480,68 @@ export function mergeRestrictionsIntoFilter(baseFilter = {}, restrictions = []) ...@@ -480,6 +480,68 @@ export function mergeRestrictionsIntoFilter(baseFilter = {}, restrictions = [])
}, { ...baseFilter }); }, { ...baseFilter });
} }
function hasRestrictionValue(value) {
return value !== undefined && value !== null && value !== '';
}
function restrictionValuesEqual(left, right) {
if (Object.is(left, right)) return true;
try {
return JSON.stringify(left) === JSON.stringify(right);
} catch (error) {
return false;
}
}
/** Resolve only restrictions that identify one safe value for a newly created record. */
export function getViewRestrictionDefaults(view, subject) {
const assignments = new Map();
const unresolved = new Set();
const errors = new Map();
const addError = (fieldPath, fieldDef) => {
if (errors.has(fieldPath)) return;
const label = fieldDef?.label || getFieldNameFromPath(fieldPath) || fieldPath;
errors.set(fieldPath, `${label}的视图约束无法自动确定唯一值,请先调整视图配置。`);
};
toArray(view?.settings?.restrictions).forEach((restriction) => {
if (restriction?.type !== 'field' || !restriction.field || restriction.value === undefined) return;
const fieldPath = restriction.field === 'title' || restriction.field.startsWith('metadata.')
? restriction.field
: `metadata.${restriction.field}`;
const fieldDef = getFieldDefinition(fieldPath, subject);
const comparator = normalizeText(restriction.comparator || '$in');
const values = toArray(restriction.value).filter(hasRestrictionValue);
if (!['$eq', '$in'].includes(comparator) || values.length !== 1) {
unresolved.add(fieldPath);
addError(fieldPath, fieldDef);
return;
}
if (unresolved.has(fieldPath)) return;
let value = normalizeValueForSubmit(values[0], fieldDef);
if (fieldDef?.multiple) value = toArray(value);
if (assignments.has(fieldPath) && !restrictionValuesEqual(assignments.get(fieldPath), value)) {
assignments.delete(fieldPath);
unresolved.add(fieldPath);
addError(fieldPath, fieldDef);
return;
}
assignments.set(fieldPath, value);
});
const metadata = {};
let title;
assignments.forEach((value, fieldPath) => {
if (unresolved.has(fieldPath)) return;
if (fieldPath === 'title') title = value;
else {
const fieldName = getFieldNameFromPath(fieldPath);
if (fieldName) metadata[fieldName] = value;
}
});
return { metadata, title, errors: [...errors.values()] };
}
export function sortRecordsByView(records, view) { export function sortRecordsByView(records, view) {
const sortColumns = toArray(view?.settings?.columns) const sortColumns = toArray(view?.settings?.columns)
.filter((column) => column.sort) .filter((column) => column.sort)
......
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