Commit 1b7e20fc authored by drigle's avatar drigle

fix inherit view restrictions on record creation

parent d369a505
......@@ -20,6 +20,7 @@ import {
getSelectOptions,
isDateFieldType,
isReferenceFieldType,
getViewRestrictionDefaults,
normalizeProjectYearDisplay,
normalizeValueForSubmit,
toArray,
......@@ -598,29 +599,46 @@ export default function DynamicRecordDialog({
.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('记录标题为必填项');
}
......@@ -635,9 +653,16 @@ 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();
if (titleColumn && title.trim()) {
payload.title = title.trim();
} else if (restrictionDefaults.title !== undefined) {
payload.title = restrictionDefaults.title;
}
await createRecord(payload);
}
await onSaved?.();
......@@ -678,7 +703,7 @@ export default function DynamicRecordDialog({
value={title}
placeholder={`请输入${titleColumn.label || '记录标题'}`}
onChange={(event) => setTitle(event.target.value)}
disabled={saving}
disabled={saving || (!editing && restrictionDefaults.title !== undefined)}
variant="secondary"
/>
</TextField>
......@@ -690,7 +715,9 @@ export default function DynamicRecordDialog({
className={shouldSpanDynamicFieldColumns(field) ? 'md:col-span-2' : undefined}
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,
......
......@@ -36,6 +36,7 @@
4. 编辑模式下主键字段禁用;必填字段按 Subject 配置校验。
5. 新建与编辑弹窗的空控件按可见字段标签显示输入、选择或搜索提示;提示仅用于界面引导,不参与校验,也不写入提交数据。
新建弹窗按当前 Grid View 的列配置决定字段可见性,并按 `settings.groups` 的子字段顺序排列;根记录标题仅在 View 配置了标题列时展示,是否必填由该列配置决定。创建时未填写的可选标题不写入空字符串。`form` 类型字段复用详情侧栏的动态表单编辑器,支持模板、分组和数据行维护,不再以 JSON 长文本输入。
从当前 Grid View 新建记录时,系统同时读取 `settings.restrictions` 中的字段约束;对于 `$eq` 或单值 `$in` 约束,自动把唯一值写入新记录的 `metadata`(约束字段在创建过程中不可修改,即使未配置为弹窗列也会写入)。多值集合或否定比较无法确定唯一归属时阻止创建并提示调整视图配置;编辑已有记录时保留记录原值,不用视图约束覆盖。
6. 删除调用 `POST /record/batch/remove`,默认软删除;列表行内不提供编辑、复制或归档按钮,字段维护统一从详情侧栏进入。
7. 详情弹窗按符合条件的 detail 视图显示字段,并支持字段内联维护;当前 Detail View 的
`settings.shortTitle === true` 时隐藏记录标题和 `title` 字段,但始终保留“记录详情”功能标题和
......
......@@ -488,6 +488,112 @@ 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 the values that a create dialog can safely inherit from a Grid View.
* A view may filter by a set or by a negative comparison, but neither defines a
* single value that can be written to a newly created record.
*/
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