refactor: define and compute rules for field interface, field type, and data type

This commit is contained in:
aaaaaajie 2025-06-28 01:45:42 +08:00
parent f6982f5dfe
commit 3a0cb77a49
7 changed files with 136 additions and 198 deletions

View File

@ -43,23 +43,7 @@ const getSchema = (schema: CollectionFieldInterface, record: any, compile) => {
initialValue.reverseField.name = `f_${uid()}`; initialValue.reverseField.name = `f_${uid()}`;
} }
// 基于当前选中的 fieldInterface 构建 fieldType 选项 const fieldTypeOptions = schema.getSecondaryDataTypeOptions();
const fieldTypeOptions = [];
const fieldType = schema.default?.type;
const availableFieldTypes = schema.availableOptions?.all[schema.name][fieldType] || [];
if (fieldType && availableFieldTypes.length > 0) {
fieldTypeOptions.push({
label: fieldType,
value: fieldType,
children: availableFieldTypes.map((availableType) => ({
label: availableType,
value: availableType,
})),
});
}
// 设置 fieldType 默认值为第一组的最深层级路径
if (fieldTypeOptions && fieldTypeOptions.length > 0) { if (fieldTypeOptions && fieldTypeOptions.length > 0) {
const getFirstLeafPath = (options) => { const getFirstLeafPath = (options) => {
const path = []; const path = [];

View File

@ -11,7 +11,7 @@ import { ISchema } from '@formily/react';
import { isArr, isEmpty, isValid } from '@formily/shared'; import { isArr, isEmpty, isValid } from '@formily/shared';
import { registerValidateRules } from '@formily/validator'; import { registerValidateRules } from '@formily/validator';
import { import {
AvailableFieldOptions, AllowedFieldOptions,
CollectionFieldInterface, CollectionFieldInterface,
} from '../../data-source/collection-field-interface/CollectionFieldInterface'; } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { i18n } from '../../i18n'; import { i18n } from '../../i18n';
@ -62,28 +62,10 @@ export class InputFieldInterface extends CollectionFieldInterface {
}, },
}; };
fieldType = 'string'; fieldType = 'string';
availableOptions: AvailableFieldOptions = { allowedOptions: AllowedFieldOptions = {
all: { interfaces: ['textarea'],
input: { types: ['string'],
string: ['varchar', 'char'], dataTypes: ['varchar', 'char'],
},
textarea: {
text: ['text'],
},
},
available: {
input: {
string: {
varchar: ['varchar'],
char: ['varchar', 'char'],
},
},
textarea: {
text: {
text: ['text'],
},
},
},
}; };
availableTypes = ['varchar', 'char']; availableTypes = ['varchar', 'char'];
hasDefaultValue = true; hasDefaultValue = true;
@ -245,4 +227,14 @@ export class InputFieldInterface extends CollectionFieldInterface {
}, },
}; };
} }
getAllowDataTypesBySelected(selectedValue: string): string[] {
if (selectedValue === 'varchar') {
return ['varchar'];
}
if (selectedValue === 'char') {
return ['varchar', 'char'];
}
return [];
}
} }

View File

