Commit fb9d7988 authored by drigle's avatar drigle

fix: inherit view restrictions and honor short titles

parent 8789a8c6
......@@ -20,7 +20,9 @@ import {
getSelectOptions,
isDateFieldType,
isReferenceFieldType,
getViewRestrictionDefaults,
normalizeValueForSubmit,
shouldHideViewTitleColumn,
toArray,
} from '@/utils/subjectView';
import { Delete, PlusIcon } from '@/components/AppIcons';
......@@ -45,8 +47,13 @@ function isConfiguredRequired(column) {
}
function getDialogColumns(subject, view) {
const hideTitleColumn = shouldHideViewTitleColumn(view);
const columns = buildViewColumns(view, subject, { includeHidden: true })
.filter((column) => column?.field && !column.hide)
.filter((column) => (
column?.field &&
!column.hide &&
(!hideTitleColumn || column.field !== 'title')
))
.map((column) => column.field === 'title' ? {
...column,
fieldDef: {
......@@ -477,33 +484,50 @@ export default function DynamicRecordDialog({
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const editing = Boolean(record);
const hideTitle = shouldHideViewTitleColumn(view);
const dialogColumns = useMemo(() => getDialogColumns(subject, view), [subject, view]);
const titleColumn = dialogColumns.find((column) => column.field === 'title');
const dialogFields = dialogColumns.filter((column) => column.field !== 'title')
.map((column) => column.fieldDef).filter((field) => field?.name);
const restrictionDefaults = useMemo(
() => getViewRestrictionDefaults(view, subject),
[subject, view],
);
const initialValues = useMemo(() => {
return (subject?.fields || []).reduce((result, field) => {
const defaults = (subject?.fields || []).reduce((result, field) => {
result[field.name] = fieldInitialValue(field, record?.metadata?.[field.name]);
return result;
}, {});
}, [subject, record]);
if (!record) {
Object.entries(restrictionDefaults.metadata).forEach(([fieldName, value]) => {
defaults[fieldName] = value;
});
}
return defaults;
}, [subject, record, restrictionDefaults]);
useEffect(() => {
if (!open) {
return;
}
setTitle(record?.title || record?.metadata?.project_name || '');
setTitle(record?.title || record?.metadata?.project_name || (
!editing ? restrictionDefaults.title ?? '' : ''
));
setValues(initialValues);
setError('');
}, [open, record, initialValues]);
}, [open, record, initialValues, editing, restrictionDefaults]);
const handleSubmit = async () => {
setSaving(true);
setError('');
try {
const prepared = await prepareValuesForSubmit(subject, values);
const validationErrors = prepared.errors;
const validationErrors = [...prepared.errors];
if (!editing && restrictionDefaults.errors.length > 0) {
validationErrors.unshift(...restrictionDefaults.errors);
}
if (titleColumn?.fieldDef?.required && !title.trim()) {
validationErrors.unshift('记录标题为必填项');
}
......@@ -518,9 +542,13 @@ export default function DynamicRecordDialog({
} else {
const payload = {
subject: subjectName,
metadata: prepared.metadata,
metadata: {
...prepared.metadata,
...restrictionDefaults.metadata,
},
};
if (titleColumn && title.trim()) payload.title = title.trim();
else if (!hideTitle && restrictionDefaults.title !== undefined) payload.title = restrictionDefaults.title;
await createRecord(payload);
}
await onSaved?.();
......@@ -542,7 +570,7 @@ export default function DynamicRecordDialog({
title={editing ? '编辑记录' : '新建记录'}
description={editing
? '修改当前记录的字段内容后保存,字段校验将在保存时执行。'
: '填写记录标题和字段内容后保存新记录。'}
: `${hideTitle ? '填写字段内容' : '填写记录标题和字段内容'}后保存新记录。`}
footer={(
<>
<Button isDisabled={saving} variant="secondary" onPress={onClose}>取消</Button>
......@@ -559,6 +587,7 @@ export default function DynamicRecordDialog({
placeholder={`请输入${titleColumn.label || '记录标题'}`}
value={title}
onChange={(event) => setTitle(event.target.value)}
disabled={saving || (!editing && restrictionDefaults.title !== undefined)}
variant="secondary"
/>
</TextField> : null}
......@@ -571,7 +600,9 @@ export default function DynamicRecordDialog({
<DynamicField
fieldDef={field}
value={values[field.name]}
disabled={saving || (editing && isPrimaryField(field))}
disabled={saving || (editing && isPrimaryField(field)) || (
!editing && Object.prototype.hasOwnProperty.call(restrictionDefaults.metadata, field.name)
)}
onChange={(nextValue) => setValues((current) => ({
...current,
[field.name]: nextValue,
......
......@@ -30,9 +30,10 @@
5. 归档页为只读,不提供新建、编辑、复制或删除。
6. 页面请求必须展示加载、成功和错误状态;删除与替换导入必须提供确认。删除确认弹窗
仅展示选中记录数量和软删除说明,不展示记录标题列表。
7. Grid View 的 `settings.shortTitle === true` 时仅隐藏列表 `title` 展示列;Detail View
的同名配置同时隐藏详情 Header 的记录标题和字段区 `title`。`false` 或未配置时正常
显示。该配置不删除标题数据,也不改变标题搜索、排序、筛选、详情身份或保存协议。
7. Grid View 的 `settings.shortTitle === true` 时隐藏列表 `title` 展示列及新建/编辑弹窗中的根级
“记录标题”输入框;此时不校验、不提交根级 `title`。Detail View 的同名配置同时隐藏详情
Header 的记录标题和字段区 `title`。`false` 或未配置时正常显示并保留标题必填逻辑。该配置
不删除已有标题数据,也不改变标题搜索、排序、筛选或详情身份。
8. Subject 配置为多选的选择字段,在新建弹窗和详情编辑中使用可搜索的下拉勾选器;
已选项以标签展示并支持逐项移除或清空,保存值仍为 `settings.options` 中的 value 数组。
9. 列表工具栏默认只显示新建、导入和主要导出等常用入口;展示表的次要导出与归档动作
......@@ -72,7 +73,8 @@
- 列表字段、字段标题和筛选项均从当前 Grid View 配置生成;切换视图会清空当前筛选和勾选状态。
- 筛选控件按视图字段类型生成,引用和用户字段通过远程搜索选择,查询提交后按字段值向 `/record/query` 发送远程 `filter`;列表不再提供关键字搜索。
- 新建记录弹窗只渲染当前视图配置的字段,包含视图显式配置的 `title` 字段;标题是否显示及是否必填遵循视图配置,业务字段仍按 Subject 定义写入 `metadata`。
- 新建记录弹窗只渲染当前视图配置的字段;`settings.shortTitle === true` 时隐藏列表和弹窗中的根级“记录标题”,不校验、不提交根级 `title`,未配置或为 `false` 时保留标题显示和必填逻辑。
- 从当前 Grid View 新建记录时读取 `settings.restrictions`:`$eq` 或单值 `$in` 约束自动写入新记录 `metadata`,对应字段(包括未列入弹窗的字段)不可修改;多值集合、否定比较或同字段冲突无法确定唯一值时禁止创建并提示调整视图。编辑已有记录保留原值,不用视图限制覆盖。
- 列表加载期间 loading 指示器在表格区域居中显示;查询失败保留错误状态,不提交成功反馈。
- Grid View 的 `settings.viewAuth` 只控制视图可见性。普通用户只读取
......
......@@ -534,6 +534,78 @@ export function mergeRestrictionsIntoFilter(baseFilter = {}, restrictions = [])
}, { ...baseFilter });
}
function normalizeRestrictionFieldPath(fieldPath) {
const normalized = normalizeText(fieldPath);
if (!normalized || normalized === 'title' || normalized.startsWith('metadata.')) {
return normalized;
}
return `metadata.${normalized}`;
}
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 unambiguous value for creation. */
export function getViewRestrictionDefaults(view, subject) {
const assignments = new Map();
const unresolvedPaths = 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 = normalizeRestrictionFieldPath(restriction.field);
const fieldDef = getFieldDefinition(fieldPath, subject);
const comparator = normalizeText(restriction.comparator || '$in');
const values = toArray(restriction.value).filter(hasRestrictionValue);
if (!fieldPath || !['$eq', '$in'].includes(comparator) || values.length !== 1) {
if (fieldPath) {
unresolvedPaths.add(fieldPath);
addError(fieldPath, fieldDef);
}
return;
}
if (unresolvedPaths.has(fieldPath)) return;
let value = normalizeValueForSubmit(values[0], fieldDef);
if (fieldDef?.multiple) value = toArray(value);
const existing = assignments.get(fieldPath);
if (assignments.has(fieldPath) && !restrictionValuesEqual(existing, value)) {
assignments.delete(fieldPath);
unresolvedPaths.add(fieldPath);
addError(fieldPath, fieldDef);
return;
}
assignments.set(fieldPath, value);
});
const metadata = {};
let title;
assignments.forEach((value, fieldPath) => {
if (unresolvedPaths.has(fieldPath)) return;
if (fieldPath === 'title') {
title = value;
return;
}
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