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}
|
onChange={field.onChange}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||||
schema={property}
|
constantProps={{
|
||||||
|
schema: property,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Feedback errors={fieldState?.errors} warnings={fieldState?.warnings} />
|
<Feedback errors={fieldState?.errors} warnings={fieldState?.warnings} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@ -14,6 +14,7 @@ interface FormItemProps {
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
labelWidth?: number;
|
labelWidth?: number;
|
||||||
|
vertical?: boolean;
|
||||||
}
|
}
|
||||||
export function FormItem({
|
export function FormItem({
|
||||||
children,
|
children,
|
||||||
@ -22,6 +23,7 @@ export function FormItem({
|
|||||||
description,
|
description,
|
||||||
type,
|
type,
|
||||||
labelWidth,
|
labelWidth,
|
||||||
|
vertical,
|
||||||
}: FormItemProps): JSX.Element {
|
}: FormItemProps): JSX.Element {
|
||||||
const renderTitle = useCallback(
|
const renderTitle = useCallback(
|
||||||
(showTooltip?: boolean) => (
|
(showTooltip?: boolean) => (
|
||||||
@ -42,9 +44,13 @@ export function FormItem({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
...(vertical
|
||||||
|
? { flexDirection: 'column' }
|
||||||
|
: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -59,7 +59,9 @@ export const PropertyEdit: React.FC<PropertyEditProps> = (props) => {
|
|||||||
<DynamicValueInput
|
<DynamicValueInput
|
||||||
value={value.default}
|
value={value.default}
|
||||||
onChange={(val) => updateProperty('default', val)}
|
onChange={(val) => updateProperty('default', val)}
|
||||||
schema={value}
|
constantProps={{
|
||||||
|
schema: value,
|
||||||
|
}}
|
||||||
style={{ flexGrow: 1 }}
|
style={{ flexGrow: 1 }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,6 +130,20 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
title: 'Loop_1',
|
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: [
|
blocks: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
PositionSchema,
|
PositionSchema,
|
||||||
FlowNodeTransformData,
|
FlowNodeTransformData,
|
||||||
} from '@flowgram.ai/free-layout-editor';
|
} 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 { defaultFormMeta } from '../default-form-meta';
|
||||||
import { FlowNodeRegistry } from '../../typings';
|
import { FlowNodeRegistry } from '../../typings';
|
||||||
@ -73,5 +73,6 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
|
|||||||
effect: {
|
effect: {
|
||||||
batchFor: provideBatchInputEffect,
|
batchFor: provideBatchInputEffect,
|
||||||
},
|
},
|
||||||
|
plugins: [createBatchOutputsFormPlugin({ outputKey: 'batchOutputs' })],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { FormRenderProps, FlowNodeJSON, Field } from '@flowgram.ai/free-layout-editor';
|
import { FormRenderProps, FlowNodeJSON, Field } from '@flowgram.ai/free-layout-editor';
|
||||||
import { SubCanvasRender } from '@flowgram.ai/free-container-plugin';
|
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 { useIsSidebar, useNodeRenderContext } from '../../hooks';
|
||||||
import { FormHeader, FormContent, FormOutputs, FormItem, Feedback } from '../../form-components';
|
import { FormHeader, FormContent, FormOutputs, FormItem, Feedback } from '../../form-components';
|
||||||
@ -33,12 +33,30 @@ export const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {
|
|||||||
</Field>
|
</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) {
|
if (isSidebar) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormHeader />
|
<FormHeader />
|
||||||
<FormContent>
|
<FormContent>
|
||||||
{batchFor}
|
{batchFor}
|
||||||
|
{batchOutputs}
|
||||||
<FormOutputs />
|
<FormOutputs />
|
||||||
</FormContent>
|
</FormContent>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export interface Material {
|
|||||||
[key: string]: any; // For other properties from config.json
|
[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[] {
|
export function listAllMaterials(): Material[] {
|
||||||
const _materials: 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;
|
schema?: IJsonSchema;
|
||||||
constantProps?: {
|
constantProps?: {
|
||||||
strategies?: Strategy[];
|
strategies?: Strategy[];
|
||||||
|
schema?: IJsonSchema; // set schema of constant input only
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,3 +5,4 @@ export * from './batch-variable-selector';
|
|||||||
export * from './constant-input';
|
export * from './constant-input';
|
||||||
export * from './dynamic-value-input';
|
export * from './dynamic-value-input';
|
||||||
export * from './condition-row';
|
export * from './condition-row';
|
||||||
|
export * from './batch-outputs';
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
import { IFlowRefValue } from '../../typings';
|
import { IFlowRefValue } from '../../typings';
|
||||||
|
|
||||||
export const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({
|
export const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({
|
||||||
private: true,
|
|
||||||
parse: (value: Record<string, IFlowRefValue>, ctx) => [
|
parse: (value: Record<string, IFlowRefValue>, ctx) => [
|
||||||
ASTFactory.createVariableDeclaration({
|
ASTFactory.createVariableDeclaration({
|
||||||
key: `${ctx.node.id}`,
|
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 './effects';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './typings';
|
export * from './typings';
|
||||||
|
export * from './form-plugins';
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { injectable } from 'inversify';
|
|||||||
import { DisposableCollection, Event, MaybePromise } from '@flowgram.ai/utils';
|
import { DisposableCollection, Event, MaybePromise } from '@flowgram.ai/utils';
|
||||||
import { type FlowNodeEntity } from '@flowgram.ai/document';
|
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 { FormManager } from '../services/form-manager';
|
||||||
import { type FormItem } from '.';
|
import { type FormItem } from '.';
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ export abstract class FormModel {
|
|||||||
*/
|
*/
|
||||||
abstract get formManager(): FormManager;
|
abstract get formManager(): FormManager;
|
||||||
|
|
||||||
abstract get formMeta(): IFormMeta;
|
abstract get formMeta(): any;
|
||||||
|
|
||||||
abstract get initialized(): boolean;
|
abstract get initialized(): boolean;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import { MaybePromise } from '@flowgram.ai/utils';
|
||||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||||
import { PlaygroundContext } from '@flowgram.ai/core';
|
import { PlaygroundContext } from '@flowgram.ai/core';
|
||||||
import { MaybePromise } from '@flowgram.ai/utils';
|
|
||||||
|
|
||||||
import { type FormItemAbilityMeta } from './form-ability.types';
|
import { type FormItemAbilityMeta } from './form-ability.types';
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ export interface IFormMeta {
|
|||||||
/**
|
/**
|
||||||
* 表单树结构root
|
* 表单树结构root
|
||||||
*/
|
*/
|
||||||
root: IFormItemMeta;
|
root?: IFormItemMeta;
|
||||||
/**
|
/**
|
||||||
* 表单全局配置
|
* 表单全局配置
|
||||||
*/
|
*/
|
||||||
@ -109,7 +109,7 @@ export interface FormMetaGeneratorParams<PlaygroundContext, FormValue = any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type FormMetaGenerator<PlaygroundContext = any, FormValue = any> = (
|
export type FormMetaGenerator<PlaygroundContext = any, FormValue = any> = (
|
||||||
params: FormMetaGeneratorParams<FormValue, FormValue>,
|
params: FormMetaGeneratorParams<FormValue, FormValue>
|
||||||
) => MaybePromise<IFormMeta>;
|
) => MaybePromise<IFormMeta>;
|
||||||
|
|
||||||
export type FormMetaOrFormMetaGenerator = FormMetaGenerator | IFormMeta;
|
export type FormMetaOrFormMetaGenerator = FormMetaGenerator | IFormMeta;
|
||||||
|
|||||||
@ -42,14 +42,15 @@ describe('FormModelV2', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formItem = formModelV2.getFormItemByPath('/');
|
const formItem = formModelV2.getFormItemByPath('/');
|
||||||
expect(formItem.value).toEqual({
|
expect(formItem?.value).toEqual({
|
||||||
a: 1,
|
a: 1,
|
||||||
b: 2,
|
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,
|
a: 3,
|
||||||
b: 4,
|
b: 4,
|
||||||
});
|
});
|
||||||
@ -336,7 +337,8 @@ describe('FormModelV2', () => {
|
|||||||
});
|
});
|
||||||
it('should call onInit when formModel init', () => {
|
it('should call onInit when formModel init', () => {
|
||||||
const mockInit = vi.fn();
|
const mockInit = vi.fn();
|
||||||
const plugin = defineFormPluginCreator('test', {
|
const plugin = defineFormPluginCreator({
|
||||||
|
name: 'test',
|
||||||
onInit: mockInit,
|
onInit: mockInit,
|
||||||
})({ opt1: 1 });
|
})({ opt1: 1 });
|
||||||
const formMeta = {
|
const formMeta = {
|
||||||
@ -353,7 +355,8 @@ describe('FormModelV2', () => {
|
|||||||
});
|
});
|
||||||
it('should call onDispose when formModel dispose', () => {
|
it('should call onDispose when formModel dispose', () => {
|
||||||
const mockDispose = vi.fn();
|
const mockDispose = vi.fn();
|
||||||
const plugin = defineFormPluginCreator('test', {
|
const plugin = defineFormPluginCreator({
|
||||||
|
name: 'test',
|
||||||
onDispose: mockDispose,
|
onDispose: mockDispose,
|
||||||
})({ opt1: 1 });
|
})({ opt1: 1 });
|
||||||
const formMeta = {
|
const formMeta = {
|
||||||
@ -373,14 +376,17 @@ describe('FormModelV2', () => {
|
|||||||
const mockEffectPlugin = vi.fn();
|
const mockEffectPlugin = vi.fn();
|
||||||
const mockEffectOrigin = vi.fn();
|
const mockEffectOrigin = vi.fn();
|
||||||
|
|
||||||
const plugin = defineFormPluginCreator('test', {
|
const plugin = defineFormPluginCreator({
|
||||||
effect: {
|
name: 'test',
|
||||||
a: [
|
onSetupFormMeta(ctx, opts) {
|
||||||
{
|
ctx.mergeEffect({
|
||||||
event: DataEvent.onValueInitOrChange,
|
a: [
|
||||||
effect: mockEffectPlugin,
|
{
|
||||||
},
|
event: DataEvent.onValueInitOrChange,
|
||||||
],
|
effect: mockEffectPlugin,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
})({ opt1: 1 });
|
})({ opt1: 1 });
|
||||||
|
|
||||||
@ -407,20 +413,23 @@ describe('FormModelV2', () => {
|
|||||||
const mockEffectOriginArrStar = vi.fn();
|
const mockEffectOriginArrStar = vi.fn();
|
||||||
const mockEffectPluginOther = vi.fn();
|
const mockEffectPluginOther = vi.fn();
|
||||||
|
|
||||||
const plugin = defineFormPluginCreator('test', {
|
const plugin = defineFormPluginCreator({
|
||||||
effect: {
|
name: 'test',
|
||||||
'arr.*': [
|
onSetupFormMeta(ctx, opts) {
|
||||||
{
|
ctx.mergeEffect({
|
||||||
event: DataEvent.onValueChange,
|
'arr.*': [
|
||||||
effect: mockEffectPluginArrStar,
|
{
|
||||||
},
|
event: DataEvent.onValueChange,
|
||||||
],
|
effect: mockEffectPluginArrStar,
|
||||||
other: [
|
},
|
||||||
{
|
],
|
||||||
event: DataEvent.onValueChange,
|
other: [
|
||||||
effect: mockEffectPluginOther,
|
{
|
||||||
},
|
event: DataEvent.onValueChange,
|
||||||
],
|
effect: mockEffectPluginOther,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
})({ opt1: 1 });
|
})({ opt1: 1 });
|
||||||
|
|
||||||
|
|||||||
@ -26,12 +26,7 @@ import {
|
|||||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||||
import { PlaygroundContext, PluginContext } from '@flowgram.ai/core';
|
import { PlaygroundContext, PluginContext } from '@flowgram.ai/core';
|
||||||
|
|
||||||
import {
|
import { convertGlobPath, findMatchedInMap, formFeedbacksToNodeCoreFormFeedbacks } from './utils';
|
||||||
convertGlobPath,
|
|
||||||
findMatchedInMap,
|
|
||||||
formFeedbacksToNodeCoreFormFeedbacks,
|
|
||||||
mergeEffectMap,
|
|
||||||
} from './utils';
|
|
||||||
import {
|
import {
|
||||||
DataEvent,
|
DataEvent,
|
||||||
Effect,
|
Effect,
|
||||||
@ -133,8 +128,10 @@ export class FormModelV2 extends FormModel implements Disposable {
|
|||||||
return this._formControl;
|
return this._formControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
get formMeta() {
|
protected _formMeta: FormMeta;
|
||||||
return this.node.getNodeRegistry().formMeta;
|
|
||||||
|
get formMeta(): FormMeta {
|
||||||
|
return this._formMeta || (this.node.getNodeRegistry().formMeta as FormMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
get values() {
|
get values() {
|
||||||
@ -195,9 +192,6 @@ export class FormModelV2 extends FormModel implements Disposable {
|
|||||||
this.plugins = plugins;
|
this.plugins = plugins;
|
||||||
plugins.forEach((plugin) => {
|
plugins.forEach((plugin) => {
|
||||||
plugin.init(this);
|
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();
|
formData.fireChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
(formMeta.plugins || [])?.forEach((_plugin) => {
|
||||||
|
if (_plugin.setupFormMeta) {
|
||||||
|
formMeta = _plugin.setupFormMeta(formMeta, this.nodeContext);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._formMeta = formMeta;
|
||||||
|
|
||||||
const { validateTrigger, validate, effect } = formMeta;
|
const { validateTrigger, validate, effect } = formMeta;
|
||||||
if (effect) {
|
if (effect) {
|
||||||
this.effectMap = effect;
|
this.effectMap = effect;
|
||||||
|
|||||||
@ -1,23 +1,31 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { Disposable } from '@flowgram.ai/utils';
|
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';
|
import { FormModelV2 } from './form-model-v2';
|
||||||
|
|
||||||
export interface FormPluginConfig<Opts = any> {
|
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 初始化时执行
|
* FormModel 初始化时执行
|
||||||
* @param ctx
|
* @param ctx
|
||||||
*/
|
*/
|
||||||
onInit?: (ctx: FormPluginCtx, opts: Opts) => void;
|
onInit?: (ctx: FormPluginCtx, opts: Opts) => void;
|
||||||
|
|
||||||
/**
|
|
||||||
* 同 FormMeta 中的effects 会与 FormMeta 中的effects 合并
|
|
||||||
*/
|
|
||||||
effect?: Record<string, EffectOptions[]>;
|
|
||||||
/**
|
/**
|
||||||
* FormModel 销毁时执行
|
* FormModel 销毁时执行
|
||||||
* @param ctx
|
|
||||||
*/
|
*/
|
||||||
onDispose?: (ctx: FormPluginCtx, opts: Opts) => void;
|
onDispose?: (ctx: FormPluginCtx, opts: Opts) => void;
|
||||||
}
|
}
|
||||||
@ -33,10 +41,11 @@ export class FormPlugin<Opts = any> implements Disposable {
|
|||||||
|
|
||||||
protected _formModel: FormModelV2;
|
protected _formModel: FormModelV2;
|
||||||
|
|
||||||
constructor(name: string, config: FormPluginConfig, opts?: Opts) {
|
constructor(config: FormPluginConfig, opts?: Opts) {
|
||||||
this.name = name;
|
this.name = config?.name || '';
|
||||||
this.pluginId = `${name}__${nanoid()}`;
|
this.pluginId = `${this.name}__${nanoid()}`;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
this.opts = opts;
|
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) {
|
init(formModel: FormModelV2) {
|
||||||
this._formModel = formModel;
|
this._formModel = formModel;
|
||||||
this.config?.onInit?.(this.ctx, this.opts);
|
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 function (opts: Opts) {
|
||||||
return new FormPlugin(name, config, opts);
|
return new FormPlugin(config, opts);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,6 +138,13 @@ export type FormPluginCtx = {
|
|||||||
formModel: FormModelV2;
|
formModel: FormModelV2;
|
||||||
} & NodeContext;
|
} & 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> {
|
export interface onFormValueChangeInPayload<TValue = FieldValue, TFormValues = FieldValue> {
|
||||||
value: TValue;
|
value: TValue;
|
||||||
prevValue: TValue;
|
prevValue: TValue;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { type VariableProviderAbilityOptions } from '../types';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function createEffectFromVariableProvider(
|
export function createEffectFromVariableProvider(
|
||||||
options: VariableProviderAbilityOptions,
|
options: VariableProviderAbilityOptions
|
||||||
): EffectOptions[] {
|
): EffectOptions[] {
|
||||||
const getScope = (node: FlowNodeEntity): Scope => {
|
const getScope = (node: FlowNodeEntity): Scope => {
|
||||||
const variableData: FlowNodeVariableData = node.getData(FlowNodeVariableData);
|
const variableData: FlowNodeVariableData = node.getData(FlowNodeVariableData);
|
||||||
@ -42,7 +42,7 @@ export function createEffectFromVariableProvider(
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
event: DataEvent.onValueInit,
|
event: DataEvent.onValueInit,
|
||||||
effect: (params => {
|
effect: ((params) => {
|
||||||
const { context } = params;
|
const { context } = params;
|
||||||
|
|
||||||
const scope = getScope(context.node);
|
const scope = getScope(context.node);
|
||||||
@ -51,8 +51,6 @@ export function createEffectFromVariableProvider(
|
|||||||
scope,
|
scope,
|
||||||
options,
|
options,
|
||||||
formItem: undefined,
|
formItem: undefined,
|
||||||
// Hack: 新表单引擎暂时不支持 triggerSync
|
|
||||||
triggerSync: undefined as any,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (disposable) {
|
if (disposable) {
|
||||||
@ -65,7 +63,7 @@ export function createEffectFromVariableProvider(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: DataEvent.onValueChange,
|
event: DataEvent.onValueChange,
|
||||||
effect: (params => {
|
effect: ((params) => {
|
||||||
transformValueToAST(params);
|
transformValueToAST(params);
|
||||||
}) as Effect,
|
}) as Effect,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,20 +1,23 @@
|
|||||||
import { DataEvent, defineFormPluginCreator } from '@flowgram.ai/node';
|
import { DataEvent, defineFormPluginCreator } from '@flowgram.ai/node';
|
||||||
|
|
||||||
export const createVariableProviderPlugin = defineFormPluginCreator('VariableProviderPlugin', {
|
export const createVariableProviderPlugin = defineFormPluginCreator({
|
||||||
|
name: 'VariableProviderPlugin',
|
||||||
onInit: (ctx, opts) => {
|
onInit: (ctx, opts) => {
|
||||||
// todo
|
// todo
|
||||||
// console.log('>>> VariableProviderPlugin init', ctx, opts);
|
// console.log('>>> VariableProviderPlugin init', ctx, opts);
|
||||||
},
|
},
|
||||||
effect: {
|
onSetupFormMeta({ mergeEffect }) {
|
||||||
arr: [
|
mergeEffect({
|
||||||
{
|
arr: [
|
||||||
event: DataEvent.onValueInitOrChange,
|
{
|
||||||
effect: () => {
|
event: DataEvent.onValueInitOrChange,
|
||||||
// todo
|
effect: () => {
|
||||||
// console.log('>>> VariableProviderPlugin effect triggered');
|
// todo
|
||||||
|
// console.log('>>> VariableProviderPlugin effect triggered');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
});
|
||||||
},
|
},
|
||||||
onDispose: (ctx, opts) => {
|
onDispose: (ctx, opts) => {
|
||||||
// todo
|
// todo
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import {
|
|||||||
type ASTNodeJSON,
|
type ASTNodeJSON,
|
||||||
type VariableDeclarationJSON,
|
type VariableDeclarationJSON,
|
||||||
} from '@flowgram.ai/variable-plugin';
|
} from '@flowgram.ai/variable-plugin';
|
||||||
|
import { Disposable } from '@flowgram.ai/utils';
|
||||||
import { FormItem } from '@flowgram.ai/form-core';
|
import { FormItem } from '@flowgram.ai/form-core';
|
||||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||||
import { Disposable } from '@flowgram.ai/utils';
|
|
||||||
|
|
||||||
export interface VariableAbilityCommonContext {
|
export interface VariableAbilityCommonContext {
|
||||||
node: FlowNodeEntity; // 节点
|
node: FlowNodeEntity; // 节点
|
||||||
@ -14,9 +14,7 @@ export interface VariableAbilityCommonContext {
|
|||||||
options: VariableAbilityOptions;
|
options: VariableAbilityOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VariableAbilityInitCtx extends VariableAbilityCommonContext {
|
export interface VariableAbilityInitCtx extends VariableAbilityCommonContext {}
|
||||||
triggerSync: () => void; // 触发变量同步
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VariableAbilityOptions {
|
export interface VariableAbilityOptions {
|
||||||
// 变量提供能力可复用
|
// 变量提供能力可复用
|
||||||
|
|||||||
@ -1,3 +1,12 @@
|
|||||||
export * from './create-variable-plugin';
|
export * from './create-variable-plugin';
|
||||||
export * from '@flowgram.ai/variable-core';
|
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 { Disposable, DisposableCollection } from '@flowgram.ai/utils';
|
||||||
|
|
||||||
import { subsToDisposable } from '../utils/toDisposable';
|
import { subsToDisposable } from '../utils/toDisposable';
|
||||||
import { type Scope } from '../scope';
|
|
||||||
import { updateChildNodeHelper } from './utils/helpers';
|
import { updateChildNodeHelper } from './utils/helpers';
|
||||||
|
import { type Scope } from '../scope';
|
||||||
import {
|
import {
|
||||||
type ASTNodeJSON,
|
type ASTNodeJSON,
|
||||||
type ObserverOrNext,
|
type ObserverOrNext,
|
||||||
@ -73,7 +73,7 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
|||||||
/**
|
/**
|
||||||
* 节点的版本号,每 fireChange 一次 version + 1
|
* 节点的版本号,每 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(() => {
|
Disposable.create(() => {
|
||||||
// 子元素删除时,父元素触发更新
|
// 子元素删除时,父元素触发更新
|
||||||
this.parent?.fireChange();
|
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(
|
child.toDispose.push(
|
||||||
Disposable.create(() => {
|
Disposable.create(() => {
|
||||||
this._children.delete(child);
|
this._children.delete(child);
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return child;
|
return child;
|
||||||
@ -204,7 +204,7 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
|||||||
protected updateChildNodeByKey(keyInThis: keyof this, nextJSON?: ASTNodeJSON) {
|
protected updateChildNodeByKey(keyInThis: keyof this, nextJSON?: ASTNodeJSON) {
|
||||||
this.withBatchUpdate(updateChildNodeHelper).call(this, {
|
this.withBatchUpdate(updateChildNodeHelper).call(this, {
|
||||||
getChildNode: () => this[keyInThis] as ASTNode,
|
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),
|
removeChildNode: () => ((this as any)[keyInThis] = undefined),
|
||||||
nextJSON,
|
nextJSON,
|
||||||
});
|
});
|
||||||
@ -216,7 +216,7 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
protected withBatchUpdate<ParamTypes extends any[], ReturnType>(
|
protected withBatchUpdate<ParamTypes extends any[], ReturnType>(
|
||||||
updater: (...args: ParamTypes) => ReturnType,
|
updater: (...args: ParamTypes) => ReturnType
|
||||||
) {
|
) {
|
||||||
return (...args: ParamTypes) => {
|
return (...args: ParamTypes) => {
|
||||||
// batchUpdate 里面套 batchUpdate 只能生效一次
|
// batchUpdate 里面套 batchUpdate 只能生效一次
|
||||||
@ -281,7 +281,7 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
|||||||
*/
|
*/
|
||||||
subscribe<Data = this>(
|
subscribe<Data = this>(
|
||||||
observer: ObserverOrNext<Data>,
|
observer: ObserverOrNext<Data>,
|
||||||
{ selector, debounceAnimation, triggerOnInit }: SubscribeConfig<this, Data> = {},
|
{ selector, debounceAnimation, triggerOnInit }: SubscribeConfig<this, Data> = {}
|
||||||
): Disposable {
|
): Disposable {
|
||||||
return subsToDisposable(
|
return subsToDisposable(
|
||||||
this.value$
|
this.value$
|
||||||
@ -289,25 +289,25 @@ export abstract class ASTNode<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
|||||||
map(() => (selector ? selector(this) : (this as any))),
|
map(() => (selector ? selector(this) : (this as any))),
|
||||||
distinctUntilChanged(
|
distinctUntilChanged(
|
||||||
(a, b) => shallowEqual(a, b),
|
(a, b) => shallowEqual(a, b),
|
||||||
value => {
|
(value) => {
|
||||||
if (value instanceof ASTNode) {
|
if (value instanceof ASTNode) {
|
||||||
// 如果 value 是 ASTNode,则进行 hash 的比较
|
// 如果 value 是 ASTNode,则进行 hash 的比较
|
||||||
return value.hash;
|
return value.hash;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
},
|
}
|
||||||
),
|
),
|
||||||
// 默认跳过 BehaviorSubject 第一次触发
|
// 默认跳过 BehaviorSubject 第一次触发
|
||||||
triggerOnInit ? tap(() => null) : skip(1),
|
triggerOnInit ? tap(() => null) : skip(1),
|
||||||
// 每个 animationFrame 内所有更新合并成一个
|
// 每个 animationFrame 内所有更新合并成一个
|
||||||
debounceAnimation ? debounceTime(0, animationFrameScheduler) : tap(() => null),
|
debounceAnimation ? debounceTime(0, animationFrameScheduler) : tap(() => null)
|
||||||
)
|
)
|
||||||
.subscribe(observer),
|
.subscribe(observer)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchGlobalEvent<ActionType extends GlobalEventActionType = GlobalEventActionType>(
|
dispatchGlobalEvent<ActionType extends GlobalEventActionType = GlobalEventActionType>(
|
||||||
event: Omit<ActionType, 'ast'>,
|
event: Omit<ActionType, 'ast'>
|
||||||
) {
|
) {
|
||||||
this.scope.event.dispatch({
|
this.scope.event.dispatch({
|
||||||
...event,
|
...event,
|
||||||
|
|||||||
@ -35,6 +35,10 @@ export abstract class BaseVariableField<VariableMeta = any> extends ASTNode<
|
|||||||
return getParentFields(this);
|
return getParentFields(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get keyPath(): string[] {
|
||||||
|
return this.parentFields.reverse().map((_field) => _field.key);
|
||||||
|
}
|
||||||
|
|
||||||
get meta(): VariableMeta {
|
get meta(): VariableMeta {
|
||||||
return this._meta;
|
return this._meta;
|
||||||
}
|
}
|
||||||
@ -47,6 +51,10 @@ export abstract class BaseVariableField<VariableMeta = any> extends ASTNode<
|
|||||||
return this._initializer;
|
return this._initializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hash(): string {
|
||||||
|
return `[${this._version}]${this.keyPath.join('.')}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 VariableDeclarationJSON 从而生成变量声明节点
|
* 解析 VariableDeclarationJSON 从而生成变量声明节点
|
||||||
*/
|
*/
|
||||||
@ -96,7 +104,7 @@ export abstract class BaseVariableField<VariableMeta = any> extends ASTNode<
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
onTypeChange(observer: (type: ASTNode | undefined) => void) {
|
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() {
|
refreshReturnType() {
|
||||||
// 被遍历表达式的返回值
|
// 被遍历表达式的返回值
|
||||||
const childReturnTypeJSON = this.wrapFor?.returnType?.toJSON();
|
const childReturnTypeJSON = this.wrapFor?.returnType?.toJSON();
|
||||||
|
|
||||||
this.updateChildNodeByKey('_returnType', {
|
this.updateChildNodeByKey('_returnType', {
|
||||||
kind: ASTKind.Array,
|
kind: ASTKind.Array,
|
||||||
items: childReturnTypeJSON,
|
items: childReturnTypeJSON,
|
||||||
@ -51,9 +52,12 @@ export class WrapArrayExpression extends BaseExpression<WrapArrayExpressionJSON>
|
|||||||
|
|
||||||
@postConstructAST()
|
@postConstructAST()
|
||||||
protected init() {
|
protected init() {
|
||||||
|
this.refreshReturnType = this.refreshReturnType.bind(this);
|
||||||
|
|
||||||
this.toDispose.push(
|
this.toDispose.push(
|
||||||
this.subscribe(this.refreshReturnType, {
|
this.subscribe(this.refreshReturnType, {
|
||||||
selector: (curr) => curr.wrapFor?.returnType,
|
selector: (curr) => curr.wrapFor?.returnType,
|
||||||
|
triggerOnInit: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import {
|
|||||||
merge,
|
merge,
|
||||||
share,
|
share,
|
||||||
skip,
|
skip,
|
||||||
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
|
tap,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { flatten } from 'lodash';
|
import { flatten } from 'lodash';
|
||||||
import { shallowEqual } from 'fast-equals';
|
import { shallowEqual } from 'fast-equals';
|
||||||
@ -17,7 +19,7 @@ import { IVariableTable } from '../types';
|
|||||||
import { type Scope } from '../scope';
|
import { type Scope } from '../scope';
|
||||||
import { subsToDisposable } from '../../utils/toDisposable';
|
import { subsToDisposable } from '../../utils/toDisposable';
|
||||||
import { createMemo } from '../../utils/memo';
|
import { createMemo } from '../../utils/memo';
|
||||||
import { Property, VariableDeclaration } from '../../ast';
|
import { BaseVariableField, VariableDeclaration } from '../../ast';
|
||||||
/**
|
/**
|
||||||
* 作用域可用变量
|
* 作用域可用变量
|
||||||
*/
|
*/
|
||||||
@ -135,11 +137,35 @@ export class ScopeAvailableData {
|
|||||||
* @param keyPath
|
* @param keyPath
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
getByKeyPath(keyPath: string[] = []): VariableDeclaration | Property | undefined {
|
getByKeyPath(keyPath: string[] = []): BaseVariableField | undefined {
|
||||||
// 检查变量是否在可访问范围内
|
// 检查变量是否在可访问范围内
|
||||||
if (!this.variableKeys.includes(keyPath[0])) {
|
if (!this.variableKeys.includes(keyPath[0])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return this.globalVariableTable.getByKeyPath(keyPath);
|
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 { type VariableEngine } from '../variable-engine';
|
||||||
import { createMemo } from '../utils/memo';
|
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';
|
import { ScopeAvailableData, ScopeEventData, ScopeOutputData } from './datas';
|
||||||
|
|
||||||
export interface IScopeConstructor {
|
export interface IScopeConstructor {
|
||||||
@ -117,4 +117,44 @@ export class Scope<ScopeMeta extends Record<string, any> = Record<string, any>>
|
|||||||
get disposed(): boolean {
|
get disposed(): boolean {
|
||||||
return this.toDispose.disposed;
|
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 {
|
export interface TestConfig extends VariableLayoutConfig {
|
||||||
enableGlobalScope?: boolean;
|
enableGlobalScope?: boolean;
|
||||||
|
onInit?: (container: Container) => void;
|
||||||
|
runExtraTest?: (container: Container) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContainer(layout: 'free' | 'fixed', config?: TestConfig): Container {
|
export function getContainer(layout: 'free' | 'fixed', config?: TestConfig): Container {
|
||||||
const { enableGlobalScope, ...layoutConfig } = config || {};
|
const { enableGlobalScope, onInit, runExtraTest, ...layoutConfig } = config || {};
|
||||||
|
|
||||||
const container = createPlaygroundContainer() as Container;
|
const container = createPlaygroundContainer() as Container;
|
||||||
container.load(VariableContainerModule);
|
container.load(VariableContainerModule);
|
||||||
@ -74,5 +76,7 @@ export function getContainer(layout: 'free' | 'fixed', config?: TestConfig): Con
|
|||||||
);
|
);
|
||||||
document.registerNodeDatas(FlowNodeVariableData);
|
document.registerNodeDatas(FlowNodeVariableData);
|
||||||
|
|
||||||
|
onInit?.(container);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,6 +95,8 @@ export const runFixedLayoutTest = (testName:string, spec: FlowDocumentJSON, conf
|
|||||||
test('test sort', () => {
|
test('test sort', () => {
|
||||||
expect(variableEngine.getAllScopes({ sort: true }).map(_scope => _scope.id)).toMatchSnapshot();
|
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', () => {
|
test('test sort', () => {
|
||||||
expect(variableEngine.getAllScopes({ sort: true }).map(_scope => _scope.id)).toMatchSnapshot();
|
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",
|
"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 { runFreeLayoutTest } from '../__mocks__/run-free-layout-test';
|
||||||
import { freeLayout1 } from '../__mocks__/free-layout-specs';
|
import { freeLayout1 } from '../__mocks__/free-layout-specs';
|
||||||
|
|
||||||
runFreeLayoutTest('Variable Free Layout', freeLayout1, {
|
runFreeLayoutTest('Variable Free Layout transform empty', freeLayout1, {
|
||||||
// 模拟清空作用域
|
// 模拟清空作用域
|
||||||
transformCovers: () => [],
|
transformCovers: () => [],
|
||||||
transformDeps: () => [],
|
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 scope is GlobalScope, return all scopes except GlobalScope
|
||||||
if (GlobalScope.is(scope)) {
|
if (GlobalScope.is(scope)) {
|
||||||
return this.variableEngine
|
const scopes = this.variableEngine
|
||||||
.getAllScopes({ sort: true })
|
.getAllScopes({ sort: true })
|
||||||
.filter((_scope) => !GlobalScope.is(_scope));
|
.filter((_scope) => !GlobalScope.is(_scope));
|
||||||
|
|
||||||
|
return this.transformService.transformCovers(scopes, { scope });
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = scope.meta.node;
|
const node = scope.meta.node;
|
||||||
|
|||||||
@ -53,19 +53,19 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
|||||||
|
|
||||||
// 获取同一层级所有输入节点
|
// 获取同一层级所有输入节点
|
||||||
protected getAllInputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {
|
protected getAllInputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {
|
||||||
const currParent = this.getParent(curr);
|
const currParent = this.getNodeParent(curr);
|
||||||
|
|
||||||
return (curr.getData(WorkflowNodeLinesData)?.allInputNodes || []).filter(
|
return (curr.getData(WorkflowNodeLinesData)?.allInputNodes || []).filter(
|
||||||
(_node) => this.getParent(_node) === currParent
|
(_node) => this.getNodeParent(_node) === currParent
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取同一层级所有输出节点
|
// 获取同一层级所有输出节点
|
||||||
protected getAllOutputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {
|
protected getAllOutputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {
|
||||||
const currParent = this.getParent(curr);
|
const currParent = this.getNodeParent(curr);
|
||||||
|
|
||||||
return (curr.getData(WorkflowNodeLinesData)?.allOutputNodes || []).filter(
|
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);
|
deps.push(currVarData.private);
|
||||||
}
|
}
|
||||||
|
|
||||||
curr = this.getParent(curr);
|
curr = this.getNodeParent(curr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If scope is GlobalScope, add globalScope to deps
|
// If scope is GlobalScope, add globalScope to deps
|
||||||
@ -110,9 +110,11 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
|||||||
getCovers(scope: FlowNodeScope): FlowNodeScope[] {
|
getCovers(scope: FlowNodeScope): FlowNodeScope[] {
|
||||||
// If scope is GlobalScope, return all scopes except GlobalScope
|
// If scope is GlobalScope, return all scopes except GlobalScope
|
||||||
if (GlobalScope.is(scope)) {
|
if (GlobalScope.is(scope)) {
|
||||||
return this.variableEngine
|
const scopes = this.variableEngine
|
||||||
.getAllScopes({ sort: true })
|
.getAllScopes({ sort: true })
|
||||||
.filter((_scope) => !GlobalScope.is(_scope));
|
.filter((_scope) => !GlobalScope.is(_scope));
|
||||||
|
|
||||||
|
return this.transformService.transformCovers(scopes, { scope });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { node } = scope.meta || {};
|
const { node } = scope.meta || {};
|
||||||
@ -127,7 +129,7 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
|||||||
|
|
||||||
if (isPrivate) {
|
if (isPrivate) {
|
||||||
// private 只能覆盖其子节点
|
// private 只能覆盖其子节点
|
||||||
queue.push(...this.getChildren(node));
|
queue.push(...this.getNodeChildren(node));
|
||||||
} else {
|
} else {
|
||||||
// 否则覆盖其所有输出线的节点
|
// 否则覆盖其所有输出线的节点
|
||||||
queue.push(...(this.getAllOutputLayerNodes(node) || []));
|
queue.push(...(this.getAllOutputLayerNodes(node) || []));
|
||||||
@ -140,7 +142,7 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
|||||||
const _node = queue.shift()!;
|
const _node = queue.shift()!;
|
||||||
const variableData: FlowNodeVariableData = _node.getData(FlowNodeVariableData);
|
const variableData: FlowNodeVariableData = _node.getData(FlowNodeVariableData);
|
||||||
scopes.push(...variableData.allScopes);
|
scopes.push(...variableData.allScopes);
|
||||||
const children = _node && this.getChildren(_node);
|
const children = _node && this.getNodeChildren(_node);
|
||||||
|
|
||||||
if (children?.length) {
|
if (children?.length) {
|
||||||
queue.push(...children);
|
queue.push(...children);
|
||||||
@ -158,9 +160,9 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
|||||||
return this.transformService.transformCovers(uniqScopes, { scope });
|
return this.transformService.transformCovers(uniqScopes, { scope });
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildren(node: FlowNodeEntity): FlowNodeEntity[] {
|
getNodeChildren(node: FlowNodeEntity): FlowNodeEntity[] {
|
||||||
if (this.configs?.getFreeChildren) {
|
if (this.configs?.getNodeChildren) {
|
||||||
return this.configs.getFreeChildren?.(node);
|
return this.configs.getNodeChildren?.(node);
|
||||||
}
|
}
|
||||||
const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();
|
const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();
|
||||||
const subCanvas = nodeMeta.subCanvas?.(node);
|
const subCanvas = nodeMeta.subCanvas?.(node);
|
||||||
@ -178,10 +180,10 @@ export class FreeLayoutScopeChain extends ScopeChain {
|
|||||||
return this.tree.getChildren(node);
|
return this.tree.getChildren(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
getParent(node: FlowNodeEntity): FlowNodeEntity | undefined {
|
getNodeParent(node: FlowNodeEntity): FlowNodeEntity | undefined {
|
||||||
// 部分场景通过连线来表达父子关系,因此需要上层配置
|
// 部分场景通过连线来表达父子关系,因此需要上层配置
|
||||||
if (this.configs?.getFreeParent) {
|
if (this.configs?.getNodeParent) {
|
||||||
return this.configs.getFreeParent(node);
|
return this.configs.getNodeParent(node);
|
||||||
}
|
}
|
||||||
let parent = node.document.originTree.getParent(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 { type ASTNode, ASTNodeJSON } from '@flowgram.ai/variable-core';
|
||||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||||
import { EntityData } from '@flowgram.ai/core';
|
import { EntityData } from '@flowgram.ai/core';
|
||||||
@ -179,4 +179,22 @@ export class FlowNodeVariableData extends EntityData {
|
|||||||
}
|
}
|
||||||
return this._private;
|
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';
|
} from './types';
|
||||||
export { GlobalScope, bindGlobalScope } from './scopes/global-scope';
|
export { GlobalScope, bindGlobalScope } from './scopes/global-scope';
|
||||||
export { ScopeChainTransformService } from './services/scope-chain-transform-service';
|
export { ScopeChainTransformService } from './services/scope-chain-transform-service';
|
||||||
|
export { getNodeScope, getNodePrivateScope } from './utils';
|
||||||
|
|||||||
@ -1,62 +1,13 @@
|
|||||||
import { injectable, interfaces } from 'inversify';
|
import { injectable, interfaces } from 'inversify';
|
||||||
import { ASTNode, ASTNodeJSON, Scope, VariableEngine } from '@flowgram.ai/variable-core';
|
import { Scope, VariableEngine } from '@flowgram.ai/variable-core';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class GlobalScope extends Scope {
|
export class GlobalScope extends Scope {
|
||||||
static readonly ID = Symbol('GlobalScope');
|
static readonly ID = Symbol('GlobalScope');
|
||||||
|
|
||||||
static is(scope: Scope): scope is GlobalScope {
|
static is(scope: Scope) {
|
||||||
return scope.id === GlobalScope.ID;
|
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) => {
|
export const bindGlobalScope = (bind: interfaces.Bind) => {
|
||||||
|
|||||||
@ -14,11 +14,14 @@ export interface TransformerContext {
|
|||||||
|
|
||||||
export type IScopeTransformer = (scopes: Scope[], ctx: TransformerContext) => Scope[];
|
export type IScopeTransformer = (scopes: Scope[], ctx: TransformerContext) => Scope[];
|
||||||
|
|
||||||
|
const passthrough: IScopeTransformer = (scopes, ctx) => scopes;
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ScopeChainTransformService {
|
export class ScopeChainTransformService {
|
||||||
protected transformDepsFns: IScopeTransformer[] = [];
|
protected transformerMap: Map<
|
||||||
|
string,
|
||||||
protected transformCoversFns: IScopeTransformer[] = [];
|
{ transformDeps: IScopeTransformer; transformCovers: IScopeTransformer }
|
||||||
|
> = new Map();
|
||||||
|
|
||||||
@lazyInject(FlowDocument) document: FlowDocument;
|
@lazyInject(FlowDocument) document: FlowDocument;
|
||||||
|
|
||||||
@ -29,43 +32,65 @@ export class ScopeChainTransformService {
|
|||||||
@inject(VariableLayoutConfig)
|
@inject(VariableLayoutConfig)
|
||||||
protected configs?: VariableLayoutConfig
|
protected configs?: VariableLayoutConfig
|
||||||
) {
|
) {
|
||||||
if (this.configs?.transformDeps) {
|
if (this.configs?.transformDeps || this.configs?.transformCovers) {
|
||||||
this.transformDepsFns.push(this.configs.transformDeps);
|
this.transformerMap.set('VARIABLE_LAYOUT_CONFIG', {
|
||||||
}
|
transformDeps: this.configs.transformDeps || passthrough,
|
||||||
if (this.configs?.transformCovers) {
|
transformCovers: this.configs.transformCovers || passthrough,
|
||||||
this.transformCoversFns.push(this.configs.transformCovers);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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[] {
|
transformDeps(scopes: Scope[], { scope }: { scope: Scope }): Scope[] {
|
||||||
return this.transformDepsFns.reduce(
|
return Array.from(this.transformerMap.values()).reduce((scopes, transformer) => {
|
||||||
(scopes, transformer) =>
|
if (!transformer.transformDeps) {
|
||||||
transformer(scopes, {
|
return scopes;
|
||||||
scope,
|
}
|
||||||
document: this.document,
|
|
||||||
variableEngine: this.variableEngine,
|
scopes = transformer.transformDeps(scopes, {
|
||||||
}),
|
scope,
|
||||||
scopes
|
document: this.document,
|
||||||
);
|
variableEngine: this.variableEngine,
|
||||||
|
});
|
||||||
|
return scopes;
|
||||||
|
}, scopes);
|
||||||
}
|
}
|
||||||
|
|
||||||
transformCovers(scopes: Scope[], { scope }: { scope: Scope }): Scope[] {
|
transformCovers(scopes: Scope[], { scope }: { scope: Scope }): Scope[] {
|
||||||
return this.transformCoversFns.reduce(
|
return Array.from(this.transformerMap.values()).reduce((scopes, transformer) => {
|
||||||
(scopes, transformer) =>
|
if (!transformer.transformCovers) {
|
||||||
transformer(scopes, {
|
return scopes;
|
||||||
scope,
|
}
|
||||||
document: this.document,
|
|
||||||
variableEngine: this.variableEngine,
|
scopes = transformer.transformCovers(scopes, {
|
||||||
}),
|
scope,
|
||||||
scopes
|
document: this.document,
|
||||||
);
|
variableEngine: this.variableEngine,
|
||||||
|
});
|
||||||
|
return scopes;
|
||||||
|
}, scopes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,4 +19,4 @@ export interface ScopeVirtualNode {
|
|||||||
export type ScopeChainNode = FlowNodeEntity | 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;
|
isNodeChildrenPrivate?: (node: ScopeChainNode) => boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用于自由画布场景,部分场景通过连线或者其他交互形式来表达节点之间的父子关系,需要可配置化
|
* 用于固定布局场景时:父子中间存在大量无用节点(如 inlineBlocks 等,需要配置化略过)
|
||||||
|
* 用于自由画布场景时:部分场景通过连线或者其他交互形式来表达节点之间的父子关系,需可配置化
|
||||||
*/
|
*/
|
||||||
getFreeChildren?: (node: FlowNodeEntity) => FlowNodeEntity[];
|
getNodeChildren?: (node: FlowNodeEntity) => FlowNodeEntity[];
|
||||||
getFreeParent?: (node: FlowNodeEntity) => FlowNodeEntity | undefined;
|
getNodeParent?: (node: FlowNodeEntity) => FlowNodeEntity | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
|
||||||
* 对依赖作用域进行微调
|
* 对依赖作用域进行微调
|
||||||
*/
|
*/
|
||||||
transformDeps?: IScopeTransformer;
|
transformDeps?: IScopeTransformer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
|
||||||
* 对依赖作用域进行微调
|
* 对依赖作用域进行微调
|
||||||
*/
|
*/
|
||||||
transformCovers?: IScopeTransformer;
|
transformCovers?: IScopeTransformer;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user