feat: enhance FlowModel and FlowContext with generic type support

This commit is contained in:
Zeke Zhang 2025-06-20 15:47:28 +08:00
parent 5b70b68383
commit f3353b1a4e
5 changed files with 107 additions and 53 deletions

View File

@ -1,4 +1,3 @@
import * as icons from '@ant-design/icons';
import { Plugin } from '@nocobase/client';
import { defineAction, defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button } from 'antd';

View File

@ -38,7 +38,10 @@ const modelMetas = new WeakMap<typeof FlowModel, FlowModelMeta>();
// 使用WeakMap存储每个类的flows
const modelFlows = new WeakMap<typeof FlowModel, Map<string, FlowDefinition>>();
export class FlowModel<Structure extends { parent?: any; subModels?: any } = DefaultStructure> {
export class FlowModel<
TFlowContext extends FlowContext = FlowContext,
Structure extends { parent?: any; subModels?: any } = DefaultStructure,
> {
public readonly uid: string;
public sortIndex: number;
public props: IModelComponentProps = {};
@ -141,12 +144,15 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
* @param {FlowDefinition<TModel>} [flowDefinition] Key
* @returns {void}
*/
public static registerFlow<TModel extends new (...args: any[]) => FlowModel<any>>(
this: TModel,
keyOrDefinition: string | FlowDefinition<InstanceType<TModel>>,
flowDefinition?: Omit<FlowDefinition<InstanceType<TModel>>, 'key'> & { key?: string },
public static registerFlow<
TFlowContext extends FlowContext = FlowContext,
TStructure extends { parent?: any; subModels?: any } = DefaultStructure,
>(
this: FlowModel<TFlowContext, TStructure>,
keyOrDefinition: string | FlowDefinition<TFlowContext>,
flowDefinition?: Omit<FlowDefinition<TFlowContext>, 'key'> & { key?: string },
): void {
let definition: FlowDefinition<InstanceType<TModel>>;
let definition: FlowDefinition<TFlowContext>;
let key: string;
if (typeof keyOrDefinition === 'string' && flowDefinition) {
@ -184,7 +190,11 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
* @param {Omit<ExtendedFlowDefinition, 'key'>} [extendDefinition] Key
* @returns {void}
*/
public static extendFlow<TModel extends FlowModel = FlowModel>(
public static extendFlow<
TFlowContext extends FlowContext = FlowContext,
TStructure extends { parent?: any; subModels?: any } = DefaultStructure,
>(
this: FlowModel<TFlowContext, TStructure>,
keyOrDefinition: string | ExtendedFlowDefinition,
extendDefinition?: Omit<ExtendedFlowDefinition, 'key'>,
): void {
@ -214,7 +224,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
);
// 移除patch标记作为新流程注册
const { patch, ...newFlowDef } = definition;
this.registerFlow(newFlowDef as FlowDefinition<TModel>);
this.registerFlow(newFlowDef as FlowDefinition<TFlowContext>);
return;
}
@ -222,7 +232,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
const mergedFlow = mergeFlowDefinitions(originalFlow, definition);
// 注册合并后的流程
this.registerFlow(mergedFlow as FlowDefinition<TModel>);
this.registerFlow(mergedFlow as FlowDefinition<TFlowContext>);
}
/**
@ -367,7 +377,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
};
const globalContexts = currentFlowEngine.getContext() || {};
const flowContext: FlowContext<this> = {
const flowContext: TFlowContext = {
exit: () => {
throw new FlowExitException(flowKey, this.uid);
},
@ -383,12 +393,12 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
extra: extra || {},
model: this,
app: globalContexts.app || {},
};
} as any;
for (const stepKey in flow.steps) {
if (Object.prototype.hasOwnProperty.call(flow.steps, stepKey)) {
const step: StepDefinition = flow.steps[stepKey];
let handler: ((ctx: FlowContext<this>, params: any) => Promise<any> | any) | undefined;
let handler: ((ctx: TFlowContext, params: any) => Promise<any> | any) | undefined;
let combinedParams: Record<string, any> = {};
let actionDefinition;
@ -478,7 +488,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
* @returns FlowModel
*/
public static extends<T extends typeof FlowModel>(this: T, flows: ExtendedFlowDefinition[] = []): T {
class CustomFlowModel extends (this as unknown as typeof FlowModel) {
class CustomFlowModel extends (this as unknown as typeof FlowModel<FlowContext>) {
// @ts-ignore
static name = `CustomFlowModel_${generateUid()}`;
}
@ -552,7 +562,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
return <div {...this.props}></div>;
}
setParent(parent: FlowModel): void {
setParent(parent: FlowModel<FlowContext>): void {
if (!parent || !(parent instanceof FlowModel)) {
throw new Error('Parent must be an instance of FlowModel.');
}
@ -560,8 +570,8 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
this._options.parentId = parent.uid;
}
addSubModel(subKey: string, options: CreateModelOptions | FlowModel) {
let model: FlowModel;
addSubModel(subKey: string, options: CreateModelOptions | FlowModel<FlowContext>) {
let model: FlowModel<FlowContext>;
if (options instanceof FlowModel) {
if (options.parent && options.parent !== this) {
throw new Error('Sub model already has a parent.');
@ -578,8 +588,8 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
return model;
}
setSubModel(subKey: string, options: CreateModelOptions | FlowModel) {
let model: FlowModel;
setSubModel(subKey: string, options: CreateModelOptions | FlowModel<FlowContext>) {
let model: FlowModel<FlowContext>;
if (options instanceof FlowModel) {
if (options.parent && options.parent !== this) {
throw new Error('Sub model already has a parent.');
@ -771,7 +781,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
for (const subModelKey in this.subModels) {
data.subModels = data.subModels || {};
if (Array.isArray(this.subModels[subModelKey])) {
data.subModels[subModelKey] = this.subModels[subModelKey].map((model: FlowModel, index) => ({
data.subModels[subModelKey] = this.subModels[subModelKey].map((model: FlowModel<FlowContext>, index) => ({
...model.serialize(),
sortIndex: index,
}));
@ -783,6 +793,8 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
}
}
export function defineFlow<TModel extends FlowModel = FlowModel>(definition: FlowDefinition): FlowDefinition<TModel> {
export function defineFlow<TModel extends FlowModel<FlowContext> = FlowModel<FlowContext>>(
definition: FlowDefinition,
): FlowDefinition<TModel> {
return definition as FlowDefinition<TModel>;
}

View File

@ -0,0 +1,42 @@
/**
* 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 { FlowContext } from '../types';
import { defineAction } from '../utils';
import { FlowModel } from './flowModel';
class TypeDemoModel extends FlowModel<FlowContext<TypeDemoModel, { abc: string }, { def: string }, { ghi: string }>> {
injectedValue: {
abc: string;
};
render() {
return null;
}
}
const demoAction = defineAction<FlowContext<TypeDemoModel, { abc: string }, { def: string }, { ghi: string }>>({
name: 'demoAction',
title: 'Demo Action',
uiSchema: {},
handler(ctx, params) {},
});
TypeDemoModel.registerFlow({
key: 'typeDemo',
title: 'Type Demo',
on: {
eventName: 'click',
},
steps: {
step1: {
handler(ctx) {},
},
},
});

View File

@ -38,7 +38,7 @@ export type DeepPartial<T> = {
/**
* Defines a flow with generic model type support.
*/
export interface FlowDefinition<TModel extends FlowModel = FlowModel> {
export interface FlowDefinition<TFlowContext extends FlowContext = FlowContext> {
key: string; // Unique identifier for the flow
title?: string;
/**
@ -56,7 +56,7 @@ export interface FlowDefinition<TModel extends FlowModel = FlowModel> {
on?: {
eventName: string;
};
steps: Record<string, StepDefinition<TModel>>;
steps: Record<string, StepDefinition<TFlowContext>>;
}
// 扩展FlowDefinition类型添加partial标记用于部分覆盖
@ -87,7 +87,12 @@ export type ReadonlyModelProps = Readonly<IModelComponentProps>;
/**
* Context object passed to handlers during flow execution.
*/
export interface FlowContext<TModel extends FlowModel = FlowModel> {
export interface FlowContext<
TModel extends FlowModel<FlowContext> = FlowModel<any>,
TExtra extends FlowExtraContext = FlowExtraContext,
TShared extends Record<string, any> = Record<string, any>,
TGlobals extends Record<string, any> = Record<string, any>,
> {
exit: () => void; // Terminate the entire flow execution
logger: {
info: (message: string, meta?: any) => void;
@ -96,19 +101,19 @@ export interface FlowContext<TModel extends FlowModel = FlowModel> {
debug: (message: string, meta?: any) => void;
};
stepResults: Record<string, any>; // Results from previous steps
shared: Record<string, any>; // Shared data within the flow (read/write)
globals: Record<string, any>; // Global context data (read-only)
extra: Record<string, any>; // Extra context passed to applyFlow (read-only)
shared: TShared; // Shared data within the flow (read/write)
globals: TGlobals; // Global context data (read-only)
extra: TExtra; // Extra context passed to applyFlow (read-only)
model: TModel; // Current model instance with specific type
app: any; // Application instance (required)
}
export type CreateSubModelOptions = CreateModelOptions | FlowModel;
export type CreateSubModelOptions = CreateModelOptions | FlowModel<FlowContext>;
/**
* Constructor for model classes.
*/
export type ModelConstructor<T extends FlowModel = FlowModel> = new (options: {
export type ModelConstructor<T extends FlowModel<FlowContext> = FlowModel<FlowContext>> = new (options: {
uid: string;
props?: IModelComponentProps;
stepParams?: StepParams;
@ -120,20 +125,18 @@ export type ModelConstructor<T extends FlowModel = FlowModel> = new (options: {
/**
* Defines a reusable action with generic model type support.
*/
export interface ActionDefinition<TModel extends FlowModel = FlowModel> {
export interface ActionDefinition<TFlowContext extends FlowContext = FlowContext> {
name: string; // Unique identifier for the action
title?: string;
handler: (ctx: FlowContext<TModel>, params: any) => Promise<any> | any;
handler: (ctx: TFlowContext, params: any) => Promise<any> | any;
uiSchema?: Record<string, ISchema>;
defaultParams?:
| Record<string, any>
| ((ctx: ParamsContext<TModel>) => Record<string, any> | Promise<Record<string, any>>);
defaultParams?: Record<string, any> | ((ctx: TFlowContext) => Record<string, any> | Promise<Record<string, any>>);
}
/**
* Base interface for a step definition with generic model type support.
*/
interface BaseStepDefinition<TModel extends FlowModel = FlowModel> {
interface BaseStepDefinition<TFlowContext extends FlowContext = FlowContext> {
title?: string;
isAwait?: boolean; // Whether to await the handler, defaults to true
}
@ -141,12 +144,11 @@ interface BaseStepDefinition<TModel extends FlowModel = FlowModel> {
/**
* Step that uses a registered Action with generic model type support.
*/
export interface ActionStepDefinition<TModel extends FlowModel = FlowModel> extends BaseStepDefinition<TModel> {
export interface ActionStepDefinition<TFlowContext extends FlowContext = FlowContext>
extends BaseStepDefinition<TFlowContext> {
use: string; // Name of the registered ActionDefinition
uiSchema?: Record<string, ISchema>; // Optional: overrides uiSchema from ActionDefinition
defaultParams?:
| Record<string, any>
| ((ctx: ParamsContext<TModel>) => Record<string, any> | Promise<Record<string, any>>); // Optional: overrides/extends defaultParams from ActionDefinition
defaultParams?: Record<string, any> | ((ctx: TFlowContext) => Record<string, any> | Promise<Record<string, any>>); // Optional: overrides/extends defaultParams from ActionDefinition
paramsRequired?: boolean; // Optional: whether the step params are required, will open the config dialog before adding the model
hideInSettings?: boolean; // Optional: whether to hide the step in the settings menu
// Cannot have its own handler
@ -156,21 +158,20 @@ export interface ActionStepDefinition<TModel extends FlowModel = FlowModel> exte
/**
* Step that defines its handler inline with generic model type support.
*/
export interface InlineStepDefinition<TModel extends FlowModel = FlowModel> extends BaseStepDefinition<TModel> {
handler: (ctx: FlowContext<TModel>, params: any) => Promise<any> | any;
export interface InlineStepDefinition<TFlowContext extends FlowContext = FlowContext>
extends BaseStepDefinition<TFlowContext> {
handler: (ctx: TFlowContext, params: any) => Promise<any> | any;
uiSchema?: Record<string, ISchema>; // Optional: uiSchema for this inline step
defaultParams?:
| Record<string, any>
| ((ctx: ParamsContext<TModel>) => Record<string, any> | Promise<Record<string, any>>); // Optional: defaultParams for this inline step
defaultParams?: Record<string, any> | ((ctx: TFlowContext) => Record<string, any> | Promise<Record<string, any>>); // Optional: defaultParams for this inline step
paramsRequired?: boolean; // Optional: whether the step params are required, will open the config dialog before adding the model
hideInSettings?: boolean; // Optional: whether to hide the step in the settings menu
// Cannot use a registered action
use?: undefined;
}
export type StepDefinition<TModel extends FlowModel = FlowModel> =
| ActionStepDefinition<TModel>
| InlineStepDefinition<TModel>;
export type StepDefinition<TFlowContext extends FlowContext = FlowContext> =
| ActionStepDefinition<TFlowContext>
| InlineStepDefinition<TFlowContext>;
/**
* Extra context for flow execution - represents the data that will appear in ctx.extra
@ -181,7 +182,7 @@ export type FlowExtraContext = Record<string, any>;
/**
* settings
*/
export interface ParamsContext<TModel extends FlowModel = FlowModel> {
export interface ParamsContext<TModel extends FlowModel<FlowContext> = FlowModel<FlowContext>> {
model: TModel;
globals: Record<string, any>;
app: any;
@ -190,7 +191,7 @@ export interface ParamsContext<TModel extends FlowModel = FlowModel> {
/**
* Action options for registering actions with generic model type support
*/
export interface ActionOptions<TModel extends FlowModel = FlowModel, P = any, R = any> {
export interface ActionOptions<TModel extends FlowModel<FlowContext> = FlowModel<FlowContext>, P = any, R = any> {
handler: (ctx: FlowContext<TModel>, params: P) => Promise<R> | R;
uiSchema?: Record<string, any>;
defaultParams?: Partial<P> | ((ctx: ParamsContext<TModel>) => Partial<P> | Promise<Partial<P>>);
@ -247,7 +248,7 @@ export interface CreateModelOptions {
sortIndex?: number; // 排序索引
[key: string]: any; // 允许额外的自定义选项
}
export interface IFlowModelRepository<T extends FlowModel = FlowModel> {
export interface IFlowModelRepository<T extends FlowModel<FlowContext> = FlowModel<FlowContext>> {
findOne(query: Record<string, any>): Promise<Record<string, any> | null>;
save(model: T): Promise<Record<string, any>>;
destroy(uid: string): Promise<boolean>;
@ -273,11 +274,11 @@ export interface RequiredConfigStepFormDialogProps {
dialogTitle?: string;
}
export type SubModelValue<TModel extends FlowModel = FlowModel> = TModel | TModel[];
export type SubModelValue<TModel extends FlowModel<FlowContext> = FlowModel<FlowContext>> = TModel | TModel[];
export interface DefaultStructure {
parent?: any;
subModels?: Record<string, FlowModel | FlowModel[]>;
subModels?: Record<string, FlowModel<FlowContext> | FlowModel<FlowContext>[]>;
}
/**

View File

@ -119,6 +119,6 @@ export class FlowExitException extends Error {
}
}
export function defineAction(options: ActionDefinition) {
export function defineAction<TFlowContext extends FlowContext>(options: ActionDefinition<TFlowContext>) {
return options;
}