mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
feat(variable): loop batch variable materials + update ref value typings (#253)
* feat(variable): loop batch variable materials + set variable ref content in demo to be array * docs(materials): dynamic value input and provide batch input
This commit is contained in:
parent
1de20674c5
commit
67fac8cdf1
@ -1,6 +1,6 @@
|
|||||||
|
import { DynamicValueInput } from '@flowgram.ai/form-materials';
|
||||||
import { Field } from '@flowgram.ai/fixed-layout-editor';
|
import { Field } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
import { FxExpression } from '../fx-expression';
|
|
||||||
import { FormItem } from '../form-item';
|
import { FormItem } from '../form-item';
|
||||||
import { Feedback } from '../feedback';
|
import { Feedback } from '../feedback';
|
||||||
import { JsonSchema } from '../../typings';
|
import { JsonSchema } from '../../typings';
|
||||||
@ -26,11 +26,12 @@ export function FormInputs() {
|
|||||||
type={property.type as string}
|
type={property.type as string}
|
||||||
required={required.includes(key)}
|
required={required.includes(key)}
|
||||||
>
|
>
|
||||||
<FxExpression
|
<DynamicValueInput
|
||||||
value={field.value}
|
value={field.value}
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
<Feedback errors={fieldState?.errors} />
|
<Feedback errors={fieldState?.errors} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
import React, { type SVGProps } from 'react';
|
|
||||||
|
|
||||||
import { VariableSelector } from '@flowgram.ai/form-materials';
|
|
||||||
import { Input, Button } from '@douyinfe/semi-ui';
|
|
||||||
|
|
||||||
import { ValueDisplay } from '../value-display';
|
|
||||||
import { FlowRefValueSchema, FlowLiteralValueSchema } from '../../typings';
|
|
||||||
|
|
||||||
export function FxIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16" {...props}>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M5.581 4.49A2.75 2.75 0 0 1 8.319 2h.931a.75.75 0 0 1 0 1.5h-.931a1.25 1.25 0 0 0-1.245 1.131l-.083.869H9.25a.75.75 0 0 1 0 1.5H6.849l-.43 4.51A2.75 2.75 0 0 1 3.681 14H2.75a.75.75 0 0 1 0-1.5h.931a1.25 1.25 0 0 0 1.245-1.132L5.342 7H3.75a.75.75 0 0 1 0-1.5h1.735zM9.22 9.22a.75.75 0 0 1 1.06 0l1.22 1.22l1.22-1.22a.75.75 0 1 1 1.06 1.06l-1.22 1.22l1.22 1.22a.75.75 0 1 1-1.06 1.06l-1.22-1.22l-1.22 1.22a.75.75 0 1 1-1.06-1.06l1.22-1.22l-1.22-1.22a.75.75 0 0 1 0-1.06"
|
|
||||||
clipRule="evenodd"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputWrap({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
readonly,
|
|
||||||
hasError,
|
|
||||||
style,
|
|
||||||
}: {
|
|
||||||
value: string;
|
|
||||||
onChange: (v: string) => void;
|
|
||||||
readonly?: boolean;
|
|
||||||
hasError?: boolean;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}) {
|
|
||||||
if (readonly) {
|
|
||||||
return <ValueDisplay value={value} hasError={hasError} />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
value={value as string}
|
|
||||||
onChange={onChange}
|
|
||||||
validateStatus={hasError ? 'error' : undefined}
|
|
||||||
style={style}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FxExpressionProps {
|
|
||||||
value?: FlowLiteralValueSchema | FlowRefValueSchema;
|
|
||||||
onChange: (value: FlowLiteralValueSchema | FlowRefValueSchema) => void;
|
|
||||||
literal?: boolean;
|
|
||||||
hasError?: boolean;
|
|
||||||
readonly?: boolean;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FxExpression(props: FxExpressionProps) {
|
|
||||||
const { value, onChange, readonly, literal, icon } = props;
|
|
||||||
if (literal) return <InputWrap value={value as string} onChange={onChange} readonly={readonly} />;
|
|
||||||
const isExpression = typeof value === 'object' && value.type === 'expression';
|
|
||||||
const toggleExpression = () => {
|
|
||||||
if (isExpression) {
|
|
||||||
onChange((value as FlowRefValueSchema).content as string);
|
|
||||||
} else {
|
|
||||||
onChange({ content: value as string, type: 'expression' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', maxWidth: 300 }}>
|
|
||||||
{isExpression ? (
|
|
||||||
<VariableSelector
|
|
||||||
value={value.content}
|
|
||||||
hasError={props.hasError}
|
|
||||||
style={{ flexGrow: 1 }}
|
|
||||||
onChange={(v) => onChange({ type: 'expression', content: v })}
|
|
||||||
readonly={readonly}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<InputWrap
|
|
||||||
value={value as string}
|
|
||||||
onChange={onChange}
|
|
||||||
hasError={props.hasError}
|
|
||||||
readonly={readonly}
|
|
||||||
style={{ flexGrow: 1, outline: props.hasError ? '1px solid red' : undefined }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!readonly &&
|
|
||||||
(icon || <Button theme="borderless" icon={<FxIcon />} onClick={toggleExpression} />)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,10 +1,9 @@
|
|||||||
import React, { useState, useLayoutEffect } from 'react';
|
import React, { useState, useLayoutEffect } from 'react';
|
||||||
|
|
||||||
import { VariableSelector } from '@flowgram.ai/form-materials';
|
import { VariableSelector, TypeSelector, DynamicValueInput } from '@flowgram.ai/form-materials';
|
||||||
import { Input, Button } from '@douyinfe/semi-ui';
|
import { Input, Button } from '@douyinfe/semi-ui';
|
||||||
import { IconCrossCircleStroked } from '@douyinfe/semi-icons';
|
import { IconCrossCircleStroked } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
import { TypeSelector } from '../type-selector';
|
|
||||||
import { JsonSchema } from '../../typings';
|
import { JsonSchema } from '../../typings';
|
||||||
import { LeftColumn, Row } from './styles';
|
import { LeftColumn, Row } from './styles';
|
||||||
|
|
||||||
@ -24,6 +23,11 @@ export const PropertyEdit: React.FC<PropertyEditProps> = (props) => {
|
|||||||
value[key] = val;
|
value[key] = val;
|
||||||
props.onChange(value, props.propertyKey);
|
props.onChange(value, props.propertyKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const partialUpdateProperty = (val?: Partial<JsonSchema>) => {
|
||||||
|
props.onChange({ ...value, ...val }, props.propertyKey);
|
||||||
|
};
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
updateKey(props.propertyKey);
|
updateKey(props.propertyKey);
|
||||||
}, [props.propertyKey]);
|
}, [props.propertyKey]);
|
||||||
@ -31,14 +35,15 @@ export const PropertyEdit: React.FC<PropertyEditProps> = (props) => {
|
|||||||
<Row>
|
<Row>
|
||||||
<LeftColumn>
|
<LeftColumn>
|
||||||
<TypeSelector
|
<TypeSelector
|
||||||
value={value.type}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{ position: 'absolute', top: 6, left: 4, zIndex: 1 }}
|
style={{ position: 'absolute', top: 2, left: 4, zIndex: 1, padding: '0 5px', height: 20 }}
|
||||||
onChange={(val) => updateProperty('type', val)}
|
onChange={(val) => partialUpdateProperty(val)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={inputKey}
|
value={inputKey}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
size="small"
|
||||||
onChange={(v) => updateKey(v.trim())}
|
onChange={(v) => updateKey(v.trim())}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (inputKey !== '') {
|
if (inputKey !== '') {
|
||||||
@ -50,22 +55,22 @@ export const PropertyEdit: React.FC<PropertyEditProps> = (props) => {
|
|||||||
style={{ paddingLeft: 26 }}
|
style={{ paddingLeft: 26 }}
|
||||||
/>
|
/>
|
||||||
</LeftColumn>
|
</LeftColumn>
|
||||||
{props.useFx ? (
|
{
|
||||||
<VariableSelector
|
<DynamicValueInput
|
||||||
value={value.default}
|
|
||||||
readonly={disabled}
|
|
||||||
onChange={(val) => updateProperty('default', val)}
|
|
||||||
style={{ flexGrow: 1, height: 32 }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
disabled={disabled}
|
|
||||||
value={value.default}
|
value={value.default}
|
||||||
onChange={(val) => updateProperty('default', val)}
|
onChange={(val) => updateProperty('default', val)}
|
||||||
|
schema={value}
|
||||||
|
style={{ flexGrow: 1 }}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
{props.onDelete && !disabled && (
|
{props.onDelete && !disabled && (
|
||||||
<Button theme="borderless" icon={<IconCrossCircleStroked />} onClick={props.onDelete} />
|
<Button
|
||||||
|
style={{ marginLeft: 5, position: 'relative', top: 2 }}
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
icon={<IconCrossCircleStroked />}
|
||||||
|
onClick={props.onDelete}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export const Row = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const LeftColumn = styled.div`
|
export const LeftColumn = styled.div`
|
||||||
width: 300px;
|
width: 120px;
|
||||||
margin-right: 10px;
|
margin-right: 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { VariableTypeIcons } from '@flowgram.ai/form-materials';
|
|
||||||
import { Tag, Dropdown } from '@douyinfe/semi-ui';
|
|
||||||
|
|
||||||
export interface TypeSelectorProps {
|
|
||||||
value?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
onChange?: (value?: string) => void;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}
|
|
||||||
const dropdownMenus = ['object', 'boolean', 'array', 'string', 'integer', 'number'];
|
|
||||||
|
|
||||||
export const TypeSelector: React.FC<TypeSelectorProps> = (props) => {
|
|
||||||
const { value, disabled } = props;
|
|
||||||
const icon = VariableTypeIcons[value as any];
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
trigger="hover"
|
|
||||||
position="bottomRight"
|
|
||||||
disabled={disabled}
|
|
||||||
render={
|
|
||||||
<Dropdown.Menu>
|
|
||||||
{dropdownMenus.map((key) => (
|
|
||||||
<Dropdown.Item
|
|
||||||
key={key}
|
|
||||||
onClick={() => {
|
|
||||||
props.onChange?.(key);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{VariableTypeIcons[key]}
|
|
||||||
<span style={{ paddingLeft: '4px' }}>{key}</span>
|
|
||||||
</Dropdown.Item>
|
|
||||||
))}
|
|
||||||
</Dropdown.Menu>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Tag
|
|
||||||
color="white"
|
|
||||||
style={props.style}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</Tag>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -15,6 +15,24 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'Hello Flow.',
|
default: 'Hello Flow.',
|
||||||
},
|
},
|
||||||
|
enable: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
array_obj: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
int: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
str: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -26,10 +44,22 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
data: {
|
data: {
|
||||||
title: 'LLM',
|
title: 'LLM',
|
||||||
inputsValues: {
|
inputsValues: {
|
||||||
modelType: 'gpt-3.5-turbo',
|
modelType: {
|
||||||
temperature: 0.5,
|
type: 'constant',
|
||||||
systemPrompt: 'You are an AI assistant.',
|
content: 'gpt-3.5-turbo',
|
||||||
prompt: '',
|
},
|
||||||
|
temperature: {
|
||||||
|
type: 'constant',
|
||||||
|
content: 0.5,
|
||||||
|
},
|
||||||
|
systemPrompt: {
|
||||||
|
type: 'constant',
|
||||||
|
content: 'You are an AI assistant.',
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
type: 'constant',
|
||||||
|
content: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
inputs: {
|
inputs: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -62,6 +92,10 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
type: 'loop',
|
type: 'loop',
|
||||||
data: {
|
data: {
|
||||||
title: 'Loop',
|
title: 'Loop',
|
||||||
|
batchFor: {
|
||||||
|
type: 'ref',
|
||||||
|
content: ['start_0', 'array_obj'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { nanoid } from 'nanoid';
|
|||||||
import { defaultFormMeta } from '../default-form-meta';
|
import { defaultFormMeta } from '../default-form-meta';
|
||||||
import { FlowNodeRegistry } from '../../typings';
|
import { FlowNodeRegistry } from '../../typings';
|
||||||
import iconLoop from '../../assets/icon-loop.svg';
|
import iconLoop from '../../assets/icon-loop.svg';
|
||||||
|
import { LoopFormRender } from './loop-form-render';
|
||||||
|
|
||||||
export const LoopNodeRegistry: FlowNodeRegistry = {
|
export const LoopNodeRegistry: FlowNodeRegistry = {
|
||||||
type: 'loop',
|
type: 'loop',
|
||||||
@ -14,7 +15,7 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
|
|||||||
meta: {
|
meta: {
|
||||||
expandable: false, // disable expanded
|
expandable: false, // disable expanded
|
||||||
},
|
},
|
||||||
formMeta: defaultFormMeta,
|
formMeta: { ...defaultFormMeta, render: LoopFormRender },
|
||||||
onAdd() {
|
onAdd() {
|
||||||
return {
|
return {
|
||||||
id: `loop_${nanoid(5)}`,
|
id: `loop_${nanoid(5)}`,
|
||||||
|
|||||||
54
apps/demo-fixed-layout/src/nodes/loop/loop-form-render.tsx
Normal file
54
apps/demo-fixed-layout/src/nodes/loop/loop-form-render.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { BatchVariableSelector, IFlowRefValue } from '@flowgram.ai/form-materials';
|
||||||
|
import { FormRenderProps, FlowNodeJSON, Field } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { useIsSidebar } from '../../hooks';
|
||||||
|
import { FormHeader, FormContent, FormOutputs, FormItem, Feedback } from '../../form-components';
|
||||||
|
|
||||||
|
interface LoopNodeJSON extends FlowNodeJSON {
|
||||||
|
data: {
|
||||||
|
batchFor: IFlowRefValue;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {
|
||||||
|
const isSidebar = useIsSidebar();
|
||||||
|
const readonly = !isSidebar;
|
||||||
|
|
||||||
|
const batchFor = (
|
||||||
|
<Field<IFlowRefValue> name={`batchFor`}>
|
||||||
|
{({ field, fieldState }) => (
|
||||||
|
<FormItem name={'batchFor'} type={'array'} required>
|
||||||
|
<BatchVariableSelector
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={field.value?.content}
|
||||||
|
onChange={(val) => field.onChange({ type: 'ref', content: val })}
|
||||||
|
readonly={readonly}
|
||||||
|
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||||
|
/>
|
||||||
|
<Feedback errors={fieldState?.errors} />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSidebar) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormHeader />
|
||||||
|
<FormContent>
|
||||||
|
{batchFor}
|
||||||
|
<FormOutputs />
|
||||||
|
</FormContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormHeader />
|
||||||
|
<FormContent>
|
||||||
|
{batchFor}
|
||||||
|
<FormOutputs />
|
||||||
|
</FormContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -7,8 +7,6 @@ import {
|
|||||||
ASTFactory,
|
ASTFactory,
|
||||||
} from '@flowgram.ai/fixed-layout-editor';
|
} from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
import { createASTFromJSONSchema } from './utils';
|
|
||||||
|
|
||||||
export interface SyncVariablePluginOptions {}
|
export interface SyncVariablePluginOptions {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +37,7 @@ export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions>
|
|||||||
|
|
||||||
// Create an Type AST from the output data's JSON schema
|
// Create an Type AST from the output data's JSON schema
|
||||||
// NOTICE: You can create a new function to generate an AST based on YOUR CUSTOM DSL
|
// NOTICE: You can create a new function to generate an AST based on YOUR CUSTOM DSL
|
||||||
const typeAST = createASTFromJSONSchema(value);
|
const typeAST = ASTFactory.createTypeASTFromSchema(value);
|
||||||
|
|
||||||
if (typeAST) {
|
if (typeAST) {
|
||||||
// Use the node's title or its ID as the title for the variable
|
// Use the node's title or its ID as the title for the variable
|
||||||
@ -53,7 +51,7 @@ export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions>
|
|||||||
icon: node.getNodeRegistry()?.info?.icon,
|
icon: node.getNodeRegistry()?.info?.icon,
|
||||||
// NOTICE: You can add more metadata here as needed
|
// NOTICE: You can add more metadata here as needed
|
||||||
},
|
},
|
||||||
key: `${node.id}.outputs`,
|
key: `${node.id}`,
|
||||||
type: typeAST,
|
type: typeAST,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
import { get } from 'lodash-es';
|
|
||||||
import { ASTFactory, type ASTNodeJSON } from '@flowgram.ai/fixed-layout-editor';
|
|
||||||
|
|
||||||
import { type JsonSchema } from '../../typings';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorts the properties of a JSON schema based on the 'extra.index' field.
|
|
||||||
* If the 'extra.index' field is not present, the property will be treated as having an index of 0.
|
|
||||||
*
|
|
||||||
* @param properties - The properties of the JSON schema to sort.
|
|
||||||
* @returns A sorted array of property entries.
|
|
||||||
*/
|
|
||||||
function sortProperties(properties: Record<string, JsonSchema>) {
|
|
||||||
return Object.entries(properties).sort(
|
|
||||||
(a, b) => (get(a?.[1], 'extra.index') || 0) - (get(b?.[1], 'extra.index') || 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a JSON schema to an Abstract Syntax Tree (AST) representation.
|
|
||||||
* This function recursively processes the JSON schema and creates corresponding AST nodes.
|
|
||||||
*
|
|
||||||
* For more information on JSON Schema, refer to the official documentation:
|
|
||||||
* https://json-schema.org/
|
|
||||||
*
|
|
||||||
* Note: Depending on your business needs, you can use your own Domain-Specific Language (DSL)
|
|
||||||
* Create a new function to convert your custom DSL to AST directly.
|
|
||||||
*
|
|
||||||
* @param jsonSchema - The JSON schema to convert.
|
|
||||||
* @returns An AST node representing the JSON schema, or undefined if the schema type is not recognized.
|
|
||||||
*/
|
|
||||||
export function createASTFromJSONSchema(jsonSchema: JsonSchema): ASTNodeJSON | undefined {
|
|
||||||
const { type } = jsonSchema || {};
|
|
||||||
|
|
||||||
if (!type) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'object':
|
|
||||||
return ASTFactory.createObject({
|
|
||||||
properties: sortProperties(jsonSchema.properties || {}).map(([key, _property]) => ({
|
|
||||||
key,
|
|
||||||
type: createASTFromJSONSchema(_property),
|
|
||||||
meta: { description: _property.description },
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
case 'array':
|
|
||||||
return ASTFactory.createArray({
|
|
||||||
items: createASTFromJSONSchema(jsonSchema.items!),
|
|
||||||
});
|
|
||||||
case 'string':
|
|
||||||
return ASTFactory.createString();
|
|
||||||
case 'number':
|
|
||||||
return ASTFactory.createNumber();
|
|
||||||
case 'boolean':
|
|
||||||
return ASTFactory.createBoolean();
|
|
||||||
case 'integer':
|
|
||||||
return ASTFactory.createInteger();
|
|
||||||
|
|
||||||
default:
|
|
||||||
// If the type is not recognized, return CustomType
|
|
||||||
return ASTFactory.createCustomType({ typeName: type });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +1,4 @@
|
|||||||
export type BasicType = 'boolean' | 'string' | 'integer' | 'number' | 'object' | 'array';
|
import type { VarJSONSchema } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
export interface JsonSchema<T extends BasicType = BasicType> {
|
export type BasicType = VarJSONSchema.BasicType;
|
||||||
type?: T;
|
export type JsonSchema = VarJSONSchema.ISchema;
|
||||||
default?: any;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
enum?: (string | number)[];
|
|
||||||
properties?: Record<string, JsonSchema>;
|
|
||||||
additionalProperties?: JsonSchema;
|
|
||||||
items?: JsonSchema;
|
|
||||||
required?: string[];
|
|
||||||
$ref?: string;
|
|
||||||
extra?: {
|
|
||||||
literal?: boolean; // is literal type
|
|
||||||
formComponent?: string; // Set the render component
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { IFlowValue } from '@flowgram.ai/form-materials';
|
||||||
import {
|
import {
|
||||||
FlowNodeJSON as FlowNodeJSONDefault,
|
FlowNodeJSON as FlowNodeJSONDefault,
|
||||||
FlowNodeRegistry as FlowNodeRegistryDefault,
|
FlowNodeRegistry as FlowNodeRegistryDefault,
|
||||||
@ -7,12 +8,6 @@ import {
|
|||||||
|
|
||||||
import { type JsonSchema } from './json-schema';
|
import { type JsonSchema } from './json-schema';
|
||||||
|
|
||||||
export type FlowLiteralValueSchema = string | number | boolean;
|
|
||||||
export type FlowRefValueSchema =
|
|
||||||
| { type: 'ref'; content?: string }
|
|
||||||
| { type: 'expression'; content?: string }
|
|
||||||
| { type: 'template'; content?: string };
|
|
||||||
export type FlowValueSchema = FlowLiteralValueSchema | FlowRefValueSchema;
|
|
||||||
/**
|
/**
|
||||||
* You can customize the data of the node, and here you can use JsonSchema to define the input and output of the node
|
* You can customize the data of the node, and here you can use JsonSchema to define the input and output of the node
|
||||||
* 你可以自定义节点的 data 业务数据, 这里演示 通过 JsonSchema 来定义节点的输入/输出
|
* 你可以自定义节点的 data 业务数据, 这里演示 通过 JsonSchema 来定义节点的输入/输出
|
||||||
@ -22,11 +17,11 @@ export interface FlowNodeJSON extends FlowNodeJSONDefault {
|
|||||||
/**
|
/**
|
||||||
* Node title
|
* Node title
|
||||||
*/
|
*/
|
||||||
title: string;
|
title?: string;
|
||||||
/**
|
/**
|
||||||
* Inputs data values
|
* Inputs data values
|
||||||
*/
|
*/
|
||||||
inputsValues?: Record<string, FlowValueSchema>;
|
inputsValues?: Record<string, IFlowValue>;
|
||||||
/**
|
/**
|
||||||
* Define the inputs data of the node by JsonSchema
|
* Define the inputs data of the node by JsonSchema
|
||||||
*/
|
*/
|
||||||
@ -35,6 +30,10 @@ export interface FlowNodeJSON extends FlowNodeJSONDefault {
|
|||||||
* Define the outputs data of the node by JsonSchema
|
* Define the outputs data of the node by JsonSchema
|
||||||
*/
|
*/
|
||||||
outputs?: JsonSchema;
|
outputs?: JsonSchema;
|
||||||
|
/**
|
||||||
|
* Rest properties
|
||||||
|
*/
|
||||||
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Field } from '@flowgram.ai/free-layout-editor';
|
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { DynamicValueInput } from '@flowgram.ai/form-materials';
|
||||||
|
|
||||||
import { FxExpression } from '../fx-expression';
|
|
||||||
import { FormItem } from '../form-item';
|
import { FormItem } from '../form-item';
|
||||||
import { Feedback } from '../feedback';
|
import { Feedback } from '../feedback';
|
||||||
import { JsonSchema } from '../../typings';
|
import { JsonSchema } from '../../typings';
|
||||||
@ -26,11 +26,12 @@ export function FormInputs() {
|
|||||||
type={property.type as string}
|
type={property.type as string}
|
||||||
required={required.includes(key)}
|
required={required.includes(key)}
|
||||||
>
|
>
|
||||||
<FxExpression
|
<DynamicValueInput
|
||||||
value={field.value}
|
value={field.value}
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
<Feedback errors={fieldState?.errors} />
|
<Feedback errors={fieldState?.errors} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
import React, { type SVGProps } from 'react';
|
|
||||||
|
|
||||||
import { VariableSelector } from '@flowgram.ai/form-materials';
|
|
||||||
import { Input, Button } from '@douyinfe/semi-ui';
|
|
||||||
|
|
||||||
import { ValueDisplay } from '../value-display';
|
|
||||||
import { FlowRefValueSchema, FlowLiteralValueSchema } from '../../typings';
|
|
||||||
|
|
||||||
export function FxIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16" {...props}>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M5.581 4.49A2.75 2.75 0 0 1 8.319 2h.931a.75.75 0 0 1 0 1.5h-.931a1.25 1.25 0 0 0-1.245 1.131l-.083.869H9.25a.75.75 0 0 1 0 1.5H6.849l-.43 4.51A2.75 2.75 0 0 1 3.681 14H2.75a.75.75 0 0 1 0-1.5h.931a1.25 1.25 0 0 0 1.245-1.132L5.342 7H3.75a.75.75 0 0 1 0-1.5h1.735zM9.22 9.22a.75.75 0 0 1 1.06 0l1.22 1.22l1.22-1.22a.75.75 0 1 1 1.06 1.06l-1.22 1.22l1.22 1.22a.75.75 0 1 1-1.06 1.06l-1.22-1.22l-1.22 1.22a.75.75 0 1 1-1.06-1.06l1.22-1.22l-1.22-1.22a.75.75 0 0 1 0-1.06"
|
|
||||||
clipRule="evenodd"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputWrap({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
readonly,
|
|
||||||
hasError,
|
|
||||||
style,
|
|
||||||
}: {
|
|
||||||
value: string;
|
|
||||||
onChange: (v: string) => void;
|
|
||||||
readonly?: boolean;
|
|
||||||
hasError?: boolean;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}) {
|
|
||||||
if (readonly) {
|
|
||||||
return <ValueDisplay value={value} hasError={hasError} />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
value={value as string}
|
|
||||||
onChange={onChange}
|
|
||||||
validateStatus={hasError ? 'error' : undefined}
|
|
||||||
style={style}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FxExpressionProps {
|
|
||||||
value?: FlowLiteralValueSchema | FlowRefValueSchema;
|
|
||||||
onChange: (value: FlowLiteralValueSchema | FlowRefValueSchema) => void;
|
|
||||||
literal?: boolean;
|
|
||||||
hasError?: boolean;
|
|
||||||
readonly?: boolean;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FxExpression(props: FxExpressionProps) {
|
|
||||||
const { value, onChange, readonly, literal, icon } = props;
|
|
||||||
if (literal) return <InputWrap value={value as string} onChange={onChange} readonly={readonly} />;
|
|
||||||
const isExpression = typeof value === 'object' && value.type === 'expression';
|
|
||||||
const toggleExpression = () => {
|
|
||||||
if (isExpression) {
|
|
||||||
onChange((value as FlowRefValueSchema).content as string);
|
|
||||||
} else {
|
|
||||||
onChange({ content: value as string, type: 'expression' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', maxWidth: 300 }}>
|
|
||||||
{isExpression ? (
|
|
||||||
<VariableSelector
|
|
||||||
value={value.content}
|
|
||||||
hasError={props.hasError}
|
|
||||||
style={{ flexGrow: 1 }}
|
|
||||||
onChange={(v) => onChange({ type: 'expression', content: v })}
|
|
||||||
readonly={readonly}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<InputWrap
|
|
||||||
value={value as string}
|
|
||||||
onChange={onChange}
|
|
||||||
hasError={props.hasError}
|
|
||||||
readonly={readonly}
|
|
||||||
style={{ flexGrow: 1, outline: props.hasError ? '1px solid red' : undefined }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!readonly &&
|
|
||||||
(icon || <Button theme="borderless" icon={<FxIcon />} onClick={toggleExpression} />)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,10 +1,9 @@
|
|||||||
import React, { useState, useLayoutEffect } from 'react';
|
import React, { useState, useLayoutEffect } from 'react';
|
||||||
|
|
||||||
import { VariableSelector } from '@flowgram.ai/form-materials';
|
import { VariableSelector, TypeSelector, DynamicValueInput } from '@flowgram.ai/form-materials';
|
||||||
import { Input, Button } from '@douyinfe/semi-ui';
|
import { Input, Button } from '@douyinfe/semi-ui';
|
||||||
import { IconCrossCircleStroked } from '@douyinfe/semi-icons';
|
import { IconCrossCircleStroked } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
import { TypeSelector } from '../type-selector';
|
|
||||||
import { JsonSchema } from '../../typings';
|
import { JsonSchema } from '../../typings';
|
||||||
import { LeftColumn, Row } from './styles';
|
import { LeftColumn, Row } from './styles';
|
||||||
|
|
||||||
@ -24,6 +23,11 @@ export const PropertyEdit: React.FC<PropertyEditProps> = (props) => {
|
|||||||
value[key] = val;
|
value[key] = val;
|
||||||
props.onChange(value, props.propertyKey);
|
props.onChange(value, props.propertyKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const partialUpdateProperty = (val?: Partial<JsonSchema>) => {
|
||||||
|
props.onChange({ ...value, ...val }, props.propertyKey);
|
||||||
|
};
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
updateKey(props.propertyKey);
|
updateKey(props.propertyKey);
|
||||||
}, [props.propertyKey]);
|
}, [props.propertyKey]);
|
||||||
@ -31,14 +35,15 @@ export const PropertyEdit: React.FC<PropertyEditProps> = (props) => {
|
|||||||
<Row>
|
<Row>
|
||||||
<LeftColumn>
|
<LeftColumn>
|
||||||
<TypeSelector
|
<TypeSelector
|
||||||
value={value.type}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{ position: 'absolute', top: 6, left: 4, zIndex: 1 }}
|
style={{ position: 'absolute', top: 2, left: 4, zIndex: 1, padding: '0 5px', height: 20 }}
|
||||||
onChange={(val) => updateProperty('type', val)}
|
onChange={(val) => partialUpdateProperty(val)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={inputKey}
|
value={inputKey}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
size="small"
|
||||||
onChange={(v) => updateKey(v.trim())}
|
onChange={(v) => updateKey(v.trim())}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (inputKey !== '') {
|
if (inputKey !== '') {
|
||||||
@ -50,22 +55,22 @@ export const PropertyEdit: React.FC<PropertyEditProps> = (props) => {
|
|||||||
style={{ paddingLeft: 26 }}
|
style={{ paddingLeft: 26 }}
|
||||||
/>
|
/>
|
||||||
</LeftColumn>
|
</LeftColumn>
|
||||||
{props.useFx ? (
|
{
|
||||||
<VariableSelector
|
<DynamicValueInput
|
||||||
value={value.default}
|
|
||||||
readonly={disabled}
|
|
||||||
onChange={(val) => updateProperty('default', val)}
|
|
||||||
style={{ flexGrow: 1, height: 32 }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
disabled={disabled}
|
|
||||||
value={value.default}
|
value={value.default}
|
||||||
onChange={(val) => updateProperty('default', val)}
|
onChange={(val) => updateProperty('default', val)}
|
||||||
|
schema={value}
|
||||||
|
style={{ flexGrow: 1 }}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
{props.onDelete && !disabled && (
|
{props.onDelete && !disabled && (
|
||||||
<Button theme="borderless" icon={<IconCrossCircleStroked />} onClick={props.onDelete} />
|
<Button
|
||||||
|
style={{ marginLeft: 5, position: 'relative', top: 2 }}
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
icon={<IconCrossCircleStroked />}
|
||||||
|
onClick={props.onDelete}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export const Row = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const LeftColumn = styled.div`
|
export const LeftColumn = styled.div`
|
||||||
width: 300px;
|
width: 120px;
|
||||||
margin-right: 10px;
|
margin-right: 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { VariableTypeIcons } from '@flowgram.ai/form-materials';
|
|
||||||
import { Tag, Dropdown } from '@douyinfe/semi-ui';
|
|
||||||
|
|
||||||
export interface TypeSelectorProps {
|
|
||||||
value?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
onChange?: (value?: string) => void;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}
|
|
||||||
const dropdownMenus = ['object', 'boolean', 'array', 'string', 'integer', 'number'];
|
|
||||||
|
|
||||||
export const TypeSelector: React.FC<TypeSelectorProps> = (props) => {
|
|
||||||
const { value, disabled } = props;
|
|
||||||
const icon = VariableTypeIcons[value as any];
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
trigger="hover"
|
|
||||||
position="bottomRight"
|
|
||||||
disabled={disabled}
|
|
||||||
render={
|
|
||||||
<Dropdown.Menu>
|
|
||||||
{dropdownMenus.map((key) => (
|
|
||||||
<Dropdown.Item
|
|
||||||
key={key}
|
|
||||||
onClick={() => {
|
|
||||||
props.onChange?.(key);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{VariableTypeIcons[key]}
|
|
||||||
<span style={{ paddingLeft: '4px' }}>{key}</span>
|
|
||||||
</Dropdown.Item>
|
|
||||||
))}
|
|
||||||
</Dropdown.Menu>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Tag
|
|
||||||
color="white"
|
|
||||||
style={props.style}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</Tag>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -20,6 +20,24 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'Hello Flow.',
|
default: 'Hello Flow.',
|
||||||
},
|
},
|
||||||
|
enable: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
array_obj: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
int: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
str: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -35,7 +53,6 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
title: 'Condition',
|
title: 'Condition',
|
||||||
inputsValues: {
|
|
||||||
conditions: [
|
conditions: [
|
||||||
{
|
{
|
||||||
key: 'if_0',
|
key: 'if_0',
|
||||||
@ -53,26 +70,6 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
inputs: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
conditions: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
key: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'end_0',
|
id: 'end_0',
|
||||||
@ -106,17 +103,9 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
title: 'Loop_2',
|
title: 'Loop_2',
|
||||||
inputsValues: {
|
batchFor: {
|
||||||
loopTimes: 2,
|
type: 'ref',
|
||||||
},
|
content: ['start_0', 'array_obj'],
|
||||||
inputs: {
|
|
||||||
type: 'object',
|
|
||||||
required: ['loopTimes'],
|
|
||||||
properties: {
|
|
||||||
loopTimes: {
|
|
||||||
type: 'number',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
outputs: {
|
outputs: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|||||||
@ -1,43 +1,50 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { Field, FieldArray } from '@flowgram.ai/free-layout-editor';
|
import { Field, FieldArray } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { IFlowValue, VariableSelector } from '@flowgram.ai/form-materials';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconPlus, IconCrossCircleStroked } from '@douyinfe/semi-icons';
|
import { IconPlus, IconCrossCircleStroked } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
import { FlowLiteralValueSchema, FlowRefValueSchema } from '../../../typings';
|
|
||||||
import { useIsSidebar } from '../../../hooks';
|
import { useIsSidebar } from '../../../hooks';
|
||||||
import { FxExpression } from '../../../form-components/fx-expression';
|
|
||||||
import { FormItem } from '../../../form-components';
|
import { FormItem } from '../../../form-components';
|
||||||
import { Feedback } from '../../../form-components';
|
import { Feedback } from '../../../form-components';
|
||||||
import { ConditionPort } from './styles';
|
import { ConditionPort } from './styles';
|
||||||
|
|
||||||
interface ConditionValue {
|
interface ConditionValue {
|
||||||
key: string;
|
key: string;
|
||||||
value: FlowLiteralValueSchema | FlowRefValueSchema;
|
value: IFlowValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConditionInputs() {
|
export function ConditionInputs() {
|
||||||
const readonly = !useIsSidebar();
|
const readonly = !useIsSidebar();
|
||||||
return (
|
return (
|
||||||
<FieldArray name="inputsValues.conditions">
|
<FieldArray name="conditions">
|
||||||
{({ field }) => (
|
{({ field }) => (
|
||||||
<>
|
<>
|
||||||
{field.map((child, index) => (
|
{field.map((child, index) => (
|
||||||
<Field<ConditionValue> key={child.name} name={child.name}>
|
<Field<ConditionValue> key={child.name} name={child.name}>
|
||||||
{({ field: childField, fieldState: childState }) => (
|
{({ field: childField, fieldState: childState }) => (
|
||||||
<FormItem name="if" type="boolean" required={true} labelWidth={40}>
|
<FormItem name="if" type="boolean" required={true} labelWidth={40}>
|
||||||
<FxExpression
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
value={childField.value.value}
|
<VariableSelector
|
||||||
onChange={(v) => childField.onChange({ key: childField.value.key, value: v })}
|
style={{ width: '100%' }}
|
||||||
icon={
|
value={childField.value?.value?.content as string[]}
|
||||||
|
onChange={(v) =>
|
||||||
|
childField.onChange({
|
||||||
|
key: childField.value.key,
|
||||||
|
value: { type: 'ref', content: v },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
hasError={Object.keys(childState?.errors || {}).length > 0}
|
||||||
|
readonly={readonly}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme="borderless"
|
||||||
icon={<IconCrossCircleStroked />}
|
icon={<IconCrossCircleStroked />}
|
||||||
onClick={() => field.delete(index)}
|
onClick={() => field.delete(index)}
|
||||||
/>
|
/>
|
||||||
}
|
</div>
|
||||||
hasError={Object.keys(childState?.errors || {}).length > 0}
|
|
||||||
readonly={readonly}
|
|
||||||
/>
|
|
||||||
<Feedback errors={childState?.errors} invalid={childState?.invalid} />
|
<Feedback errors={childState?.errors} invalid={childState?.invalid} />
|
||||||
<ConditionPort data-port-id={childField.value.key} data-port-type="output" />
|
<ConditionPort data-port-id={childField.value.key} data-port-type="output" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@ -4,6 +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 { defaultFormMeta } from '../default-form-meta';
|
import { defaultFormMeta } from '../default-form-meta';
|
||||||
import { FlowNodeRegistry } from '../../typings';
|
import { FlowNodeRegistry } from '../../typings';
|
||||||
@ -63,30 +64,15 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
|
|||||||
type: 'loop',
|
type: 'loop',
|
||||||
data: {
|
data: {
|
||||||
title: `Loop_${++index}`,
|
title: `Loop_${++index}`,
|
||||||
inputsValues: {
|
|
||||||
loopTimes: 2,
|
|
||||||
},
|
|
||||||
inputs: {
|
|
||||||
type: 'object',
|
|
||||||
required: ['loopTimes'],
|
|
||||||
properties: {
|
|
||||||
loopTimes: {
|
|
||||||
type: 'number',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outputs: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
result: { type: 'string' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
formMeta: {
|
formMeta: {
|
||||||
...defaultFormMeta,
|
...defaultFormMeta,
|
||||||
render: LoopFormRender,
|
render: LoopFormRender,
|
||||||
|
effect: {
|
||||||
|
batchFor: provideBatchInputEffect,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
onCreate() {
|
onCreate() {
|
||||||
// NOTICE: 这个函数是为了避免触发固定布局 flowDocument.addBlocksAsChildren
|
// NOTICE: 这个函数是为了避免触发固定布局 flowDocument.addBlocksAsChildren
|
||||||
|
|||||||
@ -1,17 +1,43 @@
|
|||||||
import { FormRenderProps, FlowNodeJSON } 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 { useIsSidebar } from '../../hooks';
|
import { useIsSidebar } from '../../hooks';
|
||||||
import { FormHeader, FormContent, FormInputs, FormOutputs } from '../../form-components';
|
import { FormHeader, FormContent, FormOutputs, FormItem, Feedback } from '../../form-components';
|
||||||
|
|
||||||
export const LoopFormRender = ({ form }: FormRenderProps<FlowNodeJSON>) => {
|
interface LoopNodeJSON extends FlowNodeJSON {
|
||||||
|
data: {
|
||||||
|
batchFor: IFlowRefValue;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {
|
||||||
const isSidebar = useIsSidebar();
|
const isSidebar = useIsSidebar();
|
||||||
|
const readonly = !isSidebar;
|
||||||
|
|
||||||
|
const batchFor = (
|
||||||
|
<Field<IFlowRefValue> name={`batchFor`}>
|
||||||
|
{({ field, fieldState }) => (
|
||||||
|
<FormItem name={'batchFor'} type={'array'} required>
|
||||||
|
<BatchVariableSelector
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={field.value?.content}
|
||||||
|
onChange={(val) => field.onChange({ type: 'ref', content: 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>
|
||||||
<FormInputs />
|
{batchFor}
|
||||||
<FormOutputs />
|
<FormOutputs />
|
||||||
</FormContent>
|
</FormContent>
|
||||||
</>
|
</>
|
||||||
@ -21,7 +47,7 @@ export const LoopFormRender = ({ form }: FormRenderProps<FlowNodeJSON>) => {
|
|||||||
<>
|
<>
|
||||||
<FormHeader />
|
<FormHeader />
|
||||||
<FormContent>
|
<FormContent>
|
||||||
<FormInputs />
|
{batchFor}
|
||||||
<SubCanvasRender />
|
<SubCanvasRender />
|
||||||
<FormOutputs />
|
<FormOutputs />
|
||||||
</FormContent>
|
</FormContent>
|
||||||
|
|||||||
@ -7,8 +7,6 @@ import {
|
|||||||
ASTFactory,
|
ASTFactory,
|
||||||
} from '@flowgram.ai/free-layout-editor';
|
} from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
import { createASTFromJSONSchema } from './utils';
|
|
||||||
|
|
||||||
export interface SyncVariablePluginOptions {}
|
export interface SyncVariablePluginOptions {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +37,7 @@ export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions>
|
|||||||
|
|
||||||
// Create an Type AST from the output data's JSON schema
|
// Create an Type AST from the output data's JSON schema
|
||||||
// NOTICE: You can create a new function to generate an AST based on YOUR CUSTOM DSL
|
// NOTICE: You can create a new function to generate an AST based on YOUR CUSTOM DSL
|
||||||
const typeAST = createASTFromJSONSchema(value);
|
const typeAST = ASTFactory.createTypeASTFromSchema(value);
|
||||||
|
|
||||||
if (typeAST) {
|
if (typeAST) {
|
||||||
// Use the node's title or its ID as the title for the variable
|
// Use the node's title or its ID as the title for the variable
|
||||||
@ -53,7 +51,7 @@ export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions>
|
|||||||
icon: node.getNodeRegistry()?.info?.icon,
|
icon: node.getNodeRegistry()?.info?.icon,
|
||||||
// NOTICE: You can add more metadata here as needed
|
// NOTICE: You can add more metadata here as needed
|
||||||
},
|
},
|
||||||
key: `${node.id}.outputs`,
|
key: `${node.id}`,
|
||||||
type: typeAST,
|
type: typeAST,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
import { get } from 'lodash-es';
|
|
||||||
import { ASTFactory, type ASTNodeJSON } from '@flowgram.ai/free-layout-editor';
|
|
||||||
|
|
||||||
import { type JsonSchema } from '../../typings';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorts the properties of a JSON schema based on the 'extra.index' field.
|
|
||||||
* If the 'extra.index' field is not present, the property will be treated as having an index of 0.
|
|
||||||
*
|
|
||||||
* @param properties - The properties of the JSON schema to sort.
|
|
||||||
* @returns A sorted array of property entries.
|
|
||||||
*/
|
|
||||||
function sortProperties(properties: Record<string, JsonSchema>) {
|
|
||||||
return Object.entries(properties).sort(
|
|
||||||
(a, b) => (get(a?.[1], 'extra.index') || 0) - (get(b?.[1], 'extra.index') || 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a JSON schema to an Abstract Syntax Tree (AST) representation.
|
|
||||||
* This function recursively processes the JSON schema and creates corresponding AST nodes.
|
|
||||||
*
|
|
||||||
* For more information on JSON Schema, refer to the official documentation:
|
|
||||||
* https://json-schema.org/
|
|
||||||
*
|
|
||||||
* Note: Depending on your business needs, you can use your own Domain-Specific Language (DSL)
|
|
||||||
* Create a new function to convert your custom DSL to AST directly.
|
|
||||||
*
|
|
||||||
* @param jsonSchema - The JSON schema to convert.
|
|
||||||
* @returns An AST node representing the JSON schema, or undefined if the schema type is not recognized.
|
|
||||||
*/
|
|
||||||
export function createASTFromJSONSchema(jsonSchema: JsonSchema): ASTNodeJSON | undefined {
|
|
||||||
const { type } = jsonSchema || {};
|
|
||||||
|
|
||||||
if (!type) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'object':
|
|
||||||
return ASTFactory.createObject({
|
|
||||||
properties: sortProperties(jsonSchema.properties || {}).map(([key, _property]) => ({
|
|
||||||
key,
|
|
||||||
type: createASTFromJSONSchema(_property),
|
|
||||||
meta: { description: _property.description },
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
case 'array':
|
|
||||||
return ASTFactory.createArray({
|
|
||||||
items: createASTFromJSONSchema(jsonSchema.items!),
|
|
||||||
});
|
|
||||||
case 'string':
|
|
||||||
return ASTFactory.createString();
|
|
||||||
case 'number':
|
|
||||||
return ASTFactory.createNumber();
|
|
||||||
case 'boolean':
|
|
||||||
return ASTFactory.createBoolean();
|
|
||||||
case 'integer':
|
|
||||||
return ASTFactory.createInteger();
|
|
||||||
|
|
||||||
default:
|
|
||||||
// If the type is not recognized, return CustomType
|
|
||||||
return ASTFactory.createCustomType({ typeName: type });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +1,4 @@
|
|||||||
export type BasicType = 'boolean' | 'string' | 'integer' | 'number' | 'object' | 'array';
|
import type { VarJSONSchema } from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
export interface JsonSchema<T extends BasicType = BasicType> {
|
export type BasicType = VarJSONSchema.BasicType;
|
||||||
type?: T;
|
export type JsonSchema = VarJSONSchema.ISchema;
|
||||||
default?: any;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
enum?: (string | number)[];
|
|
||||||
properties?: Record<string, JsonSchema>;
|
|
||||||
additionalProperties?: JsonSchema;
|
|
||||||
items?: JsonSchema;
|
|
||||||
required?: string[];
|
|
||||||
$ref?: string;
|
|
||||||
extra?: {
|
|
||||||
literal?: boolean; // is literal type
|
|
||||||
formComponent?: string; // Set the render component
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -6,17 +6,10 @@ import {
|
|||||||
type WorkflowEdgeJSON,
|
type WorkflowEdgeJSON,
|
||||||
WorkflowNodeMeta,
|
WorkflowNodeMeta,
|
||||||
} from '@flowgram.ai/free-layout-editor';
|
} from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { IFlowValue } from '@flowgram.ai/form-materials';
|
||||||
|
|
||||||
import { type JsonSchema } from './json-schema';
|
import { type JsonSchema } from './json-schema';
|
||||||
|
|
||||||
export type FlowLiteralValueSchema = string | number | boolean;
|
|
||||||
export type FlowObjectValueSchema = Record<string, FlowLiteralValueSchema | FlowRefValueSchema>;
|
|
||||||
export type FlowArrayValueSchema = FlowObjectValueSchema[];
|
|
||||||
export type FlowRefValueSchema =
|
|
||||||
| { type: 'ref'; content?: string }
|
|
||||||
| { type: 'expression'; content?: string }
|
|
||||||
| { type: 'template'; content?: string };
|
|
||||||
export type FlowValueSchema = FlowLiteralValueSchema | FlowRefValueSchema | FlowArrayValueSchema;
|
|
||||||
/**
|
/**
|
||||||
* You can customize the data of the node, and here you can use JsonSchema to define the input and output of the node
|
* You can customize the data of the node, and here you can use JsonSchema to define the input and output of the node
|
||||||
* 你可以自定义节点的 data 业务数据, 这里演示 通过 JsonSchema 来定义节点的输入/输出
|
* 你可以自定义节点的 data 业务数据, 这里演示 通过 JsonSchema 来定义节点的输入/输出
|
||||||
@ -30,7 +23,7 @@ export interface FlowNodeJSON extends FlowNodeJSONDefault {
|
|||||||
/**
|
/**
|
||||||
* Inputs data values
|
* Inputs data values
|
||||||
*/
|
*/
|
||||||
inputsValues?: Record<string, FlowValueSchema>;
|
inputsValues?: Record<string, IFlowValue>;
|
||||||
/**
|
/**
|
||||||
* Define the inputs data of the node by JsonSchema
|
* Define the inputs data of the node by JsonSchema
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -6,13 +6,11 @@
|
|||||||
|
|
||||||
Official form materials can be used directly via package reference:
|
Official form materials can be used directly via package reference:
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { PackageManagerTabs } from '@theme';
|
import { PackageManagerTabs } from '@theme';
|
||||||
|
|
||||||
<PackageManagerTabs command={{
|
<PackageManagerTabs command={{
|
||||||
npm: "npm install @flowgram.ai/form-materials"
|
npm: "npm install @flowgram.ai/form-materials"
|
||||||
}} />
|
}} />
|
||||||
```
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { JsonSchemaEditor } from '@flowgram.ai/form-materials'
|
import { JsonSchemaEditor } from '@flowgram.ai/form-materials'
|
||||||
@ -103,3 +101,42 @@ Adding source code via CLI:
|
|||||||
```bash
|
```bash
|
||||||
npx @flowgram.ai/materials components/json-schema-editor
|
npx @flowgram.ai/materials components/json-schema-editor
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### [DynamicValueInput](https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/dynamic-value-input/index.tsx)
|
||||||
|
|
||||||
|
DynamicValueInput is used for configuring values (constant values + variable values).
|
||||||
|
|
||||||
|
<img loading="lazy" src="/materials/dynamic-value-input.png" style={{width:500}}/>
|
||||||
|
|
||||||
|
Usage via package reference:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { DynamicValueInput } from '@flowgram.ai/materials'
|
||||||
|
```
|
||||||
|
|
||||||
|
Adding source code via CLI:
|
||||||
|
```bash
|
||||||
|
npx @flowgram.ai/materials components/dynamic-value-input
|
||||||
|
```
|
||||||
|
|
||||||
|
## Currently Supported Effect Materials
|
||||||
|
|
||||||
|
### [provideBatchInputEffect](https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/provide-batch-input/index.ts)
|
||||||
|
|
||||||
|
provideBatchInputEffect is used for configuring batch input derivation in loops. It automatically derives two variables based on the input:
|
||||||
|
|
||||||
|
- item: Derived from the input variable array type, representing each item in the loop
|
||||||
|
- index: Numeric type, representing the iteration count
|
||||||
|
|
||||||
|
<img loading="lazy" src="/materials/provide-batch-input.png" style={{width:500}}/>
|
||||||
|
|
||||||
|
Usage via package reference:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { provideBatchInputEffect } from '@flowgram.ai/materials'
|
||||||
|
```
|
||||||
|
|
||||||
|
Adding source code via CLI:
|
||||||
|
```bash
|
||||||
|
npx @flowgram.ai/materials effects/provide-batch-input
|
||||||
|
```
|
||||||
|
|||||||
BIN
apps/docs/src/public/materials/dynamic-value-input.png
Normal file
BIN
apps/docs/src/public/materials/dynamic-value-input.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
BIN
apps/docs/src/public/materials/provide-batch-input.png
Normal file
BIN
apps/docs/src/public/materials/provide-batch-input.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 281 KiB |
@ -107,3 +107,44 @@ import { JsonSchemaEditor } from '@flowgram.ai/materials'
|
|||||||
```bash
|
```bash
|
||||||
npx @flowgram.ai/materials components/json-schema-editor
|
npx @flowgram.ai/materials components/json-schema-editor
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### [DynamicValueInput](https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/dynamic-value-input/index.tsx)
|
||||||
|
|
||||||
|
DynamicValueInput 用于值(常量值 + 变量值)的配置
|
||||||
|
|
||||||
|
<img loading="lazy" src="/materials/dynamic-value-input.png" style={{width:500}}/>
|
||||||
|
|
||||||
|
|
||||||
|
通过包引用使用:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { DynamicValueInput } from '@flowgram.ai/materials'
|
||||||
|
```
|
||||||
|
|
||||||
|
通过 CLI 复制源代码使用:
|
||||||
|
```bash
|
||||||
|
npx @flowgram.ai/materials components/dynamic-value-input
|
||||||
|
```
|
||||||
|
|
||||||
|
## 当前支持的 Effect 物料
|
||||||
|
|
||||||
|
### [provideBatchInputEffect](https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/provide-batch-input/index.ts)
|
||||||
|
|
||||||
|
provideBatchInputEffect 用于配置循环批处理输入推导,会根据输入自动推导出两个变量:
|
||||||
|
|
||||||
|
- item:根据输入变量数组类型自动推导,代表循环的每一项
|
||||||
|
- index:数字类型,代表循环第几次
|
||||||
|
|
||||||
|
<img loading="lazy" src="/materials/provide-batch-input.png" style={{width:500}}/>
|
||||||
|
|
||||||
|
通过包引用使用:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { provideBatchInputEffect } from '@flowgram.ai/materials'
|
||||||
|
```
|
||||||
|
|
||||||
|
通过 CLI 复制源代码使用:
|
||||||
|
```bash
|
||||||
|
npx @flowgram.ai/materials effects/provide-batch-input
|
||||||
|
```
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const _types = ['components'];
|
const _types = ['components', 'effects', 'utils', 'typings'];
|
||||||
|
|
||||||
export function listAllMaterials() {
|
export function listAllMaterials() {
|
||||||
const _materials = [];
|
const _materials = [];
|
||||||
@ -32,7 +32,9 @@ export function listAllMaterials() {
|
|||||||
|
|
||||||
export function bfsMaterials(material, _materials = []) {
|
export function bfsMaterials(material, _materials = []) {
|
||||||
function findConfigByName(name) {
|
function findConfigByName(name) {
|
||||||
return _materials.find((_config) => _config.name === name);
|
return _materials.find(
|
||||||
|
(_config) => _config.name === name || `${_config.type}/${_config.name}` === name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const queue = [material];
|
const queue = [material];
|
||||||
@ -67,11 +69,25 @@ export function bfsMaterials(material, _materials = []) {
|
|||||||
|
|
||||||
export const copyMaterial = (material, projectInfo) => {
|
export const copyMaterial = (material, projectInfo) => {
|
||||||
const sourceDir = material.path;
|
const sourceDir = material.path;
|
||||||
const targetDir = path.join(
|
const materialRoot = path.join(
|
||||||
projectInfo.projectPath,
|
projectInfo.projectPath,
|
||||||
|
'src',
|
||||||
'form-materials',
|
'form-materials',
|
||||||
`${material.type}`,
|
`${material.type}`
|
||||||
material.name
|
|
||||||
);
|
);
|
||||||
|
const targetDir = path.join(materialRoot, material.name);
|
||||||
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
||||||
|
|
||||||
|
let materialRootIndexTs = '';
|
||||||
|
if (fs.existsSync(path.join(materialRoot, 'index.ts'))) {
|
||||||
|
materialRootIndexTs = fs.readFileSync(path.join(materialRoot, 'index.ts'), 'utf8');
|
||||||
|
}
|
||||||
|
if (!materialRootIndexTs.includes(material.name)) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(materialRoot, 'index.ts'),
|
||||||
|
`${materialRootIndexTs}${materialRootIndexTs.endsWith('\n') ? '' : '\n'}export * from './${
|
||||||
|
material.name
|
||||||
|
}';\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "batch-variable-selector",
|
||||||
|
"depMaterials": ["variable-selector"],
|
||||||
|
"depPackages": []
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { PrivateScopeProvider, VarJSONSchema } from '@flowgram.ai/editor';
|
||||||
|
|
||||||
|
import { VariableSelector, VariableSelectorProps } from '../variable-selector';
|
||||||
|
|
||||||
|
const batchVariableSchema: VarJSONSchema.ISchema = {
|
||||||
|
type: 'array',
|
||||||
|
extra: { weak: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BatchVariableSelector(props: VariableSelectorProps) {
|
||||||
|
return (
|
||||||
|
<PrivateScopeProvider>
|
||||||
|
<VariableSelector {...props} includeSchema={batchVariableSchema} />
|
||||||
|
</PrivateScopeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"name": "constant-input",
|
||||||
|
"depMaterials": [],
|
||||||
|
"depPackages": ["@douyinfe/semi-ui"]
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
/* eslint-disable react/prop-types */
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { Input, InputNumber, Select } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { PropsType, Strategy } from './types';
|
||||||
|
|
||||||
|
const defaultStrategies: Strategy[] = [
|
||||||
|
{
|
||||||
|
hit: (schema) => schema?.type === 'string',
|
||||||
|
Renderer: (props) => (
|
||||||
|
<Input placeholder="Please Input String" size="small" disabled={props.readonly} {...props} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hit: (schema) => schema?.type === 'number',
|
||||||
|
Renderer: (props) => (
|
||||||
|
<InputNumber
|
||||||
|
placeholder="Please Input Number"
|
||||||
|
size="small"
|
||||||
|
disabled={props.readonly}
|
||||||
|
hideButtons
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hit: (schema) => schema?.type === 'integer',
|
||||||
|
Renderer: (props) => (
|
||||||
|
<InputNumber
|
||||||
|
placeholder="Please Input Integer"
|
||||||
|
size="small"
|
||||||
|
disabled={props.readonly}
|
||||||
|
hideButtons
|
||||||
|
precision={0}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hit: (schema) => schema?.type === 'boolean',
|
||||||
|
Renderer: (props) => {
|
||||||
|
const { value, onChange, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
placeholder="Please Select Boolean"
|
||||||
|
size="small"
|
||||||
|
disabled={props.readonly}
|
||||||
|
optionList={[
|
||||||
|
{ label: 'True', value: 1 },
|
||||||
|
{ label: 'False', value: 0 },
|
||||||
|
]}
|
||||||
|
value={value ? 1 : 0}
|
||||||
|
onChange={(value) => onChange?.(!!value)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ConstantInput(props: PropsType) {
|
||||||
|
const { value, onChange, schema, strategies: extraStrategies, readonly, ...rest } = props;
|
||||||
|
|
||||||
|
const strategies = useMemo(
|
||||||
|
() => [...defaultStrategies, ...(extraStrategies || [])],
|
||||||
|
[extraStrategies]
|
||||||
|
);
|
||||||
|
|
||||||
|
const Renderer = useMemo(() => {
|
||||||
|
const strategy = strategies.find((_strategy) => _strategy.hit(schema));
|
||||||
|
|
||||||
|
return strategy?.Renderer;
|
||||||
|
}, [strategies, schema]);
|
||||||
|
|
||||||
|
if (!Renderer) {
|
||||||
|
return <Input size="small" disabled placeholder="Unsupported type" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Renderer value={value} onChange={onChange} readonly={readonly} {...rest} />;
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { VarJSONSchema } from '@flowgram.ai/editor';
|
||||||
|
|
||||||
|
export interface Strategy<Value = any> {
|
||||||
|
hit: (schema: VarJSONSchema.ISchema) => boolean;
|
||||||
|
Renderer: React.FC<RendererProps<Value>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RendererProps<Value = any> {
|
||||||
|
value?: Value;
|
||||||
|
onChange?: (value: Value) => void;
|
||||||
|
readonly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PropsType extends RendererProps {
|
||||||
|
schema: VarJSONSchema.ISchema;
|
||||||
|
strategies?: Strategy[];
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "dynamic-value-input",
|
||||||
|
"depMaterials": ["flow-value", "constant-input", "variable-selector"],
|
||||||
|
"depPackages": ["@douyinfe/semi-ui", "@douyinfe/semi-icons", "styled-components"]
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { VarJSONSchema } from '@flowgram.ai/editor';
|
||||||
|
import { IconButton } from '@douyinfe/semi-ui';
|
||||||
|
import { IconSetting } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { Strategy } from '../constant-input/types';
|
||||||
|
import { ConstantInput } from '../constant-input';
|
||||||
|
import { IFlowConstantRefValue } from '../../typings/flow-value';
|
||||||
|
import { UIContainer, UIMain, UITrigger } from './styles';
|
||||||
|
import { VariableSelector } from '../variable-selector';
|
||||||
|
|
||||||
|
interface PropsType {
|
||||||
|
value?: IFlowConstantRefValue;
|
||||||
|
onChange: (value?: IFlowConstantRefValue) => void;
|
||||||
|
readonly?: boolean;
|
||||||
|
hasError?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
schema?: VarJSONSchema.ISchema;
|
||||||
|
constantProps?: {
|
||||||
|
strategies?: Strategy[];
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DynamicValueInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
readonly,
|
||||||
|
style,
|
||||||
|
schema,
|
||||||
|
constantProps,
|
||||||
|
}: PropsType) {
|
||||||
|
const renderMain = () => {
|
||||||
|
if (value?.type === 'ref') {
|
||||||
|
// Display Variable Or Delete
|
||||||
|
return (
|
||||||
|
<VariableSelector
|
||||||
|
value={value?.content}
|
||||||
|
onChange={(_v) => onChange(_v ? { type: 'ref', content: _v } : undefined)}
|
||||||
|
includeSchema={schema}
|
||||||
|
readonly={readonly}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConstantInput
|
||||||
|
value={value?.content}
|
||||||
|
onChange={(_v) => onChange({ type: 'constant', content: _v })}
|
||||||
|
schema={schema || { type: 'string' }}
|
||||||
|
readonly={readonly}
|
||||||
|
{...constantProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTrigger = () => (
|
||||||
|
<VariableSelector
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={value?.type === 'ref' ? value?.content : undefined}
|
||||||
|
onChange={(_v) => onChange({ type: 'ref', content: _v })}
|
||||||
|
includeSchema={schema}
|
||||||
|
readonly={readonly}
|
||||||
|
triggerRender={() => (
|
||||||
|
<IconButton disabled={readonly} size="small" icon={<IconSetting size="small" />} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIContainer style={style}>
|
||||||
|
<UIMain>{renderMain()}</UIMain>
|
||||||
|
<UITrigger>{renderTrigger()}</UITrigger>
|
||||||
|
</UIContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const UIContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UIMain = styled.div`
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
& .semi-tree-select,
|
||||||
|
& .semi-input-number,
|
||||||
|
& .semi-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UITrigger = styled.div``;
|
||||||
@ -1,3 +1,6 @@
|
|||||||
export { VariableSelector } from './variable-selector';
|
export * from './variable-selector';
|
||||||
export { TypeSelector, JsonSchema, VariableTypeIcons, ArrayIcons } from './type-selector';
|
export * from './type-selector';
|
||||||
export { JsonSchemaEditor } from './json-schema-editor';
|
export * from './json-schema-editor';
|
||||||
|
export * from './batch-variable-selector';
|
||||||
|
export * from './constant-input';
|
||||||
|
export * from './dynamic-value-input';
|
||||||
|
|||||||
@ -36,13 +36,19 @@ export function usePropertiesEdit(
|
|||||||
const initPropertyList = useMemo(
|
const initPropertyList = useMemo(
|
||||||
() =>
|
() =>
|
||||||
isDrilldownObject
|
isDrilldownObject
|
||||||
? Object.entries(drilldown.schema?.properties || {}).map(
|
? Object.entries(drilldown.schema?.properties || {})
|
||||||
([name, _value]) =>
|
.sort(([, a], [, b]) => (a.extra?.index ?? 0) - (b.extra?.index ?? 0))
|
||||||
|
.map(
|
||||||
|
([name, _value], index) =>
|
||||||
({
|
({
|
||||||
key: genId(),
|
key: genId(),
|
||||||
name,
|
name,
|
||||||
isPropertyRequired: drilldown.schema?.required?.includes(name) || false,
|
isPropertyRequired: drilldown.schema?.required?.includes(name) || false,
|
||||||
..._value,
|
..._value,
|
||||||
|
extra: {
|
||||||
|
...(_value.extra || {}),
|
||||||
|
index,
|
||||||
|
},
|
||||||
} as PropertyValueType)
|
} as PropertyValueType)
|
||||||
)
|
)
|
||||||
: [],
|
: [],
|
||||||
@ -65,7 +71,9 @@ export function usePropertiesEdit(
|
|||||||
nameMap.set(_property.name, _property);
|
nameMap.set(_property.name, _property);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Object.entries(drilldown.schema?.properties || {}).map(([name, _value]) => {
|
return Object.entries(drilldown.schema?.properties || {})
|
||||||
|
.sort(([, a], [, b]) => (a.extra?.index ?? 0) - (b.extra?.index ?? 0))
|
||||||
|
.map(([name, _value]) => {
|
||||||
const _property = nameMap.get(name);
|
const _property = nameMap.get(name);
|
||||||
if (_property) {
|
if (_property) {
|
||||||
return {
|
return {
|
||||||
@ -121,7 +129,10 @@ export function usePropertiesEdit(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onAddProperty = () => {
|
const onAddProperty = () => {
|
||||||
updatePropertyList((_list) => [..._list, { key: genId(), name: '', type: 'string' }]);
|
updatePropertyList((_list) => [
|
||||||
|
..._list,
|
||||||
|
{ key: genId(), name: '', type: 'string', extra: { index: _list.length + 1 } },
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRemoveProperty = (key: number) => {
|
const onRemoveProperty = (key: number) => {
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { ArrayIcons, VariableTypeIcons, getSchemaIcon, options } from './constan
|
|||||||
interface PropTypes {
|
interface PropTypes {
|
||||||
value?: Partial<JsonSchema>;
|
value?: Partial<JsonSchema>;
|
||||||
onChange: (value?: Partial<JsonSchema>) => void;
|
onChange: (value?: Partial<JsonSchema>) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTypeSelectValue = (value?: Partial<JsonSchema>): string[] | undefined => {
|
export const getTypeSelectValue = (value?: Partial<JsonSchema>): string[] | undefined => {
|
||||||
@ -29,15 +31,16 @@ export const parseTypeSelectValue = (value?: string[]): Partial<JsonSchema> | un
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function TypeSelector(props: PropTypes) {
|
export function TypeSelector(props: PropTypes) {
|
||||||
const { value, onChange } = props;
|
const { value, onChange, disabled, style } = props;
|
||||||
|
|
||||||
const selectValue = useMemo(() => getTypeSelectValue(value), [value]);
|
const selectValue = useMemo(() => getTypeSelectValue(value), [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Cascader
|
<Cascader
|
||||||
|
disabled={disabled}
|
||||||
size="small"
|
size="small"
|
||||||
triggerRender={() => (
|
triggerRender={() => (
|
||||||
<Button size="small" style={{ width: 50 }}>
|
<Button size="small" style={style}>
|
||||||
{getSchemaIcon(value)}
|
{getSchemaIcon(value)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,19 +1,5 @@
|
|||||||
export type BasicType = 'boolean' | 'string' | 'integer' | 'number' | 'object' | 'array';
|
import { VarJSONSchema } from '@flowgram.ai/editor';
|
||||||
|
|
||||||
export interface JsonSchema<T = string> {
|
export type BasicType = VarJSONSchema.BasicType;
|
||||||
type?: T;
|
|
||||||
default?: any;
|
export type JsonSchema = VarJSONSchema.ISchema;
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
enum?: (string | number)[];
|
|
||||||
properties?: Record<string, JsonSchema>;
|
|
||||||
additionalProperties?: JsonSchema;
|
|
||||||
items?: JsonSchema;
|
|
||||||
required?: string[];
|
|
||||||
$ref?: string;
|
|
||||||
extra?: {
|
|
||||||
order?: number;
|
|
||||||
literal?: boolean; // is literal type
|
|
||||||
formComponent?: string; // Set the render component
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "variable-selector",
|
"name": "variable-selector",
|
||||||
"depMaterials": ["type-selector"],
|
"depMaterials": ["type-selector"],
|
||||||
"depPackages": ["@douyinfe/semi-ui"]
|
"depPackages": ["@douyinfe/semi-ui", "styled-components"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,47 +1,107 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { TreeSelect } from '@douyinfe/semi-ui';
|
import { VarJSONSchema } from '@flowgram.ai/editor';
|
||||||
|
import { TriggerRenderProps } from '@douyinfe/semi-ui/lib/es/treeSelect';
|
||||||
|
import { TreeNodeData } from '@douyinfe/semi-ui/lib/es/tree';
|
||||||
|
import { IconChevronDownStroked, IconIssueStroked } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
import { useVariableTree } from './use-variable-tree';
|
import { useVariableTree } from './use-variable-tree';
|
||||||
|
import { UIRootTitle, UITag, UITreeSelect } from './styles';
|
||||||
|
|
||||||
export interface PropTypes {
|
interface PropTypes {
|
||||||
value?: string;
|
value?: string[];
|
||||||
config: {
|
config?: {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
notFoundContent?: string;
|
||||||
};
|
};
|
||||||
onChange: (value?: string) => void;
|
onChange: (value?: string[]) => void;
|
||||||
|
includeSchema?: VarJSONSchema.ISchema | VarJSONSchema.ISchema[];
|
||||||
|
excludeSchema?: VarJSONSchema.ISchema | VarJSONSchema.ISchema[];
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
triggerRender?: (props: TriggerRenderProps) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type VariableSelectorProps = PropTypes;
|
||||||
|
|
||||||
export const VariableSelector = ({
|
export const VariableSelector = ({
|
||||||
value,
|
value,
|
||||||
config,
|
config,
|
||||||
onChange,
|
onChange,
|
||||||
style,
|
style,
|
||||||
readonly = false,
|
readonly = false,
|
||||||
|
includeSchema,
|
||||||
|
excludeSchema,
|
||||||
hasError,
|
hasError,
|
||||||
|
triggerRender,
|
||||||
}: PropTypes) => {
|
}: PropTypes) => {
|
||||||
const treeData = useVariableTree();
|
const treeData = useVariableTree({ includeSchema, excludeSchema });
|
||||||
|
|
||||||
|
const treeValue = useMemo(() => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
console.warn(
|
||||||
|
'The Value of VariableSelector is a string, it should be an ARRAY. \n',
|
||||||
|
'Please check the value of VariableSelector \n'
|
||||||
|
);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value?.join('.');
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const renderIcon = (icon: string | JSX.Element) => {
|
||||||
|
if (typeof icon === 'string') {
|
||||||
|
return <img style={{ marginRight: 8 }} width={12} height={12} src={icon} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return icon;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TreeSelect
|
<UITreeSelect
|
||||||
dropdownMatchSelectWidth={false}
|
dropdownMatchSelectWidth={false}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
size="small"
|
size="small"
|
||||||
value={value}
|
value={treeValue}
|
||||||
style={{
|
clearIcon={null}
|
||||||
...style,
|
$error={hasError}
|
||||||
outline: hasError ? '1px solid red' : undefined,
|
style={style}
|
||||||
}}
|
|
||||||
validateStatus={hasError ? 'error' : undefined}
|
validateStatus={hasError ? 'error' : undefined}
|
||||||
onChange={(option) => {
|
onChange={(_, _config) => {
|
||||||
onChange(option as string);
|
onChange((_config as TreeNodeData).keyPath as string[]);
|
||||||
}}
|
}}
|
||||||
showClear
|
renderSelectedItem={(_option: TreeNodeData) => {
|
||||||
|
if (!_option?.keyPath) {
|
||||||
|
return (
|
||||||
|
<UITag
|
||||||
|
prefixIcon={<IconIssueStroked />}
|
||||||
|
color="amber"
|
||||||
|
closable={!readonly}
|
||||||
|
onClose={() => onChange(undefined)}
|
||||||
|
>
|
||||||
|
{config?.notFoundContent ?? 'Undefined'}
|
||||||
|
</UITag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UITag
|
||||||
|
prefixIcon={renderIcon(_option.rootMeta?.icon || _option?.icon)}
|
||||||
|
closable={!readonly}
|
||||||
|
onClose={() => onChange(undefined)}
|
||||||
|
>
|
||||||
|
<UIRootTitle>
|
||||||
|
{_option.rootMeta?.title ? `${_option.rootMeta?.title} -` : null}
|
||||||
|
</UIRootTitle>
|
||||||
|
{_option.label}
|
||||||
|
</UITag>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
showClear={false}
|
||||||
|
arrowIcon={value ? null : <IconChevronDownStroked size="small" />}
|
||||||
|
triggerRender={triggerRender}
|
||||||
placeholder={config?.placeholder ?? 'Select Variable...'}
|
placeholder={config?.placeholder ?? 'Select Variable...'}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { Tag, TreeSelect } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
export const UIRootTitle = styled.span`
|
||||||
|
margin-right: 4px;
|
||||||
|
color: var(--semi-color-text-2);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UITag = styled(Tag)`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
& .semi-tag-content-center {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.semi-tag {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UITreeSelect = styled(TreeSelect)<{ $error?: boolean }>`
|
||||||
|
outline: ${({ $error }) => ($error ? '1px solid red' : 'none')};
|
||||||
|
|
||||||
|
height: 22px;
|
||||||
|
min-height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
& .semi-tree-select-selection {
|
||||||
|
padding: 0 2px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .semi-tree-select-selection-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .semi-tree-select-selection-placeholder {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { useScopeAvailable, ASTMatch, BaseVariableField } from '@flowgram.ai/editor';
|
import { useScopeAvailable, ASTMatch, BaseVariableField, VarJSONSchema } from '@flowgram.ai/editor';
|
||||||
import { TreeNodeData } from '@douyinfe/semi-ui/lib/es/tree';
|
import { TreeNodeData } from '@douyinfe/semi-ui/lib/es/tree';
|
||||||
import { Icon } from '@douyinfe/semi-ui';
|
import { Icon } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
@ -8,11 +8,16 @@ import { ArrayIcons, VariableTypeIcons } from '../type-selector/constants';
|
|||||||
|
|
||||||
type VariableField = BaseVariableField<{ icon?: string | JSX.Element; title?: string }>;
|
type VariableField = BaseVariableField<{ icon?: string | JSX.Element; title?: string }>;
|
||||||
|
|
||||||
export function useVariableTree(): TreeNodeData[] {
|
export function useVariableTree(params: {
|
||||||
|
includeSchema?: VarJSONSchema.ISchema | VarJSONSchema.ISchema[];
|
||||||
|
excludeSchema?: VarJSONSchema.ISchema | VarJSONSchema.ISchema[];
|
||||||
|
}): TreeNodeData[] {
|
||||||
|
const { includeSchema, excludeSchema } = params;
|
||||||
|
|
||||||
const available = useScopeAvailable();
|
const available = useScopeAvailable();
|
||||||
|
|
||||||
const getVariableTypeIcon = useCallback((variable: VariableField) => {
|
const getVariableTypeIcon = useCallback((variable: VariableField) => {
|
||||||
if (variable.meta.icon) {
|
if (variable.meta?.icon) {
|
||||||
if (typeof variable.meta.icon === 'string') {
|
if (typeof variable.meta.icon === 'string') {
|
||||||
return <img style={{ marginRight: 8 }} width={12} height={12} src={variable.meta.icon} />;
|
return <img style={{ marginRight: 8 }} width={12} height={12} src={variable.meta.icon} />;
|
||||||
}
|
}
|
||||||
@ -44,6 +49,10 @@ export function useVariableTree(): TreeNodeData[] {
|
|||||||
): TreeNodeData | null => {
|
): TreeNodeData | null => {
|
||||||
let type = variable?.type;
|
let type = variable?.type;
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let children: TreeNodeData[] | undefined;
|
let children: TreeNodeData[] | undefined;
|
||||||
|
|
||||||
if (ASTMatch.isObject(type)) {
|
if (ASTMatch.isObject(type)) {
|
||||||
@ -56,14 +65,27 @@ export function useVariableTree(): TreeNodeData[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currPath = [...parentFields.map((_field) => _field.key), variable.key].join('.');
|
const keyPath = [...parentFields.map((_field) => _field.key), variable.key];
|
||||||
|
const key = keyPath.join('.');
|
||||||
|
|
||||||
|
const isSchemaInclude = includeSchema ? type.isEqualWithJSONSchema(includeSchema) : true;
|
||||||
|
const isSchemaExclude = excludeSchema ? type.isEqualWithJSONSchema(excludeSchema) : false;
|
||||||
|
const isSchemaMatch = isSchemaInclude && !isSchemaExclude;
|
||||||
|
|
||||||
|
// If not match, and no children, return null
|
||||||
|
if (!isSchemaMatch && !children?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: currPath,
|
key: key,
|
||||||
label: variable.meta.title || variable.key,
|
label: variable.meta?.title || variable.key,
|
||||||
value: currPath,
|
value: key,
|
||||||
|
keyPath,
|
||||||
icon: getVariableTypeIcon(variable),
|
icon: getVariableTypeIcon(variable),
|
||||||
children,
|
children,
|
||||||
|
disabled: !isSchemaMatch,
|
||||||
|
rootMeta: parentFields[0]?.meta,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
2
packages/materials/form-materials/src/effects/index.ts
Normal file
2
packages/materials/form-materials/src/effects/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './provide-batch-input';
|
||||||
|
export * from './provide-batch-outputs';
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "provide-batch-input",
|
||||||
|
"depMaterials": ["flow-value"],
|
||||||
|
"depPackages": []
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
ASTFactory,
|
||||||
|
EffectOptions,
|
||||||
|
FlowNodeRegistry,
|
||||||
|
createEffectFromVariableProvider,
|
||||||
|
getNodeForm,
|
||||||
|
} from '@flowgram.ai/editor';
|
||||||
|
|
||||||
|
import { IFlowRefValue } from '../../typings';
|
||||||
|
|
||||||
|
export const provideBatchInputEffect: EffectOptions[] = createEffectFromVariableProvider({
|
||||||
|
private: true,
|
||||||
|
parse: (value: IFlowRefValue, ctx) => [
|
||||||
|
ASTFactory.createVariableDeclaration({
|
||||||
|
key: `${ctx.node.id}_locals`,
|
||||||
|
meta: {
|
||||||
|
title: getNodeForm(ctx.node)?.getValueIn('title'),
|
||||||
|
icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,
|
||||||
|
},
|
||||||
|
type: ASTFactory.createObject({
|
||||||
|
properties: [
|
||||||
|
ASTFactory.createProperty({
|
||||||
|
key: 'item',
|
||||||
|
initializer: ASTFactory.createEnumerateExpression({
|
||||||
|
enumerateFor: ASTFactory.createKeyPathExpression({
|
||||||
|
keyPath: value.content || [],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
ASTFactory.createProperty({
|
||||||
|
key: 'index',
|
||||||
|
type: ASTFactory.createNumber(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "provide-batch-outputs",
|
||||||
|
"depMaterials": ["flow-value"],
|
||||||
|
"depPackages": []
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
ASTFactory,
|
||||||
|
EffectOptions,
|
||||||
|
FlowNodeRegistry,
|
||||||
|
createEffectFromVariableProvider,
|
||||||
|
getNodeForm,
|
||||||
|
} from '@flowgram.ai/editor';
|
||||||
|
|
||||||
|
import { IFlowRefValue } from '../../typings';
|
||||||
|
|
||||||
|
export const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({
|
||||||
|
private: true,
|
||||||
|
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 || [],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
@ -1 +1,4 @@
|
|||||||
export * from './components';
|
export * from './components';
|
||||||
|
export * from './effects';
|
||||||
|
export * from './utils';
|
||||||
|
export * from './typings';
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "flow-value",
|
||||||
|
"depMaterials": [],
|
||||||
|
"depPackages": []
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
export interface IFlowConstantValue {
|
||||||
|
type: 'constant';
|
||||||
|
content?: string | number | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFlowRefValue {
|
||||||
|
type: 'ref';
|
||||||
|
content?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFlowExpressionValue {
|
||||||
|
type: 'expression';
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFlowTemplateValue {
|
||||||
|
type: 'template';
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IFlowValue =
|
||||||
|
| IFlowConstantValue
|
||||||
|
| IFlowRefValue
|
||||||
|
| IFlowExpressionValue
|
||||||
|
| IFlowTemplateValue;
|
||||||
|
|
||||||
|
export type IFlowConstantRefValue = IFlowConstantValue | IFlowRefValue;
|
||||||
1
packages/materials/form-materials/src/typings/index.ts
Normal file
1
packages/materials/form-materials/src/typings/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './flow-value';
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "format-legacy-ref",
|
||||||
|
"depMaterials": [],
|
||||||
|
"depPackages": []
|
||||||
|
}
|
||||||
@ -0,0 +1,153 @@
|
|||||||
|
import { isObject } from 'lodash';
|
||||||
|
|
||||||
|
interface LegacyFlowRefValueSchema {
|
||||||
|
type: 'ref';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NewFlowRefValueSchema {
|
||||||
|
type: 'ref';
|
||||||
|
content: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In flowgram 0.2.0, for introducing Loop variable functionality,
|
||||||
|
* the FlowRefValueSchema type definition is updated:
|
||||||
|
*
|
||||||
|
* interface LegacyFlowRefValueSchema {
|
||||||
|
* type: 'ref';
|
||||||
|
* content: string;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* interface NewFlowRefValueSchema {
|
||||||
|
* type: 'ref';
|
||||||
|
* content: string[];
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* For making sure backend json will not be changed, we provide format legacy ref utils for updating the formData
|
||||||
|
*
|
||||||
|
* How to use:
|
||||||
|
*
|
||||||
|
* 1. Call formatLegacyRefOnSubmit on the formData before submitting
|
||||||
|
* 2. Call formatLegacyRefOnInit on the formData after submitting
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* import { formatLegacyRefOnSubmit, formatLegacyRefOnInit } from '@flowgram.ai/form-materials';
|
||||||
|
* formMeta: {
|
||||||
|
* formatOnSubmit: (data) => formatLegacyRefOnSubmit(data),
|
||||||
|
* formatOnInit: (data) => formatLegacyRefOnInit(data),
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function formatLegacyRefOnSubmit(value: any): any {
|
||||||
|
if (isObject(value)) {
|
||||||
|
if (isLegacyFlowRefValueSchema(value)) {
|
||||||
|
return formatLegacyRefToNewRef(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).map(([key, value]: [string, any]) => [
|
||||||
|
key,
|
||||||
|
formatLegacyRefOnSubmit(value),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(formatLegacyRefOnSubmit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In flowgram 0.2.0, for introducing Loop variable functionality,
|
||||||
|
* the FlowRefValueSchema type definition is updated:
|
||||||
|
*
|
||||||
|
* interface LegacyFlowRefValueSchema {
|
||||||
|
* type: 'ref';
|
||||||
|
* content: string;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* interface NewFlowRefValueSchema {
|
||||||
|
* type: 'ref';
|
||||||
|
* content: string[];
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* For making sure backend json will not be changed, we provide format legacy ref utils for updating the formData
|
||||||
|
*
|
||||||
|
* How to use:
|
||||||
|
*
|
||||||
|
* 1. Call formatLegacyRefOnSubmit on the formData before submitting
|
||||||
|
* 2. Call formatLegacyRefOnInit on the formData after submitting
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* import { formatLegacyRefOnSubmit, formatLegacyRefOnInit } from '@flowgram.ai/form-materials';
|
||||||
|
*
|
||||||
|
* formMeta: {
|
||||||
|
* formatOnSubmit: (data) => formatLegacyRefOnSubmit(data),
|
||||||
|
* formatOnInit: (data) => formatLegacyRefOnInit(data),
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function formatLegacyRefOnInit(value: any): any {
|
||||||
|
if (isObject(value)) {
|
||||||
|
if (isNewFlowRefValueSchema(value)) {
|
||||||
|
return formatNewRefToLegacyRef(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).map(([key, value]: [string, any]) => [
|
||||||
|
key,
|
||||||
|
formatLegacyRefOnInit(value),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(formatLegacyRefOnInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLegacyFlowRefValueSchema(value: any): value is LegacyFlowRefValueSchema {
|
||||||
|
return (
|
||||||
|
isObject(value) &&
|
||||||
|
Object.keys(value).length === 2 &&
|
||||||
|
(value as any).type === 'ref' &&
|
||||||
|
typeof (value as any).content === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNewFlowRefValueSchema(value: any): value is NewFlowRefValueSchema {
|
||||||
|
return (
|
||||||
|
isObject(value) &&
|
||||||
|
Object.keys(value).length === 2 &&
|
||||||
|
(value as any).type === 'ref' &&
|
||||||
|
Array.isArray((value as any).content)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatLegacyRefToNewRef(value: LegacyFlowRefValueSchema) {
|
||||||
|
const keyPath = value.content.split('.');
|
||||||
|
|
||||||
|
if (keyPath[1] === 'outputs') {
|
||||||
|
return {
|
||||||
|
type: 'ref',
|
||||||
|
content: [`${keyPath[0]}.${keyPath[1]}`, ...(keyPath.length > 2 ? keyPath.slice(2) : [])],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'ref',
|
||||||
|
content: keyPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatNewRefToLegacyRef(value: NewFlowRefValueSchema) {
|
||||||
|
return {
|
||||||
|
type: 'ref',
|
||||||
|
content: value.content.join('.'),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
# Notice
|
||||||
|
|
||||||
|
In `@flowgram.ai/form-materials@0.2.0`, for introducing loop-related materials,
|
||||||
|
|
||||||
|
The FlowRefValueSchema type definition is updated:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface LegacyFlowRefValueSchema {
|
||||||
|
type: 'ref';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NewFlowRefValueSchema {
|
||||||
|
type: 'ref';
|
||||||
|
content: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
For making sure backend json will not be changed in your application, we provide `format-legacy-ref` utils for upgrading
|
||||||
|
|
||||||
|
|
||||||
|
How to use:
|
||||||
|
|
||||||
|
1. Call formatLegacyRefOnSubmit on the formData before submitting
|
||||||
|
2. Call formatLegacyRefOnInit on the formData after submitting
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { formatLegacyRefOnSubmit, formatLegacyRefOnInit } from '@flowgram.ai/form-materials';
|
||||||
|
|
||||||
|
formMeta: {
|
||||||
|
formatOnSubmit: (data) => formatLegacyRefOnSubmit(data),
|
||||||
|
formatOnInit: (data) => formatLegacyRefOnInit(data),
|
||||||
|
}
|
||||||
|
```
|
||||||
1
packages/materials/form-materials/src/utils/index.ts
Normal file
1
packages/materials/form-materials/src/utils/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './format-legacy-refs';
|
||||||
@ -13,7 +13,7 @@ import {
|
|||||||
ObjectType,
|
ObjectType,
|
||||||
StringType,
|
StringType,
|
||||||
} from './type';
|
} from './type';
|
||||||
import { EnumerateExpression, ExpressionList, KeyPathExpression } from './expression';
|
import { EnumerateExpression, KeyPathExpression, WrapArrayExpression } from './expression';
|
||||||
import { Property, VariableDeclaration, VariableDeclarationList } from './declaration';
|
import { Property, VariableDeclaration, VariableDeclarationList } from './declaration';
|
||||||
import { DataNode, MapNode } from './common';
|
import { DataNode, MapNode } from './common';
|
||||||
import { ASTNode, ASTNodeRegistry } from './ast-node';
|
import { ASTNode, ASTNodeRegistry } from './ast-node';
|
||||||
@ -43,7 +43,7 @@ export class ASTRegisters {
|
|||||||
this.registerAST(VariableDeclarationList);
|
this.registerAST(VariableDeclarationList);
|
||||||
this.registerAST(KeyPathExpression);
|
this.registerAST(KeyPathExpression);
|
||||||
this.registerAST(EnumerateExpression);
|
this.registerAST(EnumerateExpression);
|
||||||
this.registerAST(ExpressionList);
|
this.registerAST(WrapArrayExpression);
|
||||||
this.registerAST(MapNode);
|
this.registerAST(MapNode);
|
||||||
this.registerAST(DataNode);
|
this.registerAST(DataNode);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
import { ASTKind, ASTNodeJSON } from '../types';
|
|
||||||
import { ASTNode } from '../ast-node';
|
|
||||||
|
|
||||||
export interface ExpressionListJSON {
|
|
||||||
expressions: ASTNodeJSON[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExpressionList extends ASTNode<ExpressionListJSON> {
|
|
||||||
static kind: string = ASTKind.ExpressionList;
|
|
||||||
|
|
||||||
expressions: ASTNode[];
|
|
||||||
|
|
||||||
fromJSON({ expressions }: ExpressionListJSON): void {
|
|
||||||
this.expressions = expressions.map((_expression, idx) => {
|
|
||||||
const prevExpression = this.expressions[idx];
|
|
||||||
|
|
||||||
if (prevExpression.kind !== _expression.kind) {
|
|
||||||
prevExpression.dispose();
|
|
||||||
this.fireChange();
|
|
||||||
return this.createChildNode(_expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
prevExpression.fromJSON(_expression);
|
|
||||||
return prevExpression;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): ASTNodeJSON {
|
|
||||||
return {
|
|
||||||
kind: ASTKind.ExpressionList,
|
|
||||||
properties: this.expressions.map(_expression => _expression.toJSON()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
export { BaseExpression } from './base-expression';
|
export { BaseExpression } from './base-expression';
|
||||||
export { ExpressionList, type ExpressionListJSON } from './expression-list';
|
|
||||||
export { KeyPathExpression, type KeyPathExpressionJSON } from './keypath-expression';
|
export { KeyPathExpression, type KeyPathExpressionJSON } from './keypath-expression';
|
||||||
export { EnumerateExpression, type EnumerateExpressionJSON } from './enumerate-expression';
|
export { EnumerateExpression, type EnumerateExpressionJSON } from './enumerate-expression';
|
||||||
export { KeyPathExpressionV2 } from './keypath-expression-v2';
|
export { KeyPathExpressionV2 } from './keypath-expression-v2';
|
||||||
|
export { WrapArrayExpression, type WrapArrayExpressionJSON } from './wrap-array-expression';
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
import { postConstructAST } from '../utils/inversify';
|
||||||
|
import { ASTKind, ASTNodeJSON } from '../types';
|
||||||
|
import { BaseType } from '../type';
|
||||||
|
import { BaseExpression } from './base-expression';
|
||||||
|
|
||||||
|
export interface WrapArrayExpressionJSON {
|
||||||
|
wrapFor: ASTNodeJSON; // 需要被遍历的表达式类型
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 遍历表达式,对列表进行遍历,获取遍历后的变量类型
|
||||||
|
*/
|
||||||
|
export class WrapArrayExpression extends BaseExpression<WrapArrayExpressionJSON> {
|
||||||
|
static kind: string = ASTKind.WrapArrayExpression;
|
||||||
|
|
||||||
|
protected _wrapFor: BaseExpression | undefined;
|
||||||
|
|
||||||
|
protected _returnType: BaseType | undefined;
|
||||||
|
|
||||||
|
get wrapFor() {
|
||||||
|
return this._wrapFor;
|
||||||
|
}
|
||||||
|
|
||||||
|
get returnType(): BaseType | undefined {
|
||||||
|
return this._returnType;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshReturnType() {
|
||||||
|
// 被遍历表达式的返回值
|
||||||
|
const childReturnTypeJSON = this.wrapFor?.returnType?.toJSON();
|
||||||
|
this.updateChildNodeByKey('_returnType', {
|
||||||
|
kind: ASTKind.Array,
|
||||||
|
items: childReturnTypeJSON,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getRefFields(): [] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
fromJSON({ wrapFor: expression }: WrapArrayExpressionJSON): void {
|
||||||
|
this.updateChildNodeByKey('_wrapFor', expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): ASTNodeJSON {
|
||||||
|
return {
|
||||||
|
kind: ASTKind.WrapArrayExpression,
|
||||||
|
wrapFor: this.wrapFor?.toJSON(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@postConstructAST()
|
||||||
|
protected init() {
|
||||||
|
this.toDispose.push(
|
||||||
|
this.subscribe(this.refreshReturnType, {
|
||||||
|
selector: (curr) => curr.wrapFor?.returnType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,15 @@
|
|||||||
|
import { get } from 'lodash';
|
||||||
|
|
||||||
import { ASTKind, ASTNodeJSON } from './types';
|
import { ASTKind, ASTNodeJSON } from './types';
|
||||||
import { MapJSON } from './type/map';
|
import { MapJSON } from './type/map';
|
||||||
|
import { VarJSONSchema } from './type/json-schema';
|
||||||
import { ArrayJSON } from './type/array';
|
import { ArrayJSON } from './type/array';
|
||||||
import { CustomTypeJSON, ObjectJSON, UnionJSON } from './type';
|
import { CustomTypeJSON, ObjectJSON, UnionJSON } from './type';
|
||||||
import { EnumerateExpressionJSON, KeyPathExpressionJSON } from './expression';
|
import {
|
||||||
|
EnumerateExpressionJSON,
|
||||||
|
KeyPathExpressionJSON,
|
||||||
|
WrapArrayExpressionJSON,
|
||||||
|
} from './expression';
|
||||||
import { PropertyJSON, VariableDeclarationJSON, VariableDeclarationListJSON } from './declaration';
|
import { PropertyJSON, VariableDeclarationJSON, VariableDeclarationListJSON } from './declaration';
|
||||||
import { ASTNode } from './ast-node';
|
import { ASTNode } from './ast-node';
|
||||||
|
|
||||||
@ -65,6 +72,78 @@ export namespace ASTFactory {
|
|||||||
kind: ASTKind.KeyPathExpression,
|
kind: ASTKind.KeyPathExpression,
|
||||||
...json,
|
...json,
|
||||||
});
|
});
|
||||||
|
export const createWrapArrayExpression = (json: WrapArrayExpressionJSON) => ({
|
||||||
|
kind: ASTKind.WrapArrayExpression,
|
||||||
|
...json,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a JSON schema to an Abstract Syntax Tree (AST) representation.
|
||||||
|
* This function recursively processes the JSON schema and creates corresponding AST nodes.
|
||||||
|
*
|
||||||
|
* For more information on JSON Schema, refer to the official documentation:
|
||||||
|
* https://json-schema.org/
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param jsonSchema - The JSON schema to convert.
|
||||||
|
* @returns An AST node representing the JSON schema, or undefined if the schema type is not recognized.
|
||||||
|
*/
|
||||||
|
export function createTypeASTFromSchema(
|
||||||
|
jsonSchema: VarJSONSchema.ISchema
|
||||||
|
): ASTNodeJSON | undefined {
|
||||||
|
const { type, extra } = jsonSchema || {};
|
||||||
|
const { weak = false } = extra || {};
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'object':
|
||||||
|
if (weak) {
|
||||||
|
return { kind: ASTKind.Object, weak: true };
|
||||||
|
}
|
||||||
|
return ASTFactory.createObject({
|
||||||
|
properties: Object.entries(jsonSchema.properties || {})
|
||||||
|
/**
|
||||||
|
* Sorts the properties of a JSON schema based on the 'extra.index' field.
|
||||||
|
* If the 'extra.index' field is not present, the property will be treated as having an index of 0.
|
||||||
|
*/
|
||||||
|
.sort((a, b) => (get(a?.[1], 'extra.index') || 0) - (get(b?.[1], 'extra.index') || 0))
|
||||||
|
.map(([key, _property]) => ({
|
||||||
|
key,
|
||||||
|
type: createTypeASTFromSchema(_property),
|
||||||
|
meta: { description: _property.description },
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
case 'array':
|
||||||
|
if (weak) {
|
||||||
|
return { kind: ASTKind.Array, weak: true };
|
||||||
|
}
|
||||||
|
return ASTFactory.createArray({
|
||||||
|
items: createTypeASTFromSchema(jsonSchema.items!),
|
||||||
|
});
|
||||||
|
case 'map':
|
||||||
|
if (weak) {
|
||||||
|
return { kind: ASTKind.Map, weak: true };
|
||||||
|
}
|
||||||
|
return ASTFactory.createMap({
|
||||||
|
valueType: createTypeASTFromSchema(jsonSchema.additionalProperties!),
|
||||||
|
});
|
||||||
|
case 'string':
|
||||||
|
return ASTFactory.createString();
|
||||||
|
case 'number':
|
||||||
|
return ASTFactory.createNumber();
|
||||||
|
case 'boolean':
|
||||||
|
return ASTFactory.createBoolean();
|
||||||
|
case 'integer':
|
||||||
|
return ASTFactory.createInteger();
|
||||||
|
|
||||||
|
default:
|
||||||
|
// If the type is not recognized, return CustomType
|
||||||
|
return ASTFactory.createCustomType({ typeName: type });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 AST Class 创建
|
* 通过 AST Class 创建
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { parseTypeJsonOrKind } from '../utils/helpers';
|
import { parseTypeJsonOrKind } from '../utils/helpers';
|
||||||
|
import { VarJSONSchema } from './json-schema';
|
||||||
import { ASTKind, ASTNodeJSON, ASTNodeJSONOrKind } from '../types';
|
import { ASTKind, ASTNodeJSON, ASTNodeJSONOrKind } from '../types';
|
||||||
import { ASTNodeFlags } from '../flags';
|
import { ASTNodeFlags } from '../flags';
|
||||||
import { BaseVariableField } from '../declaration';
|
import { BaseVariableField } from '../declaration';
|
||||||
import { ASTNode } from '../ast-node';
|
import { ASTNode } from '../ast-node';
|
||||||
import { UnionJSON } from './union';
|
import { UnionJSON } from './union';
|
||||||
|
import { ASTFactory } from '../factory';
|
||||||
|
|
||||||
export abstract class BaseType<JSON extends ASTNodeJSON = any, InjectOpts = any> extends ASTNode<
|
export abstract class BaseType<JSON extends ASTNodeJSON = any, InjectOpts = any> extends ASTNode<
|
||||||
JSON,
|
JSON,
|
||||||
@ -12,7 +14,7 @@ export abstract class BaseType<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
|||||||
public flags: number = ASTNodeFlags.BasicType;
|
public flags: number = ASTNodeFlags.BasicType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 类型是否一致,节点有额外信息判断,请参考 extraTypeInfoEqual
|
* 类型是否一致
|
||||||
* @param targetTypeJSON
|
* @param targetTypeJSON
|
||||||
*/
|
*/
|
||||||
public isTypeEqual(targetTypeJSONOrKind?: ASTNodeJSONOrKind): boolean {
|
public isTypeEqual(targetTypeJSONOrKind?: ASTNodeJSONOrKind): boolean {
|
||||||
@ -20,8 +22,8 @@ export abstract class BaseType<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
|||||||
|
|
||||||
// 如果是 Union 类型,有一个子类型保持相等即可
|
// 如果是 Union 类型,有一个子类型保持相等即可
|
||||||
if (targetTypeJSON?.kind === ASTKind.Union) {
|
if (targetTypeJSON?.kind === ASTKind.Union) {
|
||||||
return ((targetTypeJSON as UnionJSON)?.types || [])?.some(_subType =>
|
return ((targetTypeJSON as UnionJSON)?.types || [])?.some((_subType) =>
|
||||||
this.isTypeEqual(_subType),
|
this.isTypeEqual(_subType)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,9 +38,40 @@ export abstract class BaseType<JSON extends ASTNodeJSON = any, InjectOpts = any>
|
|||||||
throw new Error(`Get By Key Path is not implemented for Type: ${this.kind}`);
|
throw new Error(`Get By Key Path is not implemented for Type: ${this.kind}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get AST JSON for current base type
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
toJSON(): ASTNodeJSON {
|
toJSON(): ASTNodeJSON {
|
||||||
return {
|
return {
|
||||||
kind: this.kind,
|
kind: this.kind,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Standard JSON Schema for current base type
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
toJSONSchema(): VarJSONSchema.ISchema {
|
||||||
|
return {
|
||||||
|
type: this.kind.toLowerCase() as VarJSONSchema.BasicType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the type is equal with json schema
|
||||||
|
*/
|
||||||
|
isEqualWithJSONSchema(schema: VarJSONSchema.ISchema | VarJSONSchema.ISchema[]): boolean {
|
||||||
|
if (Array.isArray(schema)) {
|
||||||
|
return this.isTypeEqual(
|
||||||
|
ASTFactory.createUnion({
|
||||||
|
types: schema
|
||||||
|
.map((_schema) => ASTFactory.createTypeASTFromSchema(_schema)!)
|
||||||
|
.filter(Boolean),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.isTypeEqual(ASTFactory.createTypeASTFromSchema(schema));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { VarJSONSchema } from './json-schema';
|
||||||
import { ASTKind } from '../types';
|
import { ASTKind } from '../types';
|
||||||
import { BaseType } from './base-type';
|
import { BaseType } from './base-type';
|
||||||
|
|
||||||
@ -7,4 +8,10 @@ export class BooleanType extends BaseType {
|
|||||||
fromJSON(): void {
|
fromJSON(): void {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONSchema(): VarJSONSchema.ISchema {
|
||||||
|
return {
|
||||||
|
type: 'boolean',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { parseTypeJsonOrKind } from '../utils/helpers';
|
import { parseTypeJsonOrKind } from '../utils/helpers';
|
||||||
|
import { VarJSONSchema } from './json-schema';
|
||||||
import { ASTKind, ASTNodeJSONOrKind } from '../types';
|
import { ASTKind, ASTNodeJSONOrKind } from '../types';
|
||||||
import { type UnionJSON } from './union';
|
import { type UnionJSON } from './union';
|
||||||
import { BaseType } from './base-type';
|
import { BaseType } from './base-type';
|
||||||
@ -35,4 +36,10 @@ export class CustomType extends BaseType<CustomTypeJSON> {
|
|||||||
|
|
||||||
return targetTypeJSON?.kind === this.kind && targetTypeJSON?.typeName === this.typeName;
|
return targetTypeJSON?.kind === this.kind && targetTypeJSON?.typeName === this.typeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONSchema(): VarJSONSchema.ISchema {
|
||||||
|
return {
|
||||||
|
type: this._typeName,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,3 +12,4 @@ export {
|
|||||||
export { BaseType } from './base-type';
|
export { BaseType } from './base-type';
|
||||||
export { type UnionJSON } from './union';
|
export { type UnionJSON } from './union';
|
||||||
export { CustomType, type CustomTypeJSON } from './custom-type';
|
export { CustomType, type CustomTypeJSON } from './custom-type';
|
||||||
|
export { VarJSONSchema } from './json-schema';
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { VarJSONSchema } from './json-schema';
|
||||||
import { ASTKind } from '../types';
|
import { ASTKind } from '../types';
|
||||||
import { ASTNodeFlags } from '../flags';
|
import { ASTNodeFlags } from '../flags';
|
||||||
import { BaseType } from './base-type';
|
import { BaseType } from './base-type';
|
||||||
@ -10,4 +11,10 @@ export class IntegerType extends BaseType {
|
|||||||
fromJSON(): void {
|
fromJSON(): void {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONSchema(): VarJSONSchema.ISchema {
|
||||||
|
return {
|
||||||
|
type: 'integer',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
export namespace VarJSONSchema {
|
||||||
|
export type BasicType = 'boolean' | 'string' | 'integer' | 'number' | 'object' | 'array' | 'map';
|
||||||
|
|
||||||
|
export interface ISchema<T = string> {
|
||||||
|
type?: T;
|
||||||
|
default?: any;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
enum?: (string | number)[];
|
||||||
|
properties?: Record<string, ISchema<T>>;
|
||||||
|
additionalProperties?: ISchema<T>;
|
||||||
|
items?: ISchema<T>;
|
||||||
|
required?: string[];
|
||||||
|
$ref?: string;
|
||||||
|
extra?: {
|
||||||
|
index?: number;
|
||||||
|
// Used in BaseType.isEqualWithJSONSchema, the type comparison will be weak
|
||||||
|
weak?: boolean;
|
||||||
|
// Set the render component
|
||||||
|
formComponent?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IBasicSchema = ISchema<BasicType>;
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { parseTypeJsonOrKind } from '../utils/helpers';
|
import { parseTypeJsonOrKind } from '../utils/helpers';
|
||||||
|
import { VarJSONSchema } from './json-schema';
|
||||||
import { ASTKind, ASTNodeJSON, ASTNodeJSONOrKind } from '../types';
|
import { ASTKind, ASTNodeJSON, ASTNodeJSONOrKind } from '../types';
|
||||||
import { BaseType } from './base-type';
|
import { BaseType } from './base-type';
|
||||||
|
|
||||||
@ -74,4 +75,11 @@ export class MapType extends BaseType<MapJSON> {
|
|||||||
valueType: this.valueType?.toJSON(),
|
valueType: this.valueType?.toJSON(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONSchema(): VarJSONSchema.ISchema {
|
||||||
|
return {
|
||||||
|
type: 'map',
|
||||||
|
additionalProperties: this.valueType?.toJSONSchema(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { VarJSONSchema } from './json-schema';
|
||||||
import { ASTKind } from '../types';
|
import { ASTKind } from '../types';
|
||||||
import { BaseType } from './base-type';
|
import { BaseType } from './base-type';
|
||||||
|
|
||||||
@ -7,4 +8,10 @@ export class NumberType extends BaseType {
|
|||||||
fromJSON(): void {
|
fromJSON(): void {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONSchema(): VarJSONSchema.ISchema {
|
||||||
|
return {
|
||||||
|
type: 'number',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { xor } from 'lodash';
|
import { xor } from 'lodash';
|
||||||
|
|
||||||
import { parseTypeJsonOrKind } from '../utils/helpers';
|
import { parseTypeJsonOrKind } from '../utils/helpers';
|
||||||
|
import { VarJSONSchema } from './json-schema';
|
||||||
import { ASTNodeJSON, ASTKind, ASTNodeJSONOrKind, type GlobalEventActionType } from '../types';
|
import { ASTNodeJSON, ASTKind, ASTNodeJSONOrKind, type GlobalEventActionType } from '../types';
|
||||||
import { ASTNodeFlags } from '../flags';
|
import { ASTNodeFlags } from '../flags';
|
||||||
import { Property, type PropertyJSON } from '../declaration/property';
|
import { Property, type PropertyJSON } from '../declaration/property';
|
||||||
@ -60,7 +61,7 @@ export class ObjectType extends BaseType<ObjectJSON> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 删除没有出现过的 property
|
// 删除没有出现过的 property
|
||||||
removedKeys.forEach(key => {
|
removedKeys.forEach((key) => {
|
||||||
const property = this.propertyTable.get(key);
|
const property = this.propertyTable.get(key);
|
||||||
property?.dispose();
|
property?.dispose();
|
||||||
this.propertyTable.delete(key);
|
this.propertyTable.delete(key);
|
||||||
@ -79,7 +80,7 @@ export class ObjectType extends BaseType<ObjectJSON> {
|
|||||||
toJSON(): ASTNodeJSON {
|
toJSON(): ASTNodeJSON {
|
||||||
return {
|
return {
|
||||||
kind: ASTKind.Object,
|
kind: ASTKind.Object,
|
||||||
properties: this.properties.map(_property => _property.toJSON()),
|
properties: this.properties.map((_property) => _property.toJSON()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,13 +132,13 @@ export class ObjectType extends BaseType<ObjectJSON> {
|
|||||||
const targetProperties = (targetTypeJSON as ObjectJSON).properties || [];
|
const targetProperties = (targetTypeJSON as ObjectJSON).properties || [];
|
||||||
|
|
||||||
const sourcePropertyKeys = Array.from(this.propertyTable.keys());
|
const sourcePropertyKeys = Array.from(this.propertyTable.keys());
|
||||||
const targetPropertyKeys = targetProperties.map(_target => _target.key);
|
const targetPropertyKeys = targetProperties.map((_target) => _target.key);
|
||||||
|
|
||||||
const isKeyStrongEqual = !xor(sourcePropertyKeys, targetPropertyKeys).length;
|
const isKeyStrongEqual = !xor(sourcePropertyKeys, targetPropertyKeys).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isKeyStrongEqual &&
|
isKeyStrongEqual &&
|
||||||
targetProperties.every(targetProperty => {
|
targetProperties.every((targetProperty) => {
|
||||||
const sourceProperty = this.propertyTable.get(targetProperty.key);
|
const sourceProperty = this.propertyTable.get(targetProperty.key);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -148,4 +149,14 @@ export class ObjectType extends BaseType<ObjectJSON> {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONSchema(): VarJSONSchema.ISchema {
|
||||||
|
return {
|
||||||
|
type: 'object',
|
||||||
|
properties: this.properties.reduce((acc, _property) => {
|
||||||
|
acc[_property.key] = _property.type?.toJSONSchema();
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, VarJSONSchema.ISchema>),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { VarJSONSchema } from './json-schema';
|
||||||
import { ASTKind } from '../types';
|
import { ASTKind } from '../types';
|
||||||
import { ASTNodeFlags } from '../flags';
|
import { ASTNodeFlags } from '../flags';
|
||||||
import { BaseType } from './base-type';
|
import { BaseType } from './base-type';
|
||||||
@ -10,4 +11,10 @@ export class StringType extends BaseType {
|
|||||||
fromJSON(): void {
|
fromJSON(): void {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONSchema(): VarJSONSchema.ISchema {
|
||||||
|
return {
|
||||||
|
type: 'string',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export enum ASTKind {
|
|||||||
*/
|
*/
|
||||||
KeyPathExpression = 'KeyPathExpression', // 通过路径系统访问变量上的字段
|
KeyPathExpression = 'KeyPathExpression', // 通过路径系统访问变量上的字段
|
||||||
EnumerateExpression = 'EnumerateExpression', // 对指定的数据进行遍历
|
EnumerateExpression = 'EnumerateExpression', // 对指定的数据进行遍历
|
||||||
ExpressionList = 'ExpressionList', // 表达式列表
|
WrapArrayExpression = 'WrapArrayExpression', // Wrap with Array Type
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用 AST 节点
|
* 通用 AST 节点
|
||||||
|
|||||||
@ -0,0 +1,61 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { DisposableCollection } from '@flowgram.ai/utils';
|
||||||
|
import { useService } from '@flowgram.ai/core';
|
||||||
|
|
||||||
|
import { useCurrentScope } from '../context';
|
||||||
|
import { VariableEngine } from '../../variable-engine';
|
||||||
|
import { Scope } from '../../scope';
|
||||||
|
import {
|
||||||
|
DisposeASTAction,
|
||||||
|
GlobalEventActionType,
|
||||||
|
NewASTAction,
|
||||||
|
UpdateASTAction,
|
||||||
|
} from '../../ast/types';
|
||||||
|
|
||||||
|
export function useScopeOutputVariables(scopeFromParam?: Scope | Scope[]) {
|
||||||
|
const currentScope = useCurrentScope();
|
||||||
|
const variableEngine = useService(VariableEngine);
|
||||||
|
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
|
|
||||||
|
const scopes = useMemo(
|
||||||
|
() => {
|
||||||
|
if (!scopeFromParam) {
|
||||||
|
return [currentScope];
|
||||||
|
}
|
||||||
|
if (Array.isArray(scopeFromParam)) {
|
||||||
|
return scopeFromParam;
|
||||||
|
}
|
||||||
|
return [scopeFromParam];
|
||||||
|
},
|
||||||
|
Array.isArray(scopeFromParam) ? scopeFromParam : [scopeFromParam]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRefreshKey((_key) => _key + 1);
|
||||||
|
|
||||||
|
const refreshWhenInScopes = useCallback((action: GlobalEventActionType) => {
|
||||||
|
if (scopes.includes(action.ast?.scope!)) {
|
||||||
|
setRefreshKey((_key) => _key + 1);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const disposable = new DisposableCollection();
|
||||||
|
|
||||||
|
disposable.pushAll([
|
||||||
|
variableEngine.onGlobalEvent<DisposeASTAction>('DisposeAST', refreshWhenInScopes),
|
||||||
|
variableEngine.onGlobalEvent<NewASTAction>('NewAST', refreshWhenInScopes),
|
||||||
|
variableEngine.onGlobalEvent<UpdateASTAction>('UpdateAST', refreshWhenInScopes),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return () => disposable.dispose();
|
||||||
|
}, [scopes]);
|
||||||
|
|
||||||
|
const variables = useMemo(
|
||||||
|
() => scopes.map((scope) => scope.output.variables).flat(),
|
||||||
|
[scopes, refreshKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user