feat: findModelsByBaseClass

This commit is contained in:
chenos 2025-06-28 17:29:11 +08:00
parent 2a306d1813
commit 68b106da04
6 changed files with 193 additions and 17 deletions

View File

@ -162,22 +162,9 @@ QuickEditForm.registerFlow({
}
const collectionField = ctx.model.collection.getField(fieldPath) as CollectionField;
if (collectionField) {
let use = 'EditableFieldModel';
if (collectionField.interface === 'number') {
use = 'InputNumberFieldModel';
}
if (collectionField.interface === 'integer') {
use = 'InputNumberFieldModel';
}
if (collectionField.interface === 'select') {
use = 'SelectFieldModel';
}
if (collectionField.interface === 'textarea') {
use = 'TextareaFieldModel';
}
if (collectionField.interface === 'datetime') {
use = 'DateTimeFieldModel';
}
const FieldModels = collectionField.getMatchFieldModelsByBaseClass('EditableFieldModel');
const use = [...FieldModels.keys()].shift() || 'EditableFieldModel';
console.log('getMatchFieldModelsByBaseClass', FieldModels);
ctx.model.addSubModel('fields', {
use,
stepParams: {

View File

@ -0,0 +1,87 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FlowEngine } from '../flowEngine';
import { FlowModel } from '../models';
class BaseModel extends FlowModel {}
class SubModelA extends BaseModel {}
class SubModelB extends BaseModel {}
class SubModelC extends SubModelA {}
describe('FlowEngine', () => {
let engine: FlowEngine;
beforeEach(() => {
engine = new FlowEngine();
engine.registerModels({
BaseModel,
SubModelA,
SubModelB,
SubModelC,
});
});
it('getModelClass should return correct class', () => {
expect(engine.getModelClass('BaseModel')).toBe(BaseModel);
expect(engine.getModelClass('SubModelA')).toBe(SubModelA);
expect(engine.getModelClass('NotExist')).toBeUndefined();
});
it('findModelsByBaseClass should return all subclasses', () => {
const result = engine.findModelsByBaseClass(BaseModel);
expect(result.has('BaseModel')).toBe(false);
expect(result.has('SubModelA')).toBe(true);
expect(result.has('SubModelB')).toBe(true);
expect(result.has('SubModelC')).toBe(true);
});
it('findModelsByBaseClass should support filter', () => {
const result = engine.findModelsByBaseClass(BaseModel, (ModelClass, name) => name.startsWith('SubModel'));
expect(result.has('BaseModel')).toBe(false);
expect(result.has('SubModelA')).toBe(true);
expect(result.has('SubModelB')).toBe(true);
expect(result.has('SubModelC')).toBe(true);
});
it('findModelClass should find by predicate', () => {
const found = engine.findModelClass((name) => name === 'SubModelB');
expect(found).toBeDefined();
expect(found?.[0]).toBe('SubModelB');
expect(found?.[1]).toBe(SubModelB);
});
it('filterModelClassByParent should return correct subclasses', () => {
const result = engine.filterModelClassByParent(SubModelA);
expect(result.has('SubModelA')).toBe(false);
expect(result.has('SubModelC')).toBe(true);
expect(result.has('BaseModel')).toBe(false);
expect(result.has('SubModelB')).toBe(false);
});
it('registerAction/getAction should work', () => {
engine.registerAction('testAction', { handler: vi.fn(), name: 'testAction' });
const action = engine.getAction('testAction');
expect(action).toBeDefined();
expect(action?.name).toBe('testAction');
});
it('registerModels should overwrite existing model', () => {
class NewBaseModel {}
engine.registerModels({ BaseModel: NewBaseModel });
expect(engine.getModelClass('BaseModel')).toBe(NewBaseModel);
});
it('setContext/getContext should merge and return context', () => {
engine.setContext({ a: 1 });
engine.setContext({ b: 2 });
expect(engine.getContext()).toMatchObject({ a: 1, b: 2 });
});
});

View File

@ -0,0 +1,51 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { describe, expect, it } from 'vitest';
import { isFieldInterfaceMatch } from '../data-source';
describe('isFieldInterfaceMatch', () => {
it('should return true when fieldInterfaces is "*" (string)', () => {
expect(isFieldInterfaceMatch('*', 'a')).toBe(true);
});
it('should return false when fieldInterfaces is a string not matching target', () => {
expect(isFieldInterfaceMatch('b', 'a')).toBe(false);
});
it('should return true when fieldInterfaces is a string matching target', () => {
expect(isFieldInterfaceMatch('a', 'a')).toBe(true);
});
it('should return false when fieldInterfaces is array not containing target', () => {
expect(isFieldInterfaceMatch(['b', 'c'], 'a')).toBe(false);
});
it('should return true when fieldInterfaces is array containing target', () => {
expect(isFieldInterfaceMatch(['a', 'b'], 'a')).toBe(true);
});
it('should return true when fieldInterfaces is array containing "*"', () => {
expect(isFieldInterfaceMatch(['*', 'b'], 'a')).toBe(true);
});
it('should return false when fieldInterfaces is null', () => {
expect(isFieldInterfaceMatch(null, 'a')).toBe(false);
});
it('should return false when fieldInterfaces is undefined', () => {
expect(isFieldInterfaceMatch(undefined, 'a')).toBe(false);
});
it('should return false when targetInterface is empty string', () => {
expect(isFieldInterfaceMatch('a', '')).toBe(false);
expect(isFieldInterfaceMatch(['a', 'b'], '')).toBe(false);
expect(isFieldInterfaceMatch('*', '')).toBe(true); // '*' 匹配任何字符串,包括空字符串
});
});

View File

@ -455,4 +455,32 @@ export class CollectionField {
const app = this.flowEngine.context.app;
return app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface(this.interface);
}
getMatchFieldModelsByBaseClass(baseClass: string) {
return this.flowEngine.findModelsByBaseClass(baseClass, (M, name) => {
console.log('getMatchFieldModelsByBaseClass', name, M['supportedFieldInterfaces'], this.interface);
if (isFieldInterfaceMatch(M['supportedFieldInterfaces'], this.interface)) {
return true;
}
return false;
});
}
}
/**
* fieldInterfaces targetInterface
* @param fieldInterfaces string | string[] | null
* @param targetInterface string
*/
export function isFieldInterfaceMatch(
fieldInterfaces: string | string[] | null | undefined,
targetInterface: string,
): boolean {
if (!fieldInterfaces) return false;
if (fieldInterfaces === '*') return true;
if (typeof fieldInterfaces === 'string') return fieldInterfaces === targetInterface;
if (Array.isArray(fieldInterfaces)) {
return fieldInterfaces.includes('*') || fieldInterfaces.includes(targetInterface);
}
return false;
}

View File

@ -213,6 +213,29 @@ export class FlowEngine {
return undefined;
}
/**
*
* @param {string | ModelConstructor} baseClass
* @param {(ModelClass: ModelConstructor, className: string) => boolean} [filter]
* @returns {Map<string, ModelConstructor>}
*/
public findModelsByBaseClass(
baseClass: string | ModelConstructor,
filter?: (ModelClass: ModelConstructor, className: string) => boolean,
): Map<string, ModelConstructor> {
const parentModelClass = typeof baseClass === 'string' ? this.getModelClass(baseClass) : baseClass;
const result = new Map<string, ModelConstructor>();
if (!parentModelClass) return result;
for (const [className, ModelClass] of this.modelClasses) {
if (isInheritedFrom(ModelClass, parentModelClass)) {
if (!filter || filter(ModelClass, className)) {
result.set(className, ModelClass);
}
}
}
return result;
}
/**
* Model
* UID

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "区块:代码",
"description": "Create code blocks that execute custom JavaScript code with external library loading support.",
"description.zh-CN": "创建代码区块,通过执行自定义 JavaScript 代码进行渲染,支持加载外部库。",
"version": "1.8.0-beta.10",
"version": "1.8.0-alpha.11",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"types": "./dist/index.d.ts",