mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
feat(variable): batch outputs (#426)
* feat: get by key path in flow-node-variable-data * feat: scope chain transform service * feat: form outputs plugin * feat: base variable field * feat: move set var get var to scope * feat: batch outputs * feat: form plugin create effect by func * feat: batch output key configuration * feat: merge effet api in form plugin * feat: form plugin * fix: variable layout test * feat: simplify defineFormPluginCreator
This commit is contained in:
parent
8fa571af9e
commit
93aa3e77b1
@ -31,7 +31,9 @@ export function FormInputs() {
|
||||
onChange={field.onChange}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
schema={property}
|
||||
constantProps={{
|
||||
schema: property,
|
||||
}}
|
||||
/>
|
||||
<Feedback errors={fieldState?.errors} warnings={fieldState?.warnings} />
|
||||
</FormItem>
|
||||
|
||||
@ -14,6 +14,7 @@ interface FormItemProps {
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
labelWidth?: number;
|
||||
vertical?: boolean;
|
||||
}
|
||||
export function FormItem({
|
||||
children,
|
||||
@ -22,6 +23,7 @@ export function FormItem({
|
||||
description,
|
||||
type,
|
||||
labelWidth,
|
||||
vertical,
|
||||
}: FormItemProps): JSX.Element {
|
||||
const renderTitle = useCallback(
|
||||
(showTooltip?: boolean) => (
|
||||
@ -42,9 +44,13 @@ export function FormItem({
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
...(vertical
|
||||
? { flexDirection: 'column' }
|
||||
: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@ -59,7 +59,9 @@ export const PropertyEdit: React.FC<PropertyEditProps> = (props) => {
|
||||
<DynamicValueInput
|
||||
value={value.default}
|
||||
onChange={(val) => updateProperty('default', val)}
|
||||
schema={value}
|
||||
constantProps={{
|
||||
schema: value,
|
||||
}}
|
||||
style={{ flexGrow: 1 }}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -130,6 +130,20 @@ export const initialData: FlowDocumentJSON = {
|
||||
},
|
||||
data: {
|
||||
title: 'Loop_1',
|
||||
batchFor: {
|
||||
type: 'ref',
|
||||
content: ['start_0', 'array_obj'],
|
||||
},
|
||||
batchOutputs: {
|
||||
results: {
|
||||
type: 'ref',
|
||||
content: ['llm_6aSyo', 'result'],
|
||||
},
|
||||
indexList: {
|
||||
type: 'ref',
|
||||
content: ['loop_sGybT_locals', 'index'],
|
||||
},
|
||||
},
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
PositionSchema,
|
||||
FlowNodeTransformData,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
import { provideBatchInputEffect } from '@flowgram.ai/form-materials';
|
||||
import { createBatchOutputsFormPlugin, provideBatchInputEffect } from '@flowgram.ai/form-materials';
|
||||
|
||||
import { defaultFormMeta } from '../default-form-meta';
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
@ -73,5 +73,6 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
|
||||
effect: {
|
||||
batchFor: provideBatchInputEffect,
|
||||
},
|
||||
plugins: [createBatchOutputsFormPlugin({ outputKey: 'batchOutputs' })],
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { FormRenderProps, FlowNodeJSON, Field } from '@flowgram.ai/free-layout-editor';
|
||||
import { SubCanvasRender } from '@flowgram.ai/free-container-plugin';
|
||||
import { BatchVariableSelector, IFlowRefValue } from '@flowgram.ai/form-materials';
|
||||
import { BatchOutputs, BatchVariableSelector, IFlowRefValue } from '@flowgram.ai/form-materials';
|
||||
|
||||
import { useIsSidebar, useNodeRenderContext } from '../../hooks';
|
||||
import { FormHeader, FormContent, FormOutputs, FormItem, Feedback } from '../../form-components';
|
||||
@ -33,12 +33,30 @@ export const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {
|
||||
</Field>
|
||||
);
|
||||
|
||||
const batchOutputs = (
|
||||
<Field<Record<string, IFlowRefValue | undefined> | undefined> name={`batchOutputs`}>
|
||||
{({ field, fieldState }) => (
|
||||
<FormItem name="batchOutputs" type="object" vertical>
|
||||
<BatchOutputs
|
||||
style={{ width: '100%' }}
|
||||
value={field.value}
|
||||
onChange={(val) => field.onChange(val)}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
/>
|
||||
<Feedback errors={fieldState?.errors} />
|
||||
</FormItem>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
|
||||
if (isSidebar) {
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
{batchFor}
|
||||
{batchOutputs}
|
||||
<FormOutputs />
|
||||
</FormContent>
|
||||
</>
|
||||
|
||||
@ -17,7 +17,7 @@ export interface Material {
|
||||
[key: string]: any; // For other properties from config.json
|
||||
}
|
||||
|
||||
const _types: string[] = ['components', 'effects', 'utils', 'typings'];
|
||||
const _types: string[] = ['components', 'effects', 'utils', 'typings', 'form-plugins'];
|
||||
|
||||
export function listAllMaterials(): Material[] {
|
||||
const _materials: Material[] = [];
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "batch-outputs",
|
||||
"depMaterials": [
|
||||
"flow-value",
|
||||
"variable-selector"
|
||||
],
|
||||
"depPackages": [
|
||||
"@douyinfe/semi-ui",
|
||||
"@douyinfe/semi-icons",
|
||||
"styled-components"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button, Input } from '@douyinfe/semi-ui';
|
||||
import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
|
||||
|
||||
import { useList } from './use-list';
|
||||
import { PropsType } from './types';
|
||||
import { VariableSelector } from '../variable-selector';
|
||||
import { UIRow, UIRows } from './styles';
|
||||
|
||||
export function BatchOutputs(props: PropsType) {
|
||||
const { readonly, style } = props;
|
||||
|
||||
const { list, add, update, remove } = useList(props);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UIRows style={style}>
|
||||
{list.map((item) => (
|
||||
<UIRow key={item.id}>
|
||||
<Input
|
||||
style={{ width: 100 }}
|
||||
disabled={readonly}
|
||||
size="small"
|
||||
value={item.key}
|
||||
onChange={(v) => update({ ...item, key: v })}
|
||||
/>
|
||||
<VariableSelector
|
||||
style={{ flexGrow: 1 }}
|
||||
readonly={readonly}
|
||||
value={item.value?.content}
|
||||
onChange={(v) =>
|
||||
update({
|
||||
...item,
|
||||
value: {
|
||||
type: 'ref',
|
||||
content: v,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
disabled={readonly}
|
||||
icon={<IconDelete />}
|
||||
size="small"
|
||||
onClick={() => remove(item.id)}
|
||||
/>
|
||||
</UIRow>
|
||||
))}
|
||||
</UIRows>
|
||||
<Button disabled={readonly} icon={<IconPlus />} size="small" onClick={add}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const UIRows = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
export const UIRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
`;
|
||||
@ -0,0 +1,17 @@
|
||||
import { IFlowRefValue } from '../../typings';
|
||||
|
||||
export type ValueType = Record<string, IFlowRefValue | undefined>;
|
||||
|
||||
export interface OutputItem {
|
||||
id: number;
|
||||
key?: string;
|
||||
value?: IFlowRefValue;
|
||||
}
|
||||
|
||||
export interface PropsType {
|
||||
value?: ValueType;
|
||||
onChange: (value?: ValueType) => void;
|
||||
readonly?: boolean;
|
||||
hasError?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { difference } from 'lodash';
|
||||
|
||||
import { OutputItem, PropsType } from './types';
|
||||
|
||||
let _id = 0;
|
||||
function genId() {
|
||||
return _id++;
|
||||
}
|
||||
|
||||
export function useList({ value, onChange }: PropsType) {
|
||||
const [list, setList] = useState<OutputItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setList((_prevList) => {
|
||||
const newKeys = Object.keys(value || {});
|
||||
const oldKeys = _prevList.map((item) => item.key).filter(Boolean) as string[];
|
||||
const addKeys = difference(newKeys, oldKeys);
|
||||
|
||||
return _prevList
|
||||
.filter((item) => !item.key || newKeys.includes(item.key))
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
key: item.key,
|
||||
value: item.key ? value?.[item.key!] : undefined,
|
||||
}))
|
||||
.concat(
|
||||
addKeys.map((_key) => ({
|
||||
id: genId(),
|
||||
key: _key,
|
||||
value: value?.[_key],
|
||||
}))
|
||||
);
|
||||
});
|
||||
}, [value]);
|
||||
|
||||
const add = () => {
|
||||
setList((prevList) => [
|
||||
...prevList,
|
||||
{
|
||||
id: genId(),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const update = (item: OutputItem) => {
|
||||
setList((prevList) => {
|
||||
const nextList = prevList.map((_item) => {
|
||||
if (_item.id === item.id) {
|
||||
return item;
|
||||
}
|
||||
return _item;
|
||||
});
|
||||
|
||||
onChange(
|
||||
Object.fromEntries(
|
||||
nextList.filter((item) => item.key).map((item) => [item.key!, item.value])
|
||||
)
|
||||
);
|
||||
|
||||
return nextList;
|
||||
});
|
||||
};
|
||||
|
||||
const remove = (itemId: number) => {
|
||||
setList((prevList) => {
|
||||
const nextList = prevList.filter((_item) => _item.id !== itemId);
|
||||
|
||||
onChange(
|
||||
Object.fromEntries(
|
||||
nextList.filter((item) => item.key).map((item) => [item.key!, item.value])
|
||||
)
|
||||
);
|
||||
|
||||
return nextList;
|
||||
});
|
||||
};
|
||||
|
||||
return { list, add, update, remove };
|
||||
}
|
||||
@ -19,6 +19,7 @@ interface PropsType {
|
||||
schema?: IJsonSchema;
|
||||
constantProps?: {
|
||||
strategies?: Strategy[];
|
||||
schema?: IJsonSchema; // set schema of constant input only
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,3 +5,4 @@ export * from './batch-variable-selector';
|
||||
export * from './constant-input';
|
||||
export * from './dynamic-value-input';
|
||||
export * from './condition-row';
|
||||
export * from './batch-outputs';
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
import { IFlowRefValue } from '../../typings';
|
||||
|
||||
export const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({
|
||||
private: true,
|
||||
parse: (value: Record<string, IFlowRefValue>, ctx) => [
|
||||
ASTFactory.createVariableDeclaration({
|
||||
key: `${ctx.node.id}`,
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "batch-outputs-plugin",
|
||||
"depMaterials": [
|
||||
"flow-value"
|
||||
],
|
||||
"depPackages": []
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
import {
|
||||
ASTFactory,
|
||||
createEffectFromVariableProvider,
|
||||
defineFormPluginCreator,
|
||||
FlowNodeRegistry,
|
||||
getNodeForm,
|
||||
getNodePrivateScope,
|
||||
getNodeScope,
|
||||
ScopeChainTransformService,
|
||||
type EffectOptions,
|
||||
type FormPluginCreator,
|
||||
FlowNodeScopeType,
|
||||
} from '@flowgram.ai/editor';
|
||||
|
||||
import { IFlowRefValue } from '../../typings';
|
||||
|
||||
export const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({
|
||||
parse: (value: Record<string, IFlowRefValue>, ctx) => [
|
||||
ASTFactory.createVariableDeclaration({
|
||||
key: `${ctx.node.id}`,
|
||||
meta: {
|
||||
title: getNodeForm(ctx.node)?.getValueIn('title'),
|
||||
icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,
|
||||
},
|
||||
type: ASTFactory.createObject({
|
||||
properties: Object.entries(value).map(([_key, value]) =>
|
||||
ASTFactory.createProperty({
|
||||
key: _key,
|
||||
initializer: ASTFactory.createWrapArrayExpression({
|
||||
wrapFor: ASTFactory.createKeyPathExpression({
|
||||
keyPath: value?.content || [],
|
||||
}),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* Free Layout only right now
|
||||
*/
|
||||
export const createBatchOutputsFormPlugin: FormPluginCreator<{ outputKey: string }> =
|
||||
defineFormPluginCreator({
|
||||
name: 'batch-outputs-plugin',
|
||||
onSetupFormMeta({ mergeEffect }, { outputKey }) {
|
||||
mergeEffect({
|
||||
[outputKey]: provideBatchOutputsEffect,
|
||||
});
|
||||
},
|
||||
onInit(ctx, { outputKey }) {
|
||||
const chainTransformService = ctx.node.getService(ScopeChainTransformService);
|
||||
|
||||
const batchNodeType = ctx.node.flowNodeType;
|
||||
|
||||
const transformerId = `${batchNodeType}-outputs`;
|
||||
|
||||
if (chainTransformService.hasTransformer(transformerId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
chainTransformService.registerTransformer(transformerId, {
|
||||
transformCovers: (covers, ctx) => {
|
||||
const node = ctx.scope.meta?.node;
|
||||
|
||||
// Child Node's variable can cover parent
|
||||
if (node?.parent?.flowNodeType === batchNodeType) {
|
||||
return [...covers, getNodeScope(node.parent)];
|
||||
}
|
||||
|
||||
return covers;
|
||||
},
|
||||
transformDeps(scopes, ctx) {
|
||||
const scopeMeta = ctx.scope.meta;
|
||||
|
||||
if (scopeMeta?.type === FlowNodeScopeType.private) {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
const node = scopeMeta?.node;
|
||||
|
||||
// Public of Loop Node depends on child Node
|
||||
if (node?.flowNodeType === batchNodeType) {
|
||||
// Get all child blocks
|
||||
const childBlocks = node.blocks;
|
||||
|
||||
// public scope of all child blocks
|
||||
return [
|
||||
getNodePrivateScope(node),
|
||||
...childBlocks.map((_childBlock) => getNodeScope(_childBlock)),
|
||||
];
|
||||
}
|
||||
|
||||
return scopes;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { createBatchOutputsFormPlugin } from './batch-outputs-plugin';
|
||||
@ -2,3 +2,4 @@ export * from './components';
|
||||
export * from './effects';
|
||||
export * from './utils';
|
||||
export * from './typings';
|
||||
export * from './form-plugins';
|
||||
|
||||
@ -2,7 +2,7 @@ import { injectable } from 'inversify';
|
||||
import { DisposableCollection, Event, MaybePromise } from '@flowgram.ai/utils';
|
||||
import { type FlowNodeEntity } from '@flowgram.ai/document';
|
||||
|
||||
import { FormFeedback, FormModelValid, IFormItem, IFormMeta } from '../types';
|
||||
import { FormFeedback, FormModelValid, IFormItem } from '../types';
|
||||
import { FormManager } from '../services/form-manager';
|
||||
import { type FormItem } from '.';
|
||||
|
||||
@ -33,7 +33,7 @@ export abstract class FormModel {
|
||||
*/
|
||||
abstract get formManager(): FormManager;
|
||||
|
||||
abstract get formMeta(): IFormMeta;
|
||||
abstract get formMeta(): any;
|
||||
|
||||
abstract get initialized(): boolean;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { MaybePromise } from '@flowgram.ai/utils';
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { PlaygroundContext } from '@flowgram.ai/core';
|
||||
import { MaybePromise } from '@flowgram.ai/utils';
|
||||
|
||||
import { type FormItemAbilityMeta } from './form-ability.types';
|
||||
|
||||
@ -82,7 +82,7 @@ export interface IFormMeta {
|
||||
/**
|
||||
* 表单树结构root
|
||||
*/
|
||||
root: IFormItemMeta;
|
||||
root?: IFormItemMeta;
|
||||
/**
|
||||
* 表单全局配置
|
||||
*/
|
||||
@ -109,7 +109,7 @@ export interface FormMetaGeneratorParams<PlaygroundContext, FormValue = any> {
|
||||
}
|
||||
|
||||
export type FormMetaGenerator<PlaygroundContext = any, FormValue = any> = (
|
||||
params: FormMetaGeneratorParams<FormValue, FormValue>,
|
||||
params: FormMetaGeneratorParams<FormValue, FormValue>
|
||||
) => MaybePromise<IFormMeta>;
|
||||
|
||||
export type FormMetaOrFormMetaGenerator = FormMetaGenerator | IFormMeta;
|
||||
|
||||
@ -42,14 +42,15 @@ describe('FormModelV2', () => {
|
||||
});
|
||||
|
||||
const formItem = formModelV2.getFormItemByPath('/');
|
||||
expect(formItem.value).toEqual({
|
||||
expect(formItem?.value).toEqual({
|
||||
a: 1,
|
||||
b: 2,
|
||||
});
|
||||
|
||||
formItem.value = { a: 3, b: 4 };
|
||||
// @ts-expect-error
|
||||
formItem?.value = { a: 3, b: 4 };
|
||||
|
||||
expect(formItem.value).toEqual({
|
||||
expect(formItem?.value).toEqual({
|
||||
a: 3,
|
||||
b: 4,
|
||||
});
|
||||
@ -336,7 +337,8 @@ describe('FormModelV2', () => {
|
||||
});
|
||||
it('should call onInit when formModel init', () => {
|
||||
const mockInit = vi.fn();
|
||||
const plugin = defineFormPluginCreator('test', {
|
||||
const plugin = defineFormPluginCreator({
|
||||
name: 'test',
|
||||
onInit: mockInit,
|
||||
})({ opt1: 1 });
|
||||
const formMeta = {
|
||||
@ -353,7 +355,8 @@ describe('FormModelV2', () => {
|
||||
});
|
||||
it('should call onDispose when formModel dispose', () => {
|
||||
const mockDispose = vi.fn();
|
||||
const plugin = defineFormPluginCreator('test', {
|
||||
const plugin = defineFormPluginCreator({
|
||||
name: 'test',
|
||||
onDispose: mockDispose,
|
||||
})({ opt1: 1 });
|
||||
const formMeta = {
|
||||
@ -373,14 +376,17 @@ describe('FormModelV2', () => {
|
||||
const mockEffectPlugin = vi.fn();
|
||||
const mockEffectOrigin = vi.fn();
|
||||
|
||||
const plugin = defineFormPluginCreator('test', {
|
||||
effect: {
|
||||
a: [
|
||||
{
|
||||
event: DataEvent.onValueInitOrChange,
|
||||
effect: mockEffectPlugin,
|
||||
},
|
||||
],
|
||||
const plugin = defineFormPluginCreator({
|
||||
name: 'test',
|
||||
onSetupFormMeta(ctx, opts) {
|
||||
ctx.mergeEffect({
|
||||
a: [
|
||||
{
|
||||
event: DataEvent.onValueInitOrChange,
|
||||
effect: mockEffectPlugin,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
})({ opt1: 1 });
|
||||
|
||||
@ -407,20 +413,23 @@ describe('FormModelV2', () => {
|
||||
const mockEffectOriginArrStar = vi.fn();
|
||||
const mockEffectPluginOther = vi.fn();
|
||||
|
||||
const plugin = defineFormPluginCreator('test', {
|
||||
effect: {
|
||||
'arr.*': [
|
||||
{
|
||||
event: DataEvent.onValueChange,
|
||||
effect: mockEffectPluginArrStar,
|
||||
},
|
||||
],
|
||||
other: [
|
||||
{
|
||||
event: DataEvent.onValueChange,
|
||||
effect: mockEffectPluginOther,
|
||||
},
|
||||
],
|
||||
const plugin = defineFormPluginCreator({
|
||||
name: 'test',
|
||||
onSetupFormMeta(ctx, opts) {
|
||||
ctx.mergeEffect({
|
||||
'arr.*': [
|
||||
{
|
||||
event: DataEvent.onValueChange,
|
||||
effect: mockEffectPluginArrStar,
|
||||
},
|
||||
],
|
||||
other: [
|
||||
{
|
||||
event: DataEvent.onValueChange,
|
||||
effect: mockEffectPluginOther,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
})({ opt1: 1 });
|
||||
|
||||
|
||||
@ -26,12 +26,7 @@ import {
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { PlaygroundContext, PluginContext } from '@flowgram.ai/core';
|
||||
|
||||
import {
|
||||
convertGlobPath,
|
||||
findMatchedInMap,
|
||||
formFeedbacksToNodeCoreFormFeedbacks,
|
||||
mergeEffectMap,
|
||||
} from './utils';
|
||||
import { convertGlobPath, findMatchedInMap, formFeedbacksToNodeCoreFormFeedbacks } from './utils';
|
||||
import {
|
||||
DataEvent,
|
||||
Effect,
|
||||
@ -133,8 +128,10 @@ export class FormModelV2 extends FormModel implements Disposable {
|
||||
return this._formControl;
|
||||
}
|
||||
|
||||
get formMeta() {
|
||||
return this.node.getNodeRegistry().formMeta;
|
||||
protected _formMeta: FormMeta;
|
||||
|
||||
get formMeta(): FormMeta {
|
||||
return this._formMeta || (this.node.getNodeRegistry().formMeta as FormMeta);
|
||||
}
|
||||
|
||||
get values() {
|
||||
@ -195,9 +192,6 @@ export class FormModelV2 extends FormModel implements Disposable {
|
||||
this.plugins = plugins;
|
||||
plugins.forEach((plugin) => {
|
||||
plugin.init(this);
|
||||
if (plugin.config?.effect) {
|
||||
mergeEffectMap(this.effectMap, plugin.config.effect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -209,6 +203,14 @@ export class FormModelV2 extends FormModel implements Disposable {
|
||||
formData.fireChange();
|
||||
});
|
||||
|
||||
(formMeta.plugins || [])?.forEach((_plugin) => {
|
||||
if (_plugin.setupFormMeta) {
|
||||
formMeta = _plugin.setupFormMeta(formMeta, this.nodeContext);
|
||||
}
|
||||
});
|
||||
|
||||
this._formMeta = formMeta;
|
||||
|
||||
const { validateTrigger, validate, effect } = formMeta;
|
||||
if (effect) {
|
||||
this.effectMap = effect;
|
||||
|
||||
@ -1,23 +1,31 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Disposable } from '@flowgram.ai/utils';
|
||||
import { type NodeFormContext } from '@flowgram.ai/form-core';
|
||||
|
||||
import { EffectOptions, FormPluginCtx } from './types';
|
||||
import { type FormMeta, type FormPluginCtx, type FormPluginSetupMetaCtx } from './types';
|
||||
import { FormModelV2 } from './form-model-v2';
|
||||
|
||||
export interface FormPluginConfig<Opts = any> {
|
||||
/**
|
||||
* form plugin name, for debug use
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* setup formMeta
|
||||
* @param ctx
|
||||
* @returns
|
||||
*/
|
||||
onSetupFormMeta?: (ctx: FormPluginSetupMetaCtx, opts: Opts) => void;
|
||||
|
||||
/**
|
||||
* FormModel 初始化时执行
|
||||
* @param ctx
|
||||
*/
|
||||
onInit?: (ctx: FormPluginCtx, opts: Opts) => void;
|
||||
|
||||
/**
|
||||
* 同 FormMeta 中的effects 会与 FormMeta 中的effects 合并
|
||||
*/
|
||||
effect?: Record<string, EffectOptions[]>;
|
||||
/**
|
||||
* FormModel 销毁时执行
|
||||
* @param ctx
|
||||
*/
|
||||
onDispose?: (ctx: FormPluginCtx, opts: Opts) => void;
|
||||
}
|
||||
@ -33,10 +41,11 @@ export class FormPlugin<Opts = any> implements Disposable {
|
||||
|
||||
protected _formModel: FormModelV2;
|
||||
|
||||
constructor(name: string, config: FormPluginConfig, opts?: Opts) {
|
||||
this.name = name;
|
||||
this.pluginId = `${name}__${nanoid()}`;
|
||||
constructor(config: FormPluginConfig, opts?: Opts) {
|
||||
this.name = config?.name || '';
|
||||
this.pluginId = `${this.name}__${nanoid()}`;
|
||||
this.config = config;
|
||||
|
||||
this.opts = opts;
|
||||
}
|
||||
|
||||
@ -52,6 +61,49 @@ export class FormPlugin<Opts = any> implements Disposable {
|
||||
};
|
||||
}
|
||||
|
||||
setupFormMeta(formMeta: FormMeta, nodeContext: NodeFormContext): FormMeta {
|
||||
const nextFormMeta: FormMeta = {
|
||||
...formMeta,
|
||||
};
|
||||
|
||||
this.config.onSetupFormMeta?.(
|
||||
{
|
||||
mergeEffect: (effect) => {
|
||||
nextFormMeta.effect = {
|
||||
...(nextFormMeta.effect || {}),
|
||||
...effect,
|
||||
};
|
||||
},
|
||||
mergeValidate: (validate) => {
|
||||
nextFormMeta.validate = {
|
||||
...(nextFormMeta.validate || {}),
|
||||
...validate,
|
||||
};
|
||||
},
|
||||
addFormatOnInit: (formatOnInit) => {
|
||||
if (!nextFormMeta.formatOnInit) {
|
||||
nextFormMeta.formatOnInit = formatOnInit;
|
||||
return;
|
||||
}
|
||||
const legacyFormatOnInit = nextFormMeta.formatOnInit;
|
||||
nextFormMeta.formatOnInit = (v, c) => formatOnInit?.(legacyFormatOnInit(v, c), c);
|
||||
},
|
||||
addFormatOnSubmit: (formatOnSubmit) => {
|
||||
if (!nextFormMeta.formatOnSubmit) {
|
||||
nextFormMeta.formatOnSubmit = formatOnSubmit;
|
||||
return;
|
||||
}
|
||||
const legacyFormatOnSubmit = nextFormMeta.formatOnSubmit;
|
||||
nextFormMeta.formatOnSubmit = (v, c) => formatOnSubmit?.(legacyFormatOnSubmit(v, c), c);
|
||||
},
|
||||
...nodeContext,
|
||||
},
|
||||
this.opts
|
||||
);
|
||||
|
||||
return nextFormMeta;
|
||||
}
|
||||
|
||||
init(formModel: FormModelV2) {
|
||||
this._formModel = formModel;
|
||||
this.config?.onInit?.(this.ctx, this.opts);
|
||||
@ -64,8 +116,10 @@ export class FormPlugin<Opts = any> implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
export function defineFormPluginCreator<Opts>(name: string, config: FormPluginConfig) {
|
||||
export type FormPluginCreator<Opts> = (opts: Opts) => FormPlugin<Opts>;
|
||||
|
||||
export function defineFormPluginCreator<Opts>(config: FormPluginConfig): FormPluginCreator<Opts> {
|
||||
return function (opts: Opts) {
|
||||
return new FormPlugin(name, config, opts);
|
||||
return new FormPlugin(config, opts);
|
||||
};
|
||||
}
|
||||
|
||||
@ -138,6 +138,13 @@ export type FormPluginCtx = {
|
||||
formModel: FormModelV2;
|
||||
} & NodeContext;
|
||||
|
||||
export type FormPluginSetupMetaCtx = {
|
||||
mergeEffect: (effect: Record<string, EffectOptions[]>) => void;
|
||||
mergeValidate: (validate: Record<FieldName, Validate>) => void;
|
||||
addFormatOnInit: (formatOnInit: FormMeta['formatOnInit']) => void;
|
||||
addFormatOnSubmit: (formatOnSubmit: FormMeta['formatOnSubmit']) => void;
|
||||
} & NodeContext;
|
||||
|
||||
export interface onFormValueChangeInPayload<TValue = FieldValue, TFormValues = FieldValue> {
|
||||
value: TValue;
|
||||
prevValue: TValue;
|
||||
|
||||
@ -10,7 +10,7 @@ import { type VariableProviderAbilityOptions } from '../types';
|
||||
* @returns
|
||||
*/
|
||||
export function createEffectFromVariableProvider(
|
||||
options: VariableProviderAbilityOptions,
|
||||
options: VariableProviderAbilityOptions
|
||||
): EffectOptions[] {
|
||||
const getScope = (node: FlowNodeEntity): Scope => {
|
||||
const variableData: FlowNodeVariableData = node.getData(FlowNodeVariableData);
|
||||
@ -42,7 +42,7 @@ export function createEffectFromVariableProvider(
|
||||
return [
|
||||
{
|
||||
event: DataEvent.onValueInit,
|
||||
effect: (params => {
|
||||
effect: ((params) => {
|
||||
const { context } = params;
|
||||
|
||||
const scope = getScope(context.node);
|
||||
@ -51,8 +51,6 @@ export function createEffectFromVariableProvider(
|
||||
scope,
|
||||
options,
|
||||
formItem: undefined,
|
||||
// Hack: 新表单引擎暂时不支持 triggerSync
|
||||
triggerSync: undefined as any,
|
||||
});
|
||||
|
||||
if (disposable) {
|
||||
@ -65,7 +63,7 @@ export function createEffectFromVariableProvider(
|
||||
},
|
||||
{
|
||||
event: DataEvent.onValueChange,
|
||||
effect: (params => {
|
||||
effect: ((params) => {
|
||||
transformValueToAST(params);
|
||||
}) as Effect,
|
||||
},
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
import { DataEvent, defineFormPluginCreator } from '@flowgram.ai/node';
|
||||
|
||||
export const createVariableProviderPlugin = defineFormPluginCreator('VariableProviderPlugin', {
|
||||
export const createVariableProviderPlugin = defineFormPluginCreator({
|
||||
name: 'VariableProviderPlugin',
|
||||
onInit: (ctx, opts) => {
|
||||
// todo
|
||||
// console.log('>>> VariableProviderPlugin init', ctx, opts);
|
||||
},
|
||||
effect: {
|
||||
arr: [
|
||||
{
|
||||
event: DataEvent.onValueInitOrChange,
|
||||
effect: () => {
|
||||
// todo
|
||||
// console.log('>>> VariableProviderPlugin effect triggered');
|
||||
onSetupFormMeta({ mergeEffect }) {
|
||||
mergeEffect({
|
||||
arr: [
|
||||
{
|
||||
event: DataEvent.onValueInitOrChange,
|
||||
effect: () => {
|
||||
// todo
|
||||
// console.log('>>> VariableProviderPlugin effect triggered');
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
},
|
||||
onDispose: (ctx, opts) => {
|
||||
// todo
|
||||
|
||||
@ -3,9 +3,9 @@ import {
|
||||
type ASTNodeJSON,
|
||||
type VariableDeclarationJSON,
|
||||
} from '@flowgram.ai/variable-plugin';
|
||||
import { Disposable } from '@flowgram.ai/utils';
|
||||
import { FormItem } from '@flowgram.ai/form-core';
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { Disposable } from '@flowgram.ai/utils';
|
||||
|
||||
export interface VariableAbilityCommonContext {
|
||||
node: FlowNodeEntity; // 节点
|
||||
@ -14,9 +14,7 @@ export interface VariableAbilityCommonContext {
|
||||
options: VariableAbilityOptions;
|
||||
}
|
||||
|
||||
export interface VariableAbilityInitCtx extends VariableAbilityCommonContext {
|
||||
triggerSync: () => void; // 触发变量同步
|
||||
}
|
||||
export interface VariableAbilityInitCtx extends VariableAbilityCommonContext {}
|
||||
|
||||
export interface VariableAbilityOptions {
|
||||
// 变量提供能力可复用
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
export * from './create-variable-plugin';
|
||||
export * from '@flowgram.ai/variable-core';
|
||||
export { FlowNodeVariableData, GlobalScope } from '@flowgram.ai/variable-layout';
|
||||
export {
|
||||
FlowNodeVariableData,
|
||||
GlobalScope,
|
||||
ScopeChainTransformService,
|
||||
getNodeScope,
|
||||
getNodePrivateScope,
|
||||
FlowNodeScopeType,
|
||||
type FlowNodeScopeMeta,
|
||||
type FlowNodeScope,
|
||||
} from '@flowgram.ai/variable-layout';
|
||||
|
||||
@ -12,8 +12,8 @@ import { shallowEqual } from 'fast-equals';
|
||||
import { Disposable, DisposableCollection } from '@flowgram.ai/utils';
|
||||
|
||||
import { subsToDisposable } from '../utils/toDisposable';
|
||||
import { type Scope } from '../scope';
|
||||
import { updateChildNodeHelper } from './utils/helpers';
|
||||
import { type Scope } from '../scope';
|
||||
import {
|
||||
type ASTNodeJSON,
|
||||
type ObserverOrNext,
|
||||
@ -73,7 +73,7 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
||||
/**
|
||||
* 节点的版本号,每 fireChange 一次 version + 1
|
||||
*/
|
||||
private _version: number = 0;
|
||||
protected _version: number = 0;
|
||||
|
||||
/**
|
||||
* 更新锁
|
||||
@ -109,8 +109,8 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
||||
Disposable.create(() => {
|
||||
// 子元素删除时,父元素触发更新
|
||||
this.parent?.fireChange();
|
||||
this.children.forEach(child => child.dispose());
|
||||
}),
|
||||
this.children.forEach((child) => child.dispose());
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
@ -191,7 +191,7 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
||||
child.toDispose.push(
|
||||
Disposable.create(() => {
|
||||
this._children.delete(child);
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
return child;
|
||||
@ -204,7 +204,7 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
||||
protected updateChildNodeByKey(keyInThis: keyof this, nextJSON?: ASTNodeJSON) {
|
||||
this.withBatchUpdate(updateChildNodeHelper).call(this, {
|
||||
getChildNode: () => this[keyInThis] as ASTNode,
|
||||
updateChildNode: _node => ((this as any)[keyInThis] = _node),
|
||||
updateChildNode: (_node) => ((this as any)[keyInThis] = _node),
|
||||
removeChildNode: () => ((this as any)[keyInThis] = undefined),
|
||||
nextJSON,
|
||||
});
|
||||
@ -216,7 +216,7 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
||||
* @returns
|
||||
*/
|
||||
protected withBatchUpdate<ParamTypes extends any[], ReturnType>(
|
||||
updater: (...args: ParamTypes) => ReturnType,
|
||||
updater: (...args: ParamTypes) => ReturnType
|
||||
) {
|
||||
return (...args: ParamTypes) => {
|
||||
// batchUpdate 里面套 batchUpdate 只能生效一次
|
||||
@ -281,7 +281,7 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
||||
*/
|
||||
subscribe<Data = this>(
|
||||
observer: ObserverOrNext<Data>,
|
||||
{ selector, debounceAnimation, triggerOnInit }: SubscribeConfig<this, Data> = {},
|
||||
{ selector, debounceAnimation, triggerOnInit }: SubscribeConfig<this, Data> = {}
|
||||
): Disposable {
|
||||
return subsToDisposable(
|
||||
this.value$
|
||||
@ -289,25 +289,25 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
||||
map(() => (selector ? selector(this) : (this as any))),
|
||||
distinctUntilChanged(
|
||||
(a, b) => shallowEqual(a, b),
|
||||
value => {
|
||||
(value) => {
|
||||
if (value instanceof ASTNode) {
|
||||
// 如果 value 是 ASTNode,则进行 hash 的比较
|
||||
return value.hash;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
}
|
||||
),
|
||||
// 默认跳过 BehaviorSubject 第一次触发
|
||||
triggerOnInit ? tap(() => null) : skip(1),
|
||||
// 每个 animationFrame 内所有更新合并成一个
|
||||
debounceAnimation ? debounceTime(0, animationFrameScheduler) : tap(() => null),
|
||||
debounceAnimation ? debounceTime(0, animationFrameScheduler) : tap(() => null)
|
||||
)
|
||||
.subscribe(observer),
|
||||
.subscribe(observer)
|
||||
);
|
||||
}
|
||||
|
||||
dispatchGlobalEvent<ActionType extends GlobalEventActionType = GlobalEventActionType>(
|
||||
event: Omit<ActionType, 'ast'>,
|
||||
event: Omit<ActionType, 'ast'>
|
||||
) {
|
||||
this.scope.event.dispatch({
|
||||
...event,
|
||||
|
||||
@ -35,6 +35,10 @@ export abstract class BaseVariableField<VariableMeta = any> extends ASTNode<
|
||||
return getParentFields(this);
|
||||
}
|
||||
|
||||
get keyPath(): string[] {
|
||||
return this.parentFields.reverse().map((_field) => _field.key);
|
||||
}
|
||||
|
||||
get meta(): VariableMeta {
|
||||
return this._meta;
|
||||
}
|
||||
@ -47,6 +51,10 @@ export abstract class BaseVariableField<VariableMeta = any> extends ASTNode<
|
||||
return this._initializer;
|
||||
}
|
||||
|
||||
get hash(): string {
|
||||
return `[${this._version}]${this.keyPath.join('.')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 VariableDeclarationJSON 从而生成变量声明节点
|
||||
*/
|
||||
@ -96,7 +104,7 @@ export abstract class BaseVariableField<VariableMeta = any> extends ASTNode<
|
||||
* @returns
|
||||
*/
|
||||
onTypeChange(observer: (type: ASTNode | undefined) => void) {
|
||||
return this.subscribe(observer, { selector: curr => curr.type });
|
||||
return this.subscribe(observer, { selector: (curr) => curr.type });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -28,6 +28,7 @@ export class WrapArrayExpression extends BaseExpression<WrapArrayExpressionJSON>
|
||||
refreshReturnType() {
|
||||
// 被遍历表达式的返回值
|
||||
const childReturnTypeJSON = this.wrapFor?.returnType?.toJSON();
|
||||
|
||||
this.updateChildNodeByKey('_returnType', {
|
||||
kind: ASTKind.Array,
|
||||
items: childReturnTypeJSON,
|
||||
@ -51,9 +52,12 @@ export class WrapArrayExpression extends BaseExpression<WrapArrayExpressionJSON>
|
||||
|
||||
@postConstructAST()
|
||||
protected init() {
|
||||
this.refreshReturnType = this.refreshReturnType.bind(this);
|
||||
|
||||
this.toDispose.push(
|
||||
this.subscribe(this.refreshReturnType, {
|
||||
selector: (curr) => curr.wrapFor?.returnType,
|
||||
triggerOnInit: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,7 +6,9 @@ import {
|
||||
merge,
|
||||
share,
|
||||
skip,
|
||||
startWith,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
import { flatten } from 'lodash';
|
||||
import { shallowEqual } from 'fast-equals';
|
||||
@ -17,7 +19,7 @@ import { IVariableTable } from '../types';
|
||||
import { type Scope } from '../scope';
|
||||
import { subsToDisposable } from '../../utils/toDisposable';
|
||||
import { createMemo } from '../../utils/memo';
|
||||
import { Property, VariableDeclaration } from '../../ast';
|
||||
import { BaseVariableField, VariableDeclaration } from '../../ast';
|
||||
/**
|
||||
* 作用域可用变量
|
||||
*/
|
||||
@ -135,11 +137,35 @@ export class ScopeAvailableData {
|
||||
* @param keyPath
|
||||
* @returns
|
||||
*/
|
||||
getByKeyPath(keyPath: string[] = []): VariableDeclaration | Property | undefined {
|
||||
getByKeyPath(keyPath: string[] = []): BaseVariableField | undefined {
|
||||
// 检查变量是否在可访问范围内
|
||||
if (!this.variableKeys.includes(keyPath[0])) {
|
||||
return;
|
||||
}
|
||||
return this.globalVariableTable.getByKeyPath(keyPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track Variable Change (Includes type update and children update) By KeyPath
|
||||
* @returns
|
||||
*/
|
||||
trackByKeyPath(
|
||||
keyPath: string[] = [],
|
||||
cb: (variable?: BaseVariableField | undefined) => void,
|
||||
opts?: {
|
||||
triggerOnInit?: boolean;
|
||||
}
|
||||
): Disposable {
|
||||
const { triggerOnInit = true } = opts || {};
|
||||
|
||||
return subsToDisposable(
|
||||
merge(this.anyVariableChange$, this.variables$)
|
||||
.pipe(
|
||||
triggerOnInit ? startWith() : tap(() => null),
|
||||
map(() => this.getByKeyPath(keyPath)),
|
||||
distinctUntilChanged((_prevNode, _node) => _prevNode?.hash !== _node?.hash)
|
||||
)
|
||||
.subscribe(cb)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { DisposableCollection } from '@flowgram.ai/utils';
|
||||
|
||||
import { type VariableEngine } from '../variable-engine';
|
||||
import { createMemo } from '../utils/memo';
|
||||
import { ASTKind, MapNode } from '../ast';
|
||||
import { ASTKind, type ASTNode, type ASTNodeJSON, MapNode } from '../ast';
|
||||
import { ScopeAvailableData, ScopeEventData, ScopeOutputData } from './datas';
|
||||
|
||||
export interface IScopeConstructor {
|
||||
@ -117,4 +117,44 @@ export class Scope<ScopeMeta extends Record<string, any> = Record<string, any>>
|
||||
get disposed(): boolean {
|
||||
return this.toDispose.disposed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a variable in the Scope with the default key 'outputs'.
|
||||
*
|
||||
* @param json - The JSON value to store.
|
||||
* @returns The updated AST node.
|
||||
*/
|
||||
public setVar(json: ASTNodeJSON): ASTNode;
|
||||
|
||||
public setVar(arg1: string | ASTNodeJSON, arg2?: ASTNodeJSON): ASTNode {
|
||||
if (typeof arg1 === 'string' && arg2 !== undefined) {
|
||||
return this.ast.set(arg1, arg2);
|
||||
}
|
||||
|
||||
if (typeof arg1 === 'object' && arg2 === undefined) {
|
||||
return this.ast.set('outputs', arg1);
|
||||
}
|
||||
|
||||
throw new Error('Invalid arguments');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a variable from the Scope by key.
|
||||
*
|
||||
* @param key - The key of the variable to retrieve. Defaults to 'outputs'.
|
||||
* @returns The value of the variable, or undefined if not found.
|
||||
*/
|
||||
public getVar(key: string = 'outputs') {
|
||||
return this.ast.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a variable from the Scope by key.
|
||||
*
|
||||
* @param key - The key of the variable to clear. Defaults to 'outputs'.
|
||||
* @returns The updated AST node.
|
||||
*/
|
||||
public clearVar(key: string = 'outputs') {
|
||||
return this.ast.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,10 +24,12 @@ import { WorkflowDocumentContainerModule, WorkflowLinesManager, WorkflowSimpleLi
|
||||
|
||||
export interface TestConfig extends VariableLayoutConfig {
|
||||
enableGlobalScope?: boolean;
|
||||
onInit?: (container: Container) => void;
|
||||
runExtraTest?: (container: Container) => void
|
||||
}
|
||||
|
||||
export function getContainer(layout: 'free' | 'fixed', config?: TestConfig): Container {
|
||||
const { enableGlobalScope, ...layoutConfig } = config || {};
|
||||
const { enableGlobalScope, onInit, runExtraTest, ...layoutConfig } = config || {};
|
||||
|
||||
const container = createPlaygroundContainer() as Container;
|
||||
container.load(VariableContainerModule);
|
||||
@ -74,5 +76,7 @@ export function getContainer(layout: 'free' | 'fixed', config?: TestConfig): Con
|
||||
);
|
||||
document.registerNodeDatas(FlowNodeVariableData);
|
||||
|
||||
onInit?.(container);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@ -95,6 +95,8 @@ export const runFixedLayoutTest = (testName:string, spec: FlowDocumentJSON, conf
|
||||
test('test sort', () => {
|
||||
expect(variableEngine.getAllScopes({ sort: true }).map(_scope => _scope.id)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
config?.runExtraTest?.(container);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -95,6 +95,8 @@ export const runFreeLayoutTest = (testName: string, spec: WorkflowJSON, config?:
|
||||
test('test sort', () => {
|
||||
expect(variableEngine.getAllScopes({ sort: true }).map(_scope => _scope.id)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
config?.runExtraTest?.(container);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Variable Fixed Layout transform empty > test get Covers 1`] = `
|
||||
Map {
|
||||
"start_0" => [],
|
||||
"end_0" => [],
|
||||
"base_1" => [],
|
||||
"base_2" => [],
|
||||
"loop_1" => [],
|
||||
"base_in_loop_1" => [],
|
||||
"base_in_loop_2" => [],
|
||||
"base_in_loop_3" => [],
|
||||
"base_3" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Fixed Layout transform empty > test get Covers After Init Private 1`] = `
|
||||
Map {
|
||||
"start_0" => [],
|
||||
"end_0" => [],
|
||||
"base_1" => [],
|
||||
"base_2" => [],
|
||||
"loop_1" => [],
|
||||
"base_in_loop_1" => [],
|
||||
"base_in_loop_2" => [],
|
||||
"base_in_loop_3" => [],
|
||||
"base_3" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Fixed Layout transform empty > test get Deps 1`] = `
|
||||
Map {
|
||||
"start_0" => [],
|
||||
"end_0" => [],
|
||||
"base_1" => [],
|
||||
"base_2" => [],
|
||||
"loop_1" => [],
|
||||
"base_in_loop_1" => [],
|
||||
"base_in_loop_2" => [],
|
||||
"base_in_loop_3" => [],
|
||||
"base_3" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Fixed Layout transform empty > test get Deps After Init Private 1`] = `
|
||||
Map {
|
||||
"start_0" => [],
|
||||
"end_0" => [],
|
||||
"base_1" => [],
|
||||
"base_2" => [],
|
||||
"loop_1" => [],
|
||||
"base_in_loop_1" => [],
|
||||
"base_in_loop_2" => [],
|
||||
"base_in_loop_3" => [],
|
||||
"base_3" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Fixed Layout transform empty > test get private scope Covers 1`] = `
|
||||
Map {
|
||||
"start_0_private" => [],
|
||||
"end_0_private" => [],
|
||||
"base_1_private" => [],
|
||||
"base_2_private" => [],
|
||||
"loop_1_private" => [],
|
||||
"base_in_loop_1_private" => [],
|
||||
"base_in_loop_2_private" => [],
|
||||
"base_in_loop_3_private" => [],
|
||||
"base_3_private" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Fixed Layout transform empty > test get private scope Deps 1`] = `
|
||||
Map {
|
||||
"start_0_private" => [],
|
||||
"end_0_private" => [],
|
||||
"base_1_private" => [],
|
||||
"base_2_private" => [],
|
||||
"loop_1_private" => [],
|
||||
"base_in_loop_1_private" => [],
|
||||
"base_in_loop_2_private" => [],
|
||||
"base_in_loop_3_private" => [],
|
||||
"base_3_private" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Fixed Layout transform empty > test sort 1`] = `
|
||||
[
|
||||
"start_0",
|
||||
"testScope",
|
||||
"end_0",
|
||||
"base_1",
|
||||
"base_2",
|
||||
"loop_1",
|
||||
"base_in_loop_1",
|
||||
"base_in_loop_2",
|
||||
"base_in_loop_3",
|
||||
"base_3",
|
||||
"start_0_private",
|
||||
"end_0_private",
|
||||
"base_1_private",
|
||||
"base_2_private",
|
||||
"loop_1_private",
|
||||
"base_in_loop_1_private",
|
||||
"base_in_loop_2_private",
|
||||
"base_in_loop_3_private",
|
||||
"base_3_private",
|
||||
]
|
||||
`;
|
||||
@ -107,3 +107,111 @@ exports[`Variable Free Layout > test sort 1`] = `
|
||||
"base_3_private",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Variable Free Layout transform empty > test get Covers 1`] = `
|
||||
Map {
|
||||
"start_0" => [],
|
||||
"end_0" => [],
|
||||
"base_1" => [],
|
||||
"base_2" => [],
|
||||
"loop_1" => [],
|
||||
"base_in_loop_1" => [],
|
||||
"base_in_loop_2" => [],
|
||||
"base_in_loop_3" => [],
|
||||
"base_3" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Free Layout transform empty > test get Covers After Init Private 1`] = `
|
||||
Map {
|
||||
"start_0" => [],
|
||||
"end_0" => [],
|
||||
"base_1" => [],
|
||||
"base_2" => [],
|
||||
"loop_1" => [],
|
||||
"base_in_loop_1" => [],
|
||||
"base_in_loop_2" => [],
|
||||
"base_in_loop_3" => [],
|
||||
"base_3" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Free Layout transform empty > test get Deps 1`] = `
|
||||
Map {
|
||||
"start_0" => [],
|
||||
"end_0" => [],
|
||||
"base_1" => [],
|
||||
"base_2" => [],
|
||||
"loop_1" => [],
|
||||
"base_in_loop_1" => [],
|
||||
"base_in_loop_2" => [],
|
||||
"base_in_loop_3" => [],
|
||||
"base_3" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Free Layout transform empty > test get Deps After Init Private 1`] = `
|
||||
Map {
|
||||
"start_0" => [],
|
||||
"end_0" => [],
|
||||
"base_1" => [],
|
||||
"base_2" => [],
|
||||
"loop_1" => [],
|
||||
"base_in_loop_1" => [],
|
||||
"base_in_loop_2" => [],
|
||||
"base_in_loop_3" => [],
|
||||
"base_3" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Free Layout transform empty > test get private scope Covers 1`] = `
|
||||
Map {
|
||||
"start_0_private" => [],
|
||||
"end_0_private" => [],
|
||||
"base_1_private" => [],
|
||||
"base_2_private" => [],
|
||||
"loop_1_private" => [],
|
||||
"base_in_loop_1_private" => [],
|
||||
"base_in_loop_2_private" => [],
|
||||
"base_in_loop_3_private" => [],
|
||||
"base_3_private" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Free Layout transform empty > test get private scope Deps 1`] = `
|
||||
Map {
|
||||
"start_0_private" => [],
|
||||
"end_0_private" => [],
|
||||
"base_1_private" => [],
|
||||
"base_2_private" => [],
|
||||
"loop_1_private" => [],
|
||||
"base_in_loop_1_private" => [],
|
||||
"base_in_loop_2_private" => [],
|
||||
"base_in_loop_3_private" => [],
|
||||
"base_3_private" => [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Variable Free Layout transform empty > test sort 1`] = `
|
||||
[
|
||||
"testScope",
|
||||
"start_0",
|
||||
"end_0",
|
||||
"base_1",
|
||||
"base_2",
|
||||
"loop_1",
|
||||
"base_in_loop_1",
|
||||
"base_in_loop_2",
|
||||
"base_in_loop_3",
|
||||
"base_3",
|
||||
"start_0_private",
|
||||
"end_0_private",
|
||||
"base_1_private",
|
||||
"base_2_private",
|
||||
"loop_1_private",
|
||||
"base_in_loop_1_private",
|
||||
"base_in_loop_2_private",
|
||||
"base_in_loop_3_private",
|
||||
"base_3_private",
|
||||
]
|
||||
`;
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
import { ScopeChainTransformService } from '../src';
|
||||
import { runFixedLayoutTest } from '../__mocks__/run-fixed-layout-test';
|
||||
import { freeLayout1 } from '../__mocks__/free-layout-specs';
|
||||
|
||||
runFixedLayoutTest('Variable Fixed Layout transform empty', freeLayout1, {
|
||||
onInit(container) {
|
||||
const transformService = container.get(ScopeChainTransformService);
|
||||
|
||||
transformService.registerTransformer('MOCK', {
|
||||
transformCovers: (scopes) => scopes,
|
||||
transformDeps: (scopes) => scopes,
|
||||
});
|
||||
|
||||
// again transformer, prevent duplicated transformerId
|
||||
transformService.registerTransformer('MOCK', {
|
||||
transformCovers: () => [],
|
||||
transformDeps: () => [],
|
||||
});
|
||||
transformService.registerTransformer('MOCK', {
|
||||
transformCovers: () => [],
|
||||
transformDeps: () => [],
|
||||
});
|
||||
},
|
||||
runExtraTest: (container) => {
|
||||
test('check has transformer', () => {
|
||||
const transformService = container.get(ScopeChainTransformService);
|
||||
expect(transformService.hasTransformer('MOCK')).to.be.true;
|
||||
expect(transformService.hasTransformer('VARIABLE_LAYOUT_CONFIG')).to.be.false;
|
||||
expect(transformService.hasTransformer('NOT_EXIST')).to.be.false;
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -1,8 +1,17 @@
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
import { ScopeChainTransformService } from '../src';
|
||||
import { runFreeLayoutTest } from '../__mocks__/run-free-layout-test';
|
||||
import { freeLayout1 } from '../__mocks__/free-layout-specs';
|
||||
|
||||
runFreeLayoutTest('Variable Free Layout', freeLayout1, {
|
||||
runFreeLayoutTest('Variable Free Layout transform empty', freeLayout1, {
|
||||
// 模拟清空作用域
|
||||
transformCovers: () => [],
|
||||
transformDeps: () => [],
|
||||
runExtraTest: (container) => {
|
||||
test('check has transformer', () => {
|
||||
const transformService = container.get(ScopeChainTransformService);
|
||||
expect(transformService.hasTransformer('VARIABLE_LAYOUT_CONFIG')).to.be.true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -136,9 +136,11 @@ export class FixedLayoutScopeChain extends ScopeChain {
|
||||
|
||||
// If scope is GlobalScope, return all scopes except GlobalScope
|
||||
if (GlobalScope.is(scope)) {
|
||||
return this.variableEngine
|
||||
const scopes = this.variableEngine
|
||||
.getAllScopes({ sort: true })
|
||||
.filter((_scope) => !GlobalScope.is(_scope));
|
||||
|
||||
return this.transformService.transformCovers(scopes, { scope });
|
||||
}
|
||||
|
||||
const node = scope.meta.node;
|
||||
|
||||
@ -53,19 +53,19 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
||||
|
||||
// 获取同一层级所有输入节点
|
||||
protected getAllInputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {
|
||||
const currParent = this.getParent(curr);
|
||||
const currParent = this.getNodeParent(curr);
|
||||
|
||||
return (curr.getData(WorkflowNodeLinesData)?.allInputNodes || []).filter(
|
||||
(_node) => this.getParent(_node) === currParent
|
||||
(_node) => this.getNodeParent(_node) === currParent
|
||||
);
|
||||
}
|
||||
|
||||
// 获取同一层级所有输出节点
|
||||
protected getAllOutputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {
|
||||
const currParent = this.getParent(curr);
|
||||
const currParent = this.getNodeParent(curr);
|
||||
|
||||
return (curr.getData(WorkflowNodeLinesData)?.allOutputNodes || []).filter(
|
||||
(_node) => this.getParent(_node) === currParent
|
||||
(_node) => this.getNodeParent(_node) === currParent
|
||||
);
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
||||
deps.push(currVarData.private);
|
||||
}
|
||||
|
||||
curr = this.getParent(curr);
|
||||
curr = this.getNodeParent(curr);
|
||||
}
|
||||
|
||||
// If scope is GlobalScope, add globalScope to deps
|
||||
@ -110,9 +110,11 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
||||
getCovers(scope: FlowNodeScope): FlowNodeScope[] {
|
||||
// If scope is GlobalScope, return all scopes except GlobalScope
|
||||
if (GlobalScope.is(scope)) {
|
||||
return this.variableEngine
|
||||
const scopes = this.variableEngine
|
||||
.getAllScopes({ sort: true })
|
||||
.filter((_scope) => !GlobalScope.is(_scope));
|
||||
|
||||
return this.transformService.transformCovers(scopes, { scope });
|
||||
}
|
||||
|
||||
const { node } = scope.meta || {};
|
||||
@ -127,7 +129,7 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
||||
|
||||
if (isPrivate) {
|
||||
// private 只能覆盖其子节点
|
||||
queue.push(...this.getChildren(node));
|
||||
queue.push(...this.getNodeChildren(node));
|
||||
} else {
|
||||
// 否则覆盖其所有输出线的节点
|
||||
queue.push(...(this.getAllOutputLayerNodes(node) || []));
|
||||
@ -140,7 +142,7 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
||||
const _node = queue.shift()!;
|
||||
const variableData: FlowNodeVariableData = _node.getData(FlowNodeVariableData);
|
||||
scopes.push(...variableData.allScopes);
|
||||
const children = _node && this.getChildren(_node);
|
||||
const children = _node && this.getNodeChildren(_node);
|
||||
|
||||
if (children?.length) {
|
||||
queue.push(...children);
|
||||
@ -158,9 +160,9 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
||||
return this.transformService.transformCovers(uniqScopes, { scope });
|
||||
}
|
||||
|
||||
getChildren(node: FlowNodeEntity): FlowNodeEntity[] {
|
||||
if (this.configs?.getFreeChildren) {
|
||||
return this.configs.getFreeChildren?.(node);
|
||||
getNodeChildren(node: FlowNodeEntity): FlowNodeEntity[] {
|
||||
if (this.configs?.getNodeChildren) {
|
||||
return this.configs.getNodeChildren?.(node);
|
||||
}
|
||||
const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();
|
||||
const subCanvas = nodeMeta.subCanvas?.(node);
|
||||
@ -178,10 +180,10 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
||||
return this.tree.getChildren(node);
|
||||
}
|
||||
|
||||
getParent(node: FlowNodeEntity): FlowNodeEntity | undefined {
|
||||
getNodeParent(node: FlowNodeEntity): FlowNodeEntity | undefined {
|
||||
// 部分场景通过连线来表达父子关系,因此需要上层配置
|
||||
if (this.configs?.getFreeParent) {
|
||||
return this.configs.getFreeParent(node);
|
||||
if (this.configs?.getNodeParent) {
|
||||
return this.configs.getNodeParent(node);
|
||||
}
|
||||
let parent = node.document.originTree.getParent(node);
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { VariableEngine } from '@flowgram.ai/variable-core';
|
||||
import { BaseVariableField, VariableEngine } from '@flowgram.ai/variable-core';
|
||||
import { type ASTNode, ASTNodeJSON } from '@flowgram.ai/variable-core';
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { EntityData } from '@flowgram.ai/core';
|
||||
@ -179,4 +179,22 @@ export class FlowNodeVariableData extends EntityData {
|
||||
}
|
||||
return this._private;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a variable field by key path in the public scope by scope chain.
|
||||
* @param keyPath - The key path of the variable field.
|
||||
* @returns The variable field, or undefined if not found.
|
||||
*/
|
||||
getByKeyPath(keyPath: string[]): BaseVariableField | undefined {
|
||||
return this.public.available.getByKeyPath(keyPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a variable field by key path in the private scope by scope chain.
|
||||
* @param keyPath - The key path of the variable field.
|
||||
* @returns The variable field, or undefined if not found.
|
||||
*/
|
||||
getByKeyPathInPrivate(keyPath: string[]): BaseVariableField | undefined {
|
||||
return this.private?.available.getByKeyPath(keyPath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,3 +9,4 @@ export {
|
||||
} from './types';
|
||||
export { GlobalScope, bindGlobalScope } from './scopes/global-scope';
|
||||
export { ScopeChainTransformService } from './services/scope-chain-transform-service';
|
||||
export { getNodeScope, getNodePrivateScope } from './utils';
|
||||
|
||||
@ -1,62 +1,13 @@
|
||||
import { injectable, interfaces } from 'inversify';
|
||||
import { ASTNode, ASTNodeJSON, Scope, VariableEngine } from '@flowgram.ai/variable-core';
|
||||
import { Scope, VariableEngine } from '@flowgram.ai/variable-core';
|
||||
|
||||
@injectable()
|
||||
export class GlobalScope extends Scope {
|
||||
static readonly ID = Symbol('GlobalScope');
|
||||
|
||||
static is(scope: Scope): scope is GlobalScope {
|
||||
static is(scope: Scope) {
|
||||
return scope.id === GlobalScope.ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a variable in the Global Scope with the given key and JSON value.
|
||||
*
|
||||
* @param key - The key under which the variable will be stored.
|
||||
* @param json - The JSON value to store.
|
||||
* @returns The updated AST node.
|
||||
*/
|
||||
public setVar(key: string, json: ASTNodeJSON): ASTNode;
|
||||
|
||||
/**
|
||||
* Sets a variable in the Global Scope with the default key 'outputs'.
|
||||
*
|
||||
* @param json - The JSON value to store.
|
||||
* @returns The updated AST node.
|
||||
*/
|
||||
public setVar(json: ASTNodeJSON): ASTNode;
|
||||
|
||||
public setVar(arg1: string | ASTNodeJSON, arg2?: ASTNodeJSON): ASTNode {
|
||||
if (typeof arg1 === 'string' && arg2 !== undefined) {
|
||||
return this.ast.set(arg1, arg2);
|
||||
}
|
||||
|
||||
if (typeof arg1 === 'object' && arg2 === undefined) {
|
||||
return this.ast.set('outputs', arg1);
|
||||
}
|
||||
|
||||
throw new Error('Invalid arguments');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a variable from the Global Scope by key.
|
||||
*
|
||||
* @param key - The key of the variable to retrieve. Defaults to 'outputs'.
|
||||
* @returns The value of the variable, or undefined if not found.
|
||||
*/
|
||||
public getVar(key: string = 'outputs') {
|
||||
return this.ast.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a variable from the Global Scope by key.
|
||||
*
|
||||
* @param key - The key of the variable to clear. Defaults to 'outputs'.
|
||||
* @returns The updated AST node.
|
||||
*/
|
||||
public clearVar(key: string = 'outputs') {
|
||||
return this.ast.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
export const bindGlobalScope = (bind: interfaces.Bind) => {
|
||||
|
||||
@ -14,11 +14,14 @@ export interface TransformerContext {
|
||||
|
||||
export type IScopeTransformer = (scopes: Scope[], ctx: TransformerContext) => Scope[];
|
||||
|
||||
const passthrough: IScopeTransformer = (scopes, ctx) => scopes;
|
||||
|
||||
@injectable()
|
||||
export class ScopeChainTransformService {
|
||||
protected transformDepsFns: IScopeTransformer[] = [];
|
||||
|
||||
protected transformCoversFns: IScopeTransformer[] = [];
|
||||
protected transformerMap: Map<
|
||||
string,
|
||||
{ transformDeps: IScopeTransformer; transformCovers: IScopeTransformer }
|
||||
> = new Map();
|
||||
|
||||
@lazyInject(FlowDocument) document: FlowDocument;
|
||||
|
||||
@ -29,43 +32,65 @@ export class ScopeChainTransformService {
|
||||
@inject(VariableLayoutConfig)
|
||||
protected configs?: VariableLayoutConfig
|
||||
) {
|
||||
if (this.configs?.transformDeps) {
|
||||
this.transformDepsFns.push(this.configs.transformDeps);
|
||||
}
|
||||
if (this.configs?.transformCovers) {
|
||||
this.transformCoversFns.push(this.configs.transformCovers);
|
||||
if (this.configs?.transformDeps || this.configs?.transformCovers) {
|
||||
this.transformerMap.set('VARIABLE_LAYOUT_CONFIG', {
|
||||
transformDeps: this.configs.transformDeps || passthrough,
|
||||
transformCovers: this.configs.transformCovers || passthrough,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerTransformDeps(transformer: IScopeTransformer) {
|
||||
this.transformDepsFns.push(transformer);
|
||||
/**
|
||||
* check if transformer registered
|
||||
* @param transformerId used to identify transformer, prevent duplicated
|
||||
* @returns
|
||||
*/
|
||||
hasTransformer(transformerId: string) {
|
||||
return this.transformerMap.has(transformerId);
|
||||
}
|
||||
|
||||
registerTransformCovers(transformer: IScopeTransformer) {
|
||||
this.transformCoversFns.push(transformer);
|
||||
/**
|
||||
* register new transform function
|
||||
* @param transformerId used to identify transformer, prevent duplicated transformer
|
||||
* @param transformer
|
||||
*/
|
||||
registerTransformer(
|
||||
transformerId: string,
|
||||
transformer: {
|
||||
transformDeps: IScopeTransformer;
|
||||
transformCovers: IScopeTransformer;
|
||||
}
|
||||
) {
|
||||
this.transformerMap.set(transformerId, transformer);
|
||||
}
|
||||
|
||||
transformDeps(scopes: Scope[], { scope }: { scope: Scope }): Scope[] {
|
||||
return this.transformDepsFns.reduce(
|
||||
(scopes, transformer) =>
|
||||
transformer(scopes, {
|
||||
scope,
|
||||
document: this.document,
|
||||
variableEngine: this.variableEngine,
|
||||
}),
|
||||
scopes
|
||||
);
|
||||
return Array.from(this.transformerMap.values()).reduce((scopes, transformer) => {
|
||||
if (!transformer.transformDeps) {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
scopes = transformer.transformDeps(scopes, {
|
||||
scope,
|
||||
document: this.document,
|
||||
variableEngine: this.variableEngine,
|
||||
});
|
||||
return scopes;
|
||||
}, scopes);
|
||||
}
|
||||
|
||||
transformCovers(scopes: Scope[], { scope }: { scope: Scope }): Scope[] {
|
||||
return this.transformCoversFns.reduce(
|
||||
(scopes, transformer) =>
|
||||
transformer(scopes, {
|
||||
scope,
|
||||
document: this.document,
|
||||
variableEngine: this.variableEngine,
|
||||
}),
|
||||
scopes
|
||||
);
|
||||
return Array.from(this.transformerMap.values()).reduce((scopes, transformer) => {
|
||||
if (!transformer.transformCovers) {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
scopes = transformer.transformCovers(scopes, {
|
||||
scope,
|
||||
document: this.document,
|
||||
variableEngine: this.variableEngine,
|
||||
});
|
||||
return scopes;
|
||||
}, scopes);
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,4 +19,4 @@ export interface ScopeVirtualNode {
|
||||
export type ScopeChainNode = FlowNodeEntity | ScopeVirtualNode;
|
||||
|
||||
// 节点内部的作用域
|
||||
export type FlowNodeScope = Scope<FlowNodeScopeMeta>;
|
||||
export interface FlowNodeScope extends Scope<FlowNodeScopeMeta> {}
|
||||
|
||||
11
packages/variable-engine/variable-layout/src/utils.ts
Normal file
11
packages/variable-engine/variable-layout/src/utils.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
|
||||
import { FlowNodeVariableData } from './flow-node-variable-data';
|
||||
|
||||
export function getNodeScope(node: FlowNodeEntity) {
|
||||
return node.getData(FlowNodeVariableData).public;
|
||||
}
|
||||
|
||||
export function getNodePrivateScope(node: FlowNodeEntity) {
|
||||
return node.getData(FlowNodeVariableData).initPrivate();
|
||||
}
|
||||
@ -12,19 +12,18 @@ export interface VariableLayoutConfig {
|
||||
isNodeChildrenPrivate?: (node: ScopeChainNode) => boolean;
|
||||
|
||||
/**
|
||||
* 用于自由画布场景,部分场景通过连线或者其他交互形式来表达节点之间的父子关系,需要可配置化
|
||||
* 用于固定布局场景时:父子中间存在大量无用节点(如 inlineBlocks 等,需要配置化略过)
|
||||
* 用于自由画布场景时:部分场景通过连线或者其他交互形式来表达节点之间的父子关系,需可配置化
|
||||
*/
|
||||
getFreeChildren?: (node: FlowNodeEntity) => FlowNodeEntity[];
|
||||
getFreeParent?: (node: FlowNodeEntity) => FlowNodeEntity | undefined;
|
||||
getNodeChildren?: (node: FlowNodeEntity) => FlowNodeEntity[];
|
||||
getNodeParent?: (node: FlowNodeEntity) => FlowNodeEntity | undefined;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* 对依赖作用域进行微调
|
||||
*/
|
||||
transformDeps?: IScopeTransformer;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* 对依赖作用域进行微调
|
||||
*/
|
||||
transformCovers?: IScopeTransformer;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user