@ -11,7 +11,7 @@ import { registerValidateFormats } from '@formily/core';
import { i18n } from '../../i18n'; import { i18n } from '../../i18n';
import { defaultProps, operators, unique, autoIncrement, primaryKey } from './properties'; import { defaultProps, operators, unique, autoIncrement, primaryKey } from './properties';
import { import {
AvailableFieldOptions, AllowedFieldOptions,
CollectionFieldInterface, CollectionFieldInterface,
} from '../../data-source/collection-field-interface/CollectionFieldInterface'; } from '../../data-source/collection-field-interface/CollectionFieldInterface';
@ -40,34 +40,10 @@ export class IntegerFieldInterface extends CollectionFieldInterface {
}, },
}; };
availableTypes = ['bigInt', 'integer', 'sort']; availableTypes = ['bigInt', 'integer', 'sort'];
availableOptions: AvailableFieldOptions = { allowedOptions: AllowedFieldOptions = {
all: { interfaces: ['number', 'input'],
integer: { types: ['bigInt'],
bigInt: ['bigInt', 'tinyint', 'integer'], dataTypes: ['bigInt', 'integer', 'tinyint', 'sort'],
},
number: {
double: ['double', 'float', 'decimal'],
},
input: {
string: ['varchar', 'char'],
},
textarea: {
text: ['text'],
},
},
available: {
input: {
string: {
varchar: ['varchar'],
char: ['varchar', 'char'],
},
},
textarea: {
text: {
text: ['text'],
},
},
},
}; };
hasDefaultValue = true; hasDefaultValue = true;
properties = { properties = {
@ -161,4 +137,19 @@ export class IntegerFieldInterface extends CollectionFieldInterface {
}, },
}; };
}; };
getAllowDataTypesBySelected(selectedValue: string): string[] {
if (selectedValue === 'bigInt') {
return ['bigInt', 'sort'];
}
if (selectedValue === 'integer') {
return ['bigInt', 'integer', 'sort'];
}
if (selectedValue === 'sort') {
return ['bigInt', 'integer', 'tinyint', 'sort'];
}
if (selectedValue === 'tinyint') {
return ['bigInt', 'integer', 'tinyint', 'sort'];
}
return [selectedValue];
}
} }

View File

@ -7,7 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; import {
AllowedFieldOptions,
CollectionFieldInterface,
} from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { i18n } from '../../i18n'; import { i18n } from '../../i18n';
import { defaultProps, operators, unique } from './properties'; import { defaultProps, operators, unique } from './properties';
@ -30,6 +33,10 @@ export class NumberFieldInterface extends CollectionFieldInterface {
}, },
}; };
availableTypes = ['double', 'float', 'decimal']; availableTypes = ['double', 'float', 'decimal'];
allowedOptions: AllowedFieldOptions = {
types: ['double'],
dataTypes: ['double', 'float', 'decimal'],
};
hasDefaultValue = true; hasDefaultValue = true;
properties = { properties = {
...defaultProps, ...defaultProps,

View File

@ -11,7 +11,7 @@ import { ISchema } from '@formily/react';
import { i18n } from '../../i18n'; import { i18n } from '../../i18n';
import { defaultProps, operators } from './properties'; import { defaultProps, operators } from './properties';
import { import {
AvailableFieldOptions, AllowedFieldOptions,
CollectionFieldInterface, CollectionFieldInterface,
} from '../../data-source/collection-field-interface/CollectionFieldInterface'; } from '../../data-source/collection-field-interface/CollectionFieldInterface';
@ -30,19 +30,10 @@ export class TextareaFieldInterface extends CollectionFieldInterface {
}, },
}; };
availableTypes = ['text', 'json', 'string']; availableTypes = ['text', 'json', 'string'];
availableOptions: AvailableFieldOptions = { allowedOptions: AllowedFieldOptions = {
all: { interfaces: ['textarea'],
textarea: { types: ['text'],
text: ['text'], dataTypes: ['text'],
},
},
available: {
textarea: {
text: {
text: ['text'],
},
},
},
}; };
hasDefaultValue = true; hasDefaultValue = true;
titleUsable = true; titleUsable = true;
@ -105,4 +96,7 @@ export class TextareaFieldInterface extends CollectionFieldInterface {
filterable = { filterable = {
operators: operators.string, operators: operators.string,
}; };
getAllowDataTypesBySelected(selectedValue: string): string[] {
return ['text'];
}
} }

View File

