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()}`;
}
// 基于当前选中的 fieldInterface 构建 fieldType 选项
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 默认值为第一组的最深层级路径
const fieldTypeOptions = schema.getSecondaryDataTypeOptions();
if (fieldTypeOptions && fieldTypeOptions.length > 0) {
const getFirstLeafPath = (options) => {
const path = [];

View File

@ -11,7 +11,7 @@ import { ISchema } from '@formily/react';
import { isArr, isEmpty, isValid } from '@formily/shared';
import { registerValidateRules } from '@formily/validator';
import {
AvailableFieldOptions,
AllowedFieldOptions,
CollectionFieldInterface,
} from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { i18n } from '../../i18n';
@ -62,28 +62,10 @@ export class InputFieldInterface extends CollectionFieldInterface {
},
};
fieldType = 'string';
availableOptions: AvailableFieldOptions = {
all: {
input: {
string: ['varchar', 'char'],
},
textarea: {
text: ['text'],
},
},
available: {
input: {
string: {
varchar: ['varchar'],
char: ['varchar', 'char'],
},
},
textarea: {
text: {
text: ['text'],
},
},
},
allowedOptions: AllowedFieldOptions = {
interfaces: ['textarea'],
types: ['string'],
dataTypes: ['varchar', 'char'],
};
availableTypes = ['varchar', 'char'];
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 { defaultProps, operators, unique, autoIncrement, primaryKey } from './properties';
import {
AvailableFieldOptions,
AllowedFieldOptions,
CollectionFieldInterface,
} from '../../data-source/collection-field-interface/CollectionFieldInterface';
@ -40,34 +40,10 @@ export class IntegerFieldInterface extends CollectionFieldInterface {
},
};
availableTypes = ['bigInt', 'integer', 'sort'];
availableOptions: AvailableFieldOptions = {
all: {
integer: {
bigInt: ['bigInt', 'tinyint', 'integer'],
},
number: {
double: ['double', 'float', 'decimal'],
},
input: {
string: ['varchar', 'char'],
},
textarea: {
text: ['text'],
},
},
available: {
input: {
string: {
varchar: ['varchar'],
char: ['varchar', 'char'],
},
},
textarea: {
text: {
text: ['text'],
},
},
},
allowedOptions: AllowedFieldOptions = {
interfaces: ['number', 'input'],
types: ['bigInt'],
dataTypes: ['bigInt', 'integer', 'tinyint', 'sort'],
};
hasDefaultValue = true;
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.
*/
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import {
AllowedFieldOptions,
CollectionFieldInterface,
} from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { i18n } from '../../i18n';
import { defaultProps, operators, unique } from './properties';
@ -30,6 +33,10 @@ export class NumberFieldInterface extends CollectionFieldInterface {
},
};
availableTypes = ['double', 'float', 'decimal'];
allowedOptions: AllowedFieldOptions = {
types: ['double'],
dataTypes: ['double', 'float', 'decimal'],
};
hasDefaultValue = true;
properties = {
...defaultProps,

View File

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

View File

@ -29,19 +29,10 @@ export interface CollectionFieldInterfaceComponentOption {
type FieldInterfaceName = string;
type FieldTypeName = string;
type FieldDataType = string;
export interface AvailableFieldOptions {
all: {
[key: FieldInterfaceName]: {
[key: FieldTypeName]: FieldDataType[];
};
};
available: {
[key: FieldInterfaceName]: {
[key: FieldTypeName]: {
[key: FieldDataType]: FieldDataType[];
};
};
};
export interface AllowedFieldOptions {
interfaces?: FieldInterfaceName[];
types: FieldTypeName[];
dataTypes: FieldDataType[];
}
export abstract class CollectionFieldInterface {
@ -51,7 +42,7 @@ export abstract class CollectionFieldInterface {
title?: string;
description?: string;
order?: number;
availableOptions?: AvailableFieldOptions;
abstract allowedOptions: AllowedFieldOptions;
default?: {
type: string;
uiSchema?: ISchema;
@ -193,98 +184,77 @@ export abstract class CollectionFieldInterface {
this.filterable.operators.push(operatorOption);
}
getAllAvailableTypes(): string[] {
if (!this.availableOptions?.all || !this.name) {
return [];
getAllowDataTypesBySelected(selectedValue: FieldDataType) {
return this.allowedOptions?.dataTypes || [];
}
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: {
currentValue: [FieldInterfaceName, ...FieldTypeName[], FieldDataType];
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;
getSecondaryDataTypeOptions(): CascaderOptionType[] {
return (this.allowedOptions?.types || []).map((type) => {
return {
label,
value: key,
disabled,
children,
label: type,
value: type,
disabled: false,
children: this.allowedOptions.dataTypes.map((dataType) => {
return {
label: dataType,
value: dataType,
disabled: false,
};
}),
};
});
}
// validateFieldType<T extends AvailableFieldOptions>(value: string): keyof T {
// const fieldTypes = this.availableOptions[this.name];
// if (!(value in Object.keys(fieldTypes))) {
// throw new Error(`Field type "${value}" is not supported by interface "${this.name}".`);
// }
// return value;
// }
validateFieldDataType(fieldType: string, value: string) {
const fieldTypes = this.availableOptions[this.name];
if (!fieldTypes || !Array.isArray(fieldTypes)) {
throw new Error(`Field type "${value}" is not supported by interface "${this.name}".`);
}
const dataTypes = Object.keys(fieldTypes).find((x) => x === fieldType);
if (!dataTypes || !Array.isArray(dataTypes)) {
throw new Error(`Field type "${dataTypes}" is not supported by interface "${this.name}".`);
}
const newValue = dataTypes.find((x) => x === value);
newValue;
return value;
getCascaderOptionType(options: {
dataType: string;
interfaces: Record<string, CollectionFieldInterface>;
compile: (value: string) => string;
}): CascaderOptionType[] {
const { dataType: selectedDataType = this.allowedOptions?.dataTypes[0], interfaces, compile } = options;
const allowedDataTypes = this.getAllowDataTypesBySelected(selectedDataType);
const otherOptionTypes = (this.allowedOptions?.interfaces || []).map((allowedInterface) => {
const newInterface = interfaces[allowedInterface];
return {
label: compile(newInterface.title),
value: newInterface.name,
disabled: false,
children: (newInterface.allowedOptions?.types || []).map((type) => {
return {
label: type,
value: type,
disabled: false,
children: (newInterface.allowedOptions?.dataTypes || []).map((dataType) => {
return {
label: dataType,
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
const createFieldTypeOptions = (
currentValue: [string, ...string[], string],
getInterface: (name: string) => CollectionFieldInterface,
dataType: string,
currentInterface: CollectionFieldInterface,
interfaces: Record<string, CollectionFieldInterface>,
compile,
) => {
const [interfaceName] = currentValue;
const currentInterface = getInterface(interfaceName);
const options = currentInterface.getAvailableOptions({ currentValue, interfaces, compile });
const options = currentInterface.getCascaderOptionType({ dataType, interfaces, compile });
return options;
};
@ -174,6 +172,7 @@ const CurrentFields = (props) => {
};
const currentValue: any = []; // [interface, fieldType, dataType]
const currentInterface = getInterface(value);
if (value) {
currentValue.push(value);
if (record.fieldType && Array.isArray(record.fieldType) && record.fieldType.length > 0) {
@ -184,9 +183,9 @@ const CurrentFields = (props) => {
return (
<Cascader
value={currentValue.length > 0 ? currentValue : undefined}
options={createFieldTypeOptions(currentValue, getInterface, interfaces, compile)}
options={createFieldTypeOptions(currentValue[2], currentInterface, interfaces, compile)}
onChange={handleChange}
placeholder={t('Select field type')}
placeholder={t('Select field interface')}
expandTrigger="hover"
changeOnSelect={false}
style={{ width: '100%', minWidth: 100 }}
@ -308,20 +307,21 @@ const InheritFields = (props) => {
title: t('Field name'),
},
{
dataIndex: 'fieldType',
title: t('Field type'),
dataIndex: 'interface',
title: t('Field interface'),
render: (value, record) => {
const currentValue: any = []; // [interface, fieldType, dataType]
if (record.interface) {
currentValue.push(record.interface);
if (value && Array.isArray(value) && value.length > 0) {
currentValue.push(...value);
const currentInterface = getInterface(value);
if (value) {
currentValue.push(value);
if (value && Array.isArray(record.fieldType) && value.length > 0) {
currentValue.push(...record.fieldType);
}
}
return (
<Cascader
value={currentValue.length > 0 ? currentValue : undefined}
options={createFieldTypeOptions(currentValue, getInterface, interfaces, compile)}
options={createFieldTypeOptions(currentValue[2], currentInterface, interfaces, compile)}
placeholder={t('Select field type')}
expandTrigger="hover"
changeOnSelect={false}