diff --git a/apps/demo-fixed-layout/src/form-components/form-inputs/index.tsx b/apps/demo-fixed-layout/src/form-components/form-inputs/index.tsx index 9a8f89ff..70953c0e 100644 --- a/apps/demo-fixed-layout/src/form-components/form-inputs/index.tsx +++ b/apps/demo-fixed-layout/src/form-components/form-inputs/index.tsx @@ -1,6 +1,6 @@ +import { DynamicValueInput } from '@flowgram.ai/form-materials'; import { Field } from '@flowgram.ai/fixed-layout-editor'; -import { FxExpression } from '../fx-expression'; import { FormItem } from '../form-item'; import { Feedback } from '../feedback'; import { JsonSchema } from '../../typings'; @@ -26,11 +26,12 @@ export function FormInputs() { type={property.type as string} required={required.includes(key)} > - 0} + schema={property} /> diff --git a/apps/demo-fixed-layout/src/form-components/fx-expression/index.tsx b/apps/demo-fixed-layout/src/form-components/fx-expression/index.tsx deleted file mode 100644 index 0b961c28..00000000 --- a/apps/demo-fixed-layout/src/form-components/fx-expression/index.tsx +++ /dev/null @@ -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) { - return ( - - - - ); -} - -function InputWrap({ - value, - onChange, - readonly, - hasError, - style, -}: { - value: string; - onChange: (v: string) => void; - readonly?: boolean; - hasError?: boolean; - style?: React.CSSProperties; -}) { - if (readonly) { - return ; - } - return ( - - ); -} - -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 ; - 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 ( -
- {isExpression ? ( - onChange({ type: 'expression', content: v })} - readonly={readonly} - /> - ) : ( - - )} - {!readonly && - (icon ||
- ); -} diff --git a/apps/demo-fixed-layout/src/form-components/properties-edit/property-edit.tsx b/apps/demo-fixed-layout/src/form-components/properties-edit/property-edit.tsx index 91d7713f..654c562a 100644 --- a/apps/demo-fixed-layout/src/form-components/properties-edit/property-edit.tsx +++ b/apps/demo-fixed-layout/src/form-components/properties-edit/property-edit.tsx @@ -1,10 +1,9 @@ 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 { IconCrossCircleStroked } from '@douyinfe/semi-icons'; -import { TypeSelector } from '../type-selector'; import { JsonSchema } from '../../typings'; import { LeftColumn, Row } from './styles'; @@ -24,6 +23,11 @@ export const PropertyEdit: React.FC = (props) => { value[key] = val; props.onChange(value, props.propertyKey); }; + + const partialUpdateProperty = (val?: Partial) => { + props.onChange({ ...value, ...val }, props.propertyKey); + }; + useLayoutEffect(() => { updateKey(props.propertyKey); }, [props.propertyKey]); @@ -31,14 +35,15 @@ export const PropertyEdit: React.FC = (props) => { updateProperty('type', val)} + style={{ position: 'absolute', top: 2, left: 4, zIndex: 1, padding: '0 5px', height: 20 }} + onChange={(val) => partialUpdateProperty(val)} /> updateKey(v.trim())} onBlur={() => { if (inputKey !== '') { @@ -50,22 +55,22 @@ export const PropertyEdit: React.FC = (props) => { style={{ paddingLeft: 26 }} /> - {props.useFx ? ( - updateProperty('default', val)} - style={{ flexGrow: 1, height: 32 }} - /> - ) : ( - updateProperty('default', val)} + schema={value} + style={{ flexGrow: 1 }} /> - )} + } {props.onDelete && !disabled && ( - )} diff --git a/packages/materials/form-materials/src/components/type-selector/types.ts b/packages/materials/form-materials/src/components/type-selector/types.ts index 8a382efd..b6e890db 100644 --- a/packages/materials/form-materials/src/components/type-selector/types.ts +++ b/packages/materials/form-materials/src/components/type-selector/types.ts @@ -1,19 +1,5 @@ -export type BasicType = 'boolean' | 'string' | 'integer' | 'number' | 'object' | 'array'; +import { VarJSONSchema } from '@flowgram.ai/editor'; -export interface JsonSchema { - type?: T; - default?: any; - title?: string; - description?: string; - enum?: (string | number)[]; - properties?: Record; - additionalProperties?: JsonSchema; - items?: JsonSchema; - required?: string[]; - $ref?: string; - extra?: { - order?: number; - literal?: boolean; // is literal type - formComponent?: string; // Set the render component - }; -} +export type BasicType = VarJSONSchema.BasicType; + +export type JsonSchema = VarJSONSchema.ISchema; diff --git a/packages/materials/form-materials/src/components/variable-selector/config.json b/packages/materials/form-materials/src/components/variable-selector/config.json index 5f208919..5146228d 100644 --- a/packages/materials/form-materials/src/components/variable-selector/config.json +++ b/packages/materials/form-materials/src/components/variable-selector/config.json @@ -1,5 +1,5 @@ { "name": "variable-selector", "depMaterials": ["type-selector"], - "depPackages": ["@douyinfe/semi-ui"] + "depPackages": ["@douyinfe/semi-ui", "styled-components"] } diff --git a/packages/materials/form-materials/src/components/variable-selector/index.tsx b/packages/materials/form-materials/src/components/variable-selector/index.tsx index d1fa12dc..a83dfd6a 100644 --- a/packages/materials/form-materials/src/components/variable-selector/index.tsx +++ b/packages/materials/form-materials/src/components/variable-selector/index.tsx @@ -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 { UIRootTitle, UITag, UITreeSelect } from './styles'; -export interface PropTypes { - value?: string; - config: { +interface PropTypes { + value?: string[]; + config?: { placeholder?: string; + notFoundContent?: string; }; - onChange: (value?: string) => void; + onChange: (value?: string[]) => void; + includeSchema?: VarJSONSchema.ISchema | VarJSONSchema.ISchema[]; + excludeSchema?: VarJSONSchema.ISchema | VarJSONSchema.ISchema[]; readonly?: boolean; hasError?: boolean; style?: React.CSSProperties; + triggerRender?: (props: TriggerRenderProps) => React.ReactNode; } +export type VariableSelectorProps = PropTypes; + export const VariableSelector = ({ value, config, onChange, style, readonly = false, + includeSchema, + excludeSchema, hasError, + triggerRender, }: 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 ; + } + + return icon; + }; return ( <> - { - onChange(option as string); + onChange={(_, _config) => { + onChange((_config as TreeNodeData).keyPath as string[]); }} - showClear + renderSelectedItem={(_option: TreeNodeData) => { + if (!_option?.keyPath) { + return ( + } + color="amber" + closable={!readonly} + onClose={() => onChange(undefined)} + > + {config?.notFoundContent ?? 'Undefined'} + + ); + } + + return ( + onChange(undefined)} + > + + {_option.rootMeta?.title ? `${_option.rootMeta?.title} -` : null} + + {_option.label} + + ); + }} + showClear={false} + arrowIcon={value ? null : } + triggerRender={triggerRender} placeholder={config?.placeholder ?? 'Select Variable...'} /> diff --git a/packages/materials/form-materials/src/components/variable-selector/styles.tsx b/packages/materials/form-materials/src/components/variable-selector/styles.tsx new file mode 100644 index 00000000..72634e5b --- /dev/null +++ b/packages/materials/form-materials/src/components/variable-selector/styles.tsx @@ -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; + } +`; diff --git a/packages/materials/form-materials/src/components/variable-selector/use-variable-tree.tsx b/packages/materials/form-materials/src/components/variable-selector/use-variable-tree.tsx index 4825e8e1..459f1785 100644 --- a/packages/materials/form-materials/src/components/variable-selector/use-variable-tree.tsx +++ b/packages/materials/form-materials/src/components/variable-selector/use-variable-tree.tsx @@ -1,6 +1,6 @@ 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 { 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 }>; -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 getVariableTypeIcon = useCallback((variable: VariableField) => { - if (variable.meta.icon) { + if (variable.meta?.icon) { if (typeof variable.meta.icon === 'string') { return ; } @@ -44,6 +49,10 @@ export function useVariableTree(): TreeNodeData[] { ): TreeNodeData | null => { let type = variable?.type; + if (!type) { + return null; + } + let children: TreeNodeData[] | undefined; 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 { - key: currPath, - label: variable.meta.title || variable.key, - value: currPath, + key: key, + label: variable.meta?.title || variable.key, + value: key, + keyPath, icon: getVariableTypeIcon(variable), children, + disabled: !isSchemaMatch, + rootMeta: parentFields[0]?.meta, }; }; diff --git a/packages/materials/form-materials/src/effects/index.ts b/packages/materials/form-materials/src/effects/index.ts new file mode 100644 index 00000000..d225fe8a --- /dev/null +++ b/packages/materials/form-materials/src/effects/index.ts @@ -0,0 +1,2 @@ +export * from './provide-batch-input'; +export * from './provide-batch-outputs'; diff --git a/packages/materials/form-materials/src/effects/provide-batch-input/config.json b/packages/materials/form-materials/src/effects/provide-batch-input/config.json new file mode 100644 index 00000000..d1f2f479 --- /dev/null +++ b/packages/materials/form-materials/src/effects/provide-batch-input/config.json @@ -0,0 +1,5 @@ +{ + "name": "provide-batch-input", + "depMaterials": ["flow-value"], + "depPackages": [] +} diff --git a/packages/materials/form-materials/src/effects/provide-batch-input/index.ts b/packages/materials/form-materials/src/effects/provide-batch-input/index.ts new file mode 100644 index 00000000..a8dff42d --- /dev/null +++ b/packages/materials/form-materials/src/effects/provide-batch-input/index.ts @@ -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().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(), + }), + ], + }), + }), + ], +}); diff --git a/packages/materials/form-materials/src/effects/provide-batch-outputs/config.json b/packages/materials/form-materials/src/effects/provide-batch-outputs/config.json new file mode 100644 index 00000000..6d89dc4a --- /dev/null +++ b/packages/materials/form-materials/src/effects/provide-batch-outputs/config.json @@ -0,0 +1,5 @@ +{ + "name": "provide-batch-outputs", + "depMaterials": ["flow-value"], + "depPackages": [] +} diff --git a/packages/materials/form-materials/src/effects/provide-batch-outputs/index.ts b/packages/materials/form-materials/src/effects/provide-batch-outputs/index.ts new file mode 100644 index 00000000..c392cd4d --- /dev/null +++ b/packages/materials/form-materials/src/effects/provide-batch-outputs/index.ts @@ -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, ctx) => [ + ASTFactory.createVariableDeclaration({ + key: `${ctx.node.id}`, + meta: { + title: getNodeForm(ctx.node)?.getValueIn('title'), + icon: ctx.node.getNodeRegistry().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 || [], + }), + }), + }) + ), + }), + }), + ], +}); diff --git a/packages/materials/form-materials/src/index.ts b/packages/materials/form-materials/src/index.ts index 07635cbb..ef5b7a92 100644 --- a/packages/materials/form-materials/src/index.ts +++ b/packages/materials/form-materials/src/index.ts @@ -1 +1,4 @@ export * from './components'; +export * from './effects'; +export * from './utils'; +export * from './typings'; diff --git a/packages/materials/form-materials/src/typings/flow-value/config.json b/packages/materials/form-materials/src/typings/flow-value/config.json new file mode 100644 index 00000000..c7faad8d --- /dev/null +++ b/packages/materials/form-materials/src/typings/flow-value/config.json @@ -0,0 +1,5 @@ +{ + "name": "flow-value", + "depMaterials": [], + "depPackages": [] +} diff --git a/packages/materials/form-materials/src/typings/flow-value/index.ts b/packages/materials/form-materials/src/typings/flow-value/index.ts new file mode 100644 index 00000000..6627f3fe --- /dev/null +++ b/packages/materials/form-materials/src/typings/flow-value/index.ts @@ -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; diff --git a/packages/materials/form-materials/src/typings/index.ts b/packages/materials/form-materials/src/typings/index.ts new file mode 100644 index 00000000..acdb9548 --- /dev/null +++ b/packages/materials/form-materials/src/typings/index.ts @@ -0,0 +1 @@ +export * from './flow-value'; diff --git a/packages/materials/form-materials/src/utils/format-legacy-refs/config.json b/packages/materials/form-materials/src/utils/format-legacy-refs/config.json new file mode 100644 index 00000000..aaa9ec6a --- /dev/null +++ b/packages/materials/form-materials/src/utils/format-legacy-refs/config.json @@ -0,0 +1,5 @@ +{ + "name": "format-legacy-ref", + "depMaterials": [], + "depPackages": [] +} diff --git a/packages/materials/form-materials/src/utils/format-legacy-refs/index.ts b/packages/materials/form-materials/src/utils/format-legacy-refs/index.ts new file mode 100644 index 00000000..0bacdc7e --- /dev/null +++ b/packages/materials/form-materials/src/utils/format-legacy-refs/index.ts @@ -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('.'), + }; +} diff --git a/packages/materials/form-materials/src/utils/format-legacy-refs/readme.md b/packages/materials/form-materials/src/utils/format-legacy-refs/readme.md new file mode 100644 index 00000000..f80ea06e --- /dev/null +++ b/packages/materials/form-materials/src/utils/format-legacy-refs/readme.md @@ -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), +} +``` diff --git a/packages/materials/form-materials/src/utils/index.ts b/packages/materials/form-materials/src/utils/index.ts new file mode 100644 index 00000000..1ff7a260 --- /dev/null +++ b/packages/materials/form-materials/src/utils/index.ts @@ -0,0 +1 @@ +export * from './format-legacy-refs'; diff --git a/packages/variable-engine/variable-core/src/ast/ast-registers.ts b/packages/variable-engine/variable-core/src/ast/ast-registers.ts index bf71a478..2b602179 100644 --- a/packages/variable-engine/variable-core/src/ast/ast-registers.ts +++ b/packages/variable-engine/variable-core/src/ast/ast-registers.ts @@ -13,7 +13,7 @@ import { ObjectType, StringType, } from './type'; -import { EnumerateExpression, ExpressionList, KeyPathExpression } from './expression'; +import { EnumerateExpression, KeyPathExpression, WrapArrayExpression } from './expression'; import { Property, VariableDeclaration, VariableDeclarationList } from './declaration'; import { DataNode, MapNode } from './common'; import { ASTNode, ASTNodeRegistry } from './ast-node'; @@ -43,7 +43,7 @@ export class ASTRegisters { this.registerAST(VariableDeclarationList); this.registerAST(KeyPathExpression); this.registerAST(EnumerateExpression); - this.registerAST(ExpressionList); + this.registerAST(WrapArrayExpression); this.registerAST(MapNode); this.registerAST(DataNode); } diff --git a/packages/variable-engine/variable-core/src/ast/expression/expression-list.ts b/packages/variable-engine/variable-core/src/ast/expression/expression-list.ts deleted file mode 100644 index 3bd8c90f..00000000 --- a/packages/variable-engine/variable-core/src/ast/expression/expression-list.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ASTKind, ASTNodeJSON } from '../types'; -import { ASTNode } from '../ast-node'; - -export interface ExpressionListJSON { - expressions: ASTNodeJSON[]; -} - -export class ExpressionList extends ASTNode { - 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()), - }; - } -} diff --git a/packages/variable-engine/variable-core/src/ast/expression/index.ts b/packages/variable-engine/variable-core/src/ast/expression/index.ts index 833c372a..8f1ef075 100644 --- a/packages/variable-engine/variable-core/src/ast/expression/index.ts +++ b/packages/variable-engine/variable-core/src/ast/expression/index.ts @@ -1,5 +1,5 @@ export { BaseExpression } from './base-expression'; -export { ExpressionList, type ExpressionListJSON } from './expression-list'; export { KeyPathExpression, type KeyPathExpressionJSON } from './keypath-expression'; export { EnumerateExpression, type EnumerateExpressionJSON } from './enumerate-expression'; export { KeyPathExpressionV2 } from './keypath-expression-v2'; +export { WrapArrayExpression, type WrapArrayExpressionJSON } from './wrap-array-expression'; diff --git a/packages/variable-engine/variable-core/src/ast/expression/wrap-array-expression.ts b/packages/variable-engine/variable-core/src/ast/expression/wrap-array-expression.ts new file mode 100644 index 00000000..65184051 --- /dev/null +++ b/packages/variable-engine/variable-core/src/ast/expression/wrap-array-expression.ts @@ -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 { + 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, + }) + ); + } +} diff --git a/packages/variable-engine/variable-core/src/ast/factory.ts b/packages/variable-engine/variable-core/src/ast/factory.ts index d6f0bebb..a62dec15 100644 --- a/packages/variable-engine/variable-core/src/ast/factory.ts +++ b/packages/variable-engine/variable-core/src/ast/factory.ts @@ -1,8 +1,15 @@ +import { get } from 'lodash'; + import { ASTKind, ASTNodeJSON } from './types'; import { MapJSON } from './type/map'; +import { VarJSONSchema } from './type/json-schema'; import { ArrayJSON } from './type/array'; import { CustomTypeJSON, ObjectJSON, UnionJSON } from './type'; -import { EnumerateExpressionJSON, KeyPathExpressionJSON } from './expression'; +import { + EnumerateExpressionJSON, + KeyPathExpressionJSON, + WrapArrayExpressionJSON, +} from './expression'; import { PropertyJSON, VariableDeclarationJSON, VariableDeclarationListJSON } from './declaration'; import { ASTNode } from './ast-node'; @@ -65,6 +72,78 @@ export namespace ASTFactory { kind: ASTKind.KeyPathExpression, ...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 创建 diff --git a/packages/variable-engine/variable-core/src/ast/type/base-type.ts b/packages/variable-engine/variable-core/src/ast/type/base-type.ts index 3d19feb7..6f977b61 100644 --- a/packages/variable-engine/variable-core/src/ast/type/base-type.ts +++ b/packages/variable-engine/variable-core/src/ast/type/base-type.ts @@ -1,9 +1,11 @@ import { parseTypeJsonOrKind } from '../utils/helpers'; +import { VarJSONSchema } from './json-schema'; import { ASTKind, ASTNodeJSON, ASTNodeJSONOrKind } from '../types'; import { ASTNodeFlags } from '../flags'; import { BaseVariableField } from '../declaration'; import { ASTNode } from '../ast-node'; import { UnionJSON } from './union'; +import { ASTFactory } from '../factory'; export abstract class BaseType extends ASTNode< JSON, @@ -12,7 +14,7 @@ export abstract class BaseType public flags: number = ASTNodeFlags.BasicType; /** - * 类型是否一致,节点有额外信息判断,请参考 extraTypeInfoEqual + * 类型是否一致 * @param targetTypeJSON */ public isTypeEqual(targetTypeJSONOrKind?: ASTNodeJSONOrKind): boolean { @@ -20,8 +22,8 @@ export abstract class BaseType // 如果是 Union 类型,有一个子类型保持相等即可 if (targetTypeJSON?.kind === ASTKind.Union) { - return ((targetTypeJSON as UnionJSON)?.types || [])?.some(_subType => - this.isTypeEqual(_subType), + return ((targetTypeJSON as UnionJSON)?.types || [])?.some((_subType) => + this.isTypeEqual(_subType) ); } @@ -36,9 +38,40 @@ export abstract class BaseType throw new Error(`Get By Key Path is not implemented for Type: ${this.kind}`); } + /** + * Get AST JSON for current base type + * @returns + */ toJSON(): ASTNodeJSON { return { 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)); + } } diff --git a/packages/variable-engine/variable-core/src/ast/type/boolean.ts b/packages/variable-engine/variable-core/src/ast/type/boolean.ts index f5387c40..918b1e21 100644 --- a/packages/variable-engine/variable-core/src/ast/type/boolean.ts +++ b/packages/variable-engine/variable-core/src/ast/type/boolean.ts @@ -1,3 +1,4 @@ +import { VarJSONSchema } from './json-schema'; import { ASTKind } from '../types'; import { BaseType } from './base-type'; @@ -7,4 +8,10 @@ export class BooleanType extends BaseType { fromJSON(): void { // noop } + + toJSONSchema(): VarJSONSchema.ISchema { + return { + type: 'boolean', + }; + } } diff --git a/packages/variable-engine/variable-core/src/ast/type/custom-type.ts b/packages/variable-engine/variable-core/src/ast/type/custom-type.ts index fc175272..423c0a67 100644 --- a/packages/variable-engine/variable-core/src/ast/type/custom-type.ts +++ b/packages/variable-engine/variable-core/src/ast/type/custom-type.ts @@ -1,4 +1,5 @@ import { parseTypeJsonOrKind } from '../utils/helpers'; +import { VarJSONSchema } from './json-schema'; import { ASTKind, ASTNodeJSONOrKind } from '../types'; import { type UnionJSON } from './union'; import { BaseType } from './base-type'; @@ -35,4 +36,10 @@ export class CustomType extends BaseType { return targetTypeJSON?.kind === this.kind && targetTypeJSON?.typeName === this.typeName; } + + toJSONSchema(): VarJSONSchema.ISchema { + return { + type: this._typeName, + }; + } } diff --git a/packages/variable-engine/variable-core/src/ast/type/index.ts b/packages/variable-engine/variable-core/src/ast/type/index.ts index 72fcfe84..f3302b94 100644 --- a/packages/variable-engine/variable-core/src/ast/type/index.ts +++ b/packages/variable-engine/variable-core/src/ast/type/index.ts @@ -12,3 +12,4 @@ export { export { BaseType } from './base-type'; export { type UnionJSON } from './union'; export { CustomType, type CustomTypeJSON } from './custom-type'; +export { VarJSONSchema } from './json-schema'; diff --git a/packages/variable-engine/variable-core/src/ast/type/integer.ts b/packages/variable-engine/variable-core/src/ast/type/integer.ts index dbf4569b..2f293986 100644 --- a/packages/variable-engine/variable-core/src/ast/type/integer.ts +++ b/packages/variable-engine/variable-core/src/ast/type/integer.ts @@ -1,3 +1,4 @@ +import { VarJSONSchema } from './json-schema'; import { ASTKind } from '../types'; import { ASTNodeFlags } from '../flags'; import { BaseType } from './base-type'; @@ -10,4 +11,10 @@ export class IntegerType extends BaseType { fromJSON(): void { // noop } + + toJSONSchema(): VarJSONSchema.ISchema { + return { + type: 'integer', + }; + } } diff --git a/packages/variable-engine/variable-core/src/ast/type/json-schema.ts b/packages/variable-engine/variable-core/src/ast/type/json-schema.ts new file mode 100644 index 00000000..f1b770e2 --- /dev/null +++ b/packages/variable-engine/variable-core/src/ast/type/json-schema.ts @@ -0,0 +1,26 @@ +export namespace VarJSONSchema { + export type BasicType = 'boolean' | 'string' | 'integer' | 'number' | 'object' | 'array' | 'map'; + + export interface ISchema { + type?: T; + default?: any; + title?: string; + description?: string; + enum?: (string | number)[]; + properties?: Record>; + additionalProperties?: ISchema; + items?: ISchema; + 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; +} diff --git a/packages/variable-engine/variable-core/src/ast/type/map.ts b/packages/variable-engine/variable-core/src/ast/type/map.ts index 7829ab86..7f477fe8 100644 --- a/packages/variable-engine/variable-core/src/ast/type/map.ts +++ b/packages/variable-engine/variable-core/src/ast/type/map.ts @@ -1,4 +1,5 @@ import { parseTypeJsonOrKind } from '../utils/helpers'; +import { VarJSONSchema } from './json-schema'; import { ASTKind, ASTNodeJSON, ASTNodeJSONOrKind } from '../types'; import { BaseType } from './base-type'; @@ -74,4 +75,11 @@ export class MapType extends BaseType { valueType: this.valueType?.toJSON(), }; } + + toJSONSchema(): VarJSONSchema.ISchema { + return { + type: 'map', + additionalProperties: this.valueType?.toJSONSchema(), + }; + } } diff --git a/packages/variable-engine/variable-core/src/ast/type/number.ts b/packages/variable-engine/variable-core/src/ast/type/number.ts index 834fcadd..d85a705b 100644 --- a/packages/variable-engine/variable-core/src/ast/type/number.ts +++ b/packages/variable-engine/variable-core/src/ast/type/number.ts @@ -1,3 +1,4 @@ +import { VarJSONSchema } from './json-schema'; import { ASTKind } from '../types'; import { BaseType } from './base-type'; @@ -7,4 +8,10 @@ export class NumberType extends BaseType { fromJSON(): void { // noop } + + toJSONSchema(): VarJSONSchema.ISchema { + return { + type: 'number', + }; + } } diff --git a/packages/variable-engine/variable-core/src/ast/type/object.ts b/packages/variable-engine/variable-core/src/ast/type/object.ts index 683f2639..e9cd19be 100644 --- a/packages/variable-engine/variable-core/src/ast/type/object.ts +++ b/packages/variable-engine/variable-core/src/ast/type/object.ts @@ -1,6 +1,7 @@ import { xor } from 'lodash'; import { parseTypeJsonOrKind } from '../utils/helpers'; +import { VarJSONSchema } from './json-schema'; import { ASTNodeJSON, ASTKind, ASTNodeJSONOrKind, type GlobalEventActionType } from '../types'; import { ASTNodeFlags } from '../flags'; import { Property, type PropertyJSON } from '../declaration/property'; @@ -60,7 +61,7 @@ export class ObjectType extends BaseType { }); // 删除没有出现过的 property - removedKeys.forEach(key => { + removedKeys.forEach((key) => { const property = this.propertyTable.get(key); property?.dispose(); this.propertyTable.delete(key); @@ -79,7 +80,7 @@ export class ObjectType extends BaseType { toJSON(): ASTNodeJSON { return { 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 { const targetProperties = (targetTypeJSON as ObjectJSON).properties || []; 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; return ( isKeyStrongEqual && - targetProperties.every(targetProperty => { + targetProperties.every((targetProperty) => { const sourceProperty = this.propertyTable.get(targetProperty.key); return ( @@ -148,4 +149,14 @@ export class ObjectType extends BaseType { }) ); } + + toJSONSchema(): VarJSONSchema.ISchema { + return { + type: 'object', + properties: this.properties.reduce((acc, _property) => { + acc[_property.key] = _property.type?.toJSONSchema(); + return acc; + }, {} as Record), + }; + } } diff --git a/packages/variable-engine/variable-core/src/ast/type/string.ts b/packages/variable-engine/variable-core/src/ast/type/string.ts index 04bc38f4..4c16cf5a 100644 --- a/packages/variable-engine/variable-core/src/ast/type/string.ts +++ b/packages/variable-engine/variable-core/src/ast/type/string.ts @@ -1,3 +1,4 @@ +import { VarJSONSchema } from './json-schema'; import { ASTKind } from '../types'; import { ASTNodeFlags } from '../flags'; import { BaseType } from './base-type'; @@ -10,4 +11,10 @@ export class StringType extends BaseType { fromJSON(): void { // noop } + + toJSONSchema(): VarJSONSchema.ISchema { + return { + type: 'string', + }; + } } diff --git a/packages/variable-engine/variable-core/src/ast/types.ts b/packages/variable-engine/variable-core/src/ast/types.ts index 2e702638..3c8933d1 100644 --- a/packages/variable-engine/variable-core/src/ast/types.ts +++ b/packages/variable-engine/variable-core/src/ast/types.ts @@ -43,7 +43,7 @@ export enum ASTKind { */ KeyPathExpression = 'KeyPathExpression', // 通过路径系统访问变量上的字段 EnumerateExpression = 'EnumerateExpression', // 对指定的数据进行遍历 - ExpressionList = 'ExpressionList', // 表达式列表 + WrapArrayExpression = 'WrapArrayExpression', // Wrap with Array Type /** * 通用 AST 节点 diff --git a/packages/variable-engine/variable-core/src/react/hooks/useScopeOutputVariables.ts b/packages/variable-engine/variable-core/src/react/hooks/useScopeOutputVariables.ts new file mode 100644 index 00000000..86a311a0 --- /dev/null +++ b/packages/variable-engine/variable-core/src/react/hooks/useScopeOutputVariables.ts @@ -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('DisposeAST', refreshWhenInScopes), + variableEngine.onGlobalEvent('NewAST', refreshWhenInScopes), + variableEngine.onGlobalEvent('UpdateAST', refreshWhenInScopes), + ]); + + return () => disposable.dispose(); + }, [scopes]); + + const variables = useMemo( + () => scopes.map((scope) => scope.output.variables).flat(), + [scopes, refreshKey] + ); + + return variables; +}