@ -29,19 +29,10 @@ export interface CollectionFieldInterfaceComponentOption {
type FieldInterfaceName = string; type FieldInterfaceName = string;
type FieldTypeName = string; type FieldTypeName = string;
type FieldDataType = string; type FieldDataType = string;
export interface AvailableFieldOptions { export interface AllowedFieldOptions {
all: { interfaces?: FieldInterfaceName[];
[key: FieldInterfaceName]: { types: FieldTypeName[];
[key: FieldTypeName]: FieldDataType[]; dataTypes: FieldDataType[];
};
};
available: {
[key: FieldInterfaceName]: {
[key: FieldTypeName]: {
[key: FieldDataType]: FieldDataType[];
};
};
};
} }
export abstract class CollectionFieldInterface { export abstract class CollectionFieldInterface {
@ -51,7 +42,7 @@ export abstract class CollectionFieldInterface {
title?: string; title?: string;
description?: string; description?: string;
order?: number; order?: number;
availableOptions?: AvailableFieldOptions; abstract allowedOptions: AllowedFieldOptions;
default?: { default?: {
type: string; type: string;
uiSchema?: ISchema; uiSchema?: ISchema;
@ -193,98 +184,77 @@ export abstract class CollectionFieldInterface {
this.filterable.operators.push(operatorOption); this.filterable.operators.push(operatorOption);
} }
getAllAvailableTypes(): string[] { getAllowDataTypesBySelected(selectedValue: FieldDataType) {
if (!this.availableOptions?.all || !this.name) { return this.allowedOptions?.dataTypes || [];
return [];
}
const interfaceOptions = this.availableOptions.all[this.name];
if (!interfaceOptions) {
return [];
}
const allTypes: string[] = [];
Object.values(interfaceOptions).forEach((typeArray) => {
if (Array.isArray(typeArray)) {
allTypes.push(...typeArray);
}
});
return [...new Set(allTypes)];
} }
getAvailableOptions(options: { getSecondaryDataTypeOptions(): CascaderOptionType[] {
currentValue: [FieldInterfaceName, ...FieldTypeName[], FieldDataType]; return (this.allowedOptions?.types || []).map((type) => {
interfaces: Record<string, CollectionFieldInterface>;
compile: (v: string) => string;
}): CascaderOptionType[] {
const { currentValue, interfaces, compile } = options;
const dataType = currentValue[currentValue.length - 1];
const allAvailableOptions = this.availableOptions?.all || {};
const availableOptions = this.availableOptions?.available || {};
const result = this.buildTree({
all: allAvailableOptions,
available: availableOptions,
dataType,
interfaceMap: interfaces,
compile,
});
return result;
}
private buildTree(options: {
all;
available;
dataType;
compile: (v: string) => string;
interfaceMap?: Record<string, CollectionFieldInterface>;
}) {
const { all, available, dataType, compile, interfaceMap } = options;
if (Array.isArray(all)) {
const allowed = Array.isArray(available?.[dataType]) ? available[dataType] : [];
return all.map((item) => ({
label: item,
value: item,
disabled: !allowed.includes(item),
}));
}
return Object.entries(all).map(([key, value]) => {
const next = available?.[key];
const children = this.buildTree({ all: value, available: next, dataType, compile });
const disabled = Array.isArray(value) ? children.every((child) => child.disabled) : !next;
const label = interfaceMap ? compile(interfaceMap[key]?.title) : key;
return { return {
label, label: type,
value: key, value: type,
disabled, disabled: false,
children, children: this.allowedOptions.dataTypes.map((dataType) => {
return {
label: dataType,
value: dataType,
disabled: false,
};
}),
}; };
}); });
} }
// validateFieldType<T extends AvailableFieldOptions>(value: string): keyof T { getCascaderOptionType(options: {
// const fieldTypes = this.availableOptions[this.name]; dataType: string;
// if (!(value in Object.keys(fieldTypes))) { interfaces: Record<string, CollectionFieldInterface>;
// throw new Error(`Field type "${value}" is not supported by interface "${this.name}".`); compile: (value: string) => string;
// } }): CascaderOptionType[] {
const { dataType: selectedDataType = this.allowedOptions?.dataTypes[0], interfaces, compile } = options;
// return value; const allowedDataTypes = this.getAllowDataTypesBySelected(selectedDataType);
// } const otherOptionTypes = (this.allowedOptions?.interfaces || []).map((allowedInterface) => {
const newInterface = interfaces[allowedInterface];
validateFieldDataType(fieldType: string, value: string) { return {
const fieldTypes = this.availableOptions[this.name]; label: compile(newInterface.title),
if (!fieldTypes || !Array.isArray(fieldTypes)) { value: newInterface.name,
throw new Error(`Field type "${value}" is not supported by interface "${this.name}".`); disabled: false,
} children: (newInterface.allowedOptions?.types || []).map((type) => {
const dataTypes = Object.keys(fieldTypes).find((x) => x === fieldType); return {
if (!dataTypes || !Array.isArray(dataTypes)) { label: type,
throw new Error(`Field type "${dataTypes}" is not supported by interface "${this.name}".`); value: type,
} disabled: false,
const newValue = dataTypes.find((x) => x === value); children: (newInterface.allowedOptions?.dataTypes || []).map((dataType) => {
newValue; return {
label: dataType,
return value; value: dataType,
disabled: false,
};
}),
};
}),
};
});
return [
{
label: compile(this.title),
value: this.name,
disabled: false,
children: (this.allowedOptions?.types || []).map((type) => {
return {
label: type,
value: type,
disabled: false,
children: (this.allowedOptions?.dataTypes || []).map((dataType) => {
return {
label: dataType,
value: dataType,
disabled: !allowedDataTypes.includes(dataType),
};
}),
};
}),
},
...otherOptionTypes,
];
} }
} }

