This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { FormPathService } from '../src/form/services/form-path-service';
|
||||
|
||||
describe('FormPathService', () => {
|
||||
it('should parse array item path correctly', () => {
|
||||
const path = 'a/b/2/description';
|
||||
const result = FormPathService.parseArrayItemPath(path);
|
||||
|
||||
expect(result).toEqual({
|
||||
itemIndex: 2,
|
||||
arrayPath: 'a/b/[]',
|
||||
itemMetaPath: 'a/b/[]/description',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle non-numeric item index correctly', () => {
|
||||
const path = 'a/b';
|
||||
const result = FormPathService.parseArrayItemPath(path);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle empty path correctly', () => {
|
||||
const path = '';
|
||||
const result = FormPathService.parseArrayItemPath(path);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('form test', () => {
|
||||
it('form test', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('form test');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
const { defineFlatConfig } = require('@flowgram.ai/eslint-config');
|
||||
|
||||
module.exports = defineFlatConfig({
|
||||
preset: 'web',
|
||||
packageRoot: __dirname,
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@flowgram.ai/form-core",
|
||||
"version": "0.1.8",
|
||||
"description": "automation form core",
|
||||
"keywords": [
|
||||
"flow",
|
||||
"engine"
|
||||
],
|
||||
"homepage": "https://flowgram.ai/",
|
||||
"repository": "https://github.com/bytedance/flowgram.ai",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/esm/index.js",
|
||||
"require": "./dist/index.js"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build:fast -- --dts-resolve",
|
||||
"build:fast": "tsup src/index.ts --format cjs,esm --sourcemap --legacy-output",
|
||||
"build:watch": "npm run build:fast -- --dts-resolve",
|
||||
"clean": "rimraf dist",
|
||||
"test": "vitest run",
|
||||
"test:cov": "vitest run --coverage",
|
||||
"ts-check": "tsc --noEmit",
|
||||
"watch": "npm run build:fast -- --dts-resolve --watch --ignore-watch dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@flowgram.ai/core": "workspace:*",
|
||||
"@flowgram.ai/document": "workspace:*",
|
||||
"@flowgram.ai/utils": "workspace:*",
|
||||
"inversify": "^6.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"reflect-metadata": "~0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@flowgram.ai/eslint-config": "workspace:*",
|
||||
"@flowgram.ai/ts-config": "workspace:*",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"eslint": "^9.0.0",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { NodeContainerModule } from '../node/node-container-module';
|
||||
import { FormCoreContainerModule } from '../form';
|
||||
import { ErrorContainerModule } from '../error';
|
||||
|
||||
export function createNodeContainerModules() {
|
||||
return [NodeContainerModule, FormCoreContainerModule, ErrorContainerModule];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { EntityDataRegistry } from '@flowgram.ai/core';
|
||||
|
||||
import { FlowNodeFormData } from '../form';
|
||||
import { FlowNodeErrorData } from '../error';
|
||||
|
||||
export function createNodeEntityDatas(): EntityDataRegistry[] {
|
||||
return [FlowNodeFormData, FlowNodeErrorData];
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './create-node-container-modules';
|
||||
export * from './node-render';
|
||||
export * from './node-material-client';
|
||||
export * from './create-node-entity-datas';
|
||||
export * from '../form/client';
|
||||
export * from '../error/client';
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { MATERIAL_KEY } from '../node/core-materials';
|
||||
import { NodeManager, NodePlaceholderRender, Render } from '../node';
|
||||
|
||||
export function registerNodeErrorRender(nodeManager: NodeManager, render: Render): void {
|
||||
nodeManager.registerMaterialRender(MATERIAL_KEY.NODE_ERROR_RENDER, render);
|
||||
}
|
||||
|
||||
export function registerNodePlaceholderRender(
|
||||
nodeManager: NodeManager,
|
||||
render: NodePlaceholderRender,
|
||||
): void {
|
||||
nodeManager.registerMaterialRender(MATERIAL_KEY.NODE_PLACEHOLDER_RENDER, render);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback, useEffect } from 'react';
|
||||
|
||||
import { PlaygroundContext, useRefresh, useService, PluginContext } from '@flowgram.ai/core';
|
||||
|
||||
import { NodeEngineReactContext } from '../node-react/context/node-engine-react-context';
|
||||
import { useNodeEngineContext } from '../node-react';
|
||||
import { NodeManager } from '../node/node-manager';
|
||||
import { PLUGIN_KEY } from '../node/core-plugins';
|
||||
import { MATERIAL_KEY, type NodeRenderProps } from '../node';
|
||||
import { getFormModel, isNodeFormReady } from '../form';
|
||||
import { FlowNodeErrorData } from '../error/flow-node-error-data';
|
||||
import { getNodeError } from '../error';
|
||||
|
||||
const PureNodeRender = ({ node }: NodeRenderProps) => {
|
||||
const refresh = useRefresh();
|
||||
const nodeErrorData = node.getData<FlowNodeErrorData>(FlowNodeErrorData);
|
||||
const formModel = getFormModel(node);
|
||||
const isNodeError = !!getNodeError(node);
|
||||
const isFormReady = isNodeFormReady(node);
|
||||
const playgroundContext = useService<PlaygroundContext>(PlaygroundContext);
|
||||
const clientContext = useService<PluginContext>(PluginContext);
|
||||
const nodeManager = useService<NodeManager>(NodeManager);
|
||||
const nodeFormRender = nodeManager.getPluginRender(PLUGIN_KEY.FORM);
|
||||
const nodeErrorRender = nodeManager.getPluginRender(PLUGIN_KEY.ERROR);
|
||||
const nodePlaceholderRender = nodeManager.getMaterialRender(MATERIAL_KEY.NODE_PLACEHOLDER_RENDER);
|
||||
|
||||
const nodeEngineContext = useNodeEngineContext();
|
||||
|
||||
useEffect(() => {
|
||||
const errorDisposable = nodeErrorData.onDataChange(() => {
|
||||
refresh();
|
||||
});
|
||||
const formDisposable = formModel.onInitialized(() => {
|
||||
refresh();
|
||||
});
|
||||
return () => {
|
||||
errorDisposable.dispose();
|
||||
formDisposable.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
if (isNodeError) {
|
||||
return nodeErrorRender!({ node, playgroundContext, clientContext });
|
||||
}
|
||||
if (!formModel.formMeta) {
|
||||
return null;
|
||||
}
|
||||
if (isFormReady) {
|
||||
return nodeFormRender!({ node, playgroundContext, clientContext });
|
||||
}
|
||||
return nodePlaceholderRender?.({ node, playgroundContext }) || null;
|
||||
}, [
|
||||
isNodeError,
|
||||
isFormReady,
|
||||
nodeErrorRender,
|
||||
nodeFormRender,
|
||||
nodePlaceholderRender,
|
||||
node,
|
||||
playgroundContext,
|
||||
]);
|
||||
|
||||
return (
|
||||
<NodeEngineReactContext.Provider value={nodeEngineContext.json}>
|
||||
{nodeManager.nodeRenderHoc(renderContent)()}
|
||||
</NodeEngineReactContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const NodeRender = memo(PureNodeRender);
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
|
||||
import { FlowNodeErrorData } from './flow-node-error-data';
|
||||
|
||||
export function getNodeError(node: FlowNodeEntity) {
|
||||
return node.getData<FlowNodeErrorData>(FlowNodeErrorData).getError();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { bindContributions } from '@flowgram.ai/utils';
|
||||
|
||||
import { NodeContribution } from '../node';
|
||||
import { ErrorNodeContribution } from './error-node-contribution';
|
||||
|
||||
export const ErrorContainerModule = new ContainerModule(bind => {
|
||||
bindContributions(bind, ErrorNodeContribution, [NodeContribution]);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
|
||||
import { NodeContribution } from '../node';
|
||||
import { NodeManager, PLUGIN_KEY } from '../node';
|
||||
import { errorPluginRender } from './renders';
|
||||
|
||||
@injectable()
|
||||
export class ErrorNodeContribution implements NodeContribution {
|
||||
onRegister(nodeManager: NodeManager) {
|
||||
nodeManager.registerPluginRender(PLUGIN_KEY.ERROR, errorPluginRender);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { EntityData } from '@flowgram.ai/core';
|
||||
|
||||
export interface ErrorData {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class FlowNodeErrorData extends EntityData {
|
||||
static type = 'FlowNodeErrorData';
|
||||
|
||||
getDefaultData(): ErrorData {
|
||||
return { error: null };
|
||||
}
|
||||
|
||||
setError(e: ErrorData['error']) {
|
||||
this.update({ error: e });
|
||||
}
|
||||
|
||||
getError(): Error {
|
||||
return this.data.error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './flow-node-error-data';
|
||||
export * from './types';
|
||||
export * from './error-container-module';
|
||||
export * from './client';
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { NodeErrorRenderProps } from '../types';
|
||||
|
||||
const ERROR_STYLE = {
|
||||
color: '#f54a45',
|
||||
};
|
||||
|
||||
export const defaultErrorRender = ({ error }: NodeErrorRenderProps) => (
|
||||
<div style={ERROR_STYLE}>{error.message}</div>
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { PlaygroundContext, useRefresh, useService, PluginContext } from '@flowgram.ai/core';
|
||||
|
||||
import { FlowNodeErrorData } from '../flow-node-error-data';
|
||||
import { MATERIAL_KEY, NodeManager, NodePluginRender } from '../../node';
|
||||
import { defaultErrorRender } from './default-error-render';
|
||||
|
||||
interface NodeRenderProps {
|
||||
node: FlowNodeEntity;
|
||||
playgroundContext: PlaygroundContext;
|
||||
clientContext: PluginContext;
|
||||
}
|
||||
|
||||
export const ErrorRender = ({ node, playgroundContext, clientContext }: NodeRenderProps) => {
|
||||
const refresh = useRefresh();
|
||||
const nodeErrorData = node.getData<FlowNodeErrorData>(FlowNodeErrorData);
|
||||
const nodeError = nodeErrorData.getError();
|
||||
const nodeManager = useService<NodeManager>(NodeManager);
|
||||
const nodeErrorRender = nodeManager.getMaterialRender(MATERIAL_KEY.NODE_ERROR_RENDER);
|
||||
|
||||
const renderError = useCallback(() => {
|
||||
if (!nodeErrorRender) {
|
||||
return defaultErrorRender({
|
||||
error: nodeError,
|
||||
context: { node, playgroundContext, clientContext },
|
||||
});
|
||||
}
|
||||
return nodeErrorRender({
|
||||
error: nodeError,
|
||||
context: { node, playgroundContext, clientContext },
|
||||
});
|
||||
}, [nodeError, node, playgroundContext, clientContext]);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = nodeErrorData.onDataChange(() => {
|
||||
refresh();
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return nodeError ? renderError() : null;
|
||||
};
|
||||
|
||||
export const errorPluginRender: NodePluginRender = (props) => <ErrorRender {...props} />;
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './error-render';
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { NodeContext, Render } from '../node';
|
||||
|
||||
export interface NodeErrorRenderProps {
|
||||
error: Error;
|
||||
context: NodeContext;
|
||||
}
|
||||
|
||||
export type NodeErrorRender = Render<NodeErrorRenderProps>;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormItemAbility } from '../../models/form-item-ability';
|
||||
|
||||
export class DecoratorAbility implements FormItemAbility {
|
||||
static readonly type = 'decorator';
|
||||
|
||||
get type(): string {
|
||||
return DecoratorAbility.type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './decorator-ability';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormItemContext, FormItemFeedback } from '../../types';
|
||||
import { SetterOrDecoratorContext } from '../../abilities/setter-ability';
|
||||
|
||||
export interface DecoratorAbilityOptions {
|
||||
/**
|
||||
* 已注册的decorator的唯一标识
|
||||
*/
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface DecoratorComponentProps<CustomOptions = any>
|
||||
extends FormItemFeedback,
|
||||
FormItemContext {
|
||||
readonly: boolean;
|
||||
children?: any;
|
||||
options: DecoratorAbilityOptions & CustomOptions;
|
||||
context: SetterOrDecoratorContext;
|
||||
}
|
||||
|
||||
export interface DecoratorExtension {
|
||||
key: string;
|
||||
component: (props: DecoratorComponentProps) => any;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormItemAbility } from '../../models/form-item-ability';
|
||||
|
||||
export class DefaultAbility implements FormItemAbility {
|
||||
static readonly type = 'default';
|
||||
|
||||
get type(): string {
|
||||
return DefaultAbility.type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './default-ability';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormItemContext, FormItemMaterialContext } from '../..';
|
||||
|
||||
export interface GetDefaultValueProps extends FormItemContext {
|
||||
options: DefaultAbilityOptions;
|
||||
context: FormItemMaterialContext;
|
||||
}
|
||||
|
||||
export interface DefaultAbilityOptions<T = any> {
|
||||
getDefaultValue: (params: GetDefaultValueProps) => T;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormItemAbility } from '../../models/form-item-ability';
|
||||
|
||||
export class EffectAbility implements FormItemAbility {
|
||||
static readonly type = 'effect';
|
||||
|
||||
get type(): string {
|
||||
return EffectAbility.type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './effect-ability';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormItemContext, FormItemEventName } from '../../types';
|
||||
import { type FormItemMaterialContext } from '../../models/form-item-material-context';
|
||||
|
||||
export interface EffectAbilityOptions {
|
||||
/**
|
||||
* 已注册的effect 唯一标识
|
||||
*/
|
||||
key?: string;
|
||||
/**
|
||||
* 触发 effect 的事件
|
||||
*/
|
||||
event?: FormItemEventName;
|
||||
/**
|
||||
* 如果不使用已经注册的effect, 也支持直接写effect函数
|
||||
*/
|
||||
effect?: EffectFunction;
|
||||
}
|
||||
|
||||
export interface EffectEvent {
|
||||
target: any & { value: any };
|
||||
currentTarget: any;
|
||||
type: FormItemEventName;
|
||||
}
|
||||
|
||||
export interface EffectProps<CustomOptions = any, Event = EffectEvent> extends FormItemContext {
|
||||
event: Event;
|
||||
options: EffectAbilityOptions & CustomOptions;
|
||||
context: FormItemMaterialContext;
|
||||
}
|
||||
|
||||
export type EffectFunction = (props: EffectProps) => void;
|
||||
|
||||
export interface EffectExtension {
|
||||
key: string;
|
||||
effect: EffectFunction;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './setter-ability';
|
||||
export * from './decorator-ability';
|
||||
export * from './visibility-ability';
|
||||
export * from './effect-ability';
|
||||
export * from './default-ability';
|
||||
export * from './validation-ability';
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './setter-ability';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormItemAbility } from '../../models/form-item-ability';
|
||||
|
||||
export class SetterAbility implements FormItemAbility {
|
||||
static readonly type = 'setter';
|
||||
|
||||
get type(): string {
|
||||
return SetterAbility.type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { FormItemContext, FormItemFeedback } from '../../types';
|
||||
import { type FormItemMaterialContext } from '../../models/form-item-material-context';
|
||||
import { ValidatorFunction } from '../../abilities/validation-ability';
|
||||
|
||||
export interface SetterAbilityOptions {
|
||||
/**
|
||||
* 已注册的setter的唯一标识
|
||||
*/
|
||||
key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter context 是 FormItemMaterialContext 的外观
|
||||
* 基于外观设计模式设计,屏蔽了FormItemMaterialContext中一些setter不可见的接口
|
||||
* readonly: 对于setter 已经放在props 根级别,所以在这里屏蔽,防止干扰
|
||||
* getFormItemValueByPath: setter需通过表单联动方式获取其他表单项的值,不推荐是用这个方法,所以屏蔽
|
||||
*/
|
||||
export type SetterOrDecoratorContext = Omit<
|
||||
FormItemMaterialContext,
|
||||
'getFormItemValueByPath' | 'readonly'
|
||||
>;
|
||||
|
||||
export interface SetterComponentProps<T = any, CustomOptions = any>
|
||||
extends FormItemFeedback,
|
||||
FormItemContext {
|
||||
value: T;
|
||||
onChange: (v: T) => void;
|
||||
/**
|
||||
* 节点引擎全局readonly
|
||||
*/
|
||||
readonly: boolean;
|
||||
children?: any;
|
||||
options: SetterAbilityOptions & CustomOptions;
|
||||
context: SetterOrDecoratorContext;
|
||||
}
|
||||
|
||||
export interface SetterExtension {
|
||||
key: string;
|
||||
component: (props: SetterComponentProps) => any;
|
||||
validator?: ValidatorFunction;
|
||||
}
|
||||
|
||||
export type SetterHoc = (
|
||||
Component: React.JSXElementConstructor<SetterComponentProps>
|
||||
) => React.JSXElementConstructor<SetterComponentProps>;
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
// export type ValidatorFormats =
|
||||
// | 'url'
|
||||
// | 'email'
|
||||
// | 'ipv6'
|
||||
// | 'ipv4'
|
||||
// | 'number'
|
||||
// | 'integer'
|
||||
// | 'idcard'
|
||||
// | 'qq'
|
||||
// | 'phone'
|
||||
// | 'money'
|
||||
// | 'zh'
|
||||
// | 'date'
|
||||
// | 'zip'
|
||||
// | (string & {});
|
||||
//
|
||||
// export interface IValidatorRules<Context = any> {
|
||||
// format?: ValidatorFormats;
|
||||
// validator?: ValidatorFunction<Context>;
|
||||
// required?: boolean;
|
||||
// pattern?: RegExp | string;
|
||||
// max?: number;
|
||||
// maximum?: number;
|
||||
// maxItems?: number;
|
||||
// minItems?: number;
|
||||
// maxLength?: number;
|
||||
// minLength?: number;
|
||||
// exclusiveMaximum?: number;
|
||||
// exclusiveMinimum?: number;
|
||||
// minimum?: number;
|
||||
// min?: number;
|
||||
// len?: number;
|
||||
// whitespace?: boolean;
|
||||
// enum?: any[];
|
||||
// const?: any;
|
||||
// multipleOf?: number;
|
||||
// uniqueItems?: boolean;
|
||||
// maxProperties?: number;
|
||||
// minProperties?: number;
|
||||
// message?: string;
|
||||
//
|
||||
// [key: string]: any;
|
||||
// }
|
||||
//
|
||||
// export interface IValidateResult {
|
||||
// type: 'error' | 'warning';
|
||||
// message: string;
|
||||
// }
|
||||
//
|
||||
// export const isValidateResult = (obj: any): obj is IValidateResult =>
|
||||
// Boolean(obj.type) && Boolean(obj.message);
|
||||
//
|
||||
// export type ValidatorFunctionResponse =
|
||||
// | null
|
||||
// | void
|
||||
// | undefined
|
||||
// | string
|
||||
// | boolean
|
||||
// | IValidateResult;
|
||||
//
|
||||
// export type ValidatorFunction<Context = any> = (
|
||||
// value: any,
|
||||
// ctx: Context,
|
||||
// ) => ValidatorFunctionResponse | Promise<ValidatorFunctionResponse>;
|
||||
//
|
||||
// export type Validator<Context = any> =
|
||||
// | ValidatorFormats
|
||||
// | ValidatorFunction<Context>
|
||||
// | IValidatorRules;
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './validation-ability';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormItemMaterialContext } from '../../models';
|
||||
|
||||
// 这里参照了rehaje 的validator function 返回格式
|
||||
export interface IValidateResult {
|
||||
type: 'error' | 'warning';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ValidatorFunctionResponse =
|
||||
| null
|
||||
| void
|
||||
| undefined
|
||||
| string
|
||||
| boolean
|
||||
| IValidateResult;
|
||||
|
||||
export interface ValidationAbilityOptions {
|
||||
/**
|
||||
* 已注册的validator唯一标识
|
||||
*/
|
||||
key?: string;
|
||||
/**
|
||||
* 不使用已注册的validator 也支持在options中直接写validator
|
||||
*/
|
||||
validator?: ValidatorFunction;
|
||||
}
|
||||
|
||||
export interface ValidatorProps<T = any, CustomOptions = any> {
|
||||
value: T;
|
||||
options: ValidationAbilityOptions & CustomOptions;
|
||||
context: FormItemMaterialContext;
|
||||
}
|
||||
|
||||
export type ValidatorFunction = (props: ValidatorProps) => ValidatorFunctionResponse;
|
||||
|
||||
export interface ValidationExtension {
|
||||
key: string;
|
||||
validator: ValidatorFunction;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormItemAbility } from '../../models/form-item-ability';
|
||||
|
||||
export class ValidationAbility implements FormItemAbility {
|
||||
static readonly type = 'validation';
|
||||
|
||||
get type(): string {
|
||||
return ValidationAbility.type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './visibility-ability';
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormItemAbility } from '../../models/form-item-ability';
|
||||
|
||||
export interface VisibilityAbilityOptions {
|
||||
/**
|
||||
* 是否隐藏
|
||||
*/
|
||||
hidden: string | boolean;
|
||||
/**
|
||||
* 隐藏是否要清空表单值, 默认为false
|
||||
*/
|
||||
clearWhenHidden?: boolean;
|
||||
}
|
||||
|
||||
export class VisibilityAbility implements FormItemAbility {
|
||||
static readonly type = 'visibility';
|
||||
|
||||
get type(): string {
|
||||
return VisibilityAbility.type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
|
||||
import { FlowNodeFormData } from '../flow-node-form-data';
|
||||
import { FormModel } from '..';
|
||||
|
||||
export function isNodeFormReady(node: FlowNodeEntity) {
|
||||
return node.getData<FlowNodeFormData>(FlowNodeFormData).getFormModel<FormModel>().initialized;
|
||||
}
|
||||
|
||||
export function getFormModel(node: FlowNodeEntity) {
|
||||
return node.getData<FlowNodeFormData>(FlowNodeFormData).formModel;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Disposable, Emitter } from '@flowgram.ai/utils';
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { EntityData } from '@flowgram.ai/core';
|
||||
|
||||
import { FlowNodeErrorData } from '../error';
|
||||
import { FormMetaOrFormMetaGenerator } from './types';
|
||||
import { FormModel, type FormModelFactory } from './models';
|
||||
|
||||
interface Options {
|
||||
formModelFactory: FormModelFactory;
|
||||
}
|
||||
|
||||
export interface DetailChangeEvent {
|
||||
path: string;
|
||||
oldValue: any;
|
||||
value: any;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
export interface OnFormValuesChangePayload {
|
||||
values: any;
|
||||
prevValues: any;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class FlowNodeFormData extends EntityData {
|
||||
static type = 'FlowNodeEntityFormData';
|
||||
|
||||
readonly formModel: FormModel;
|
||||
|
||||
protected flowNodeEntity: FlowNodeEntity;
|
||||
|
||||
/**
|
||||
* @deprecated rehaje 版表单form Values change 事件
|
||||
* @protected
|
||||
*/
|
||||
protected onDetailChangeEmitter = new Emitter<DetailChangeEvent>();
|
||||
|
||||
/**
|
||||
* @deprecated 该方法为旧版引擎(rehaje)表单数据变更事件, 新版节点引擎请使用
|
||||
* this.getFormModel<FormModelV2>().onFormValuesChange.
|
||||
* @protected
|
||||
*/
|
||||
readonly onDetailChange = this.onDetailChangeEmitter.event;
|
||||
|
||||
constructor(entity: FlowNodeEntity, opts: Options) {
|
||||
super(entity);
|
||||
|
||||
this.flowNodeEntity = entity;
|
||||
this.formModel = opts.formModelFactory(entity);
|
||||
|
||||
this.toDispose.push(this.onDetailChangeEmitter);
|
||||
|
||||
this.toDispose.push(
|
||||
Disposable.create(() => {
|
||||
this.formModel.dispose();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getFormModel<TFormModel>(): TFormModel {
|
||||
// @ts-ignore
|
||||
return this.formModel as TFormModel;
|
||||
}
|
||||
|
||||
getDefaultData(): any {
|
||||
return {};
|
||||
}
|
||||
|
||||
createForm(formMetaOrFormMetaGenerator: any, initialValue?: any): void {
|
||||
const errorData = this.flowNodeEntity.getData<FlowNodeErrorData>(FlowNodeErrorData);
|
||||
|
||||
errorData.setError(null);
|
||||
try {
|
||||
this.formModel.init(formMetaOrFormMetaGenerator, initialValue);
|
||||
} catch (e) {
|
||||
errorData.setError(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
updateFormValues(value: any) {
|
||||
this.formModel.updateFormValues(value);
|
||||
}
|
||||
|
||||
recreateForm(formMetaOrFormMetaGenerator: FormMetaOrFormMetaGenerator, initialValue?: any): void {
|
||||
this.createForm(formMetaOrFormMetaGenerator, initialValue);
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
return this.formModel.toJSON();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated rehaje 版表单form Values change 事件触发函数
|
||||
* @protected
|
||||
*/
|
||||
fireDetaiChange(detailChangeEvent: DetailChangeEvent) {
|
||||
this.onDetailChangeEmitter.fire(detailChangeEvent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { type FormManager } from './services/form-manager';
|
||||
|
||||
export const FormContribution = Symbol('FormContribution');
|
||||
|
||||
export interface FormContribution {
|
||||
onRegister?(formManager: FormManager): void;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { bindContributions } from '@flowgram.ai/utils';
|
||||
|
||||
import { FormContextMaker } from './services/form-context-maker';
|
||||
import { NodeContribution } from '../node';
|
||||
import { FormManager, FormPathService } from './services';
|
||||
import { FormNodeContribution } from './form-node-contribution';
|
||||
|
||||
export const FormCoreContainerModule = new ContainerModule((bind) => {
|
||||
bind(FormManager).toSelf().inSingletonScope();
|
||||
bind(FormPathService).toSelf().inSingletonScope();
|
||||
bind(FormContextMaker).toSelf().inSingletonScope();
|
||||
bindContributions(bind, FormNodeContribution, [NodeContribution]);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
|
||||
import { NodeContribution, NodeManager, PLUGIN_KEY } from '../node';
|
||||
import { formPluginRender } from './form-render';
|
||||
|
||||
@injectable()
|
||||
export class FormNodeContribution implements NodeContribution {
|
||||
onRegister(nodeManager: NodeManager) {
|
||||
nodeManager.registerPluginRender(PLUGIN_KEY.FORM, formPluginRender);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { useRefresh } from '@flowgram.ai/utils';
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { PlaygroundContext } from '@flowgram.ai/core';
|
||||
|
||||
import { NodeContext } from '../node';
|
||||
import { FormModel } from './models';
|
||||
import { FlowNodeFormData } from './flow-node-form-data';
|
||||
|
||||
interface FormRenderProps {
|
||||
node: FlowNodeEntity;
|
||||
playgroundContext?: PlaygroundContext;
|
||||
}
|
||||
|
||||
function getFormModelFromNode(node: FlowNodeEntity) {
|
||||
return node.getData(FlowNodeFormData)?.getFormModel<FormModel>();
|
||||
}
|
||||
|
||||
export function FormRender({ node }: FormRenderProps): any {
|
||||
const refresh = useRefresh();
|
||||
|
||||
const formModel = getFormModelFromNode(node);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = formModel?.onInitialized(() => {
|
||||
refresh();
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [formModel]);
|
||||
|
||||
return formModel?.initialized ? formModel.render() : null;
|
||||
}
|
||||
|
||||
export const formPluginRender = (props: NodeContext) => <FormRender {...props} />;
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './form-core-container-module';
|
||||
export * from './form-contribution';
|
||||
export * from './services';
|
||||
export * from './models';
|
||||
export * from './types';
|
||||
export * from './abilities';
|
||||
export * from './flow-node-form-data';
|
||||
export * from './client';
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
|
||||
export interface Extension {
|
||||
key: string;
|
||||
}
|
||||
@injectable()
|
||||
export class FormAbilityExtensionRegistry {
|
||||
protected registry = new Map<string, Extension>();
|
||||
|
||||
register(extension: Extension): void {
|
||||
this.registry.set(extension.key, extension);
|
||||
}
|
||||
|
||||
get<T extends Extension>(key: string): T | undefined {
|
||||
return this.registry.get(key) as T | undefined;
|
||||
}
|
||||
|
||||
get objectMap(): Record<string, Extension> {
|
||||
return Object.fromEntries(this.registry);
|
||||
}
|
||||
|
||||
get collection(): Extension[] {
|
||||
return Array.from(this.registry.values());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export interface FormItemAbility {
|
||||
type: string;
|
||||
/**
|
||||
* 注册到formManager时钩子时调用
|
||||
*/
|
||||
onAbilityRegister?: () => void;
|
||||
}
|
||||
|
||||
export interface AbilityClass {
|
||||
type: string;
|
||||
|
||||
new (): FormItemAbility;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { type FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { PlaygroundContext } from '@flowgram.ai/core';
|
||||
|
||||
import { type FormModel, IFormItemMeta } from '..';
|
||||
|
||||
export interface FormItemMaterialContext {
|
||||
/**
|
||||
* 当前表单项的meta
|
||||
*/
|
||||
meta: IFormItemMeta;
|
||||
/**
|
||||
* 当前表单项的路径
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* 节点引擎全局readonly
|
||||
*/
|
||||
readonly: boolean;
|
||||
/**
|
||||
* 通过路径获取表单项的值
|
||||
* @param path 表单项在当前表单中的绝对路径,路径协议遵循glob
|
||||
*/
|
||||
getFormItemValueByPath: <T>(path: string) => T;
|
||||
/**
|
||||
* 节点表单校验回调函数注册
|
||||
*/
|
||||
onFormValidate: FormModel['onValidate'];
|
||||
/**
|
||||
* 获取Node模型
|
||||
*/
|
||||
node: FlowNodeEntity;
|
||||
/**
|
||||
* 获取FormModel原始模型
|
||||
*/
|
||||
form: FormModel;
|
||||
/**
|
||||
* 业务注入的全局context
|
||||
*/
|
||||
playgroundContext: PlaygroundContext;
|
||||
/**
|
||||
* 数组场景下当前项的index
|
||||
*/
|
||||
index?: number | undefined;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { DisposableCollection, Emitter } from '@flowgram.ai/utils';
|
||||
|
||||
import { FormItemDomRef, type IFormItemMeta } from '..';
|
||||
import { type FormModel } from '.';
|
||||
|
||||
export abstract class FormItem {
|
||||
readonly meta: IFormItemMeta;
|
||||
|
||||
readonly path: string;
|
||||
|
||||
readonly formModel: FormModel;
|
||||
|
||||
readonly onInitEventEmitter = new Emitter<FormItem>();
|
||||
|
||||
readonly onInit = this.onInitEventEmitter.event;
|
||||
|
||||
protected toDispose: DisposableCollection = new DisposableCollection();
|
||||
|
||||
readonly onDispose = this.toDispose.onDispose;
|
||||
|
||||
// todo(heyuan): 将dom 相关逻辑拆到form item插件里
|
||||
private _domRef: FormItemDomRef;
|
||||
|
||||
protected constructor(meta: IFormItemMeta, path: string, formModel: FormModel) {
|
||||
this.meta = meta;
|
||||
this.path = path;
|
||||
this.formModel = formModel;
|
||||
this.toDispose.push(this.onInitEventEmitter);
|
||||
}
|
||||
|
||||
abstract get value(): any;
|
||||
|
||||
abstract set value(value: any);
|
||||
|
||||
abstract validate(): void;
|
||||
|
||||
set domRef(domRef: FormItemDomRef) {
|
||||
this._domRef = domRef;
|
||||
}
|
||||
|
||||
get domRef() {
|
||||
return this._domRef;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { type IFormItemMeta, type IFormMeta, type IFormMetaOptions } from '../types';
|
||||
import { FormPathService } from '../services';
|
||||
|
||||
export interface FormMetaTraverseParams {
|
||||
formItemMeta: IFormItemMeta;
|
||||
parentPath?: string;
|
||||
handle: (params: { formItemMeta: IFormItemMeta; path: string }) => any;
|
||||
}
|
||||
|
||||
export class FormMeta implements IFormMeta {
|
||||
constructor(root: IFormItemMeta, options: IFormMetaOptions) {
|
||||
this._root = root;
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
protected _root: IFormItemMeta;
|
||||
|
||||
get root(): IFormItemMeta {
|
||||
return this._root;
|
||||
}
|
||||
|
||||
protected _options: IFormMetaOptions;
|
||||
|
||||
get options(): IFormMetaOptions {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
static traverse({ formItemMeta, parentPath = '', handle }: FormMetaTraverseParams): void {
|
||||
if (!formItemMeta) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isRoot = !parentPath;
|
||||
|
||||
const path = isRoot
|
||||
? FormPathService.ROOT
|
||||
: formItemMeta.name
|
||||
? FormPathService.join([parentPath, formItemMeta.name])
|
||||
: parentPath;
|
||||
|
||||
handle({ formItemMeta, path });
|
||||
|
||||
if (formItemMeta.items) {
|
||||
this.traverse({
|
||||
formItemMeta: formItemMeta.items,
|
||||
handle,
|
||||
parentPath: FormPathService.toArrayPath(path),
|
||||
});
|
||||
}
|
||||
|
||||
if (formItemMeta.children && formItemMeta.children.length) {
|
||||
formItemMeta.children.forEach((child) => {
|
||||
this.traverse({ formItemMeta: child, handle, parentPath: path });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
import { DisposableCollection, Event, MaybePromise } from '@flowgram.ai/utils';
|
||||
import { type FlowNodeEntity } from '@flowgram.ai/document';
|
||||
|
||||
import { FormFeedback, FormModelValid, IFormItem } from '../types';
|
||||
import { FormManager } from '../services/form-manager';
|
||||
import { type FormItem } from '.';
|
||||
|
||||
export type FormModelFactory = (entity: FlowNodeEntity) => FormModel;
|
||||
export const FormModelFactory = Symbol('FormModelFactory');
|
||||
export const FormModelEntity = Symbol('FormModelEntity');
|
||||
|
||||
@injectable()
|
||||
export abstract class FormModel {
|
||||
readonly onValidate: Event<FormModel>;
|
||||
|
||||
readonly onValidChange: Event<FormModelValid>;
|
||||
|
||||
readonly onFeedbacksChange: Event<FormFeedback[]>;
|
||||
|
||||
readonly onInitialized: Event<FormModel>;
|
||||
|
||||
protected toDispose: DisposableCollection = new DisposableCollection();
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use `formModel.node` instead in FormModelV2
|
||||
*/
|
||||
abstract get flowNodeEntity(): FlowNodeEntity;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
abstract get formManager(): FormManager;
|
||||
|
||||
abstract get formMeta(): any;
|
||||
|
||||
abstract get initialized(): boolean;
|
||||
|
||||
abstract get valid(): FormModelValid;
|
||||
|
||||
abstract updateFormValues(value: any): void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use `formModel.getFieldIn` instead in FormModelV2 to get the model of a form field
|
||||
* do not use this in FormModelV2 since it only return an empty Map.
|
||||
*/
|
||||
abstract get formItemPathMap(): Map<string, IFormItem>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
abstract clearValid(): void;
|
||||
|
||||
abstract validate(): Promise<boolean>;
|
||||
|
||||
abstract validateWithFeedbacks(): Promise<FormFeedback[]>;
|
||||
|
||||
abstract init(formMetaOrFormMetaGenerator: any, initialValue?: any): MaybePromise<void>;
|
||||
|
||||
abstract toJSON(): any;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use `formModel.getField` instead in FormModelV2
|
||||
*/
|
||||
abstract getFormItemByPath(path: string): FormItem | undefined;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use `formModel.getFieldValue` instead in FormModelV2 to get the model of a form field by path
|
||||
*/
|
||||
abstract getFormItemValueByPath<T = any>(path: string): any | undefined;
|
||||
|
||||
abstract render(): any;
|
||||
|
||||
abstract dispose(): void;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './form-model';
|
||||
export * from './form-item';
|
||||
export * from './form-meta';
|
||||
export * from './form-ability-extension-registry';
|
||||
export * from './form-item-ability';
|
||||
export * from './form-item-material-context';
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { injectPlaygroundContext, PlaygroundContext } from '@flowgram.ai/core';
|
||||
|
||||
import { NodeEngineContext } from '../../node';
|
||||
import { FormItem, FormItemMaterialContext } from '..';
|
||||
|
||||
@injectable()
|
||||
export class FormContextMaker {
|
||||
@inject(NodeEngineContext) readonly nodeEngineContext: NodeEngineContext;
|
||||
|
||||
@injectPlaygroundContext() readonly playgroundContext: PlaygroundContext;
|
||||
|
||||
makeFormItemMaterialContext(
|
||||
formItem: FormItem,
|
||||
options?: { getIndex: () => number | undefined }
|
||||
): FormItemMaterialContext {
|
||||
return {
|
||||
meta: formItem.meta,
|
||||
path: formItem.path,
|
||||
readonly: this.nodeEngineContext.readonly,
|
||||
getFormItemValueByPath: formItem.formModel.getFormItemValueByPath.bind(formItem.formModel),
|
||||
onFormValidate: formItem.formModel.onValidate.bind(formItem.formModel),
|
||||
form: formItem.formModel,
|
||||
node: formItem.formModel.flowNodeEntity,
|
||||
playgroundContext: this.playgroundContext,
|
||||
index: options?.getIndex(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { mapValues } from 'lodash-es';
|
||||
import { inject, injectable, multiInject, optional, postConstruct } from 'inversify';
|
||||
import { Emitter } from '@flowgram.ai/utils';
|
||||
import { injectPlaygroundContext, PlaygroundContext } from '@flowgram.ai/core';
|
||||
|
||||
import { AbilityClass, FormItemAbility } from '../models/form-item-ability';
|
||||
import { FormAbilityExtensionRegistry, FormModel } from '../models';
|
||||
import { FormContribution } from '../form-contribution';
|
||||
import {
|
||||
DecoratorAbility,
|
||||
DecoratorExtension,
|
||||
SetterAbility,
|
||||
SetterExtension,
|
||||
SetterHoc,
|
||||
} from '../abilities';
|
||||
import { FormContextMaker, FormPathService } from './index';
|
||||
|
||||
@injectable()
|
||||
export class FormManager {
|
||||
readonly abilityRegistry: Map<string, FormItemAbility> = new Map();
|
||||
|
||||
readonly setterHocs: SetterHoc[] = [];
|
||||
|
||||
readonly extensionRegistryMap: Map<string, FormAbilityExtensionRegistry> = new Map();
|
||||
|
||||
@inject(FormPathService) readonly pathManager: FormPathService;
|
||||
|
||||
@inject(FormContextMaker) readonly formContextMaker: FormContextMaker;
|
||||
|
||||
@injectPlaygroundContext() readonly playgroundContext: PlaygroundContext;
|
||||
|
||||
@multiInject(FormContribution) @optional() protected formContributions: FormContribution[] = [];
|
||||
|
||||
private readonly onFormModelWillInitEmitter = new Emitter<{
|
||||
model: FormModel;
|
||||
data: any;
|
||||
}>();
|
||||
|
||||
readonly onFormModelWillInit = this.onFormModelWillInitEmitter.event;
|
||||
|
||||
get components(): Record<string, any> {
|
||||
return mapValues(
|
||||
this.extensionRegistryMap.get(SetterAbility.type)?.objectMap || {},
|
||||
(setter: SetterExtension) => setter.component
|
||||
);
|
||||
}
|
||||
|
||||
get decorators(): Record<string, any> {
|
||||
return mapValues(
|
||||
this.extensionRegistryMap.get(DecoratorAbility.type)?.objectMap || {},
|
||||
(decorator: DecoratorExtension) => decorator.component
|
||||
);
|
||||
}
|
||||
|
||||
registerAbilityExtension(type: string, extension: any): void {
|
||||
if (!this.extensionRegistryMap.get(type)) {
|
||||
this.extensionRegistryMap.set(type, new FormAbilityExtensionRegistry());
|
||||
}
|
||||
const registry = this.extensionRegistryMap.get(type);
|
||||
if (!registry) {
|
||||
return;
|
||||
}
|
||||
registry.register(extension);
|
||||
}
|
||||
|
||||
getAbilityExtension(abilityType: string, extensionKey: string): any {
|
||||
return this.extensionRegistryMap.get(abilityType)?.get(extensionKey);
|
||||
}
|
||||
|
||||
registerAbility(Ability: AbilityClass): void {
|
||||
const ability = new Ability();
|
||||
this.abilityRegistry.set(ability.type, ability);
|
||||
}
|
||||
|
||||
registerAbilities(Abilities: AbilityClass[]): void {
|
||||
Abilities.forEach(this.registerAbility.bind(this));
|
||||
}
|
||||
|
||||
getAbility<ExtendAbility>(type: string): (FormItemAbility & ExtendAbility) | undefined {
|
||||
return this.abilityRegistry.get(type) as FormItemAbility & ExtendAbility;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Setter Hoc and setter are no longer supported in NodeEngineV2
|
||||
* @param hoc
|
||||
*/
|
||||
registerSetterHoc(hoc: SetterHoc): void {
|
||||
this.setterHocs.push(hoc);
|
||||
}
|
||||
|
||||
fireFormModelWillInit(model: FormModel, data: any) {
|
||||
this.onFormModelWillInitEmitter.fire({
|
||||
model,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.onFormModelWillInitEmitter.dispose();
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.formContributions.forEach((contrib) => contrib.onRegister?.(this));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
|
||||
@injectable()
|
||||
export class FormPathService {
|
||||
static readonly ROOT = '/';
|
||||
|
||||
static readonly DIVIDER = '/';
|
||||
|
||||
static readonly RELATIVE_PARENT = '..';
|
||||
|
||||
static readonly RELATIVE_CURRENT = '.';
|
||||
|
||||
static readonly ARRAY = '[]';
|
||||
|
||||
static normalize(path: string) {
|
||||
if (path === FormPathService.ROOT) {
|
||||
return path;
|
||||
}
|
||||
// 去掉末尾的斜杠
|
||||
if (path.endsWith(FormPathService.DIVIDER)) {
|
||||
path = path.slice(0, -1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
static join(paths: string[]): string {
|
||||
if (paths[1].startsWith(FormPathService.ROOT)) {
|
||||
throw new Error(
|
||||
`FormPathService Error: join failed, invalid paths[1], paths[1]= ${paths[1]}`,
|
||||
);
|
||||
}
|
||||
if (paths[0].endsWith(FormPathService.DIVIDER)) {
|
||||
return `${paths[0]}${paths[1]}`;
|
||||
}
|
||||
return paths.join(FormPathService.DIVIDER);
|
||||
}
|
||||
|
||||
static toArrayPath(path: string): string {
|
||||
return FormPathService.join([path, FormPathService.ARRAY]);
|
||||
}
|
||||
|
||||
static parseArrayItemPath(path: string) {
|
||||
const names = path.split('/');
|
||||
|
||||
let i = 0;
|
||||
while (i < names.length) {
|
||||
const itemIndex = parseInt(names[i]);
|
||||
|
||||
if (!isNaN(itemIndex)) {
|
||||
const arrayPath = FormPathService.toArrayPath(
|
||||
names.slice(0, i).join(FormPathService.DIVIDER),
|
||||
);
|
||||
const restPath = names.slice(i + 1).join(FormPathService.DIVIDER);
|
||||
const itemMetaPath = FormPathService.join([arrayPath, restPath]);
|
||||
return { itemIndex, arrayPath, itemMetaPath };
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
simplify(path: string) {
|
||||
const segments = path.split(FormPathService.DIVIDER);
|
||||
const resSegments: string[] = [];
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (!segments[i]) {
|
||||
throw new Error('FormPathService: join failed');
|
||||
}
|
||||
|
||||
if (segments[i] === FormPathService.RELATIVE_CURRENT) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (segments[i] === FormPathService.RELATIVE_PARENT) {
|
||||
resSegments.pop();
|
||||
}
|
||||
resSegments.push(segments[i]);
|
||||
}
|
||||
return resSegments.join(FormPathService.DIVIDER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './form-path-service';
|
||||
export * from './form-manager';
|
||||
export * from './form-context-maker';
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { PlaygroundContext } from '@flowgram.ai/core';
|
||||
import { MaybePromise } from '@flowgram.ai/utils';
|
||||
|
||||
import { type IFormItem } from './form-model.types';
|
||||
import { IFormItemMeta } from './form-meta.types';
|
||||
|
||||
export interface FormItemAbilityMeta<Options = any> {
|
||||
type: string;
|
||||
options: Options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export interface FormItemContext {
|
||||
/**
|
||||
* @deprecated Use context.node instead
|
||||
*/
|
||||
formItemMeta: IFormItemMeta;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
formItem: IFormItem;
|
||||
/**
|
||||
* @deprecated Use context.node instead
|
||||
*/
|
||||
flowNodeEntity: FlowNodeEntity;
|
||||
/**
|
||||
* @deprecated Use context.playgroundContext instead
|
||||
*/
|
||||
playgroundContext: PlaygroundContext;
|
||||
}
|
||||
|
||||
export interface FormItemHookParams extends FormItemContext {
|
||||
formItem: IFormItem;
|
||||
}
|
||||
|
||||
export interface FormItemHooks<T> {
|
||||
/**
|
||||
* FormItem初始化钩子
|
||||
*/
|
||||
onInit?: (params: FormItemHookParams & T) => void;
|
||||
/**
|
||||
* FormItem提交时钩子
|
||||
*/
|
||||
onSubmit?: (params: FormItemHookParams & T) => void;
|
||||
/**
|
||||
* FormItem克隆时钩子
|
||||
*/
|
||||
onClone?: (params: FormItemHookParams & T) => MaybePromise<void>;
|
||||
/**
|
||||
* 克隆后执行的逻辑
|
||||
*/
|
||||
afterClone?: (params: FormItemHookParams & T) => void;
|
||||
/**
|
||||
* FormItem全局校验时钩子
|
||||
*/
|
||||
onValidate?: (params: FormItemHookParams & T) => void;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { MaybePromise } from '@flowgram.ai/utils';
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { PlaygroundContext, PluginContext } from '@flowgram.ai/core';
|
||||
|
||||
import { type FormItemAbilityMeta } from './form-ability.types';
|
||||
|
||||
export type FormDataTypeName =
|
||||
| 'string'
|
||||
| 'number'
|
||||
| 'integer'
|
||||
| 'boolean'
|
||||
| 'object'
|
||||
| 'array'
|
||||
| 'null';
|
||||
|
||||
export type FormDataType =
|
||||
| string //
|
||||
| number
|
||||
| boolean
|
||||
| FormDataObject
|
||||
| DataArray
|
||||
| null;
|
||||
|
||||
export interface FormDataObject {
|
||||
[key: string]: FormDataType;
|
||||
}
|
||||
|
||||
export type DataArray = Array<FormDataType>;
|
||||
|
||||
export const FORM_VOID = 'form-void' as const;
|
||||
|
||||
export interface TreeNode<T> {
|
||||
name: string;
|
||||
children?: TreeNode<T>[];
|
||||
}
|
||||
|
||||
export interface IFormItemMeta extends TreeNode<IFormItemMeta> {
|
||||
/**
|
||||
* 表单项名称
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 数据类型
|
||||
*/
|
||||
type: FormDataTypeName | typeof FORM_VOID;
|
||||
/**
|
||||
* 枚举值
|
||||
*/
|
||||
enum?: FormDataType[];
|
||||
/**
|
||||
* 数组类型item的数据类型描述
|
||||
*/
|
||||
items?: IFormItemMeta;
|
||||
/**
|
||||
* 表单项标题
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* 表单项描述
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* 表单项默认值
|
||||
*/
|
||||
default?: FormDataType;
|
||||
/**
|
||||
* 是否必填
|
||||
*/
|
||||
required?: boolean;
|
||||
/**
|
||||
* 扩展能力
|
||||
*/
|
||||
abilities?: FormItemAbilityMeta[];
|
||||
|
||||
/**
|
||||
* 子表单项
|
||||
*/
|
||||
children?: IFormItemMeta[];
|
||||
}
|
||||
|
||||
export interface IFormMeta {
|
||||
/**
|
||||
* 表单树结构root
|
||||
*/
|
||||
root?: IFormItemMeta;
|
||||
/**
|
||||
* 表单全局配置
|
||||
*/
|
||||
options?: IFormMetaOptions;
|
||||
}
|
||||
|
||||
export interface NodeFormContext {
|
||||
node: FlowNodeEntity;
|
||||
playgroundContext: PlaygroundContext;
|
||||
clientContext: PluginContext & Record<string, any>;
|
||||
}
|
||||
|
||||
export interface IFormMetaOptions {
|
||||
formatOnInit?: (value: any, context: NodeFormContext) => any;
|
||||
|
||||
formatOnSubmit?: (value: any, context: NodeFormContext) => any;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface FormMetaGeneratorParams<PlaygroundContext, FormValue = any> {
|
||||
node: FlowNodeEntity;
|
||||
playgroundContext: PlaygroundContext;
|
||||
initialValue?: FormValue;
|
||||
}
|
||||
|
||||
export type FormMetaGenerator<PlaygroundContext = any, FormValue = any> = (
|
||||
params: FormMetaGeneratorParams<FormValue, FormValue>
|
||||
) => MaybePromise<IFormMeta>;
|
||||
|
||||
export type FormMetaOrFormMetaGenerator = FormMetaGenerator | IFormMeta;
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export interface IFormItem<T = any> {
|
||||
value: T;
|
||||
}
|
||||
|
||||
export enum FormItemEventName {
|
||||
onFormValueChange = 'onFormValueChange',
|
||||
onFormItemInit = 'onFormItemInit',
|
||||
}
|
||||
|
||||
export type FormModelValid = boolean | null;
|
||||
|
||||
export type FeedbackStatus = 'error' | 'warning' | 'pending';
|
||||
export type FeedbackText = string;
|
||||
|
||||
export interface FormItemFeedback {
|
||||
feedbackStatus?: FeedbackStatus;
|
||||
feedbackText?: FeedbackText;
|
||||
}
|
||||
|
||||
export interface FormFeedback {
|
||||
feedbackStatus?: FeedbackStatus;
|
||||
feedbackText?: FeedbackText;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface FormItemDomRef {
|
||||
current: HTMLElement | null;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './form-model.types';
|
||||
export * from './form-meta.types';
|
||||
export * from './form-ability.types';
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './node';
|
||||
export * from './error';
|
||||
export * from './form';
|
||||
export * from './client';
|
||||
export * from './node-react';
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './node-engine-react-context';
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { INodeEngineContext, NodeEngineContext } from '../../node';
|
||||
|
||||
export const NodeEngineReactContext = React.createContext<INodeEngineContext>(
|
||||
NodeEngineContext.DEFAULT_JSON,
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './use-node-engine-context';
|
||||
export * from './use-form-Item';
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { useEntityFromContext, useRefresh } from '@flowgram.ai/core';
|
||||
|
||||
import { FlowNodeFormData, FormModel, IFormItem } from '../../form';
|
||||
|
||||
export function useFormItem(path: string): IFormItem | undefined {
|
||||
const refresh = useRefresh();
|
||||
const node = useEntityFromContext<FlowNodeEntity>();
|
||||
const formData = node.getData<FlowNodeFormData>(FlowNodeFormData);
|
||||
const formItem = formData.getFormModel<FormModel>().getFormItemByPath(path);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = formData.onDataChange(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return formItem;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useService, useRefresh } from '@flowgram.ai/core';
|
||||
|
||||
import { NodeEngineContext } from '../../node';
|
||||
|
||||
export function useNodeEngineContext(): NodeEngineContext {
|
||||
const refresh = useRefresh();
|
||||
const nodeEngineContext = useService<NodeEngineContext>(NodeEngineContext);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = nodeEngineContext.onChange(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return nodeEngineContext;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './hooks';
|
||||
export * from './context';
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export const MATERIAL_KEY = {
|
||||
NODE_ERROR_RENDER: 'node_error_render',
|
||||
NODE_PLACEHOLDER_RENDER: 'node_placeholder_render',
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export const PLUGIN_KEY = {
|
||||
FORM: 'Plugin_Form',
|
||||
ERROR: 'Plugin_Error',
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { NodeContribution } from './node-contribution';
|
||||
export * from './node-container-module';
|
||||
export * from './node-manager';
|
||||
export * from './types';
|
||||
export * from './core-plugins';
|
||||
export * from './core-materials';
|
||||
export * from './node-engine';
|
||||
export * from './node-engine-context';
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { ContainerModule } from 'inversify';
|
||||
|
||||
import { NodeManager } from './node-manager';
|
||||
import { NodeEngineContext } from './node-engine-context';
|
||||
import { NodeEngine } from './node-engine';
|
||||
|
||||
export const NodeContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(NodeEngine).toSelf().inSingletonScope();
|
||||
bind(NodeManager).toSelf().inSingletonScope();
|
||||
bind(NodeEngineContext).toSelf().inSingletonScope();
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { type NodeManager } from './node-manager';
|
||||
|
||||
export const NodeContribution = Symbol('NodeContribution');
|
||||
|
||||
export interface NodeContribution {
|
||||
onRegister?(nodeManager: NodeManager): void;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
import { Emitter } from '@flowgram.ai/utils';
|
||||
|
||||
export interface INodeEngineContext {
|
||||
readonly: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* NodeEngineContext 在 Node Engine 中为全局单例, 它的作用是让Node之间共享数据。
|
||||
* context 分为内置context(如 readonly) 和 自定义context(业务可以按需注入)
|
||||
*/
|
||||
@injectable()
|
||||
export class NodeEngineContext {
|
||||
static DEFAULT_READONLY = false;
|
||||
|
||||
static DEFAULT_JSON = { readonly: NodeEngineContext.DEFAULT_READONLY };
|
||||
|
||||
readonly onChangeEmitter = new Emitter<NodeEngineContext>();
|
||||
|
||||
readonly onChange = this.onChangeEmitter.event;
|
||||
|
||||
private _readonly: boolean = NodeEngineContext.DEFAULT_READONLY;
|
||||
|
||||
private _json: INodeEngineContext = NodeEngineContext.DEFAULT_JSON;
|
||||
|
||||
get json(): INodeEngineContext {
|
||||
return this._json;
|
||||
}
|
||||
|
||||
get readonly(): boolean {
|
||||
return this._readonly;
|
||||
}
|
||||
|
||||
set readonly(value: boolean) {
|
||||
this._readonly = value;
|
||||
this.fireChange();
|
||||
}
|
||||
|
||||
private fireChange(): void {
|
||||
this.updateJSON();
|
||||
this.onChangeEmitter.fire(this);
|
||||
}
|
||||
|
||||
private updateJSON(): void {
|
||||
this._json = {
|
||||
readonly: this._readonly,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { injectable, inject } from 'inversify';
|
||||
|
||||
import { NodeManager } from './node-manager';
|
||||
import { NodeEngineContext } from './node-engine-context';
|
||||
|
||||
@injectable()
|
||||
export class NodeEngine {
|
||||
@inject(NodeManager) nodeManager: NodeManager;
|
||||
|
||||
@inject(NodeEngineContext) context: NodeEngineContext;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { flow } from 'lodash-es';
|
||||
import { injectable, multiInject, optional, postConstruct } from 'inversify';
|
||||
|
||||
import { NodeErrorRenderProps } from '../error';
|
||||
import { NodePluginRender, NodeRenderHoc, Render } from './types';
|
||||
import { NodeContribution } from './node-contribution';
|
||||
|
||||
export enum MaterialRenderKey {
|
||||
CustomNodeError = 'Material_CustomNodeError',
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class NodeManager {
|
||||
readonly materialRenderRegistry: Map<string, Render> = new Map();
|
||||
|
||||
readonly pluginRenderRegistry: Map<string, Render> = new Map();
|
||||
|
||||
readonly nodeRenderHocs: NodeRenderHoc[] = [];
|
||||
|
||||
@multiInject(NodeContribution) @optional() protected nodeContributions: NodeContribution[] = [];
|
||||
|
||||
registerMaterialRender(key: string, render: Render) {
|
||||
this.materialRenderRegistry.set(key, render);
|
||||
}
|
||||
|
||||
getMaterialRender(key: string): Render | undefined {
|
||||
return this.materialRenderRegistry.get(key);
|
||||
}
|
||||
|
||||
registerPluginRender(key: string, render: NodePluginRender): void {
|
||||
this.pluginRenderRegistry.set(key, render);
|
||||
}
|
||||
|
||||
getPluginRender(key: string): NodePluginRender | undefined {
|
||||
return this.pluginRenderRegistry.get(key);
|
||||
}
|
||||
|
||||
registerNodeErrorRender(render: Render<NodeErrorRenderProps>) {
|
||||
this.registerMaterialRender(MaterialRenderKey.CustomNodeError, render);
|
||||
}
|
||||
|
||||
get nodeRenderHoc() {
|
||||
return flow(this.nodeRenderHocs);
|
||||
}
|
||||
|
||||
registerNodeRenderHoc(hoc: NodeRenderHoc) {
|
||||
this.nodeRenderHocs.push(hoc);
|
||||
}
|
||||
|
||||
get nodeErrorRender() {
|
||||
return this.materialRenderRegistry.get(MaterialRenderKey.CustomNodeError);
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.nodeContributions.forEach((contrib) => contrib.onRegister?.(this));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
|
||||
import { NodeFormContext } from '../form';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use `NodeFormContext` instead
|
||||
*/
|
||||
export type NodeContext = NodeFormContext;
|
||||
|
||||
export type Render<T = any> = (props: T) => any;
|
||||
|
||||
export type NodePluginRender = Render<NodeFormContext>;
|
||||
|
||||
export type NodePlaceholderRender = Render<NodeFormContext>;
|
||||
|
||||
export interface NodeRenderProps {
|
||||
node: FlowNodeEntity;
|
||||
}
|
||||
|
||||
export type NodeRenderHoc = (
|
||||
Component: React.JSXElementConstructor<NodeRenderProps>
|
||||
) => React.JSXElementConstructor<NodeRenderProps>;
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"baseUrl": "./",
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
mockReset: false,
|
||||
environment: 'jsdom',
|
||||
setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],
|
||||
include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],
|
||||
exclude: [
|
||||
'**/node_modules/**',
|
||||
'**/dist/**',
|
||||
'**/lib/**', // lib 编译结果忽略掉
|
||||
'**/cypress/**',
|
||||
'**/.{idea,git,cache,output,temp}/**',
|
||||
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
Reference in New Issue
Block a user