Commit 558ad817 authored by drigle's avatar drigle

fix: sync view restrictions and short title behavior

parent 91a90cce
......@@ -92,6 +92,7 @@ import {
normalizeDelayRequest,
normalizeDelayRequestItems,
normalizeDepartmentRectificationItems,
getViewRestrictionDefaults,
} from '../problemRectificationListService';
import {
calculateProblemSeverityLevel,
......@@ -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 (
<ResourceFilterMultiSelect
onChange={onChange}
......@@ -408,11 +409,12 @@ function MultiSelectDropdown({ value, options, onChange, placeholder = '请选
searchable
value={Array.isArray(value) ? value : []}
variant={variant}
disabled={disabled}
/>
);
}
function CreateFieldSelect({ value, options, onChange, placeholder = '请选择', variant }) {
function CreateFieldSelect({ value, options, onChange, placeholder = '请选择', variant, disabled = false }) {
return (
<XinyuanAutocomplete
aria-label={placeholder}
......@@ -423,12 +425,13 @@ function CreateFieldSelect({ value, options, onChange, placeholder = '请选择'
placeholder={placeholder}
selectedKey={value || null}
variant={variant}
isDisabled={disabled}
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 isMultiple = Boolean(fieldDef?.multiple);
const fieldType = CREATE_FIELD_TYPE_OVERRIDES[fieldDef?.name] || fieldDef?.type || 'text';
......@@ -458,6 +461,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
variant={variant}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
disabled={disabled}
/>
);
}
......@@ -467,6 +471,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
<FormDatePicker
value={value || ''}
variant={variant}
disabled={disabled}
onChange={(event) => onChange(event.target.value)}
/>
);
......@@ -481,6 +486,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
onChange={onChange}
placeholder="请选择"
variant={variant}
disabled={disabled}
/>
);
}
......@@ -491,6 +497,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
options={options}
onChange={onChange}
variant={variant}
disabled={disabled}
/>
);
}
......@@ -502,6 +509,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
options={overrideOptions.map((option) => ({ label: option, value: option }))}
onChange={onChange}
variant={variant}
disabled={disabled}
/>
);
}
......@@ -515,6 +523,7 @@ function CreateField({ fieldDef, value, onChange, variant }) {
variant={variant}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
disabled={disabled}
/>
);
}
......@@ -1058,9 +1067,14 @@ export default function ProblemRectificationListPage() {
[activeGridView, subject],
);
const restrictionDefaults = useMemo(
() => getViewRestrictionDefaults(activeGridView, subject),
[activeGridView, subject],
);
const initialCreateValues = useMemo(
() => buildInitialCreateValues(createFields),
[createFields],
() => buildInitialCreateValues(createFields, restrictionDefaults),
[createFields, restrictionDefaults],
);
useEffect(() => {
......@@ -1103,12 +1117,20 @@ export default function ProblemRectificationListPage() {
}, [showPushModal, selectedRecords]);
const handleCreate = async () => {
if (restrictionDefaults.errors.length > 0) {
setCreateValidationMessage(restrictionDefaults.errors.join('\n'));
return;
}
const hideTitle = shouldHideViewTitleColumn(activeGridView);
if (
!normalizeText(formValues.title) &&
!normalizeText(formValues.project_name) &&
!normalizeText(formValues.problem_brief)
(!hideTitle && !normalizeText(formValues.title) && !normalizeText(formValues.project_name) && !normalizeText(formValues.problem_brief)) ||
(hideTitle && !normalizeText(formValues.project_name) && !normalizeText(formValues.problem_brief))
) {
setCreateValidationMessage('记录标题、检查项目名称、问题简述至少填写一项。');
setCreateValidationMessage(
hideTitle
? '检查项目名称、问题简述至少填写一项。'
: '记录标题、检查项目名称、问题简述至少填写一项。',
);
return;
}
......@@ -1116,7 +1138,7 @@ export default function ProblemRectificationListPage() {
setError('');
try {
const payload = buildProblemRectificationCreatePayload(formValues, createFields);
const payload = buildProblemRectificationCreatePayload(formValues, createFields, restrictionDefaults, { hideTitle });
await createRecord(payload);
setCreateValidationMessage('');
setShowCreateModal(false);
......@@ -2123,6 +2145,7 @@ export default function ProblemRectificationListPage() {
<CreateField
fieldDef={fieldDef}
value={formValues[fieldDef.name]}
disabled={actionLoading || Object.prototype.hasOwnProperty.call(restrictionDefaults.metadata, fieldDef.name)}
variant={createFieldVariant}
onChange={(nextValue) =>
setFormValues((current) => {
......
......@@ -58,7 +58,7 @@
问题整改列表的新增表单、筛选和表格均读取当前激活 Grid View:
- 新增表单按 `settings.columns` 顺序渲染可见字段,并包含记录标题字段;标题是否必填跟随视图列配置,未配置短标题时使用标题作为默认回退值。
- 新增表单按 `settings.columns` 顺序渲染可见字段,并包含记录标题字段;标题是否必填跟随视图列配置,未配置短标题时使用标题作为默认回退值。`settings.shortTitle = true` 时隐藏列表及新增表单的根级记录标题,不校验或提交根级 `title`。
- 筛选控件由视图列的 Subject 字段类型自动生成。`select` 选项来自 `settings.options`,日期、数字、布尔、用户和引用字段使用对应控件;提交时按 `field:<path>` 通过 `/record/query` 远程筛选,不再提供独立关键词搜索。
- 引用和用户多选的输入摘要、列表勾选与草稿统一使用归一化 ID;引用写入只保存目标记录 `_id` 或 `_id[]`。
- 表格请求期间 DataGrid 使用居中的 loading 遮罩,空结果与加载中状态分开处理。
......@@ -70,7 +70,7 @@
- `settings.groups` 决定列表列分组,例如把多个问题分类字段归入同一分组;前端需展示分组表头,同一分组下的列即使在 `settings.columns` 中不连续,也应在表格内聚拢展示,并支持每个分组独立折叠 / 展开,折叠后隐藏该分组子列
- `settings.shortTitle = true` 仅隐藏列表中的 `title` 展示列,不删除记录标题数据,也不改变标题字段的视图排序、详情标题或权限判断;标题列原本承载的部门明细数量 / 展开、延期申请阶段 / 详情和锁定状态迁移到固定的“问题动态”上下文列,详情、延期申请、锁定、复制和删除继续由固定行操作列提供
- `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 Tab 的查看权限后,可以打开该 Tab 中记录的详情并查看 Detail `columns` 配置的字段;Detail 不使用 `viewAuth` 再次隐藏详情字段,详情字段权限只控制是否允许编辑
- `type = "detail"` 的视图用于详情抽屉字段编排,按 `columns` 顺序渲染字段;当前详情视图 `settings.shortTitle = true` 时,同时隐藏弹框 Header 中的记录标题和字段区中的 `title` 字段,未配置或为 `false` 时正常显示。当前 Detail 的 `settings.auth` 或 `settings.disableColumns` 存在有效字段配置时,该 Detail 同时作为详情字段编辑权限来源,切换详情页签后立即按新视图更新展示和权限
......
......@@ -27,6 +27,7 @@ import {
normalizeText,
recordMatchesRestrictions,
sortRecordsByView,
getViewRestrictionDefaults,
} from '@/utils/subjectView';
export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification';
......@@ -1567,19 +1568,20 @@ export function buildProblemRectificationCreateFormFields(gridView, subject) {
: fallbackFields;
}
export function buildInitialCreateValues(createFields) {
export function buildInitialCreateValues(createFields, restrictionDefaults = {}) {
const values = createFields.reduce((nextValues, field) => ({
...nextValues,
[field.fieldDef.name]: emptyValueForField(field.fieldDef),
}), {});
}), { ...(restrictionDefaults.metadata || {}) });
const resolvedValues = { ...values, ...(restrictionDefaults.metadata || {}) };
return {
...values,
problem_severity_level: calculateProblemSeverityLevel(values),
...resolvedValues,
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 fieldName = field.fieldDef?.name;
if (!fieldName) {
......@@ -1599,13 +1601,22 @@ export function buildProblemRectificationCreatePayload(formValues, createFields
const projectName = normalizeText(metadata.project_name);
const problemBrief = normalizeText(metadata.problem_brief);
const recordTitle = normalizeText(formValues.title);
const resolvedTitle = hideTitle
? undefined
: (recordTitle || restrictionDefaults.title || projectName || problemBrief || '问题整改记录');
return {
subject: PROBLEM_RECTIFICATION_SUBJECT,
title: recordTitle || projectName || problemBrief || '问题整改记录',
...(resolvedTitle ? { title: resolvedTitle } : {}),
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 = [])
}, { ...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) {
const sortColumns = toArray(view?.settings?.columns)
.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