View File

@ -108,14 +108,12 @@ const titlePrompt = 'Default title for each record';
// Field Interface -> Field Type -> Data Type // Field Interface -> Field Type -> Data Type
const createFieldTypeOptions = ( const createFieldTypeOptions = (
currentValue: [string, ...string[], string], dataType: string,
getInterface: (name: string) => CollectionFieldInterface, currentInterface: CollectionFieldInterface,
interfaces: Record<string, CollectionFieldInterface>, interfaces: Record<string, CollectionFieldInterface>,
compile, compile,
) => { ) => {
const [interfaceName] = currentValue; const options = currentInterface.getCascaderOptionType({ dataType, interfaces, compile });
const currentInterface = getInterface(interfaceName);
const options = currentInterface.getAvailableOptions({ currentValue, interfaces, compile });
return options; return options;
}; };
@ -174,6 +172,7 @@ const CurrentFields = (props) => {
}; };
const currentValue: any = []; // [interface, fieldType, dataType] const currentValue: any = []; // [interface, fieldType, dataType]
const currentInterface = getInterface(value);
if (value) { if (value) {
currentValue.push(value); currentValue.push(value);
if (record.fieldType && Array.isArray(record.fieldType) && record.fieldType.length > 0) { if (record.fieldType && Array.isArray(record.fieldType) && record.fieldType.length > 0) {
@ -184,9 +183,9 @@ const CurrentFields = (props) => {
return ( return (
<Cascader <Cascader
value={currentValue.length > 0 ? currentValue : undefined} value={currentValue.length > 0 ? currentValue : undefined}
options={createFieldTypeOptions(currentValue, getInterface, interfaces, compile)} options={createFieldTypeOptions(currentValue[2], currentInterface, interfaces, compile)}
onChange={handleChange} onChange={handleChange}
placeholder={t('Select field type')} placeholder={t('Select field interface')}
expandTrigger="hover" expandTrigger="hover"
changeOnSelect={false} changeOnSelect={false}
style={{ width: '100%', minWidth: 100 }} style={{ width: '100%', minWidth: 100 }}
@ -308,20 +307,21 @@ const InheritFields = (props) => {
title: t('Field name'), title: t('Field name'),
}, },
{ {
dataIndex: 'fieldType', dataIndex: 'interface',
title: t('Field type'), title: t('Field interface'),
render: (value, record) => { render: (value, record) => {
const currentValue: any = []; // [interface, fieldType, dataType] const currentValue: any = []; // [interface, fieldType, dataType]
if (record.interface) { const currentInterface = getInterface(value);
currentValue.push(record.interface); if (value) {
if (value && Array.isArray(value) && value.length > 0) { currentValue.push(value);
currentValue.push(...value); if (value && Array.isArray(record.fieldType) && value.length > 0) {
currentValue.push(...record.fieldType);
} }
} }
return ( return (
<Cascader <Cascader
value={currentValue.length > 0 ? currentValue : undefined} value={currentValue.length > 0 ? currentValue : undefined}
options={createFieldTypeOptions(currentValue, getInterface, interfaces, compile)} options={createFieldTypeOptions(currentValue[2], currentInterface, interfaces, compile)}
placeholder={t('Select field type')} placeholder={t('Select field type')}
expandTrigger="hover" expandTrigger="hover"
changeOnSelect={false} changeOnSelect={false}