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 { Plugin } from '@nocobase/client';
import { defineAction, defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine'; import { defineAction, defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button } from 'antd'; import { Button } from 'antd';

View File

@ -38,7 +38,10 @@ const modelMetas = new WeakMap<typeof FlowModel, FlowModelMeta>();
// 使用WeakMap存储每个类的flows // 使用WeakMap存储每个类的flows
const modelFlows = new WeakMap<typeof FlowModel, Map<string, FlowDefinition>>(); 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 readonly uid: string;
public sortIndex: number; public sortIndex: number;
public props: IModelComponentProps = {}; public props: IModelComponentProps = {};
@ -141,12 +144,15 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
* @param {FlowDefinition<TModel>} [flowDefinition] Key * @param {FlowDefinition<TModel>} [flowDefinition] Key
* @returns {void} * @returns {void}
*/ */
public static registerFlow<TModel extends new (...args: any[]) => FlowModel<any>>( public static registerFlow<
this: TModel, TFlowContext extends FlowContext = FlowContext,
keyOrDefinition: string | FlowDefinition<InstanceType<TModel>>, TStructure extends { parent?: any; subModels?: any } = DefaultStructure,
flowDefinition?: Omit<FlowDefinition<InstanceType<TModel>>, 'key'> & { key?: string }, >(
this: FlowModel<TFlowContext, TStructure>,
keyOrDefinition: string | FlowDefinition<TFlowContext>,
flowDefinition?: Omit<FlowDefinition<TFlowContext>, 'key'> & { key?: string },
): void { ): void {
let definition: FlowDefinition<InstanceType<TModel>>; let definition: FlowDefinition<TFlowContext>;
let key: string; let key: string;
if (typeof keyOrDefinition === 'string' && flowDefinition) { 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 * @param {Omit<ExtendedFlowDefinition, 'key'>} [extendDefinition] Key
* @returns {void} * @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, keyOrDefinition: string | ExtendedFlowDefinition,
extendDefinition?: Omit<ExtendedFlowDefinition, 'key'>, extendDefinition?: Omit<ExtendedFlowDefinition, 'key'>,
): void { ): void {
@ -214,7 +224,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
); );
// 移除patch标记作为新流程注册 // 移除patch标记作为新流程注册
const { patch, ...newFlowDef } = definition; const { patch, ...newFlowDef } = definition;
this.registerFlow(newFlowDef as FlowDefinition<TModel>); this.registerFlow(newFlowDef as FlowDefinition<TFlowContext>);
return; return;
} }
@ -222,7 +232,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
const mergedFlow = mergeFlowDefinitions(originalFlow, definition); 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 globalContexts = currentFlowEngine.getContext() || {};
const flowContext: FlowContext<this> = { const flowContext: TFlowContext = {
exit: () => { exit: () => {
throw new FlowExitException(flowKey, this.uid); throw new FlowExitException(flowKey, this.uid);
}, },
@ -383,12 +393,12 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
extra: extra || {}, extra: extra || {},
model: this, model: this,
app: globalContexts.app || {}, app: globalContexts.app || {},
}; } as any;
for (const stepKey in flow.steps) { for (const stepKey in flow.steps) {
if (Object.prototype.hasOwnProperty.call(flow.steps, stepKey)) { if (Object.prototype.hasOwnProperty.call(flow.steps, stepKey)) {
const step: StepDefinition = 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 combinedParams: Record<string, any> = {};
let actionDefinition; let actionDefinition;
@ -478,7 +488,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
* @returns FlowModel * @returns FlowModel
*/ */
public static extends<T extends typeof FlowModel>(this: T, flows: ExtendedFlowDefinition[] = []): T { 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 // @ts-ignore
static name = `CustomFlowModel_${generateUid()}`; static name = `CustomFlowModel_${generateUid()}`;
} }
@ -552,7 +562,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
return <div {...this.props}></div>; return <div {...this.props}></div>;
} }
setParent(parent: FlowModel): void { setParent(parent: FlowModel<FlowContext>): void {
if (!parent || !(parent instanceof FlowModel)) { if (!parent || !(parent instanceof FlowModel)) {
throw new Error('Parent must be an instance of 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; this._options.parentId = parent.uid;
} }
addSubModel(subKey: string, options: CreateModelOptions | FlowModel) { addSubModel(subKey: string, options: CreateModelOptions | FlowModel<FlowContext>) {
let model: FlowModel; let model: FlowModel<FlowContext>;
if (options instanceof FlowModel) { if (options instanceof FlowModel) {
if (options.parent && options.parent !== this) { if (options.parent && options.parent !== this) {
throw new Error('Sub model already has a parent.'); 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; return model;
} }
setSubModel(subKey: string, options: CreateModelOptions | FlowModel) { setSubModel(subKey: string, options: CreateModelOptions | FlowModel<FlowContext>) {
let model: FlowModel; let model: FlowModel<FlowContext>;
if (options instanceof FlowModel) { if (options instanceof FlowModel) {
if (options.parent && options.parent !== this) { if (options.parent && options.parent !== this) {
throw new Error('Sub model already has a parent.'); 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) { for (const subModelKey in this.subModels) {
data.subModels = data.subModels || {}; data.subModels = data.subModels || {};
if (Array.isArray(this.subModels[subModelKey])) { 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(), ...model.serialize(),
sortIndex: index, 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>; 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. * 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 key: string; // Unique identifier for the flow
title?: string; title?: string;
/** /**
@ -56,7 +56,7 @@ export interface FlowDefinition<TModel extends FlowModel = FlowModel> {
on?: { on?: {
eventName: string; eventName: string;
}; };
steps: Record<string, StepDefinition<TModel>>; steps: Record<string, StepDefinition<TFlowContext>>;
} }
// 扩展FlowDefinition类型添加partial标记用于部分覆盖 // 扩展FlowDefinition类型添加partial标记用于部分覆盖
@ -87,7 +87,12 @@ export type ReadonlyModelProps = Readonly<IModelComponentProps>;
/** /**
* Context object passed to handlers during flow execution. * 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 exit: () => void; // Terminate the entire flow execution
logger: { logger: {
info: (message: string, meta?: any) => void; info: (message: string, meta?: any) => void;
@ -96,19 +101,19 @@ export interface FlowContext<TModel extends FlowModel = FlowModel> {
debug: (message: string, meta?: any) => void; debug: (message: string, meta?: any) => void;
}; };
stepResults: Record<string, any>; // Results from previous steps stepResults: Record<string, any>; // Results from previous steps
shared: Record<string, any>; // Shared data within the flow (read/write) shared: TShared; // Shared data within the flow (read/write)
globals: Record<string, any>; // Global context data (read-only) globals: TGlobals; // Global context data (read-only)
extra: Record<string, any>; // Extra context passed to applyFlow (read-only) extra: TExtra; // Extra context passed to applyFlow (read-only)
model: TModel; // Current model instance with specific type model: TModel; // Current model instance with specific type
app: any; // Application instance (required) app: any; // Application instance (required)
} }
export type CreateSubModelOptions = CreateModelOptions | FlowModel; export type CreateSubModelOptions = CreateModelOptions | FlowModel<FlowContext>;
/** /**
* Constructor for model classes. * 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; uid: string;
props?: IModelComponentProps; props?: IModelComponentProps;
stepParams?: StepParams; stepParams?: StepParams;
@ -120,20 +125,18 @@ export type ModelConstructor<T extends FlowModel = FlowModel> = new (options: {
/** /**
* Defines a reusable action with generic model type support. * 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 name: string; // Unique identifier for the action
title?: string; title?: string;
handler: (ctx: FlowContext<TModel>, params: any) => Promise<any> | any; handler: (ctx: TFlowContext, params: any) => Promise<any> | any;
uiSchema?: Record<string, ISchema>; uiSchema?: Record<string, ISchema>;
defaultParams?: defaultParams?: Record<string, any> | ((ctx: TFlowContext) => Record<string, any> | Promise<Record<string, any>>);
| Record<string, any>
| ((ctx: ParamsContext<TModel>) => Record<string, any> | Promise<Record<string, any>>);
} }
/** /**
* Base interface for a step definition with generic model type support. * 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; title?: string;
isAwait?: boolean; // Whether to await the handler, defaults to true 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. * 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 use: string; // Name of the registered ActionDefinition
uiSchema?: Record<string, ISchema>; // Optional: overrides uiSchema from ActionDefinition uiSchema?: Record<string, ISchema>; // Optional: overrides uiSchema from ActionDefinition
defaultParams?: defaultParams?: Record<string, any> | ((ctx: TFlowContext) => Record<string, any> | Promise<Record<string, any>>); // Optional: overrides/extends defaultParams from ActionDefinition
| Record<string, any>
| ((ctx: ParamsContext<TModel>) => 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 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 hideInSettings?: boolean; // Optional: whether to hide the step in the settings menu
// Cannot have its own handler // 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. * Step that defines its handler inline with generic model type support.
*/ */
export interface InlineStepDefinition<TModel extends FlowModel = FlowModel> extends BaseStepDefinition<TModel> { export interface InlineStepDefinition<TFlowContext extends FlowContext = FlowContext>
handler: (ctx: FlowContext<TModel>, params: any) => Promise<any> | any; extends BaseStepDefinition<TFlowContext> {
handler: (ctx: TFlowContext, params: any) => Promise<any> | any;
uiSchema?: Record<string, ISchema>; // Optional: uiSchema for this inline step uiSchema?: Record<string, ISchema>; // Optional: uiSchema for this inline step
defaultParams?: defaultParams?: Record<string, any> | ((ctx: TFlowContext) => Record<string, any> | Promise<Record<string, any>>); // Optional: defaultParams for this inline step
| Record<string, any>
| ((ctx: ParamsContext<TModel>) => 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 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 hideInSettings?: boolean; // Optional: whether to hide the step in the settings menu
// Cannot use a registered action // Cannot use a registered action
use?: undefined; use?: undefined;
} }
export type StepDefinition<TModel extends FlowModel = FlowModel> = export type StepDefinition<TFlowContext extends FlowContext = FlowContext> =
| ActionStepDefinition<TModel> | ActionStepDefinition<TFlowContext>
| InlineStepDefinition<TModel>; | InlineStepDefinition<TFlowContext>;
/** /**
* Extra context for flow execution - represents the data that will appear in ctx.extra * 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 * settings
*/ */
export interface ParamsContext<TModel extends FlowModel = FlowModel> { export interface ParamsContext<TModel extends FlowModel<FlowContext> = FlowModel<FlowContext>> {
model: TModel; model: TModel;
globals: Record<string, any>; globals: Record<string, any>;
app: any; app: any;
@ -190,7 +191,7 @@ export interface ParamsContext<TModel extends FlowModel = FlowModel> {
/** /**
* Action options for registering actions with generic model type support * 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; handler: (ctx: FlowContext<TModel>, params: P) => Promise<R> | R;
uiSchema?: Record<string, any>; uiSchema?: Record<string, any>;
defaultParams?: Partial<P> | ((ctx: ParamsContext<TModel>) => Partial<P> | Promise<Partial<P>>); defaultParams?: Partial<P> | ((ctx: ParamsContext<TModel>) => Partial<P> | Promise<Partial<P>>);
@ -247,7 +248,7 @@ export interface CreateModelOptions {
sortIndex?: number; // 排序索引 sortIndex?: number; // 排序索引
[key: string]: any; // 允许额外的自定义选项 [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>; findOne(query: Record<string, any>): Promise<Record<string, any> | null>;
save(model: T): Promise<Record<string, any>>; save(model: T): Promise<Record<string, any>>;
destroy(uid: string): Promise<boolean>; destroy(uid: string): Promise<boolean>;
@ -273,11 +274,11 @@ export interface RequiredConfigStepFormDialogProps {
dialogTitle?: string; 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 { export interface DefaultStructure {
parent?: any; 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; return options;
} }